赞
踩
最近在学习,在SpringBoot上进行分片上传、断点续传、直接上传到Minio服务器上,中间也遇到的不少坑、自定义minio继承MinioClient来实现分片上传、比较适合初学者。
一、大致的流程如下:
前端代码地址:https://github.com/lanweihong/vue-minio-upload-sample
流程图如下:
二、实现过程
2.1、获取 minio 依赖
我这里使用的Gradle管理的项目:
// minio
implementation 'io.minio:minio:8.2.1'
如果用maven管理项目可以访问官网:
2.2、配置minio yml文件
application.yml配置如下:
- minio:
- endpoint: ${MINIO_ENDPOINT:https://play.minio.io:9000}
- access: ${MINIO_ASSESSKEY:Q3AM3UQ867SPQQA43P2F}
- secre: ${MINIO_SECRETKEY:zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG}
- bucket: ${MINIO_BUCKET:tuinetest}
2.3、获取minio默认属性
- @Component
- @ConfigurationProperties(prefix = "minio")
- public class MinioProperties {
-
- /**
- * 文件服务地址
- */
- private String endpoint;
-
- /**
- * 文件服务器账号
- */
- private String access;
-
- /**
- * 文件服务认证信息
- */
- private String secre;
-
- /**
- * 桶存储名称
- */
- private String bucket;
-
- public String getEndpoint() {
- return endpoint;
- }
-
- public void setEndpoint(String endpoint) {
- this.endpoint = endpoint;
- }
-
- public String getAccess() {
- return accesskey;
- }
-
- public void setAccess(String access) {
- this.access = access;
- }
-
- public String getSecre() {
- return secre;
- }
-
- public void setSecret(String secre) {
- this.secre = secre;
- }
-
- public String getBucket() {
- return bucket;
- }
-
- public void setBucket(String bucket) {
- this.bucket = bucket;
- }
-
- @Override
- public String toString() {
- return "MinioProperties{" +
- "endpoint='" + endpoint + '\'' +
- ", access='" + access + '\'' +
- ", secre='" + secre + '\'' +
- ", bucket='" + bucket + '\'' +
- '}';
- }
-
- }
2.4、自定义Minio继承 MinioClient
2.4.1、如果我们不继承MinioClient 访问 listParts 查询分片信息是会报错的、通过继承 MinioClient 来去实现这个方法就可以了。
2.4.2、方法上参数写着注解说明、这个参数是用到了、没写则是没有用到,直接传NULL即可。
- @Component
- public class CustomMinioClient extends MinioClient {
-
- public CustomMinioClient(MinioClient client) {
- super(client);
- }
-
- /**
- * 初始化分片上传、获取 uploadId
- *
- * @param bucket String 存储桶名称
- * @param region String
- * @param object String 文件名称
- * @param headers Multimap<String, String> 请求头
- * @param extraQueryParams Multimap<String, String>
- * @return String
- */
- 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 {
- CreateMultipartUploadResponse response = this.createMultipartUpload(bucket, region, object, headers, extraQueryParams);
-
- return response.result().uploadId();
- }
-
- /**
- * 合并分片
- *
- * @param bucketName String 桶名称
- * @param region String
- * @param objectName String 文件名称
- * @param uploadId String 上传的 uploadId
- * @param parts Part[] 分片集合
- * @param extraHeaders Multimap<String, String>
- * @param extraQueryParams Multimap<String, String>
- * @return ObjectWriteResponse
- */
- 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 {
-
- return this.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
- }
-
- /**
- * 查询当前上传后的分片信息
- *
- * @param bucketName String 桶名称
- * @param region String
- * @param objectName String 文件名称
- * @param maxParts Integer 分片数量
- * @param partNumberMarker Integer 分片起始值
- * @param uploadId String 上传的 uploadId
- * @param extraHeaders Multimap<String, String>
- * @param extraQueryParams Multimap<String, String>
- * @return ListPartsResponse
- */
- 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 {
- return this.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
- }
- }
2.5、链接minio、上传、合并分片的核心方法
Mono<Map<String, Object>> 返回的格式和消息编码可以看、2.5.1、
- public class MinIoUtils {
-
- // 这两个引用 上面都有写
- private final MinioProperties minioProperties;
- private CustomMinioClient customMinioClient;
-
- public MinIoUtils(MinioProperties minioProperties) {
- this.minioProperties = minioProperties;
- }
-
- @PostConstruct
- public void init() {
- MinioClient minioClient = MinioClient.builder()
- .endpoint(minioProperties.getEndpoint())
- .credentials(minioProperties.getAccesskey(), minioProperties.getSecretkey())
- .build();
- customMinioClient = new CustomMinioClient(minioClient);
- }
-
-
- /**
- * 单文件签名上传
- *
- * @param objectName 文件全路径名称
- * @param bucketName 桶名称
- * @return /
- */
- public Mono<Map<String, Object>> getUploadObjectUrl(String objectName, String bucketName) {
- try {
- logger.info("tip message: 通过 <{}-{}> 开始单文件上传<minio>", objectName, bucketName);
- Map<String, Object> resMap = new HashMap<>();
- String url = customMinioClient.getPresignedObjectUrl(
- GetPresignedObjectUrlArgs.builder()
- .method(Method.PUT)
- .bucket(bucketName)
- .object(objectName)
- .expiry(1, TimeUnit.DAYS)
- .build());
- logger.info("tip message: 单个文件上传、成功");
- resMap.put(SystemEnumEntity.ApiRes.CODE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getCode());
- resMap.put(SystemEnumEntity.ApiRes.MESSAGE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getMsg());
- resMap.put(SystemEnumEntity.ApiRes.COUNT.getValue(), url);
- return Mono.just(resMap);
- } catch (Exception e) {
- logger.error("error message: 单个文件上传失败、原因:", e);
- // 返回 文件上传失败
- return Mono.error(new BusinessException(GuiErrorCode.UPLOAD_FILE_FAILED));
- }
- }
-
- /**
- * 初始化分片上传
- *
- * @param objectName 文件全路径名称
- * @param partCount 分片数量
- * @param contentType 类型,如果类型使用默认流会导致无法预览
- * @param bucketName 桶名称
- * @return Mono<Map < String, Object>>
- */
- public Mono<Map<String, Object>> initMultiPartUpload(String objectName, int partCount, String contentType, String bucketName) {
- logger.info("tip message: 通过 <{}-{}-{}-{}> 开始初始化<分片上传>数据", objectName, partCount, contentType, bucketName);
- Map<String, Object> resMap = new HashMap<>();
- try {
- if (CharSequenceUtil.isBlank(contentType)) {
- contentType = "application/octet-stream";
- }
- HashMultimap<String, String> headers = HashMultimap.create();
- headers.put("Content-Type", contentType);
- String uploadId = customMinioClient.initMultiPartUpload(bucketName, null, objectName, headers, null);
-
- resMap.put("uploadId", uploadId);
- List<String> partList = new ArrayList<>();
-
- Map<String, String> reqParams = new HashMap<>();
- reqParams.put("uploadId", uploadId);
- for (int i = 1; i <= partCount; i++) {
- reqParams.put("partNumber", String.valueOf(i));
- String uploadUrl = customMinioClient.getPresignedObjectUrl(
- GetPresignedObjectUrlArgs.builder()
- .method(Method.PUT)
- .bucket(bucketName)
- .object(objectName)
- .expiry(1, TimeUnit.DAYS)
- .extraQueryParams(reqParams)
- .build());
- partList.add(uploadUrl);
- }
- logger.info("tip message: 文件初始化<分片上传>、成功");
- resMap.put(SystemEnumEntity.ApiRes.CODE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getCode());
- resMap.put(SystemEnumEntity.ApiRes.MESSAGE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getMsg());
- resMap.put(SystemEnumEntity.ApiRes.COUNT.getValue(), partList);
- return Mono.just(resMap);
- } catch (Exception e) {
- logger.error("error message: 初始化分片上传失败、原因:", e);
- // 返回 文件上传失败
- return Mono.error(new BusinessException(GuiErrorCode.UPLOAD_FILE_FAILED));
- }
- }
-
- /**
- * 分片上传完后合并
- *
- * @param objectName 文件全路径名称
- * @param uploadId 返回的uploadId
- * @param bucketName 桶名称
- * @return boolean
- */
- public boolean mergeMultipartUpload(String objectName, String uploadId, String bucketName) {
- try {
- logger.info("tip message: 通过 <{}-{}-{}> 合并<分片上传>数据", objectName, uploadId, bucketName);
- //目前仅做了最大1000分片
- Part[] parts = new Part[1000];
- // 查询上传后的分片数据
- ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
- int partNumber = 1;
- for (Part part : partResult.result().partList()) {
- parts[partNumber - 1] = new Part(partNumber, part.etag());
- partNumber++;
- }
- // 合并分片
- customMinioClient.mergeMultipartUpload(bucketName, null, objectName, uploadId, parts, null, null);
- } catch (Exception e) {
- logger.error("error message: 合并失败、原因:", e);
- return false;
- }
- return true;
- }
-
- /**
- * 通过 sha256 获取上传中的分片信息
- *
- * @param objectName 文件全路径名称
- * @param uploadId 返回的uploadId
- * @param bucketName 桶名称
- * @return Mono<Map < String, Object>>
- */
- public Mono<Map<String, Object>> getByFileSha256(String objectName, String uploadId, String bucketName) {
- logger.info("通过 <{}-{}-{}> 查询<minio>上传分片数据", objectName, uploadId, bucketName);
- Map<String, Object> resMap = new HashMap<>();
- try {
- // 查询上传后的分片数据
- ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
- List<Integer> collect = partResult.result().partList().stream().map(Part::partNumber).collect(Collectors.toList());
- resMap.put(SystemEnumEntity.ApiRes.CODE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getCode());
- resMap.put(SystemEnumEntity.ApiRes.MESSAGE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getMsg());
- resMap.put(SystemEnumEntity.ApiRes.COUNT.getValue(), collect);
- return Mono.just(resMap);
- } catch (Exception e) {
- logger.error("error message: 查询上传后的分片信息失败、原因:", e);
- return Mono.error(new BusinessException(SystemErrorCode.DATA_NOT_EXISTS));
- }
- }
- }
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、都是有前端传过来,后端自己生成也可以
-
- /**
- * 分片上传初始化
- *
- * @param richTextNewsRelationVO RichTextNewsRelationVO
- * @return Map<String, Object>
- */
- Mono<Map<String, Object>> initMultiPartUpload(RichTextNewsRelationVO richTextNewsRelationVO);
-
- /**
- * 完成分片上传
- *
- * @param richTextNewsRelationVO RichTextNewsRelationVO
- * @return boolean
- */
- boolean mergeMultipartUpload(RichTextNewsRelationVO richTextNewsRelationVO);
-
- /**
- * 通过 sha256 获取已上传的数据
- * @param sha256 String
- * @return Mono<Map<String, Object>>
- */
- Mono<Map<String, Object>> getByFileSha256(String sha256);
2.7:Minio Servicelmpl实现层
注意:缺少的两个service是、一个是用来获取存储桶名称、一个是用来查询数据库获取桶名称和UploadId的、这你可以吧桶替换成你自己的传过来即可、从库里获取的是为了做:断点续传操作
如果你这只是为了跑通流程、getByFileSha256()这个方法可以先忽略掉。
- @Service
- public class UploadServiceImpl implements UploadService {
-
- private final Logger logger = LoggerFactory.getLogger(UploadServiceImpl.class);
-
- private final FileServiceImpl fileService;
-
- public UploadServiceImpl(FileServiceImpl minIoUtils) {
- this.fileService = minIoUtils;
-
- }
-
- /**
- * 文件分片上传
- *
- * @param richTextNewsRelationVO RichTextNewsRelationVO
- * @return Mono<Map < String, Object>>
- */
- @Override
- public Mono<Map<String, Object>> initMultiPartUpload(RichTextNewsRelationVO richTextNewsRelationVO) {
- logger.info("tip message: 通过 <{}> 开始初始化<分片上传>任务",richTextNewsRelationVO);
- // 获取桶
- String bucketName = richTextNewsRelationService.getBucketName(richTextNewsRelationVO.getNewsFileType());
- // 单文件上传可拆分,这里只做演示,可直接上传完成
- if (richTextNewsRelationVO.getPartCount() == 1) {
- logger.info("tip message: 当前分片数量 <{}> 进行单文件上传",richTextNewsRelationVO.getPartCount());
- return fileService.getUploadObjectUrl(richTextNewsRelationVO.getFileName(),bucketName);
- }
- // 分片上传
- else {
- logger.info("tip message: 当前分片数量 <{}> 进行分片上传",richTextNewsRelationVO.getPartCount());
- return fileService.initMultiPartUpload(richTextNewsRelationVO.getFileName(), richTextNewsRelationVO.getPartCount(), richTextNewsRelationVO.getContentType(),bucketName);
- }
- }
-
- /**
- * 文件合并
- *
- * @param richTextNewsRelationVO RichTextNewsRelationVO
- * @return boolean
- */
- @Override
- public boolean mergeMultipartUpload(RichTextNewsRelationVO richTextNewsRelationVO) {
- logger.info("tip message: 通过 <{}> 开始合并<分片上传>任务",richTextNewsRelationVO);
- // 获取桶
- String bucketName = richTextNewsRelationService.getBucketName(richTextNewsRelationVO.getNewsFileType());
- return fileService.mergeMultipartUpload(richTextNewsRelationVO.getFileName(), richTextNewsRelationVO.getUploadId(),bucketName);
- }
-
- /**
- * 通过 sha256 获取已上传的数据(断点续传)
- * @param sha256 String
- * @return Mono<Map<String, Object>>
- */
- @Override
- public Mono<Map<String, Object>> getByFileSha256(String sha256) {
- logger.info("tip message: 通过 <{}> 查询数据是否存在",sha256);
- // 获取文件名称和id
- RichTextNewsRelation byNewsFileSha256 = richTextNewsRelationMapper.findByNewsFileSha256(sha256);
- if(Optional.ofNullable(byNewsFileSha256).isEmpty()){
- // 返回数据不存在
- logger.error("error message: 文件数据不存在");
- return Mono.error(new BusinessException(SystemErrorCode.DATA_NOT_EXISTS));
- }
- // 获取桶
- String bucketName = richTextNewsRelationService.getBucketName(byNewsFileSha256.getNewsFileType());
- return fileService.getByFileSha256(byNewsFileSha256.getFileName(),byNewsFileSha256.getUploadId(),bucketName);
- }
- }
2.7、Minio Controller层
- @RestController
- public class FileMinioResource {
-
- private final Logger logger = LoggerFactory.getLogger(FileMinioResource.class);
-
- private final UploadService uploadService;
-
- public FileMinioResource(UploadService uploadService) {
- this.uploadService = uploadService;
- }
-
- /**
- * 分片初始化
- *
- * @param richTextNewsRelationVO RichTextNewsRelationVO
- * @return Mono<Map < String, Object>>
- */
- @PostMapping("/multipart/init")
- public Mono<Map<String, Object>> initMultiPartUpload(@RequestBody RichTextNewsRelationVO richTextNewsRelationVO) {
- logger.info("REST: 通过 <{}> 初始化上传任务",richTextNewsRelationVO);
- return uploadService.initMultiPartUpload(richTextNewsRelationVO);
- }
-
- /**
- * 完成上传
- *
- * @param richTextNewsRelationVO RichTextNewsRelationVO
- * @return Mono<Map < String, Object>>
- */
- @PutMapping("/multipart/complete")
- public Mono<Map<String, Object>> completeMultiPartUpload(@RequestBody RichTextNewsRelationVO richTextNewsRelationVO) {
- logger.info("REST: 通过 <{}> 合并上传任务",richTextNewsRelationVO);
- Map<String, Object> resMap = new HashMap<>();
- boolean result = uploadService.mergeMultipartUpload(richTextNewsRelationVO);
- resMap.put(SystemEnumEntity.ApiRes.CODE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getCode());
- resMap.put(SystemEnumEntity.ApiRes.MESSAGE.getValue(), SystemErrorCode.DATA_UPDATE_SUCCESS.getMsg());
- resMap.put(SystemEnumEntity.ApiRes.DATA.getValue(), result);
- return Mono.just(resMap);
- }
-
- /**
- * 校验文件是否存在
- *
- * @param sha256 String
- * @return Mono<Map < String, Object>>
- */
- @GetMapping("/multipart/check")
- public Mono<Map<String, Object>> checkFileUploadedByMd5(@RequestParam("sha256") String sha256) {
- logger.info("REST: 通过查询 <{}> 文件是否存在、是否进行断点续传",sha256);
- if(StringUtil.isEmpty(sha256)){
- logger.error("查询文件是否存在、入参无效");
- return Mono.error(new BusinessException(GuiErrorCode.ACCESS_PARAMETER_INVALID));
- }
- return uploadService.getByFileSha256(sha256);
- }
- }
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;
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。