diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java
index 5c76e1a7c8..d74d805601 100644
--- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java
@@ -4,29 +4,31 @@ import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient;
-import com.amazonaws.HttpMethod;
-import com.amazonaws.auth.AWSStaticCredentialsProvider;
-import com.amazonaws.auth.BasicAWSCredentials;
-import com.amazonaws.client.builder.AwsClientBuilder;
-import com.amazonaws.services.s3.AmazonS3Client;
-import com.amazonaws.services.s3.AmazonS3ClientBuilder;
-import com.amazonaws.services.s3.model.ObjectMetadata;
-import com.amazonaws.services.s3.model.S3Object;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3Configuration;
+import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
+import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
-import java.io.ByteArrayInputStream;
-import java.util.Date;
-import java.util.concurrent.TimeUnit;
+import java.net.URI;
+import java.time.Duration;
/**
* 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
- *
- * S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库
*
* @author 芋道源码
*/
public class S3FileClient extends AbstractFileClient {
- private AmazonS3Client client;
+ private S3Client client;
+ private S3Presigner presigner;
public S3FileClient(Long id, S3FileClientConfig config) {
super(id, config);
@@ -38,31 +40,78 @@ public class S3FileClient extends AbstractFileClient {
if (StrUtil.isEmpty(config.getDomain())) {
config.setDomain(buildDomain());
}
- // 初始化客户端
- client = (AmazonS3Client)AmazonS3ClientBuilder.standard()
- .withCredentials(buildCredentials())
- .withEndpointConfiguration(buildEndpointConfiguration())
+ // 初始化 S3 客户端
+ Region region = Region.of("us-east-1"); // 必须填,但填什么都行,常见的值有 "us-east-1",不填会报错
+ AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
+ AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret()));
+ URI endpoint = URI.create(buildEndpoint());
+ S3Configuration serviceConfiguration = S3Configuration.builder() // Path-style 访问
+ .pathStyleAccessEnabled(Boolean.TRUE.equals(config.getEnablePathStyleAccess())).build();
+ client = S3Client.builder()
+ .credentialsProvider(credentialsProvider)
+ .region(region)
+ .endpointOverride(endpoint)
+ .serviceConfiguration(serviceConfiguration)
+ .build();
+ presigner = S3Presigner.builder()
+ .credentialsProvider(credentialsProvider)
+ .region(region)
+ .endpointOverride(endpoint)
+ .serviceConfiguration(serviceConfiguration)
.build();
}
- /**
- * 基于 config 秘钥,构建 S3 客户端的认证信息
- *
- * @return S3 客户端的认证信息
- */
- private AWSStaticCredentialsProvider buildCredentials() {
- return new AWSStaticCredentialsProvider(
- new BasicAWSCredentials(config.getAccessKey(), config.getAccessSecret()));
+ @Override
+ public String upload(byte[] content, String path, String type) {
+ // 构造 PutObjectRequest
+ PutObjectRequest putRequest = PutObjectRequest.builder()
+ .bucket(config.getBucket())
+ .key(path)
+ .contentType(type)
+ .contentLength((long) content.length)
+ .build();
+ // 上传文件
+ client.putObject(putRequest, RequestBody.fromBytes(content));
+ // 拼接返回路径
+ return config.getDomain() + "/" + path;
+ }
+
+ @Override
+ public void delete(String path) {
+ DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder()
+ .bucket(config.getBucket())
+ .key(path)
+ .build();
+ client.deleteObject(deleteRequest);
+ }
+
+ @Override
+ public byte[] getContent(String path) {
+ GetObjectRequest getRequest = GetObjectRequest.builder()
+ .bucket(config.getBucket())
+ .key(path)
+ .build();
+ return IoUtil.readBytes(client.getObject(getRequest));
+ }
+
+ @Override
+ public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) {
+ Duration expiration = Duration.ofHours(24);
+ return new FilePresignedUrlRespDTO(getPresignedUrl(path, expiration), config.getDomain() + "/" + path);
}
/**
- * 构建 S3 客户端的 Endpoint 配置,包括 region、endpoint
+ * 生成动态的预签名上传 URL
*
- * @return S3 客户端的 EndpointConfiguration 配置
+ * @param path 相对路径
+ * @param expiration 过期时间
+ * @return 生成的上传 URL
*/
- private AwsClientBuilder.EndpointConfiguration buildEndpointConfiguration() {
- return new AwsClientBuilder.EndpointConfiguration(config.getEndpoint(),
- null); // 无需设置 region
+ private String getPresignedUrl(String path, Duration expiration) {
+ return presigner.presignPutObject(PutObjectPresignRequest.builder()
+ .signatureDuration(expiration)
+ .putObjectRequest(b -> b.bucket(config.getBucket()).key(path))
+ .build()).url().toString();
}
/**
@@ -79,40 +128,17 @@ public class S3FileClient extends AbstractFileClient {
return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
}
- @Override
- public String upload(byte[] content, String path, String type) throws Exception {
- // 元数据,主要用于设置文件类型
- ObjectMetadata objectMetadata = new ObjectMetadata();
- objectMetadata.setContentType(type);
- objectMetadata.setContentLength(content.length); // 如果不设置,会有 “ No content length specified for stream data” 警告日志
- // 执行上传
- client.putObject(config.getBucket(),
- path, // 相对路径
- new ByteArrayInputStream(content), // 文件内容
- objectMetadata);
-
- // 拼接返回路径
- return config.getDomain() + "/" + path;
- }
-
- @Override
- public void delete(String path) throws Exception {
- client.deleteObject(config.getBucket(), path);
- }
-
- @Override
- public byte[] getContent(String path) throws Exception {
- S3Object tempS3Object = client.getObject(config.getBucket(), path);
- return IoUtil.readBytes(tempS3Object.getObjectContent());
- }
-
- @Override
- public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception {
- // 设定过期时间为 10 分钟。取值范围:1 秒 ~ 7 天
- Date expiration = new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(10));
- // 生成上传 URL
- String uploadUrl = String.valueOf(client.generatePresignedUrl(config.getBucket(), path, expiration , HttpMethod.PUT));
- return new FilePresignedUrlRespDTO(uploadUrl, config.getDomain() + "/" + path);
+ /**
+ * 节点地址补全协议头
+ *
+ * @return 节点地址
+ */
+ private String buildEndpoint() {
+ // 如果已经是 http 或者 https,则不进行拼接
+ if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
+ return config.getEndpoint();
+ }
+ return StrUtil.format("https://{}", config.getEndpoint());
}
}
diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java
index 02fc5d0cea..c4a402b3b3 100644
--- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java
+++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java
@@ -67,6 +67,12 @@ public class S3FileClientConfig implements FileClientConfig {
@NotNull(message = "accessSecret 不能为空")
private String accessSecret;
+ /**
+ * 是否启用 PathStyle 访问
+ */
+ @NotNull(message = "enablePathStyleAccess 不能为空")
+ private Boolean enablePathStyleAccess;
+
@SuppressWarnings("RedundantIfStatement")
@AssertTrue(message = "domain 不能为空")
@JsonIgnore