Browse Source

[重大更新] 升级 awsS3 到2.X版本 支持异步与自动分片上传下载

疯狂的狮子Li 1 year ago
parent
commit
348bd00fa3

+ 18 - 4
pom.xml

@@ -44,7 +44,8 @@
         <ip2region.version>2.7.0</ip2region.version>
 
         <!-- OSS 配置 -->
-        <aws-java-sdk-s3.version>1.12.600</aws-java-sdk-s3.version>
+        <aws.sdk.version>2.23.0</aws.sdk.version>
+        <aws.crt.version>0.29.6</aws.crt.version>
         <!-- SMS 配置 -->
         <sms4j.version>2.2.0</sms4j.version>
         <!-- 限制框架中的fastjson版本 -->
@@ -235,10 +236,23 @@
                 <version>${okhttp.version}</version>
             </dependency>
 
+            <!--  AWS SDK for Java 2.x  -->
             <dependency>
-                <groupId>com.amazonaws</groupId>
-                <artifactId>aws-java-sdk-s3</artifactId>
-                <version>${aws-java-sdk-s3.version}</version>
+                <groupId>software.amazon.awssdk</groupId>
+                <artifactId>s3</artifactId>
+                <version>${aws.sdk.version}</version>
+            </dependency>
+            <!-- 使用AWS基于 CRT 的 S3 客户端 -->
+            <dependency>
+                <groupId>software.amazon.awssdk.crt</groupId>
+                <artifactId>aws-crt</artifactId>
+                <version>${aws.crt.version}</version>
+            </dependency>
+            <!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
+            <dependency>
+                <groupId>software.amazon.awssdk</groupId>
+                <artifactId>s3-transfer-manager</artifactId>
+                <version>${aws.sdk.version}</version>
             </dependency>
             <!--短信sms4j-->
             <dependency>

+ 2 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java

