minio 后端大文件分片上传,合并,删除分片

2024-08-25 06:20

本文主要是介绍minio 后端大文件分片上传,合并,删除分片,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

背景

网上大多数minio大文件上传都是采用后台返回前端预上传链接,然后由前端去put请求直接和minio通信上传分片文件,然后调用后台合并分片逻辑来达到快申诉上传的目的,详情可以参考我的上两篇文章

最近有个项目域名是https的,但是上传大文件走https太慢,而且服务器配置很拉跨,https里走http预上传不知道为啥老是报错。所以研究下直接从后台分片,然后逐个上传,然后合并,删除分片。

springboot+elementui

集成minio

 <dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.3.1</version>
</dependency>

yml配置

minio:url: http://127.0.0.1:9000 //用于后台内部domain: http://127.0.0.1:9000//用于返回给前端,这个可以改成线上域名accessKey: minioadminsecretKey: minioadminbucketName: minioBackName#默认文件存放路径filePath: common/

MinioConfig 配置

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.minio.MinioClient;/*** Minio 配置信息** @author */
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig
{/*** 服务地址*/private String url;private String domain;public String getDomain() {return domain;}public void setDomain(String domain) {this.domain = domain;}/*** 用户名*/private String accessKey;/*** 密码*/private String secretKey;/*** 存储桶名称*/private String bucketName;/*** 文件存储指定位置路径*/private String filePath;public String getUrl(){return url;}public void setUrl(String url){this.url = url;}public String getAccessKey(){return accessKey;}public void setAccessKey(String accessKey){this.accessKey = accessKey;}public String getSecretKey(){return secretKey;}public void setSecretKey(String secretKey){this.secretKey = secretKey;}public String getBucketName(){return bucketName;}public void setBucketName(String bucketName){this.bucketName = bucketName;}public String getFilePath() {return filePath;}public void setFilePath(String filePath) {this.filePath = filePath;}@Beanpublic MinioClient getMinioClient(){return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();}
}

ISysFileService 文件上传接口

import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** 文件上传接口** @author */
public interface ISysFileService {/*** 文件上传接口** @param file 上传的文件* @return 访问地址* @throws Exception*/public String uploadFile(MultipartFile file) throws Exception;/*** 异步文件上传接口** @param file 上传的文件* @return 访问地址* @throws Exception*/public String uploadFileAsync(MultipartFile file) throws Exception;}

MinioSysFileServiceImpl 实现类

