1、前端上传文件前请求媒资接口层检查文件是否存在,如果已经存在则不再上传。
2、如果文件在系统不存在前端开始上传,首先对视频文件进行分块
3、前端分块进行上传,上传前首先检查分块是否上传,如已上传则不再上传,如果未上传则开始上传分块。
4、前端请求媒资管理接口层请求上传分块。
5、接口层请求服务层上传分块。
6、服务端将分块信息上传到MinIO。
7、前端将分块上传完毕请求接口层合并分块。
8、接口层请求服务层合并分块。
9、服务层根据文件信息找到MinIO中的分块文件,下载到本地临时目录,将所有分块下载完毕后开始合并 。
10、合并完成将合并后的文件上传到MinIO。
@Override
public RestResponse checkFile(String fileMd5) {
//在文件表存在,并且在文件系统存在,此文件才存在
MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
if(mediaFiles==null){
return RestResponse.success(false);
}
//查看是否在文件系统存在
GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(mediaFiles.getBucket()).object(mediaFiles.getFilePath()).build();
try {
InputStream inputStream = minioClient.getObject(getObjectArgs);
if(inputStream==null){
//文件不存在
return RestResponse.success(false);
}
}catch (Exception e){
//文件不存在
return RestResponse.success(false);
}
//文件已存在
return RestResponse.success(true);
}
//得到分块文件的目录
private String getChunkFileFolderPath(String fileMd5) {
return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
}
@Override
public RestResponse checkChunk(String fileMd5, int chunkIndex) {
//得到分块文件所在目录
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
//分块文件的路径
String chunkFilePath = chunkFileFolderPath + chunkIndex;
//查询文件系统分块文件是否存在
//查看是否在文件系统存在
GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(bucket_videofiles).object(chunkFilePath).build();
try {
InputStream inputStream = minioClient.getObject(getObjectArgs);
if(inputStream==null){
//文件不存在
return RestResponse.success(false);
}
}catch (Exception e){
//文件不存在
return RestResponse.success(false);
}
return RestResponse.success(true);
}
@Override
public RestResponse uploadChunk(String fileMd5, int chunk, byte[] bytes) {
//得到分块文件所在目录
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
//分块文件的路径
String chunkFilePath = chunkFileFolderPath + chunk;
try {
//将分块上传到文件系统
addMediaFilesToMinIO(bytes, bucket_videofiles, chunkFilePath);
//上传成功
return RestResponse.success(true);
} catch (Exception e) {
log.debug("上传分块文件失败:{}", e.getMessage());
return RestResponse.validfail(false,"上传分块失败");
}
}
1.从minio下载分块的代码
//根据桶和文件路径从minio下载文件
public File downloadFileFromMinIO(File file,String bucket,String objectName){
GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(bucket).object(objectName).build();
try {
}catch (Exception e){
e.printStackTrace();
XueChengPlusException.cast("查询分块文件出错");
}
return null;
}
2.要有个下载所有分块的代码,将所有传到minio中的分块下载到一个数组中
/**
* @description 下载分块
* @param fileMd5
* @param chunkTotal 分块数量
* @return java.io.File[] 分块文件数组
* @author Mr.M
* @date 2022/10/14 15:07
*/
private File[] checkChunkStatus(String fileMd5,int chunkTotal ){
//得到分块文件所在目录
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
//分块文件数组
File[] chunkFiles = new File[chunkTotal];
//开始下载
for (int i = 0; i < chunkTotal; i++) {
//分块文件的路径
String chunkFilePath = chunkFileFolderPath + i;
//分块文件
File chunkFile = null;
try {
chunkFile = File.createTempFile("chunk", null);
} catch (IOException e) {
e.printStackTrace();
XueChengPlusException.cast("创建分块临时文件出错"+e.getMessage());
}
//下载分块文件
downloadFileFromMinIO(chunkFile, bucket_videofiles, chunkFilePath);
chunkFiles[i] = chunkFile;
}
return chunkFiles;
}
然后在合并文件块时,由于使用的文件比较大,所以,要使用minio自带的上传方法,而不是byte的上传方法,二者区别如下:
自带的上传方法(不能断点续传):
private void addMediaFilesToMinIO(String filePath, String bucket, String objectName){
try {
UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.filename(filePath)
.build();
//上传
minioClient.uploadObject(uploadObjectArgs);
log.debug("文件上传成功:{}",filePath);
} catch (Exception e) {
XueChengPlusException.cast("文件上传到文件系统失败");
}
}
自定义的上传方法(基于byte数组,可以断点续传,因为在调用的时候是一个个块调用这个方法):
调用如下
@ApiOperation(value = "上传分块文件")
@PostMapping("/upload/uploadchunk")
public RestResponse uploadchunk(@RequestParam("file") MultipartFile file,
@RequestParam("fileMd5") String fileMd5,
@RequestParam("chunk") int chunk) throws Exception {
return mediaFileService.uploadChunk(fileMd5,chunk,file.getBytes());
}
@Override
public RestResponse uploadChunk(String fileMd5, int chunk, byte[] bytes) {
//得到分块文件所在目录
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
//分块文件的路径
String chunkFilePath = chunkFileFolderPath + chunk;
try {
//将分块上传到文件系统
addMediaFilesToMinIO(bytes, bucket_videofiles, chunkFilePath);
//上传成功
return RestResponse.success(true);
} catch (Exception e) {
log.debug("上传分块文件失败:{}", e.getMessage());
return RestResponse.validfail(false,"上传分块失败");
}
}
//将文件上传到分布式文件系统
private void addMediaFilesToMinIO(byte[] bytes, String bucket, String objectName) {
//资源的媒体类型
String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//默认未知二进制流
if (objectName.indexOf(".") >= 0) {
//取objectName中的扩展名
String extension = objectName.substring(objectName.lastIndexOf("."));
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
if (extensionMatch != null) {
contentType = extensionMatch.getMimeType();
}
}
try {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.bucket(bucket)
.object(objectName)
//InputStream stream, long objectSize 对象大小, long partSize 分片大小(-1表示5M,最大不要超过5T,最多10000)
.stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
.contentType(contentType)
.build();
//上传到minio
minioClient.putObject(putObjectArgs);
} catch (Exception e) {
e.printStackTrace();
log.debug("上传文件到文件系统出错:{}", e.getMessage());
XueChengPlusException.cast("上传文件到文件系统出错");
}
}
合并分块:
/***
* @description 将下载好的分块何并起来,形成一个整体文件,然后传回到文件系统和数据库系统
* @param companyId
* @param fileMd5
* @param chunkTotal
* @param uploadFileParamsDto
* @return
* @author
* @date
*/
@Override
public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
//下载分块
File[] files = checkChunkStatus(fileMd5, chunkTotal);
//得到合并后文件的扩展名
String fileName = uploadFileParamsDto.getFilename();
//扩展名
String extension
= fileName.substring(fileName.lastIndexOf("."));
File tempMergeFile = null;
try {
//创建临时何并后的文件
try {
tempMergeFile = File.createTempFile("merge", extension);
} catch (IOException e) {
XueChengPlusException.cast("创建临时何并文件出错");
}
//将散的文件何并为打的文件
try (RandomAccessFile raf_write = new RandomAccessFile(tempMergeFile, "rw");) {
byte[] b = new byte[1024];
for (File file : files) {
RandomAccessFile raf_read = new RandomAccessFile(file, "r");
int len = -1;
while ((len = raf_read.read(b)) != -1) {
raf_write.write(b);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
//检验何并后的文件是否正确
try {
FileInputStream mergeFileStream = new FileInputStream(tempMergeFile);
String md5Hex = DigestUtils.md5Hex(mergeFileStream);
if (md5Hex != fileMd5) {
log.debug("文件校验失败,文件路径: {}, 原始文件md5: {}",
tempMergeFile.getAbsolutePath(), fileMd5);
XueChengPlusException.cast("何并文件md5值校验不通过");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
//拿到何并文件在minio中的位置
String mergeFilePath = getFilePathByMd5(fileMd5, extension);
//将何并后的文件上传到文件系统
//不能使用byte流的方式,因为这样速度会慢,所以选择minio自己提供的方式来进行上传
addMediaFilesToMinIO(tempMergeFile.getAbsolutePath(), bucket_videofiles, mergeFilePath);
//将文件信息保存入库
uploadFileParamsDto.setFileSize(tempMergeFile.length());
addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_videofiles, mergeFilePath);
return RestResponse.success(true);
}finally {
if(tempMergeFile != null){
tempMergeFile.delete();
}
//删除临时分块文件
if(files != null){
for (File file : files) {
if(file.exists()){
file.delete();
}
}
}
}
}
完整代码如下(含修正):
package com.xuecheng.media.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.service.MediaFileService;
import io.minio.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.*;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
/**
* @author Mr.M
* @version 1.0
* @description TODO
* @date 2022/9/10 8:58
*/
@Slf4j
@Service
public class MediaFileServiceImpl implements MediaFileService {
@Autowired
MediaFilesMapper mediaFilesMapper;
@Autowired
MinioClient minioClient;
//普通文件存储的桶
@Value("${minio.bucket.files}")
private String bucket_files;
//视频文件存储的桶
@Value("${minio.bucket.videofiles}")
private String bucket_videofiles;
@Autowired
MediaFileService currentProxy;
@Override
public PageResult queryMediaFiels(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {
//构建查询条件对象
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
//分页对象
Page page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
// 查询数据内容获得结果
Page pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
// 获取数据列表
List list = pageResult.getRecords();
// 获取数据总数
long total = pageResult.getTotal();
// 构建结果集
PageResult mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
return mediaListResult;
}
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
//得到文件的md5值
String fileMd5 = DigestUtils.md5Hex(bytes);
if (StringUtils.isEmpty(folder)) {
//自动生成目录的路径 按年月日生成,
folder = getFileFolder(new Date(), true, true, true);
} else if (folder.indexOf("/") < 0) {
folder = folder + "/";
}
//文件名称
String filename = uploadFileParamsDto.getFilename();
if (StringUtils.isEmpty(objectName)) {
//如果objectName为空,使用文件的md5值为objectName
objectName = fileMd5 + filename.substring(filename.lastIndexOf("."));
}
objectName = folder + objectName;
try {
addMediaFilesToMinIO(bytes, bucket_files, objectName);
MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_files, objectName);
//准备返回数据
UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
return uploadFileResultDto;
} catch (Exception e) {
log.debug("上传文件失败:{}", e.getMessage());
throw new RuntimeException(e.getMessage());
}
// return null;
}
/**
* @param companyId
* @param fileId
* @param uploadFileParamsDto
* @param bucket
* @param objectName
* @return com.xuecheng.media.model.po.MediaFiles
* @description 将文件信息入库
* @author Mr.M
* @date 2022/10/14 9:14
*/
@Transactional
public MediaFiles addMediaFilesToDb(Long companyId, String fileId, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName) {
//保存到数据库
MediaFiles mediaFiles = mediaFilesMapper.selectById(fileId);
if (mediaFiles == null) {
mediaFiles = new MediaFiles();
//封装数据
BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);
mediaFiles.setId(fileId);
mediaFiles.setFileId(fileId);
mediaFiles.setCompanyId(companyId);
mediaFiles.setBucket(bucket);
mediaFiles.setFilePath(objectName);
mediaFiles.setUrl("/" + bucket + "/" + objectName);
mediaFiles.setCreateDate(LocalDateTime.now());
mediaFiles.setStatus("1");
mediaFiles.setAuditStatus("002003");
//插入文件表
mediaFilesMapper.insert(mediaFiles);
//抛出异常,制造异常
// int i=1/0;
}
return mediaFiles;
}
@Override
public RestResponse checkFile(String fileMd5) {
//在文件表存在,并且在文件系统存在,此文件才存在
MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
if(mediaFiles==null){
return RestResponse.success(false);
}
//查看是否在文件系统存在
GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(mediaFiles.getBucket()).object(mediaFiles.getFilePath()).build();
try {
InputStream inputStream = minioClient.getObject(getObjectArgs);
if(inputStream==null){
//文件不存在
return RestResponse.success(false);
}
}catch (Exception e){
//文件不存在
return RestResponse.success(false);
}
//文件已存在
return RestResponse.success(true);
}
@Override
public RestResponse checkChunk(String fileMd5, int chunkIndex) {
//得到分块文件所在目录
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
//分块文件的路径
String chunkFilePath = chunkFileFolderPath + chunkIndex;
//查询文件系统分块文件是否存在
//查看是否在文件系统存在
GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(bucket_videofiles).object(chunkFilePath).build();
try {
InputStream inputStream = minioClient.getObject(getObjectArgs);
if(inputStream==null){
//文件不存在
return RestResponse.success(false);
}
}catch (Exception e){
//文件不存在
return RestResponse.success(false);
}
return RestResponse.success(true);
}
@Override
public RestResponse uploadChunk(String fileMd5, int chunk, byte[] bytes) {
//得到分块文件所在目录
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
//分块文件的路径
String chunkFilePath = chunkFileFolderPath + chunk;
try {
//将分块上传到文件系统
addMediaFilesToMinIO(bytes, bucket_videofiles, chunkFilePath);
//上传成功
return RestResponse.success(true);
} catch (Exception e) {
log.debug("上传分块文件失败:{}", e.getMessage());
return RestResponse.validfail(false,"上传分块失败");
}
}
//合并分块
@Override
public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
//下载分块
File[] chunkFiles = checkChunkStatus(fileMd5, chunkTotal);
//得到合并后文件的扩展名
String filename = uploadFileParamsDto.getFilename();
//扩展名
String extension = filename.substring(filename.lastIndexOf("."));
File tempMergeFile = null;
try {
try {
//创建一个临时文件作为合并文件
tempMergeFile = File.createTempFile("'merge'", extension);
} catch (IOException e) {
XueChengPlusException.cast("创建临时合并文件出错");
}
//创建合并文件的流对象
try( RandomAccessFile raf_write =new RandomAccessFile(tempMergeFile, "rw")) {
byte[] b = new byte[1024];
for (File file : chunkFiles) {
//读取分块文件的流对象
try(RandomAccessFile raf_read = new RandomAccessFile(file, "r");) {
int len = -1;
while ((len = raf_read.read(b)) != -1) {
//向合并文件写数据
raf_write.write(b, 0, len);
}
}
}
} catch (IOException e) {
XueChengPlusException.cast("合并文件过程出错");
}
//校验合并后的文件是否正确
try {
FileInputStream mergeFileStream = new FileInputStream(tempMergeFile);
String mergeMd5Hex = DigestUtils.md5Hex(mergeFileStream);
if (!fileMd5.equals(mergeMd5Hex)) {
log.debug("合并文件校验不通过,文件路径:{},原始文件md5:{}", tempMergeFile.getAbsolutePath(), fileMd5);
XueChengPlusException.cast("合并文件校验不通过");
}
} catch (IOException e) {
log.debug("合并文件校验出错,文件路径:{},原始文件md5:{}", tempMergeFile.getAbsolutePath(), fileMd5);
XueChengPlusException.cast("合并文件校验出错");
}
//拿到合并文件在minio的存储路径
String mergeFilePath = getFilePathByMd5(fileMd5, extension);
//将合并后的文件上传到文件系统
addMediaFilesToMinIO(tempMergeFile.getAbsolutePath(), bucket_videofiles, mergeFilePath);
//将文件信息入库保存
uploadFileParamsDto.setFileSize(tempMergeFile.length());//合并文件的大小
addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_videofiles, mergeFilePath);
return RestResponse.success(true);
}finally {
//删除临时分块文件
if(chunkFiles!=null){
for (File chunkFile : chunkFiles) {
if(chunkFile.exists()){
chunkFile.delete();
}
}
}
//删除合并的临时文件
if(tempMergeFile!=null){
tempMergeFile.delete();
}
}
}
private String getFilePathByMd5(String fileMd5,String fileExt){
return fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" +fileMd5 +fileExt;
}
/**
* @description 下载分块
* @param fileMd5
* @param chunkTotal 分块数量
* @return java.io.File[] 分块文件数组
* @author Mr.M
* @date 2022/10/14 15:07
*/
private File[] checkChunkStatus(String fileMd5,int chunkTotal ){
//得到分块文件所在目录
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
//分块文件数组
File[] chunkFiles = new File[chunkTotal];
//开始下载
for (int i = 0; i < chunkTotal; i++) {
//分块文件的路径
String chunkFilePath = chunkFileFolderPath + i;
//分块文件
File chunkFile = null;
try {
chunkFile = File.createTempFile("chunk", null);
} catch (IOException e) {
e.printStackTrace();
XueChengPlusException.cast("创建分块临时文件出错"+e.getMessage());
}
//下载分块文件
downloadFileFromMinIO(chunkFile, bucket_videofiles, chunkFilePath);
chunkFiles[i] = chunkFile;
}
return chunkFiles;
}
//根据桶和文件路径从minio下载文件
public File downloadFileFromMinIO(File file,String bucket,String objectName){
GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(bucket).object(objectName).build();
try(
InputStream inputStream = minioClient.getObject(getObjectArgs);
FileOutputStream outputStream =new FileOutputStream(file);
) {
IOUtils.copy(inputStream,outputStream);
return file;
}catch (Exception e){
e.printStackTrace();
XueChengPlusException.cast("查询分块文件出错");
}
return null;
}
//得到分块文件的目录
private String getChunkFileFolderPath(String fileMd5) {
return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
}
//将文件上传到文件系统
private void addMediaFilesToMinIO(String filePath, String bucket, String objectName){
try {
UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.filename(filePath)
.build();
//上传
minioClient.uploadObject(uploadObjectArgs);
log.debug("文件上传成功:{}",filePath);
} catch (Exception e) {
XueChengPlusException.cast("文件上传到文件系统失败");
}
}
//将文件上传到分布式文件系统
private void addMediaFilesToMinIO(byte[] bytes, String bucket, String objectName) {
//资源的媒体类型
String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//默认未知二进制流
if (objectName.indexOf(".") >= 0) {
//取objectName中的扩展名
String extension = objectName.substring(objectName.lastIndexOf("."));
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
if (extensionMatch != null) {
contentType = extensionMatch.getMimeType();
}
}
try {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.bucket(bucket)
.object(objectName)
//InputStream stream, long objectSize 对象大小, long partSize 分片大小(-1表示5M,最大不要超过5T,最多10000)
.stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
.contentType(contentType)
.build();
//上传到minio
minioClient.putObject(putObjectArgs);
} catch (Exception e) {
e.printStackTrace();
log.debug("上传文件到文件系统出错:{}", e.getMessage());
XueChengPlusException.cast("上传文件到文件系统出错");
}
}
//根据日期拼接目录
private String getFileFolder(Date date, boolean year, boolean month, boolean day) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
//获取当前日期字符串
String dateString = sdf.format(new Date());
//取出年、月、日
String[] dateStringArray = dateString.split("-");
StringBuffer folderString = new StringBuffer();
if (year) {
folderString.append(dateStringArray[0]);
folderString.append("/");
}
if (month) {
folderString.append(dateStringArray[1]);
folderString.append("/");
}
if (day) {
folderString.append(dateStringArray[2]);
folderString.append("/");
}
return folderString.toString();
}
public static void main(String[] args) {
String extension = ".jpg";
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
if (extensionMatch != null) {
System.out.println(extensionMatch.getMimeType());
}
}
}