# Conflicts:
#	yudao-dependencies/pom.xml
This commit is contained in:
YunaiV 2025-04-26 09:44:14 +08:00
commit 2da16930ae
3 changed files with 100 additions and 67 deletions

View File

@ -115,9 +115,10 @@
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId> <!-- 文件客户端:解决 sftp 连接 -->
</dependency>
<!-- 文件客户端解决阿里云、腾讯云、minio 等 S3 连接 -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId><!-- 文件客户端解决阿里云、腾讯云、minio 等 S3 连接 -->
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>

View File

@ -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阿里云腾讯云七牛云华为云等云服务
* <p>
* S3 协议的客户端采用亚马逊提供的 software.amazon.awssdk.s3
*
* @author 芋道源码
*/
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
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<S3FileClientConfig> {
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 配置包括 regionendpoint
* 生成动态的预签名上传 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<S3FileClientConfig> {
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;
/**
* 节点地址补全协议头
*
* @return 节点地址
*/
private String buildEndpoint() {
// 如果已经是 http 或者 https则不进行拼接
if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
return config.getEndpoint();
}
@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 StrUtil.format("https://{}", config.getEndpoint());
}
}

View File

@ -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