断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载
,断点续传可以提高节省操作时间,提高用户体验性。
断点续传流程如下图:
RandomAccessFile: 读写流
package com.xuecheng.media;
import javafx.print.Collation;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.jupiter.api.Test;
import java.io.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class BigFileTest {
//分块测试
@Test
public void testChunk() throws IOException {
//源文件
File sourceFile = new File("F:\\develop\\video\\3.mp4");
//分块文件存储路径
String chunkFilePath = "F:\\develop\\video\\chunk\\";
//分块文件大小
int chunkSize = 1024 * 1024 * 5;
//分块文件个数
int chunkNum = (int) Math.ceil(sourceFile.length() * 1.0 / chunkSize);
//使用流从源文件读数据,向分块文件中写数据
RandomAccessFile raf_r = new RandomAccessFile(sourceFile, "r");
//缓存区
byte[] bytes = new byte[1024];
for (int i = 0; i < chunkNum; i++) {
File chunkFile = new File(chunkFilePath + i);
//分块文件写入流
RandomAccessFile raf_rw = new RandomAccessFile(chunkFile,"rw");
int len = -1;
while ((len=raf_r.read(bytes))!=-1){
raf_rw.write(bytes,0,len);
if(chunkFile.length()>=chunkSize){
break;
}
}
raf_rw.close();
}
raf_r.close();
}
//将分块进行合并
@Test
public void testMerge() throws IOException {
//块文件目录
File chunkFolder = new File("F:\\develop\\video\\chunk");
//源文件
File sourceFile = new File("F:\\develop\\video\\3.mp4");
//合并后的文件
File mergeFile = new File("F:\\develop\\video\\3 _2.mp4");
//取出所有分块文件
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();
//合并文件完成后对合并的文件md5校验
FileInputStream fileInputStream_merge = new FileInputStream(mergeFile);
FileInputStream fileInputStream_source = new FileInputStream(sourceFile);
String md5_merge = DigestUtils.md5Hex(fileInputStream_merge);
String md5_source = DigestUtils.md5Hex(fileInputStream_source);
if(md5_merge.equals(md5_source)){
System.out.println("文件合并成功");
}
}
}
流程:
在Java的MinIO库中,ComposeSource是一个用于指定合并操作的源对象的类。它用于构建对象合并(Object Composition)的配置。
MinIO合并分块用到minioClient.composeObject方法
//将分块文件上传到minio
@Test
public void uploadChunk() throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
for (int i = 0; i < 33; i++) {
//上传文件的参数信息
UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
.bucket("testbucket")//桶
.filename("F:\\develop\\video\\chunk\\"+i) //指定本地文件路径
.object("chunk/"+i)//对象名 放在子目录下
.build();
//上传文件
minioClient.uploadObject(uploadObjectArgs);
System.out.println("上传分块"+i+"成功");
}
}
//调用minio接口合并分块
@Test
public void testMerge() throws Exception {
List<ComposeSource> sources = new ArrayList<>();
for (int i = 0; i < 33; i++) {
//指定分块文件的信息
ComposeSource composeSource = ComposeSource.builder().bucket("testbucket").object("chunk/" + i).build();
sources.add(composeSource);
}
// List sources = Stream.iterate(0, i -> ++i).limit(6).map(i -> ComposeSource.builder().bucket("testbucket").object("chunk/" + i).build()).collect(Collectors.toList());
//指定合并后的objectName等信息
ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
.bucket("testbucket")
.object("merge01.mp4")
.sources(sources)//指定源文件
.build();
//合并文件,
//报错size 1048576 must be greater than 5242880,minio默认的分块文件大小为5M
minioClient.composeObject(composeObjectArgs);
}
package com.xuecheng.media.api;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.service.MediaFileService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
@Api(value = "大文件上传接口", tags = "大文件上传接口")
@RestController
public class BigFilesController {
@Autowired
MediaFileService mediaFileService;
@ApiOperation(value = "文件上传前检查文件")
@PostMapping("/upload/checkfile")
public RestResponse<Boolean> checkfile(
@RequestParam("fileMd5") String fileMd5
) throws Exception {
RestResponse<Boolean> booleanRestResponse = mediaFileService.checkFile(fileMd5);
return booleanRestResponse;
}
@ApiOperation(value = "分块文件上传前的检测")
@PostMapping("/upload/checkchunk")
public RestResponse<Boolean> checkchunk(@RequestParam("fileMd5") String fileMd5,
@RequestParam("chunk") int chunk) throws Exception {
RestResponse<Boolean> booleanRestResponse = mediaFileService.checkChunk(fileMd5,chunk);
return booleanRestResponse;
}
@ApiOperation(value = "上传分块文件")
@PostMapping("/upload/uploadchunk")
public RestResponse uploadchunk(@RequestParam("file") MultipartFile file,
@RequestParam("fileMd5") String fileMd5,
@RequestParam("chunk") int chunk) throws Exception {
//创建一个临时文件
File tempFile = File.createTempFile("minio", ".temp");
file.transferTo(tempFile);
//文件路径
String localFilePath = tempFile.getAbsolutePath();
RestResponse restResponse = mediaFileService.uploadChunk(fileMd5, chunk, localFilePath);
return restResponse;
}
@ApiOperation(value = "合并文件")
@PostMapping("/upload/mergechunks")
public RestResponse mergechunks(@RequestParam("fileMd5") String fileMd5,
@RequestParam("fileName") String fileName,
@RequestParam("chunkTotal") int chunkTotal) throws Exception {
Long companyId = 1232141425L;
//文件信息对象
UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
uploadFileParamsDto.setFilename(fileName);
uploadFileParamsDto.setTags("视频文件");
uploadFileParamsDto.setFileType("001002");
RestResponse restResponse = mediaFileService.mergechunks(1232141425L, fileMd5, chunkTotal, uploadFileParamsDto);
return restResponse;
}
}
@Override
public RestResponse<Boolean> checkFile(String fileMd5) {
//先查询数据库
MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
if(mediaFiles!=null){
//桶
String bucket = mediaFiles.getBucket();
//objectname
String filePath = mediaFiles.getFilePath();
//如果数据库存在再查询 minio
GetObjectArgs getObjectArgs = GetObjectArgs.builder()
.bucket(bucket)
.object(filePath)
.build();
//查询远程服务获取到一个流对象
try {
FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
if(inputStream!=null){
//文件已存在
return RestResponse.success(true);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//文件不存在
return RestResponse.success(false);
}
@Override
public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
//根据md5得到分块文件所在目录的路径
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
//如果数据库存在再查询 minio
GetObjectArgs getObjectArgs = GetObjectArgs.builder()
.bucket(bucket_video)
.object(chunkFileFolderPath+chunkIndex)
.build();
//查询远程服务获取到一个流对象
try {
FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
if(inputStream!=null){
//文件已存在
return RestResponse.success(true);
}
} catch (Exception e) {
e.printStackTrace();
}
//文件不存在
return RestResponse.success(false);
}
@Override
public RestResponse uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {
//分块文件的路径
String chunkFilePath = getChunkFileFolderPath(fileMd5) + chunk;
//获取mimeType
String mimeType = getMimeType(null);
//将分块文件上传到minio
boolean b = addMediaFilesToMinIO(localChunkFilePath, mimeType, bucket_video, chunkFilePath);
if(!b){
return RestResponse.validfail(false,"上传分块文件失败");
}
//上传成功
return RestResponse.success(true);
}
@Override
public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
//分块文件所在目录
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
//找到所有的分块文件
List<ComposeSource> sources = Stream.iterate(0, i -> ++i).limit(chunkTotal).map(i -> ComposeSource.builder().bucket(bucket_video).object(chunkFileFolderPath + i).build()).collect(Collectors.toList());
//源文件名称
String filename = uploadFileParamsDto.getFilename();
//扩展名
String extension = filename.substring(filename.lastIndexOf("."));
//合并后文件的objectname
String objectName = getFilePathByMd5(fileMd5, extension);
//指定合并后的objectName等信息
ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
.bucket(bucket_video)
.object(objectName)//合并后的文件的objectname
.sources(sources)//指定源文件
.build();
//===========合并文件============
//报错size 1048576 must be greater than 5242880,minio默认的分块文件大小为5M
try {
minioClient.composeObject(composeObjectArgs);
} catch (Exception e) {
e.printStackTrace();
log.error("合并文件出错,bucket:{},objectName:{},错误信息:{}",bucket_video,objectName,e.getMessage());
return RestResponse.validfail(false,"合并文件异常");
}
//===========校验合并后的和源文件是否一致,视频上传才成功===========
//先下载合并后的文件
File file = downloadFileFromMinIO(bucket_video, objectName);
try(FileInputStream fileInputStream = new FileInputStream(file)){
//计算合并后文件的md5
String mergeFile_md5 = DigestUtils.md5Hex(fileInputStream);
//比较原始md5和合并后文件的md5
if(!fileMd5.equals(mergeFile_md5)){
log.error("校验合并文件md5值不一致,原始文件:{},合并文件:{}",fileMd5,mergeFile_md5);
return RestResponse.validfail(false,"文件校验失败");
}
//文件大小
uploadFileParamsDto.setFileSize(file.length());
}catch (Exception e) {
return RestResponse.validfail(false,"文件校验失败");
}
//==============将文件信息入库============
MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_video, objectName);
if(mediaFiles == null){
return RestResponse.validfail(false,"文件入库失败");
}
//==========清理分块文件=========
clearChunkFiles(chunkFileFolderPath,chunkTotal);
return RestResponse.success(true);
}
/**
* 清除分块文件
* @param chunkFileFolderPath 分块文件路径
* @param chunkTotal 分块文件总数
*/
private void clearChunkFiles(String chunkFileFolderPath,int chunkTotal){
Iterable<DeleteObject> objects = Stream.iterate(0, i -> ++i).limit(chunkTotal).map(i -> new DeleteObject(chunkFileFolderPath+ i)).collect(Collectors.toList());;
RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(bucket_video).objects(objects).build();
Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
//要想真正删除
results.forEach(f->{
try {
DeleteError deleteError = f.get();
} catch (Exception e) {
e.printStackTrace();
}
});
}
/**
* 从minio下载文件
* @param bucket 桶
* @param objectName 对象名称
* @return 下载后的文件
*/
public File downloadFileFromMinIO(String bucket,String objectName){
//临时文件
File minioFile = null;
FileOutputStream outputStream = null;
try{
InputStream stream = minioClient.getObject(GetObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.build());
//创建临时文件
minioFile=File.createTempFile("minio", ".merge");
outputStream = new FileOutputStream(minioFile);
IOUtils.copy(stream,outputStream);
return minioFile;
} catch (Exception e) {
e.printStackTrace();
}finally {
if(outputStream!=null){
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}