diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/constant/OssConstant.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/constant/OssConstant.java index 9d8db9335..ef9ccefc5 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/constant/OssConstant.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/constant/OssConstant.java @@ -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:"; } diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java index eb1656c06..0562d2a52 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java @@ -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 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 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 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 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 已经上传的分片信息 + */ + public List listParts(String uploadId, String key) { + // 使用 s3client 请求获取已经上传的分片信息 + CompletableFuture 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 completedParts) { + // 使用 s3client 请求完成分片上传任务 + CompletableFuture 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 partInfoList) { + List 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 parts = listParts(uploadId, key); + // 构建 CompletedPart 列表 + List completedParts = parts.stream() + .map(part -> CompletedPart.builder().partNumber(part.partNumber()).eTag(part.eTag()).build()) + .toList(); + // 合并分片并返回结果 + return completePartUpload(uploadId, key, completedParts); + } + /** * 下载文件从 Amazon S3 到临时目录 * diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/CreatePartUploadResult.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/CreatePartUploadResult.java new file mode 100644 index 000000000..8ada7c516 --- /dev/null +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/CreatePartUploadResult.java @@ -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; + +} diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/PartInfo.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/PartInfo.java new file mode 100644 index 000000000..75c583439 --- /dev/null +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/PartInfo.java @@ -0,0 +1,32 @@ +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; +} diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/PartUploadInfo.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/PartUploadInfo.java new file mode 100644 index 000000000..4e4ef31a8 --- /dev/null +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/PartUploadInfo.java @@ -0,0 +1,81 @@ +package org.dromara.common.oss.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 分片上传信息对象 + * TODO 用于创建分片上传任务时,数据落库之前,存放分片上传信息数据到Redis + * + * @author SunnyDeer0911 + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +@Builder +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 partInfoList; +} diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/PartUploadResult.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/PartUploadResult.java new file mode 100644 index 000000000..f366d2a74 --- /dev/null +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/PartUploadResult.java @@ -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; + +} diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/utils/PartUploadCacheHelper.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/utils/PartUploadCacheHelper.java new file mode 100644 index 000000000..4a8a04184 --- /dev/null +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/utils/PartUploadCacheHelper.java @@ -0,0 +1,101 @@ +package org.dromara.common.oss.utils; + +import cn.hutool.core.collection.CollUtil; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.oss.constant.OssConstant; +import org.dromara.common.oss.core.OssClient; +import org.dromara.common.oss.entity.PartUploadInfo; +import org.dromara.common.oss.factory.OssFactory; +import org.dromara.common.redis.utils.RedisUtils; + +import java.util.Collection; + +/** + * 分片上传缓存助手 + * + * @author SunnyDeer0911 + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PartUploadCacheHelper { + + /** + * 获取分片上传信息cacheKey + * @param uploadId 文件分片上传ID + * @return 分片上传信息cacheKey + */ + public static String getPartUploadInfoCacheKey(String uploadId){ + if (StringUtils.isBlank(uploadId)) { + return null; + } + return OssConstant.PART_UPLOAD_INFO_CACHE_KEY + uploadId; + } + + /** + * 从缓存中获取分片上传信息对象 + * @param uploadId 文件分片上传ID + * @return 获取分片上传信息对象 + */ + public static PartUploadInfo getCache(String uploadId){ + String cacheKey = getPartUploadInfoCacheKey(uploadId); + if (cacheKey==null) { + return null; + } + return RedisUtils.getCacheObject(cacheKey); + } + + /** + * 将分片上传信息对象放入缓存 + * @param partUploadInfo 分片上传信息对象 + */ + public static void putCache(PartUploadInfo partUploadInfo){ + String cacheKey = getPartUploadInfoCacheKey(partUploadInfo.getUploadId()); + if (cacheKey==null) { + return; + } + RedisUtils.setCacheObject(cacheKey, partUploadInfo); + } + + /** + * 移除分片上传信息对象缓存 + * @param uploadId 文件分片上传ID + */ + public static void removeCache(String uploadId){ + String cacheKey = getPartUploadInfoCacheKey(uploadId); + if (cacheKey==null) { + return; + } + removeCacheByKey(cacheKey); + } + + /** + * 移除分片上传信息对象缓存 + * @param cacheKey 缓存key + */ + private static void removeCacheByKey(String cacheKey){ + PartUploadInfo cache = RedisUtils.getCacheObject(cacheKey); + if (cache==null) { + return; + } + // 如果未合并,则调用OssClient终止分片上传任务的方法,让OSS删除已上传的分片 + if (!cache.isNeedMerge() && CollUtil.isNotEmpty(cache.getPartInfoList())) { + OssClient instance = OssFactory.instance(cache.getService()); + // 终止分片上传任务 + instance.abortPartUpload(cache.getUploadId(),cache.getFileName()); + } + RedisUtils.deleteObject(cacheKey); + } + + /** + * 移除所有分片上传信息对象缓存 + */ + public static void removeAllCache(){ + Collection removeKeys = RedisUtils.keys(OssConstant.PART_UPLOAD_INFO_CACHE_KEY + "*"); + if (CollUtil.isEmpty(removeKeys)) { + return; + } + removeKeys.forEach(PartUploadCacheHelper::removeCacheByKey); + } + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysOssController.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysOssController.java index 73ada3b84..03d8d7b36 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysOssController.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysOssController.java @@ -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 partUpload(@RequestPart("file") MultipartFile file, @Validated SysOssPartUploadBo bo) { + if (ObjectUtil.isNull(file)) { + return R.fail("上传文件不能为空"); + } + return R.ok(ossService.partUpload(file, bo)); + } } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysOssPartUploadBo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysOssPartUploadBo.java new file mode 100644 index 000000000..295c82900 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysOssPartUploadBo.java @@ -0,0 +1,64 @@ +package org.dromara.system.domain.bo; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +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; + + /** + * 文件名 + */ + @NotBlank(message = "文件名不能为空") + private String fileName; + + /** + * 文件大小(文件的总大小) + */ + @NotNull(message = "文件大小不能为空") + private Long fileSize; + + /** + * 分片序号*(分片序号从1开始) + */ + @NotNull(message = "分片序号不能为空") + @Min(value = 1, message = "分片序号不能小于1") + private Integer partNumber; + + /** + * 分片大小 + */ +// @NotNull(message = "分片大小不能为空") + private Long partSize; + + /** + * 分片数量 + */ +// @NotNull(message = "分片数量不能为空") + private Long totalParts; + + /** + * 是否需要合并 + */ + @NotNull(message = "是否需要合并不能为空") + private Boolean needMerge; +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysOssPartUploadVo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysOssPartUploadVo.java new file mode 100644 index 000000000..335200d83 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysOssPartUploadVo.java @@ -0,0 +1,47 @@ +package org.dromara.system.domain.vo; + +import lombok.Data; +import org.dromara.common.oss.entity.PartInfo; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 分片上传对象信息VO + * + * @author SunnyDeer0911 + */ +@Data +public class SysOssPartUploadVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 分片上传ID + */ + private String uploadId; + + /** + * URL地址 + */ + private String url; + + /** + * 已完成上传的分片列表 + */ + List partInfoList; + + /** + * 合并完成 + */ + private Boolean mergeCompleted; + + /** + * 分片上传ID - 仅在文件成功合并,数据落库时才会生成 + * @see this#mergeCompleted 分片文件合并状态 + */ + private Long ossId; + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java index 057c068ce..2833aff91 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java @@ -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 ids, Boolean isValid); + /** + * 上传文件分片到对象存储服务 + * + * @param file 要上传的文件对象 + * @param bo OSS分片上传业务对象 + * @return 上传成功后的 SysOssPartUploadVo 对象,包含文件分片上传结果信息 + */ + SysOssPartUploadVo partUpload(MultipartFile file, SysOssPartUploadBo bo); + } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java index 1df221c32..46563f409 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java @@ -19,11 +19,14 @@ 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.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.oss.utils.PartUploadCacheHelper; 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 +240,113 @@ public class SysOssServiceImpl implements ISysOssService, OssService { return baseMapper.deleteBatchIds(ids) > 0; } + @Override + public SysOssPartUploadVo partUpload(MultipartFile file, SysOssPartUploadBo bo) { + try { + String uploadId = bo.getUploadId(); + + // 如果 uploadId 为空,则说明是新建上传分片 + if (StringUtils.isBlank(uploadId)) { + return createPartUpload(file, bo); + } + + // uploadId不为空,从缓存中获取分片上传信息 + PartUploadInfo partUploadInfo = PartUploadCacheHelper.getCache(uploadId); + if (ObjectUtil.isNull(partUploadInfo)) { + throw new ServiceException("未找到分片上传信息!"); + } + // TODO 是否需要合并分片 + partUploadInfo.setNeedMerge(bo.getNeedMerge()); + OssClient storage = OssFactory.instance(partUploadInfo.getService()); + // 上传分片并返回结果 + return partUpload(storage, partUploadInfo, file, bo.getPartNumber()); + } catch (Exception e) { + throw new ServiceException(e.getMessage()); + } + } + + /** + * 创建并上传分片 + * + * @param file 需要进行上传的分片文件 + * @param bo OSS分片上传业务对象 + * @return 分片上传对象信息VO + */ + private SysOssPartUploadVo createPartUpload(MultipartFile file, SysOssPartUploadBo bo) throws IOException { + // 文件大小 + Long fileSize = bo.getFileSize(); + // 分片大小 + Long partSize = bo.getPartSize(); + // 分片数量 + Long totalParts = bo.getTotalParts(); + // 文件名 + String originalFileName = bo.getFileName(); + // 从文件名中获取文件后缀 + String suffix = StringUtils.substring(originalFileName, originalFileName.lastIndexOf("."), originalFileName.length()); + OssClient storage = OssFactory.instance(); + CreatePartUploadResult partUpload = storage.createPartUploadSuffix(suffix); + // 构建分片上传信息对象 + PartUploadInfo partUploadInfo = new PartUploadInfo(); + partUploadInfo.setUploadId(partUpload.getUploadId()); + partUploadInfo.setFileName(partUpload.getFilename()); + partUploadInfo.setOriginalName(originalFileName); + partUploadInfo.setFileSuffix(suffix); + partUploadInfo.setUrl(partUpload.getUrl()); + partUploadInfo.setFileSize(fileSize); + partUploadInfo.setService(storage.getConfigKey()); + partUploadInfo.setPartSize(partSize); + partUploadInfo.setTotalParts(totalParts); + partUploadInfo.setPartInfoList(new ArrayList<>()); + // TODO 是否需要合并分片 + partUploadInfo.setNeedMerge(bo.getNeedMerge()); + // 上传分片并返回结果 + return partUpload(storage, partUploadInfo, file, bo.getPartNumber()); + } + + /** + * 上传分片 + * + * @param storage OSS客户端 + * @param partUploadInfo 分片上传信息对象 + * @param file 需要进行上传的分片文件 + * @param partNumber 分片序号 + * @return 分片上传对象信息VO + */ + private SysOssPartUploadVo partUpload(OssClient storage, PartUploadInfo partUploadInfo, MultipartFile file, Integer partNumber) throws IOException { + // 文件上传ID + String uploadId = partUploadInfo.getUploadId(); + // 构建分片上传对象信息VO + SysOssPartUploadVo sysOssPartUploadVo = new SysOssPartUploadVo(); + sysOssPartUploadVo.setUploadId(uploadId); + sysOssPartUploadVo.setUrl(partUploadInfo.getUrl()); + sysOssPartUploadVo.setPartInfoList(partUploadInfo.getPartInfoList()); + sysOssPartUploadVo.setMergeCompleted(partUploadInfo.isNeedMerge()); + // 上传分片 + PartUploadResult partUploadResult = storage.partUpload(file.getInputStream(), partUploadInfo.getFileName(), uploadId, partNumber, file.getSize()); + // 将完成上传的分片信息放入集合中 + partUploadInfo.getPartInfoList().add(new PartInfo(partUploadResult.getPartNumber(), partUploadResult.getETag())); + // 分片上传信息放入缓存 + PartUploadCacheHelper.putCache(partUploadInfo); + // 检查是否需要合并 - 已经完成所有上传时操作 + if (partUploadInfo.isNeedMerge()) { + // 如果已经完成最后一片的上传,则进行合并 + UploadResult uploadResult = storage.completePartUpload(uploadId, partUploadInfo.getFileName(), partUploadInfo.getPartInfoList()); + // 不报错即合并成功,删除缓存中的分片上传信息 + PartUploadCacheHelper.removeCache(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); + // 回填对象存储ID + sysOssPartUploadVo.setOssId(oss.getOssId()); + } + return sysOssPartUploadVo; + } + /** * 桶类型为 private 的URL 修改为临时URL时长为120s *