Minio入门系列【18】Minio+vue-uploader 分片上传方案及案例详解(源码文尾附上)

本文主要是介绍Minio入门系列【18】Minio+vue-uploader 分片上传方案及案例详解(源码文尾附上),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 前言
    • 优化方案
    • 案例
      • 1. 后端获取分片上传URL
      • 2. 前端执行分片上传
      • 3. 合并文件
      • 4. 测试

前言

我们之前分析过Minio 的上传接口源码,其是进行了分块,再上传分块到Minio 服务器,最后再对块进行合并。

针对大文件的上传,如果采用上传到文件服务,再上传到Minio,其效率是非常低的,首先上传到文件服务(会存放在Tomcat 临时目录)就已经比较慢了。

针对大文件的上传,我们需要一个优化方案。

优化方案

在这里插入图片描述

  1. 前端服务进行大文件分片处理,将分片信息传递给文件服务,文件服务返回所有分片的上传链接及uploadId。
  2. 前端服务直接请求Minio 服务器,并发上传分片
  3. 所有分片上传完成后,使用uploadId 调用文件服务进行文件合并

案例

本案例基于Spring Boot集成Minio

1. 后端获取分片上传URL

在Minio 的上传接口源码中,创建分片请求的方法是protected 关键字修饰的,无法通过创建MinioClient对象来访问,那么只能通过子类继承来访问了。
在这里插入图片描述
首先自定义一个Minio 客户端类,继承MinioClient类,其作用主要是将以下几个方法暴露出来,以便调用:

  • createMultipartUpload:创建分片请求,返回uploadId
  • listMultipart:查询分片信息
  • completeMultipartUpload:根据uploadId 合并已上传的分片
public class PearlMinioClient extends MinioClient {protected PearlMinioClient(MinioClient client) {super(client);}/*** 创建分片上传请求** @param bucketName       存储桶* @param region           区域* @param objectName       对象名* @param headers          消息头* @param extraQueryParams 额外查询参数*/@Overridepublic CreateMultipartUploadResponse createMultipartUpload(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {return super.createMultipartUpload(bucketName, region, objectName, headers, extraQueryParams);}/*** 完成分片上传,执行合并文件** @param bucketName       存储桶* @param region           区域* @param objectName       对象名* @param uploadId         上传ID* @param parts            分片* @param extraHeaders     额外消息头* @param extraQueryParams 额外查询参数*/@Overridepublic ObjectWriteResponse completeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {return super.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);}/*** 查询分片数据** @param bucketName       存储桶* @param region           区域* @param objectName       对象名* @param uploadId         上传ID* @param extraHeaders     额外消息头* @param extraQueryParams 额外查询参数*/public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {return super.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);}
}

在配置类中注入我们的自定义客户端:

    @Bean@SneakyThrows@ConditionalOnMissingBean(PearlMinioClient.class)public PearlMinioClient minioClient(OssProperties ossProperties) {MinioClient minioClient = MinioClient.builder().endpoint(ossProperties.getEndpoint()).credentials(ossProperties.getAccessKey(), ossProperties.getSecretKey()).build();return new PearlMinioClient(minioClient);}

在模板类MinioTemplate中,将之前的MinIO 客户端换成PearlMinioClient。并将上面关于分片的几个操作方法集成进来。

