升级 awsS3 到2.X版本 支持异步与自动分片上传下载

This commit is contained in:
数据小王子 2024-02-29 14:15:17 +08:00
parent fc2eab2a6d
commit d151c053a7
7 changed files with 529 additions and 175 deletions
pom.xml
ruoyi-common
ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils
ruoyi-common-oss
pom.xml
src/main/java/com/ruoyi/common/oss
ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl

23
pom.xml
View File

@ -50,7 +50,8 @@
<!-- 离线IP地址定位库 --> <!-- 离线IP地址定位库 -->
<ip2region.version>2.7.0</ip2region.version> <ip2region.version>2.7.0</ip2region.version>
<!-- OSS 配置 --> <!-- 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>
<!-- 加解密依赖库 --> <!-- 加解密依赖库 -->
<bcprov-jdk.version>1.77</bcprov-jdk.version> <bcprov-jdk.version>1.77</bcprov-jdk.version>
<!-- SMS 配置 --> <!-- SMS 配置 -->
@ -340,11 +341,23 @@
<version>${ip2region.version}</version> <version>${ip2region.version}</version>
</dependency> </dependency>
<!-- OSS 配置 --> <!-- AWS SDK for Java 2.x -->
<dependency> <dependency>
<groupId>com.amazonaws</groupId> <groupId>software.amazon.awssdk</groupId>
<artifactId>aws-java-sdk-s3</artifactId> <artifactId>s3</artifactId>
<version>${aws-java-sdk-s3.version}</version> <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> </dependency>
<!-- 加解密依赖库 --> <!-- 加解密依赖库 -->

View File

