Merge remote-tracking branch 'my/dev' into dev

This commit is contained in:
L 2024-03-30 23:07:55 +08:00
commit 43a8b503ff
11 changed files with 591 additions and 3 deletions

View File

@ -37,4 +37,8 @@ public interface OssConstant {
*/
String IS_HTTPS = "Y";
/**
* 分片上传信息 cacheKey
*/
String PART_UPLOAD_INFO_CACHE_KEY = GlobalConstants.GLOBAL_REDIS_KEY + "sys_oss:part_upload_info:";
}

View File

@ -7,6 +7,9 @@ import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.file.FileUtils;
import org.dromara.common.oss.constant.OssConstant;
import org.dromara.common.oss.entity.CreatePartUploadResult;
import org.dromara.common.oss.entity.PartInfo;
import org.dromara.common.oss.entity.PartUploadResult;
import org.dromara.common.oss.entity.UploadResult;
import org.dromara.common.oss.enumd.AccessPolicyType;
import org.dromara.common.oss.enumd.PolicyType;
@ -19,8 +22,7 @@ import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
import software.amazon.awssdk.services.s3.model.S3Exception;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.transfer.s3.S3TransferManager;
import software.amazon.awssdk.transfer.s3.model.*;
@ -35,6 +37,9 @@ import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* S3 存储协议 所有兼容S3协议的云厂商均支持
@ -233,6 +238,212 @@ public class OssClient {
}
}
/**
* 创建分片任务
*
* @param key Amazon S3 中的对象键
* @return CreatePartUploadResult 创建上传分片任务返回体包含上传后的文件信息
* @throws OssException 如果创建失败抛出自定义异常
*/
public CreatePartUploadResult createPartUpload(String key) {
try {
// 创建分片上传
CompletableFuture<CreateMultipartUploadResponse> createPartUploadFuture = client.createMultipartUpload(builder -> builder.bucket(properties.getBucketName())
.key(key)
.acl(getAccessPolicy().getObjectCannedACL())
.build());
// 等待创建分片上传任务完成
CreateMultipartUploadResponse createPartUploadResult = createPartUploadFuture.join();
// 获取分片上传任务 uploadId
String uploadId = createPartUploadResult.uploadId();
return CreatePartUploadResult.builder().uploadId(uploadId).url(getUrl() + StringUtils.SLASH + key).filename(key).build();
} catch (Exception e) {
throw new OssException("创建分片上传任务失败,请检查配置信息:[" + e.getMessage() + "]");
}
}
/**
* 创建分片任务
*
* @param suffix 对象键的后缀
* @return CreatePartUploadResult 创建上传分片任务返回体包含上传后的文件信息
* @throws OssException 如果创建失败抛出自定义异常
*/
public CreatePartUploadResult createPartUploadSuffix(String suffix) {
return createPartUpload(getPath(properties.getPrefix(), suffix));
}
/**
* 分片文件上传
*
* @param data 文件字节数组
* @param key Amazon S3 中的对象键
* @param uploadId oss分片上传id
* @param partNumber 分片序号
* @return PartUploadResult 分片上传结果包含上传后的分片文件信息
* @throws OssException 如果上传失败抛出自定义异常
*/
public PartUploadResult partUpload(byte[] data, String key, String uploadId, Integer partNumber) {
try {
// 创建异步请求体
AsyncRequestBody requestBody = AsyncRequestBody.fromBytes(data);
// 使用 s3client 进行分片上传
CompletableFuture<UploadPartResponse> partUploadFuture = client.uploadPart(builder -> builder.bucket(properties.getBucketName())
.key(key)
.uploadId(uploadId)
.partNumber(partNumber)
.build(), requestBody);
// 等待文件上传操作完成
UploadPartResponse partUploadResult = partUploadFuture.join();
String eTag = partUploadResult.eTag();
// 提取上传结果中的 ETag并构建一个自定义的 PartUploadResult 对象
return PartUploadResult.builder().uploadId(uploadId).partNumber(partNumber).partSize((long) data.length).eTag(eTag).build();
} catch (Exception e) {
throw new OssException("上传分片文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
}
/**
* 分片文件上传
*
* @param inputStream 要上传的输入流
* @param key Amazon S3 中的对象键
* @param uploadId oss分片上传id
* @param partNumber 分片序号
* @param partSize 分片大小
* @return PartUploadResult 分片上传结果包含上传后的分片文件信息
* @throws OssException 如果上传失败抛出自定义异常
*/
public PartUploadResult partUpload(InputStream inputStream, String key, String uploadId, Integer partNumber, Long partSize) {
if (!(inputStream instanceof ByteArrayInputStream)) {
inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream));
}
try {
// 创建异步请求体length如果为空会报错
BlockingInputStreamAsyncRequestBody requestBody = AsyncRequestBody.forBlockingInputStream(partSize);
// 使用 s3client 进行分片上传
CompletableFuture<UploadPartResponse> partUploadFuture = client.uploadPart(builder -> builder.bucket(properties.getBucketName())
.key(key)
.uploadId(uploadId)
.partNumber(partNumber)
.build(), requestBody);
// 将输入流写入请求体
requestBody.writeInputStream(inputStream);
// 等待文件上传操作完成
UploadPartResponse partUploadResult = partUploadFuture.join();
String eTag = partUploadResult.eTag();
// 提取上传结果中的 ETag并构建一个自定义的 PartUploadResult 对象
return PartUploadResult.builder().uploadId(uploadId).partNumber(partNumber).partSize(partSize).eTag(eTag).build();
} catch (Exception e) {
throw new OssException("上传分片文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
}
/**
* 终止分片上传任务
*
* @param uploadId 分片上传任务ID
* @param key 文件在 Amazon S3 中的对象键
* @return boolean 终止分片上传任务结果
* @throws OssException 如果终止分片上传失败抛出自定义异常
*/
public boolean abortPartUpload(String uploadId, String key) {
try {
// 使用 s3client 请求终止分片上传任务
CompletableFuture<AbortMultipartUploadResponse> abortPartUploadFuture = client.abortMultipartUpload(builder -> builder.bucket(properties.getBucketName())
.uploadId(uploadId)
.key(key)
.build());
// 等待终止分片上传任务请求响应
AbortMultipartUploadResponse abortPartUploadResult = abortPartUploadFuture.join();
// 没有异常即成功
return true;
} catch (Exception e) {
// throw new OssException("终止分片上传任务失败,请检查配置信息:[" + e.getMessage() + "]");
return false;
}
}
/**
* 列出已经上传的分片
*
* @param uploadId 分片上传任务ID
* @param key 文件在 Amazon S3 中的对象键
* @return List<Part> 已经上传的分片信息
*/
public List<Part> listParts(String uploadId, String key) {
// 使用 s3client 请求获取已经上传的分片信息
CompletableFuture<ListPartsResponse> listPartsFuture = client.listParts(builder -> builder.bucket(properties.getBucketName())
.uploadId(uploadId)
.key(key)
.build());
// 等待获取已经上传的分片信息请求响应
ListPartsResponse listPartsResult = listPartsFuture.join();
return listPartsResult.parts();
}
/**
* 完成分片上传 - 合并分片
*
* @param uploadId 分片上传任务ID
* @param key 文件在 Amazon S3 中的对象键
* @param completedParts 已经上传的分片信息
* @return UploadResult 包含上传后的文件信息
*/
public UploadResult completePartUpload(String uploadId, String key, Collection<CompletedPart> completedParts) {
// 使用 s3client 请求完成分片上传任务
CompletableFuture<CompleteMultipartUploadResponse> completePartUploadFuture = client.completeMultipartUpload(builder -> builder.bucket(properties.getBucketName())
.uploadId(uploadId)
.key(key)
.multipartUpload(CompletedMultipartUpload.builder().parts(completedParts).build())
.build());
// 等待完成分片上传任务请求响应
CompleteMultipartUploadResponse completePartUploadResult = completePartUploadFuture.join();
// 提取上传结果中的 ETag并构建一个自定义的 UploadResult 对象
return UploadResult.builder().url(getUrl() + "/" + key).filename(key).eTag(completePartUploadResult.eTag()).build();
}
/**
* 完成分片上传 - 合并分片
*
* @param uploadId 分片上传任务ID
* @param key 文件在 Amazon S3 中的对象键
* @param partInfoList 已经上传的分片信息
* @return UploadResult 包含上传后的文件信息
*/
public UploadResult completePartUpload(String uploadId, String key, List<PartInfo> partInfoList) {
List<CompletedPart> completedParts = partInfoList.stream()
.map(partInfo -> CompletedPart.builder()
.partNumber(partInfo.getPartNumber())
.eTag(partInfo.getETag())
.build())
.toList();
return completePartUpload(uploadId, key, completedParts);
}
/**
* 完成分片上传 - 合并分片根据已上传的分片结果合并
*
* @param uploadId 分片上传任务ID
* @param key 文件在 Amazon S3 中的对象键
* @return UploadResult 包含上传后的文件信息
*/
public UploadResult completePartUpload(String uploadId, String key) {
// 获取已上传的文件分片列表
List<Part> parts = listParts(uploadId, key);
// 构建 CompletedPart 列表
List<CompletedPart> completedParts = parts.stream()
.map(part -> CompletedPart.builder().partNumber(part.partNumber()).eTag(part.eTag()).build())
.toList();
// 合并分片并返回结果
return completePartUpload(uploadId, key, completedParts);
}
/**
* 下载文件从 Amazon S3 到临时目录
*

View File

@ -0,0 +1,30 @@
package org.dromara.common.oss.entity;
import lombok.Builder;
import lombok.Data;
/**
* 创建上传分片任务返回体
*
* @author SunnyDeer0911
*/
@Data
@Builder
public class CreatePartUploadResult {
/**
* 文件路径
*/
private String url;
/**
* 分片上传ID
*/
private String uploadId;
/**
* 文件名
*/
private String filename;
}

View File

@ -0,0 +1,26 @@
package org.dromara.common.oss.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 分片信息对象
*
* @author SunnyDeer0911
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PartInfo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Integer partNumber;
private String eTag;
}

View File

@ -0,0 +1,74 @@
package org.dromara.common.oss.entity;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 分片上传信息对象
*
* @author SunnyDeer0911
*/
@Data
public class PartUploadInfo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 文件上传ID分片上传扩展字段文件分片上传S3 OSS返回的唯一标识
*/
private String uploadId;
/**
* 文件名
*/
private String fileName;
/**
* 原名
*/
private String originalName;
/**
* 文件后缀名
*/
private String fileSuffix;
/**
* URL地址
*/
private String url;
/**
* 文件大小
*/
private Long fileSize;
/**
* 服务商
*/
private String service;
/**
* 分片大小
*/
private Long partSize;
/**
* 分片数量
*/
private Long totalParts;
/**
* 合并完成
*/
private boolean needMerge;
/**
* 已完成上传的分片列表
*/
List<PartInfo> partInfoList;
}

