目录
[一.什么是 MinIO ?](#一.什么是 MinIO ?)
[二.MinIO 的安装与部署:](#二.MinIO 的安装与部署:)
[三.Spring Cloud 集成 MinIO:](#三.Spring Cloud 集成 MinIO:)
[(1)MinioConfig 配置类:](#(1)MinioConfig 配置类:)
[(2)Controller 接口定义:](#(2)Controller 接口定义:)
[【3】获取文件默认存储目录路径 年/ 月/ 日:](#【3】获取文件默认存储目录路径 年/ 月/ 日:)
[(4) 三层架构------清除分块文件:](#(4) 三层架构——清除分块文件:)

引言:
一个计算机无法存储海量的文件,通过网络将若干计算机组织起来共同去存储海量的文件,去接收海量用户的请求,这些组织起来的计算机通过网络进行通信,如下图:

好处:
1、一台计算机的文件系统处理能力扩充到多台计算机同时处理。
2、一台计算机挂了还有另外副本计算机提供数据。
3、每台计算机可以放在不同的地域,这样用户就可以就近访问,提高访问速度。
市面上有哪些分布式文件系统的产品呢?
NFS:在客户端上映射NFS服务器的驱动器,客户端通过网络访问NFS服务器的硬盘完全透明。
GFS:采用主从结构,一个GFS集群由一个master和大量的chunkserver组成,master存储了数据文件的元数据,一个文件被分成了若干块存储在多个chunkserver中。用户从master中获取数据元信息,向chunkserver存储数据。
HDFS:是Hadoop抽象文件系统的一种实现,高度容错性的系统,适合部署在廉价的机器上。能提供高吞吐量的数据访问,非常适合大规模数据集上的应用,HDFS的文件分布在集群机器上,同时提供副本进行容错及可靠性保证。例如客户端写入读取文件的直接操作都是分布在集群各个机器上的,没有单点性能压力。
一.什么是 MinIO ?
MinIO是一个高性能、分布式对象存储系统,专为大规模数据基础设施而设计,它兼容亚马逊 S3 云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等。
它一大特点就是轻量,使用简单,功能强大,支持各种平台,单个文件最大5TB,兼容 Amazon S3接口,提供了 Java、Python、GO等多版本SDK支持。
中文:https://www.minio.org.cn/,http://docs.minio.org.cn/docs/
MinIO的主要特点包括:
- 高性能:作为世界上最快的对象存储之一,MinIO可以支持高达每秒数百GB的吞吐量
- 简单易用:简单的命令行和Web界面,几分钟内即可完成安装和配置
- 云原生:从公有云到私有云再到边缘计算,MinIO都能完美运行
- 开源:采用Apache V2开源协议,可以自由使用和修改
- 轻量级:单个二进制文件即可运行,没有外部依赖
MinIO集群采用去中心化共享架构,每个结点是对等关系,通过Nginx可对MinIO进行负载均衡访问。
去中心化有什么好处?
在大数据领域,通常的设计理念都是无中心和分布式。Minio分布式模式可以帮助你搭建一个高可用的对象存储服务,你可以使用这些存储设备,而不用考虑其真实物理位置。
它将分布在不同服务器上的多块硬盘组成一个对象存储服务。由于硬盘分布在不同的节点上,分布式Minio避免了单点故障。如下图:
Minio使用纠删码技术来保护数据,它是一种恢复丢失和损坏数据的数学算法,它将数据分块冗余的分散存储在各各节点的磁盘上,所有的可用磁盘组成一个集合,上图由8块硬盘组成一个集合,当上传一个文件时会通过纠删码算法计算对文件进行分块存储,除了将文件本身分成4个数据块,还会生成4个校验块,数据块和校验块会分散的存储在这8块硬盘上。
使用纠删码的好处是即便丢失一半数量(N / 2)的硬盘,仍然可以恢复数据。 比如上边集合中有4个以内的硬盘损害仍可保证数据恢复,不影响上传和下载,如果多于一半的硬盘坏了则无法恢复。
二.MinIO 的安装与部署:
下边在本机演示MinIO恢复数据的过程,在本地创建4个目录表示4个硬盘。

下载minio,下载地址在Minio下载地址 :

随后CMD进入有minio.exe的目录,运行下边的命令:( 替换自己的安装地址)
bash
minio.exe server E:\minio_data\data1 E:\minio_data\data2 E:\minio_data\data3 E:\minio_data\data4
启动结果如下:

说明如下:
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| WARNING: MINIO_ACCESS_KEY and MINIO_SECRET_KEY are deprecated. Please use MINIO_ROOT_USER and MINIO_ROOT_PASSWORD Formatting 1st pool, 1 set(s), 4 drives per set. WARNING: Host local has more than 2 drives of set. A host failure will result in data becoming unavailable. WARNING: Detected default credentials 'minioadmin:minioadmin', we recommend that you change these values with 'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD' environment variables |1)老版本使用的MINIO_ACCESS_KEY 和 MINIO_SECRET_KEY不推荐使用,推荐使用MINIO_ROOT_USER 和MINIO_ROOT_PASSWORD设置账号和密码。
2)pool即minio节点组成的池子,当前有一个pool和4个硬盘组成的set集合
3)因为集合是4个硬盘,大于2的硬盘损坏数据将无法恢复。
4)账号和密码默认为minioadmin、minioadmin,可以在环境变量中设置通过'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD' 进行设置。
下边输入 http://192.168.88.1:9000 进行登录(看自己的地址),账号和密码均为为:minioadmin


输入bucket的名称,点击"CreateBucket",创建成功:

随后就可以进行上传、删除等操作了。
三.Spring Cloud 集成 MinIO:
1.前提准备:
(1)安装依赖:
XML
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.2</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!--根据扩展名取mimetype-->
<dependency>
<groupId>com.j256.simplemagic</groupId>
<artifactId>simplemagic</artifactId>
<version>1.17</version>
</dependency>
(2)配置MinIO连接:
因为我们要上传普通文件与视频文件,所以创建 mediafiles(普通文件) 与 video(视频文件) 两个 buckets 。
在 bootstrap.yml
中添加配置:
XML
minio:
endpoint: http://192.168.56.1:9000
accessKey: minioadmin
secretKey: minioadmin
bucket:
files: mediafiles
videofiles: video
需要三个参数才能连接到minio服务。
|------------|------------------------------|
| 参数 | 说明 |
| Endpoint | 对象存储服务的URL |
| Access Key | Access key就像用户ID,可以唯一标识你的账户。 |
| Secret Key | Secret key是你账户的密码。 |
随后也可以添加对上传文件的限制:
java
spring:
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
max-file-size:单个文件的大小限制
Max-request-size: 单次请求的大小限制
(3)修改bucket的访问权限:
点击"Manage"修改bucket的访问权限:

选择public权限:

2.测试上传、删除、下载文件:
首先初始化 minioClient:
java
MinioClient minioClient =
MinioClient.builder()
.endpoint("http://192.168.56.1:9000")
.credentials("minioadmin", "minioadmin")
.build();
随后设置contentType可以通过com.j256.simplemagic.ContentType枚举类查看常用的mimeType(媒体类型)。通过扩展名得到mimeType,代码如下:
java
// 根据扩展名取出mimeType
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(".mp4");
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;// 通用mimeType,字节流
校验文件的完整性,对文件计算出md5值,比较原始文件的md5和目标文件的md5,一致则说明完整:
java
//校验文件的完整性对文件的内容进行md5
FileInputStream fileInputStream1 = new FileInputStream(new File("D:\\develop\\upload\\1.mp4"));
String source_md5 = DigestUtils.md5Hex(fileInputStream1);
FileInputStream fileInputStream = new FileInputStream(new File("D:\\develop\\upload\\1a.mp4"));
String local_md5 = DigestUtils.md5Hex(fileInputStream);
if(source_md5.equals(local_md5)){
System.out.println("下载成功");
}
下面是完整的测试代码:
java
package com.xuecheng.media;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import io.minio.*;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.compress.utils.IOUtils;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import java.io.*;
public class MinioTest {
// 初始化minioClient
MinioClient minioClient =
MinioClient.builder()
.endpoint("http://192.168.56.1:9000")
.credentials("minioadmin", "minioadmin")
.build();
@Test
public void test_upload() throws Exception {
// 通过扩展名得到媒体资源类型 mimeType
// 根据扩展名取出mimeType
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(".mp4");
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;// 通用mimeType,字节流
if(extensionMatch != null){
mimeType = extensionMatch.getMimeType();
}
// 上传文件的参数信息
UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
.bucket("testbucket")// 桶
.filename("C:\\Users\\Eleven\\Videos\\4月16日.mp4") // 指定本地文件路径
// .object("1.mp4")// 对象名在桶下存储该文件
.object("test/01/1.mp4")// 对象名 放在子目录下
.contentType(mimeType)// 设置媒体文件类型
.build();
// 上传文件
minioClient.uploadObject(uploadObjectArgs);
}
// 删除文件
@Test
public void test_delete() throws Exception {
//RemoveObjectArgs
RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder()
.bucket("testbucket")
.object("test/01/1.mp4")
.build();
// 删除文件
minioClient.removeObject(removeObjectArgs);
}
// 查询文件 从minio中下载
@Test
public void test_getFile() throws Exception {
GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket("testbucket").object("test/01/1.mp4").build();
//查询远程服务获取到一个流对象
FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
//指定输出流
FileOutputStream outputStream = new FileOutputStream(new File("D:\\develop\\upload\\1a.mp4"));
IOUtils.copy(inputStream,outputStream);
//校验文件的完整性对文件的内容进行md5
FileInputStream fileInputStream1 = new FileInputStream(new File("D:\\develop\\upload\\1.mp4"));
String source_md5 = DigestUtils.md5Hex(fileInputStream1);
FileInputStream fileInputStream = new FileInputStream(new File("D:\\develop\\upload\\1a.mp4"));
String local_md5 = DigestUtils.md5Hex(fileInputStream);
if(source_md5.equals(local_md5)){
System.out.println("下载成功");
}
}
}
3.图片操作:

上传课程图片总体上包括两部分:
1、上传课程图片前端请求媒资管理服务将文件上传至分布式文件系统,并且在媒资管理数据库保存文件信息。
2、上传图片成功保存图片地址到课程基本信息表中。
详细流程如下:
1、前端进入上传图片界面
2、上传图片,请求媒资管理服务。
3、媒资管理服务将图片文件存储在MinIO。
4、媒资管理记录文件信息到数据库。
5、前端请求内容管理服务保存课程信息,在内容管理数据库保存图片地址。
首先在minio配置bucket,bucket名称为:mediafiles,并设置bucket的权限为公开。

在nacos配置中minio的相关信息,进入media-service-dev.yaml:

配置信息如下:
bash
minio:
endpoint: http://192.168.56.1:9000
accessKey: minioadmin
secretKey: minioadmin
bucket:
files: mediafiles
videofiles: video
(1)MinioConfig 配置类:
java
package com.xuecheng.media.config;
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author eleven
* @version 1.0
* @description TODO
* @date 2025/6/4 15:00
*/
@Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
@Bean
public MinioClient minioClient() {
MinioClient minioClient = MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
return minioClient;
}
}
(2)Controller 接口定义:
根据需求分析,下边进行接口定义,此接口定义为一个通用的上传文件接口,可以上传图片或其它文件。
首先分析接口:
请求地址:/media/upload/coursefile
请求内容:**Content-Type:**multipart/form-data;
因为无法直接获取上传文件的本地路径,所以创建临时文件作为中转,临时文件名以"minio"为前缀,".temp"为后缀。
java
package com.xuecheng.media.api;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.service.MediaFileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
/**
* @description 媒资文件管理接口
* @author eleven
* @version 1.0
*/
@Tag(name = "媒资文件管理接口",description = "媒资文件管理接口")
@RestController
public class MediaFilesController {
@Autowired
MediaFileService mediaFileService;
@Operation(summary = "上传图片")
@RequestMapping(value = "/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile filedata) throws IOException {
// 准备上传文件的信息
UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
// 原始文件名称
uploadFileParamsDto.setFilename(filedata.getOriginalFilename());
// 文件大小
uploadFileParamsDto.setFileSize(filedata.getSize());
// 文件类型
uploadFileParamsDto.setFileType("001001"); // 自定义的字典,001001代表图片
// 因为无法直接获得该文件的路径,所以创建一个临时文件
File tempFile = File.createTempFile("minio", ".temp");
filedata.transferTo(tempFile); // 拷贝文件
Long companyId = 1232141425L;
// 文件路径
String localFilePath = tempFile.getAbsolutePath();
//调用service上传图片
UploadFileResultDto uploadFileResultDto = mediaFileService.uploadFile(companyId, uploadFileParamsDto, localFilePath);
return uploadFileResultDto;
}
}
(3)Service开发:
这里分几个方法进行开发:
【1】根据扩展名获取mimeType:
java
/**
* 根据扩展名获取mimeType
*/
private String getMimeType(String extension){
if(extension == null){
extension = "";
}
// 根据扩展名取出mimeType
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType,字节流
if(extensionMatch != null){
mimeType = extensionMatch.getMimeType();
}
return mimeType;
}
【2】将文件上传到minio:
java
/**
* 将文件上传到minio
* @param localFilePath 文件本地路径
* @param mimeType 媒体类型
* @param bucket 桶
* @param objectName 对象名
* @return
*/
public boolean addMediaFilesToMinIO(String localFilePath,String mimeType,String bucket, String objectName){
try {
UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
.bucket(bucket)//桶
.filename(localFilePath) //指定本地文件路径
.object(objectName)//对象名 放在子目录下
.contentType(mimeType)//设置媒体文件类型
.build();
//上传文件
minioClient.uploadObject(uploadObjectArgs);
log.debug("上传文件到minio成功,bucket:{},objectName:{}",bucket,objectName);
return true;
} catch (Exception e) {
e.printStackTrace();
log.error("上传文件出错,bucket:{},objectName:{},错误信息:{}",bucket,objectName,e.getMessage());
}
return false;
}
【3】获取文件默认存储目录路径 年/ 月/ 日:
java
/**
* 获取文件默认存储目录路径 年/月/日
*/
private String getDefaultFolderPath() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String folder = sdf.format(new Date()).replace("-", "/")+"/";
return folder;
}
【4】获取文件的md5:
java
/**
* 获取文件的md5
*/
private String getFileMd5(File file) {
try (FileInputStream fileInputStream = new FileInputStream(file)) {
String fileMd5 = DigestUtils.md5Hex(fileInputStream);
return fileMd5;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
随后 MediaFileServiceImpl 类创建方法实现上传图片并校验是否成功上传:
如果在uploadFile方法上添加@Transactional,当调用uploadFile方法前会开启数据库事务,如果上传文件过程时间较长那么数据库的事务持续时间就会变长,这样数据库链接释放就慢,最终导致数据库链接不够用。
我们只将addMediaFilesToDb方法添加事务控制即可,将该方法提取出来在 MediaFileTransactionalServiceImpl 中创建方法。
java
@Autowired
MediaFilesMapper mediaFilesMapper;
@Autowired
MinioClient minioClient;
@Autowired
private MediaFileTransactionalServiceImpl transactionalService; // 事务,操作数据库
//存储普通文件
@Value("${minio.bucket.files}")
private String bucket_mediafiles;
//存储视频
@Value("${minio.bucket.videofiles}")
private String bucket_video;
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) {
// 文件名
String filename = uploadFileParamsDto.getFilename();
// 先得到扩展名
String extension = filename.substring(filename.lastIndexOf("."));
// 根据扩展名得到mimeType
String mimeType = getMimeType(extension);
// 子目录
String defaultFolderPath = getDefaultFolderPath();
// 文件的md5值
String fileMd5 = getFileMd5(new File(localFilePath));
String objectName = defaultFolderPath+fileMd5+extension;
// 上传文件到minio
boolean result = addMediaFilesToMinIO(localFilePath, mimeType, bucket_mediafiles, objectName);
if(!result){
XueChengPlusException.cast("上传文件失败");
}
try {
// 调用事务方法
MediaFiles mediaFiles = transactionalService.addMediaFilesToDbWithTransaction(
companyId, fileMd5, uploadFileParamsDto, bucket_mediafiles, objectName);
UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
return uploadFileResultDto;
} catch (Exception e) {
// 如果事务失败,尝试删除已上传的MinIO文件
try {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucket_mediafiles)
.object(objectName)
.build());
} catch (Exception ex) {
log.error("回滚时删除MinIO文件失败", ex);
}
throw e;
}
}
而为了回滚数据库,我们在新建的 MediaFileTransactionalServiceImpl 类中创建:
java
package com.xuecheng.media.service.impl;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.service.MediaFileTransactionalService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
@Slf4j
@Service
public class MediaFileTransactionalServiceImpl implements MediaFileTransactionalService {
@Autowired
private MediaFilesMapper mediaFilesMapper;
/**
* @description 将文件信息添加到文件表
* @param companyId 机构id
* @param fileMd5 文件md5值
* @param uploadFileParamsDto 上传文件的信息
* @param bucket 桶
* @param objectName 对象名称
* @return com.xuecheng.media.model.po.MediaFiles
* @author Mr.M
* @date 2022/10/12 21:22
*/
@Override
@Transactional(rollbackFor = Exception.class)
public MediaFiles addMediaFilesToDbWithTransaction(Long companyId, String fileMd5,
UploadFileParamsDto uploadFileParamsDto,
String bucket, String objectName) {
// 原addMediaFilesToDb方法内容
MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
if(mediaFiles == null){
mediaFiles = new MediaFiles();
BeanUtils.copyProperties(uploadFileParamsDto,mediaFiles);
mediaFiles.setId(fileMd5);
mediaFiles.setCompanyId(companyId);
mediaFiles.setBucket(bucket);
mediaFiles.setFilePath(objectName);
mediaFiles.setFileId(fileMd5);
mediaFiles.setUrl("/" + bucket + "/" + objectName);
mediaFiles.setCreateDate(LocalDateTime.now());
mediaFiles.setStatus("1");
mediaFiles.setAuditStatus("002003");
int insert = mediaFilesMapper.insert(mediaFiles);
if(insert <= 0){
log.error("向数据库保存文件失败,bucket:{},objectName:{}",bucket,objectName);
throw new XueChengPlusException("保存文件信息失败"); // 触发回滚
}
}
return mediaFiles;
}
}
4.视频操作:
(1)断点上传:
通常视频文件都比较大,所以对于媒资系统上传文件的需求要满足大文件的上传要求。http协议本身对上传文件大小没有限制,但是客户的网络环境质量、电脑硬件环境等参差不齐,如果一个大文件快上传完了网断了没有上传完成,需要客户重新上传,用户体验非常差,所以对于大文件上传的要求最基本的是断点续传。
什么是断点续传?
引用百度百科:断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性。
断点续传流程如下图:

流程如下:
1、前端上传前先把文件分成块
2、一块一块的上传,上传中断后重新上传,已上传的分块则不用再上传
3、各分块上传完成最后在服务端合并文件
(2)测试文件分块上传与合并:
为了更好的理解文件分块上传的原理,下边用java代码测试文件的分块与合并。
文件分块的流程如下:
1、获取源文件长度
2、根据设定的分块文件的大小计算出块数
3、从源文件读数据依次向每一个块文件写数据。
而为了实现文件分块,需要使用 RandomAccessFile。
RandomAccessFile
是 Java 提供的 随机访问文件 类,允许对文件进行 任意位置读写,适用于大文件分块、断点续传、数据库索引等场景。构造方法:
java// 模式: // "r" : 只读 // "rw": 读写(文件不存在则自动创建) // "rws": 读写 + 同步写入元数据(强制刷盘) // "rwd": 读写 + 同步写入文件内容(强制刷盘) RandomAccessFile raf = new RandomAccessFile(File file, String mode); RandomAccessFile raf = new RandomAccessFile(String path, String mode);
操作指针:
方法 作用 long getFilePointer()
返回当前指针位置 void seek(long pos)
移动指针到指定位置 long length()
返回文件长度 void setLength(long newLength)
扩展/截断文件 读写数据:
方法 说明 int read()
读取1字节(返回 0~255
,失败返回-1
)int read(byte[] b)
读取数据到字节数组 readInt()
,readDouble()
读取基本类型 write(byte[] b)
写入字节数组 writeInt()
,writeUTF()
写入基本类型或字符串
【1】分块上传:
流程分析:
①初始化阶段
- 设置源文件路径 和分块存储目录,自动创建不存在的目录
- 定义每个分块大小为1MB,并根据文件总大小计算所需分块数量
- 初始化1KB的读写缓冲区:byte[] b = new byte[1024];
②文件读取准备
- 使用 RandomAccessFile 以只读模式(r)打开源文件
- 文件指针自动记录读取位置,确保连续性
③分块处理核心流程
- 循环创建每个分块文件,先删除已存在的旧文件
- 为每个分块创建新的 RandomAccessFile 写入流(rw)
- 通过缓冲区循环读取源文件数据,写入分块文件
- 实时检查分块文件大小,达到1MB立即切换下一个分块
④收尾工作
- 每个分块写入完成后立即关闭文件流
- 所有分块处理完毕后关闭源文件流
java
package com.xuecheng.media;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;
/**
* @author eleven
* @version 1.0
* @description 大文件处理测试
*/
public class BigFileTest {
/**
* 测试文件分块方法
*/
@Test
public void testChunk() throws IOException {
File sourceFile = new File("d:/develop/bigfile_test/nacos.mp4");
String chunkPath = "d:/develop/bigfile_test/chunk/";
File chunkFolder = new File(chunkPath); // 分块地址文件
if (!chunkFolder.exists()) {
chunkFolder.mkdirs(); // 不存在则创建
}
// 分块大小
long chunkSize = 1024 * 1024 * 1;
// 分块数量 (向上取整)
long chunkNum = (long) Math.ceil(sourceFile.length() * 1.0 / chunkSize);
System.out.println("分块总数:" + chunkNum);
// 缓冲区大小
byte[] b = new byte[1024];
// 使用流从源文件中读数据,向分块文件中写数据
// 使用RandomAccessFile访问文件
RandomAccessFile raf_read = new RandomAccessFile(sourceFile, "r"); // r:允许对文件进行读操作
// 分块
for (int i = 0; i < chunkNum; i++) {
// 创建分块文件
File file = new File(chunkPath + i);
if(file.exists()){
file.delete(); // 确保每个文件都是重新生成
}
boolean newFile = file.createNewFile(); // 在指定路径下创建一个空的物理文件
// 如果文件已存在(尽管前面有 file.delete(),但极端情况下可能删除失败)
// createNewFile() 会返回 false,防止后续 RandomAccessFile 仍会覆盖写入
if (newFile) {
// 在 RandomAccessFile 中,文件指针(File Pointer) 会自动记录当前读写位置
// 确保每次 read() 或 write() 操作都会从上次结束的位置继续。
// 创建分块文件写入流,向分块文件中写数据
RandomAccessFile raf_write = new RandomAccessFile(file, "rw"); // rw:允许对文件进行读写操作
int len = -1;
// 从源文件(raf_read)读取数据到缓冲区 byte[] b,每次最多读取 1024 字节(缓冲区大小)
// len 返回实际读取的字节数,如果 len = -1 表示源文件已读完
while ((len = raf_read.read(b)) != -1) {
// 将缓冲区 b 中的数据读取并写入目标文件(分块文件file),写入范围是 [0, len),确保只写入有效数据
raf_write.write(b, 0, len);
// 确保每个分块文件不超过指定大小chunkSize
if (file.length() >= chunkSize) {
break;
}
}
raf_write.close();
System.out.println("完成分块"+i);
}
}
raf_read.close();
}
}
【2】合并分块文件:
流程分析:
- 初始化阶段:检查并创建合并文件,初始化写入流和缓冲区(1KB),获取所有分块文件并按文件名数字排序,确保按原始顺序合并。
- 文件合并阶段 :
- 遍历每个分块文件,使用
RandomAccessFile
读取数据到缓冲区- 通过
seek(0)
确保每次从分块文件头部读取- 将缓冲区数据写入合并文件,循环直到当前分块读取完毕
- 资源释放:每个分块处理完后立即关闭流,全部合并后关闭写入流。
- 完整性校验 :
- 使用
FileInputStream
读取原始文件和合并文件的二进制内容- 通过
DigestUtils.md5Hex()
计算MD5哈希值比对- 完全一致则判定合并成功
java
package com.xuecheng.media;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;
/**
* @author eleven
* @version 1.0
* @description 大文件处理测试
*/
public class BigFileTest {
/**
* 测试文件合并方法
*/
@Test
public void testMerge() throws IOException {
// 块文件目录
File chunkFolder = new File("d:/develop/bigfile_test/chunk/");
// 原始文件
File originalFile = new File("d:/develop/bigfile_test/nacos.mp4");
// 合并文件
File mergeFile = new File("d:/develop/bigfile_test/nacos01.mp4");
if (mergeFile.exists()) {
mergeFile.delete();
}
// 创建新的合并文件
mergeFile.createNewFile();
// 用于写文件
RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");
// 指针指向文件顶端
raf_write.seek(0);
// 缓冲区
byte[] b = new byte[1024];
// 分块列表
File[] fileArray = chunkFolder.listFiles();
// 转成集合,便于排序
List<File> fileList = Arrays.asList(fileArray);
// 从小到大排序
Collections.sort(fileList, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName());
}
});
// 合并文件
for (File chunkFile : fileList) {
RandomAccessFile raf_read = new RandomAccessFile(chunkFile, "rw");
int len = -1;
while ((len = raf_read.read(b)) != -1) {
raf_write.write(b, 0, len);
}
raf_read.close();
}
raf_write.close();
//校验文件
try (
FileInputStream fileInputStream = new FileInputStream(originalFile);
FileInputStream mergeFileStream = new FileInputStream(mergeFile);
) {
//取出原始文件的md5
String originalMd5 = DigestUtils.md5Hex(fileInputStream);
//取出合并文件的md5进行比较
String mergeFileMd5 = DigestUtils.md5Hex(mergeFileStream);
if (originalMd5.equals(mergeFileMd5)) {
System.out.println("合并文件成功");
} else {
System.out.println("合并文件失败");
}
}
}
}
(3)使用MinIO合并分块:
【1】将分块文件上传至minio:
java
// 测试方法:将本地分块文件上传至MinIO对象存储
@Test
public void uploadChunk() {
// 1. 初始化分块文件目录
String chunkFolderPath = "D:\\develop\\upload\\chunk\\"; // 本地分块文件存储路径
File chunkFolder = new File(chunkFolderPath); // 创建文件对象表示该目录
// 2. 获取所有分块文件
File[] files = chunkFolder.listFiles(); // 列出目录下所有文件(分块文件)
// 3. 遍历并上传每个分块文件
for (int i = 0; i < files.length; i++) {
try {
// 3.1 构建上传参数对象
UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
.bucket("testbucket") // 设置目标存储桶名称
.object("chunk/" + i) // 设置对象存储路径(格式:chunk/0, chunk/1...)
.filename(files[i].getAbsolutePath()) // 设置本地文件绝对路径
.build(); // 构建上传参数
// 3.2 执行上传操作
minioClient.uploadObject(uploadObjectArgs); // 调用MinIO客户端上传文件
// 3.3 打印上传成功日志
System.out.println("上传分块成功" + i); // 标识当前上传的分块序号
} catch (Exception e) {
// 3.4 捕获并打印上传异常
e.printStackTrace(); // 打印异常堆栈(如网络问题、权限不足等)
}
}
}
【2】合并文件,要求分块文件最小5M:
java
//合并文件,要求分块文件最小5M
@Test
public void test_merge() throws Exception {
List<ComposeSource> sources =
Stream.iterate(0, i -> ++i) // 从0开始生成无限递增序列
.limit(6) // 限制取前6个元素(0-5)
.map(i -> ComposeSource.builder() // 将每个整数映射为ComposeSource对象
.bucket("testbucket") // 设置存储桶名
.object("chunk/" + i) // 设置分块对象路径
.build()) // 构建ComposeSource
.collect(Collectors.toList()); // 收集为List
// 合并操作构建对象
ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
.bucket("testbucket")
.object("merge01.mp4") // 合并后的文件名
.sources(sources).build(); // 要合并的分块文件列表
minioClient.composeObject(composeObjectArgs);
}
【3】清除分块文件:
java
// 测试方法:清除MinIO中的分块文件
@Test
public void test_removeObjects() {
// 1. 准备待删除的分块文件列表
// 使用Stream API生成0-5的序列,构建DeleteObject列表
List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i) // 生成无限递增序列(0,1,2...)
.limit(6) // 限制只处理前6个分块(0-5)
.map(i -> new DeleteObject( // 将每个数字转为DeleteObject
"chunk/".concat(Integer.toString(i)) // 构造分块路径格式:chunk/0, chunk/1...
))
.collect(Collectors.toList()); // 收集为List
// 2. 构建删除参数对象
RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
.bucket("testbucket") // 设置目标存储桶
.objects(deleteObjects) // 设置要删除的对象列表
.build(); // 构建参数对象
// 3. 执行批量删除操作
// 返回一个包含删除结果的Iterable对象(可能包含成功/失败信息)
Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
// 4. 处理删除结果(检查是否有删除失败的记录)
results.forEach(r -> {
DeleteError deleteError = null;
try {
// 获取单个删除操作的结果(如果删除失败会抛出异常)
deleteError = r.get();
// 如果deleteError不为null,表示对应文件删除失败
} catch (Exception e) {
// 打印删除过程中出现的异常(如网络问题、权限不足等)
e.printStackTrace();
}
});
}
(4)三层架构------上传分块:
下图是上传视频的整体流程:
1、前端对文件进行分块。
2、前端上传分块文件前请求媒资服务检查文件是否存在,如果已经存在则不再上传。
3、如果分块文件不存在则前端开始上传
4、前端请求媒资服务上传分块。
5、媒资服务将分块上传至MinIO。
6、前端将分块上传完毕 请求媒资服务合并分块。
7、媒资服务判断分块上传完成 则请求MinIO合并文件。
8、合并完成校验 合并后的文件是否完整,如果不完整则删除文件。
其实整体实现无外乎就是将逻辑由一个文件的操作变为多文件操作。
【1】检查文件是否存在:
java
/**
* 文件上传前检查文件是否存在(基于文件MD5值)
* @param fileMd5 文件的MD5哈希值(用于唯一标识文件)
* @return RestResponse<Boolean> 封装检查结果(true=文件已存在,false=文件不存在)
*/
@Override
public RestResponse<Boolean> checkFile(String fileMd5) {
// 1. 根据文件MD5查询数据库中的文件记录
MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
// 2. 如果数据库中存在该文件记录,则进一步检查MinIO存储中是否真实存在该文件
if (mediaFiles != null) {
// 从数据库记录中获取MinIO存储的桶名称
String bucket = mediaFiles.getBucket();
// 从数据库记录中获取MinIO中的文件路径(对象键)
String filePath = mediaFiles.getFilePath();
// 3. 初始化文件输入流(用于检查文件是否存在)
InputStream stream = null;
try {
// 4. 通过MinIO客户端API获取文件对象
// - 使用GetObjectArgs构建获取对象的参数
// - .bucket(bucket) 指定存储桶
// - .object(filePath) 指定对象路径
stream = minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucket)
.object(filePath)
.build());
// 5. 如果成功获取到输入流(不为null),说明文件确实存在于MinIO中
if (stream != null) {
// 文件已存在,返回成功响应(true)
return RestResponse.success(true);
}
} catch (Exception e) {
// 6. 捕获并处理可能发生的异常(如网络问题、MinIO服务不可用、权限问题等)
// - 使用自定义异常处理器抛出业务异常
// - 异常信息会包含具体的错误详情
XueChengPlusException.cast(e.getMessage());
} finally {
// 7. 资源清理:确保输入流被正确关闭(防止资源泄漏)
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
// 关闭流时的异常可以记录日志,但不需要中断业务流程
log.error("关闭MinIO文件流失败", e);
}
}
}
}
// 8. 如果数据库中没有记录 或 MinIO中不存在文件,则返回文件不存在
return RestResponse.success(false);
}
【2】文件上传前检查分块文件是否存在:
首先我们保存分块文件的路径格式如下:
假设
fileMd5 = "d41d8cd98f00b204e9800998ecf8427e"
(一个标准的32位MD5值),生成的路径会是:
javad/4/d41d8cd98f00b204e9800998ecf8427e/chunk/
即:
- 第1级目录:
d
(MD5的第1个字符)- 第2级目录:
4
(MD5的第2个字符)- 第3级目录:完整的MD5值(
d41d8cd98f00b204e9800998ecf8427e
)- 第4级目录:固定字符串
chunk
最终路径示例:
javad/4/d41d8cd98f00b204e9800998ecf8427e/chunk/
所以获取路径代码为:
java// 得到分块文件的目录 private String getChunkFileFolderPath(String fileMd5) { return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/"; }
下面为检查分块文件是否存在代码:
java
/**
* 文件上传前检查指定分块文件是否存在(用于大文件分片上传的断点续传/秒传功能)
* @param fileMd5 文件的MD5值(用于唯一标识整个文件)
* @param chunkIndex 当前分块的序号(从0开始或从1开始,需与前端约定一致)
* @return RestResponse<Boolean> 封装检查结果(true=分块已存在,false=分块不存在)
*/
@Override
public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
// 1. 根据文件MD5生成分块存储目录路径
// 例如:/chunks/{fileMd5}/ 这样的目录结构,用于按文件分组存储分块
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
// 2. 拼接完整的分块文件路径
// 例如:/chunks/{fileMd5}/1 表示文件MD5为{fileMd5}的第1个分块
String chunkFilePath = chunkFileFolderPath + chunkIndex;
// 3. 初始化文件输入流(用于检查分块是否存在)
InputStream fileInputStream = null;
try {
// 4. 通过MinIO客户端API尝试获取分块对象
// - 使用GetObjectArgs构建获取对象的参数
// - .bucket(bucket_videofiles) 指定存储桶(视频文件专用桶)
// - .object(chunkFilePath) 指定分块对象路径
fileInputStream = minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucket_videofiles)
.object(chunkFilePath)
.build());
// 5. 如果成功获取到输入流(不为null),说明分块确实存在于MinIO中
if (fileInputStream != null) {
// 分块已存在,返回成功响应(true)
return RestResponse.success(true);
}
} catch (Exception e) {
// 6. 捕获并处理可能发生的异常
// - NoSuchKeyException:分块不存在(MinIO特定异常)
// - 其他异常:可能是网络问题、MinIO服务不可用、权限问题等
// 当前实现只是打印堆栈跟踪,建议:
// 1. 使用日志框架记录异常(如SLF4J)
// 2. 区分不同类型的异常返回更精确的响应
e.printStackTrace();
} finally {
// 7. 资源清理:确保输入流被正确关闭(防止资源泄漏)
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
// 关闭流时的异常可以记录日志,但不需要中断业务流程
e.printStackTrace();
}
}
}
// 8. 如果MinIO中不存在分块(或发生异常),返回文件不存在
return RestResponse.success(false);
}
【3】上传分块文件:
首先根据扩展名获取mimeType:
如果传入的extension为空,那么就使用通用的mimeType字节流:
javaString APPLICATION_OCTET_STREAM_VALUE = "application/octet-stream";
java
/**
* 根据扩展名获取mimeType
*/
private String getMimeType(String extension){
if(extension == null){
extension = "";
}
// 根据扩展名取出mimeType
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;// 通用mimeType,字节流
if(extensionMatch != null){
mimeType = extensionMatch.getMimeType();
}
return mimeType;
}
随后编写 addMediaFilesToMinIO 上传文件方法:
java
/**
* 将文件上传到minio
* @param localFilePath 文件本地路径
* @param mimeType 媒体类型
* @param bucket 桶
* @param objectName 对象名
* @return
*/
public boolean addMediaFilesToMinIO(String localFilePath,String mimeType,String bucket, String objectName){
try {
UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
.bucket(bucket) // 桶
.filename(localFilePath) // 指定本地文件路径
.object(objectName) // 对象名 放在子目录下
.contentType(mimeType) // 设置媒体文件类型
.build();
// 上传文件
minioClient.uploadObject(uploadObjectArgs);
log.debug("上传文件到minio成功,bucket:{},objectName:{}",bucket,objectName);
return true;
} catch (Exception e) {
e.printStackTrace();
log.error("上传文件出错,bucket:{},objectName:{},错误信息:{}",bucket,objectName,e.getMessage());
}
return false;
}
整体调用:
java
@Override
public RestResponse uploadChunk(String fileMd5, int chunk, String localChuckFilePath) {
// 得到分块文件的目录路径
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
// 得到分块文件的路径
String chunkFilePath = chunkFileFolderPath + chunk;
String mimeType = getMimeType(null);
boolean b = addMediaFilesToMinIO(localChuckFilePath, mimeType, bucket_mediafiles, chunkFilePath);
if(!b){
return RestResponse.validfail(false,"上传分块文件失败");
}
return RestResponse.success(true);
}
(4) 三层架构------清除分块文件:
java
/**
* 清除分块文件
* @param chunkFileFolderPath 分块文件路径
* @param chunkTotal 分块文件总数
*/
private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal) {
try {
// 使用Stream生成从0到chunkTotal-1的整数序列
// 每个整数代表一个分块文件的序号
List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i)
// 限制流的大小为chunkTotal,即只生成chunkTotal个序号
.limit(chunkTotal)
// 将每个序号转换为对应的DeleteObject对象
// 文件名格式为:chunkFileFolderPath + 序号(转换为字符串)
.map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
// 将所有DeleteObject对象收集到一个List中
.collect(Collectors.toList());
// 构建删除对象的参数
// 指定存储桶名称为"video"
// 设置要删除的对象列表
RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
.bucket("video")
.objects(deleteObjects)
.build();
// 执行批量删除操作,返回一个包含删除结果的Iterable
Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
// 遍历删除结果
results.forEach(r -> {
DeleteError deleteError = null;
try {
// 获取删除操作的错误信息(如果有)
deleteError = r.get();
if (deleteError != null) {
log.error("清除分块文件失败,objectname:{}", deleteError.objectName(), deleteError);
} else {
log.error("清除分块文件失败,但未获取到具体的错误信息");
}
} catch (Exception e) {
// 如果获取错误信息时发生异常,打印堆栈并记录错误日志
e.printStackTrace();
// 记录错误日志,包含出错的对象名和异常信息
log.error("清楚分块文件失败,objectname:{}", deleteError.objectName(), e);
}
});
} catch (Exception e) {
// 如果整个删除过程中发生异常,打印堆栈并记录错误日志
e.printStackTrace();
// 记录错误日志,包含分块文件路径和异常信息
log.error("清楚分块文件失败,chunkFileFolderPath:{}", chunkFileFolderPath, e);
}
}
(5)三层架构------从MinIO下载文件:
java
/**
* 从MinIO下载文件
* @param bucket 桶名称
* @param objectName 对象在桶中的名称
* @return 下载后的文件(临时文件),如果下载失败则返回null
*/
public File downloadFileFromMinIO(String bucket, String objectName) {
// 创建临时文件用于存储下载的内容
File minioFile = null;
// 使用try-with-resources确保InputStream和FileOutputStream都能正确关闭
// 这样可以避免资源泄漏,无需在finally块中手动关闭
try (
// 从MinIO获取对象(文件)的输入流
InputStream stream = minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucket) // 指定桶名称
.object(objectName) // 指定对象名称
.build()
);
// 创建文件输出流,用于将下载的内容写入临时文件
FileOutputStream outputStream = new FileOutputStream(minioFile)
) {
// 创建临时文件
// 文件名前缀为"minio",后缀为".merge"
minioFile = File.createTempFile("minio", ".merge");
// 使用IOUtils工具类将输入流的内容复制到输出流
// 这样可以高效地将文件内容从MinIO传输到本地临时文件
IOUtils.copy(stream, outputStream);
// 返回下载的临时文件
return minioFile;
} catch (Exception e) {
// 如果下载过程中发生任何异常,打印堆栈跟踪
e.printStackTrace();
// 可以添加更详细的日志记录
log.error("从MinIO下载文件失败,bucket: {}, objectName: {}", bucket, objectName, e);
// 下载失败,返回null
return null;
}
// 注意:由于使用了try-with-resources,不再需要finally块来手动关闭资源
// 资源会在try块结束时自动关闭
}
(6)三层架构------合并分块文件:
首先得到合并后的地址:

java
/**
* 得到合并后的文件的地址
* @param fileMd5 文件id即md5值
* @param fileExt 文件扩展名
* @return
*/
private String getFilePathByMd5(String fileMd5,String fileExt){
return fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" +fileMd5 +fileExt;
}
随后调用方法:
java
/**
* 合并文件分块为完整文件(用于大文件分片上传的最终合并阶段)
*
* @param companyId 公司ID(用于业务关联)
* @param fileMd5 文件的MD5值(用于唯一标识整个文件)
* @param chunkTotal 分块总数(用于确定需要合并的分块数量)
* @param uploadFileParamsDto 文件上传参数DTO(包含文件名等信息)
* @return RestResponse<Boolean> 封装合并结果(true=合并成功,false=合并失败)
*/
@Override
public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
// =====获取分块文件路径=====
// 根据文件MD5生成分块存储的目录路径(如:/chunks/{fileMd5}/)
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
// 组成将分块文件路径组成 List<ComposeSource>
// 使用Stream生成从0到chunkTotal-1的分块索引列表
// 为每个分块构建ComposeSource对象(包含bucket和object路径信息)
List<ComposeSource> sourceObjectList = Stream.iterate(0, i -> ++i)
.limit(chunkTotal)
.map(i -> ComposeSource.builder()
.bucket(bucket_videofiles) // 指定存储桶(视频文件专用桶)
.object(chunkFileFolderPath.concat(Integer.toString(i))) // 构建分块路径(如:/chunks/{fileMd5}/0)
.build())
.collect(Collectors.toList());
// =====合并=====
// 从DTO中获取原始文件名(如:"example.mp4")
String fileName = uploadFileParamsDto.getFilename();
// 提取文件扩展名(如:".mp4")
String extName = fileName.substring(fileName.lastIndexOf("."));
// 根据文件MD5和扩展名生成合并后的文件存储路径(如:/videos/{fileMd5}.mp4)
String mergeFilePath = getFilePathByMd5(fileMd5, extName);
try {
// 调用MinIO的composeObject方法合并分块
// 参数说明:
// - bucket: 存储桶名称
// - object: 合并后的文件路径
// - sources: 待合并的分块列表
ObjectWriteResponse response = minioClient.composeObject(
ComposeObjectArgs.builder()
.bucket(bucket_videofiles)
.object(mergeFilePath)
.sources(sourceObjectList)
.build());
// 记录合并成功的日志
log.debug("合并文件成功:{}", mergeFilePath);
} catch (Exception e) {
// 合并失败的异常处理
log.debug("合并文件失败,fileMd5:{},异常:{}", fileMd5, e.getMessage(), e);
return RestResponse.validfail(false, "合并文件失败。");
}
// ====验证md5====
// 从MinIO下载合并后的文件到本地临时文件
File minioFile = downloadFileFromMinIO(bucket_videofiles, mergeFilePath);
if (minioFile == null) {
// 下载失败的处理
log.debug("下载合并后文件失败,mergeFilePath:{}", mergeFilePath);
return RestResponse.validfail(false, "下载合并后文件失败。");
}
try (InputStream newFileInputStream = new FileInputStream(minioFile)) {
// 计算下载文件的MD5值(用于校验文件完整性)
String md5Hex = DigestUtils.md5Hex(newFileInputStream);
// 比较计算出的MD5与原始MD5是否一致
// 不一致说明文件在合并过程中可能损坏或不完整
if (!fileMd5.equals(md5Hex)) {
return RestResponse.validfail(false, "文件合并校验失败,最终上传失败。");
}
// 设置文件大小到DTO中(用于后续入库)
uploadFileParamsDto.setFileSize(minioFile.length());
} catch (Exception e) {
// 文件校验过程中的异常处理
log.debug("校验文件失败,fileMd5:{},异常:{}", fileMd5, e.getMessage(), e);
return RestResponse.validfail(false, "文件合并校验失败,最终上传失败。");
} finally {
// 确保临时文件被删除(避免磁盘空间泄漏)
if (minioFile != null) {
minioFile.delete();
}
}
// 文件入库
// 将文件元数据(包括公司ID、MD5、文件参数等)事务性保存到数据库
mediaFileTransactionalService.addMediaFilesToDbWithTransaction(
companyId,
fileMd5,
uploadFileParamsDto,
bucket_videofiles,
mergeFilePath);
// =====清除分块文件=====
// 合并完成后删除所有分块文件(释放存储空间)
clearChunkFiles(chunkFileFolderPath, chunkTotal);
// 返回成功响应
return RestResponse.success(true);
}