/*** Minio 文件存储** @author */
@Primary
@Service
@Slf4j
public class MinioSysFileServiceImpl implements ISysFileService {//minio每个分片不能低于5MB,最后一个分片可以不管 13MB文件可分成3个分片 5MB 5MB 3MBprivate static final int PART_SIZE = 5 * 1024 * 1024; // 5MB parts/*** minio基础参数 配置类*/@Autowiredprivate MinioConfig minioConfig;/*** minio客户端连接 连接minio工具*/@Autowiredprivate MinioClient client;/*** 本地文件上传接口** @param file 上传的文件* @return 访问地址* @throws Exception*/@Overridepublic String uploadFile(MultipartFile file) throws Exception {String fileName = file.getOriginalFilename();//获取文件名称fileName = minioConfig.getFilePath()+ DateUtils.getDate()+"/"+fileName.substring(fileName.lastIndexOf("/") + 1, fileName.length());long startTime = System.currentTimeMillis()/1000;//获取文件流InputStream inputStream = file.getInputStream();//获取文件大小long fileSize = file.getSize();//计算分片数量int partCount = (int) (fileSize / PART_SIZE);if (fileSize % PART_SIZE > 0) {partCount++;}long partTime =  System.currentTimeMillis()/1000;System.out.println("分片耗时"+(partTime-startTime));//存放分片流List<InputStream> parts = new ArrayList<>();//存放分片minio地址List<String> fileList = new ArrayList<>();//分配分片流 for (int i = 0; i < partCount; i++) {// 每次只需要从原始文件InputStream中读取指定大小的数据即可byte[] partData = new byte[PART_SIZE];int read = inputStream.read(partData);if (read == -1) {break; // 文件已经读完了}// 将读取的数据作为一个新的InputStream添加到parts列表中parts.add(new ByteArrayInputStream(partData, 0, read));}long readTime = System.currentTimeMillis()/1000;System.out.println("读取文件耗时"+(readTime-partTime));//上传分片流到miniofor (int i = 0; i < parts.size(); i++) {// 构建每个part的object nameString partObjectName = fileName + ".part" + i;fileList.add(partObjectName);InputStream partStream = parts.get(i);PutObjectArgs args = PutObjectArgs.builder().bucket(minioConfig.getBucketName()).object(partObjectName).stream(partStream, partStream.available(), -1).contentType(file.getContentType()).build();ObjectWriteResponse objectWriteResponse = client.putObject(args);//System.out.println("分片上传结果======++++++"+objectWriteResponse);}long upLoadTime = System.currentTimeMillis()/1000;System.out.println("上传分片耗时"+(upLoadTime-readTime));//关闭主文件输入流和分片输入流inputStream.close();for (InputStream part : parts) {part.close();}//获取需要合并的分片组装成ComposeSourceList<ComposeSource> sourceObjectList = new ArrayList<>(fileList.size());for (String chunk : fileList){sourceObjectList.add(ComposeSource.builder().bucket(minioConfig.getBucketName()).object(chunk).build());}//合并分片ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder().bucket(minioConfig.getBucketName())//合并后的文件的objectname.object(fileName)//指定源文件.sources(sourceObjectList).build();client.composeObject(composeObjectArgs);long mergeTime = System.currentTimeMillis()/1000;System.out.println("合并分片耗时"+(mergeTime-upLoadTime));//删除已经上传的分片,组装成DeleteObjectList<DeleteObject> collect = fileList.stream().map(DeleteObject::new).collect(Collectors.toList());//执行删除RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(minioConfig.getBucketName()).objects(collect).build();Iterable<Result<DeleteError>> results = client.removeObjects(removeObjectsArgs);//如果没有下面try的代码,文件史删除不了的,加上下面的代码就可以删除了try{for (Result<DeleteError> result : results){DeleteError deleteError = result.get();System.out.println("error in deleteing object"+deleteError.objectName()+";"+deleteError.message());}}catch (Exception e){System.out.println("minio删除文件失败");e.printStackTrace();}long deleteTime = System.currentTimeMillis()/1000;System.out.println("删除分片耗时"+(deleteTime-mergeTime));return fileName;}/*** 异步上传大文件采用链式** @param file* @return* @throws Exception*/@Overridepublic String uploadFileAsync(MultipartFile file) throws Exception {long startTimes = System.currentTimeMillis() / 1000;assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);String fileName = FileUploadUtils.extractFilename(file);fileName = minioConfig.getFilePath() + DateUtils.getDate() + "/" + fileName.substring(fileName.lastIndexOf("/") + 1, fileName.length());InputStream inputStream = file.getInputStream();ForkJoinPool pool = new ForkJoinPool();// 创建分片流异步执行任务:读取大文件分成N个流CompletableFuture<List<InputStream>> createPartNumTask = CompletableFuture.supplyAsync(() -> {List<InputStream> parts = new ArrayList<>();long fileSize = file.getSize();int partCount = (int) (fileSize / PART_SIZE);if (fileSize % PART_SIZE > 0) {partCount++;}long startTime = System.currentTimeMillis() / 1000;for (int i = 0; i < partCount; i++) {// 每次只需要从原始文件InputStream中读取指定大小的数据即可byte[] partData = new byte[PART_SIZE];int read = 0;try {read = inputStream.read(partData);} catch (IOException e) {e.printStackTrace();}if (read == -1) {break; // 文件已经读完了}// 将读取的数据作为一个新的InputStream添加到parts列表中parts.add(new ByteArrayInputStream(partData, 0, read));}long endTime = System.currentTimeMillis() / 1000;System.out.println(Thread.currentThread() + "执行创建分片流任务耗时->" + (endTime - startTime) + "秒");return parts;}, pool);//createPartNum关联的异步任务的返回值作为方法入参,传入到thenApply的方法中//thenApply这里实际创建了一个新的CompletableFuture实例String finalFileName = fileName;CompletableFuture<List<String>> createUploadTask = createPartNumTask.thenApply((parts) -> {long startTime = System.currentTimeMillis() / 1000;List<String> fileList = new ArrayList<>();for (int i = 0; i < parts.size(); i++) {// 构建每个part的object nameString partObjectName = finalFileName + ".part" + i;fileList.add(partObjectName);InputStream partStream = parts.get(i);PutObjectArgs args = null;try {args = PutObjectArgs.builder().bucket(minioConfig.getBucketName()).object(partObjectName).stream(partStream, partStream.available(), -1).contentType(file.getContentType()).build();} catch (IOException e) {e.printStackTrace();}try {ObjectWriteResponse objectWriteResponse = client.putObject(args);} catch (ErrorResponseException e) {e.printStackTrace();} catch (InsufficientDataException e) {e.printStackTrace();} catch (InternalException e) {e.printStackTrace();} catch (InvalidKeyException e) {e.printStackTrace();} catch (InvalidResponseException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (NoSuchAlgorithmException e) {e.printStackTrace();} catch (ServerException e) {e.printStackTrace();} catch (XmlParserException e) {e.printStackTrace();}}try {inputStream.close();} catch (IOException e) {e.printStackTrace();}for (InputStream part : parts) {try {part.close();} catch (IOException e) {e.printStackTrace();}}long endTime = System.currentTimeMillis() / 1000;System.out.println(Thread.currentThread() + "执行上传分片流任务->" + (endTime - startTime) + "秒");return fileList;});String finalFileName1 = fileName;CompletableFuture<List<String>> megreTask = createUploadTask.thenApply((fileList) -> {long startTime = System.currentTimeMillis() / 1000;List<ComposeSource> sourceObjectList = new ArrayList<>(fileList.size());for (String chunk : fileList) {sourceObjectList.add(ComposeSource.builder().bucket(minioConfig.getBucketName()).object(chunk).build());}ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder().bucket(minioConfig.getBucketName())//合并后的文件的objectname.object(finalFileName1)//指定源文件.sources(sourceObjectList).build();try {client.composeObject(composeObjectArgs);} catch (ErrorResponseException e) {e.printStackTrace();} catch (InsufficientDataException e) {e.printStackTrace();} catch (InternalException e) {e.printStackTrace();} catch (InvalidKeyException e) {e.printStackTrace();} catch (InvalidResponseException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (NoSuchAlgorithmException e) {e.printStackTrace();} catch (ServerException e) {e.printStackTrace();} catch (XmlParserException e) {e.printStackTrace();}long endTime = System.currentTimeMillis() / 1000;System.out.println(Thread.currentThread() + "执行合并分片任务->" + (endTime - startTime) + "秒");return fileList;});CompletableFuture<Boolean> deleteTask = megreTask.thenApply((fileList) -> {long startTime = System.currentTimeMillis() / 1000;List<DeleteObject> collect = fileList.stream().map(DeleteObject::new).collect(Collectors.toList());RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(minioConfig.getBucketName()).objects(collect).build();Iterable<Result<DeleteError>> results = client.removeObjects(removeObjectsArgs);try {for (Result<DeleteError> result : results) {DeleteError deleteError = result.get();System.out.println("error in deleteing object" + deleteError.objectName() + ";" + deleteError.message());}} catch (Exception e) {System.out.println("minio删除文件失败");e.printStackTrace();}long endTime = System.currentTimeMillis() / 1000;System.out.println(Thread.currentThread() + "执行删除分片任务->" + (endTime - startTime) + "秒");return true;});long endTimes = System.currentTimeMillis() / 1000;System.out.println("主线程执行耗时" + (endTimes - startTimes) + "秒");return fileName;}}

controller 访问接口

@RestController
public class CommonController
{@Value("${minio.domain}")//线上的域名private String minioUrl;@Value("${minio.bucketName}")//桶名private String minioBucketName;/*** 文件上传请求*/@PostMapping("/common/upload/minio")public AjaxResult upload(MultipartFile file){try {String url = sysFileService.uploadFile(file);String filePath = minioUrl.+ "/"+minioBucketName+"/"+ url;AjaxResult ajax = AjaxResult.success();ajax.put("fileName", file.getOriginalFilename());//我这里返回的是视频原来的文件名ajax.put("url", filePath);return ajax;}catch (Exception e){log.error("上传文件失败", e);return AjaxResult.error(e.getMessage());}}/*** 异步文件上传请求*/@PostMapping("/common/upload/minioAsync")public AjaxResult upload(MultipartFile file){try {long startTime = System.currentTimeMillis()/1000;String url = sysFileService.uploadFileAsync(file);System.out.println("文件返回时间_END耗时"+(System.currentTimeMillis()/1000-startTime)+"秒");String filePath = minioUrl.+ "/"+minioBucketName+"/"+ url;AjaxResult ajax = AjaxResult.success();ajax.put("fileName", file.getOriginalFilename());//我这里返回的是视频原来的文件名ajax.put("url", filePath);return ajax;}catch (Exception e){log.error("上传文件失败", e);return AjaxResult.error(e.getMessage());}}
}

优化空间

MinioSysFileServiceImpl 实现类可以采用异步多线程的方式去执行,前端调用直接返回文件路径,不用管文件是否上传完,异步执行完成以后 调用mino 判断是否存在该文件,来判断该文件是否上传完成,另外想要实现,秒传,断点续传,只要加上MD5编码和redis就可以实现。后面我会补充

优化一 异步上传

代码我我已经再上面补充过了,主要讲下异步多线程 

CompletableFuturejava.util.concurrent库在java 8中新增的主要工具,同传统的Future相比,其支持流式计算、函数式编程、完成通知、自定义异常处理等很多新的特性

CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(()->{System.out.println("compute 1");return 1;});CompletableFuture<Integer> future2 = future1.thenApply((p)->{System.out.println("compute 2");return p+10;});System.out.println("result: " + future2.join());

这个例子中展示了任务链

在上面的示例中,future1通过调用thenApply将后置任务连接起来,并形成future2。该示例的最终打印结果为11,可见程序在运行中,future1的结果计算出来后,会传递给通过thenApply连接的任务,从而产生future2的最终结果为1+10=11。当然,在实际使用中,我们理论上可以无限连接后续计算任务,从而实现链条更长的流式计算。

需要注意的是,通过thenApply连接的任务,当且仅当前置任务计算完成时,才会开始后置任务的计算。因此,这组函数主要用于连接前后有依赖的任务链。

利用异步多线程执行流程截图

看一下minio 里有没有文件

这篇关于minio 后端大文件分片上传,合并,删除分片的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

Redis过期删除机制与内存淘汰策略的解析指南

《Redis过期删除机制与内存淘汰策略的解析指南》在使用Redis构建缓存系统时,很多开发者只设置了EXPIRE但却忽略了背后Redis的过期删除机制与内存淘汰策略,下面小编就来和大家详细介绍一下... 目录1、简述2、Redis http://www.chinasem.cn的过期删除策略(Key Expir

使用C#删除Excel表格中的重复行数据的代码详解

《使用C#删除Excel表格中的重复行数据的代码详解》重复行是指在Excel表格中完全相同的多行数据,删除这些重复行至关重要,因为它们不仅会干扰数据分析,还可能导致错误的决策和结论,所以本文给大家介绍... 目录简介使用工具C# 删除Excel工作表中的重复行语法工作原理实现代码C# 删除指定Excel单元

Python中合并列表(list)的六种方法小结

《Python中合并列表(list)的六种方法小结》本文主要介绍了Python中合并列表(list)的六种方法小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋... 目录一、直接用 + 合并列表二、用 extend() js方法三、用 zip() 函数交叉合并四、用

GitLab文件的上传与下载方式

《GitLab文件的上传与下载方式》:本文主要介绍GitLab文件的上传与下载方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录GitLab 项目拉取到本地GitLab 项目上传方法方法 1:本地项目未初始化Git方法 2:本地项目已初始化GitGitLab 上

Nginx 413修改上传文件大小限制的方法详解

《Nginx413修改上传文件大小限制的方法详解》在使用Nginx作为Web服务器时,有时会遇到客户端尝试上传大文件时返回​​413RequestEntityTooLarge​​... 目录1. 理解 ​​413 Request Entity Too Large​​ 错误2. 修改 Nginx 配置2.1

利用Python实现Excel文件智能合并工具

《利用Python实现Excel文件智能合并工具》有时候,我们需要将多个Excel文件按照特定顺序合并成一个文件,这样可以更方便地进行后续的数据处理和分析,下面我们看看如何使用Python实现Exce... 目录运行结果为什么需要这个工具技术实现工具的核心功能代码解析使用示例工具优化与扩展有时候,我们需要将

Python对PDF书签进行添加,修改提取和删除操作

《Python对PDF书签进行添加,修改提取和删除操作》PDF书签是PDF文件中的导航工具,通常包含一个标题和一个跳转位置,本教程将详细介绍如何使用Python对PDF文件中的书签进行操作... 目录简介使用工具python 向 PDF 添加书签添加书签添加嵌套书签Python 修改 PDF 书签Pytho

SpringBoot后端实现小程序微信登录功能实现

《SpringBoot后端实现小程序微信登录功能实现》微信小程序登录是开发者通过微信提供的身份验证机制,获取用户唯一标识(openid)和会话密钥(session_key)的过程,这篇文章给大家介绍S... 目录SpringBoot实现微信小程序登录简介SpringBoot后端实现微信登录SpringBoo

Java应用如何防止恶意文件上传

《Java应用如何防止恶意文件上传》恶意文件上传可能导致服务器被入侵,数据泄露甚至服务瘫痪,因此我们必须采取全面且有效的防范措施来保护Java应用的安全,下面我们就来看看具体的实现方法吧... 目录恶意文件上传的潜在风险常见的恶意文件上传手段防范恶意文件上传的关键策略严格验证文件类型检查文件内容控制文件存储

Python实现获取带合并单元格的表格数据

《Python实现获取带合并单元格的表格数据》由于在日常运维中经常出现一些合并单元格的表格,如果要获取数据比较麻烦,所以本文我们就来聊聊如何使用Python实现获取带合并单元格的表格数据吧... 由于在日常运维中经常出现一些合并单元格的表格,如果要获取数据比较麻烦,现将将封装成类,并通过调用list_exc