背景
文件分块上传,其实就是AWS开一个上传任务,然后,客户端并发把切割后的文件块上传到同一个上传任务中。当所有文件块都上传完成时,客户端告诉AWS上传任务完成即可。
思路
主要就是3步:
- 开启上传任务
- 切割文件块,并发上传文件块
- 完成上传任务
这里的最后一步完成上传任务,其实就是s3把所有文件块合并成一个大文件。
方案
这里主要实现服务端三步逻辑,即开启上传任务,文件块上传,解释上传任务。
开启上传任务
@Override
public BaseResponse initUploadFileTask(InitUploadFileTaskReq initUploadFileTaskReq) {
try {
String bucketName = initUploadFileTaskReq.getBucketName();
String keyName = initUploadFileTaskReq.getKeyName();
handBucket(bucketName, s3Client);
CreateMultipartUploadRequest.Builder createMultipartUploadRequestBuilder =
CreateMultipartUploadRequest.builder();
createMultipartUploadRequestBuilder
.bucket(bucketName)
.key(keyName)
.storageClass(StorageClass.INTELLIGENT_TIERING);
String contentType = initUploadFileTaskReq.getContentType();
if (StringUtils.hasText(contentType)) {
createMultipartUploadRequestBuilder.contentType(initUploadFileTaskReq.getContentType());
}
// 开启上传任务
CreateMultipartUploadResponse createMultipartUploadResponse =
s3Client.createMultipartUpload(createMultipartUploadRequestBuilder.build());
log.info(
"开始上传任务请求参数:{}, 返回任务ID:{}",
initUploadFileTaskReq,
createMultipartUploadResponse.uploadId());
return BaseResponse.builder()
.data(
InitUploadFileTaskRes.builder()
.uploadId(createMultipartUploadResponse.uploadId())
.build())
.build();
} catch (SdkClientException e) {
log.error("异常开始上传任务请求参数:{}", initUploadFileTaskReq);
throw new HandleException(
String.format(
"异常开始上传任务请求参数:%s, 异常:%s", initUploadFileTaskReq.toString(), e.getMessage()));
}
}
这里的InitUploadFileTaskReq
和BaseResponse
是DTO层对象,不可考虑,这里针对桶名和key名开启分块上传任务,并返回任务ID给前端。其他初始化s3的客户端,已经构建分块上传任务请求逻辑,和之前文章《AWS之S3文件上传(简单文件上传)》中类似。这里开启分块上传任务关键代码行如下:
CreateMultipartUploadResponse createMultipartUploadResponse =
s3Client.createMultipartUpload(createMultipartUploadRequestBuilder.build());
主要从CreateMultipartUploadResponse
中获得任务id。
分块上传
@Override
public BaseResponse uploadMultipartFile(UploadFileReq req) {
String bucketName = req.getBucketName();
String keyName = req.getKeyName();
String uploadId = req.getUploadId();
int partNumber = req.getPartNumber();
MultipartFile multipartFile = req.getFile();
File tmpFile = null;
try {
tmpFile = File.createTempFile("uploadFileTemp", ".tmp");
multipartFile.transferTo(tmpFile);
UploadPartRequest uploadRequest =
UploadPartRequest.builder()
.bucket(bucketName)
.key(keyName)
.uploadId(uploadId)
.partNumber(partNumber)
.contentLength(tmpFile.length())
.build();
RequestBody requestBody = RequestBody.fromFile(tmpFile);
// 上传分块文件
UploadPartResponse uploadPartResponse = s3Client.uploadPart(uploadRequest, requestBody);
log.info("上传文件段对象:{}-返回ETag对象:{}", req, uploadPartResponse.eTag());
return BaseResponse.builder()
.data(
PartETagRes.builder()
.eTag(uploadPartResponse.eTag())
.partNumber(partNumber)
.build())
.build();
} catch (SdkClientException | IOException e) {
log.error("上传文件段对象异常:{},错误:{}", req, e);
throw new HandleException(
String.format("上传文件段对象异常:%s,错误:%s", req.toString(), e.getMessage()));
} finally {
if (tmpFile != null) {
if (tmpFile.exists()) {
if (!tmpFile.delete()) {
log.error(String.format("临时文件删除失败!%s", tmpFile.getAbsolutePath()));
}
}
}
}
}
这里关键行如下:
UploadPartResponse uploadPartResponse = s3Client.uploadPart(uploadRequest, requestBody);
从分块上传UploadPartResponse
中获取到eTag
值,并告诉前端即可。
这里到partNumber
是文件分块顺序,即当前子文件块在母文件中排序是多少。所以,前端需要记住子文件块数组中,每个子文件块对应的eTag值为多少。
结束上传任务
@Override
public BaseResponse completeMultipartUploadFile(CompleteUploadFileReq req) {
try {
String bucketName = req.getBucketName();
String keyName = req.getKeyName();
String uploadId = req.getUploadId();
List<PartETagRes> partETagResList = req.getPartETagResList();
List<CompletedPart> partETags =
partETagResList.stream()
.map(
partETagRes ->
CompletedPart.builder()
.partNumber(partETagRes.getPartNumber())
.eTag(partETagRes.getETag())
.build())
.collect(Collectors.toList());
CompleteMultipartUploadRequest completeMultipartUploadRequest =
CompleteMultipartUploadRequest.builder()
.bucket(bucketName)
.key(keyName)
.uploadId(uploadId)
.multipartUpload(CompletedMultipartUpload.builder().parts(partETags).build())
.build();
// 完成上传任务
CompleteMultipartUploadResponse completeMultipartUploadResponse =
s3Client.completeMultipartUpload(completeMultipartUploadRequest);
log.info("完成合并文件段请求参数:{}-返回结构:{}", req, completeMultipartUploadResponse.toString());
return BaseResponse.builder()
.data(
CompleteMultipartUploadFileRes.builder()
.url(String.format("%s/%s", bucketName, keyName))
.name(keyName2FileName(keyName))
.build())
.build();
} catch (SdkClientException e) {
log.error(String.format("请求参数%s, 完成合并文件段请求异常:", req.toString()), e);
throw new HandleException(
String.format("完成合并文件段请求参数:%s, 异常:%s", req.toString(), e.getMessage()));
}
}
结束上传任务关键行为:
CompleteMultipartUploadResponse completeMultipartUploadResponse =
s3Client.completeMultipartUpload(completeMultipartUploadRequest);
前端需要准备List<CompletedPart>
的数据,即每个分块的序号和eTag数据,s3需要依赖这个信息来合并文件。
总结
到这里就完成s3分块上传任务实现了,主要就是3个步骤。开启任务,并发上传分块,结束任务。AWS的S3文件上传暂时搞一段落了。