View File

@ -0,0 +1,35 @@
package org.dromara.common.oss.entity;
import lombok.Builder;
import lombok.Data;
/**
* 分片上传返回体
*
* @author SunnyDeer0911
*/
@Data
@Builder
public class PartUploadResult {
/**
* 分片上传ID
*/
private String uploadId;
/**
* 分片序号
*/
private Integer partNumber;
/***
* 分片大小
*/
private Long partSize;
/**
* 已上传对象的实体标记用来校验文件
*/
private String eTag;
}

View File

@ -11,6 +11,8 @@ import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.system.domain.bo.SysOssBo;
import org.dromara.system.domain.bo.SysOssPartUploadBo;
import org.dromara.system.domain.vo.SysOssPartUploadVo;
import org.dromara.system.domain.vo.SysOssUploadVo;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.service.ISysOssService;
@ -105,4 +107,20 @@ public class SysOssController extends BaseController {
return toAjax(ossService.deleteWithValidByIds(List.of(ossIds), true));
}
/**
* 上传OSS对象分片文件
*
* @param file 分片文件
* @param bo OSS分片上传业务对象
* @return R 上传结果
*/
@SaCheckPermission("system:oss:upload")
@Log(title = "OSS对象存储-分片上传", businessType = BusinessType.INSERT)
@PostMapping(value = "/part/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<SysOssPartUploadVo> partUpload(@RequestPart("file") MultipartFile file, @Validated SysOssPartUploadBo bo) {
if (ObjectUtil.isNull(file)) {
return R.fail("上传文件不能为空");
}
return R.ok(ossService.partUpload(file, bo));
}
}

