近期的项目中涉及到1G以上的视频上传,除了考虑上传效率之外,还要考虑到用户在上传过程中出现断网,浏览器意外关闭等情况。下面是我调研的两种处理方式:
一、传统的处理方案是在本地新建临时文件夹,储存视频分片,等全部上传完成后进行合并,然后删除临时文件夹,这样做会存在问题:比如多个用户都上传了一半视频不想上传了,没有进行合并,那么将会大致大量的临时文件夹保存在服务器,造成空间浪费。这里还需要自己想办法手动清理临时文件中保存的视频分片。
二、通过Minio实现分片及断点续传,该方案可以解决上述传统方案中所产生的问题,minio自己会去处理没有上传完的分片。下面记录一下我的使用过程。
思路:
1、前端根据视频文件计算MD5,MD5用来做断点续传,如果不需要断点续传可以不计算MD5。
2、前端掉用后端接口获取切片临时上传地址,获取地址后,前端直连Minio将切片视频进行上传
3、如果出现上传中断,再次选择该文件上传时,后端根据MD5去数据库查取数据,并调用minio的listParts方法获取已上传的切片,后端处理数据后,响应给前端未上传的切片上传地址,前端拿到后将未上传的切分进行上传。
4、前端在所有切片上传完成后,调用后端的completeMultipartUpload方法合并所有切片。
代码:
依赖引入
io.minio
minio
${minio.version}
minio连接配置类: MinioConfig
public class MinioConfig {
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
/**
* 注入minio 客户端
* @return
*/
@Bean
public MinioUtils minioClient(){
MinioClient minioClient = MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
return new MinioUtils(minioClient);
}
}
minio工具类:MinioClient
public class MinioUtils extends MinioClient {
public MinioUtilS(MinioClient client) {
super(client);
}
/**
* 上传分片上传请求,返回uploadId
* @param bucketName 存储桶
* @param region 区域
* @param objectName 对象名
* @param headers 消息头
* @param extraQueryParams 额外查询参数
*/
public CreateMultipartUploadResponse getUploadId(String bucketName, String region, String objectName, Multimap headers, Multimap extraQueryParams) throws Exception{
return super.createMultipartUpload(bucketName, region, objectName, headers, extraQueryParams);
}
/**
* 返回临时带签名、过期时间为1天的PUT请求方式的访问URL
* @param bucketName 桶名
* @param filePath Oss文件路径
* @param queryParams 查询参数
* @return 临时带签名、过期时间为1天的PUT请求方式的访问URL
*/
public String getPresignedObjectUrl(String bucketName, String filePath, Map queryParams) throws Exception{
return super.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(bucketName)
.object(filePath)
.expiry(1, TimeUnit.DAYS)
.extraQueryParams(queryParams)
.build());
}
/**
* 查询分片数据
* @param bucketName 存储桶
* @param region 区域
* @param objectName 对象名
* @param uploadId 上传ID
* @param extraHeaders 额外消息头
* @param extraQueryParams 额外查询参数
*/
public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap extraHeaders, Multimap extraQueryParams) throws ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, IOException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
return super.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
}
/**
* 完成分片上传,执行合并文件
* @param bucketName 存储桶
* @param region 区域
* @param objectName 对象名
* @param uploadId 上传ID
* @param parts 分片
* @param extraHeaders 额外消息头
* @param extraQueryParams 额外查询参数
*/
@Override
public ObjectWriteResponse completeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap extraHeaders, Multimap extraQueryParams) throws ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, IOException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
return super.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
}
}
Controller
@RestController
@RequestMapping("/minio")
public class MinioController {
@Autowired
private MinioService minioService;
@PostMapping("/getUploadUrls")
@ApiModelProperty(value = "获取minio的uploadId及分片上传路径", notes = "获取minio的uploadId及分片上传路径")
public MinioResponse createMultipartUpload(@RequestBody MinioDTO minioDTO) throws Exception{
return minioService.createMultipartUpload(minioDTO);
}
@PostMapping("/merge")
@ApiModelProperty(value = "合并切片视频", notes = "合并切片视频")
public String completeMultipartUpload(@RequestBody MinioDTO minioDTO) throws Exception {
return minioService.completeMultipartUpload(minioDTO);
}
}
具体的方法实现需要根据实际业务需求去开发,思路提供在这里