

























通常一些文件如视频文件体积都比较大,对于这些文件的上传需求要满足大文件的上传要求。http协议本身对上传文件大小没有限制,但是客户的网络环境质量、电脑硬件环境等参差不齐,如果一个大文件快上传完了网断了没有上传完成,需要客户重新上传,用户体验非常差,所以对于大文件上传的要求最基本的是断点续传。
引用百度百科:断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性。
大文件分片上传方案基于 “前端分片 + 后端合并 + 断点续传 + 秒传” 核心设计,完整流程分为 前端预处理 → 校验阶段 → 分片上传 → 后端合并 → 播放 / 下载 五个核心阶段,前后端分工明确:
import com.lgh.common.properties.MinIoProperties;
import com.lgh.web.service.VedioService;
import io.minio.*;
import io.minio.errors.MinioException;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 视频服务实现类
* @Author GuihaoLv
*/
@Service
@Slf4j
public class VedioServiceImpl implements VedioService {
@Autowired
private MinioClient minioClient;
@Autowired
private MinIoProperties minIoProperties;
/**
* 文件上传前检查
* @param fileMd5
* @return
*/
public Boolean checkFileExists(String fileMd5) {
// 1. 构建完整文件在MinIO中的存储路径(与分块目录规则一致)
String fullFilePath = getFullFilePath(fileMd5);
try {
// 2. 检查MinIO中是否存在该对象
StatObjectArgs statArgs = StatObjectArgs.builder()
.bucket(minIoProperties.getBucketName())
.object(fullFilePath)
.build();
minioClient.statObject(statArgs);
log.info("文件已存在,MD5:{},MinIO路径:{}", fileMd5, fullFilePath);
return true;
} catch (MinioException e) {
// MinIO返回对象不存在异常,说明文件未上传
if (e.getMessage().equals("NoSuchKey")) {
log.info("文件不存在,MD5:{}", fileMd5);
return false;
}
// 其他异常打印日志,返回false
log.error("检查文件存在性失败,MD5:{}", fileMd5, e);
return false;
} catch (Exception e) {
log.error("检查文件存在性异常,MD5:{}", fileMd5, e);
return false;
}
}
/**
* 检查分块是否已存在
* 逻辑:拼接分块文件路径,检查MinIO中是否存在该分块对象
*/
public Boolean checkChunkExists(String fileMd5, int chunk) {
// 1. 构建分块文件在MinIO中的路径
String chunkFilePath = getChunkFileFolderPath(fileMd5) + chunk;
try {
// 2. 检查分块对象是否存在
StatObjectArgs statArgs = StatObjectArgs.builder()
.bucket(minIoProperties.getBucketName())
.object(chunkFilePath)
.build();
minioClient.statObject(statArgs);
log.info("分块已存在,MD5:{},分块索引:{}", fileMd5, chunk);
return true;
} catch (MinioException e) {
if (e.getMessage().equals("NoSuchKey")) {
log.info("分块不存在,MD5:{},分块索引:{}", fileMd5, chunk);
return false;
}
log.error("检查分块存在性失败,MD5:{},分块索引:{}", fileMd5, chunk, e);
return false;
} catch (Exception e) {
log.error("检查分块存在性异常,MD5:{},分块索引:{}", fileMd5, chunk, e);
return false;
}
}
/**
* 上传分块文件到MinIO
* 逻辑:MultipartFile转InputStream,上传到分块指定路径
*/
public Boolean uploadChunk(MultipartFile file, String fileMd5, int chunk) {
// 1. 校验参数
if (file.isEmpty()) {
log.error("上传分块失败,文件为空,MD5:{},分块索引:{}", fileMd5, chunk);
return false;
}
// 2. 构建分块存储路径
String chunkFilePath = getChunkFileFolderPath(fileMd5) + chunk;
try (InputStream inputStream = file.getInputStream()) {
// 3. 上传分块到MinIO
PutObjectArgs putArgs = PutObjectArgs.builder()
.bucket(minIoProperties.getBucketName())
.object(chunkFilePath)
.stream(inputStream, file.getSize(), -1) // -1表示自动检测文件大小
.contentType(file.getContentType())
.build();
minioClient.putObject(putArgs);
log.info("分块上传成功,MD5:{},分块索引:{},路径:{}", fileMd5, chunk, chunkFilePath);
return true;
} catch (Exception e) {
log.error("分块上传失败,MD5:{},分块索引:{}", fileMd5, chunk, e);
return false;
}
}
/**
* 合并分块文件(优化版)
* 核心逻辑:1. 有序构建分块列表 2. 合并文件 3. MD5校验 4. 清理分块
* @param fileMd5 文件唯一标识(MD5)
* @param fileName 原始文件名(含扩展名)
* @param chunkTotal 分块总数(新增参数:避免遍历MinIO获取分块,提升性能)
* @return 合并是否成功
*/