AWS-S3实现Minio分片上传、断点续传、秒传、分片下载、停息下载 ...

打印 上一主题 下一主题

主题 509|帖子 509|积分 1527

前言

Amazon Simple Storage Service(S3),简单存储服务,是一个公开的云存储服务。Web应用程序开发人员可以使用它存储数字资产,包罗图片、视频、音乐和文档。S3提供一个RESTful API以编程方式实现与该服务的交互。目前市面上主流的存储厂商都支持S3协议接口。
本文鉴戒风希落https://www.cnblogs.com/jsonq/p/18186340大佬的文章及代码修改而来。
项目采用前后端分离模式:
前端:vue3 + element-plus + axios + spark-md5
后端:Springboot 3X + minio+aws-s3 + redis + mysql + mybatisplus
本文全部代码以上传gitee:https://gitee.com/luzhiyong_erfou/learning-notes/tree/master/aws-s3-upload
一、功能展示

上传功能点



  • 大文件分片上传
  • 文件秒传
  • 断点续传
  • 上传进度
下载功能点



  • 分片下载
  • 停息下载
  • 下载进度
结果展示


二、思绪流程

上传流程

一个文件的上传,对接后端的哀求有三个


  • 点击上传时,哀求 <检查文件 md5> 接口,判断文件的状态(已存在、未存在、传输部门)
  • 根据差别的状态,通过 <初始化分片上传地点>,得到该文件的分片地点
  • 前端将分片地点和分片文件逐一对应进行上传,直接上传至对象存储
  • 上传完毕,调用 <合并文件> 接口,合并文件,文件数据入库

整体步调:


  • 前端计算文件 md5,并发哀求查询此文件的状态
  • 若文件已上传,则后端直接返回上传成功,并返回 url 地点
  • 若文件未上传,则前端哀求初始化分片接口,返回上传地点。循环将分片文件和分片地点逐一对一应 若文件上传一部门,后端会返回该文件的uploadId (minio中的文件标识)和listParts(已上传的分片索引),前端哀求初始化分片接口,后端重新天生上传地点。前端循环将已上传的分片过滤掉,未上传的分片和分片地点逐一对应。
  • 前端通太过片地点将分片文件逐一上传
  • 上传完毕后,前端调用合并分片接口
  • 后端判断该文件是单片照旧分片,单片则不走合并,仅信息入库,分片则先合并,再信息入库。删除 redis 中的文件信息,返回文件地点。
下载流程

整体步调:


  • 前端计算分片下载的哀求次数并设置每次哀求的偏移长度
  • 循环调用后端接口
  • 后端判断文件是否缓存并获取文件信息,根据前端传入的便宜长度和分片巨细获取文件流返回前端
  • 前端记录每片的blob
  • 根据文件流转成的 blob 下载文件

三、代码示例

