mirror of
https://gitee.com/dromara/RuoYi-Vue-Plus.git
synced 2026-09-20 02:08:15 +08:00
Pre Merge pull request !504 from 秋辞未寒/dev
This commit is contained in:
commit
94e97d9f5b
@ -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:";
|
||||
}
|
||||
|
||||
@ -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 到临时目录
|
||||
*
|
||||
|
||||
@ -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;
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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<PartInfo> partInfoList;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
@ -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<String> removeKeys = RedisUtils.keys(OssConstant.PART_UPLOAD_INFO_CACHE_KEY + "*");
|
||||
if (CollUtil.isEmpty(removeKeys)) {
|
||||
return;
|
||||
}
|
||||
removeKeys.forEach(PartUploadCacheHelper::removeCacheByKey);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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<PartInfo> partInfoList;
|
||||
|
||||
/**
|
||||
* 合并完成
|
||||
*/
|
||||
private Boolean mergeCompleted;
|
||||
|
||||
/**
|
||||
* 分片上传ID - 仅在文件成功合并,数据落库时才会生成
|
||||
* @see this#mergeCompleted 分片文件合并状态
|
||||
*/
|
||||
private Long ossId;
|
||||
|
||||
}
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
*
|
||||
|
||||
Loading…
Reference in New Issue
Block a user