View File

@ -0,0 +1,62 @@
package org.dromara.system.domain.bo;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* OSS分片上传业务对象
*
* @author SunnyDeer0911
*/
@Data
public class SysOssPartUploadBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 文件上传ID分片上传扩展字段文件分片上传S3 OSS返回的唯一标识
* TODO 上传第一片分片时允许为空后续需要从第一次分片上传的响应中获取回
*/
private String uploadId;
/**
* 文件名
* TODO 允许为空如果为空则从上传的文件中获取
*/
private String fileName;
/**
* 文件大小
* TODO 文件的总大小上传第一片分片时不允许为空后续请求用不到可传可不传
*/
@NotNull(message = "文件大小不能为空")
private Long fileSize;
/**
* 分片序号
* TODO 分片序号不能为空从1开始
*/
@NotNull(message = "分片序号不能为空")
private Integer partNumber;
/**
* 分片大小
*/
private Long partSize;
/**
* 分片数量
*/
private Long totalParts;
/**
* 完成状态
* TODO 允许为空为空默认
*/
private Boolean needMerge;
}

View File

@ -0,0 +1,44 @@
package org.dromara.system.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.dromara.common.oss.entity.PartInfo;
import org.dromara.common.oss.entity.PartUploadInfo;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 分片上传对象信息VO
*
* @author SunnyDeer0911
*/
@Data
@AutoMapper(target = PartUploadInfo.class)
public class SysOssPartUploadVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 分片上传ID
*/
private String uploadId;
/**
* URL地址
*/
private String url;
/**
* 已完成上传的分片列表
*/
List<PartInfo> partInfoList;
/**
* 合并完成
*/
private Boolean mergeCompleted;
}