@Slf4j
@AllArgsConstructor
public class MinioTemplate {/*** MinIO 客户端*/PearlMinioClient pearlMinioClient;/*** MinIO 配置类*/OssProperties ossProperties;/*** 查询所有存储桶** @return Bucket 集合*/@SneakyThrowspublic List<Bucket> listBuckets() {return pearlMinioClient.listBuckets();}/*** 桶是否存在** @param bucketName 桶名* @return 是否存在*/@SneakyThrowspublic boolean bucketExists(String bucketName) {return pearlMinioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());}/*** 创建存储桶** @param bucketName 桶名*/@SneakyThrowspublic void makeBucket(String bucketName) {if (!bucketExists(bucketName)) {pearlMinioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());}}/*** 删除一个空桶 如果存储桶存在对象不为空时,删除会报错。** @param bucketName 桶名*/@SneakyThrowspublic void removeBucket(String bucketName) {pearlMinioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());}/*** 上传文件** @param inputStream      流* @param originalFileName 原始文件名* @param bucketName       桶名* @return OssFile*/@SneakyThrowspublic OssFile putObject(InputStream inputStream, String bucketName, String originalFileName) {String uuidFileName = generateOssUuidFileName(originalFileName);try {if (StrUtil.isEmpty(bucketName)) {bucketName = ossProperties.getDefaultBucketName();}pearlMinioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(uuidFileName).stream(inputStream, inputStream.available(), -1).build());return new OssFile(uuidFileName, originalFileName);} finally {if (inputStream != null) {inputStream.close();}}}/*** 返回临时带签名、过期时间一天、Get请求方式的访问URL** @param bucketName  桶名* @param ossFilePath Oss文件路径* @return*/@SneakyThrowspublic String getPresignedObjectUrl(String bucketName, String ossFilePath,Map<String, String> queryParams) {return pearlMinioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.PUT).bucket(bucketName).object(ossFilePath).expiry(60 * 60 * 24).extraQueryParams(queryParams).build());}/*** GetObject接口用于获取某个文件(Object)。此操作需要对此Object具有读权限。** @param bucketName  桶名* @param ossFilePath Oss文件路径*/@SneakyThrowspublic InputStream getObject(String bucketName, String ossFilePath) {return pearlMinioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(ossFilePath).build());}/*** 查询桶的对象信息** @param bucketName 桶名* @param recursive  是否递归查询* @return*/@SneakyThrowspublic Iterable<Result<Item>> listObjects(String bucketName, boolean recursive) {return pearlMinioClient.listObjects(ListObjectsArgs.builder().bucket("my-bucketname").recursive(recursive).build());}/*** 获取带签名的临时上传元数据对象,前端可获取后,直接上传到Minio** @param bucketName* @param fileName* @return*/@SneakyThrowspublic Map<String, String> getPresignedPostFormData(String bucketName, String fileName) {// 为存储桶创建一个上传策略,过期时间为7天PostPolicy policy = new PostPolicy(bucketName, ZonedDateTime.now().plusDays(1));// 设置一个参数key,值为上传对象的名称policy.addEqualsCondition("key", fileName);// 添加Content-Type,例如以"image/"开头,表示只能上传照片,这里吃吃所有policy.addStartsWithCondition("Content-Type", MediaType.ALL_VALUE);// 设置上传文件的大小 64kiB to 10MiB.//policy.addContentLengthRangeCondition(64 * 1024, 10 * 1024 * 1024);return pearlMinioClient.getPresignedPostFormData(policy);}/***  上传分片上传请求,返回uploadId*/public CreateMultipartUploadResponse uploadId(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {return pearlMinioClient.createMultipartUpload(bucketName, region, objectName, headers, extraQueryParams);}/*** 完成分片上传,执行合并文件** @param bucketName       存储桶* @param region           区域* @param objectName       对象名* @param uploadId         上传ID* @param parts            分片* @param extraHeaders     额外消息头* @param extraQueryParams 额外查询参数*/public ObjectWriteResponse completeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {return pearlMinioClient.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);}public String generateOssUuidFileName(String originalFilename) {return "files" + StrUtil.SLASH + DateUtil.format(new Date(), "yyyy-MM-dd") + StrUtil.SLASH + UUID.randomUUID() + StrUtil.UNDERLINE + originalFilename;}/*** 初始化默认存储桶*/@PostConstructpublic void initDefaultBucket() {String defaultBucketName = ossProperties.getDefaultBucketName();if (bucketExists(defaultBucketName)) {log.info("默认存储桶:defaultBucketName已存在");} else {log.info("创建默认存储桶:defaultBucketName");makeBucket(ossProperties.getDefaultBucketName());};}/*** 查询分片数据** @param bucketName       存储桶* @param region           区域* @param objectName       对象名* @param uploadId         上传ID* @param extraHeaders     额外消息头* @param extraQueryParams 额外查询参数*/public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {return pearlMinioClient.listMultipart(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);}
}

