1、前端对文件进行分块。
2、前端上传分块文件前请求媒资服务检查文件是否存在,如果已经存在则不再上传。
3、如果分块文件不存在则前端开始上传
4、前端请求媒资服务上传分块。
5、媒资服务将分块上传至MinIO。
6、前端将分块上传完毕请求媒资服务合并分块。
7、媒资服务判断分块上传完成则请求MinIO合并文件。
8、合并完成校验合并后的文件是否完整,如果完整则上传完成,否则删除文件。
组件
<el-upload
action='#'
:auto-upload='false'
:accept="'video/*'"
:show-file-list='false'
el-upload>
方法
/**
* 文件状态改变时的钩子
*/
// 文件分块上传
private handleChange(
file: ElUploadInternalFileDetail,
fileList: ElUploadInternalFileDetail[]
) {
this.fileList = fileList
uploadByPieces({
file,
pieceSize: 1, //分片大小
success: (data) => {
file.percentage = (data.num/data.chunkCount) * 100
console.log('success::' + data)
},
error: (e) => {
console.log(file, fileList)
// 出错了可以从列表中删除吧
// fileList.forEach((n,i) => { if(n.uid == file.uid){
// fileList.splice(i,1)
// } })
console.log('error::' + '视频分片上传失败')
}
})
}
分块工具
export const uploadByPieces = ({ file, pieceSize = 2, success, error }: any) => {
// 上传过程中用到的变量
let fileMD5 = '' // md5加密文件的标识
// 传过来 默认是1,所以 每个分块是1mb
const chunkSize = pieceSize * 1024 * 1024 // 分片大小
const chunkCount = Math.ceil(file.size / chunkSize) // 总片数
//得到某一片的分片
const getChunkInfo = (file, currentChunk, chunkSize) => {
let start = currentChunk * chunkSize
let end = Math.min(file.size, start + chunkSize)
let chunk = file.raw.slice(start, end)
return chunk
}
// 第一步
const readFileMD5 = () => {
// 得到 第一片和 最后一片
const startChunk = getChunkInfo(file, 0, chunkSize)
const endChunk = getChunkInfo(file, chunkCount - 1, chunkSize)
// 对第一片进行转码然 后md5加密,网上很多是直接 对整个文件转码加密得到标识,但是我发现大文件尤其是几个G的文件会崩溃,所以我是 先分片然后取第一片加密
let fileRederInstance = new FileReader()
fileRederInstance.readAsBinaryString(file.raw)
fileRederInstance.addEventListener('load', (e) => {
let fileBolb = (e.target as any).result
fileMD5 = CryptoJS.MD5(CryptoJS.enc.Latin1.parse(fileBolb)).toString()
// 上传前提交注册 - 参数
const params = {
fileMd5: fileMD5
// fileName:file.name,
// fileSize:file.size,
// mimetype:file.raw.type,
// fileExt:file.name.split('.').at(-1)
}
// 上传前提交注册 - 接口调用
upRegister(params).then(res => {
// 文件不存在就上传
if (res.code == 0) {
readChunkMD5(0)
}
}).catch(err => error(err))
})
}
// 针对每个分片文件进行上传处理
const readChunkMD5 = async (num) => {
if (num <= chunkCount - 1) {
//得到当前需要上传的分片文件
const chunk = getChunkInfo(file, num, chunkSize)
// 上传分块前检查
//await checkchunk({fileMd5:fileMD5,chunk:num,chunkSize:chunkCount}).then( async res => {
await checkchunk({ fileMd5: fileMD5, chunk: num }).then(async res => {
if (res.code == 0 && res.result == false) {
// 分块上传
let fetchForm = new FormData()
fetchForm.append('file', chunk)
fetchForm.append('fileMd5', fileMD5)
fetchForm.append('chunk', num)
await upChunk(fetchForm).then(async res => {
// 上传成功
success({ num, chunkCount, state: 'uploading' })
if (res.code == 0) {
readChunkMD5(num + 1)
}
}).catch(err => {
error(err)
})
} else {
success({ num, chunkCount, state: 'uploading' })
// 上传成功就开始下一块
readChunkMD5(num + 1)
}
})
} else {
// 上传结束请求合并
// 提交合并
mergeChunks({
fileMd5: fileMD5,
fileName: file.name,
chunkTotal: chunkCount
// mimetype:file.raw.type,
// fileExt:file.name.split('.').at(-1)
}).then(res => {
// 合并成功了
success({ num, chunkCount, state: 'success' })
}).catch(err => {
error(err)
})
}
}
readFileMD5() // 开始执行代码
}
@Autowired
private MediaFileService mediaFileService;
@ApiOperation(value = "文件上传前检查文件")
@PostMapping("/upload/checkfile")
public RestResponse<Boolean> checkfile(
@RequestParam("fileMd5") String fileMd5
) throws Exception {
return mediaFileService.checkFile (fileMd5);
}
@ApiOperation(value = "分块文件上传前的检测")
@PostMapping("/upload/checkchunk")
public RestResponse<Boolean> checkchunk(@RequestParam("fileMd5") String fileMd5,
@RequestParam("chunk") int chunk) throws Exception {
return mediaFileService.checkChunk (fileMd5, chunk);
}
@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 absolutePath = tempFile.getAbsolutePath ();
return mediaFileService.uploadChunk (fileMd5, chunk, absolutePath);
}
@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.setFileType ("001002");
uploadFileParamsDto.setTags ("课程视频");
uploadFileParamsDto.setRemark ("");
uploadFileParamsDto.setFilename (fileName);
return mediaFileService.mergeChunks (companyId, fileMd5, chunkTotal, uploadFileParamsDto);
}
检查 整个文件可在 minio
public RestResponse<Boolean> checkFile(String fileMd5) throws Exception {
MediaFiles mediaFiles = getById (fileMd5);
// 数据库存在,查 minio
if (mediaFiles != null) {
String bucket = mediaFiles.getBucket ();
String filePath = mediaFiles.getFilePath ();
FilterInputStream filterInputStream = minioClient.getObject (GetObjectArgs.builder ()
.bucket (bucket).object (filePath).build ());
// minio存在
if (filterInputStream != null) return RestResponse.success (true);
}
// 文件不存在
return RestResponse.success (false);
}
//得到分块文件的目录
private String getChunkFileFolderPath(String fileMd5) {
return fileMd5.substring (0, 1) + "/" + fileMd5.substring (1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
}
上传分块前 检查分块是否存在 minio
public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
String chunkFilePath = getChunkFileFolderPath (fileMd5) + chunkIndex;
GetObjectResponse getRes = null;
try {
getRes = minioClient.getObject (GetObjectArgs.builder ().object (chunkFilePath).bucket (videofiles).build ());
} catch (Exception e) {
// 分块不存在
return RestResponse.success (false);
}
// 分块存在
if (getRes != null) return RestResponse.success (true);
// 分块不存在
return RestResponse.success (false);
}
上传分块文件到minio
// 获取mimeType
private String getMimeType(String extension) {
if (extension == null)
extension = "";
//根据扩展名取出mimeType
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch (extension);
//通用mimeType,字节流
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
if (extensionMatch != null) {
mimeType = extensionMatch.getMimeType ();
}
return mimeType;
}
public RestResponse uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {
//得到 分块文件的目录路径
String chunkFileFolderPath = getChunkFileFolderPath (fileMd5);
//得到分块文件的路径
String chunkFilePath = chunkFileFolderPath + chunk;
//mimeType
String mimeType = getMimeType (null);
//将文件存储至minIO
boolean b = addFileToMinIo (localChunkFilePath, mimeType, videofiles, chunkFilePath);
if (!b) {
log.debug ("上传分块文件失败:{}", chunkFilePath);
return RestResponse.validfail (false, "上传分块失败");
}
log.debug ("上传分块文件成功:{}", chunkFilePath);
return RestResponse.success (true);
}
private boolean addFileToMinIo(String localFilePath, String mimeType, String bucket, String objectName) {
try {
UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder ()
// 哪个桶
.bucket (bucket)
// 给文件取个名,可以有多层目录
.object (objectName)
// 源文件路径
.filename (localFilePath)
.contentType (mimeType)
.build ();
minioClient.uploadObject (uploadObjectArgs);
log.debug ("上传文件到minio成功,bucket:{},objectName:{}", bucket, objectName);
System.out.println ("上传成功");
return true;
} catch (Exception e) {
log.error ("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}", bucket, objectName, e.getMessage (), e);
XueChengPlusException.cast ("上传文件到文件系统失败");
}
return false;
}
合并分块文件
public RestResponse mergeChunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) throws Exception {
String filename = uploadFileParamsDto.getFilename ();
// 合并
// 得到 分块文件的目录路径
String chunkFileFolderPath = getChunkFileFolderPath (fileMd5);
List<ComposeSource> sources = new ArrayList<> ();
for (int i = 0; i < chunkTotal; i++) {
String chunkFilePath = chunkFileFolderPath + i;
ComposeSource composeSource = ComposeSource.builder ()
.bucket (videofiles)
.object (chunkFilePath)
.build ();
sources.add (composeSource);
}
String extName = filename.substring (filename.lastIndexOf ("."));
// 存储合并文件的地址
String filePath = getFilePathByMd5 (fileMd5, extName);
try {
//合并文件
ObjectWriteResponse response = minioClient.composeObject (
ComposeObjectArgs.builder ()
.bucket (videofiles)
.object (filePath)
.sources (sources)
.build ());
log.debug ("合并文件成功:{}", filePath);
} catch (Exception e) {
log.debug ("合并文件失败,fileMd5:{},异常:{}", fileMd5, e.getMessage (), e);
return RestResponse.validfail (false, "合并文件失败。");
}
// 合并完成后 校验
File file = null;
try {
file = downloadFile (videofiles, filePath);
} catch (IOException e) {
log.error ("下载合并后文件失败!!");
return RestResponse.validfail (false, "下载合并后文件失败。");
}
String downloadMd5 = null;
try (FileInputStream fileInputStream = new FileInputStream (file)) {
downloadMd5 = DigestUtils.md5Hex (fileInputStream);
} catch (Exception e) {
throw new RuntimeException ("获取文件md5失败");
} finally {
if (file != null) {
file.delete ();
}
}
if (!fileMd5.equals (downloadMd5)) {
log.error ("校验失败,删除该文件:{}", filePath);
// 删除文件
minioClient.removeObject (RemoveObjectArgs.builder ().bucket (videofiles).object (filePath).build ());
return RestResponse.validfail (false, "文件合并校验失败,最终上传失败。");
}
// 文件入库
currentProxy.addMediaFilesToDb (companyId, fileMd5, uploadFileParamsDto, videofiles, filePath);
// 校验结束,删除分块
removeChunks (videofiles, chunkFileFolderPath, chunkTotal);
// 完成合并
return RestResponse.success (true);
}
/**
* 清除分块
*
* @param bucket
* @param chunkFileFolderPath
* @param chunkTotal
*/
private void removeChunks(String bucket, String chunkFileFolderPath, int chunkTotal) {
// 存储删除分块的集合
ArrayList<DeleteObject> list = new ArrayList<> ();
for (int i = 0; i < chunkTotal; i++) {
DeleteObject deleteObject = new DeleteObject (chunkFileFolderPath + i);
list.add (deleteObject);
}
RemoveObjectsArgs build = RemoveObjectsArgs.builder ().bucket (bucket).objects (list).build ();
minioClient.removeObjects (build);
}
private File downloadFile(String bucket, String filename) throws IOException {
FileOutputStream fos = null;
// 下载的文件
File minioFile = null;
try {
InputStream inputStream = minioClient.getObject (GetObjectArgs.builder ()
.bucket (bucket)
.object (filename).build ());
minioFile = File.createTempFile ("minio", ".merge");
fos = new FileOutputStream (minioFile);
IoUtils.copy (inputStream, fos);
return minioFile;
} catch (Exception e) {
log.error ("下载文件失败:{},文件名:{}", bucket, filename);
throw new RuntimeException (e);
} finally {
if (fos != null) {
fos.close ();
}
}
}
/**
* 得到合并后的文件的地址
*
* @param fileMd5 文件id即md5值
* @param fileExt 文件扩展名
* @return
*/
private String getFilePathByMd5(String fileMd5, String fileExt) {
return fileMd5.substring (0, 1) + "/" + fileMd5.substring (1, 2) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
}
上传一个文件进行分块上传,上传一半不传了,之前上传到minio的分块文件要清理吗?怎么做的?
1、在数据库中有一张文件表记录minio中存储的文件信息。
2、文件开始上传时会写入文件表,状态为上传中,上传完成会更新状态为上传完成。
3、当一个文件传了一半不再上传了说明该文件没有上传完成,会有定时任务去查询文件表中的记录,如果文件未上传完成则删除minio中没有上传成功的文件目录。