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

文章目录

  • 前言
  • 一、功能展示
    • 上传功能点
    • 下载功能点
    • 效果展示
  • 二、思路流程
    • 上传流程
    • 下载流程
  • 三、代码示例
  • 四、疑问

前言

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

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.superlu.s3uploadservice.common.R;
import cn.superlu.s3uploadservice.config.FileProperties;
import cn.superlu.s3uploadservice.constant.FileHttpCodeEnum;
import cn.superlu.s3uploadservice.mapper.SysFileUploadMapper;
import cn.superlu.s3uploadservice.model.bo.FileUploadInfo;
import cn.superlu.s3uploadservice.model.entity.SysFileUpload;
import cn.superlu.s3uploadservice.model.vo.BaseFileVo;
import cn.superlu.s3uploadservice.model.vo.UploadUrlsVO;
import cn.superlu.s3uploadservice.service.SysFileUploadService;
import cn.superlu.s3uploadservice.utils.AmazonS3Util;
import cn.superlu.s3uploadservice.utils.MinioUtil;
import cn.superlu.s3uploadservice.utils.RedisUtil;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectInputStream;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;import java.io.BufferedOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;@Service
@Slf4j
@RequiredArgsConstructor
public class SysFileUploadServiceImpl extends ServiceImpl<SysFileUploadMapper, SysFileUpload> implements SysFileUploadService {private static final Integer BUFFER_SIZE = 1024 * 64; // 64KBprivate final RedisUtil redisUtil;private final MinioUtil minioUtil;private final AmazonS3Util amazonS3Util;private final FileProperties fileProperties;/*** 检查文件是否存在* @param md5* @return*/@Overridepublic R<BaseFileVo<FileUploadInfo>> checkFileByMd5(String md5) {log.info("查询md5: <{}> 在redis是否存在", md5);FileUploadInfo fileUploadInfo = (FileUploadInfo)redisUtil.get(md5);if (fileUploadInfo != null) {log.info("查询到md5:在redis中存在:{}", JSONUtil.toJsonStr(fileUploadInfo));if(fileUploadInfo.getChunkCount()==1){return R.ok( BaseFileVo.builder(FileHttpCodeEnum.NOT_UPLOADED, null));}else{List<Integer> listParts = minioUtil.getListParts(fileUploadInfo.getObject(), fileUploadInfo.getUploadId());
//              List<Integer> listParts = amazonS3Util.getListParts(fileUploadInfo.getObject(), fileUploadInfo.getUploadId());fileUploadInfo.setListParts(listParts);return R.ok( BaseFileVo.builder(FileHttpCodeEnum.UPLOADING, fileUploadInfo));}}log.info("redis中不存在md5: <{}> 查询mysql是否存在", md5);SysFileUpload file = baseMapper.selectOne(new LambdaQueryWrapper<SysFileUpload>().eq(SysFileUpload::getMd5, md5));if (file != null) {log.info("mysql中存在md5: <{}> 的文件 该文件已上传至minio 秒传直接过", md5);FileUploadInfo dbFileInfo = BeanUtil.toBean(file, FileUploadInfo.class);return R.ok( BaseFileVo.builder(FileHttpCodeEnum.UPLOAD_SUCCESS, dbFileInfo));}return R.ok( BaseFileVo.builder(FileHttpCodeEnum.NOT_UPLOADED, null));}/*** 初始化文件分片地址及相关数据* @param fileUploadInfo* @return*/@Overridepublic R<BaseFileVo<UploadUrlsVO>> initMultipartUpload(FileUploadInfo fileUploadInfo) {log.info("查询md5: <{}> 在redis是否存在", fileUploadInfo.getMd5());FileUploadInfo redisFileUploadInfo = (FileUploadInfo)redisUtil.get(fileUploadInfo.getMd5());// 若 redis 中有该 md5 的记录,以 redis 中为主String object;if (redisFileUploadInfo != null) {fileUploadInfo = redisFileUploadInfo;object = redisFileUploadInfo.getObject();} else {String originFileName = fileUploadInfo.getOriginFileName();String suffix = FileUtil.extName(originFileName);String fileName = FileUtil.mainName(originFileName);// 对文件重命名,并以年月日文件夹格式存储String nestFile = DateUtil.format(LocalDateTime.now(), "yyyy/MM/dd");object = nestFile + "/" + fileName + "_" + fileUploadInfo.getMd5() + "." + suffix;fileUploadInfo.setObject(object).setType(suffix);}UploadUrlsVO urlsVO;// 单文件上传if (fileUploadInfo.getChunkCount() == 1) {log.info("当前分片数量 <{}> 单文件上传", fileUploadInfo.getChunkCount());
//            urlsVO = minioUtil.getUploadObjectUrl(fileUploadInfo.getContentType(), object);urlsVO=amazonS3Util.getUploadObjectUrl(fileUploadInfo.getContentType(), object);} else {// 分片上传log.info("当前分片数量 <{}> 分片上传", fileUploadInfo.getChunkCount());
//            urlsVO = minioUtil.initMultiPartUpload(fileUploadInfo, object);urlsVO = amazonS3Util.initMultiPartUpload(fileUploadInfo, object);}fileUploadInfo.setUploadId(urlsVO.getUploadId());// 存入 redis (单片存 redis 唯一用处就是可以让单片也入库,因为单片只有一个请求,基本不会出现问题)redisUtil.set(fileUploadInfo.getMd5(), fileUploadInfo, fileProperties.getOss().getBreakpointTime(), TimeUnit.DAYS);return R.ok(BaseFileVo.builder(FileHttpCodeEnum.SUCCESS, urlsVO));}/*** 合并分片* @param md5* @return*/@Overridepublic R<BaseFileVo<String>> mergeMultipartUpload(String md5) {FileUploadInfo redisFileUploadInfo = (FileUploadInfo)redisUtil.get(md5);String url = StrUtil.format("{}/{}/{}", fileProperties.getOss().getEndpoint(), fileProperties.getBucketName(), redisFileUploadInfo.getObject());SysFileUpload files = BeanUtil.toBean(redisFileUploadInfo, SysFileUpload.class);files.setUrl(url).setBucket(fileProperties.getBucketName()).setCreateTime(LocalDateTime.now());Integer chunkCount = redisFileUploadInfo.getChunkCount();// 分片为 1 ,不需要合并,否则合并后看返回的是 true 还是 falseboolean isSuccess = chunkCount == 1 || minioUtil.mergeMultipartUpload(redisFileUploadInfo.getObject(), redisFileUploadInfo.getUploadId());
//        boolean isSuccess = chunkCount == 1 || amazonS3Util.mergeMultipartUpload(redisFileUploadInfo.getObject(), redisFileUploadInfo.getUploadId());if (isSuccess) {baseMapper.insert(files);redisUtil.del(md5);return R.ok(BaseFileVo.builder(FileHttpCodeEnum.SUCCESS, url));}return R.ok(BaseFileVo.builder(FileHttpCodeEnum.UPLOAD_FILE_FAILED, null));}/*** 分片下载* @param id* @param request* @param response* @return* @throws IOException*/@Overridepublic ResponseEntity<byte[]> downloadMultipartFile(Long id, HttpServletRequest request, HttpServletResponse response) throws IOException {// redis 缓存当前文件信息,避免分片下载时频繁查库SysFileUpload file = null;SysFileUpload redisFile = (SysFileUpload)redisUtil.get(String.valueOf(id));if (redisFile == null) {SysFileUpload dbFile = baseMapper.selectById(id);if (dbFile == null) {return null;} else {file = dbFile;redisUtil.set(String.valueOf(id), file, 1, TimeUnit.DAYS);}} else {file = redisFile;}String range = request.getHeader("Range");String fileName = file.getOriginFileName();log.info("下载文件的 object <{}>", file.getObject());// 获取 bucket 桶中的文件元信息,获取不到会抛出异常
//        StatObjectResponse objectResponse = minioUtil.statObject(file.getObject());S3Object s3Object = amazonS3Util.statObject(file.getObject());long startByte = 0; // 开始下载位置
//        long fileSize = objectResponse.size();long fileSize = s3Object.getObjectMetadata().getContentLength();long endByte = fileSize - 1; // 结束下载位置log.info("文件总长度:{},当前 range:{}", fileSize, range);BufferedOutputStream os = null; // buffer 写入流
//        GetObjectResponse stream = null; // minio 文件流// 存在 range,需要根据前端下载长度进行下载,即分段下载// 例如:range=bytes=0-52428800if (range != null && range.contains("bytes=") && range.contains("-")) {range = range.substring(range.lastIndexOf("=") + 1).trim(); // 0-52428800String[] ranges = range.split("-");// 判断range的类型if (ranges.length == 1) {// 类型一:bytes=-2343 后端转换为 0-2343if (range.startsWith("-")) endByte = Long.parseLong(ranges[0]);// 类型二:bytes=2343- 后端转换为 2343-最后if (range.endsWith("-")) startByte = Long.parseLong(ranges[0]);} else if (ranges.length == 2) { // 类型三:bytes=22-2343startByte = Long.parseLong(ranges[0]);endByte = Long.parseLong(ranges[1]);}}// 要下载的长度// 确保返回的 contentLength 不会超过文件的实际剩余大小long contentLength = Math.min(endByte - startByte + 1, fileSize - startByte);// 文件类型String contentType = request.getServletContext().getMimeType(fileName);// 解决下载文件时文件名乱码问题byte[] fileNameBytes = fileName.getBytes(StandardCharsets.UTF_8);fileName = new String(fileNameBytes, 0, fileNameBytes.length, StandardCharsets.ISO_8859_1);// 响应头设置---------------------------------------------------------------------------------------------// 断点续传,获取部分字节内容:response.setHeader("Accept-Ranges", "bytes");// http状态码要为206:表示获取部分内容,SC_PARTIAL_CONTENT,若部分浏览器不支持,改成 SC_OKresponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);response.setContentType(contentType);
//        response.setHeader("Last-Modified", objectResponse.lastModified().toString());response.setHeader("Last-Modified", s3Object.getObjectMetadata().getLastModified().toString());response.setHeader("Content-Disposition", "attachment;filename=" + fileName);response.setHeader("Content-Length", String.valueOf(contentLength));// Content-Range,格式为:[要下载的开始位置]-[结束位置]/[文件总大小]response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + fileSize);
//        response.setHeader("ETag", "\"".concat(objectResponse.etag()).concat("\""));response.setHeader("ETag", "\"".concat(s3Object.getObjectMetadata().getETag()).concat("\""));response.setContentType("application/octet-stream;charset=UTF-8");S3ObjectInputStream objectInputStream=null;try {// 获取文件流String object = s3Object.getKey();S3Object currentObject = amazonS3Util.getObject(object, startByte, contentLength);objectInputStream = currentObject.getObjectContent();
//            stream = minioUtil.getObject(objectResponse.object(), startByte, contentLength);os = new BufferedOutputStream(response.getOutputStream());// 将读取的文件写入到 OutputStreambyte[] bytes = new byte[BUFFER_SIZE];long bytesWritten = 0;int bytesRead = -1;while ((bytesRead = objectInputStream.read(bytes)) != -1) {
//            while ((bytesRead = stream.read(bytes)) != -1) {if (bytesWritten + bytesRead >= contentLength) {os.write(bytes, 0, (int)(contentLength - bytesWritten));break;} else {os.write(bytes, 0, bytesRead);bytesWritten += bytesRead;}}os.flush();response.flushBuffer();// 返回对应http状态return new ResponseEntity<>(bytes, HttpStatus.OK);} catch (Exception e) {e.printStackTrace();} finally {if (os != null) os.close();
//            if (stream != null) stream.close();if (objectInputStream != null) objectInputStream.close();}return null;}@Overridepublic R<List<SysFileUpload>> getFileList() {List<SysFileUpload> filesList = this.list();return R.ok(filesList);}}

AmazonS3Util


import cn.hutool.core.util.IdUtil;
import cn.superlu.s3uploadservice.config.FileProperties;
import cn.superlu.s3uploadservice.constant.FileHttpCodeEnum;
import cn.superlu.s3uploadservice.model.bo.FileUploadInfo;
import cn.superlu.s3uploadservice.model.vo.UploadUrlsVO;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.HttpMethod;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.*;
import com.google.common.collect.HashMultimap;
import io.minio.GetObjectArgs;
import io.minio.GetObjectResponse;
import io.minio.StatObjectArgs;
import io.minio.StatObjectResponse;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;@Slf4j
@Component
public class AmazonS3Util {@Resourceprivate FileProperties fileProperties;private AmazonS3 amazonS3;// spring自动注入会失败@PostConstructpublic void init() {ClientConfiguration clientConfiguration = new ClientConfiguration();clientConfiguration.setMaxConnections(100);AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration(fileProperties.getOss().getEndpoint(), fileProperties.getOss().getRegion());AWSCredentials awsCredentials = new BasicAWSCredentials(fileProperties.getOss().getAccessKey(),fileProperties.getOss().getSecretKey());AWSCredentialsProvider awsCredentialsProvider = new AWSStaticCredentialsProvider(awsCredentials);this.amazonS3 = AmazonS3ClientBuilder.standard().withEndpointConfiguration(endpointConfiguration).withClientConfiguration(clientConfiguration).withCredentials(awsCredentialsProvider).disableChunkedEncoding().withPathStyleAccessEnabled(true).build();}/*** 获取 Minio 中已经上传的分片文件* @param object 文件名称* @param uploadId 上传的文件id(由 minio 生成)* @return List<Integer>*/@SneakyThrowspublic List<Integer> getListParts(String object, String uploadId) {ListPartsRequest listPartsRequest = new ListPartsRequest( fileProperties.getBucketName(), object, uploadId);PartListing listParts = amazonS3.listParts(listPartsRequest);return listParts.getParts().stream().map(PartSummary::getPartNumber).collect(Collectors.toList());}/*** 单文件签名上传* @param object 文件名称(uuid 格式)* @return UploadUrlsVO*/public UploadUrlsVO getUploadObjectUrl(String contentType, String object) {try {log.info("<{}> 开始单文件上传<>", object);UploadUrlsVO urlsVO = new UploadUrlsVO();List<String> urlList = new ArrayList<>();// 主要是针对图片,若需要通过浏览器直接查看,而不是下载,需要指定对应的 content-typeHashMultimap<String, String> headers = HashMultimap.create();if (contentType == null || contentType.equals("")) {contentType = "application/octet-stream";}headers.put("Content-Type", contentType);String uploadId = IdUtil.simpleUUID();Map<String, String> reqParams = new HashMap<>();reqParams.put("uploadId", uploadId);//生成预签名的 URLGeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(fileProperties.getBucketName(),object, HttpMethod.PUT);generatePresignedUrlRequest.addRequestParameter("uploadId", uploadId);URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest);urlList.add(url.toString());urlsVO.setUploadId(uploadId).setUrls(urlList);return urlsVO;} catch (Exception e) {log.error("单文件上传失败: {}", e.getMessage());throw new RuntimeException(FileHttpCodeEnum.UPLOAD_FILE_FAILED.getMsg());}}/*** 初始化分片上传* @param fileUploadInfo 前端传入的文件信息* @param object object* @return UploadUrlsVO*/public UploadUrlsVO initMultiPartUpload(FileUploadInfo fileUploadInfo, String object) {Integer chunkCount = fileUploadInfo.getChunkCount();String contentType = fileUploadInfo.getContentType();String uploadId = fileUploadInfo.getUploadId();log.info("文件<{}> - 分片<{}> 初始化分片上传数据 请求头 {}", object, chunkCount, contentType);UploadUrlsVO urlsVO = new UploadUrlsVO();try {// 如果初始化时有 uploadId,说明是断点续传,不能重新生成 uploadIdif (uploadId == null || uploadId.equals("")) {// 第一步,初始化,声明下面将有一个 Multipart Upload// 设置文件类型ObjectMetadata metadata = new ObjectMetadata();metadata.setContentType(contentType);InitiateMultipartUploadRequest initRequest = new InitiateMultipartUploadRequest(fileProperties.getBucketName(),object, metadata);uploadId = amazonS3.initiateMultipartUpload(initRequest).getUploadId();log.info("没有uploadId,生成新的{}",uploadId);}urlsVO.setUploadId(uploadId);List<String> partList = new ArrayList<>();for (int i = 1; i <= chunkCount; i++) {//生成预签名的 URL//设置过期时间,例如 1 小时后Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000);GeneratePresignedUrlRequest generatePresignedUrlRequest =new GeneratePresignedUrlRequest(fileProperties.getBucketName(), object,HttpMethod.PUT).withExpiration(expiration);generatePresignedUrlRequest.addRequestParameter("uploadId", uploadId);generatePresignedUrlRequest.addRequestParameter("partNumber", String.valueOf(i));URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest);partList.add(url.toString());}log.info("文件初始化分片成功");urlsVO.setUrls(partList);return urlsVO;} catch (Exception e) {log.error("初始化分片上传失败: {}", e.getMessage());// 返回 文件上传失败throw new RuntimeException(FileHttpCodeEnum.UPLOAD_FILE_FAILED.getMsg());}}/*** 合并文件* @param object object* @param uploadId uploadUd*/@SneakyThrowspublic boolean mergeMultipartUpload(String object, String uploadId) {log.info("通过 <{}-{}-{}> 合并<分片上传>数据", object, uploadId, fileProperties.getBucketName());//构建查询parts条件ListPartsRequest listPartsRequest = new ListPartsRequest(fileProperties.getBucketName(),object,uploadId);listPartsRequest.setMaxParts(1000);listPartsRequest.setPartNumberMarker(0);//请求查询PartListing partList=amazonS3.listParts(listPartsRequest);List<PartSummary> parts = partList.getParts();if (parts==null|| parts.isEmpty()) {// 已上传分块数量与记录中的数量不对应,不能合并分块throw new RuntimeException("分片缺失,请重新上传");}// 合并分片CompleteMultipartUploadRequest compRequest = new CompleteMultipartUploadRequest(fileProperties.getBucketName(),object,uploadId,parts.stream().map(partSummary -> new PartETag(partSummary.getPartNumber(), partSummary.getETag())).collect(Collectors.toList()));amazonS3.completeMultipartUpload(compRequest);return true;}/*** 获取文件内容和元信息,该文件不存在会抛异常* @param object object* @return StatObjectResponse*/@SneakyThrowspublic S3Object statObject(String object) {return amazonS3.getObject(fileProperties.getBucketName(), object);}@SneakyThrowspublic S3Object getObject(String object, Long offset, Long contentLength) {GetObjectRequest request = new GetObjectRequest(fileProperties.getBucketName(), object);request.setRange(offset, offset + contentLength - 1);  // 设置偏移量和长度return amazonS3.getObject(request);}}

minioUtil

import cn.hutool.core.util.IdUtil;
import cn.superlu.s3uploadservice.config.CustomMinioClient;
import cn.superlu.s3uploadservice.config.FileProperties;
import cn.superlu.s3uploadservice.constant.FileHttpCodeEnum;
import cn.superlu.s3uploadservice.model.bo.FileUploadInfo;
import cn.superlu.s3uploadservice.model.vo.UploadUrlsVO;
import com.google.common.collect.HashMultimap;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Part;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;@Slf4j
@Component
public class MinioUtil {private CustomMinioClient customMinioClient;@Resourceprivate FileProperties fileProperties;// spring自动注入会失败@PostConstructpublic void init() {MinioAsyncClient minioClient = MinioAsyncClient.builder().endpoint(fileProperties.getOss().getEndpoint()).credentials(fileProperties.getOss().getAccessKey(), fileProperties.getOss().getSecretKey()).build();customMinioClient = new CustomMinioClient(minioClient);}/*** 获取 Minio 中已经上传的分片文件* @param object 文件名称* @param uploadId 上传的文件id(由 minio 生成)* @return List<Integer>*/@SneakyThrowspublic List<Integer> getListParts(String object, String uploadId) {ListPartsResponse partResult = customMinioClient.listMultipart(fileProperties.getBucketName(), null, object, 1000, 0, uploadId, null, null);return partResult.result().partList().stream().map(Part::partNumber).collect(Collectors.toList());}/*** 单文件签名上传* @param object 文件名称(uuid 格式)* @return UploadUrlsVO*/public UploadUrlsVO getUploadObjectUrl(String contentType, String object) {try {log.info("<{}> 开始单文件上传<minio>", object);UploadUrlsVO urlsVO = new UploadUrlsVO();List<String> urlList = new ArrayList<>();// 主要是针对图片,若需要通过浏览器直接查看,而不是下载,需要指定对应的 content-typeHashMultimap<String, String> headers = HashMultimap.create();if (contentType == null || contentType.equals("")) {contentType = "application/octet-stream";}headers.put("Content-Type", contentType);String uploadId = IdUtil.simpleUUID();Map<String, String> reqParams = new HashMap<>();reqParams.put("uploadId", uploadId);String url = customMinioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.PUT).bucket(fileProperties.getBucketName()).object(object).extraHeaders(headers).extraQueryParams(reqParams).expiry(fileProperties.getOss().getExpiry(), TimeUnit.DAYS).build());urlList.add(url);urlsVO.setUploadId(uploadId).setUrls(urlList);return urlsVO;} catch (Exception e) {log.error("单文件上传失败: {}", e.getMessage());throw new RuntimeException(FileHttpCodeEnum.UPLOAD_FILE_FAILED.getMsg());}}/*** 初始化分片上传* @param fileUploadInfo 前端传入的文件信息* @param object object* @return UploadUrlsVO*/public UploadUrlsVO initMultiPartUpload(FileUploadInfo fileUploadInfo, String object) {Integer chunkCount = fileUploadInfo.getChunkCount();String contentType = fileUploadInfo.getContentType();String uploadId = fileUploadInfo.getUploadId();log.info("文件<{}> - 分片<{}> 初始化分片上传数据 请求头 {}", object, chunkCount, contentType);UploadUrlsVO urlsVO = new UploadUrlsVO();try {HashMultimap<String, String> headers = HashMultimap.create();if (contentType == null || contentType.equals("")) {contentType = "application/octet-stream";}headers.put("Content-Type", contentType);// 如果初始化时有 uploadId,说明是断点续传,不能重新生成 uploadIdif (fileUploadInfo.getUploadId() == null || fileUploadInfo.getUploadId().equals("")) {uploadId = customMinioClient.initMultiPartUpload(fileProperties.getBucketName(), null, object, headers, null);}urlsVO.setUploadId(uploadId);List<String> partList = new ArrayList<>();Map<String, String> reqParams = new HashMap<>();reqParams.put("uploadId", uploadId);for (int i = 1; i <= chunkCount; i++) {reqParams.put("partNumber", String.valueOf(i));String uploadUrl = customMinioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.PUT).bucket(fileProperties.getBucketName()).object(object).expiry(1, TimeUnit.DAYS).extraQueryParams(reqParams).build());partList.add(uploadUrl);}log.info("文件初始化分片成功");urlsVO.setUrls(partList);return urlsVO;} catch (Exception e) {log.error("初始化分片上传失败: {}", e.getMessage());// 返回 文件上传失败throw new RuntimeException(FileHttpCodeEnum.UPLOAD_FILE_FAILED.getMsg());}}/*** 合并文件* @param object object* @param uploadId uploadUd*/@SneakyThrowspublic boolean mergeMultipartUpload(String object, String uploadId) {log.info("通过 <{}-{}-{}> 合并<分片上传>数据", object, uploadId, fileProperties.getBucketName());//目前仅做了最大1000分片Part[] parts = new Part[1000];// 查询上传后的分片数据ListPartsResponse partResult = customMinioClient.listMultipart(fileProperties.getBucketName(), null, object, 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(fileProperties.getBucketName(), null, object, uploadId, parts, null, null);return true;}/*** 获取文件内容和元信息,该文件不存在会抛异常* @param object object* @return StatObjectResponse*/@SneakyThrowspublic StatObjectResponse statObject(String object) {return customMinioClient.statObject(StatObjectArgs.builder().bucket(fileProperties.getBucketName()).object(object).build()).get();}@SneakyThrowspublic GetObjectResponse getObject(String object, Long offset, Long contentLength) {return customMinioClient.getObject(GetObjectArgs.builder().bucket(fileProperties.getBucketName()).object(object).offset(offset).length(contentLength).build()).get();}}

四、疑问

我在全部使用aws-s3上传时出现一个问题至今没有办法解决。只能在查询分片的时候用minio的包进行。

分片后调用amazonS3.listParts()一直超时

这个问题我在
https://gitee.com/Gary2016/minio-upload/issues/I8H8GM
也看到有人跟我有相同的问题

有解决的朋友麻烦评论区告知下方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/871531.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024.7.12 检测H1S-0806MT-XP (问题:脉冲自己会给)

步骤一&#xff1a;先把H1s里面的程序上载保存&#xff0c;避免丢失。 注意&#xff1a;上载程序时&#xff0c;参数也需要上载。&#xff08;勾选软原件内存选项&#xff09; 步…

EasyExcel批量读取Excel文件数据导入到MySQL表中

1、EasyExcel简介 官网&#xff1a;EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel 官网 2、代码实战 首先引入jar包 <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.2</v…

智慧校园缴费管理-缴费项目类型功能概述

智慧校园的缴费管理系统&#xff0c;以缴费项目类型为核心功能之一&#xff0c;精细划分并优化了各类缴费流程&#xff0c;为学生和家长带来更为直观、便捷的财务管理体验。这一功能通过整合校园内广泛的费用类别&#xff0c;确保每一笔费用都能准确、高效地处理&#xff0c;体…

Provider(2)- SourceAudioBufferProvider

SourceAudioBufferProvider 从Source源端出来的数据&#xff0c;通常是来自于应用层&#xff0c;但没有与应用层直接连接&#xff0c;通过MonoPipe相关类连接&#xff0c;其SourceAudioBufferProvider和MonoPipe相关类的包含关系图如下&#xff1a; 如上图&#xff0c;Sourc…

11计算机视觉—语义分割与转置卷积

目录 1.语义分割应用语义分割和实例分割2.语义分割数据集:Pascal VOC2012 语义分割数据集预处理数据:我们使用图像增广中的随机裁剪,裁剪输入图像和标签的相同区域。3.转置卷积 上采样填充、步幅和多通道填充步幅多通道转置卷积是一种卷积:重新排列输入和核转置卷积是一种卷…

Java高级重点知识点-22-缓冲流、转换流、序列化流、打印流

文章目录 缓冲流字节缓冲流字符缓冲流 转换流InputStreamReader类OutputStreamWriter类 序列化ObjectOutputStream类ObjectInputStream类 打印流 缓冲流 缓冲流,也叫高效流&#xff0c;是对4个基本的 FileXxx 流的增强&#xff0c;所以也是4个流 基本原理&#xff1a; 缓冲流的…

ES13的4个改革性新特性

1、类字段声明 在 ES13 之前,类字段只能在构造函数中声明, ES13 消除了这个限制 // 之前 class Car {constructor() {this.color = blue;this.age = 2

C++ | Leetcode C++题解之第232题用栈实现队列

题目&#xff1a; 题解&#xff1a; class MyQueue { private:stack<int> inStack, outStack;void in2out() {while (!inStack.empty()) {outStack.push(inStack.top());inStack.pop();}}public:MyQueue() {}void push(int x) {inStack.push(x);}int pop() {if (outStac…

linux_进程周边知识——理解冯诺依曼体系结构

前言&#xff1a; 本篇内容是为了让友友们较好地理解进程的概念&#xff0c; 而在真正了解进行概念之前&#xff0c; 要先了解一下冯诺依曼体系结构。 所以博主会先对冯诺伊曼体系结构进行解释&#xff0c; 然后再讲解进程的概念。 ps&#xff1a; 本篇内容适合了解一些linux指…

基于复旦微JFMQL100TAI的全国产化FPGA+AI人工智能异构计算平台,兼容XC7Z045-2FFG900I

基于上海复旦微电子FMQL45T900的全国产化ARM核心板。该核心板将复旦微的FMQL45T900&#xff08;与XILINX的XC7Z045-2FFG900I兼容&#xff09;的最小系统集成在了一个87*117mm的核心板上&#xff0c;可以作为一个核心模块&#xff0c;进行功能性扩展&#xff0c;能够快速的搭建起…

springboot大学校园二手书交易APP

摘 要 在数字化与移动互联网迅猛发展的今天&#xff0c;人们对于图书的需求与消费方式也在悄然改变。为了满足广大读者对图书的热爱与追求&#xff0c;我们倾力打造了一款基于Android平台的图书交易APP。这款APP不仅汇聚了海量的图书资源&#xff0c;提供了便捷的交易平台&…

【产品经理】WMS多仓调拨转移说明

对于仓储管理来说&#xff0c;越来越多企业开始应用WMS进行系统化的管理&#xff0c;以提升仓库的作业效率。本文作者从业务流程和基础功能两个方面展开介绍&#xff0c;希望对你有帮助。 一、业务流程 。在线下业务流程拓展&#xff0c;仓库不断增多的过程中&#xff0c;由于…

vscode终端(控制台打印乱码)

乱码出现的两种可能&#xff08;重点是下面标题2&#xff09; 1、文件中的汉字本来就是乱码&#xff0c;输出到控制台(终端)那就当然是乱码 在vscode中设置文件的编码格式为UTF-8&#xff0c; 2、输出到控制台(终端)之前的汉字不是乱码&#xff0c;针对此种情况如下设置 原因…

GuLi商城-商品服务-API-品牌管理-JSR303分组校验

注解:@Validated 实体类: package com.nanjing.gulimall.product.entity;import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.nanjing.common.valid.ListValue; import com.nanjing.common.valid.Updat…

【Python学习笔记】Optuna + Transformer B站视频实践

【Python学习笔记】Optuna Transformer 实践 背景前摇&#xff08;省流可不看&#xff09;&#xff1a; 之前以泰坦尼克号数据集为案例&#xff0c;学习了Optuna的基本操作&#xff0c;为了进一步巩固知识和便于包装简历&#xff0c;决定找个唬人一点的项目练练手。 ————…

[读论文]Transformers are SSMs

Notation T T T: Sequence length/ time length $$: 摘要 虽然transformer一直是深度学习在语言建模方面成功的主要架构&#xff0c;但状态空间模型(ssm)&#xff0c;如Mamba&#xff0c;最近被证明在中小规模上与transformer相匹配或优于transformer。这些模型族实际上是非常…

数据结构(4.1)——串的存储结构

串的顺序存储 串&#xff08;String&#xff09;的顺序存储是指使用一段连续的存储单元来存储字符串中的字符。 计算串的长度 静态存储(定长顺序存储) #define MAXLEN 255//预定义最大串为255typedef struct {char ch[MAXLEN];//每个分量存储一个字符int length;//串的实际长…

子进程继承父进程文件描述符导致父进程打开设备文件失败

开发过程中有时会遇到需要在程序中执行三方程序或者shell脚本&#xff0c;一般会通过system(), popen(), exec簇来完成该功能。我们知道以上方法会通过fork创建子进程后在子进程中执行相应指令。如图1为某个示例流程&#xff0c;具体的程序执行流程如图2所示&#xff0c;线程my…

计算机图形学入门28:相机、透镜和光场

1.前言 相机(Cameras)、透镜(Lenses)和光场(Light Fields)都是图形学中重要的组成部分。在之前的学习中&#xff0c;都是默认它们的存在&#xff0c;所以现在也需要单独拿出来学习下。 2.成像方法 计算机图形学有两种成像方法&#xff0c;即合成(Synthesis)和捕捉(Capture)。前…

pytorch的基本使用(上)

目录 一、安装pytorch1、用conda指令创建一个pytorch的环境2、安装pytorch&#xff08;无独显&#xff09; 二、编译器选择1、pycharm&#xff08;1&#xff09;安装pycharm&#xff08;2&#xff09;选择编译器&#xff08;3&#xff09;检测能否正常运行小技巧 pycharm 的创建…