SpringBoot 后端项目利用 Minio 实现分片上传、断点续传

一、准备工作

安装 Minio 服务后,在 SpringBoot 项目中添加依赖:

	<!-- MinIO --><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.2.1</version></dependency>

在代码中获取 MinioClient(用于操作 Minio 的服务端):

MinioClient client = MinioClient.builder().endpoint("http://192.168.xx.133:9000")  // 服务端IP+端口.credentials(minioProperties.getAccessKey(), // 服务端用户名minioProperties.getSecretKey()) // 服务端密码.build();

二、实现分片上传+断点续传

2.1 思路

分片上传和断点续传的实现过程中,需要在Minio内部记录已上传的分片文件。

这些分片文件将以文件md5作为父目录,分片文件的名字按照01,02,...的顺序进行命名。同时,还必须知道当前文件的分片总数,这样就能够根据总数来判断文件是否上传完毕了。

比如,一个文件被分成了10片,所以总数是10。当前端发起上传请求时,把一个个文件分片依次上传,Minio 服务器中存储的临时文件依次是010203 等等。

假设前端把05分片上传完毕了之后断开了连接,由于 Minio 服务器仍然存储着01~05的分片文件,因此前端再次上传文件时,只需从06序号开始上传分片,而不用从头开始传输。这就是所谓的断点续传

2.2 代码

① 分片上传API

为了实现以上思路,考虑实现一个方法,用于上传文件的某一个分片。