@@ -22,6 +22,8 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils {
 
     public static final String SEPARATOR = ",";
 
+    public static final String SLASH = "/";
+
     /**
      * 获取参数不为空值
      *

+ 38 - 2
ruoyi-common/ruoyi-common-oss/pom.xml

@@ -26,10 +26,46 @@
             <artifactId>ruoyi-common-redis</artifactId>
         </dependency>
 
+        <!--  AWS SDK for Java 2.x  -->
         <dependency>
-            <groupId>com.amazonaws</groupId>
-            <artifactId>aws-java-sdk-s3</artifactId>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>s3</artifactId>
+            <exclusions>
+                <!-- 将基于 Netty 的 HTTP 客户端从类路径中移除 -->
+                <exclusion>
+                    <groupId>software.amazon.awssdk</groupId>
+                    <artifactId>netty-nio-client</artifactId>
+                </exclusion>
+                <!-- 将基于 CRT 的 HTTP 客户端从类路径中移除 -->
+                <exclusion>
+                    <groupId>software.amazon.awssdk</groupId>
+                    <artifactId>aws-crt-client</artifactId>
+                </exclusion>
+                <!-- 将基于 Apache 的 HTTP 客户端从类路径中移除 -->
+                <exclusion>
+                    <groupId>software.amazon.awssdk</groupId>
+                    <artifactId>apache-client</artifactId>
+                </exclusion>
+                <!-- 将配置基于 URL 连接的 HTTP 客户端从类路径中移除 -->
+                <exclusion>
+                    <groupId>software.amazon.awssdk</groupId>
+                    <artifactId>url-connection-client</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
+
+        <!-- 使用AWS基于 CRT 的 S3 客户端 -->
+        <dependency>
+            <groupId>software.amazon.awssdk.crt</groupId>
+            <artifactId>aws-crt</artifactId>
+        </dependency>
+
+        <!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
+        <dependency>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>s3-transfer-manager</artifactId>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 438 - 131
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/core/OssClient.java

@@ -2,73 +2,115 @@ package org.dromara.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 org.dromara.common.core.constant.Constants;
 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.UploadResult;
 import org.dromara.common.oss.enumd.AccessPolicyType;
 import org.dromara.common.oss.enumd.PolicyType;
 import org.dromara.common.oss.exception.OssException;
 import org.dromara.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.nio.file.Paths;
+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());
-
-            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 认证信息
+            StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
+                AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()));
+
+            //创建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,141 +120,361 @@ 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();
+    /**
+     * 下载文件从 Amazon S3 到临时目录
+     *
+     * @param path 文件在 Amazon S3 中的对象键
+     * @return 下载后的文件在本地的临时路径
+     * @throws OssException 如果下载失败,抛出自定义异常
+     */
+    public Path fileDownload(String path) {
+        // 从路径中移除 URL 前缀
+        String url = removeBaseUrl(path);
+
+        // 构建临时文件路径 文件名必须是唯一不存在的,路径必须是存在的
+        Path tempFilePath = Paths.get(extractFileName(url));
+        // 使用 S3TransferManager 下载文件
+        FileDownload downloadFile = transferManager.downloadFile(
+            x -> x.getObjectRequest(
+                    y -> y.bucket(properties.getBucketName())
+                        .key(url)
+                        .build())
+                .addTransferListener(LoggingTransferListener.create())
+                .destination(tempFilePath)
+                .build());
+        // 等待文件下载操作完成
+        downloadFile.completionFuture().join();
+        return tempFilePath;
     }
 
+    /**
+     * 删除云存储服务中指定路径下文件
+     *
+     * @param path 指定路径
+     */
     public void delete(String path) {
-        path = path.replace(getUrl() + "/", "");
         try {
-            client.deleteObject(properties.getBucketName(), path);
+            client.deleteObject(
+                x -> x.bucket(properties.getBucketName())
+                    .key(removeBaseUrl(path))
+                    .build());
         } 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);
+    /**
+     * 获取私有URL链接
+     *
+     * @param objectKey 对象KEY
+     * @param second    授权时间
+     */
+    public String getPrivateUrl(String objectKey, Integer second) {
+        // 使用 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));
     }
 
-    public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) {
-        return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
+    /**
+     * 上传 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, getPath(properties.getPrefix(), suffix));
+        return upload(file.toPath(), getPath(properties.getPrefix(), suffix), null);
     }
 
     /**
-     * 获取文件元数据
+     * 获取文件输入流
      *
      * @param path 完整文件路径
+     * @return 输入流
      */
-    public ObjectMetadata getObjectMetadata(String path) {
-        path = path.replace(getUrl() + "/", "");
-        S3Object object = client.getObject(properties.getBucketName(), path);
-        return object.getObjectMetadata();
+    public InputStream getObjectContent(String path) throws IOException {
+        // 下载文件到临时目录
+        Path tempFilePath = fileDownload(path);
+        // 创建输入流
+        InputStream inputStream = Files.newInputStream(tempFilePath);
+        // 删除临时文件
+        FileUtils.del(tempFilePath);
+        // 返回对象内容的输入流
+        return inputStream;
     }
 
-    public InputStream getObjectContent(String path) {
-        path = path.replace(getUrl() + "/", "");
-        S3Object object = client.getObject(properties.getBucketName(), path);
-        return object.getObjectContent();
+    /**
+     * 获取 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 = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://";
+        String header = getIsHttps();
         // 云服务商直接返回
         if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
-            if (StringUtils.isNotBlank(domain)) {
-                return header + domain;
-            }
-            return header + properties.getBucketName() + "." + endpoint;
+            return header + (StringUtils.isNotEmpty(domain) ? domain : properties.getBucketName() + "." + endpoint);
         }
-        // minio 单独处理
-        if (StringUtils.isNotBlank(domain)) {
-            return header + domain + "/" + properties.getBucketName();
+        // 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 + "/" + 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 path = DateUtils.datePath() + "/" + uuid;
-        if (StringUtils.isNotBlank(prefix)) {
-            path = prefix + "/" + path;
-        }
+        // 生成日期路径
+        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, "");
+    }
+
+    /**
+     * 从文件路径中提取文件名
+     *
+     * @param path 文件路径
+     * @return 提取的文件名或默认文件名
+     */
+    public String extractFileName(String path) {
+        return FileUtils.getTmpDir() + StringUtils.SLASH + Paths.get(path).getFileName().toString();
+    }
 
+    /**
+     * 服务商
+     */
     public String getConfigKey() {
         return configKey;
     }
 
     /**
-     * 获取私有URL链接
+     * 获取是否使用 HTTPS 的配置,并返回相应的协议头部。
      *
-     * @param objectKey 对象KEY
-     * @param second    授权时间
+     * @return 协议头部,根据是否使用 HTTPS 返回 "https://" 或 "http://"
      */
-    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);
-        return url.toString();
+    public String getIsHttps() {
+        return OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? Constants.HTTPS : Constants.HTTP;
     }
 
     /**
@@ -231,32 +493,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);
     }
 
 }

+ 6 - 0
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/entity/UploadResult.java

@@ -21,4 +21,10 @@ public class UploadResult {
      * 文件名
      */
     private String filename;
+
+    /**
+     * 已上传对象的实体标记(用来校验文件)
+     */
+    private String eTag;
+
 }

+ 12 - 6
ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/enumd/AccessPolicyType.java

@@ -1,8 +1,9 @@
 package org.dromara.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;
 
     /**
      * 桶策略类型

+ 1 - 1
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java

@@ -138,7 +138,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
         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());
         }