|
@@ -1,316 +0,0 @@
|
|
|
-package com.gxzc.zen.common.util.upload
|
|
|
-
|
|
|
-import com.gxzc.zen.common.exception.ZenException
|
|
|
-import com.gxzc.zen.common.exception.ZenExceptionEnum
|
|
|
-import com.gxzc.zen.common.properties.FSProperties
|
|
|
-import com.gxzc.zen.common.properties.UploadProperties
|
|
|
-import com.gxzc.zen.common.util.FileUtil
|
|
|
-import com.gxzc.zen.common.util.KeyLock
|
|
|
-import com.gxzc.zen.common.util.SpringContextHolder
|
|
|
-import com.gxzc.zen.common.util.upload.constants.CheckStatus
|
|
|
-import com.gxzc.zen.common.util.upload.constants.UploadStatus
|
|
|
-import com.gxzc.zen.common.util.upload.result.CheckResult
|
|
|
-import com.gxzc.zen.common.util.upload.result.UploadResult
|
|
|
-import org.apache.commons.io.FilenameUtils
|
|
|
-import org.slf4j.LoggerFactory
|
|
|
-import org.springframework.web.multipart.MultipartFile
|
|
|
-import java.io.*
|
|
|
-import java.nio.file.Files
|
|
|
-import java.nio.file.Paths
|
|
|
-
|
|
|
-/**
|
|
|
- * 上传 工具类
|
|
|
- * @author NorthLan
|
|
|
- * @date 2018/5/19
|
|
|
- * @url https://noahlan.com
|
|
|
- */
|
|
|
-object UploadUtil {
|
|
|
- private val logger = LoggerFactory.getLogger(UploadUtil::class.java)
|
|
|
-
|
|
|
- private val uploadProperties: UploadProperties by lazy { SpringContextHolder.getBean(UploadProperties::class.java)!! }
|
|
|
- private val fsProperties: FSProperties by lazy { SpringContextHolder.getBean(FSProperties::class.java)!! }
|
|
|
-
|
|
|
- private val checkMergeLock = KeyLock<Int>()
|
|
|
- private val mergeLock = KeyLock<Int>()
|
|
|
- private val uploadLock = KeyLock<Int>()
|
|
|
-
|
|
|
- private val FILE_SEPARATOR = System.getProperty("file.separator") // 适配操作系统的文件路径
|
|
|
-
|
|
|
- /**
|
|
|
- * 正常上传<br>
|
|
|
- * 文件夹结构/文件名
|
|
|
- */
|
|
|
- fun upload(fileMetadata: ZenFileMetadata, file: MultipartFile): UploadResult {
|
|
|
- val tmpPath = uploadProperties.tmpPath
|
|
|
- val dataPath = uploadProperties.dataPath
|
|
|
- val chunkSize = fsProperties.chunkSize
|
|
|
- var retFile: File? = null
|
|
|
- if (validateRequest(fileMetadata, file)) {
|
|
|
- val filename = fileMetadata.filename!!
|
|
|
- val md5 = fileMetadata.md5!!
|
|
|
- val hashedMd5 = md5.hashCode()
|
|
|
- // 文件分片 如果分片小于chunkSize && totalChunk = 1,直接转存
|
|
|
- if (fileMetadata.totalChunks == 1 && fileMetadata.chunkSize!! <= chunkSize) {
|
|
|
-
|
|
|
- uploadLock.lock(hashedMd5)
|
|
|
- try {
|
|
|
- // 目标文件流 通过文件名来拼接
|
|
|
- val outputFullFilename = getFullDestFilename(dataPath, filename, md5)
|
|
|
- val outputPath = Paths.get(FilenameUtils.getFullPath(outputFullFilename))
|
|
|
- // 不存在则创建文件夹
|
|
|
- if (Files.notExists(outputPath)) {
|
|
|
- Files.createDirectories(outputPath)
|
|
|
- }
|
|
|
- retFile = File(outputFullFilename)
|
|
|
- file.transferTo(retFile)
|
|
|
- retFile.setLastModified(fileMetadata.lastModified!!)
|
|
|
- } finally {
|
|
|
- uploadLock.unlock(hashedMd5)
|
|
|
- }
|
|
|
- } else {
|
|
|
- val chunkFilename = getChunkFilename(tmpPath, fileMetadata.chunkNumber, fileMetadata.md5)
|
|
|
- val directory = Paths.get(tmpPath)
|
|
|
- if (Files.notExists(directory)) {
|
|
|
- Files.createDirectories(directory)
|
|
|
- }
|
|
|
- file.transferTo(File(chunkFilename))
|
|
|
-
|
|
|
- mergeLock.lock(hashedMd5)
|
|
|
- try {
|
|
|
- // 检查分块完整性
|
|
|
- if (checkChunks(tmpPath, fileMetadata)) {
|
|
|
- // 合并
|
|
|
- retFile = try {
|
|
|
- mergeChunks(tmpPath, dataPath, fileMetadata)
|
|
|
- } catch (e: Throwable) {
|
|
|
- logger.error("merge file chunks exception, cause ", e)
|
|
|
- null
|
|
|
- }
|
|
|
- }
|
|
|
- } finally {
|
|
|
- mergeLock.unlock(hashedMd5)
|
|
|
- }
|
|
|
- }
|
|
|
- } else {
|
|
|
- throw ZenException(ZenExceptionEnum.FILE_METADATA_VALIDATE_ERROR)
|
|
|
- }
|
|
|
- var status: String = UploadStatus.UPLOADING
|
|
|
- if (retFile != null) {
|
|
|
- status = UploadStatus.UPLOADED
|
|
|
- }
|
|
|
- return UploadResult().apply {
|
|
|
- this.status = status
|
|
|
- this.file = retFile
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 检测文件是否存在 实现文件秒传
|
|
|
- * 真实文件已存在,秒传
|
|
|
- * 真实文件不存在,检查分片上传情况
|
|
|
- */
|
|
|
- fun checkUpload(fileMetadata: ZenFileMetadata): CheckResult {
|
|
|
- val tmpPath = uploadProperties.tmpPath
|
|
|
- val dataPath = uploadProperties.dataPath
|
|
|
- val ret = CheckResult()
|
|
|
- if (validateRequest(fileMetadata, null)) {
|
|
|
- if (fileExists(fileMetadata)) {
|
|
|
- ret.uploadedChunks = mutableListOf()
|
|
|
- for (i in 1..fileMetadata.totalChunks!!) {
|
|
|
- ret.uploadedChunks!!.add(i)
|
|
|
- }
|
|
|
- ret.file = File(getFullDestFilename(dataPath, fileMetadata.filename!!, fileMetadata.md5!!))
|
|
|
- ret.checkStatus = CheckStatus.FILE_EXISTS
|
|
|
- } else {
|
|
|
- // 检查分片存在情况
|
|
|
- ret.checkStatus = CheckStatus.NO_FILE_FRAG_CHUNK
|
|
|
- for (i in 1..fileMetadata.totalChunks!!) {
|
|
|
- if (chunkExists(i, fileMetadata.md5!!)) {
|
|
|
- if (ret.uploadedChunks == null) {
|
|
|
- ret.uploadedChunks = mutableListOf()
|
|
|
- }
|
|
|
- ret.uploadedChunks!!.add(i)
|
|
|
- }
|
|
|
- }
|
|
|
- if (ret.uploadedChunks != null && ret.uploadedChunks!!.size == fileMetadata.totalChunks) {
|
|
|
- // 合并
|
|
|
- ret.checkStatus = CheckStatus.NO_FILE_FULL_CHUNK
|
|
|
- val hashedMd5 = fileMetadata.md5!!.hashCode()
|
|
|
- checkMergeLock.lock(hashedMd5)
|
|
|
- try {
|
|
|
- ret.file = try {
|
|
|
- mergeChunks(tmpPath, dataPath, fileMetadata)
|
|
|
- } catch (e: Throwable) {
|
|
|
- logger.error("merge file chunks exception, cause ", e)
|
|
|
- null
|
|
|
- }
|
|
|
- } finally {
|
|
|
- checkMergeLock.unlock(hashedMd5)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- } else {
|
|
|
- throw ZenException(ZenExceptionEnum.FILE_METADATA_VALIDATE_ERROR)
|
|
|
- }
|
|
|
- return ret
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 检查文件是否存在 (真实文件)
|
|
|
- */
|
|
|
- private fun fileExists(fileMetadata: ZenFileMetadata): Boolean {
|
|
|
- val dataPath = uploadProperties.dataPath
|
|
|
- // 获取filename 真实文件
|
|
|
- val filename = fileMetadata.filename!!
|
|
|
- val md5 = fileMetadata.md5!!
|
|
|
- val realFilename = getFullDestFilename(dataPath, filename, md5)
|
|
|
- return Files.exists(Paths.get(realFilename)) && FileUtil.md5HeadTail(realFilename, fsProperties.chunkSize.toInt()) == md5 // # 防篡改
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 检查分片是否存在 (tmp目录)
|
|
|
- */
|
|
|
- private fun chunkExists(chunkNumber: Int?, md5: String): Boolean {
|
|
|
- val tmpPath = uploadProperties.tmpPath
|
|
|
- return Files.exists(Paths.get(getChunkFilename(tmpPath, chunkNumber, md5)))
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 检查分块是否上传完毕
|
|
|
- */
|
|
|
- private fun checkChunks(tmpPath: String, fileMetadata: ZenFileMetadata): Boolean {
|
|
|
- val totalChunks = fileMetadata.totalChunks!!
|
|
|
- val md5 = fileMetadata.md5!!
|
|
|
- for (i in 1..totalChunks) {
|
|
|
- if (Files.notExists(Paths.get(getChunkFilename(tmpPath, i, md5)))) {
|
|
|
- return false
|
|
|
- }
|
|
|
- }
|
|
|
- return true
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 合并所有分块
|
|
|
- * 支持 文件夹合并
|
|
|
- */
|
|
|
- private fun mergeChunks(sourceRootPath: String, destRootPath: String, fileMetadata: ZenFileMetadata): File? {
|
|
|
- // 源文件的文件夹信息 然后组合拼接
|
|
|
- val filename = fileMetadata.filename!!
|
|
|
- val totalChunks = fileMetadata.totalChunks!!
|
|
|
- val md5 = fileMetadata.md5!!
|
|
|
-
|
|
|
- // 目标文件流 通过文件名来拼接
|
|
|
- val outputFullFilename = getFullDestFilename(destRootPath, filename, md5)
|
|
|
- val outputPath = Paths.get(FilenameUtils.getFullPath(outputFullFilename))
|
|
|
- // 不存在则创建文件夹
|
|
|
- if (Files.notExists(outputPath)) {
|
|
|
- Files.createDirectories(outputPath)
|
|
|
- }
|
|
|
- val outputFile = File(outputFullFilename)
|
|
|
- val destOutputStream = BufferedOutputStream(FileOutputStream(outputFile))
|
|
|
-
|
|
|
- val buffer = ByteArray(4096)
|
|
|
- var readBytesLength: Int
|
|
|
- for (i: Int in 1..totalChunks) {
|
|
|
- val sourceFile = File(getChunkFilename(sourceRootPath, i, md5))
|
|
|
- val sourceInputStream = BufferedInputStream(FileInputStream(sourceFile))
|
|
|
-
|
|
|
- readBytesLength = sourceInputStream.read(buffer)
|
|
|
- while (readBytesLength != -1) {
|
|
|
- destOutputStream.write(buffer, 0, readBytesLength)
|
|
|
- readBytesLength = sourceInputStream.read(buffer)
|
|
|
- }
|
|
|
- sourceInputStream.close()
|
|
|
- // 删除分片
|
|
|
- sourceFile.delete()
|
|
|
- }
|
|
|
- //
|
|
|
- destOutputStream.flush()
|
|
|
- destOutputStream.close()
|
|
|
-
|
|
|
- // 修改文件 修改时间
|
|
|
- outputFile.setLastModified(fileMetadata.lastModified!!)
|
|
|
-
|
|
|
- return outputFile
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成 分片文件名
|
|
|
- */
|
|
|
- private fun getChunkFilename(path: String, chunkNumber: Int?, identifier: String?): String {
|
|
|
- return "$path/upload-$identifier.$chunkNumber"
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成 输出文件名
|
|
|
- */
|
|
|
- private fun getDestFileName(filename: String, md5: String): String {
|
|
|
- val ext = FilenameUtils.getExtension(filename)
|
|
|
- return if (!ext.isNullOrEmpty()) {
|
|
|
- "$md5.$ext"
|
|
|
- } else {
|
|
|
- md5
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取输出完整文件名
|
|
|
- */
|
|
|
- private fun getFullDestFilename(dataPath: String, filename: String, md5: String): String {
|
|
|
- return FilenameUtils.normalize("$dataPath$FILE_SEPARATOR${getDestFileName(filename, md5)}")
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * validate request multipart chunks
|
|
|
- */
|
|
|
- private fun validateRequest(fileMetadata: ZenFileMetadata, file: MultipartFile?): Boolean {
|
|
|
- val md5 = fileMetadata.md5
|
|
|
- val chunkNumber = fileMetadata.chunkNumber
|
|
|
- val chunkSize = fileMetadata.chunkSize
|
|
|
- val totalSize = fileMetadata.totalSize
|
|
|
- val filename = fileMetadata.filename
|
|
|
-
|
|
|
- if (chunkNumber == null || chunkNumber <= 0 ||
|
|
|
- chunkSize == null || chunkSize <= 0 ||
|
|
|
- totalSize == null || totalSize <= 0 ||
|
|
|
- md5 == null || md5.isEmpty() ||
|
|
|
- filename == null || filename.isEmpty()) {
|
|
|
- return false
|
|
|
- }
|
|
|
-
|
|
|
- val numberOfChunks = Math.max(Math.floor(totalSize / (chunkSize * 1.0)), 1.0).toInt()
|
|
|
- if (chunkNumber > numberOfChunks) {
|
|
|
- return false
|
|
|
- }
|
|
|
-
|
|
|
- // is the file too large?
|
|
|
- fsProperties.let {
|
|
|
- val maxFileSize = it.maxFileSize
|
|
|
- if (maxFileSize > 0) {
|
|
|
- if (totalSize > maxFileSize) {
|
|
|
- logger.error("filesize limit: [${maxFileSize / 1024 / 1024} MB], now [${totalSize / 1024 / 1024} MB]")
|
|
|
- return false
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (file != null) {
|
|
|
- // The chunk in the POST request isn't the correct size
|
|
|
- if (chunkNumber < numberOfChunks && file.size != chunkSize) {
|
|
|
- logger.error("The chunk in the POST request isn't the correct size")
|
|
|
- return false
|
|
|
- }
|
|
|
- // The chunks in the POST is the last one, and the fil is not the correct size
|
|
|
- if (numberOfChunks > 1 && chunkNumber == numberOfChunks && file.size != (totalSize % chunkSize) + chunkSize) {
|
|
|
- logger.error("The chunks in the POST is the last one, and the fil is not the correct size")
|
|
|
- return false
|
|
|
- }
|
|
|
- // The file is only a single chunk, and the data size does not fit
|
|
|
- if (numberOfChunks == 1 && file.size != totalSize) {
|
|
|
- logger.error("The file is only a single chunk, and the data size does not fit")
|
|
|
- return false
|
|
|
- }
|
|
|
- }
|
|
|
- return true
|
|
|
- }
|
|
|
-}
|