diff --git a/pom.xml b/pom.xml
index ebfa899..88def23 100644
--- a/pom.xml
+++ b/pom.xml
@@ -50,7 +50,8 @@
2.7.0
- 1.12.600
+ 2.23.0
+ 0.29.6
1.77
@@ -340,11 +341,23 @@
${ip2region.version}
-
+
- com.amazonaws
- aws-java-sdk-s3
- ${aws-java-sdk-s3.version}
+ software.amazon.awssdk
+ s3
+ ${aws.sdk.version}
+
+
+
+ software.amazon.awssdk.crt
+ aws-crt
+ ${aws.crt.version}
+
+
+
+ software.amazon.awssdk
+ s3-transfer-manager
+ ${aws.sdk.version}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/StringUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/StringUtils.java
index 6f11904..e4d3f8b 100644
--- a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/StringUtils.java
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/StringUtils.java
@@ -26,6 +26,8 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils
{
public static final String SEPARATOR = ",";
+ public static final String SLASH = "/";
+
/** 空字符串 */
private static final String NULLSTR = "";
diff --git a/ruoyi-common/ruoyi-common-oss/pom.xml b/ruoyi-common/ruoyi-common-oss/pom.xml
index 1d5bd98..8a86517 100644
--- a/ruoyi-common/ruoyi-common-oss/pom.xml
+++ b/ruoyi-common/ruoyi-common-oss/pom.xml
@@ -31,9 +31,44 @@
ruoyi-common-redis
+
- com.amazonaws
- aws-java-sdk-s3
+ software.amazon.awssdk
+ s3
+
+
+
+ software.amazon.awssdk
+ netty-nio-client
+
+
+
+ software.amazon.awssdk
+ aws-crt-client
+
+
+
+ software.amazon.awssdk
+ apache-client
+
+
+
+ software.amazon.awssdk
+ url-connection-client
+
+
+
+
+
+
+ software.amazon.awssdk.crt
+ aws-crt
+
+
+
+
+ software.amazon.awssdk
+ s3-transfer-manager
diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/core/OssClient.java b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/core/OssClient.java
index d22f540..aee7cfc 100644
--- a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/core/OssClient.java
+++ b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/core/OssClient.java
@@ -2,73 +2,114 @@ package com.ruoyi.common.oss.core;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
-import com.amazonaws.ClientConfiguration;
-import com.amazonaws.HttpMethod;
-import com.amazonaws.Protocol;
-import com.amazonaws.auth.AWSCredentials;
-import com.amazonaws.auth.AWSCredentialsProvider;
-import com.amazonaws.auth.AWSStaticCredentialsProvider;
-import com.amazonaws.auth.BasicAWSCredentials;
-import com.amazonaws.client.builder.AwsClientBuilder;
-import com.amazonaws.services.s3.AmazonS3;
-import com.amazonaws.services.s3.AmazonS3Client;
-import com.amazonaws.services.s3.AmazonS3ClientBuilder;
-import com.amazonaws.services.s3.model.*;
+import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.utils.StringUtils;
+import com.ruoyi.common.core.utils.file.FileUtils;
import com.ruoyi.common.oss.constant.OssConstant;
import com.ruoyi.common.oss.entity.UploadResult;
import com.ruoyi.common.oss.enumd.AccessPolicyType;
import com.ruoyi.common.oss.enumd.PolicyType;
import com.ruoyi.common.oss.exception.OssException;
import com.ruoyi.common.oss.properties.OssProperties;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.async.AsyncRequestBody;
+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.presigner.S3Presigner;
+import software.amazon.awssdk.transfer.s3.S3TransferManager;
+import software.amazon.awssdk.transfer.s3.model.*;
+import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener;
import java.io.ByteArrayInputStream;
import java.io.File;
+import java.io.IOException;
import java.io.InputStream;
+import java.net.URI;
import java.net.URL;
-import java.util.Date;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
/**
* S3 存储协议 所有兼容S3协议的云厂商均支持
* 阿里云 腾讯云 七牛云 minio
*
- * @author Lion Li
+ * @author AprilWind
*/
public class OssClient {
+ /**
+ * 服务商
+ */
private final String configKey;
+ /**
+ * 配置属性
+ */
private final OssProperties properties;
- private final AmazonS3 client;
+ /**
+ * Amazon S3 异步客户端
+ */
+ private final S3AsyncClient client;
+ /**
+ * 用于管理 S3 数据传输的高级工具
+ */
+ private final S3TransferManager transferManager;
+
+ /**
+ * AWS S3 预签名 URL 的生成器
+ */
+ private final S3Presigner presigner;
+
+ /**
+ * 构造方法
+ *
+ * @param configKey 配置键
+ * @param ossProperties Oss配置属性
+ */
public OssClient(String configKey, OssProperties ossProperties) {
this.configKey = configKey;
this.properties = ossProperties;
try {
- AwsClientBuilder.EndpointConfiguration endpointConfig =
- new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion());
+ // 创建 AWS 认证信息
+ StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
+ AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()));
- AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey());
- AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
- ClientConfiguration clientConfig = new ClientConfiguration();
- if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) {
- clientConfig.setProtocol(Protocol.HTTPS);
- } else {
- clientConfig.setProtocol(Protocol.HTTP);
- }
- AmazonS3ClientBuilder build = AmazonS3Client.builder()
- .withEndpointConfiguration(endpointConfig)
- .withClientConfiguration(clientConfig)
- .withCredentials(credentialsProvider)
- .disableChunkedEncoding();
- if (!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)) {
+ //创建AWS基于 CRT 的 S3 客户端
+ this.client = S3AsyncClient.crtBuilder()
+ .credentialsProvider(credentialsProvider)
+ .endpointOverride(URI.create(getEndpoint()))
+ .region(of())
+ .targetThroughputInGbps(20.0)
+ .minimumPartSizeInBytes(10 * 1025 * 1024L)
+ .checksumValidationEnabled(false)
+ .build();
+
+ //AWS基于 CRT 的 S3 AsyncClient 实例用作 S3 传输管理器的底层客户端
+ this.transferManager = S3TransferManager.builder().s3Client(this.client).build();
+
+ // 检查是否连接到 MinIO,MinIO 使用 HTTPS 限制使用域名访问,需要启用路径样式访问
+ S3Configuration config = S3Configuration.builder().chunkedEncodingEnabled(false)
// minio 使用https限制使用域名访问 需要此配置 站点填域名
- build.enablePathStyleAccess();
- }
- this.client = build.build();
+ .pathStyleAccessEnabled(!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)).build();
+ // 创建 预签名 URL 的生成器 实例,用于生成 S3 预签名 URL
+ this.presigner = S3Presigner.builder()
+ .region(of())
+ .credentialsProvider(credentialsProvider)
+ .endpointOverride(URI.create(getDomain()))
+ .serviceConfiguration(config)
+ .build();
+
+ // 创建存储桶
createBucket();
} catch (Exception e) {
if (e instanceof OssException) {
@@ -78,126 +119,158 @@ public class OssClient {
}
}
+ /**
+ * 同步创建存储桶
+ * 如果存储桶不存在,会进行创建;如果存储桶存在,不执行任何操作
+ *
+ * @throws OssException 当创建存储桶时发生异常时抛出
+ */
public void createBucket() {
+ String bucketName = properties.getBucketName();
try {
- String bucketName = properties.getBucketName();
- if (client.doesBucketExistV2(bucketName)) {
- return;
+ // 尝试获取存储桶的信息
+ client.headBucket(
+ x -> x.bucket(bucketName)
+ .build())
+ .join();
+ } catch (Exception ex) {
+ if (ex.getCause() instanceof NoSuchBucketException) {
+ try {
+ // 存储桶不存在,尝试创建存储桶
+ client.createBucket(
+ x -> x.bucket(bucketName))
+ .join();
+
+ // 设置存储桶的访问策略(Bucket Policy)
+ client.putBucketPolicy(
+ x -> x.bucket(bucketName)
+ .policy(getPolicy(bucketName, getAccessPolicy().getPolicyType())))
+ .join();
+ } catch (S3Exception e) {
+ // 存储桶创建或策略设置失败
+ throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
+ }
+ } else {
+ throw new OssException("判断Bucket是否存在失败,请核对配置信息:[" + ex.getMessage() + "]");
}
- CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
- AccessPolicyType accessPolicy = getAccessPolicy();
- createBucketRequest.setCannedAcl(accessPolicy.getAcl());
- client.createBucket(createBucketRequest);
- client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType()));
- } catch (Exception e) {
- throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
}
}
- public UploadResult upload(byte[] data, String path, String contentType) {
- return upload(new ByteArrayInputStream(data), path, contentType);
+ /**
+ * 上传文件到 Amazon S3,并返回上传结果
+ *
+ * @param filePath 本地文件路径
+ * @param key 在 Amazon S3 中的对象键
+ * @param md5Digest 本地文件的 MD5 哈希值(可选)
+ * @return UploadResult 包含上传后的文件信息
+ * @throws OssException 如果上传失败,抛出自定义异常
+ */
+ public UploadResult upload(Path filePath, String key, String md5Digest) {
+ try {
+ // 构建上传请求对象
+ FileUpload fileUpload = transferManager.uploadFile(
+ x -> x.putObjectRequest(
+ y -> y.bucket(properties.getBucketName())
+ .key(key)
+ .contentMD5(StringUtils.isNotEmpty(md5Digest) ? md5Digest : null)
+ .build())
+ .addTransferListener(LoggingTransferListener.create())
+ .source(filePath).build());
+
+ // 等待上传完成并获取上传结果
+ CompletedFileUpload uploadResult = fileUpload.completionFuture().join();
+ String eTag = uploadResult.response().eTag();
+
+ // 提取上传结果中的 ETag,并构建一个自定义的 UploadResult 对象
+ return UploadResult.builder().url(getUrl() + StringUtils.SLASH + key).filename(key).eTag(eTag).build();
+ } catch (Exception e) {
+ // 捕获异常并抛出自定义异常
+ throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
+ } finally {
+ // 无论上传是否成功,最终都会删除临时文件
+ FileUtils.del(filePath);
+ }
}
- public UploadResult upload(InputStream inputStream, String path, String contentType) {
+ /**
+ * 上传 InputStream 到 Amazon S3
+ *
+ * @param inputStream 要上传的输入流
+ * @param key 在 Amazon S3 中的对象键
+ * @param length 输入流的长度
+ * @return UploadResult 包含上传后的文件信息
+ * @throws OssException 如果上传失败,抛出自定义异常
+ */
+ public UploadResult upload(InputStream inputStream, String key, Long length) {
+ // 如果输入流不是 ByteArrayInputStream,则将其读取为字节数组再创建 ByteArrayInputStream
if (!(inputStream instanceof ByteArrayInputStream)) {
inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream));
}
try {
- ObjectMetadata metadata = new ObjectMetadata();
- metadata.setContentType(contentType);
- metadata.setContentLength(inputStream.available());
- PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata);
- // 设置上传对象的 Acl 为公共读
- putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
- client.putObject(putObjectRequest);
+ // 创建异步请求体(length如果为空会报错)
+ BlockingInputStreamAsyncRequestBody body = AsyncRequestBody.forBlockingInputStream(length);
+
+ // 使用 transferManager 进行上传
+ Upload upload = transferManager.upload(
+ x -> x.requestBody(body)
+ .putObjectRequest(
+ y -> y.bucket(properties.getBucketName())
+ .key(key)
+ .build())
+ .build());
+
+ // 将输入流写入请求体
+ body.writeInputStream(inputStream);
+
+ // 等待文件上传操作完成
+ CompletedUpload uploadResult = upload.completionFuture().join();
+ String eTag = uploadResult.response().eTag();
+
+ // 提取上传结果中的 ETag,并构建一个自定义的 UploadResult 对象
+ return UploadResult.builder().url(getUrl() + StringUtils.SLASH + key).filename(key).eTag(eTag).build();
} catch (Exception e) {
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
- return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
- }
-
- public UploadResult upload(File file, String path) {
- try {
- PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file);
- // 设置上传对象的 Acl 为公共读
- putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
- client.putObject(putObjectRequest);
- } catch (Exception e) {
- throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
- }
- return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
- }
-
- public void delete(String path) {
- path = path.replace(getUrl() + "/", "");
- try {
- client.deleteObject(properties.getBucketName(), path);
- } catch (Exception e) {
- throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
- }
- }
-
- public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
- return upload(data, getPath(properties.getPrefix(), suffix), contentType);
- }
-
- public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) {
- return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
- }
-
- public UploadResult uploadSuffix(File file, String suffix) {
- return upload(file, getPath(properties.getPrefix(), suffix));
}
/**
- * 获取文件元数据
+ * 下载文件从 Amazon S3 到临时目录
*
- * @param path 完整文件路径
+ * @param path 文件在 Amazon S3 中的对象键
+ * @return 下载后的文件在本地的临时路径
+ * @throws OssException 如果下载失败,抛出自定义异常
*/
- public ObjectMetadata getObjectMetadata(String path) {
- path = path.replace(getUrl() + "/", "");
- S3Object object = client.getObject(properties.getBucketName(), path);
- return object.getObjectMetadata();
+ public Path fileDownload(String path) {
+ // 构建临时文件
+ Path tempFilePath = FileUtils.createTempFile().toPath();
+ // 使用 S3TransferManager 下载文件
+ FileDownload downloadFile = transferManager.downloadFile(
+ x -> x.getObjectRequest(
+ y -> y.bucket(properties.getBucketName())
+ .key(removeBaseUrl(path))
+ .build())
+ .addTransferListener(LoggingTransferListener.create())
+ .destination(tempFilePath)
+ .build());
+ // 等待文件下载操作完成
+ downloadFile.completionFuture().join();
+ return tempFilePath;
}
- public InputStream getObjectContent(String path) {
- path = path.replace(getUrl() + "/", "");
- S3Object object = client.getObject(properties.getBucketName(), path);
- return object.getObjectContent();
- }
-
- public String getUrl() {
- String domain = properties.getDomain();
- String endpoint = properties.getEndpoint();
- String header = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://";
- // 云服务商直接返回
- if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
- if (StringUtils.isNotBlank(domain)) {
- return header + domain;
- }
- return header + properties.getBucketName() + "." + endpoint;
+ /**
+ * 删除云存储服务中指定路径下文件
+ *
+ * @param path 指定路径
+ */
+ public void delete(String path) {
+ try {
+ client.deleteObject(
+ x -> x.bucket(properties.getBucketName())
+ .key(removeBaseUrl(path))
+ .build());
+ } catch (Exception e) {
+ throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
- // minio 单独处理
- if (StringUtils.isNotBlank(domain)) {
- return header + domain + "/" + properties.getBucketName();
- }
- return header + endpoint + "/" + properties.getBucketName();
- }
-
- public String getPath(String prefix, String suffix) {
- // 生成uuid
- String uuid = IdUtil.fastSimpleUUID();
- // 文件路径
- String path = DateUtils.datePath() + "/" + uuid;
- if (StringUtils.isNotBlank(prefix)) {
- path = prefix + "/" + path;
- }
- return path + suffix;
- }
-
-
- public String getConfigKey() {
- return configKey;
}
/**
@@ -207,14 +280,189 @@ public class OssClient {
* @param second 授权时间
*/
public String getPrivateUrl(String objectKey, Integer second) {
- GeneratePresignedUrlRequest generatePresignedUrlRequest =
- new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey)
- .withMethod(HttpMethod.GET)
- .withExpiration(new Date(System.currentTimeMillis() + 1000L * second));
- URL url = client.generatePresignedUrl(generatePresignedUrlRequest);
+ // 使用 AWS S3 预签名 URL 的生成器 获取对象的预签名 URL
+ URL url = presigner.presignGetObject(
+ x -> x.signatureDuration(Duration.ofSeconds(second))
+ .getObjectRequest(
+ y -> y.bucket(properties.getBucketName())
+ .key(objectKey)
+ .build())
+ .build())
+ .url();
return url.toString();
}
+ /**
+ * 上传 byte[] 数据到 Amazon S3,使用指定的后缀构造对象键。
+ *
+ * @param data 要上传的 byte[] 数据
+ * @param suffix 对象键的后缀
+ * @return UploadResult 包含上传后的文件信息
+ * @throws OssException 如果上传失败,抛出自定义异常
+ */
+ public UploadResult uploadSuffix(byte[] data, String suffix) {
+ return upload(new ByteArrayInputStream(data), getPath(properties.getPrefix(), suffix), Long.valueOf(data.length));
+ }
+
+ /**
+ * 上传 InputStream 到 Amazon S3,使用指定的后缀构造对象键。
+ *
+ * @param inputStream 要上传的输入流
+ * @param suffix 对象键的后缀
+ * @param length 输入流的长度
+ * @return UploadResult 包含上传后的文件信息
+ * @throws OssException 如果上传失败,抛出自定义异常
+ */
+ public UploadResult uploadSuffix(InputStream inputStream, String suffix, Long length) {
+ return upload(inputStream, getPath(properties.getPrefix(), suffix), length);
+ }
+
+ /**
+ * 上传文件到 Amazon S3,使用指定的后缀构造对象键
+ *
+ * @param file 要上传的文件
+ * @param suffix 对象键的后缀
+ * @return UploadResult 包含上传后的文件信息
+ * @throws OssException 如果上传失败,抛出自定义异常
+ */
+ public UploadResult uploadSuffix(File file, String suffix) {
+ return upload(file.toPath(), getPath(properties.getPrefix(), suffix), null);
+ }
+
+ /**
+ * 获取文件输入流
+ *
+ * @param path 完整文件路径
+ * @return 输入流
+ */
+ public InputStream getObjectContent(String path) throws IOException {
+ // 下载文件到临时目录
+ Path tempFilePath = fileDownload(path);
+ // 创建输入流
+ InputStream inputStream = Files.newInputStream(tempFilePath);
+ // 删除临时文件
+ FileUtils.del(tempFilePath);
+ // 返回对象内容的输入流
+ return inputStream;
+ }
+
+ /**
+ * 获取 S3 客户端的终端点 URL
+ *
+ * @return 终端点 URL
+ */
+ public String getEndpoint() {
+ // 根据配置文件中的是否使用 HTTPS,设置协议头部
+ String header = getIsHttps();
+ // 拼接协议头部和终端点,得到完整的终端点 URL
+ return header + properties.getEndpoint();
+ }
+
+ /**
+ * 获取 S3 客户端的终端点 URL(自定义域名)
+ *
+ * @return 终端点 URL
+ */
+ public String getDomain() {
+ // 从配置中获取域名、终端点、是否使用 HTTPS 等信息
+ String domain = properties.getDomain();
+ String endpoint = properties.getEndpoint();
+ String header = getIsHttps();
+
+ // 如果是云服务商,直接返回域名或终端点
+ if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
+ return StringUtils.isNotEmpty(domain) ? header + domain : header + endpoint;
+ }
+
+ // 如果是 MinIO,处理域名并返回
+ if (StringUtils.isNotEmpty(domain)) {
+ return domain.startsWith(Constants.HTTPS) || domain.startsWith(Constants.HTTP) ? domain : header + domain;
+ }
+
+ // 返回终端点
+ return header + endpoint;
+ }
+
+ /**
+ * 根据传入的 region 参数返回相应的 AWS 区域
+ * 如果 region 参数非空,使用 Region.of 方法创建并返回对应的 AWS 区域对象
+ * 如果 region 参数为空,返回一个默认的 AWS 区域(例如,us-east-1),作为广泛支持的区域
+ *
+ * @return 对应的 AWS 区域对象,或者默认的广泛支持的区域(us-east-1)
+ */
+ public Region of() {
+ //AWS 区域字符串
+ String region = properties.getRegion();
+ // 如果 region 参数非空,使用 Region.of 方法创建对应的 AWS 区域对象,否则返回默认区域
+ return StringUtils.isNotEmpty(region) ? Region.of(region) : Region.US_EAST_1;
+ }
+
+ /**
+ * 获取云存储服务的URL
+ *
+ * @return 文件路径
+ */
+ public String getUrl() {
+ String domain = properties.getDomain();
+ String endpoint = properties.getEndpoint();
+ String header = getIsHttps();
+ // 云服务商直接返回
+ if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
+ return header + (StringUtils.isNotEmpty(domain) ? domain : properties.getBucketName() + "." + endpoint);
+ }
+ // MinIO 单独处理
+ if (StringUtils.isNotEmpty(domain)) {
+ // 如果 domain 以 "https://" 或 "http://" 开头
+ return (domain.startsWith(Constants.HTTPS) || domain.startsWith(Constants.HTTP)) ?
+ domain + StringUtils.SLASH + properties.getBucketName() : header + domain + StringUtils.SLASH + properties.getBucketName();
+ }
+ return header + endpoint + StringUtils.SLASH + properties.getBucketName();
+ }
+
+ /**
+ * 生成一个符合特定规则的、唯一的文件路径。通过使用日期、UUID、前缀和后缀等元素的组合,确保了文件路径的独一无二性
+ *
+ * @param prefix 前缀
+ * @param suffix 后缀
+ * @return 文件路径
+ */
+ public String getPath(String prefix, String suffix) {
+ // 生成uuid
+ String uuid = IdUtil.fastSimpleUUID();
+ // 生成日期路径
+ String datePath = DateUtils.datePath();
+ // 拼接路径
+ String path = StringUtils.isNotEmpty(prefix) ?
+ prefix + StringUtils.SLASH + datePath + StringUtils.SLASH + uuid : datePath + StringUtils.SLASH + uuid;
+ return path + suffix;
+ }
+
+ /**
+ * 移除路径中的基础URL部分,得到相对路径
+ *
+ * @param path 完整的路径,包括基础URL和相对路径
+ * @return 去除基础URL后的相对路径
+ */
+ public String removeBaseUrl(String path) {
+ return path.replace(getUrl() + StringUtils.SLASH, "");
+ }
+
+ /**
+ * 服务商
+ */
+ public String getConfigKey() {
+ return configKey;
+ }
+
+ /**
+ * 获取是否使用 HTTPS 的配置,并返回相应的协议头部。
+ *
+ * @return 协议头部,根据是否使用 HTTPS 返回 "https://" 或 "http://"
+ */
+ public String getIsHttps() {
+ return OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? Constants.HTTPS : Constants.HTTP;
+ }
+
/**
* 检查配置是否相同
*/
@@ -231,32 +479,77 @@ public class OssClient {
return AccessPolicyType.getByType(properties.getAccessPolicy());
}
+ /**
+ * 生成 AWS S3 存储桶访问策略
+ *
+ * @param bucketName 存储桶
+ * @param policyType 桶策略类型
+ * @return 符合 AWS S3 存储桶访问策略格式的字符串
+ */
private static String getPolicy(String bucketName, PolicyType policyType) {
- StringBuilder builder = new StringBuilder();
- builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n");
- builder.append(switch (policyType) {
- case WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n";
- case READ_WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n";
- default -> "\"s3:GetBucketLocation\"\n";
- });
- builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
- builder.append(bucketName);
- builder.append("\"\n},\n");
- if (policyType == PolicyType.READ) {
- builder.append("{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
- builder.append(bucketName);
- builder.append("\"\n},\n");
- }
- builder.append("{\n\"Action\": ");
- builder.append(switch (policyType) {
- case WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n";
- case READ_WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n";
- default -> "\"s3:GetObject\",\n";
- });
- builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
- builder.append(bucketName);
- builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n");
- return builder.toString();
+ String policy = switch (policyType) {
+ case WRITE -> """
+ {
+ "Version": "2012-10-17",
+ "Statement": []
+ }
+ """;
+ case READ_WRITE -> """
+ {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Principal": "*",
+ "Action": [
+ "s3:GetBucketLocation",
+ "s3:ListBucket",
+ "s3:ListBucketMultipartUploads"
+ ],
+ "Resource": "arn:aws:s3:::bucketName"
+ },
+ {
+ "Effect": "Allow",
+ "Principal": "*",
+ "Action": [
+ "s3:AbortMultipartUpload",
+ "s3:DeleteObject",
+ "s3:GetObject",
+ "s3:ListMultipartUploadParts",
+ "s3:PutObject"
+ ],
+ "Resource": "arn:aws:s3:::bucketName/*"
+ }
+ ]
+ }
+ """;
+ case READ -> """
+ {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Principal": "*",
+ "Action": ["s3:GetBucketLocation"],
+ "Resource": "arn:aws:s3:::bucketName"
+ },
+ {
+ "Effect": "Deny",
+ "Principal": "*",
+ "Action": ["s3:ListBucket"],
+ "Resource": "arn:aws:s3:::bucketName"
+ },
+ {
+ "Effect": "Allow",
+ "Principal": "*",
+ "Action": "s3:GetObject",
+ "Resource": "arn:aws:s3:::bucketName/*"
+ }
+ ]
+ }
+ """;
+ };
+ return policy.replaceAll("bucketName", bucketName);
}
}
diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/entity/UploadResult.java b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/entity/UploadResult.java
index fd2e7fc..dc5b977 100644
--- a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/entity/UploadResult.java
+++ b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/entity/UploadResult.java
@@ -21,4 +21,9 @@ public class UploadResult {
* 文件名
*/
private String filename;
+
+ /**
+ * 已上传对象的实体标记(用来校验文件)
+ */
+ private String eTag;
}
diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/enumd/AccessPolicyType.java b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/enumd/AccessPolicyType.java
index 140f67a..2784630 100644
--- a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/enumd/AccessPolicyType.java
+++ b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/enumd/AccessPolicyType.java
@@ -1,8 +1,9 @@
package com.ruoyi.common.oss.enumd;
-import com.amazonaws.services.s3.model.CannedAccessControlList;
import lombok.AllArgsConstructor;
import lombok.Getter;
+import software.amazon.awssdk.services.s3.model.BucketCannedACL;
+import software.amazon.awssdk.services.s3.model.ObjectCannedACL;
/**
* 桶访问策略配置
@@ -16,27 +17,32 @@ public enum AccessPolicyType {
/**
* private
*/
- PRIVATE("0", CannedAccessControlList.Private, PolicyType.WRITE),
+ PRIVATE("0", BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE, PolicyType.WRITE),
/**
* public
*/
- PUBLIC("1", CannedAccessControlList.PublicRead, PolicyType.READ),
+ PUBLIC("1", BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE, PolicyType.READ_WRITE),
/**
* custom
*/
- CUSTOM("2",CannedAccessControlList.PublicRead, PolicyType.READ);
+ CUSTOM("2", BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ, PolicyType.READ);
/**
- * 桶 权限类型
+ * 桶 权限类型(数据库值)
*/
private final String type;
+ /**
+ * 桶 权限类型
+ */
+ private final BucketCannedACL bucketCannedACL;
+
/**
* 文件对象 权限类型
*/
- private final CannedAccessControlList acl;
+ private final ObjectCannedACL objectCannedACL;
/**
* 桶策略类型
diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java
index 9151225..578f986 100644
--- a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java
+++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java
@@ -145,7 +145,7 @@ public class SysOssServiceImpl extends BaseServiceImpl imp
OssClient storage = OssFactory.instance();
UploadResult uploadResult;
try {
- uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType());
+ uploadResult = storage.uploadSuffix(file.getBytes(), suffix);
} catch (IOException e) {
throw new ServiceException(e.getMessage());
}