接下来就是创建请求了,返回一个Map 集合,实际开发应该封装为响应对象。这里主要是接受分片数量,然后为每一个分块,创建一个带签名的上传URL。

    /*** 返回分片上传需要的签名数据URL及 uploadId** @param bucketName* @param fileName* @return*/@GetMapping("/createMultipartUpload")@SneakyThrows@ResponseBodypublic Map<String, Object> createMultipartUpload(String bucketName, String fileName, Integer chunkSize) {// 1. 根据文件名创建签名Map<String, Object> result = new HashMap<>();// 2. 获取uploadIdCreateMultipartUploadResponse response = minioTemplate.uploadId(bucketName, null, fileName, null, null);String uploadId = response.result().uploadId();result.put("uploadId", uploadId);// 3. 请求Minio 服务,获取每个分块带签名的上传URLMap<String, String> reqParams = new HashMap<>();reqParams.put("uploadId", uploadId);List<String> partList = new ArrayList<>();// 4. 循环分块数 从1开始for (int i = 1; i <= chunkSize; i++) {reqParams.put("partNumber", String.valueOf(i));String uploadUrl = minioTemplate.getPresignedObjectUrl(bucketName, fileName, reqParams);// 获取URLresult.put("chunk_" + (i - 1), uploadUrl); // 添加到集合}return result;}

2. 前端执行分片上传

前端依然是采用vue-uploader,首先在文件添加的事件中,后台请求创建分片URL 上传接口,并将uploadId、每个分块的上传链接设置到file.chunkUrlData属性中:

    onFileAdded(file) {console.log('文件被添加:' + file.name);this.panelShow = true;// 计算MD5// this.computeMD5(file, this.options.chunkSize);// 获取分块上传链接// eslint-disable-next-line no-unused-varsvar res = this.getChunkUploadUrl(file);console.log('文件被添加查看是否获取到分块URL');console.log(file.chunkUrlData);},async getChunkUploadUrl(file) {// 向具有指定ID的用户发出请求console.log(file);console.log('获取分块上传链接');const fileName = file.name; // 文件名const chunkSize = file.chunks.length; // 分片数// 请求后台返回每个分块的上传链接// eslint-disable-next-line no-unused-varsconst res = await this.$http.get('/file/createMultipartUpload', {params: {fileName: fileName,chunkSize: chunkSize,bucketName: 'pearl-buckent'}}).then(function (response) {console.log('获取到的uploadId:' + response.data.uploadId);console.log('获取到的分片上传集合URL:');console.log(response.data);file.chunkUrlData = response.data;}).catch(function (error) {console.log(error);});},

options中,我们首先要设置以下几个重要选项:

  • 动态的target,因为每个分块的上传路径都是不一样的,所以要从file.chunkUrlData取出当前分块的URL
  • 分块大小chunkSize,Minio 默认是5MB,我们这里也是用这个大小
  • 查询参数query,因为每个分块上传时都需要一个partNumber参数,可以通过这个query函数来进行传递
  • 其他兼容Minio分片上传配置,详情见注释
      options: {// 目标上传 URL,可以是字符串也可以是函数,如果是函数的话,则会传入 Uploader.File 实例、// 当前块 Uploader.Chunk 以及是否是测试模式,默认值为 '/'target: function (file, chunkFile, mode) {// 分块上传前每次都会进入到该方法console.log('进入到target');console.log('文件名:' + file.name);console.log('当前分块序号' + chunkFile.offset);console.log('获取到分块上传URL:');console.log(file.chunkUrlData);const key = 'chunk_' + chunkFile.offset;// 键值 用于获取分块链接URLreturn file.chunkUrlData[key];},// 为每个块向服务器发出 GET 请求,以查看它是否已经存在。如果在服务器端实现,// 这将允许在浏览器崩溃甚至计算机重新启动后继续上传。(默认: true)testChunks: false,// 分块时按照该值来分。最后一个上传块的大小是可能是大于等于1倍的这个值但是小于两倍的这个值大小,// 可见这个 Issue #51,默认 1*1024*1024。chunkSize: 5 * 1024 * 1024,// 强制所有块小于或等于 chunkSize。否则,最后一个块将大于或等于chunkSize。(默认: false)forceChunkSize: true,// 包含在带有数据的多部分 POST 中的额外参数。这可以是一个对象或一个函数。如果是一个函数,// 它将被传递一个 Uploader.File、一个 Uploader.Chunk 对象和一个 isTest 布尔值(默认值{}:)query: function (file, chunkFile, mode) {const data = {'partNumber': chunkFile.offset + 1};return data;},uploadMethod: 'PUT',//  当上传的时候所使用的是方式,可选 multipart、octet,默认 multipart,参考 multipart vs octet。// MiniO 的分片不能使用表单method: 'octet',//  处理请求参数,默认 function (params) {return params},一般用于修改参数名字或者删除参数。0.5.2版本后,// Minio的连接后面不能拼接参数,所以设置为空processParams: function (params) { return {}; }// headers: {//  'Content-Type': 'binary/octet-stream'// }},

3. 合并文件

后台提供一个文件合并的接口,根据uploadId去查询分块信息,然后进行合并操作:

    /*** 分片上传完后合并** @param objectName 文件全路径名称* @param uploadId   返回的uploadId* @return /*/@GetMapping("/completeMultipartUpload")@SneakyThrows@ResponseBodypublic boolean completeMultipartUpload(String bucketName,String objectName, String uploadId) {try {Part[] parts = new Part[10000];ListPartsResponse partResult = minioTemplate.listMultipart("pearl-buckent", null, objectName, 1000, 0, uploadId, null, null);int partNumber = 1;System.err.println(partResult.result().partList().size() + "========================");for (Part part : partResult.result().partList()) {parts[partNumber - 1] = new Part(partNumber, part.etag());partNumber++;}minioTemplate.completeMultipartUpload("pearl-buckent", null, objectName, uploadId, parts, null, null);} catch (Exception e) {e.printStackTrace();return false;}return true;}

前端代码中,我们在文件上传成功事件中调用后台合并接口:

    // 单个文件上传成功onFileSuccess(rootFile, file, message) {console.log('单个文件上传成功', arguments);// 调用后台合并文件const fileName = file.name; // 文件名const uploadId = file.chunkUrlData.uploadId; // 分片数console.log();this.$http.get('/file/completeMultipartUpload', {params: {objectName: fileName,uploadId: uploadId,bucketName: 'pearl-buckent'}}).then(function (response) {console.log(response);}).catch(function (error) {console.log(error);});console.log('合并完成');}

4. 测试

首先添加一个文件,可以看到打印了uploadId和每个分块上传的URL:

在这里插入图片描述
点击开始按钮,可以看到多个分片的并发请求,最后调用了合并文件接口,并返回了true。
在这里插入图片描述
查看Minio 控制台,可以看到文件大小都一致。
在这里插入图片描述

下载上传的文件,进行播放,发现一切正常,说明基本上就实现该功能了,可能还会有其他小问题,就需要开发时进行严格的测试并修改了。
在这里插入图片描述
最后附上前后端代码地址。
在这里插入图片描述

这篇关于Minio入门系列【18】Minio+vue-uploader 分片上传方案及案例详解(源码文尾附上)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/382694

相关文章

redis中使用lua脚本的原理与基本使用详解

《redis中使用lua脚本的原理与基本使用详解》在Redis中使用Lua脚本可以实现原子性操作、减少网络开销以及提高执行效率,下面小编就来和大家详细介绍一下在redis中使用lua脚本的原理... 目录Redis 执行 Lua 脚本的原理基本使用方法使用EVAL命令执行 Lua 脚本使用EVALSHA命令

SpringBoot3.4配置校验新特性的用法详解

《SpringBoot3.4配置校验新特性的用法详解》SpringBoot3.4对配置校验支持进行了全面升级,这篇文章为大家详细介绍了一下它们的具体使用,文中的示例代码讲解详细,感兴趣的小伙伴可以参考... 目录基本用法示例定义配置类配置 application.yml注入使用嵌套对象与集合元素深度校验开发

Python中的Walrus运算符分析示例详解

《Python中的Walrus运算符分析示例详解》Python中的Walrus运算符(:=)是Python3.8引入的一个新特性,允许在表达式中同时赋值和返回值,它的核心作用是减少重复计算,提升代码简... 目录1. 在循环中避免重复计算2. 在条件判断中同时赋值变量3. 在列表推导式或字典推导式中简化逻辑

Java Stream流使用案例深入详解

《JavaStream流使用案例深入详解》:本文主要介绍JavaStream流使用案例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录前言1. Lambda1.1 语法1.2 没参数只有一条语句或者多条语句1.3 一个参数只有一条语句或者多

SpringBoot整合mybatisPlus实现批量插入并获取ID详解

《SpringBoot整合mybatisPlus实现批量插入并获取ID详解》这篇文章主要为大家详细介绍了SpringBoot如何整合mybatisPlus实现批量插入并获取ID,文中的示例代码讲解详细... 目录【1】saveBATch(一万条数据总耗时:2478ms)【2】集合方式foreach(一万条数

Python装饰器之类装饰器详解

《Python装饰器之类装饰器详解》本文将详细介绍Python中类装饰器的概念、使用方法以及应用场景,并通过一个综合详细的例子展示如何使用类装饰器,希望对大家有所帮助,如有错误或未考虑完全的地方,望不... 目录1. 引言2. 装饰器的基本概念2.1. 函数装饰器复习2.2 类装饰器的定义和使用3. 类装饰

MySQL 中的 JSON 查询案例详解

《MySQL中的JSON查询案例详解》:本文主要介绍MySQL的JSON查询的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录mysql 的 jsON 路径格式基本结构路径组件详解特殊语法元素实际示例简单路径复杂路径简写操作符注意MySQL 的 J

Python ZIP文件操作技巧详解

《PythonZIP文件操作技巧详解》在数据处理和系统开发中,ZIP文件操作是开发者必须掌握的核心技能,Python标准库提供的zipfile模块以简洁的API和跨平台特性,成为处理ZIP文件的首选... 目录一、ZIP文件操作基础三板斧1.1 创建压缩包1.2 解压操作1.3 文件遍历与信息获取二、进阶技

Python Transformers库(NLP处理库)案例代码讲解

《PythonTransformers库(NLP处理库)案例代码讲解》本文介绍transformers库的全面讲解,包含基础知识、高级用法、案例代码及学习路径,内容经过组织,适合不同阶段的学习者,对... 目录一、基础知识1. Transformers 库简介2. 安装与环境配置3. 快速上手示例二、核心模

一文详解Java异常处理你都了解哪些知识

《一文详解Java异常处理你都了解哪些知识》:本文主要介绍Java异常处理的相关资料,包括异常的分类、捕获和处理异常的语法、常见的异常类型以及自定义异常的实现,文中通过代码介绍的非常详细,需要的朋... 目录前言一、什么是异常二、异常的分类2.1 受检异常2.2 非受检异常三、异常处理的语法3.1 try-