断点续传含义:
断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分(分块),每一个部分采用一个线程进行上传或下载。如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性。
分块:
@Test
public void testChunk() throws IOException {
//1.获取源文件
File sourceFile = new File("D:\\MyVido\\EV\\绝地求生两个倒霉蛋.mp4");
//2.创建分块目录
String chunkPath = "E:\\csc\\XueCheng\\minIo\\chunkPath\\";
File chunkFolder = new File(chunkPath);
if (!chunkFolder.exists()) {
chunkFolder.mkdirs();
}
//3.设置分块大小--->5M
int chunkSize = 1024 * 1024 * 5;
//4.计算分块数量:ceil向上取整
int chunkNum = (int) Math.ceil(sourceFile.length() * 1.0/chunkSize);
//5.使用RandomAccessFile访问文件: 读源文件的流
RandomAccessFile raf_read = new RandomAccessFile(sourceFile, "r");
//6.3创建缓存区
byte[] bytes = new byte[1024];
//6.开始分块
for (int i = 0; i < chunkNum; i++) {
//6.1创建分款文件
File chunkFile = new File(chunkPath + i);
//6.2分块文件的写入流
RandomAccessFile raf_rw = new RandomAccessFile(chunkFile, "rw");
int len = -1;
while ((len = raf_read.read(bytes)) != -1){
//从缓冲区写数据
raf_rw.write(bytes,0,len);
if(chunkFile.length() >= chunkSize){
break;
}
}
raf_rw.close();
}
raf_read.close();
}
合并分块:
@Test
public void testMerge() throws IOException {
//分块后的目录
File chunkFolder = new File("E:\\csc\\XueCheng\\minIo\\chunkPath\\");
//原始文件
File originalFile = new File("D:\\MyVido\\EV\\绝地求生两个倒霉蛋.mp4");
//合并后的文件
File mergeFile = new File("E:\\csc\\XueCheng\\minIo\\绝地求生两个倒霉蛋.mp4");
if (mergeFile.exists()) {
mergeFile.delete();
}
//取出所有分块文件
File[] files = chunkFolder.listFiles();
//将数组转换成list
List<File> filesList = Arrays.asList(files);
//排序
Collections.sort(filesList, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName());
}
});
//向合并文件写的流
RandomAccessFile raf_rw = new RandomAccessFile(mergeFile, "rw");
byte[] bytes = new byte[1024];
//遍历分块文件,向合并文件写
for (File file: filesList) {
//读分块的流
RandomAccessFile raf_r = new RandomAccessFile(file, "r");
int len = -1;
while ((len = raf_r.read(bytes)) != -1){
raf_rw.write(bytes,0,len);
}
raf_r.close();
}
raf_rw.close();
//校验
FileInputStream fileInputStream_original = new FileInputStream(originalFile);
FileInputStream fileInputStream_merge = new FileInputStream(mergeFile);
String originalMd5 = DigestUtils.md5Hex(fileInputStream_original);
String mergeMd5 = DigestUtils.md5Hex(fileInputStream_merge);
if(originalMd5.equals(mergeMd5)){
System.out.println("合并文件成功");
}
}
实例代码:
业务流程
前端将视频分块--->上传分块前先请求服务检查文件是否存在,如果已经存在则不再上传
--->如果不存在,则开始向minio上传分块--->上传完成请求服务开始合并分块--->合并完后清理分块
检查文件是否存在
@Override
public RestResponse<Boolean> checkFile(String fileMd5) {
MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
if (mediaFiles != null) {
String bucket = mediaFiles.getBucket();
String filePath = mediaFiles.getFilePath();
GetObjectArgs getObjectArgs = GetObjectArgs.builder()
.bucket(bucket)
.object(filePath)
.build();
try {
FilterInputStream filterInputStream = minioClient.getObject(getObjectArgs);
if (filterInputStream != null) {
//文件已经存在
return RestResponse.success(true,"文件已经存在");
}
} catch (Exception e) {
e.printStackTrace();
}
}
return RestResponse.success(false);
}
检查分块是否存在
@Override
public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
//分块目录: md5值的前两位作为两个子目录+md5+chunk
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
GetObjectArgs getObjectArgs = GetObjectArgs.builder()
.bucket(videofiles)
.object(chunkFileFolderPath + chunkIndex)
.build();
try {
FilterInputStream filterInputStream = minioClient.getObject(getObjectArgs);
if (filterInputStream != null) {
return RestResponse.success(true);
}
} catch (Exception e) {
e.printStackTrace();
}
return RestResponse.success(false);
}
上传分块
@Override
public RestResponse uploadChunk(String fileMd5, int chunk, String localFilePath) {
String mimeType = getMimeType(null);
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5) + chunk;
boolean b = addMediaFilesToMinio(mimeType, localFilePath, videofiles, chunkFileFolderPath);
if (!b) {
return RestResponse.validfail(false, "上传分块文件失败");
}
log.info("fileMd5:{}", fileMd5);
return RestResponse.success(true);
}
/**
* @author changshichao
* @description 将文件上传到minio
* @date 2023-08-29 14:48
*/
public boolean addMediaFilesToMinio(String mimeType, String localFilePath, String bucket, String objectName) {
try {
UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
.bucket(bucket)//桶
.filename(localFilePath)//指定本地文件路径
.object(objectName)//对象名.在子目录下存储该文件
.contentType(mimeType)//设置媒体文件类型
.build();
minioClient.uploadObject(uploadObjectArgs);
return true;
} catch (Exception e) {
log.error("上传文件出错,bucket:{},objectName:{},错误信息:{}", bucket, objectName, e.getMessage());
}
return false;
}
合并分块
public RestResponse mergeChunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
//分块文件的目录
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
String filename = uploadFileParamsDto.getFilename();
String extension = filename.substring(filename.lastIndexOf("."));
//合并后的文件信息
String objectName = getFilePathByMd5(fileMd5, extension);
//组成将分块文件路径组成 List<ComposeSource>
List<ComposeSource> sources = Stream.iterate(0, i -> ++i)
.limit(chunkTotal)
.map(i -> ComposeSource.builder()
.bucket(videofiles)
.object(chunkFileFolderPath.concat(Integer.toString(i)))
.build())
.collect(Collectors.toList());
try {
//1.合并分块
ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
.bucket(videofiles)
.sources(sources)
.object(objectName)
.build();
log.debug("合并文件成功:{}",objectName);
minioClient.composeObject(composeObjectArgs);
} catch (Exception e) {
e.printStackTrace();
log.debug("合并文件出错,bucket:{},objectName:{},错误信息:{}", videofiles, objectName, e.getMessage());
return RestResponse.validfail(false, "合并文件异常");
}
//2.检验合并后的文件和源文件是否一致,视频上传成功
//2.1下载合并后的文件
File file = downloadFileFromMinIo(videofiles, objectName);
//2.2校验
try (FileInputStream fileInputStream = new FileInputStream(file)) {
String mergeFile_Md5 = DigestUtils.md5Hex(fileInputStream);
if (!fileMd5.equals(mergeFile_Md5)) {
log.error("校验合并文件md5值不一致,原始文件:{},合并文件", fileMd5, mergeFile_Md5);
return RestResponse.validfail(false, "文件校验失败");
}
} catch (Exception e) {
return RestResponse.validfail(false, "文件校验失败");
}
//文件大小
uploadFileParamsDto.setFileSize(file.length());
//3.文件入库
MediaFiles mediaFiles = addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, videofiles, objectName);
if (mediaFiles == null) {
return RestResponse.validfail(false, "文件入库失败");
}
//4.清理分块
clearChunkFiles(chunkFileFolderPath, chunkTotal);
return RestResponse.success(true);
}
清理分块
private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal) {
try {
List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i)
.limit(chunkTotal)
.map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
.collect(Collectors.toList());
RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
.bucket(videofiles)
.objects(deleteObjects)
.build();
Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
//想要真正删除,必须执行下列
results.forEach(r -> {
DeleteError deleteError = null;
try {
deleteError = r.get();
} catch (Exception e) {
e.printStackTrace();
log.error("清楚分块文件失败,objectname:{}", deleteError.objectName(), e);
}
});
} catch (Exception e) {
e.printStackTrace();
log.error("清楚分块文件失败,chunkFileFolderPath:{}", chunkFileFolderPath, e);
}
}