大家好,我是程序员阿药。今天和大家分享的是一个上传文件的方式:断点续传。
一次性上传的弊端
较大的文件一次性上传会比较慢,中途退出或网络延迟、故障等,容易出现超时等问题。待问题修复后再次上传时仍需要从头开始上传。
断点续传的优势
断点续传允许在上传中断后继续上传,而无需从头开始。
断点续传的原理
断点续传需要先把上传文件按一定大小分块,然后将所有分块上传,最后通过上传的所有分块合并出原文件。如图:
断点续传流程
1. 执行文件分块(文件分块)、2. 检查分块是否已经上传、3. 上传分块、4. 合并分块
如图:
断点续传代码
public void uploadFile() throws IOException {
// 原文件路径
String sourcePath = "D:\\Data\\videos\\test.mp4";
// 本地分块文件存储路径
String localChunkFilePath = "D:\\Data\\chunk\\";
// 原文件md5
String sourceMd5 = DigestUtils.md5DigestAsHex(new FileInputStream(sourcePath));
// 1.执行文件分块,返回分块总数
int chunkTotal = fileToChunk(sourcePath, localChunkFilePath);
int i = 0;
for (; i < chunkTotal; i++) {
// 2.检查分块是否已经上传
boolean isSuccess = uploadController.checkChunks(sourceMd5, i);
if (isSuccess) {
System.out.println(i + "号分块已经上传成功");
} else {
// 3.上传分块
isSuccess = uploadController.uploadChunks(sourceMd5, i, localChunkFilePath + i);
if (isSuccess) {
System.out.println("执行上传" + i + "号分块成功");
} else {
System.out.println("执行上传" + i + "号分块失败");
break;
}
}
}
// 4.合并分块
if (i == chunkTotal) {
System.out.println("所有分块均上传成功,开始合并分块");
boolean isSuccess = uploadController.mergeChunks(sourceMd5, chunkTotal, sourcePath);
if (isSuccess) {
System.out.println("合并分块成功");
} else {
System.out.println("合并分块失败");
}
}
}
/**
* 检查分块是否已经上传
* @param fileMd5 文件md5
* @param chunkIndex 分块序号
*/
public boolean checkChunks(String fileMd5, int chunkIndex) {
// 根据md5得到分块文件所在目录的路径
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
// 分块路径
String chunkPath = chunkFileFolderPath + chunkIndex;
// 判断分块是否已经存在
StatObjectArgs statObjectArgs = StatObjectArgs.builder()
.bucket(bucket)
.object(chunkPath)
.build();
try {
minioClient.statObject(statObjectArgs);
} catch (Exception e) {
// 分块不存在
return false;
}
// 分块存在
return true;
}
/**
* 上传分块
* @param fileMd5 文件md5
* @param chunkIndex 分块序号
* @param localChunkFilePath 分块文件本地路径
*/
public boolean uploadChunks(String fileMd5, int chunkIndex, String localChunkFilePath) {
// 分块路径
String chunkPath = getChunkFileFolderPath(fileMd5) + chunkIndex;
// 获取文件的mimeType
String mimeType = getMimeType(".temp");
// 将分块文件上传到minio
try {
UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
.bucket(bucket) // 存储桶
.filename(localChunkFilePath) // 分块文件本地路径
.object(chunkPath) // 对象名(分块路径)
.contentType(mimeType) // 设置媒体文件类型
.build();
// 将分块从本地 localChunkFilePath 位置上传到 bucket 桶中的 chunkPath 位置
minioClient.uploadObject(uploadObjectArgs);
} catch (Exception e) {
// 上传失败
return false;
}
// 上传成功
return true;
}
/**
* 合并分块
* @param fileMd5 文件md5
* @param chunkTotal 分块总数
* @param sourceFileName 原文件名称
*/
public boolean mergeChunks(String fileMd5, int chunkTotal, String sourceFileName) {
// 分块文件所在目录
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
// 找到所有的分块文件
List<ComposeSource> sources = Stream.iterate(0, i -> ++i)
.limit(chunkTotal)
.map(i -> ComposeSource.builder().bucket(bucket).object(chunkFileFolderPath + i).build())
.collect(Collectors.toList());
// 源文件扩展名
String extension = sourceFileName.substring(sourceFileName.lastIndexOf("."));
// 分块合并后文件的路径(对象名)
String objectName = getFilePathByMd5(fileMd5, extension);
// 指定合并后文件的objectName等信息
ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
.bucket(bucket)
.object(objectName) // 合并后文件的路径(对象名)
.sources(sources) // 所有分块
.build();
// 合并文件
// 报错size 1048576 must be greater than 5242880,minio默认的分块文件大小为5M
try {
minioClient.composeObject(composeObjectArgs);
} catch (Exception e) {
return false;
}
// 校验合并后的和源文件是否一致,一致则上传成功
// 先下载合并后的文件
File file = downloadFileFromMinIO(objectName);
try (FileInputStream fileInputStream = new FileInputStream(file)) {
// 计算合并后文件的md5
String mergeFileMd5 = DigestUtils.md5DigestAsHex(fileInputStream);
// 比较原始md5和合并后文件的md5
if (!fileMd5.equals(mergeFileMd5)) {
// 合并失败
return false;
}
} catch (Exception e) {
// 合并失败
return false;
}
// 合并成功
return true;
}
测试结果
注意:报错size 1048576 must be greater than 5242880(Minio默认的分块文件大小为5M)
1. 上传过程未中断。文件分块后全部上传成功,且分块合并原文件成功。如图:
2. 制造上传分块过程中断场景,将已经上传的6号分块及以后分块全部删除,同时删除合并后的文件。如图:
再次运行代码,查看分块是否可以从断点处(6号分块)开始上传。测试结果如图:
今天的分享到此结束啦,喜欢的小伙伴可以点赞关注支持一下。
有需要全部源码的可以关注我的微信公众号:”程序员阿药“,回复:”断点续传“ 即可获得全部源码。