service
  1. import cn.hutool.core.bean.BeanUtil;
  2. import cn.hutool.core.date.DateUtil;
  3. import cn.hutool.core.io.FileUtil;
  4. import cn.hutool.core.util.StrUtil;
  5. import cn.hutool.json.JSONUtil;
  6. import cn.superlu.s3uploadservice.common.R;
  7. import cn.superlu.s3uploadservice.config.FileProperties;
  8. import cn.superlu.s3uploadservice.constant.FileHttpCodeEnum;
  9. import cn.superlu.s3uploadservice.mapper.SysFileUploadMapper;
  10. import cn.superlu.s3uploadservice.model.bo.FileUploadInfo;
  11. import cn.superlu.s3uploadservice.model.entity.SysFileUpload;
  12. import cn.superlu.s3uploadservice.model.vo.BaseFileVo;
  13. import cn.superlu.s3uploadservice.model.vo.UploadUrlsVO;
  14. import cn.superlu.s3uploadservice.service.SysFileUploadService;
  15. import cn.superlu.s3uploadservice.utils.AmazonS3Util;
  16. import cn.superlu.s3uploadservice.utils.MinioUtil;
  17. import cn.superlu.s3uploadservice.utils.RedisUtil;
  18. import com.amazonaws.services.s3.model.S3Object;
  19. import com.amazonaws.services.s3.model.S3ObjectInputStream;
  20. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  21. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  22. import jakarta.servlet.http.HttpServletRequest;
  23. import jakarta.servlet.http.HttpServletResponse;
  24. import lombok.RequiredArgsConstructor;
  25. import lombok.extern.slf4j.Slf4j;
  26. import org.springframework.http.HttpStatus;
  27. import org.springframework.http.ResponseEntity;
  28. import org.springframework.stereotype.Service;
  29. import java.io.BufferedOutputStream;
  30. import java.io.IOException;
  31. import java.nio.charset.StandardCharsets;
  32. import java.time.LocalDateTime;
  33. import java.util.List;
  34. import java.util.concurrent.TimeUnit;
  35. @Service
  36. @Slf4j
  37. @RequiredArgsConstructor
  38. public class SysFileUploadServiceImpl extends ServiceImpl<SysFileUploadMapper, SysFileUpload> implements SysFileUploadService {
  39.     private static final Integer BUFFER_SIZE = 1024 * 64; // 64KB
  40.     private final RedisUtil redisUtil;
  41.     private final MinioUtil minioUtil;
  42.     private final AmazonS3Util amazonS3Util;
  43.     private final FileProperties fileProperties;
  44.     /**
  45.      * 检查文件是否存在
  46.      * @param md5
  47.      * @return
  48.      */
  49.     @Override
  50.     public R<BaseFileVo<FileUploadInfo>> checkFileByMd5(String md5) {
  51.         log.info("查询md5: <{}> 在redis是否存在", md5);
  52.         FileUploadInfo fileUploadInfo = (FileUploadInfo)redisUtil.get(md5);
  53.         if (fileUploadInfo != null) {
  54.             log.info("查询到md5:在redis中存在:{}", JSONUtil.toJsonStr(fileUploadInfo));
  55.             if(fileUploadInfo.getChunkCount()==1){
  56.                 return R.ok( BaseFileVo.builder(FileHttpCodeEnum.NOT_UPLOADED, null));
  57.             }else{
  58.                 List<Integer> listParts = minioUtil.getListParts(fileUploadInfo.getObject(), fileUploadInfo.getUploadId());
  59. //              List<Integer> listParts = amazonS3Util.getListParts(fileUploadInfo.getObject(), fileUploadInfo.getUploadId());
  60.                 fileUploadInfo.setListParts(listParts);
  61.                 return R.ok( BaseFileVo.builder(FileHttpCodeEnum.UPLOADING, fileUploadInfo));
  62.             }
  63.         }
  64.         log.info("redis中不存在md5: <{}> 查询mysql是否存在", md5);
  65.         SysFileUpload file = baseMapper.selectOne(new LambdaQueryWrapper<SysFileUpload>().eq(SysFileUpload::getMd5, md5));
  66.         if (file != null) {
  67.             log.info("mysql中存在md5: <{}> 的文件 该文件已上传至minio 秒传直接过", md5);
  68.             FileUploadInfo dbFileInfo = BeanUtil.toBean(file, FileUploadInfo.class);
  69.             return R.ok( BaseFileVo.builder(FileHttpCodeEnum.UPLOAD_SUCCESS, dbFileInfo));
  70.         }
  71.         return R.ok( BaseFileVo.builder(FileHttpCodeEnum.NOT_UPLOADED, null));
  72.     }
  73.     /**
  74.      * 初始化文件分片地址及相关数据
  75.      * @param fileUploadInfo
  76.      * @return
  77.      */
  78.     @Override
  79.     public R<BaseFileVo<UploadUrlsVO>> initMultipartUpload(FileUploadInfo fileUploadInfo) {
  80.         log.info("查询md5: <{}> 在redis是否存在", fileUploadInfo.getMd5());
  81.         FileUploadInfo redisFileUploadInfo = (FileUploadInfo)redisUtil.get(fileUploadInfo.getMd5());
  82.         // 若 redis 中有该 md5 的记录,以 redis 中为主
  83.         String object;
  84.         if (redisFileUploadInfo != null) {
  85.             fileUploadInfo = redisFileUploadInfo;
  86.             object = redisFileUploadInfo.getObject();
  87.         } else {
  88.             String originFileName = fileUploadInfo.getOriginFileName();
  89.             String suffix = FileUtil.extName(originFileName);
  90.             String fileName = FileUtil.mainName(originFileName);
  91.             // 对文件重命名,并以年月日文件夹格式存储
  92.             String nestFile = DateUtil.format(LocalDateTime.now(), "yyyy/MM/dd");
  93.             object = nestFile + "/" + fileName + "_" + fileUploadInfo.getMd5() + "." + suffix;
  94.             fileUploadInfo.setObject(object).setType(suffix);
  95.         }
  96.         UploadUrlsVO urlsVO;
  97.         // 单文件上传
  98.         if (fileUploadInfo.getChunkCount() == 1) {
  99.             log.info("当前分片数量 <{}> 单文件上传", fileUploadInfo.getChunkCount());
  100. //            urlsVO = minioUtil.getUploadObjectUrl(fileUploadInfo.getContentType(), object);
  101.             urlsVO=amazonS3Util.getUploadObjectUrl(fileUploadInfo.getContentType(), object);
  102.         } else {
  103.             // 分片上传
  104.             log.info("当前分片数量 <{}> 分片上传", fileUploadInfo.getChunkCount());
  105. //            urlsVO = minioUtil.initMultiPartUpload(fileUploadInfo, object);
  106.             urlsVO = amazonS3Util.initMultiPartUpload(fileUploadInfo, object);
  107.         }
  108.         fileUploadInfo.setUploadId(urlsVO.getUploadId());
  109.         // 存入 redis (单片存 redis 唯一用处就是可以让单片也入库,因为单片只有一个请求,基本不会出现问题)
  110.         redisUtil.set(fileUploadInfo.getMd5(), fileUploadInfo, fileProperties.getOss().getBreakpointTime(), TimeUnit.DAYS);
  111.         return R.ok(BaseFileVo.builder(FileHttpCodeEnum.SUCCESS, urlsVO));
  112.     }
  113.     /**
  114.      * 合并分片
  115.      * @param md5
  116.      * @return
  117.      */
  118.     @Override
  119.     public R<BaseFileVo<String>> mergeMultipartUpload(String md5) {
  120.         FileUploadInfo redisFileUploadInfo = (FileUploadInfo)redisUtil.get(md5);
  121.         String url = StrUtil.format("{}/{}/{}", fileProperties.getOss().getEndpoint(), fileProperties.getBucketName(), redisFileUploadInfo.getObject());
  122.         SysFileUpload files = BeanUtil.toBean(redisFileUploadInfo, SysFileUpload.class);
  123.         files.setUrl(url)
  124.                 .setBucket(fileProperties.getBucketName())
  125.                 .setCreateTime(LocalDateTime.now());
  126.         Integer chunkCount = redisFileUploadInfo.getChunkCount();
  127.         // 分片为 1 ,不需要合并,否则合并后看返回的是 true 还是 false
  128.         boolean isSuccess = chunkCount == 1 || minioUtil.mergeMultipartUpload(redisFileUploadInfo.getObject(), redisFileUploadInfo.getUploadId());
  129. //        boolean isSuccess = chunkCount == 1 || amazonS3Util.mergeMultipartUpload(redisFileUploadInfo.getObject(), redisFileUploadInfo.getUploadId());
  130.         if (isSuccess) {
  131.             baseMapper.insert(files);
  132.             redisUtil.del(md5);
  133.             return R.ok(BaseFileVo.builder(FileHttpCodeEnum.SUCCESS, url));
  134.         }
  135.         return R.ok(BaseFileVo.builder(FileHttpCodeEnum.UPLOAD_FILE_FAILED, null));
  136.     }
  137.     /**
  138.      * 分片下载
  139.      * @param id
  140.      * @param request
  141.      * @param response
  142.      * @return
  143.      * @throws IOException
  144.      */
  145.     @Override
  146.     public ResponseEntity<byte[]> downloadMultipartFile(Long id, HttpServletRequest request, HttpServletResponse response) throws IOException {
  147.         // redis 缓存当前文件信息,避免分片下载时频繁查库
  148.         SysFileUpload file = null;
  149.         SysFileUpload redisFile = (SysFileUpload)redisUtil.get(String.valueOf(id));
  150.         if (redisFile == null) {
  151.             SysFileUpload dbFile = baseMapper.selectById(id);
  152.             if (dbFile == null) {
  153.                 return null;
  154.             } else {
  155.                 file = dbFile;
  156.                 redisUtil.set(String.valueOf(id), file, 1, TimeUnit.DAYS);
  157.             }
  158.         } else {
  159.             file = redisFile;
  160.         }
  161.         String range = request.getHeader("Range");
  162.         String fileName = file.getOriginFileName();
  163.         log.info("下载文件的 object <{}>", file.getObject());
  164.         // 获取 bucket 桶中的文件元信息,获取不到会抛出异常
  165. //        StatObjectResponse objectResponse = minioUtil.statObject(file.getObject());
  166.         S3Object s3Object = amazonS3Util.statObject(file.getObject());
  167.         long startByte = 0; // 开始下载位置
  168. //        long fileSize = objectResponse.size();
  169.         long fileSize = s3Object.getObjectMetadata().getContentLength();
  170.         long endByte = fileSize - 1; // 结束下载位置
  171.         log.info("文件总长度:{},当前 range:{}", fileSize, range);
  172.         BufferedOutputStream os = null; // buffer 写入流
  173. //        GetObjectResponse stream = null; // minio 文件流
  174.         // 存在 range,需要根据前端下载长度进行下载,即分段下载
  175.         // 例如:range=bytes=0-52428800
  176.         if (range != null && range.contains("bytes=") && range.contains("-")) {
  177.             range = range.substring(range.lastIndexOf("=") + 1).trim(); // 0-52428800
  178.             String[] ranges = range.split("-");
  179.             // 判断range的类型
  180.             if (ranges.length == 1) {
  181.                 // 类型一:bytes=-2343 后端转换为 0-2343
  182.                 if (range.startsWith("-")) endByte = Long.parseLong(ranges[0]);
  183.                 // 类型二:bytes=2343- 后端转换为 2343-最后
  184.                 if (range.endsWith("-")) startByte = Long.parseLong(ranges[0]);
  185.             } else if (ranges.length == 2) { // 类型三:bytes=22-2343
  186.                 startByte = Long.parseLong(ranges[0]);
  187.                 endByte = Long.parseLong(ranges[1]);
  188.             }
  189.         }
  190.         // 要下载的长度
  191.         // 确保返回的 contentLength 不会超过文件的实际剩余大小
  192.         long contentLength = Math.min(endByte - startByte + 1, fileSize - startByte);
  193.         // 文件类型
  194.         String contentType = request.getServletContext().getMimeType(fileName);
  195.         // 解决下载文件时文件名乱码问题
  196.         byte[] fileNameBytes = fileName.getBytes(StandardCharsets.UTF_8);
  197.         fileName = new String(fileNameBytes, 0, fileNameBytes.length, StandardCharsets.ISO_8859_1);
  198.         // 响应头设置---------------------------------------------------------------------------------------------
  199.         // 断点续传,获取部分字节内容:
  200.         response.setHeader("Accept-Ranges", "bytes");
  201.         // http状态码要为206:表示获取部分内容,SC_PARTIAL_CONTENT,若部分浏览器不支持,改成 SC_OK
  202.         response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
  203.         response.setContentType(contentType);
  204. //        response.setHeader("Last-Modified", objectResponse.lastModified().toString());
  205.         response.setHeader("Last-Modified", s3Object.getObjectMetadata().getLastModified().toString());
  206.         response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
  207.         response.setHeader("Content-Length", String.valueOf(contentLength));
  208.         // Content-Range,格式为:[要下载的开始位置]-[结束位置]/[文件总大小]
  209.         response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + fileSize);
  210. //        response.setHeader("ETag", """.concat(objectResponse.etag()).concat("""));
  211.         response.setHeader("ETag", """.concat(s3Object.getObjectMetadata().getETag()).concat("""));
  212.         response.setContentType("application/octet-stream;charset=UTF-8");
  213.         S3ObjectInputStream objectInputStream=null;
  214.         try {
  215.             // 获取文件流
  216.             String object = s3Object.getKey();
  217.             S3Object currentObject = amazonS3Util.getObject(object, startByte, contentLength);
  218.             objectInputStream = currentObject.getObjectContent();
  219. //            stream = minioUtil.getObject(objectResponse.object(), startByte, contentLength);
  220.             os = new BufferedOutputStream(response.getOutputStream());
  221.             // 将读取的文件写入到 OutputStream
  222.             byte[] bytes = new byte[BUFFER_SIZE];
  223.             long bytesWritten = 0;
  224.             int bytesRead = -1;
  225.             while ((bytesRead = objectInputStream.read(bytes)) != -1) {
  226. //            while ((bytesRead = stream.read(bytes)) != -1) {
  227.                 if (bytesWritten + bytesRead >= contentLength) {
  228.                     os.write(bytes, 0, (int)(contentLength - bytesWritten));
  229.                     break;
  230.                 } else {
  231.                     os.write(bytes, 0, bytesRead);
  232.                     bytesWritten += bytesRead;
  233.                 }
  234.             }
  235.             os.flush();
  236.             response.flushBuffer();
  237.             // 返回对应http状态
  238.             return new ResponseEntity<>(bytes, HttpStatus.OK);
  239.         } catch (Exception e) {
  240.             e.printStackTrace();
  241.         } finally {
  242.             if (os != null) os.close();
  243. //            if (stream != null) stream.close();
  244.             if (objectInputStream != null) objectInputStream.close();
  245.         }
  246.         return null;
  247.     }
  248.     @Override
  249.     public R<List<SysFileUpload>> getFileList() {
  250.         List<SysFileUpload> filesList = this.list();
  251.         return R.ok(filesList);
  252.     }
  253. }
