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
也看到有人跟我有相同的问题
有解决的朋友麻烦评论区告知下方法。
相关文章:

AWS-S3实现Minio分片上传、断点续传、秒传、分片下载、暂停下载
文章目录 前言一、功能展示上传功能点下载功能点效果展示 二、思路流程上传流程下载流程 三、代码示例四、疑问 前言 Amazon Simple Storage Service(S3),简单存储服务,是一个公开的云存储服务。Web应用程序开发人员可以使用它存…...
Selenium - 设置元素等待及加载策略
7月18日资源分享: 耿直哥三部曲全——机器学习,强化学习,深度学习 链接: https://pan.baidu.com/s/1c_eVVeqCZmB6zszHt6ZXiw?pwdtf2a 在使用Selenium进行网页自动化测试时,一个常见的问题是页面加载速度和元素的可见性问题。…...

【数据结构】线性结构——数组、链表、栈和队列
目录 前言 一、数组(Array) 1.1优点 1.2缺点 1.3适用场景 二、链表(Linked List) 2.1优点 2.2缺点 2.3适用场景 三、栈(Stack) 3.1优点 3.2缺点 3.3适用场景 四、队列(Queue) 4.1优点…...

json将列表字典等转字符串,然后解析又转回来
在 Python 中使用 json 模块来方便地在数据和 JSON 格式字符串之间进行转换,以便进行数据的存储、传输或与其他支持 JSON 格式的系统进行交互。 JSON 字符串通过 json.loads() 函数转换为 Python 对象。 pthon对象通过json.dumps()转为字符串 import jsonstr_list…...

记录|.NET上位机开发和PLC通信的实现
本文记录源自:B站视频 实验结果:跟视频做下来是没有问题的。能运行。 自己补充做了视频中未实现的读取和写入数据部分【欢迎小伙伴指正不对的地方】 目录 前言一、项目Step1. 创建项目Step2. 创建动态图片展示Step3. 创建图片型按钮Step4. 创建下拉框Ste…...

微服务实战系列之玩转Docker(二)
前言 上一篇,博主对Docker的背景、理念和实现路径进行了简单的阐述。作为云原生技术的核心之一,轻量级的容器Docker,受到业界追捧。因为它抛弃了笨重的OS,也不带Data,可以说,能够留下来的都是打仗的“精锐…...

Linux:信号的概念与产生
信号概念 信号是进程之间事件异步通知的一种方式 在Linux命令行中,我们可以通过ctrl c来终止一个前台运行的进程,其实这就是一个发送信号的行为。我们按下ctrl c是在shell进程中,而被终止的进程,是在前台运行的另外一个进程。因…...

