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. 为什么单元测试很重要?‼️ 从前,有一个名叫小明的程序员,他非常聪明,但有一个致命的缺点:懒惰。小明的代码写得又快又好,但他总觉得单元测试是一件麻烦事,觉得代码…...
分布式搜索引擎ES-elasticsearch入门
1.分布式搜索引擎:luceneVS Solr VS Elasticsearch 什么是分布式搜索引擎 搜索引擎:数据源:数据库或者爬虫资源 分布式存储与搜索:多个节点组成的服务,提高扩展性(扩展成集群) 使用搜索引擎为搜索提供服务。可以从海量…...
TCP三次握手与四次挥手详解
1.什么是TCP TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的通信协议,属于互联网协议族(TCP/IP)的一部分。TCP 提供可靠的、顺序的、无差错的数据传输服务&…...
【Windows】操作系统之任务管理器(第一篇)
一、操作系统简介 Windows操作系统是由微软公司(Microsoft)开发的一款图形操作系统,它以其强大的功能和广泛的用户基础,成为了目前世界上用户使用最多、兼容性最强的操作系统之一。以下是关于Windows操作系统的详细介绍ÿ…...
图同构的必要条件
来源:离散数学...
Django获取request请求中的参数
支持 post put json_str request.body # 属性获取最原始的请求体数据 json_dict json.loads(json_str)# 将原始数据转成字典格式 json_dict.get("key", "默认值") # 获取数据参考 https://blog.csdn.net/user_san/article/details/109654028...
kotlin compose 实现应用内多语言切换(不重新打开App)
1. 示例图 2.具体实现 如何实现上述示例,且不需要重新打开App ①自定义 MainApplication 实现 Application ,定义两个变量: class MainApplication : Application() { object GlobalDpData { var language: String = "" var defaultLanguage: Strin…...
记录些MySQL题集(16)
MySQL 存储过程与触发器 一、初识MySQL的存储过程 Stored Procedure存储过程是数据库系统中一个十分重要的功能,使用存储过程可以大幅度缩短大SQL的响应时间,同时也可以提高数据库编程的灵活性。 存储过程是一组为了完成特定功能的SQL语句集合&#x…...
【算法基础】Dijkstra 算法
定义: g [ i ] [ j ] g[i][j] g[i][j] 表示 v i v_i vi 到 $v_j $的边权重,如果没有连接,则 g [ i ] [ j ] ∞ g[i][j] \infty g[i][j]∞ d i s [ i ] dis[i] dis[i] 表示 v k v_k vk 到节点 v i v_i vi 的最短长度, …...
使用 Flask 3 搭建问答平台(三):注册页面模板渲染
前言 前端文件下载 链接https://pan.baidu.com/s/1Ju5hhhhy5pcUMM7VS3S5YA?pwd6666%C2%A0 知识点 1. 在路由中渲染前端页面 2. 使用 JinJa 2 模板实现前端代码复用 一、auth.py from flask import render_templatebp.route(/register, methods[GET]) def register():re…...
pycharm如何debug for循环里面的错误值
一般debug时,在for循环里面的话,需要自己一步一步点。如果循环几百次那种就比较麻烦。此时可以采用try except的方式来解决 例子如下 #ptyhon debug for循环的代码 num[1,2,3,s,4] ans0 for i in num:try:ansiexcept:print(错误) print(ans) 结果如下&a…...
自己做网站还需要交其他费用吗/网络推广工作怎么样
1、关闭firewall: systemctl stop firewalld.service #停止firewallsystemctl disable firewalld.service #禁止firewall开机启动2、安装iptables防火墙 #安装 yum install iptables-services 编辑防火墙配置文件 vi /etc/sysconfig/iptables# sample configuration…...
华为官方网站专卖店/查网站
矩阵的Hadamard积与符号模式【摘要】:我们主要讨论了非负矩阵、M-阵的Hadamard积与Fan积问题,以及矩阵Hadamard积的一些范数不等式.同时也讨论了逆M-矩阵、零模式不变矩阵、符号模式矩阵、k-幂等阵和符号k-幂等阵等特殊矩阵的相关问题&#x…...
制作作业平台网站的设计/个人网站怎么建立
弱口令(weak password) 没有严格和准确的定义,通常认为容易被别人(他们有可能对你很了解)猜测到或被破解工具破解的口令均为弱口令。弱口令指的是仅包含简单数字和字母的口令,例如“123”、“abc”等,因为这样的口令很…...
宣威网站建设c3sales/百度信息流广告怎么投放
说明: 该教程绕开了javascript的一些基本的语法知识,直接讲解javascript中最难理解的两个部分,也是和其他主流面向对象语言区别最大的两个部分——原型和闭包,当然,肯定少不了原型链和作用域链。帮你揭开javascript最神…...
百度做网站优化多少钱一年/手机百度一下百度
常见的聊天窗口image.png 聊天窗口布局 左上:聊天历史信息显示 左中:当前信息编辑区域 左下:按钮区域 右侧:显示展示区域 Frame控件 容器区域布局 frmLT,frmLC,frmLB,frmRT #创建frmLT容器 frmLT Frame(width 500, height 320,…...
安阳网站推广优化/百度查重软件
1.Visual Studio Code简介 1.1.什么是Visual Studio Code? Visual Studio Code是微软推出的一个运行于 Mac OS X、Windows和 Linux 之上的,针对于编写现代 Web 和云应用的跨平台源代码编辑器。 1.2.主要功能 集成了所有一款现代编辑器所应该具备的特性,…...