/*** 将文件进行分片上传* <p>有一个未处理的bug(虽然概率很低很低):</p>* 当两个线程同时上传md5相同的文件时,由于两者会定位到同一个桶的同一个临时目录,两个线程会相互产生影响!* * @param file 分片文件* @param currIndex 当前文件的分片索引* @param totalPieces 切片总数(对于同一个文件,请确保切片总数始终不变)* @param md5 整体文件MD5* @return 剩余未上传的文件索引集合*/public FragResult uploadFileFragment(MultipartFile file,Integer currIndex, Integer totalPieces, String md5) throws Exception {checkNull(currIndex, totalPieces, md5);// 临时文件存放桶if ( !this.bucketExists(DEFAULT_TEMP_BUCKET_NAME) ) {this.createBucket(DEFAULT_TEMP_BUCKET_NAME);}// 得到已上传的文件索引Iterable<Result<Item>> results = this.getFilesByPrefix(DEFAULT_TEMP_BUCKET_NAME, md5.concat("/"), false);Set<Integer> savedIndex = Sets.newHashSet();boolean fileExists = false;for (Result<Item> item : results) {Integer idx = Integer.valueOf( getContentAfterSlash(item.get().objectName()) );if (currIndex.equals( idx )) {fileExists = true;}savedIndex.add( idx );}// 得到未上传的文件索引Set<Integer> remainIndex = Sets.newTreeSet();for (int i = 0; i < totalPieces; i++) {if ( !savedIndex.contains(i) ) {remainIndex.add(i);}}if (fileExists) {return new FragResult(false, remainIndex, "index [" + currIndex + "] exists");}this.uploadFileStream(DEFAULT_TEMP_BUCKET_NAME, this.getFileTempPath(md5, currIndex, totalPieces), file.getInputStream());// 还剩一个索引未上传,当前上传索引刚好是未上传索引,上传完当前索引后就完全结束了。if ( remainIndex.size() == 1 && remainIndex.contains(currIndex) ) {return new FragResult(true, null, "completed");}return new FragResult(false, remainIndex, "index [" + currIndex + "] has been uploaded");}

值得注意的是,我在项目中实践该方法时,上述参数都是由前端传来的,因此文件分片过程发生在前端,分片的大小也由前端定义。

② 合并文件API

当所有分片文件上传完毕,需要手动调用 Minio 原生 API 来合并临时文件(当然,在上面的那个方法中,当最后一个分片上传完毕后直接执行合并操作也是可以的)

临时文件合并完毕后,将会自动删除所有临时文件。

/*** 合并分片文件,并放到指定目录* 前提是之前已把所有分片上传完毕。* * @param bucketName 目标文件桶名* @param targetName 目标文件名(含完整路径)* @param totalPieces 切片总数(对于同一个文件,请确保切片总数始终不变)* @param md5 文件md5* @return minio原生对象,记录了文件上传信息*/public boolean composeFileFragment(String bucketName, String targetName, Integer totalPieces, String md5) throws Exception {checkNull(bucketName, targetName, totalPieces, md5);// 检查文件索引是否都上传完毕Iterable<Result<Item>> results = this.getFilesByPrefix(DEFAULT_TEMP_BUCKET_NAME, md5.concat("/"), false);Set<String> savedIndex = Sets.newTreeSet();for (Result<Item> item : results) {savedIndex.add( item.get().objectName() );}if (savedIndex.size() == totalPieces) {// 文件路径 转 文件合并对象List<ComposeSource> sourceObjectList = savedIndex.stream().map(filePath -> ComposeSource.builder().bucket(DEFAULT_TEMP_BUCKET_NAME).object( filePath ).build()).collect(Collectors.toList());ObjectWriteResponse objectWriteResponse = client.composeObject(ComposeObjectArgs.builder().bucket(bucketName).object(targetName).sources(sourceObjectList).build());// 上传成功,则删除所有的临时分片文件List<String> filePaths = Stream.iterate(0, i -> ++i).limit(totalPieces).map(i -> this.getFileTempPath(md5, i, totalPieces) ).collect(Collectors.toList());Iterable<Result<DeleteError>> deleteResults = this.removeFiles(DEFAULT_TEMP_BUCKET_NAME, filePaths);// 遍历错误集合(无元素则成功)for (Result<DeleteError> result : deleteResults) {DeleteError error = result.get();System.err.printf("[Bigfile] 分片'%s'删除失败! 错误信息: %s", error.objectName(), error.message());}return true;}throw new GlobalException("The fragment index is not complete. Please check parameters [totalPieces] or [md5]");}

以上方法的源码我放到了https://github.com/sky-boom/minio-spring-boot-starter,对原生的 Minio API 进行了封装,抽取成了minio-spring-boot-starter组件,感兴趣的朋友欢迎前去查看。

2.3 后端调用API示例

这里以单线程的分片上传为例(即前端每次只上传一个分片文件,调用分片上传接口后,接口返回下一个分片文件的序号)

① Controller 层

    /*** 分片上传* @param user 用户对象* @param fileAddDto file: 分片文件, *                   currIndex: 当前分片索引, *                   totalPieces: 分片总数,*                   md5: 文件md5* @return 前端需上传的下一个分片序号(-1表示上传完成)*/@PostMapping("/file/big/upload")public ResultData<String> uploadBigFile(User user, BigFileAddDto fileAddDto) {// 1.文件为空,返回失败 (一般不是用户的问题)if (fileAddDto.getFile() == null) {throw new GlobalException();}// 2.名字为空,或包含特殊字符,则提示错误String fileName = fileAddDto.getFile().getOriginalFilename();if (StringUtils.isEmpty(fileName) || fileName.matches(FileSysConstant.NAME_EXCEPT_SYMBOL)) {throw new GlobalException(ResultCode.INCORRECT_FILE_NAME);}// 3. 执行分片上传String result = fileSystemService.uploadBigFile(user, fileAddDto);return GlobalResult.success(result);}

② Service 层

	@Overridepublic String uploadBigFile(User user, BigFileAddDto fileAddDto) {try {MultipartFile file = fileAddDto.getFile();Integer currIndex = fileAddDto.getCurrIndex();Integer totalPieces = fileAddDto.getTotalPieces();String md5 = fileAddDto.getMd5();log.info("[Bigfile] 上传文件md5: {} ,分片索引: {}", md5, currIndex);FragResult fragResult = minioUtils.uploadFileFragment(file, currIndex, totalPieces, md5);// 分片全部上传完毕if ( fragResult.isAllCompleted() ) {FileInfo fileInfo = getFileInfo(fileAddDto, user.getId());DBUtils.checkOperation( fileSystemMapper.insertFile(fileInfo) );String realPath = generateRealPath(generateVirtPath(fileAddDto.getParentPath(), file.getOriginalFilename()));// 发起文件合并请求, 无异常则成功minioUtils.composeFileFragment(getBucketByUsername(user.getUsername()), realPath, totalPieces, md5);return "-1";} else {Iterator<Integer> iterator = fragResult.getRemainIndex().iterator();if (iterator.hasNext()) {String nextIndex = iterator.next().toString();log.info("[BigFile] 下一个需上传的文件索引是:{}", nextIndex);return nextIndex;}}} catch (Exception e) {e.printStackTrace();}log.error("[Bigfile] 上传文件时出现异常");throw new GlobalException(ResultCode.FILE_UPLOAD_ERROR);}

2.4 前端

前端主要负责:

  • 规定文件分片的大小(比如5M),然后把文件进行拆分。
  • 计算文件分片的总数,并按序号把分片文件依次传递给后端。
  • 前端每上传完一个分片文件,接口都会返回下一个需要上传的分片文件。此时前端把对应的分片文件继续上传即可。
  • 当接口返回“-1”,表示所有文件已上传完毕。

前端代码此处不展示,有缘后续再花时间补充吧………………

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

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

相关文章

【js】日期、时间正则匹配

1、日期的正则表达式 格式&#xff1a;2023-08-11 var reg /^[1-9]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/; var regExp new RegExp(reg); if(!regExp.test(value)){alert("日期格式不正确");return; }2、时间的正则表达式 格式&#xff1a;23:00:00…

英码国产高配边缘计算盒子上市!搭载TPU处理器BM1684X,适配麒麟系统,支持OTA升级!

随着人工智能技术不断深入实际应用场景&#xff0c;加速各行各业场景应用落地&#xff0c;边缘计算的重要性越发凸显。相较于传统的集中式云计算&#xff0c;边缘计算在距离数据源或用户更近的地方提供计算能力&#xff0c;不仅满足了对实时性要求较高的场景应用需求&#xff0…

操作系统结构

操作系统结构 分层法模块化宏内核微内核微内核的基本概念微内核的基本功能 内核 分层法 分层法是将操作系统分为若干层&#xff0c;最底层为硬件&#xff0c;最高层为用户接口&#xff0c;每层只能调用紧邻它的底层的功能和服务&#xff08;单向依赖&#xff09; 分层法的优点…

如何通过CSS选择器选择一个元素的子元素?如何选择第一个子元素和最后一个子元素?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 选择一个元素的子元素⭐ 选择第一个子元素和最后一个子元素⭐ 注意事项⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&…

线程池,以及线程池的实现以及面试常问的问题,工厂模式,常见的锁策略(面试常考,要了解,不行就背)

一、&#x1f49b; 线程池的基本介绍 内存池&#xff0c;进程池&#xff0c;连接池&#xff0c;常量池&#xff0c;这些池子概念上都是一样的&#xff5e;&#xff5e; 如果我们需要频繁的创建销毁线程&#xff0c;此时创建销毁的成本就不能忽视了&#xff0c;因此就可以使用线…

Java中使用instanceof判断对象类型

记录&#xff1a;470 场景&#xff1a;Java中使用instanceof判断对象类型。例如在解析JSON字符串转换为指定类型时&#xff0c;先判断类型&#xff0c;再定向转换。在List<Object>中遍历Object时&#xff0c;先判断类型&#xff0c;再定向转换。 版本&#xff1a;JDK 1…

Redis系列(一):深入了解Redis数据类型和底层数据结构

Redis有以下几种常用的数据类型&#xff1a; redis数据是如何组织的 为了实现从键到值的快速访问&#xff0c;Redis 使用了一个哈希表来保存所有键值对。 Redis全局哈希表&#xff08;Global Hash Table&#xff09;是指在Redis数据库内部用于存储所有键值对的主要数据结构。…

安卓13不再支持PPTP怎么办?新的连接解决方案分享

随着Android 13的发布&#xff0c;我们迎来了一个令人兴奋的新品时刻。然而&#xff0c;对于一些用户而言&#xff0c;这也意味着必须面对一个重要的问题&#xff1a;Android 13不再支持PPTP协议。如果你是一个习惯使用PPTP协议来连接换地址的用户&#xff0c;那么你可能需要重…

C++ 泛型编程:函数模板

文章目录 前言一、什么是泛型编程二、函数模板三、函数模板的使用四、多参数函数模板五&#xff0c;示例代码&#xff1a;总结 前言 当需要编写通用的代码以处理不同类型的数据时&#xff0c;C 中的函数模板是一个很有用的工具。函数模板允许我们编写一个通用的函数定义&#…

Vue day02 Computed和Watch

1.事件绑定 可以用 v-on 指令监听DOM 事件&#xff0c;并在触发时运行一些 JavaScript 代码。v-on 还可以接收一个需要调用的方法名称。 <button v-on:click"handler">good</button> methods: { handler: function (event) { if (event) { alert(event.t…

接口测试之Jmeter+Ant+Jenkins接口自动化测试平台

平台简介 一个完整的接口自动化测试平台需要支持接口的自动执行&#xff0c;自动生成测试报告&#xff0c;以及持续集成。Jmeter支持接口的测试&#xff0c;Ant支持自动构建&#xff0c;而Jenkins支持持续集成&#xff0c;所以三者组合在一起可以构成一个功能完善的接口自动化…

BOLT- 识别和优化热门的基本块

在BOLT中&#xff0c;识别和优化热门的基本块之所以关键&#xff0c;是因为BOLT的主要目标是优化程序以更好地利用硬件特性&#xff0c;特别是指令缓存&#xff08;ICache&#xff09;。以下是BOLT如何识别和优化热门基本块的流程&#xff1a; 收集性能数据: BOLT开始的时候并不…

idea - 刷新 Git 分支数据 / 命令刷新 Git 分支数据

一、idea - 刷新 Git 分支数据 idea 找到 fetch 选项&#xff0c;重新获取分支数据 二、命令刷新 Git 分支数据 git fetch参考链接 1. 远程Gitlab新建的分支在IDEA里不显示

jxls导出问题

![请添加图片描述](https://img-blog.csdnimg.cn/bc74c4207818491c93b75e19b3333451.png 为什么最后导出的文件还是按原样导出啊&#xff0c;没有填充数据 ![在这里插入图片描述](https://img-blog.csdnimg.cn/d4500b9a98c042f6b64a5d0650071303.png

qt多线程使用方式

有5个方式&#xff1a;可以参考这个博客&#xff1a;Qt 中开启线程的五种方式_qt 线程_lucky-billy的博客-CSDN博客 注&#xff1a;为了实现更加灵活的线程管理&#xff08;因为这5种都有一些不方便之处&#xff1a;QThread需要子类化且不能传参&#xff0c;moveToThread不能传…

【leetcode】459. 重复的子字符串(easy)

给定一个非空的字符串 s &#xff0c;检查是否可以通过由它的一个子串重复多次构成。 示例 1: 输入: s “abab” 输出: true 解释: 可由子串 “ab” 重复两次构成。 示例 2: 输入: s “aba” 输出: false 示例 3: 输入: s “abcabcabcabc” 输出: true 解释: 可由子串 “ab…

ChatGPT等人工智能编写文章的内容今后将成为常态

BuzzFeed股价上涨200%可能标志着“转向人工智能”媒体趋势的开始。 周四&#xff0c;一份内部备忘录被华尔街日报透露BuzzFeed正计划使用ChatGPT聊天机器人-风格文本合成技术来自OpenAI&#xff0c;用于创建个性化盘问和将来可能的其他内容。消息传出后&#xff0c;BuzzFeed的…

ubuntu 20.04 RK3568网络的优先级设置

1、背景 硬件使用RK3568 CPU&#xff0c;操作系统采用ubuntu 20.04 Lxqt桌面的版本。硬件上具有一个有线以太网卡&#xff0c;一个wifi网卡&#xff0c;一个5G网卡。由于操作系统默认的网络优先级为有线网卡的最高&#xff0c;5G网卡次之。在一个业务应用中需要5G网卡的连接外…

文本三剑客之grep命令和awk命令 1.0 版本

grep awk 1.grep命令1.1 基本格式1.2 常用选项 2.awk命令2.1 awk工作原理2.2 awk命令格式2.3 awk常用内置变量 1.grep命令 1.1 基本格式 grep [选项]… 查找条件 目标文件1.2 常用选项 选项功能 -m [ x ]匹配x次 后停止,x为具体数字-v取反 -i忽略字符大小写 -n显示匹配的 …

Dynamic CRM开发 - 实体介绍

实体简介 在CRM中,实体(Entity)是数据的基本载体,也是构建业务逻辑网络的基础节点。 实体可以理解为数据库中的一张表(实体中的字段对应数据库表的字段),比如创建一个实体存储客户信息,创建一个实体存储产品信息,产品实体里可以创建一个查找类型的字段(类似表的外键)…