当前位置:   article > 正文

SpringBoot 分片上传、断点续传、秒传、直传Minio_minio 前端直传

minio 前端直传

        最近在学习,在SpringBoot上进行分片上传、断点续传、直接上传到Minio服务器上,中间也遇到的不少坑、自定义minio继承MinioClient来实现分片上传、比较适合初学者。

一、大致的流程如下:

  1. 前端获取文件MD5,发送至后台判断是否有该文件,有则直接转存;
  2. 前端调用初始化接口,后端调用 minio 初始化,返回分片上传地址和 uploadId;
  3. 前端上传分片文件;
  4. 上传完成后,前端发送请求至后台服务,后台服务调用 minio 合并文件;

前端代码地址:https://github.com/lanweihong/vue-minio-upload-sample

流程图如下:

二、实现过程

2.1、获取 minio 依赖

我这里使用的Gradle管理的项目:

// minio

implementation 'io.minio:minio:8.2.1'

如果用maven管理项目可以访问官网:

地址:https://mvnrepository.com/

2.2、配置minio yml文件

application.yml配置如下:

  1. minio:
  2. endpoint: ${MINIO_ENDPOINT:https://play.minio.io:9000}
  3. access: ${MINIO_ASSESSKEY:Q3AM3UQ867SPQQA43P2F}
  4. secre: ${MINIO_SECRETKEY:zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG}
  5. bucket: ${MINIO_BUCKET:tuinetest}

2.3、获取minio默认属性

  1. @Component
  2. @ConfigurationProperties(prefix = "minio")
  3. public class MinioProperties {
  4. /**
  5. * 文件服务地址
  6. */
  7. private String endpoint;
  8. /**
  9. * 文件服务器账号
  10. */
  11. private String access;
  12. /**
  13. * 文件服务认证信息
  14. */
  15. private String secre;
  16. /**
  17. * 桶存储名称
  18. */
  19. private String bucket;
  20. public String getEndpoint() {
  21. return endpoint;
  22. }
  23. public void setEndpoint(String endpoint) {
  24. this.endpoint = endpoint;
  25. }
  26. public String getAccess() {
  27. return accesskey;
  28. }
  29. public void setAccess(String access) {
  30. this.access = access;
  31. }
  32. public String getSecre() {
  33. return secre;
  34. }
  35. public void setSecret(String secre) {
  36. this.secre = secre;
  37. }
  38. public String getBucket() {
  39. return bucket;
  40. }
  41. public void setBucket(String bucket) {
  42. this.bucket = bucket;
  43. }
  44. @Override
  45. public String toString() {
  46. return "MinioProperties{" +
  47. "endpoint='" + endpoint + '\'' +
  48. ", access='" + access + '\'' +
  49. ", secre='" + secre + '\'' +
  50. ", bucket='" + bucket + '\'' +
  51. '}';
  52. }
  53. }

2.4、自定义Minio继承 MinioClient 

2.4.1、如果我们不继承MinioClient 访问 listParts 查询分片信息是会报错的、通过继承 MinioClient 来去实现这个方法就可以了。

2.4.2、方法上参数写着注解说明、这个参数是用到了、没写则是没有用到,直接传NULL即可。

  1. @Component
  2. public class CustomMinioClient extends MinioClient {
  3. public CustomMinioClient(MinioClient client) {
  4. super(client);
  5. }
  6. /**
  7. * 初始化分片上传、获取 uploadId
  8. *
  9. * @param bucket String 存储桶名称
  10. * @param region String
  11. * @param object String 文件名称
  12. * @param headers Multimap<String, String> 请求头
  13. * @param extraQueryParams Multimap<String, String>
  14. * @return String
  15. */
  16. public String initMultiPartUpload(String bucket, String region, String object, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException {
  17. CreateMultipartUploadResponse response = this.createMultipartUpload(bucket, region, object, headers, extraQueryParams);
  18. return response.result().uploadId();
  19. }
  20. /**
  21. * 合并分片
  22. *
  23. * @param bucketName String 桶名称
  24. * @param region String
  25. * @param objectName String 文件名称
  26. * @param uploadId String 上传的 uploadId
  27. * @param parts Part[] 分片集合
  28. * @param extraHeaders Multimap<String, String>
  29. * @param extraQueryParams Multimap<String, String>
  30. * @return ObjectWriteResponse
  31. */
  32. public ObjectWriteResponse mergeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException {
  33. return this.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
  34. }
  35. /**
  36. * 查询当前上传后的分片信息
  37. *
  38. * @param bucketName String 桶名称
  39. * @param region String
  40. * @param objectName String 文件名称
  41. * @param maxParts Integer 分片数量
  42. * @param partNumberMarker Integer 分片起始值
  43. * @param uploadId String 上传的 uploadId
  44. * @param extraHeaders Multimap<String, String>
  45. * @param extraQueryParams Multimap<String, String>
  46. * @return ListPartsResponse
  47. */
  48. public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
  49. return this.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
  50. }
  51. }

2.5、链接minio、上传、合并分片的核心方法

Mono<Map<String, Object>> 返回的格式和消息编码可以看、2.5.1、

  1. public class MinIoUtils {
  2. // 这两个引用 上面都有写
  3. private final MinioProperties minioProperties;
  4. private CustomMinioClient customMinioClient;
  5. public MinIoUtils(MinioProperties minioProperties) {
  6. this.minioProperties = minioProperties;
  7. }
  8. @PostConstruct
  9. public void init() {
  10. MinioClient minioClient = MinioClient.builder()
  11. .endpoint(minioProperties.getEndpoint())
  12. .credentials(minioProperties.getAccesskey(), minioProperties.getSecretkey())
  13. .build();
  14. customMinioClient = new CustomMinioClient(minioClient);
  15. }
  16. /**
  17. * 单文件签名上传
  18. *
  19. * @param objectName 文件全路径名称
  20. * @param bucketName 桶名称
  21. * @return /
  22. */
  23. public Mono<Map<String, Object>> getUploadObjectUrl(String objectName, String bucketName) {
  24. try {
  25. logger.info("tip message: 通过 <{}-{}> 开始单文件上传<minio>", objectName, bucketName);
  26. Map<String, Object> resMap = new HashMap<>();
  27. String url = customMinioClient.getPresignedObjectUrl(
  28. GetPresignedObjectUrlArgs.builder()
  29. .method(Method.PUT)
  30. .bucket(bucketName)
  31. .object(objectName)
  32. .expiry(1, TimeUnit.DAYS)
  33. .build());
  34. logger.info("tip message: 单个文件上传、成功");
  35. resMap.put(SystemEnumEntity.ApiRes.CODE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getCode());
  36. resMap.put(SystemEnumEntity.ApiRes.MESSAGE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getMsg());
  37. resMap.put(SystemEnumEntity.ApiRes.COUNT.getValue(), url);
  38. return Mono.just(resMap);
  39. } catch (Exception e) {
  40. logger.error("error message: 单个文件上传失败、原因:", e);
  41. // 返回 文件上传失败
  42. return Mono.error(new BusinessException(GuiErrorCode.UPLOAD_FILE_FAILED));
  43. }
  44. }
  45. /**
  46. * 初始化分片上传
  47. *
  48. * @param objectName 文件全路径名称
  49. * @param partCount 分片数量
  50. * @param contentType 类型,如果类型使用默认流会导致无法预览
  51. * @param bucketName 桶名称
  52. * @return Mono<Map < String, Object>>
  53. */
  54. public Mono<Map<String, Object>> initMultiPartUpload(String objectName, int partCount, String contentType, String bucketName) {
  55. logger.info("tip message: 通过 <{}-{}-{}-{}> 开始初始化<分片上传>数据", objectName, partCount, contentType, bucketName);
  56. Map<String, Object> resMap = new HashMap<>();
  57. try {
  58. if (CharSequenceUtil.isBlank(contentType)) {
  59. contentType = "application/octet-stream";
  60. }
  61. HashMultimap<String, String> headers = HashMultimap.create();
  62. headers.put("Content-Type", contentType);
  63. String uploadId = customMinioClient.initMultiPartUpload(bucketName, null, objectName, headers, null);
  64. resMap.put("uploadId", uploadId);
  65. List<String> partList = new ArrayList<>();
  66. Map<String, String> reqParams = new HashMap<>();
  67. reqParams.put("uploadId", uploadId);
  68. for (int i = 1; i <= partCount; i++) {
  69. reqParams.put("partNumber", String.valueOf(i));
  70. String uploadUrl = customMinioClient.getPresignedObjectUrl(
  71. GetPresignedObjectUrlArgs.builder()
  72. .method(Method.PUT)
  73. .bucket(bucketName)
  74. .object(objectName)
  75. .expiry(1, TimeUnit.DAYS)
  76. .extraQueryParams(reqParams)
  77. .build());
  78. partList.add(uploadUrl);
  79. }
  80. logger.info("tip message: 文件初始化<分片上传>、成功");
  81. resMap.put(SystemEnumEntity.ApiRes.CODE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getCode());
  82. resMap.put(SystemEnumEntity.ApiRes.MESSAGE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getMsg());
  83. resMap.put(SystemEnumEntity.ApiRes.COUNT.getValue(), partList);
  84. return Mono.just(resMap);
  85. } catch (Exception e) {
  86. logger.error("error message: 初始化分片上传失败、原因:", e);
  87. // 返回 文件上传失败
  88. return Mono.error(new BusinessException(GuiErrorCode.UPLOAD_FILE_FAILED));
  89. }
  90. }
  91. /**
  92. * 分片上传完后合并
  93. *
  94. * @param objectName 文件全路径名称
  95. * @param uploadId 返回的uploadId
  96. * @param bucketName 桶名称
  97. * @return boolean
  98. */
  99. public boolean mergeMultipartUpload(String objectName, String uploadId, String bucketName) {
  100. try {
  101. logger.info("tip message: 通过 <{}-{}-{}> 合并<分片上传>数据", objectName, uploadId, bucketName);
  102. //目前仅做了最大1000分片
  103. Part[] parts = new Part[1000];
  104. // 查询上传后的分片数据
  105. ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
  106. int partNumber = 1;
  107. for (Part part : partResult.result().partList()) {
  108. parts[partNumber - 1] = new Part(partNumber, part.etag());
  109. partNumber++;
  110. }
  111. // 合并分片
  112. customMinioClient.mergeMultipartUpload(bucketName, null, objectName, uploadId, parts, null, null);
  113. } catch (Exception e) {
  114. logger.error("error message: 合并失败、原因:", e);
  115. return false;
  116. }
  117. return true;
  118. }
  119. /**
  120. * 通过 sha256 获取上传中的分片信息
  121. *
  122. * @param objectName 文件全路径名称
  123. * @param uploadId 返回的uploadId
  124. * @param bucketName 桶名称
  125. * @return Mono<Map < String, Object>>
  126. */
  127. public Mono<Map<String, Object>> getByFileSha256(String objectName, String uploadId, String bucketName) {
  128. logger.info("通过 <{}-{}-{}> 查询<minio>上传分片数据", objectName, uploadId, bucketName);
  129. Map<String, Object> resMap = new HashMap<>();
  130. try {
  131. // 查询上传后的分片数据
  132. ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
  133. List<Integer> collect = partResult.result().partList().stream().map(Part::partNumber).collect(Collectors.toList());
  134. resMap.put(SystemEnumEntity.ApiRes.CODE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getCode());
  135. resMap.put(SystemEnumEntity.ApiRes.MESSAGE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getMsg());
  136. resMap.put(SystemEnumEntity.ApiRes.COUNT.getValue(), collect);
  137. return Mono.just(resMap);
  138. } catch (Exception e) {
  139. logger.error("error message: 查询上传后的分片信息失败、原因:", e);
  140. return Mono.error(new BusinessException(SystemErrorCode.DATA_NOT_EXISTS));
  141. }
  142. }
  143. }

2.5.1、返回的消息编码:

        

public class SystemEnumEntity {
    /**
     * 消息返回格式定义
     */
    public enum ApiRes implements EnumInterface {
        // api消息编码
        CODE("code", "api消息编码"),
        // api消息编码含义
        MESSAGE("msg", "api消息编码含义"),
        // api结果
        DATA("data", "api结果"),
        // 分页时使用,用于记录总条数
        COUNT("count", "总记录数");

        final String value;
        final String label;

        ApiRes(String value, String label) {
            this.value = value;
            this.label = label;
        }

        public static ApiRes getEnumByValue(String value) {
            for (ApiRes apiRes : ApiRes.values()) {
                if (apiRes.value.equals(value)) {
                    return apiRes;
                }
            }
            return null;
        }

        @Override
        public String getValue() {
            return this.value;
        }

        @Override
        public String getLabel() {
            return this.label;
        }
    }

}

2.6、Minio Service 层:

注:sha256和MD5是差不多的,我这用的是 sha256、都是有前端传过来,后端自己生成也可以

  1. /**
  2. * 分片上传初始化
  3. *
  4. * @param richTextNewsRelationVO RichTextNewsRelationVO
  5. * @return Map<String, Object>
  6. */
  7. Mono<Map<String, Object>> initMultiPartUpload(RichTextNewsRelationVO richTextNewsRelationVO);
  8. /**
  9. * 完成分片上传
  10. *
  11. * @param richTextNewsRelationVO RichTextNewsRelationVO
  12. * @return boolean
  13. */
  14. boolean mergeMultipartUpload(RichTextNewsRelationVO richTextNewsRelationVO);
  15. /**
  16. * 通过 sha256 获取已上传的数据
  17. * @param sha256 String
  18. * @return Mono<Map<String, Object>>
  19. */
  20. Mono<Map<String, Object>> getByFileSha256(String sha256);

2.7:Minio  Servicelmpl实现层

        注意:缺少的两个service是、一个是用来获取存储桶名称、一个是用来查询数据库获取桶名称和UploadId的、这你可以吧桶替换成你自己的传过来即可、从库里获取的是为了做:断点续传操作

如果你这只是为了跑通流程、getByFileSha256()这个方法可以先忽略掉。

  1. @Service
  2. public class UploadServiceImpl implements UploadService {
  3. private final Logger logger = LoggerFactory.getLogger(UploadServiceImpl.class);
  4. private final FileServiceImpl fileService;
  5. public UploadServiceImpl(FileServiceImpl minIoUtils) {
  6. this.fileService = minIoUtils;
  7. }
  8. /**
  9. * 文件分片上传
  10. *
  11. * @param richTextNewsRelationVO RichTextNewsRelationVO
  12. * @return Mono<Map < String, Object>>
  13. */
  14. @Override
  15. public Mono<Map<String, Object>> initMultiPartUpload(RichTextNewsRelationVO richTextNewsRelationVO) {
  16. logger.info("tip message: 通过 <{}> 开始初始化<分片上传>任务",richTextNewsRelationVO);
  17. // 获取桶
  18. String bucketName = richTextNewsRelationService.getBucketName(richTextNewsRelationVO.getNewsFileType());
  19. // 单文件上传可拆分,这里只做演示,可直接上传完成
  20. if (richTextNewsRelationVO.getPartCount() == 1) {
  21. logger.info("tip message: 当前分片数量 <{}> 进行单文件上传",richTextNewsRelationVO.getPartCount());
  22. return fileService.getUploadObjectUrl(richTextNewsRelationVO.getFileName(),bucketName);
  23. }
  24. // 分片上传
  25. else {
  26. logger.info("tip message: 当前分片数量 <{}> 进行分片上传",richTextNewsRelationVO.getPartCount());
  27. return fileService.initMultiPartUpload(richTextNewsRelationVO.getFileName(), richTextNewsRelationVO.getPartCount(), richTextNewsRelationVO.getContentType(),bucketName);
  28. }
  29. }
  30. /**
  31. * 文件合并
  32. *
  33. * @param richTextNewsRelationVO RichTextNewsRelationVO
  34. * @return boolean
  35. */
  36. @Override
  37. public boolean mergeMultipartUpload(RichTextNewsRelationVO richTextNewsRelationVO) {
  38. logger.info("tip message: 通过 <{}> 开始合并<分片上传>任务",richTextNewsRelationVO);
  39. // 获取桶
  40. String bucketName = richTextNewsRelationService.getBucketName(richTextNewsRelationVO.getNewsFileType());
  41. return fileService.mergeMultipartUpload(richTextNewsRelationVO.getFileName(), richTextNewsRelationVO.getUploadId(),bucketName);
  42. }
  43. /**
  44. * 通过 sha256 获取已上传的数据(断点续传)
  45. * @param sha256 String
  46. * @return Mono<Map<String, Object>>
  47. */
  48. @Override
  49. public Mono<Map<String, Object>> getByFileSha256(String sha256) {
  50. logger.info("tip message: 通过 <{}> 查询数据是否存在",sha256);
  51. // 获取文件名称和id
  52. RichTextNewsRelation byNewsFileSha256 = richTextNewsRelationMapper.findByNewsFileSha256(sha256);
  53. if(Optional.ofNullable(byNewsFileSha256).isEmpty()){
  54. // 返回数据不存在
  55. logger.error("error message: 文件数据不存在");
  56. return Mono.error(new BusinessException(SystemErrorCode.DATA_NOT_EXISTS));
  57. }
  58. // 获取桶
  59. String bucketName = richTextNewsRelationService.getBucketName(byNewsFileSha256.getNewsFileType());
  60. return fileService.getByFileSha256(byNewsFileSha256.getFileName(),byNewsFileSha256.getUploadId(),bucketName);
  61. }
  62. }

2.7、Minio Controller层

  1. @RestController
  2. public class FileMinioResource {
  3. private final Logger logger = LoggerFactory.getLogger(FileMinioResource.class);
  4. private final UploadService uploadService;
  5. public FileMinioResource(UploadService uploadService) {
  6. this.uploadService = uploadService;
  7. }
  8. /**
  9. * 分片初始化
  10. *
  11. * @param richTextNewsRelationVO RichTextNewsRelationVO
  12. * @return Mono<Map < String, Object>>
  13. */
  14. @PostMapping("/multipart/init")
  15. public Mono<Map<String, Object>> initMultiPartUpload(@RequestBody RichTextNewsRelationVO richTextNewsRelationVO) {
  16. logger.info("REST: 通过 <{}> 初始化上传任务",richTextNewsRelationVO);
  17. return uploadService.initMultiPartUpload(richTextNewsRelationVO);
  18. }
  19. /**
  20. * 完成上传
  21. *
  22. * @param richTextNewsRelationVO RichTextNewsRelationVO
  23. * @return Mono<Map < String, Object>>
  24. */
  25. @PutMapping("/multipart/complete")
  26. public Mono<Map<String, Object>> completeMultiPartUpload(@RequestBody RichTextNewsRelationVO richTextNewsRelationVO) {
  27. logger.info("REST: 通过 <{}> 合并上传任务",richTextNewsRelationVO);
  28. Map<String, Object> resMap = new HashMap<>();
  29. boolean result = uploadService.mergeMultipartUpload(richTextNewsRelationVO);
  30. resMap.put(SystemEnumEntity.ApiRes.CODE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getCode());
  31. resMap.put(SystemEnumEntity.ApiRes.MESSAGE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getMsg());
  32. resMap.put(SystemEnumEntity.ApiRes.DATA.getValue(), result);
  33. return Mono.just(resMap);
  34. }
  35. /**
  36. * 校验文件是否存在
  37. *
  38. * @param sha256 String
  39. * @return Mono<Map < String, Object>>
  40. */
  41. @GetMapping("/multipart/check")
  42. public Mono<Map<String, Object>> checkFileUploadedByMd5(@RequestParam("sha256") String sha256) {
  43. logger.info("REST: 通过查询 <{}> 文件是否存在、是否进行断点续传",sha256);
  44. if(StringUtil.isEmpty(sha256)){
  45. logger.error("查询文件是否存在、入参无效");
  46. return Mono.error(new BusinessException(GuiErrorCode.ACCESS_PARAMETER_INVALID));
  47. }
  48. return uploadService.getByFileSha256(sha256);
  49. }
  50. }

2.8、VO参数

public class RichTextNewsRelationVO implements Serializable {
@NotBlank(message = "文件名不能为空")
private String fileName;

@NotNull(message = "文件大小不能为空")
private Double fileSize;

@NotBlank(message = "Content-Type不能为空")
private String contentType;

@NotNull(message = "分片数量不能为空")
private Integer partCount;

@NotBlank(message = "uploadId 不能为空")
private String uploadId;

// 桶名称

private String bucketName;

}

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/528128
推荐阅读
相关标签
  

闽ICP备14008679号