分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。
分片上传的场景
网络环境环境不好,存在需要重传风险的场景
在文件第一次上传时,上传文件的md5值,从而判断文件是否存在minio中
public Result<Boolean> checkFile(String fileMd5) {
//正常做业务时应该先从数据库中查询
//如果数据库存在再查询 minio
GetObjectArgs getObjectArgs = GetObjectArgs.builder()
.bucket(bucketName)
// todo 这里固定了文件的后缀,实际情况下应该从数据库开始查询,得到文件的路径
.object(getFilePathByMd5(fileMd5,"png"))
.build();
//查询远程服务获取到一个流对象
try {
FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
if(inputStream!=null){
//文件已存在
return Result.success(true);
}
} catch (Exception e) {
e.printStackTrace();
}
//文件不存在
return Result.success(false);
}
检查分块是前端把需要上传的文件经过大小计算后,算出分块的数量,然后把循环发送文件的md5值和分块序号,然后在minio中检查对应文件夹下是否有对应的分块,如果检查到某一处没有对应的分块,便知道传输中断的位置。
public Result<Boolean> checkChunk(String fileMd5, int chunkIndex) {
//根据md5得到分块文件所在目录的路径
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
//如果数据库存在再查询 minio
GetObjectArgs getObjectArgs = GetObjectArgs.builder()
.bucket(bucketName)
.object(chunkFileFolderPath+chunkIndex)
.build();
//查询远程服务获取到一个流对象
try {
FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
if(inputStream!=null){
//文件已存在
return Result.success(true);
}
} catch (Exception e) {
e.printStackTrace();
}
//文件不存在
return Result.success(false);
}
public Result uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {
//分块文件的路径
String chunkFilePath = getChunkFileFolderPath(fileMd5) + chunk;
//获取mimeType
String mimeType = localChunkFilePath.substring(localChunkFilePath.lastIndexOf("."));
//将分块文件上传到minio
boolean b = addMediaFilesToMinIO(localChunkFilePath, mimeType, bucketName, chunkFilePath);
if(!b){
return Result.error("上传分块文件失败");
}
//上传成功
return Result.success(true);
}
合并分块文件之前,需要检查文件是否和源文件相同,我们通过把分块合并后取文件的md5值和传输过来的MD5值作比较,如果相同则证明传输正确,把合并后的文件存入minio中,并清除分块文件
public Result mergechunks(String fileMd5, int chunkTotal) {
//分块文件所在目录
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
//找到所有的分块文件
List<ComposeSource> sources = Stream.iterate(0, i -> ++i)
.limit(chunkTotal).map(i -> ComposeSource.builder()
.bucket(bucketName)
.object(chunkFileFolderPath + i).build()).collect(Collectors.toList());
//合并后文件的objectname
String objectName = getFilePathByMd5(fileMd5, "png");
//指定合并后的objectName等信息
ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
.bucket(bucketName)
.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:{},错误信息:{}",bucketName,objectName,e.getMessage());
return Result.error("合并文件异常");
}
//===========校验合并后的和源文件是否一致,视频上传才成功===========
//先下载合并后的文件
File file = downloadFileFromMinIO(bucketName, 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 Result.error("文件校验失败");
}
}catch (Exception e) {
return Result.error("文件校验失败");
}
//==============将文件信息入库============
// 在做业务时要将得到的路径存入数据库
//==========清理分块文件=========
clearChunkFiles(chunkFileFolderPath,chunkTotal);
return Result.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(bucketName).objects(objects).build();
Iterable<io.minio.Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
//要想真正删除
results.forEach(f->{
try {
DeleteError deleteError = f.get();
} catch (Exception e) {
e.printStackTrace();
}
});
}