云监控(华为) | 实训学习day2(10)
spring boot基于框架的实现 简单应用 - 用户数据显示 开发步骤 第一步:文件-----》新建---项目 第二步:弹出的对话框中,左侧选择maven,右侧不选任何内容. 第三步,选择maven后,下一步 第4步 :出现对话框中填写项目名称 第5步&…...
数据结构第35节 性能优化 算法的选择
算法的选择对于优化程序性能至关重要。不同的算法在时间复杂度、空间复杂度以及适用场景上有着明显的差异。下面我将结合具体的代码示例,来讲解几种常见的算法选择及其优化方法。 示例 1: 排序算法 场景描述: 假设我们需要对一个整数数组进行排序。 算法选择: …...
每天一个数据分析题(四百三十六)- 正态分布
X为服从正态分布的随机变量N(2, 9), 如果P(X>c)P(X<c), 则c的值为() A. 3 B. 2 C. 9 D. 2/3 数据分析认证考试介绍:点击进入 题目来源于CDA模拟题库 点击此处获取答案 数据分析专项练习题库 内容涵盖Python,SQL&…...
跟我学C++中级篇——虚函数的性能
一、虚函数性能 一般来说,面向对象的设计中,继承和多态是其中两个非常重要的特征。从使用的过程来看,一般应用到继承的,使用多态的可能性就非常大。而多态的实现有很多种, 但开发者通常认为的多态(动多态&…...

trl - 微调、对齐大模型的全栈工具
文章目录 一、关于 TRL亮点 二、安装1、Python包2、从源码安装3、存储库 三、命令行界面(CLI)四、如何使用1、SFTTrainer2、RewardTrainer3、PPOTrainer4、DPOTrainer 五、其它开发 & 贡献参考文献最近策略优化 PPO直接偏好优化 DPO 一、关于 TRL T…...

GuLi商城-商品服务-API-品牌管理-品牌分类关联与级联更新
先配置mybatis分页: 品牌管理增加模糊查询: 品牌管理关联分类: 一个品牌可以有多个分类 一个分类也可以有多个品牌 多对多的关系,用中间表 涉及的类: 方法都比较简单,就不贴代码了...

【linux】服务器ubuntu安装cuda11.0、cuDNN教程,简单易懂,包教包会
【linux】服务器ubuntu安装cuda11.0、cuDNN教程,简单易懂,包教包会 【创作不易,求点赞关注收藏】 文章目录 【linux】服务器ubuntu安装cuda11.0、cuDNN教程,简单易懂,包教包会一、版本情况介绍二、安装cuda1、到官网…...

在 Apifox 中如何高效批量添加接口请求 Body 参数?
在使用 Apifox 进行 API 设计时,你可能会遇到需要添加大量请求参数的情况。想象一下,如果一个接口需要几十甚至上百个参数,若要在接口的「修改文档」里一个个手动添加这些参数,那未免也太麻烦了,耗时且易出错。这时候&…...

专业PDF编辑工具:Acrobat Pro DC 2024.002.20933绿色版,提升你的工作效率!
软件介绍 Adobe Acrobat Pro DC 2024绿色便携版是一款功能强大的PDF编辑和转换软件,由Adobe公司推出。它是Acrobat XI系列的后续产品,提供了全新的用户界面和增强功能。用户可以借助这款软件将纸质文件转换为可编辑的电子文件,便于传输、签署…...

车载音视频App框架设计
简介 统一播放器提供媒体播放一致性的交互和视觉体验,减少各个媒体应用和场景独自开发的重复工作量,实现媒体播放链路的一致性,减少碎片化的Bug。本文面向应用开发者介绍如何快速接入媒体播放器。 主要功能: 新设计的统一播放U…...

StarRocks on AWS Graviton3,实现 50% 以上性价比提升
在数据时代,企业拥有前所未有的大量数据资产,但如何从海量数据中发掘价值成为挑战。数据分析凭借强大的分析能力,可从不同维度挖掘数据中蕴含的见解和规律,为企业战略决策提供依据。数据分析在营销、风险管控、产品优化等领域发挥…...
VUE中setup()
在Vue中,setup() 函数是Vue 3.0及更高版本引入的一个重要特性,它是Composition API的入口点。setup() 函数用于初始化组件的状态和逻辑,包括定义响应式数据、方法和生命周期钩子。以下是关于setup() 函数的详细解释: 1. 作用与特…...

【单元测试】SpringBoot
【单元测试】SpringBoot 1. 为什么单元测试很重要?‼️ 从前,有一个名叫小明的程序员,他非常聪明,但有一个致命的缺点:懒惰。小明的代码写得又快又好,但他总觉得单元测试是一件麻烦事,觉得代码…...
内存分配函数malloc kmalloc vmalloc
内存分配函数malloc kmalloc vmalloc malloc实现步骤: 1)请求大小调整:首先,malloc 需要调整用户请求的大小,以适应内部数据结构(例如,可能需要存储额外的元数据)。通常,这包括对齐调整,确保分配的内存地址满足特定硬件要求(如对齐到8字节或16字节边界)。 2)空闲…...
ES6从入门到精通:前言
ES6简介 ES6(ECMAScript 2015)是JavaScript语言的重大更新,引入了许多新特性,包括语法糖、新数据类型、模块化支持等,显著提升了开发效率和代码可维护性。 核心知识点概览 变量声明 let 和 const 取代 var…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容
基于 UniApp + WebSocket实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...

【大模型RAG】Docker 一键部署 Milvus 完整攻略
本文概要 Milvus 2.5 Stand-alone 版可通过 Docker 在几分钟内完成安装;只需暴露 19530(gRPC)与 9091(HTTP/WebUI)两个端口,即可让本地电脑通过 PyMilvus 或浏览器访问远程 Linux 服务器上的 Milvus。下面…...
【论文笔记】若干矿井粉尘检测算法概述
总的来说,传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度,通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...
【Go】3、Go语言进阶与依赖管理
前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课,做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程,它的核心机制是 Goroutine 协程、Channel 通道,并基于CSP(Communicating Sequential Processes࿰…...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...

Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

华硕a豆14 Air香氛版,美学与科技的馨香融合
在快节奏的现代生活中,我们渴望一个能激发创想、愉悦感官的工作与生活伙伴,它不仅是冰冷的科技工具,更能触动我们内心深处的细腻情感。正是在这样的期许下,华硕a豆14 Air香氛版翩然而至,它以一种前所未有的方式&#x…...
【Go语言基础【12】】指针:声明、取地址、解引用
文章目录 零、概述:指针 vs. 引用(类比其他语言)一、指针基础概念二、指针声明与初始化三、指针操作符1. &:取地址(拿到内存地址)2. *:解引用(拿到值) 四、空指针&am…...