前言
本文将分享下本人做大文件上传的一些思路,以及相关代码的实现。至于minio的搭建,还是比较简单的。本文就不再赘述。本文搭建的🌰例子也仅仅是把主要流程走通,相关的demo代码可能会有bug
。
有不同思路的大佬也可以在评区分享下,开拓下思路。
其实主要需要实现的就是分片上传。断点续传,秒传仅仅是在分片上传的基础上增加的逻辑扩张。
demo源码地址
https://gitee.com/Gary2016/minio-upload
演示
大致步骤
流程图
- 前端获取到文件流,计算出文件的唯一标识identifier(md5摘要)。
- 将获取到的identifier传递给后端,查询该文件的上传任务记录。如果没有则初始化一个上传任务
- 校验上传任务记录是否完成上传(成功执行合并分片的操作后视为完成上传)
3.1 任务完成,直接返回文件地址
3.2 任务未完成,获取已上传的分片。前端按照分片任务中记录的分片大小将文件分片。然后遍历所有分片进行单片上传,如果分块存在于已上传的分片列表中,则跳过该分块的上传。所有分片完成上传后,请求后端合并分片的接口进行合并。合并完成后,返回文件地址
单片上传
单片上传是通过预签名上传的方式:获取到minio经过签名的上传地址后由前端直接向minio服务器发起真正的上传请求。避免上传时占用应用服务器的带宽,影响系统稳定。
代码实现
主要技术栈
vue 3.0
element plus
promise-queue-plus
springboot 2.7.3
mybatis-plus 3.5.1
aws-java-sdk-s3 1.12.263
mysql8
minio 最新版
后端实现
数据库设计
实现断点续传,秒传的前提就是服务端需要记录文件的上传进度。因此,需要一张表来记录文件的上传记录。至于已上传的分块记录由minio提供的接口来获取。
以下是表设计
CREATE TABLE `sys_upload_task` (
`id` bigint NOT NULL,
`upload_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '分片上传的uploadId',
`file_identifier` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文件唯一标识(md5)',
`file_name` varchar(500) COLLATE utf8mb4_general_ci NOT NULL COMMENT '文件名',
`bucket_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '所属桶名',
`object_key` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文件的key',
`total_size` bigint NOT NULL COMMENT '文件大小(byte)',
`chunk_size` bigint NOT NULL COMMENT '每个分片大小(byte)',
`chunk_num` int NOT NULL COMMENT '分片数量',
PRIMARY KEY (`id`),
UNIQUE KEY `uq_file_identifier` (`file_identifier`) USING BTREE,
UNIQUE KEY `uq_upload_id` (`upload_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='分片上传-分片任务记录';
接口设计
以下接口的响应参数均被包装在Result对象的data字段中
Result
名称 | 类型 | 说明 |
---|---|---|
code | int | 自定义状态码(成功:200000,失败:500000) |
data | object | 接口真实数据 |
msg | string | 信息 |
1.根据文件唯一标识获取上传任务
主要流程就是查询数据库记录,存在上传任务再通过amazon s3的sdk方法:amazonS3.doesObjectExist
,判断是否存在文件对象,存在则说明已经合并完成。
接口地址:/v1/minio/tasks/{identifier}
请求方式:GET
响应参数:
名称 | 类型 | 说明 |
---|---|---|
finished | boolean | 是否完成上传 |
path | string | 文件地址 |
taskRecord | TaskRecordDTO | 任务记录信息 |
TaskRecordDTO
名称 | 类型 | 说明 |
---|---|---|
id | long | 任务id |
uploadId | string | minio的uploadId |
fileIdentifier | string | 文件唯一标识(MD5) |
fileName | string | 文件名称 |
bucketName | string | 所属桶名 |
objectKey | string | 文件的key |
totalSize | long | 文件大小(byte) |
chunkSize | long | 每个分片大小(byte) |
chunkNum | int | 分片数量 |
exitPartList | PartSummary[] | 已上传完的分片 (finished为true时,该字段为null) |
PartSummary(该类由s3的sdk提供)
名称 | 类型 | 说明 |
---|---|---|
partNumber | int | 分片编号 |
lastModified | Date | 最后修改时间 |
eTag | string | 分片的eTag(MD5) |
size | long | 分片大小 |
主要代码
/**
* 获取上传进度
* @param identifier 文件md5
* @return
*/
@GetMapping("/{identifier}")
public Result<TaskInfoDTO> taskInfo (@PathVariable("identifier") String identifier) {
return Result.ok(sysUploadTaskService.getTaskInfo(identifier));
}
@Override
public TaskInfoDTO getTaskInfo(String identifier) {
SysUploadTask task = getByIdentifier(identifier);
if (task == null) {
return null;
}
TaskInfoDTO result = new TaskInfoDTO().setFinished(true).setTaskRecord(TaskRecordDTO.convertFromEntity(task)).setPath(getPath(task.getBucketName(), task.getObjectKey()));
boolean doesObjectExist = amazonS3.doesObjectExist(task.getBucketName(), task.getObjectKey());
if (!doesObjectExist) {
// 未上传完,返回已上传的分片
ListPartsRequest listPartsRequest = new ListPartsRequest(task.getBucketName(), task.getObjectKey(), task.getUploadId());
PartListing partListing = amazonS3.listParts(listPartsRequest);
result.setFinished(false).getTaskRecord().setExitPartList(partListing.getParts());
}
return result;
}
2.初始化一个上传任务
当接口1返回的数据为null时,调用此接口初始化一个上传任务。
接口地址:/v1/minio/tasks
请求方式:POST
请求参数(body):
名称 | 类型 | 说明 |
---|---|---|
identifier | string | 文件唯一标识(MD5) |
totalSize | long | 文件大小(byte) |
chunkSize | long | 分片大小(byte) |
fileName | string | 文件名称 |
响应参数:与接口1的响应参数一致,此处就不再重复
主要代码
/**
* 创建一个上传任务
* @return
*/
@PostMapping
public Result<TaskInfoDTO> initTask (@Valid @RequestBody