当前位置: 首页 > news >正文

Java技术栈 —— 使用MinIO进行大文件分片上传与下载

Java技术栈 —— 使用MinIO进行大文件分片上传与下载

  • 一、搭建MinIO服务器
    • 1.1 安装MinIO
    • 1.2 启动MinIO
  • 二、普通上传
  • 三、分片上传
    • Q&A

大文档和大视频的上传下载,一般都采用分片的方式,就像修长城那样,再长再高的建筑,也是一块砖一块砖垒起来的,伟大的中国人民 —— 也就是你,其实早已深谙分片上传精髓。

一、搭建MinIO服务器

搭建MinIO的过程,涉及到的第一个问题,MinIO是一个对象存储服务(Object Storage Service),对象存储服务和数据库的区别是什么?MinIO是用来存数据的,Database也是用来存数据的,二者有什么区别?这些问题可以看参考文章[1]和[2]。

参考文章或视频链接
[1] 对象存储与传统数据库比较
[2] 对象存储 OSS - 阿里云

1.1 安装MinIO

下列的安装和启动过程废弃,推荐使用docker进行安装

# 使用国内镜像下载MinIO
wget http://dl.minio.org.cn/server/minio/release/linux-amd64/minio 
chmod +x minio
参考文章或视频链接
[1] 基于MinIO搭建高性能文件服务器
[2] MinIO下载和MinIO中国镜像地址 - CSDN
[3] 官方文档重点参考 Deploy MinIO: Single-Node Single-Drive - MinIO
[4] How to install MinIO on Linux – Step by Step Practical Guide

1.2 启动MinIO

# /data/xxfolder/minio是数据存储路径, 你可以自己修改, MinIO 将数据存储在 /data/xxfolder/minio 目录下
MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password ./minio server /data/xxfolder/minio --address ":9000" --console-address ":9001"MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password ./minio server /home/programmer/DevelopEnvironment/MinIO/data --address ":9000" --console-address ":9001"MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password ./minio server /home/lyp/data/minio --address ":9000" --console-address ":9001"
参考文章或视频链接
[1] 基于MinIO搭建高性能文件服务器
[2] MinIO Server - MinIO Documentation

二、普通上传

这个阶段,我们的主要目标还是测试接口,不会特意开发一个前端页面,那如何测试呢?Python的FastAPI框架,可以使用/docs这种地址进行测试,那么Java中,就可以用swagger或其它api的doc框架,但swagger不支持springboot3.0以上的版本,springboot 3.x要使用下面这个库,具体使用方法见参考文章[5]和[6]

<dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>2.1.0</version>
</dependency>

下面是具体代码,我使用的JDK17,字节码版本也是17