复制代码
AmazonS3Util
  1. import cn.hutool.core.util.IdUtil;
  2. import cn.superlu.s3uploadservice.config.FileProperties;
  3. import cn.superlu.s3uploadservice.constant.FileHttpCodeEnum;
  4. import cn.superlu.s3uploadservice.model.bo.FileUploadInfo;
  5. import cn.superlu.s3uploadservice.model.vo.UploadUrlsVO;
  6. import com.amazonaws.ClientConfiguration;
  7. import com.amazonaws.HttpMethod;
  8. import com.amazonaws.auth.AWSCredentials;
  9. import com.amazonaws.auth.AWSCredentialsProvider;
  10. import com.amazonaws.auth.AWSStaticCredentialsProvider;
  11. import com.amazonaws.auth.BasicAWSCredentials;
  12. import com.amazonaws.client.builder.AwsClientBuilder;
  13. import com.amazonaws.services.s3.AmazonS3;
  14. import com.amazonaws.services.s3.AmazonS3ClientBuilder;
  15. import com.amazonaws.services.s3.model.*;
  16. import com.google.common.collect.HashMultimap;
  17. import io.minio.GetObjectArgs;
  18. import io.minio.GetObjectResponse;
  19. import io.minio.StatObjectArgs;
  20. import io.minio.StatObjectResponse;
  21. import jakarta.annotation.PostConstruct;
  22. import jakarta.annotation.Resource;
  23. import lombok.SneakyThrows;
  24. import lombok.extern.slf4j.Slf4j;
  25. import org.springframework.stereotype.Component;
  26. import java.net.URL;
  27. import java.util.*;
  28. import java.util.stream.Collectors;
  29. @Slf4j
  30. @Component
  31. public class AmazonS3Util {
  32.     @Resource
  33.     private FileProperties fileProperties;
  34.     private AmazonS3 amazonS3;
  35.     // spring自动注入会失败
  36.     @PostConstruct
  37.     public void init() {
  38.         ClientConfiguration clientConfiguration = new ClientConfiguration();
  39.         clientConfiguration.setMaxConnections(100);
  40.         AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration(
  41.                 fileProperties.getOss().getEndpoint(), fileProperties.getOss().getRegion());
  42.         AWSCredentials awsCredentials = new BasicAWSCredentials(fileProperties.getOss().getAccessKey(),
  43.                 fileProperties.getOss().getSecretKey());
  44.         AWSCredentialsProvider awsCredentialsProvider = new AWSStaticCredentialsProvider(awsCredentials);
  45.         this.amazonS3 = AmazonS3ClientBuilder.standard()
  46.                 .withEndpointConfiguration(endpointConfiguration)
  47.                 .withClientConfiguration(clientConfiguration)
  48.                 .withCredentials(awsCredentialsProvider)
  49.                 .disableChunkedEncoding()
  50.                 .withPathStyleAccessEnabled(true)
  51.                 .build();
  52.     }
  53.     /**
  54.      * 获取 Minio 中已经上传的分片文件
  55.      * @param object 文件名称
  56.      * @param uploadId 上传的文件id(由 minio 生成)
  57.      * @return List<Integer>
  58.      */
  59.     @SneakyThrows
  60.     public List<Integer> getListParts(String object, String uploadId) {
  61.         ListPartsRequest listPartsRequest = new ListPartsRequest( fileProperties.getBucketName(), object, uploadId);
  62.         PartListing listParts = amazonS3.listParts(listPartsRequest);
  63.         return listParts.getParts().stream().map(PartSummary::getPartNumber).collect(Collectors.toList());
  64.     }
  65.     /**
  66.      * 单文件签名上传
  67.      * @param object 文件名称(uuid 格式)
  68.      * @return UploadUrlsVO
  69.      */
  70.     public UploadUrlsVO getUploadObjectUrl(String contentType, String object) {
  71.         try {
  72.             log.info("<{}> 开始单文件上传<>", object);
  73.             UploadUrlsVO urlsVO = new UploadUrlsVO();
  74.             List<String> urlList = new ArrayList<>();
  75.             // 主要是针对图片,若需要通过浏览器直接查看,而不是下载,需要指定对应的 content-type
  76.             HashMultimap<String, String> headers = HashMultimap.create();
  77.             if (contentType == null || contentType.equals("")) {
  78.                 contentType = "application/octet-stream";
  79.             }
  80.             headers.put("Content-Type", contentType);
  81.             String uploadId = IdUtil.simpleUUID();
  82.             Map<String, String> reqParams = new HashMap<>();
  83.             reqParams.put("uploadId", uploadId);
  84.             //生成预签名的 URL
  85.             GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(
  86.                     fileProperties.getBucketName(),
  87.                     object, HttpMethod.PUT);
  88.             generatePresignedUrlRequest.addRequestParameter("uploadId", uploadId);
  89.             URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest);
  90.             urlList.add(url.toString());
  91.             urlsVO.setUploadId(uploadId).setUrls(urlList);
  92.             return urlsVO;
  93.         } catch (Exception e) {
  94.             log.error("单文件上传失败: {}", e.getMessage());
  95.             throw new RuntimeException(FileHttpCodeEnum.UPLOAD_FILE_FAILED.getMsg());
  96.         }
  97.     }
  98.     /**
  99.      * 初始化分片上传
  100.      * @param fileUploadInfo 前端传入的文件信息
  101.      * @param object object
  102.      * @return UploadUrlsVO
  103.      */
  104.     public UploadUrlsVO initMultiPartUpload(FileUploadInfo fileUploadInfo, String object) {
  105.         Integer chunkCount = fileUploadInfo.getChunkCount();
  106.         String contentType = fileUploadInfo.getContentType();
  107.         String uploadId = fileUploadInfo.getUploadId();
  108.         log.info("文件<{}> - 分片<{}> 初始化分片上传数据 请求头 {}", object, chunkCount, contentType);
  109.         UploadUrlsVO urlsVO = new UploadUrlsVO();
  110.         try {
  111.             // 如果初始化时有 uploadId,说明是断点续传,不能重新生成 uploadId
  112.             if (uploadId == null || uploadId.equals("")) {
  113.                 // 第一步,初始化,声明下面将有一个 Multipart Upload
  114.                 // 设置文件类型
  115.                 ObjectMetadata metadata = new ObjectMetadata();
  116.                 metadata.setContentType(contentType);
  117.                 InitiateMultipartUploadRequest initRequest = new InitiateMultipartUploadRequest(fileProperties.getBucketName(),
  118.                         object, metadata);
  119.                 uploadId = amazonS3.initiateMultipartUpload(initRequest).getUploadId();
  120.                 log.info("没有uploadId,生成新的{}",uploadId);
  121.             }
  122.             urlsVO.setUploadId(uploadId);
  123.             List<String> partList = new ArrayList<>();
  124.             for (int i = 1; i <= chunkCount; i++) {
  125.                 //生成预签名的 URL
  126.                 //设置过期时间,例如 1 小时后
  127.                 Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000);
  128.                 GeneratePresignedUrlRequest generatePresignedUrlRequest =
  129.                         new GeneratePresignedUrlRequest(fileProperties.getBucketName(), object,HttpMethod.PUT)
  130.                                 .withExpiration(expiration);
  131.                 generatePresignedUrlRequest.addRequestParameter("uploadId", uploadId);
  132.                 generatePresignedUrlRequest.addRequestParameter("partNumber", String.valueOf(i));
  133.                 URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest);
  134.                 partList.add(url.toString());
  135.             }
  136.             log.info("文件初始化分片成功");
  137.             urlsVO.setUrls(partList);
  138.             return urlsVO;
  139.         } catch (Exception e) {
  140.             log.error("初始化分片上传失败: {}", e.getMessage());
  141.             // 返回 文件上传失败
  142.             throw new RuntimeException(FileHttpCodeEnum.UPLOAD_FILE_FAILED.getMsg());
  143.         }
  144.     }
  145.     /**
  146.      * 合并文件
  147.      * @param object object
  148.      * @param uploadId uploadUd
  149.      */
  150.     @SneakyThrows
  151.     public boolean mergeMultipartUpload(String object, String uploadId) {
  152.         log.info("通过 <{}-{}-{}> 合并<分片上传>数据", object, uploadId, fileProperties.getBucketName());
  153.         //构建查询parts条件
  154.         ListPartsRequest listPartsRequest = new ListPartsRequest(
  155.                 fileProperties.getBucketName(),
  156.                 object,
  157.                 uploadId);
  158.         listPartsRequest.setMaxParts(1000);
  159.         listPartsRequest.setPartNumberMarker(0);
  160.         //请求查询
  161.         PartListing partList=amazonS3.listParts(listPartsRequest);
  162.         List<PartSummary> parts = partList.getParts();
  163.         if (parts==null|| parts.isEmpty()) {
  164.             // 已上传分块数量与记录中的数量不对应,不能合并分块
  165.             throw new RuntimeException("分片缺失,请重新上传");
  166.         }
  167.         // 合并分片
  168.         CompleteMultipartUploadRequest compRequest = new CompleteMultipartUploadRequest(
  169.                 fileProperties.getBucketName(),
  170.                 object,
  171.                 uploadId,
  172.                 parts.stream().map(partSummary -> new PartETag(partSummary.getPartNumber(), partSummary.getETag())).collect(Collectors.toList()));
  173.         amazonS3.completeMultipartUpload(compRequest);
  174.         return true;
  175.     }
  176.     /**
  177.      * 获取文件内容和元信息,该文件不存在会抛异常
  178.      * @param object object
  179.      * @return StatObjectResponse
  180.      */
  181.     @SneakyThrows
  182.     public S3Object statObject(String object) {
  183.         return amazonS3.getObject(fileProperties.getBucketName(), object);
  184.     }
  185.     @SneakyThrows
  186.     public S3Object getObject(String object, Long offset, Long contentLength) {
  187.         GetObjectRequest request = new GetObjectRequest(fileProperties.getBucketName(), object);
  188.         request.setRange(offset, offset + contentLength - 1);  // 设置偏移量和长度
  189.         return amazonS3.getObject(request);
  190.     }
  191. }