View File

@ -3,6 +3,8 @@ package org.dromara.system.service;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.system.domain.bo.SysOssBo;
import org.dromara.system.domain.bo.SysOssPartUploadBo;
import org.dromara.system.domain.vo.SysOssPartUploadVo;
import org.dromara.system.domain.vo.SysOssVo;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.multipart.MultipartFile;
@ -77,4 +79,13 @@ public interface ISysOssService {
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 上传文件分片到对象存储服务
*
* @param file 要上传的文件对象
* @param bo OSS分片上传业务对象
* @return 上传成功后的 SysOssPartUploadVo 对象包含文件分片上传结果信息
*/
SysOssPartUploadVo partUpload(MultipartFile file, SysOssPartUploadBo bo);
}

View File

@ -18,12 +18,16 @@ import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.file.FileUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.oss.constant.OssConstant;
import org.dromara.common.oss.core.OssClient;
import org.dromara.common.oss.entity.UploadResult;
import org.dromara.common.oss.entity.*;
import org.dromara.common.oss.enumd.AccessPolicyType;
import org.dromara.common.oss.factory.OssFactory;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.system.domain.SysOss;
import org.dromara.system.domain.bo.SysOssBo;
import org.dromara.system.domain.bo.SysOssPartUploadBo;
import org.dromara.system.domain.vo.SysOssPartUploadVo;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.mapper.SysOssMapper;
import org.dromara.system.service.ISysOssService;
@ -237,6 +241,75 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
return baseMapper.deleteBatchIds(ids) > 0;
}
@Override
public SysOssPartUploadVo partUpload(MultipartFile file, SysOssPartUploadBo bo) {
OssClient storage;
PartUploadInfo partUploadInfo;
String uploadId = bo.getUploadId();
// 如果上传ID为空则说明是创建任务此时
if (StringUtils.isBlank(uploadId)) {
storage = OssFactory.instance();
String originalFileName = bo.getFileName();
// 如果文件名为空则使用从上传的文件中获取
if (StringUtils.isBlank(originalFileName)) {
originalFileName = file.getOriginalFilename();
}
String suffix = StringUtils.substring(originalFileName, originalFileName.lastIndexOf("."), originalFileName.length());
CreatePartUploadResult partUpload = storage.createPartUploadSuffix(suffix);
uploadId = partUpload.getUploadId();
partUploadInfo = new PartUploadInfo();
partUploadInfo.setUploadId(uploadId);
partUploadInfo.setFileName(partUpload.getFilename());
partUploadInfo.setOriginalName(originalFileName);
partUploadInfo.setFileSuffix(suffix);
partUploadInfo.setUrl(partUpload.getUrl());
partUploadInfo.setFileSize(bo.getFileSize());
partUploadInfo.setService(storage.getConfigKey());
partUploadInfo.setPartSize(bo.getPartSize());
partUploadInfo.setTotalParts(bo.getTotalParts());
partUploadInfo.setPartInfoList(new ArrayList<>());
partUploadInfo.setNeedMerge(bo.getNeedMerge());
} else {
// uploadId不为空且文件不为空则进行文件上传
partUploadInfo = RedisUtils.getCacheObject(OssConstant.PART_UPLOAD_INFO_CACHE_KEY + uploadId);
partUploadInfo.setNeedMerge(bo.getNeedMerge());
storage = OssFactory.instance(partUploadInfo.getService());
}
return partUpload(storage, file, partUploadInfo, bo.getPartNumber());
}
private SysOssPartUploadVo partUpload(OssClient storage, MultipartFile file, PartUploadInfo partUploadInfo, Integer partNumber) {
try {
String uploadId = partUploadInfo.getUploadId();
PartUploadResult partUploadResult = storage.partUpload(file.getInputStream(), partUploadInfo.getFileName(), partUploadInfo.getUploadId(), partNumber, file.getSize());
// 将完成上传的分片信息放入集合中
partUploadInfo.getPartInfoList().add(new PartInfo(partUploadResult.getPartNumber(), partUploadResult.getETag()));
// 分片上传信息放入redis
RedisUtils.setCacheObject(OssConstant.PART_UPLOAD_INFO_CACHE_KEY + partUploadInfo.getUploadId(), partUploadInfo);
// 检查是否需要合并 - 已经完成所有上传时操作
if (partUploadInfo.isNeedMerge()) {
// 如果已经完成最后一片的上传则进行合并
UploadResult uploadResult = storage.completePartUpload(uploadId, partUploadInfo.getFileName(), partUploadInfo.getPartInfoList());
// 不报错即合并成功删除redis中的分片上传信息
RedisUtils.deleteObject(OssConstant.PART_UPLOAD_INFO_CACHE_KEY + uploadId);
// 数据落库
SysOss oss = new SysOss();
oss.setFileSuffix(partUploadInfo.getFileSuffix());
oss.setOriginalName(partUploadInfo.getOriginalName());
oss.setService(storage.getConfigKey());
oss.setFileName(partUploadInfo.getFileName());
oss.setUrl(partUploadInfo.getUrl());
baseMapper.insert(oss);
}
SysOssPartUploadVo sysOssPartUploadVo = MapstructUtils.convert(partUploadInfo, SysOssPartUploadVo.class);
sysOssPartUploadVo.setMergeCompleted(partUploadInfo.isNeedMerge());
return sysOssPartUploadVo;
} catch (Exception e) {
throw new ServiceException(e.getMessage());
}
}
/**
* 桶类型为 private 的URL 修改为临时URL时长为120s
*