server:port: 8080servlet:context-path: /spring-demospring:application:name: business-fileuploadminio:endpoint: http://192.168.206.131:19000accessKey: adminsecretKey: passwordbucket: temp-bucket
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
@Data
public class MinioConfig {@Value("${minio.endpoint}")private String minioEndpoint;@Value("${minio.accessKey}")private String minioAccessKey;@Value("${minio.secretKey}")private String minioSecretKey;@Value("${minio.bucket}")private String minioBucket;@Beanpublic MinioClient minioClient() {// 创建 MinioClient 客户端return MinioClient.builder().endpoint(minioEndpoint).credentials(minioAccessKey, minioSecretKey).build();}
}
import dev.polaris.service.MinioFileUploadService;
import io.minio.RemoveObjectArgs;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;@RestController
@RequestMapping("/file")
public class FileUploadController {private static final Logger log = LoggerFactory.getLogger(FileUploadController.class);@Resourceprivate MinioFileUploadService minioFileUploadService;/*** 上传文件* multipart/form-data 是一种用于 HTTP 协议的媒体类型,它允许将表单数据(包括文件)打包成多个部分(parts)进行传输* @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) 表明该方法只接受 multipart/form-data 类型的请求,其他类型的请求会被拒绝(返回 415 Unsupported Media Type 状态码)。* @PostMapping(value = "/upload") 表示该方法处理所有类型的 POST 请求,而不限制 Content-Type。*/@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)public String upload(@RequestParam("file") MultipartFile file) throws Exception {return minioFileUploadService.upload(file);}/*** 删除文件*/@DeleteMapping("/delete")public void delete(@RequestParam("path") String path) throws Exception {minioFileUploadService.delete(path);}
}
import dev.polaris.config.MinioConfig;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import io.minio.errors.*;
import jakarta.annotation.Resource;
import org.apache.commons.io.FilenameUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;@Service
public class MinioFileUploadService {@Resourceprivate MinioConfig minioConfig;@Resourceprivate MinioClient minioClient;public String upload(MultipartFile file) throws Exception {// 优化文件命名:结合UUID和原始文件名(如果适用)String originalFileName = FilenameUtils.getName(file.getName());// 文件名,保留原始文件名后缀String path = originalFileName + "-" + UUID.randomUUID().toString();// 使用 try-with-resources 确保资源释放try (InputStream inputStream = file.getInputStream()) {// 考虑文件类型检查和安全性验证(示例未实现,实际应用中需要根据具体要求实现)if (!isValidFile(file)) {throw new IllegalArgumentException("Invalid file type or content.");}// 调用 MinIO 客户端上传文件,添加异常处理try {minioClient.putObject(PutObjectArgs.builder()// 存储桶.bucket(minioConfig.getMinioBucket())// 文件名.object(path)// 文件内容,使用文件长度而不是 -1.stream(inputStream, file.getSize(), -1)// 文件类型.contentType(file.getContentType()).build());System.out.println("File uploaded successfully.");} catch (MinioException e) {System.err.println("Error uploading file: " + e.getMessage());// 可以进一步处理异常,如记录日志、重试等}} catch (IOException e) {System.err.println("Error opening file: " + e.getMessage());// 处理文件读取失败的情况}return String.format("%s/%s/%s", minioConfig.getMinioEndpoint(), minioConfig.getMinioBucket(), path);}// 示例性文件验证方法,需根据实际需求实现private boolean isValidFile(MultipartFile file) {// 实现文件类型检查和内容安全验证return true;}public void delete(String path) {try {minioClient.removeObject(RemoveObjectArgs.builder()// 存储桶.bucket(minioConfig.getMinioBucket())// 文件名.object(path).build());} catch (ErrorResponseException | NoSuchAlgorithmException | XmlParserException | InsufficientDataException |InternalException | InvalidKeyException | InvalidResponseException | IOException | ServerException e) {throw new RuntimeException(e);}}
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class BusinessFileuploadApplication {public static void main(String[] args) {SpringApplication.run(BusinessFileuploadApplication.class, args);}}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>dev.polaris</groupId><artifactId>SkillLib</artifactId><version>1.0-SNAPSHOT</version></parent><groupId>dev.polaris</groupId><artifactId>SkillFragment-fileupload</artifactId><version>0.0.1-SNAPSHOT</version><name>business-fileupload</name><description>business-fileupload</description><url/><licenses><license/></licenses><developers><developer/></developers><scm><connection/><developerConnection/><tag/><url/></scm><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>2.1.0</version></dependency><!-- MinIO --><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.5.11</version></dependency><!--lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

启动后,具体访问地址为

http://localhost:{your_port}/spring-demo/swagger-ui/index.html
参考文章或视频链接
[1] SpringBoot 如何生成接口文档,老鸟们都这么玩的!
[2] SpringBoot 整合 swagger2 实现快速测试和快速生成 API 文档
[3] Swagger与SpringBoot版本不兼容
[4] Spring Boot 3.0.0 support #4041
[5] 使用 Spring Doc 为 Spring REST API 生成 OpenAPI 3.0 文档
[6] Swagger UI文件上传

三、分片上传

FastDFS应被放弃,它维护难,使用难,性能比不过Minio,见参考文章[2]。如果想用直接的实现,看参考文章[6],只是没有实现分片合并下载与视频定位,但也可以快速开发,非常方便。

下面是具体的分片上传业务逻辑。
(1)未超过分片阈值的文件,直接上传。
(2)超过分片阈值的文件,分片上传,跳转步骤(3)
(3)前端计算整个文件的sizeMD5值,后端返回chunk数与文件序号fileNumber
(4)前端逐chunk向后端发送数据,若遭遇中断,如关闭浏览器页面,后端服务被停,跳转步骤(5)
(5)继续上传。前端向后端发送继续上传请求,后端返回待继续上传数据的chunk

public class uploadPartDemo {public static void main(String[] args) throws InsufficientDataException, IOException, NoSuchAlgorithmException, InvalidKeyException, XmlParserException, InternalException {MinioAsyncClient minioAsyncClient = MinioAsyncClient.builder().endpoint("http://localhost:9000")  // MinIO服务地址.credentials("", "")  // 默认的管理员凭证.build();String bucketName = "{yourbucket}";String region = null;String objectName = "{yourObjectName}";// 1.初始化分片上传任务CompletableFuture<CreateMultipartUploadResponse> uploadIdFuture = minioAsyncClient.createMultipartUploadAsync(bucketName, region, objectName, null, null);// 获取 uploadIdString uploadId = uploadIdFuture.join().result().uploadId();// 2.读取文件并拆分为多个分片(Part)List<CompletableFuture<Part>> futures = new ArrayList<>();List<Part> uploadedParts = new ArrayList<>();String imgPath = "yourfile";try (InputStream is = Files.newInputStream(Paths.get(imgPath))) {int partNumber = 1;long partSize = 5 * 1024 * 1024; // 5MB 每片byte[] buffer = new byte[(int) partSize];while (true) {int read = is.read(buffer);if (read <= 0) break;final int partLen = read;final int partNum = partNumber++;CompletableFuture<UploadPartResponse> future = minioAsyncClient.uploadPartAsync(bucketName,null,objectName,Arrays.copyOf(buffer, partLen), // 当前分片数据partLen,uploadId,partNum,null,null);// 如果要由前端发起合并操作,应该有一个全局的hashMap, 来记录哪些分片已上传, 不应该让前端记录// 转换为 Part 对象(含 ETag)CompletableFuture<Part> partFuture = future.thenApply(resp ->new Part(partNum, resp.etag()));futures.add(partFuture);}}// 3.等待所有分片上传完成// 合并所有 CompletableFutureCompletableFuture<Void> allDone = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));allDone.join(); // 等待全部完成// 收集结果for (CompletableFuture<Part> f : futures) {uploadedParts.add(f.join());}// 4.完成分片上传(合并)//目前仅做了最大1000分片Part[] parts = new Part[1000];// 查询上传后的分片数据CompletableFuture<ListPartsResponse> partResult = minioAsyncClient.listPartsAsync(bucketName, null, objectName, 1000, 0, uploadId, null, null);ListPartsResponse listPartsResponse = partResult.join();int partNumber = 0;for (Part part : listPartsResponse.result().partList()) {parts[partNumber] = new Part(partNumber, part.etag());partNumber++;}
//        CompletableFuture<ObjectWriteResponse> result = minioAsyncClient.completeMultipartUploadAsync(bucketName, region, objectName, uploadId, parts, null, null);
//        ObjectWriteResponse response = result.join();//        System.out.println("上传完成,ETag: " + response.etag());// 5.异常处理与清理(可选)}
}

以下是流程图

Q&A

MinioAsyncClient和MinioClient的联系和区别

参考文章或视频链接
[1] 大厂的中小型文件存储技术方案选型
[2] MinIO很强-让我放弃FastDFS拥抱MinIO的8个理由
[3] Java大文件分片上传(minio版),超详细
[4] 使用Minio实现大文件的分片上传
[5] SpringBoot通过Minio实现大文件分片上传
[6] 直接可用实现 SpringBoot整合minio实现断点续传、分片上传(附源码)
[7] Minio进阶
[8] 图片分片上传功能的实践
[9] (分片上传),需求图片视频文件太大,需要前端做分片上传
[10] 陈天技术摘抄
[11] Spring Boot与MinIO融合:高效实现文件分片上传技术
[12] 客户端直连S3实现分片续传思路与实践
[13] 基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件
http://www.dtcms.com/a/449801.html

相关文章:

  • `modprobe`命令 与 `KVM`模块 笔记251006
  • 山东省建设监理协会网站打不开赣州招聘网最新招聘
  • 贵阳网站建设设计个人网页设计作品集分析
  • 音乐介绍网站怎么做做暧暧小视频网站
  • 公网带宽1m能建设电商网站吗wordpress新建数据库
  • C57-断言函数assert
  • 网站的制作建站人汽车业务网站开发公司
  • 详解指针2
  • 第一章 :感知机(上)
  • 做网站都要会些什么设计网站建设合同书6
  • 网站开发工程师 能做什么响应式布局的概念
  • 反激开关电源
  • 长沙网站建设外贸0基础做电商从何下手
  • vs2015做网站做民宿需要和多家网站合作吗
  • 集团型网站建设室内设计平面图简单
  • 比利时网站后缀用php做的网站前后台模板
  • 视频网站如何做微信营销长春火车站和高铁站是一个站吗
  • steamdeck 龙神Ryujinx模拟器输入控制器无效
  • 彩票网站网站建设路由器上建网站
  • 网站建设如何推广业务如何备份wordpress网页
  • linux学习笔记(16)进程间通信——管道
  • 巩义网站建设定制电子商城系统开发
  • AI编程开发系统020-基于Vue+SpringBoot的景云手机维修管理网站系统(源码+部署说明+演示视频+源码介绍+lw)
  • 【算法】【优选算法】BFS 解决拓扑排序
  • 做网站3年3万wordpress搭建ctf
  • 网站建设设计公司哪家好企业网站html源代码
  • 质因数分解的数学奥秘与高效解法(洛谷P1075)
  • 站长工具seo综合查询降级网站后期的维护
  • 电商网站建设资讯淘宝网站图片维护怎么做
  • 机器学习完整流程详解