复制代码
minioUtil
  1. import cn.hutool.core.util.IdUtil;
  2. import cn.superlu.s3uploadservice.config.CustomMinioClient;
  3. import cn.superlu.s3uploadservice.config.FileProperties;
  4. import cn.superlu.s3uploadservice.constant.FileHttpCodeEnum;
  5. import cn.superlu.s3uploadservice.model.bo.FileUploadInfo;
  6. import cn.superlu.s3uploadservice.model.vo.UploadUrlsVO;
  7. import com.google.common.collect.HashMultimap;
  8. import io.minio.*;
  9. import io.minio.http.Method;
  10. import io.minio.messages.Part;
  11. import jakarta.annotation.PostConstruct;
  12. import jakarta.annotation.Resource;
  13. import lombok.SneakyThrows;
  14. import lombok.extern.slf4j.Slf4j;
  15. import org.springframework.stereotype.Component;
  16. import java.util.*;
  17. import java.util.concurrent.TimeUnit;
  18. import java.util.stream.Collectors;
  19. @Slf4j
  20. @Component
  21. public class MinioUtil {
  22.     private CustomMinioClient customMinioClient;
  23.     @Resource
  24.     private FileProperties fileProperties;
  25.     // spring自动注入会失败
  26.     @PostConstruct
  27.     public void init() {
  28.         MinioAsyncClient minioClient = MinioAsyncClient.builder()
  29.                 .endpoint(fileProperties.getOss().getEndpoint())
  30.                 .credentials(fileProperties.getOss().getAccessKey(), fileProperties.getOss().getSecretKey())
  31.                 .build();
  32.         customMinioClient = new CustomMinioClient(minioClient);
  33.     }
  34.     /**
  35.      * 获取 Minio 中已经上传的分片文件
  36.      * @param object 文件名称
  37.      * @param uploadId 上传的文件id(由 minio 生成)
  38.      * @return List<Integer>
  39.      */
  40.     @SneakyThrows
  41.     public List<Integer> getListParts(String object, String uploadId) {
  42.         ListPartsResponse partResult = customMinioClient.listMultipart(fileProperties.getBucketName(), null, object, 1000, 0, uploadId, null, null);
  43.         return partResult.result().partList().stream()
  44.                 .map(Part::partNumber)
  45.                 .collect(Collectors.toList());
  46.     }
  47.     /**
  48.      * 单文件签名上传
  49.      * @param object 文件名称(uuid 格式)
  50.      * @return UploadUrlsVO
  51.      */
  52.     public UploadUrlsVO getUploadObjectUrl(String contentType, String object) {
  53.         try {
  54.             log.info("<{}> 开始单文件上传<minio>", object);
  55.             UploadUrlsVO urlsVO = new UploadUrlsVO();
  56.             List<String> urlList = new ArrayList<>();
  57.             // 主要是针对图片,若需要通过浏览器直接查看,而不是下载,需要指定对应的 content-type
  58.             HashMultimap<String, String> headers = HashMultimap.create();
  59.             if (contentType == null || contentType.equals("")) {
  60.                 contentType = "application/octet-stream";
  61.             }
  62.             headers.put("Content-Type", contentType);
  63.             String uploadId = IdUtil.simpleUUID();
  64.             Map<String, String> reqParams = new HashMap<>();
  65.             reqParams.put("uploadId", uploadId);
  66.             String url = customMinioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
  67.                     .method(Method.PUT)
  68.                     .bucket(fileProperties.getBucketName())
  69.                     .object(object)
  70.                     .extraHeaders(headers)
  71.                     .extraQueryParams(reqParams)
  72.                     .expiry(fileProperties.getOss().getExpiry(), TimeUnit.DAYS)
  73.                     .build());
  74.             urlList.add(url);
  75.             urlsVO.setUploadId(uploadId).setUrls(urlList);
  76.             return urlsVO;
  77.         } catch (Exception e) {
  78.             log.error("单文件上传失败: {}", e.getMessage());
  79.             throw new RuntimeException(FileHttpCodeEnum.UPLOAD_FILE_FAILED.getMsg());
  80.         }
  81.     }
  82.     /**
  83.      * 初始化分片上传
  84.      * @param fileUploadInfo 前端传入的文件信息
  85.      * @param object object
  86.      * @return UploadUrlsVO
  87.      */
  88.     public UploadUrlsVO initMultiPartUpload(FileUploadInfo fileUploadInfo, String object) {
  89.         Integer chunkCount = fileUploadInfo.getChunkCount();
  90.         String contentType = fileUploadInfo.getContentType();
  91.         String uploadId = fileUploadInfo.getUploadId();
  92.         log.info("文件<{}> - 分片<{}> 初始化分片上传数据 请求头 {}", object, chunkCount, contentType);
  93.         UploadUrlsVO urlsVO = new UploadUrlsVO();
  94.         try {
  95.             HashMultimap<String, String> headers = HashMultimap.create();
  96.             if (contentType == null || contentType.equals("")) {
  97.                 contentType = "application/octet-stream";
  98.             }
  99.             headers.put("Content-Type", contentType);
  100.             // 如果初始化时有 uploadId,说明是断点续传,不能重新生成 uploadId
  101.             if (fileUploadInfo.getUploadId() == null || fileUploadInfo.getUploadId().equals("")) {
  102.                 uploadId = customMinioClient.initMultiPartUpload(fileProperties.getBucketName(), null, object, headers, null);
  103.             }
  104.             urlsVO.setUploadId(uploadId);
  105.             List<String> partList = new ArrayList<>();
  106.             Map<String, String> reqParams = new HashMap<>();
  107.             reqParams.put("uploadId", uploadId);
  108.             for (int i = 1; i <= chunkCount; i++) {
  109.                 reqParams.put("partNumber", String.valueOf(i));
  110.                 String uploadUrl = customMinioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
  111.                         .method(Method.PUT)
  112.                         .bucket(fileProperties.getBucketName())
  113.                         .object(object)
  114.                         .expiry(1, TimeUnit.DAYS)
  115.                         .extraQueryParams(reqParams)
  116.                         .build());
  117.                 partList.add(uploadUrl);
  118.             }
  119.             log.info("文件初始化分片成功");
  120.             urlsVO.setUrls(partList);
  121.             return urlsVO;
  122.         } catch (Exception e) {
  123.             log.error("初始化分片上传失败: {}", e.getMessage());
  124.             // 返回 文件上传失败
  125.             throw new RuntimeException(FileHttpCodeEnum.UPLOAD_FILE_FAILED.getMsg());
  126.         }
  127.     }
  128.     /**
  129.      * 合并文件
  130.      * @param object object
  131.      * @param uploadId uploadUd
  132.      */
  133.     @SneakyThrows
  134.     public boolean mergeMultipartUpload(String object, String uploadId) {
  135.         log.info("通过 <{}-{}-{}> 合并<分片上传>数据", object, uploadId, fileProperties.getBucketName());
  136.         //目前仅做了最大1000分片
  137.         Part[] parts = new Part[1000];
  138.         // 查询上传后的分片数据
  139.         ListPartsResponse partResult = customMinioClient.listMultipart(fileProperties.getBucketName(), null, object, 1000, 0, uploadId, null, null);
  140.         int partNumber = 1;
  141.         for (Part part : partResult.result().partList()) {
  142.             parts[partNumber - 1] = new Part(partNumber, part.etag());
  143.             partNumber++;
  144.         }
  145.         // 合并分片
  146.         customMinioClient.mergeMultipartUpload(fileProperties.getBucketName(), null, object, uploadId, parts, null, null);
  147.         return true;
  148.     }
  149.     /**
  150.      * 获取文件内容和元信息,该文件不存在会抛异常
  151.      * @param object object
  152.      * @return StatObjectResponse
  153.      */
  154.     @SneakyThrows
  155.     public StatObjectResponse statObject(String object) {
  156.         return customMinioClient.statObject(StatObjectArgs.builder()
  157.                 .bucket(fileProperties.getBucketName())
  158.                 .object(object)
  159.                 .build())
  160.                 .get();
  161.     }
  162.     @SneakyThrows
  163.     public GetObjectResponse getObject(String object, Long offset, Long contentLength) {
  164.         return customMinioClient.getObject(GetObjectArgs.builder()
  165.                 .bucket(fileProperties.getBucketName())
  166.                 .object(object)
  167.                 .offset(offset)
  168.                 .length(contentLength)
  169.                 .build())
  170.                 .get();
  171.     }
  172. }
复制代码
四、疑问

我在全部使用aws-s3上传时出现一个问题至今没有办法解决。只能在查询分片的时间用minio的包进行。
分片后调用amazonS3.listParts()一直超时。
这个问题我在
https://gitee.com/Gary2016/minio-upload/issues/I8H8GM
也看到有人跟我有雷同的问题
有解决的朋友贫苦评论区告知下方法。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

雁过留声

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表