FileUploadService.java 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. package cn.reghao.dfs.store.service;
  2. import cn.reghao.dfs.store.model.dto.*;
  3. import cn.reghao.dfs.store.util.redis.ds.RedisSet;
  4. import cn.reghao.jutil.jdk.security.DigestUtil;
  5. import cn.reghao.dfs.store.config.PathUrl;
  6. import cn.reghao.dfs.store.db.repository.FileRepository;
  7. import cn.reghao.dfs.store.model.dto.*;
  8. import cn.reghao.dfs.store.model.po.FileInfo;
  9. import cn.reghao.dfs.store.util.media.ImageOps;
  10. import lombok.extern.slf4j.Slf4j;
  11. import org.springframework.stereotype.Service;
  12. import org.springframework.transaction.annotation.Transactional;
  13. import org.springframework.web.multipart.MultipartFile;
  14. import java.io.*;
  15. /**
  16. * @author reghao
  17. * @date 2021-08-04 17:29:47
  18. */
  19. @Slf4j
  20. @Service
  21. public class FileUploadService {
  22. // 10MiB
  23. private final static long PART_SIZE = 1024*1024*10;
  24. private final FileUrlService fileUrlService;
  25. private final FileStoreService fileStoreService;
  26. private final FileTypeService fileTypeService;
  27. private final FileRepository fileRepository;
  28. private final RedisSet redisSet;
  29. public FileUploadService(FileUrlService fileUrlService, FileStoreService fileStoreService,
  30. FileTypeService fileTypeService, FileRepository fileRepository,
  31. RedisSet redisSet) {
  32. this.fileUrlService = fileUrlService;
  33. this.fileStoreService = fileStoreService;
  34. this.fileTypeService = fileTypeService;
  35. this.fileRepository = fileRepository;
  36. this.redisSet = redisSet;
  37. }
  38. public synchronized UploadPrepareRet prepareUpload(UploadPrepare uploadPrepare) {
  39. // TODO 避免用户通过 sha256sum 查询文件是否存在, 这是一个不安全的行为, 用户可能会伪造 sha256sum 来获取不属于他权限的文件
  40. // TODO 解决办法是检查用户已上传文件的 sha256sum, 这样可确保他拥有 sha256sum 所属文件的权限, 但这会带来服务器带宽和磁盘 IO 的性能开销
  41. String sha256sum = uploadPrepare.getFileSha256sum();
  42. FileInfo fileInfo = fileRepository.getFileInfo(sha256sum);
  43. if (fileInfo == null) {
  44. String uploadId = fileRepository.createFileUploadId(uploadPrepare);
  45. return new UploadPrepareRet(uploadId, PART_SIZE, false);
  46. }
  47. String uploadId = fileRepository.getOrCreateUploadId(fileInfo.getFileId());
  48. // uploaded 为 true 则 exist 也应该为 true, 但这里统一将 exist 设置为 false, 表示客户端总是要传输文件流
  49. boolean uploaded = fileInfo.getUploaded();
  50. return new UploadPrepareRet(uploadId, PART_SIZE, false);
  51. }
  52. /**
  53. * 将文件存放到 FileStore,并在数据库中记录文件相关信息
  54. *
  55. * @param
  56. * @return
  57. * @date 2021-12-08 下午3:30
  58. */
  59. public void putFile(UploadFile uploadFile) throws Exception {
  60. String uploadId = uploadFile.getUploadId();
  61. FileInfo fileInfo = fileRepository.getFileInfoByUploadId(uploadId);
  62. if (!fileInfo.getUploaded()) {
  63. put(uploadFile.getFile(), fileInfo);
  64. }
  65. }
  66. @Transactional(rollbackFor = Exception.class)
  67. public synchronized String put(MultipartFile multipartFile, FileInfo fileInfo) throws Exception {
  68. byte[] bytes = multipartFile.getBytes();
  69. String sha256sum = DigestUtil.sha256sum(bytes);
  70. if (!sha256sum.equals(fileInfo.getSha256sum())) {
  71. throw new Exception("uploadId 和 sha256sum 不匹配!");
  72. }
  73. String contentType = multipartFile.getContentType();
  74. String fileId = fileInfo.getFileId();
  75. String suffix = fileInfo.getSuffix();
  76. PathUrl pathUrl;
  77. if (contentType != null && contentType.startsWith("image")) {
  78. ImageOps.Size size1 = ImageOps.info(new ByteArrayInputStream(bytes));
  79. pathUrl = fileUrlService.getImagePathAndUrl(sha256sum, fileId, suffix, size1.getWidth(), size1.getHeight());
  80. } else {
  81. pathUrl = fileUrlService.genPathAndUrl(sha256sum, fileId, suffix);
  82. }
  83. FileContentType fileType = fileTypeService.getFileType(contentType, pathUrl.getFilePath());
  84. fileStoreService.saveFile(pathUrl.getFilePath(), bytes);
  85. fileRepository.saveFile(fileId, fileType, pathUrl);
  86. return pathUrl.getUrl();
  87. }
  88. /**
  89. * 处理通过 HTTP 请求提交的分片文件
  90. *
  91. * @param
  92. * @return
  93. * @date 2021-12-08 下午3:31
  94. */
  95. @Transactional(rollbackFor = Exception.class)
  96. public synchronized UploadFilePartRet putFilePart(UploadFilePart uploadFilePart) throws Exception {
  97. int partIndex = uploadFilePart.getSplitIndex();
  98. int partNum = uploadFilePart.getSplitNum();
  99. MultipartFile multipartFile = uploadFilePart.getFile();
  100. String uploadId = uploadFilePart.getUploadId();
  101. FileInfo fileInfo = fileRepository.getFileInfoByFileId(uploadId);
  102. // 没有检查文件的 sha256sum, 存在安全隐患
  103. if (fileInfo.getUploaded()) {
  104. return new UploadFilePartRet(uploadId, true);
  105. }
  106. String fileId = fileInfo.getFileId();
  107. String sha256sum = fileInfo.getSha256sum();
  108. String suffix = fileInfo.getSuffix();
  109. PathUrl pathUrl = fileUrlService.genPathAndUrl(sha256sum, fileId, suffix);
  110. String key = RedisKey.filePartKey(fileId);
  111. if (!redisSet.sismember(key, partIndex)) {
  112. redisSet.sadd(key, partIndex);
  113. long size = fileInfo.getSize();
  114. File file = fileStoreService.createFile(pathUrl.getFilePath(), size);
  115. long pos = partIndex * PART_SIZE;
  116. fileStoreService.writeToFile(multipartFile.getInputStream(), file, pos);
  117. }
  118. long len = redisSet.scard(key);
  119. if (len != partNum) {
  120. return new UploadFilePartRet(fileId, false);
  121. } else {
  122. String path = pathUrl.getFilePath();
  123. FileInputStream fis = new FileInputStream(path);
  124. String sha256sumMerged = DigestUtil.sha256sum(fis);
  125. if (!sha256sum.equals(sha256sumMerged)) {
  126. throw new Exception("uploadId 和 sha256sum 不匹配!");
  127. }
  128. FileContentType fileType = fileTypeService.getFileType(fileInfo.getContentType(), pathUrl.getFilePath());
  129. fileRepository.saveFile(fileId, fileType, pathUrl);
  130. redisSet.spop(key, len);
  131. return new UploadFilePartRet(uploadId, true);
  132. }
  133. }
  134. }