@ -26,6 +26,8 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils
{ {
public static final String SEPARATOR = ","; public static final String SEPARATOR = ",";
public static final String SLASH = "/";
/** 空字符串 */ /** 空字符串 */
private static final String NULLSTR = ""; private static final String NULLSTR = "";

View File

@ -31,9 +31,44 @@
<artifactId>ruoyi-common-redis</artifactId> <artifactId>ruoyi-common-redis</artifactId>
</dependency> </dependency>
<!-- AWS SDK for Java 2.x -->
<dependency> <dependency>
<groupId>com.amazonaws</groupId> <groupId>software.amazon.awssdk</groupId>
<artifactId>aws-java-sdk-s3</artifactId> <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> </dependency>
</dependencies> </dependencies>

View File

@ -2,73 +2,114 @@ package com.ruoyi.common.oss.core;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import com.amazonaws.ClientConfiguration; import com.ruoyi.common.core.constant.Constants;
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.utils.DateUtils; import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.utils.StringUtils; 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.constant.OssConstant;
import com.ruoyi.common.oss.entity.UploadResult; import com.ruoyi.common.oss.entity.UploadResult;
import com.ruoyi.common.oss.enumd.AccessPolicyType; import com.ruoyi.common.oss.enumd.AccessPolicyType;
import com.ruoyi.common.oss.enumd.PolicyType; import com.ruoyi.common.oss.enumd.PolicyType;
import com.ruoyi.common.oss.exception.OssException; import com.ruoyi.common.oss.exception.OssException;
import com.ruoyi.common.oss.properties.OssProperties; 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.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI;
import java.net.URL; import java.net.URL;
import java.util.Date; import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
/** /**
* S3 存储协议 所有兼容S3协议的云厂商均支持 * S3 存储协议 所有兼容S3协议的云厂商均支持
* 阿里云 腾讯云 七牛云 minio * 阿里云 腾讯云 七牛云 minio
* *
* @author Lion Li * @author AprilWind
*/ */
public class OssClient { public class OssClient {
/**
* 服务商
*/
private final String configKey; private final String configKey;
/**
* 配置属性
*/
private final OssProperties properties; 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) { public OssClient(String configKey, OssProperties ossProperties) {
this.configKey = configKey; this.configKey = configKey;
this.properties = ossProperties; this.properties = ossProperties;
try { try {
AwsClientBuilder.EndpointConfiguration endpointConfig = // 创建 AWS 认证信息
new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion()); StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()));
AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey()); //创建AWS基于 CRT S3 客户端
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials); this.client = S3AsyncClient.crtBuilder()
ClientConfiguration clientConfig = new ClientConfiguration(); .credentialsProvider(credentialsProvider)
if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) { .endpointOverride(URI.create(getEndpoint()))
clientConfig.setProtocol(Protocol.HTTPS); .region(of())
} else { .targetThroughputInGbps(20.0)
clientConfig.setProtocol(Protocol.HTTP); .minimumPartSizeInBytes(10 * 1025 * 1024L)
} .checksumValidationEnabled(false)
AmazonS3ClientBuilder build = AmazonS3Client.builder() .build();
.withEndpointConfiguration(endpointConfig)
.withClientConfiguration(clientConfig) //AWS基于 CRT S3 AsyncClient 实例用作 S3 传输管理器的底层客户端
.withCredentials(credentialsProvider) this.transferManager = S3TransferManager.builder().s3Client(this.client).build();
.disableChunkedEncoding();
if (!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)) { // 检查是否连接到 MinIOMinIO 使用 HTTPS 限制使用域名访问需要启用路径样式访问
S3Configuration config = S3Configuration.builder().chunkedEncodingEnabled(false)
// minio 使用https限制使用域名访问 需要此配置 站点填域名 // minio 使用https限制使用域名访问 需要此配置 站点填域名
build.enablePathStyleAccess(); .pathStyleAccessEnabled(!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)).build();
}
this.client = build.build();
// 创建 预签名 URL 的生成器 实例用于生成 S3 预签名 URL
this.presigner = S3Presigner.builder()
.region(of())
.credentialsProvider(credentialsProvider)
.endpointOverride(URI.create(getDomain()))
.serviceConfiguration(config)
.build();
// 创建存储桶
createBucket(); createBucket();
} catch (Exception e) { } catch (Exception e) {
if (e instanceof OssException) { if (e instanceof OssException) {
@ -78,126 +119,158 @@ public class OssClient {
} }
} }
/**
* 同步创建存储桶
* 如果存储桶不存在会进行创建如果存储桶存在不执行任何操作
*
* @throws OssException 当创建存储桶时发生异常时抛出
*/
public void createBucket() { public void createBucket() {
String bucketName = properties.getBucketName();
try { try {
String bucketName = properties.getBucketName(); // 尝试获取存储桶的信息
if (client.doesBucketExistV2(bucketName)) { client.headBucket(
return; 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)) { if (!(inputStream instanceof ByteArrayInputStream)) {
inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream)); inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream));
} }
try { try {
ObjectMetadata metadata = new ObjectMetadata(); // 创建异步请求体length如果为空会报错
metadata.setContentType(contentType); BlockingInputStreamAsyncRequestBody body = AsyncRequestBody.forBlockingInputStream(length);
metadata.setContentLength(inputStream.available());
PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata); // 使用 transferManager 进行上传
// 设置上传对象的 Acl 为公共读 Upload upload = transferManager.upload(
putObjectRequest.setCannedAcl(getAccessPolicy().getAcl()); x -> x.requestBody(body)
client.putObject(putObjectRequest); .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) { } catch (Exception e) {
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]"); 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) { public Path fileDownload(String path) {
path = path.replace(getUrl() + "/", ""); // 构建临时文件
S3Object object = client.getObject(properties.getBucketName(), path); Path tempFilePath = FileUtils.createTempFile().toPath();
return object.getObjectMetadata(); // 使用 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(); * @param path 指定路径
} */
public void delete(String path) {
public String getUrl() { try {
String domain = properties.getDomain(); client.deleteObject(
String endpoint = properties.getEndpoint(); x -> x.bucket(properties.getBucketName())
String header = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://"; .key(removeBaseUrl(path))
// 云服务商直接返回 .build());
if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) { } catch (Exception e) {
if (StringUtils.isNotBlank(domain)) { throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
return header + domain;
}
return header + properties.getBucketName() + "." + endpoint;
} }
// 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 授权时间 * @param second 授权时间
*/ */
public String getPrivateUrl(String objectKey, Integer second) { public String getPrivateUrl(String objectKey, Integer second) {
GeneratePresignedUrlRequest generatePresignedUrlRequest = // 使用 AWS S3 预签名 URL 的生成器 获取对象的预签名 URL
new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey) URL url = presigner.presignGetObject(
.withMethod(HttpMethod.GET) x -> x.signatureDuration(Duration.ofSeconds(second))
.withExpiration(new Date(System.currentTimeMillis() + 1000L * second)); .getObjectRequest(
URL url = client.generatePresignedUrl(generatePresignedUrlRequest); y -> y.bucket(properties.getBucketName())
.key(objectKey)
.build())
.build())
.url();
return url.toString(); 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()); return AccessPolicyType.getByType(properties.getAccessPolicy());
} }
/**
* 生成 AWS S3 存储桶访问策略
*
* @param bucketName 存储桶
* @param policyType 桶策略类型
* @return 符合 AWS S3 存储桶访问策略格式的字符串
*/
private static String getPolicy(String bucketName, PolicyType policyType) { private static String getPolicy(String bucketName, PolicyType policyType) {
StringBuilder builder = new StringBuilder(); String policy = switch (policyType) {
builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n"); case WRITE -> """
builder.append(switch (policyType) { {
case WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n"; "Version": "2012-10-17",
case READ_WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n"; "Statement": []
default -> "\"s3:GetBucketLocation\"\n"; }
}); """;
builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::"); case READ_WRITE -> """
builder.append(bucketName); {
builder.append("\"\n},\n"); "Version": "2012-10-17",
if (policyType == PolicyType.READ) { "Statement": [
builder.append("{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::"); {
builder.append(bucketName); "Effect": "Allow",
builder.append("\"\n},\n"); "Principal": "*",
} "Action": [
builder.append("{\n\"Action\": "); "s3:GetBucketLocation",
builder.append(switch (policyType) { "s3:ListBucket",
case WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n"; "s3:ListBucketMultipartUploads"
case READ_WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n"; ],
default -> "\"s3:GetObject\",\n"; "Resource": "arn:aws:s3:::bucketName"
}); },
builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::"); {
builder.append(bucketName); "Effect": "Allow",
builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n"); "Principal": "*",
return builder.toString(); "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);
} }
} }

View File

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

View File

@ -1,8 +1,9 @@
package com.ruoyi.common.oss.enumd; package com.ruoyi.common.oss.enumd;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; 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
*/ */
PRIVATE("0", CannedAccessControlList.Private, PolicyType.WRITE), PRIVATE("0", BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE, PolicyType.WRITE),
/** /**
* public * public
*/ */
PUBLIC("1", CannedAccessControlList.PublicRead, PolicyType.READ), PUBLIC("1", BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE, PolicyType.READ_WRITE),
/** /**
* custom * custom
*/ */
CUSTOM("2",CannedAccessControlList.PublicRead, PolicyType.READ); CUSTOM("2", BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ, PolicyType.READ);
/** /**
* 权限类型 * 权限类型数据库值
*/ */
private final String type; private final String type;
/**
* 权限类型
*/
private final BucketCannedACL bucketCannedACL;
/** /**
* 文件对象 权限类型 * 文件对象 权限类型
*/ */
private final CannedAccessControlList acl; private final ObjectCannedACL objectCannedACL;
/** /**
* 桶策略类型 * 桶策略类型

View File

@ -145,7 +145,7 @@ public class SysOssServiceImpl extends BaseServiceImpl<SysOssMapper, SysOss> imp
OssClient storage = OssFactory.instance(); OssClient storage = OssFactory.instance();
UploadResult uploadResult; UploadResult uploadResult;
try { try {
uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType()); uploadResult = storage.uploadSuffix(file.getBytes(), suffix);
} catch (IOException e) { } catch (IOException e) {
throw new ServiceException(e.getMessage()); throw new ServiceException(e.getMessage());
} }