|
@@ -1,20 +1,21 @@
|
|
-package com.gxzc.zen.common.util
|
|
|
|
|
|
+package com.gxzc.zen.common.util.upload
|
|
|
|
|
|
-import com.github.benmanes.caffeine.cache.Caffeine
|
|
|
|
-import com.gxzc.zen.common.dto.ZenFileMetadata
|
|
|
|
-import com.gxzc.zen.common.dto.ZenFileResponse
|
|
|
|
import com.gxzc.zen.common.exception.ZenException
|
|
import com.gxzc.zen.common.exception.ZenException
|
|
import com.gxzc.zen.common.exception.ZenExceptionEnum
|
|
import com.gxzc.zen.common.exception.ZenExceptionEnum
|
|
import com.gxzc.zen.common.properties.UploadProperties
|
|
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.apache.commons.io.FilenameUtils
|
|
import org.slf4j.LoggerFactory
|
|
import org.slf4j.LoggerFactory
|
|
-import org.springframework.cache.caffeine.CaffeineCache
|
|
|
|
import org.springframework.web.multipart.MultipartFile
|
|
import org.springframework.web.multipart.MultipartFile
|
|
import java.io.*
|
|
import java.io.*
|
|
import java.nio.file.Files
|
|
import java.nio.file.Files
|
|
import java.nio.file.Paths
|
|
import java.nio.file.Paths
|
|
-import java.util.concurrent.ConcurrentHashMap
|
|
|
|
-import java.util.concurrent.TimeUnit
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
* 上传 工具类
|
|
* 上传 工具类
|
|
@@ -33,44 +34,43 @@ object UploadUtil {
|
|
return field
|
|
return field
|
|
}
|
|
}
|
|
|
|
|
|
- private val uploadedStatusMap = ConcurrentHashMap<String, String>() // 文件上传状态缓存
|
|
|
|
- private val batchCountMap = CaffeineCache("batchCountMap", Caffeine.newBuilder().expireAfterWrite(1L, TimeUnit.HOURS).build(), true) // 文件批量上传批次文件数量记录
|
|
|
|
|
|
+ private val checkMergeLock = KeyLock<Int>()
|
|
|
|
+ private val mergeLock = KeyLock<Int>()
|
|
|
|
+ private val uploadLock = KeyLock<Int>()
|
|
|
|
|
|
private val FILE_SEPARATOR = System.getProperty("file.separator") // 适配操作系统的文件路径
|
|
private val FILE_SEPARATOR = System.getProperty("file.separator") // 适配操作系统的文件路径
|
|
|
|
|
|
- object STATUS {
|
|
|
|
- const val CHECKING = "checking" // 检查中
|
|
|
|
- const val MERGING = "merging" // 合并中
|
|
|
|
- const val UPLOADED = "uploaded" // 单文件上传完毕
|
|
|
|
- const val BATCH_UPLOADED = "batchUploaded" // 批量文件上传完毕
|
|
|
|
- const val UPLOADING = "uploading" // 上传中
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
/**
|
|
/**
|
|
* 正常上传<br>
|
|
* 正常上传<br>
|
|
* 文件夹结构/文件名
|
|
* 文件夹结构/文件名
|
|
*/
|
|
*/
|
|
- fun upload(fileMetadata: ZenFileMetadata, file: MultipartFile): ZenFileResponse {
|
|
|
|
|
|
+ fun upload(fileMetadata: ZenFileMetadata, file: MultipartFile): UploadResult {
|
|
val tmpPath = uploadProperties!!.tmpPath!!
|
|
val tmpPath = uploadProperties!!.tmpPath!!
|
|
val dataPath = uploadProperties!!.dataPath!!
|
|
val dataPath = uploadProperties!!.dataPath!!
|
|
val chunkSize = uploadProperties!!.chunkSize!!
|
|
val chunkSize = uploadProperties!!.chunkSize!!
|
|
var retFile: File? = null
|
|
var retFile: File? = null
|
|
if (validateRequest(fileMetadata, file)) {
|
|
if (validateRequest(fileMetadata, file)) {
|
|
|
|
+ val filename = fileMetadata.filename!!
|
|
|
|
+ val md5 = fileMetadata.md5!!
|
|
|
|
+ val hashedMd5 = md5.hashCode()
|
|
// 文件分片 如果分片小于chunkSize && totalChunk = 1,直接转存
|
|
// 文件分片 如果分片小于chunkSize && totalChunk = 1,直接转存
|
|
if (fileMetadata.totalChunks == 1 && fileMetadata.chunkSize!! <= chunkSize) {
|
|
if (fileMetadata.totalChunks == 1 && fileMetadata.chunkSize!! <= chunkSize) {
|
|
- val filename = fileMetadata.filename!!
|
|
|
|
- val md5 = fileMetadata.md5!!
|
|
|
|
|
|
|
|
- // 目标文件流 通过文件名来拼接
|
|
|
|
- val outputFullFilename = getFullDestFilename(dataPath, filename, md5)
|
|
|
|
- val outputPath = Paths.get(FilenameUtils.getFullPath(outputFullFilename))
|
|
|
|
- // 不存在则创建文件夹
|
|
|
|
- if (Files.notExists(outputPath)) {
|
|
|
|
- Files.createDirectories(outputPath)
|
|
|
|
|
|
+ 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)
|
|
}
|
|
}
|
|
- retFile = File(outputFullFilename)
|
|
|
|
- file.transferTo(retFile)
|
|
|
|
- retFile.setLastModified(fileMetadata.lastModified!!)
|
|
|
|
} else {
|
|
} else {
|
|
val chunkFilename = getChunkFilename(tmpPath, fileMetadata.chunkNumber, fileMetadata.md5)
|
|
val chunkFilename = getChunkFilename(tmpPath, fileMetadata.chunkNumber, fileMetadata.md5)
|
|
val directory = Paths.get(tmpPath)
|
|
val directory = Paths.get(tmpPath)
|
|
@@ -78,59 +78,45 @@ object UploadUtil {
|
|
Files.createDirectories(directory)
|
|
Files.createDirectories(directory)
|
|
}
|
|
}
|
|
file.transferTo(File(chunkFilename))
|
|
file.transferTo(File(chunkFilename))
|
|
- // 检查分块完整性
|
|
|
|
- if (checkChunks(tmpPath, fileMetadata)) {
|
|
|
|
- // 合并
|
|
|
|
- retFile = try {
|
|
|
|
- mergeChunks(tmpPath, dataPath, fileMetadata)
|
|
|
|
- } catch (e: Throwable) {
|
|
|
|
- logger.error("merge file chunks exception, cause ", e)
|
|
|
|
- uploadedStatusMap.remove(fileMetadata.md5!!)
|
|
|
|
- null
|
|
|
|
|
|
+
|
|
|
|
+ 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 {
|
|
} else {
|
|
throw ZenException(ZenExceptionEnum.FILE_METADATA_VALIDATE_ERROR)
|
|
throw ZenException(ZenExceptionEnum.FILE_METADATA_VALIDATE_ERROR)
|
|
}
|
|
}
|
|
- var status: String = STATUS.UPLOADING
|
|
|
|
|
|
+ var status: String = UploadStatus.UPLOADING
|
|
if (retFile != null) {
|
|
if (retFile != null) {
|
|
- status = setBatch(fileMetadata)
|
|
|
|
|
|
+ status = UploadStatus.UPLOADED
|
|
}
|
|
}
|
|
- return ZenFileResponse().apply {
|
|
|
|
|
|
+ return UploadResult().apply {
|
|
this.status = status
|
|
this.status = status
|
|
this.file = retFile
|
|
this.file = retFile
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- /**
|
|
|
|
- * 设置批次文件数量信息 ++
|
|
|
|
- */
|
|
|
|
- private fun setBatch(fileMetadata: ZenFileMetadata): String {
|
|
|
|
- val batchId = fileMetadata.batchId!!
|
|
|
|
- val totalNumber = fileMetadata.totalNumber!!
|
|
|
|
- synchronized(batchCountMap) {
|
|
|
|
- var count = batchCountMap[batchId]?.get() as? Int ?: 0
|
|
|
|
- logger.debug("batchId: $batchId, count: ${count + 1}, totalNumber: $totalNumber")
|
|
|
|
- return if (++count >= totalNumber) {
|
|
|
|
- batchCountMap.evict(batchId)
|
|
|
|
- STATUS.BATCH_UPLOADED
|
|
|
|
- } else {
|
|
|
|
- batchCountMap.put(batchId, count)
|
|
|
|
- STATUS.UPLOADED
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
/**
|
|
/**
|
|
* 检测文件是否存在 实现文件秒传
|
|
* 检测文件是否存在 实现文件秒传
|
|
* 真实文件已存在,秒传
|
|
* 真实文件已存在,秒传
|
|
* 真实文件不存在,检查分片上传情况
|
|
* 真实文件不存在,检查分片上传情况
|
|
*/
|
|
*/
|
|
- fun checkUpload(fileMetadata: ZenFileMetadata): ZenFileResponse {
|
|
|
|
|
|
+ fun checkUpload(fileMetadata: ZenFileMetadata): CheckResult {
|
|
val tmpPath = uploadProperties!!.tmpPath!!
|
|
val tmpPath = uploadProperties!!.tmpPath!!
|
|
val dataPath = uploadProperties!!.dataPath!!
|
|
val dataPath = uploadProperties!!.dataPath!!
|
|
- val ret = ZenFileResponse().apply { status = STATUS.CHECKING }
|
|
|
|
|
|
+ val ret = CheckResult()
|
|
if (validateRequest(fileMetadata, null)) {
|
|
if (validateRequest(fileMetadata, null)) {
|
|
if (fileExists(fileMetadata)) {
|
|
if (fileExists(fileMetadata)) {
|
|
ret.uploadedChunks = mutableListOf()
|
|
ret.uploadedChunks = mutableListOf()
|
|
@@ -138,8 +124,10 @@ object UploadUtil {
|
|
ret.uploadedChunks!!.add(i)
|
|
ret.uploadedChunks!!.add(i)
|
|
}
|
|
}
|
|
ret.file = File(getFullDestFilename(dataPath, fileMetadata.filename!!, fileMetadata.md5!!))
|
|
ret.file = File(getFullDestFilename(dataPath, fileMetadata.filename!!, fileMetadata.md5!!))
|
|
|
|
+ ret.checkStatus = CheckStatus.FILE_EXISTS
|
|
} else {
|
|
} else {
|
|
// 检查分片存在情况
|
|
// 检查分片存在情况
|
|
|
|
+ ret.checkStatus = CheckStatus.NO_FILE_FRAG_CHUNK
|
|
for (i in 1..fileMetadata.totalChunks!!) {
|
|
for (i in 1..fileMetadata.totalChunks!!) {
|
|
if (chunkExists(i, fileMetadata.md5!!)) {
|
|
if (chunkExists(i, fileMetadata.md5!!)) {
|
|
if (ret.uploadedChunks == null) {
|
|
if (ret.uploadedChunks == null) {
|
|
@@ -150,24 +138,24 @@ object UploadUtil {
|
|
}
|
|
}
|
|
if (ret.uploadedChunks != null && ret.uploadedChunks!!.size == fileMetadata.totalChunks) {
|
|
if (ret.uploadedChunks != null && ret.uploadedChunks!!.size == fileMetadata.totalChunks) {
|
|
// 合并
|
|
// 合并
|
|
- ret.file = try {
|
|
|
|
- mergeChunks(tmpPath, dataPath, fileMetadata)
|
|
|
|
- } catch (e: Throwable) {
|
|
|
|
- logger.error("merge file chunks exception, cause ", e)
|
|
|
|
- uploadedStatusMap.remove(fileMetadata.md5!!)
|
|
|
|
- null
|
|
|
|
|
|
+ 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)
|
|
}
|
|
}
|
|
-
|
|
|
|
-// // 所有分块传完 移除其中一个分块,再传一次而后 merge
|
|
|
|
-// ret.uploadedChunks!!.removeAt(0)
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
} else {
|
|
throw ZenException(ZenExceptionEnum.FILE_METADATA_VALIDATE_ERROR)
|
|
throw ZenException(ZenExceptionEnum.FILE_METADATA_VALIDATE_ERROR)
|
|
}
|
|
}
|
|
- if (ret.file != null) {
|
|
|
|
- ret.status = setBatch(fileMetadata)
|
|
|
|
- }
|
|
|
|
return ret
|
|
return ret
|
|
}
|
|
}
|
|
|
|
|
|
@@ -215,15 +203,6 @@ object UploadUtil {
|
|
val totalChunks = fileMetadata.totalChunks!!
|
|
val totalChunks = fileMetadata.totalChunks!!
|
|
val md5 = fileMetadata.md5!!
|
|
val md5 = fileMetadata.md5!!
|
|
|
|
|
|
- if (uploadedStatusMap[md5] == STATUS.MERGING) {
|
|
|
|
- return null
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- uploadedStatusMap[md5] = STATUS.MERGING
|
|
|
|
-
|
|
|
|
- val t = System.currentTimeMillis()
|
|
|
|
- logger.debug("start merging chunks for [$filename], now: $t")
|
|
|
|
-
|
|
|
|
// 目标文件流 通过文件名来拼接
|
|
// 目标文件流 通过文件名来拼接
|
|
val outputFullFilename = getFullDestFilename(destRootPath, filename, md5)
|
|
val outputFullFilename = getFullDestFilename(destRootPath, filename, md5)
|
|
val outputPath = Paths.get(FilenameUtils.getFullPath(outputFullFilename))
|
|
val outputPath = Paths.get(FilenameUtils.getFullPath(outputFullFilename))
|
|
@@ -253,13 +232,9 @@ object UploadUtil {
|
|
destOutputStream.flush()
|
|
destOutputStream.flush()
|
|
destOutputStream.close()
|
|
destOutputStream.close()
|
|
|
|
|
|
-
|
|
|
|
// 修改文件 修改时间
|
|
// 修改文件 修改时间
|
|
outputFile.setLastModified(fileMetadata.lastModified!!)
|
|
outputFile.setLastModified(fileMetadata.lastModified!!)
|
|
|
|
|
|
- uploadedStatusMap.remove(md5)
|
|
|
|
-
|
|
|
|
- logger.debug("merging successful, cost: ${System.currentTimeMillis() - t} ms.")
|
|
|
|
return outputFile
|
|
return outputFile
|
|
}
|
|
}
|
|
|
|
|