init: 导入RuoYi‑Vue‑Plus 6.X完整代码

This commit is contained in:
admin
2026-08-09 17:51:11 +08:00
commit 51cac459b3
914 changed files with 103833 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-oss</artifactId>
<description>
ruoyi-common-oss oss服务
</description>
<dependencies>
<!-- 序列化模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-json</artifactId>
</dependency>
<!-- 缓存服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<!-- AWS SDK for Java 2.x -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<exclusions>
<!-- 东西 30M 特别大的 jar 包 性能跟 Netty 差不多 有需要可以自行替换使用 -->
<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>
<!-- 适用于 Netty 的客户端 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>netty-nio-client</artifactId>
</dependency>
<!-- 客户端的性能增强传输管理器 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3-transfer-manager</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,100 @@
package org.dromara.common.oss.client;
import org.dromara.common.oss.config.OssAsyncExecutorConfig;
import org.dromara.common.oss.config.OssClientConfig;
import org.dromara.common.oss.exception.S3StorageException;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.checksums.RequestChecksumCalculation;
import software.amazon.awssdk.core.checksums.ResponseChecksumValidation;
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
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.presigner.S3Presigner;
import software.amazon.awssdk.transfer.s3.S3TransferManager;
import java.net.URI;
import java.time.Duration;
import java.util.concurrent.Executors;
/**
* 默认S3存储客户端实现类。
*
* @author 秋辞未寒
*/
public class DefaultOssClientImpl extends AbstractOssClientImpl {
/**
* 构造默认 S3 存储客户端。
*
* @param clientId 客户端 ID
* @param config S3 存储客户端配置
*/
public DefaultOssClientImpl(String clientId, OssClientConfig config) {
super(clientId, config);
}
/**
* 初始化默认 S3 客户端、传输管理器和预签名生成器。
*/
@Override
void doInitialize() {
// 校验配置
String accessKey = config.accessKey()
.filter(bucket -> !bucket.isBlank())
.orElseThrow(() -> S3StorageException.form("accessKey is not configured."));
String secretKey = config.secretKey()
.filter(bucket -> !bucket.isBlank())
.orElseThrow(() -> S3StorageException.form("secretKey is not configured."));
String endpointUrl = config.getEndpointUrl();
String domainUrl = config.getDomainUrl();
Region region = config.region().orElse(Region.US_EAST_1);
// MinIO 使用 HTTPS 限制使用域名访问,站点填域名。需要启用路径样式访问
boolean usePathStyleAccess = config.usePathStyleAccess();
// 创建 AWS 认证信息
StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey));
// 创建AWS基于 Netty 的 S3 客户端
this.s3AsyncClient = S3AsyncClient.builder()
.credentialsProvider(credentialsProvider)
.endpointOverride(URI.create(endpointUrl))
.region(region)
.forcePathStyle(usePathStyleAccess)
.serviceConfiguration(S3Configuration.builder().build())
.requestChecksumCalculation(RequestChecksumCalculation.WHEN_REQUIRED)
.responseChecksumValidation(ResponseChecksumValidation.WHEN_REQUIRED)
.httpClient(
NettyNioAsyncHttpClient.builder()
.connectionTimeout(Duration.ofSeconds(60))
.connectionAcquisitionTimeout(Duration.ofSeconds(30))
.maxConcurrency(100)
.maxPendingConnectionAcquires(1000)
.build()
)
.build();
//AWS基于 CRT 的 S3 AsyncClient 实例用作 S3 传输管理器的底层客户端
this.s3TransferManager = S3TransferManager.builder().s3Client(this.s3AsyncClient).build();
// 创建 预签名 URL 的生成器 实例,用于生成 S3 预签名 URL
this.s3Presigner = S3Presigner.builder()
.region(region)
.credentialsProvider(credentialsProvider)
.endpointOverride(URI.create(domainUrl))
.serviceConfiguration(S3Configuration.builder()
.pathStyleAccessEnabled(usePathStyleAccess)
.build())
.build();
// 创建异步调度器对象
OssAsyncExecutorConfig asyncExecutorConfig = config.asyncExecutorConfig();
// 是否使用虚拟线程
if (asyncExecutorConfig.enabledVirtualThread()) {
this.asyncExecutor = Executors.newVirtualThreadPerTaskExecutor();
} else {
this.asyncExecutor = Executors.newScheduledThreadPool(asyncExecutorConfig.corePoolSize());
}
}
}
@@ -0,0 +1,598 @@
package org.dromara.common.oss.client;
import cn.hutool.core.util.IdUtil;
import org.dromara.common.oss.config.OssClientConfig;
import org.dromara.common.oss.io.OutputStreamDownloadSubscriber;
import org.dromara.common.oss.model.GetObjectResult;
import org.dromara.common.oss.model.HandleAsyncResult;
import org.dromara.common.oss.model.Options;
import org.dromara.common.oss.model.PutObjectResult;
import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
import software.amazon.awssdk.transfer.s3.model.CompletedUpload;
import software.amazon.awssdk.transfer.s3.progress.TransferListener;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Collection;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* S3 存储客户端接口。
* <p>
* 本接口同时提供两套对象操作 API:
* 一套通过 {@code bucketXxx(...)} 显式指定存储桶,
* 另一套通过无前缀方法使用默认存储桶。
* </p>
*
* @author 秋辞未寒
*/
public interface OssClient extends AutoCloseable {
/**
* S3 存储客户端ID
* <p>
* 用于标识客户端,初始化后不允许更改
*
* @return S3 存储客户端ID
*/
default String clientId() {
return IdUtil.fastSimpleUUID();
}
/**
* 获取客户端配置copy副本
*/
OssClientConfig config();
/**
* 是否已经初始化
*/
boolean isInitialized();
/**
* 初始化客户端
*/
void initialize();
/**
* 校验客户端配置
*
* <p>注意:该方法不会修改任何既有的配置和状态,你看可以理解为这仅仅是一个配置展示的方法,以供调用者根据当前的配置,自行决定是否需要重新构建客户端。</p>
*
* @param verifyConfigAction 校验配置动作函数
* @return 是否一致
*/
boolean verifyConfig(Function<OssClientConfig, Boolean> verifyConfigAction);
/**
* 校验客户端配置与传入的待校验配置是否一致
*
* <p>注意:该方法不会修改任何既有的配置和状态,你看可以理解为这仅仅是一个配置展示的方法,以供调用者根据当前的配置,自行决定是否需要重新构建客户端。</p>
*
* @param verifyConfig 待校验的配置
* @return 是否一致
*/
boolean verifyConfig(OssClientConfig verifyConfig);
/**
* 执行自定义上传请求。
*
* @param body 上传请求体
* @param putObjectRequestBuilderConsumer PutObject 请求构建回调
* @param transferListeners 传输监听器集合
* @param handleAsyncAction 上传完成后的结果处理函数
* @param <T> 返回值类型
* @return 处理后的结果
*/
<T> T doCustomUpload(AsyncRequestBody body, Consumer<PutObjectRequest.Builder> putObjectRequestBuilderConsumer, Collection<TransferListener> transferListeners, BiFunction<CompletedUpload, Throwable, T> handleAsyncAction);
/**
* 执行自定义上传请求。
*
* @param body 上传请求体
* @param putObjectRequestBuilderConsumer PutObject 请求构建回调
* @param handleAsyncAction 上传完成后的结果处理函数
* @param <T> 返回值类型
* @return 处理后的结果
*/
<T> T doCustomUpload(AsyncRequestBody body, Consumer<PutObjectRequest.Builder> putObjectRequestBuilderConsumer, BiFunction<CompletedUpload, Throwable, T> handleAsyncAction);
/**
* 执行自定义上传请求,并返回统一异步处理结果。
*
* @param body 上传请求体
* @param putObjectRequestBuilderConsumer PutObject 请求构建回调
* @param transferListeners 传输监听器集合
* @return 上传结果
*/
HandleAsyncResult<PutObjectResponse> doCustomUpload(AsyncRequestBody body, Consumer<PutObjectRequest.Builder> putObjectRequestBuilderConsumer, Collection<TransferListener> transferListeners);
/**
* 执行自定义上传请求,并返回统一异步处理结果。
*
* @param body 上传请求体
* @param putObjectRequestBuilderConsumer PutObject 请求构建回调
* @return 上传结果
*/
HandleAsyncResult<PutObjectResponse> doCustomUpload(AsyncRequestBody body, Consumer<PutObjectRequest.Builder> putObjectRequestBuilderConsumer);
/**
* 将本地路径对应的文件上传到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param path 文件路径
* @param options 可选项
* @return 上传结果
*/
PutObjectResult bucketUpload(String bucket, String key, Path path, Options options);
/**
* 将本地路径对应的文件上传到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param path 文件路径
* @return 上传结果
*/
PutObjectResult bucketUpload(String bucket, String key, Path path);
/**
* 将文件上传到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param file 文件对象
* @param options 可选项
* @return 上传结果
*/
PutObjectResult bucketUpload(String bucket, String key, File file, Options options);
/**
* 将文件上传到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param file 文件对象
* @return 上传结果
*/
PutObjectResult bucketUpload(String bucket, String key, File file);
/**
* 将随机访问文件上传到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param file 随机访问文件
* @param options 可选项
* @return 上传结果
*/
PutObjectResult bucketUpload(String bucket, String key, RandomAccessFile file, Options options);
/**
* 将随机访问文件上传到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param file 随机访问文件
* @return 上传结果
*/
PutObjectResult bucketUpload(String bucket, String key, RandomAccessFile file);
/**
* 将可读通道中的数据上传到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param channel 数据通道
* @param contentLength 内容长度
* @param options 可选项
* @return 上传结果
*/
PutObjectResult bucketUpload(String bucket, String key, ReadableByteChannel channel, long contentLength, Options options);
/**
* 将可读通道中的数据上传到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param channel 数据通道
* @param contentLength 内容长度
* @return 上传结果
*/
PutObjectResult bucketUpload(String bucket, String key, ReadableByteChannel channel, long contentLength);
/**
* 将可读通道中的数据上传到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param in 输入流
* @param contentLength 内容长度
* @param options 可选项
* @return 上传结果
*/
PutObjectResult bucketUpload(String bucket, String key, InputStream in, long contentLength, Options options);
/**
* 将输入流中的数据上传到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param in 输入流
* @param contentLength 内容长度
* @return 上传结果
*/
PutObjectResult bucketUpload(String bucket, String key, InputStream in, long contentLength);
/**
* 将字节数组上传到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param data 字节数组
* @param options 可选项
* @return 上传结果
*/
PutObjectResult bucketUpload(String bucket, String key, byte[] data, Options options);
/**
* 将字节数组上传到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param data 字节数组
* @return 上传结果
*/
PutObjectResult bucketUpload(String bucket, String key, byte[] data);
/**
* 执行自定义下载请求。
*
* @param getObjectRequestBuilderConsumer GetObject 请求构建回调
* @param responseTransformer 下载响应转换器
* @param transferListeners 传输监听器集合
* @param <T> 下载结果类型
* @return 下载结果
*/
<T> T doCustomDownload(Consumer<GetObjectRequest.Builder> getObjectRequestBuilderConsumer, AsyncResponseTransformer<GetObjectResponse, T> responseTransformer, Collection<TransferListener> transferListeners);
/**
* 将指定存储桶中的对象下载到订阅器。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param downloadSubscriber 下载订阅器
* @return 下载结果
*/
GetObjectResult bucketDownload(String bucket, String key, OutputStreamDownloadSubscriber downloadSubscriber);
/**
* 将指定存储桶中的对象下载到转换器中,由使用者决定返回值。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param downloadTransformer 下载转换器
* @return 下载结果
*/
<T> T bucketDownload(String bucket, String key, BiFunction<GetObjectResult, InputStream, T> downloadTransformer);
/**
* 将指定存储桶中的对象下载到本地路径。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param path 本地路径
* @return 下载结果
*/
GetObjectResult bucketDownload(String bucket, String key, Path path);
/**
* 将指定存储桶中的对象下载到文件。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param file 本地文件
* @return 下载结果
*/
GetObjectResult bucketDownload(String bucket, String key, File file);
/**
* 将指定存储桶中的对象下载到随机访问文件。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param file 随机访问文件
* @return 下载结果
*/
GetObjectResult bucketDownload(String bucket, String key, RandomAccessFile file);
/**
* 将指定存储桶中的对象下载到可写通道。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param channel 可写通道
* @return 下载结果
*/
GetObjectResult bucketDownload(String bucket, String key, WritableByteChannel channel);
/**
* 将指定存储桶中的对象下载到输出流。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param out 输出流
* @return 下载结果
*/
GetObjectResult bucketDownload(String bucket, String key, OutputStream out);
/**
* 删除指定存储桶中的对象。
*
* @param bucket 存储桶名称
* @param key 对象键
* @return 是否删除成功
*/
boolean bucketDelete(String bucket, String key);
/**
* 生成指定存储桶中文件下载的预签名 URL。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param expiredTime URL 过期时间
* @return 预签名下载 URL
*/
String bucketPresignGetUrl(String bucket, String key, Duration expiredTime);
/**
* 生成指定存储桶中文件上传的预签名 URL。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param expiredTime URL 过期时间
* @param metadata 对象元数据
* @return 预签名上传 URL
*/
String bucketPresignPutUrl(String bucket, String key, Duration expiredTime, Map<String, String> metadata);
/**
* 将本地路径对应的文件上传到默认存储桶。
*
* @param key 对象键
* @param path 文件路径
* @param options 可选项
* @return 上传结果
*/
PutObjectResult upload(String key, Path path, Options options);
/**
* 将本地路径对应的文件上传到默认存储桶。
*
* @param key 对象键
* @param path 文件路径
* @return 上传结果
*/
PutObjectResult upload(String key, Path path);
/**
* 将文件上传到默认存储桶。
*
* @param key 对象键
* @param file 文件对象
* @param options 可选项
* @return 上传结果
*/
PutObjectResult upload(String key, File file, Options options);
/**
* 将文件上传到默认存储桶。
*
* @param key 对象键
* @param file 文件对象
* @return 上传结果
*/
PutObjectResult upload(String key, File file);
/**
* 将随机访问文件上传到默认存储桶。
*
* @param key 对象键
* @param file 随机访问文件
* @param options 可选项
* @return 上传结果
*/
PutObjectResult upload(String key, RandomAccessFile file, Options options);
/**
* 将随机访问文件上传到默认存储桶。
*
* @param key 对象键
* @param file 随机访问文件
* @return 上传结果
*/
PutObjectResult upload(String key, RandomAccessFile file);
/**
* 将可读通道中的数据上传到默认存储桶。
*
* @param key 对象键
* @param channel 数据通道
* @param contentLength 内容长度
* @param options 可选项
* @return 上传结果
*/
PutObjectResult upload(String key, ReadableByteChannel channel, long contentLength, Options options);
/**
* 将可读通道中的数据上传到默认存储桶。
*
* @param key 对象键
* @param channel 数据通道
* @param contentLength 内容长度
* @return 上传结果
*/
PutObjectResult upload(String key, ReadableByteChannel channel, long contentLength);
/**
* 将输入流中的数据上传到默认存储桶。
*
* @param key 对象键
* @param in 输入流
* @param contentLength 内容长度
* @param options 可选项
* @return 上传结果
*/
PutObjectResult upload(String key, InputStream in, long contentLength, Options options);
/**
* 将输入流中的数据上传到默认存储桶。
*
* @param key 对象键
* @param in 输入流
* @param contentLength 内容长度
* @return 上传结果
*/
PutObjectResult upload(String key, InputStream in, long contentLength);
/**
* 将字节数组上传到默认存储桶。
*
* @param key 对象键
* @param data 字节数组
* @param options 可选项
* @return 上传结果
*/
PutObjectResult upload(String key, byte[] data, Options options);
/**
* 将字节数组上传到默认存储桶。
*
* @param key 对象键
* @param data 字节数组
* @return 上传结果
*/
PutObjectResult upload(String key, byte[] data);
/**
* 将默认存储桶中的对象下载到订阅器。
*
* @param key 对象键
* @param downloadSubscriber 下载订阅器
* @return 下载结果
*/
GetObjectResult download(String key, OutputStreamDownloadSubscriber downloadSubscriber);
/**
* 将指定存储桶中的对象下载到转换器中,由使用者决定返回值。
*
* @param key 对象键
* @param downloadTransformer 下载转换器
* @return 下载结果
*/
<T> T download(String key, BiFunction<GetObjectResult, InputStream, T> downloadTransformer);
/**
* 将默认存储桶中的对象下载到本地路径。
*
* @param key 对象键
* @param path 本地路径
* @return 下载结果
*/
GetObjectResult download(String key, Path path);
/**
* 将默认存储桶中的对象下载到文件。
*
* @param key 对象键
* @param file 本地文件
* @return 下载结果
*/
GetObjectResult download(String key, File file);
/**
* 将默认存储桶中的对象下载到随机访问文件。
*
* @param key 对象键
* @param file 随机访问文件
* @return 下载结果
*/
GetObjectResult download(String key, RandomAccessFile file);
/**
* 将默认存储桶中的对象下载到可写通道。
*
* @param key 对象键
* @param channel 可写通道
* @return 下载结果
*/
GetObjectResult download(String key, WritableByteChannel channel);
/**
* 将默认存储桶中的对象下载到输出流。
*
* @param key 对象键
* @param out 输出流
* @return 下载结果
*/
GetObjectResult download(String key, OutputStream out);
/**
* 删除默认存储桶中的对象。
*
* @param key 对象键
* @return 是否删除成功
*/
boolean delete(String key);
/**
* 生成默认存储桶中文件下载的预签名 URL。
*
* @param key 对象键
* @param expiredTime URL 过期时间
* @return 预签名下载 URL
*/
String presignGetUrl(String key, Duration expiredTime);
/**
* 生成默认存储桶中文件上传的预签名 URL。
*
* @param key 对象键
* @param expiredTime URL 过期时间
* @param metadata 对象元数据
* @return 预签名上传 URL
*/
String presignPutUrl(String key, Duration expiredTime, Map<String, String> metadata);
/**
* 根据客户端配置生成默认对象Key。
*
* @param fileName 原始文件名
* @return 对象Key
*/
String buildPathKey(String fileName);
/**
* 根据业务前缀和客户端默认前缀生成对象Key。
*
* @param businessPrefix 业务前缀
* @param fileName 原始文件名
* @return 对象Key
*/
String buildPathKey(String businessPrefix, String fileName);
}
@@ -0,0 +1,68 @@
package org.dromara.common.oss.config;
import lombok.Builder;
import org.dromara.common.oss.enums.AccessPolicy;
import org.jspecify.annotations.NonNull;
import java.io.Serial;
import java.io.Serializable;
import java.util.Optional;
/**
* S3 ACL访问策略配置
*
* @param enabled 是否启用ACL
* @param accessPolicy 访问策略
* @author 秋辞未寒
*/
@Builder
public record AccessControlPolicyConfig(
boolean enabled
, AccessPolicy accessPolicy
) implements Config<AccessControlPolicyConfig, AccessControlPolicyConfig.AccessControlPolicyConfigBuilder>, Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 默认访问策略配置
*/
public static final AccessControlPolicyConfig DEFAULT = AccessControlPolicyConfig.builder()
.enabled(false)
.accessPolicy(AccessPolicy.PUBLIC_READ_WRITE)
.build();
/**
* 获取访问策略,未配置时返回默认策略。
*
* @return 访问策略
*/
@Override
public @NonNull AccessPolicy accessPolicy() {
return Optional.ofNullable(accessPolicy)
.orElse(AccessPolicy.PUBLIC_READ_WRITE);
}
/**
* 复制访问策略配置。
*
* @return 配置副本
*/
@Override
public AccessControlPolicyConfig copy() {
return toBuilder().build();
}
/**
* 转换为构建器。
*
* @return 配置构建器
*/
@Override
public AccessControlPolicyConfigBuilder toBuilder() {
return builder()
.enabled(enabled)
.accessPolicy(accessPolicy);
}
}
@@ -0,0 +1,26 @@
package org.dromara.common.oss.config;
/**
* 配置对象接口
*
* @param <T> 配置类型
* @param <B> 配置构建器类型
* @author 秋辞未寒
*/
public interface Config<T, B> {
/**
* 配置对象拷贝
*
* @return 拷贝后的新配置对象
*/
T copy();
/**
* 转为构建器对象
*
* @return 构建器对象
*/
B toBuilder();
}
@@ -0,0 +1,76 @@
package org.dromara.common.oss.config;
import lombok.Builder;
import java.io.Serial;
import java.io.Serializable;
/**
* S3 异步执行器配置
*
* @param enabledVirtualThread 是否启用虚拟线程
* @param corePoolSize 核心线程数
* <p>
* 默认为当前CPU核心数,该配置项在配置了虚拟线程后会失效
* @author 秋辞未寒
*/
@Builder
public record OssAsyncExecutorConfig(
boolean enabledVirtualThread
, int corePoolSize
) implements Config<OssAsyncExecutorConfig, OssAsyncExecutorConfig.OssAsyncExecutorConfigBuilder>, Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 默认核心线程数 = 当前处理器核心数
*/
public static final int DEFAULT_CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors();
/**
* 默认异步执行器配置
*/
public static final OssAsyncExecutorConfig DEFAULT = OssAsyncExecutorConfig.builder()
.enabledVirtualThread(true)
.corePoolSize(DEFAULT_CORE_POOL_SIZE)
.build();
/**
* 是否启用虚拟线程
*/
@Override
public boolean enabledVirtualThread() {
return enabledVirtualThread;
}
/**
* 核心线程数
*/
@Override
public int corePoolSize() {
return corePoolSize;
}
/**
* 复制异步执行器配置。
*
* @return 配置副本
*/
@Override
public OssAsyncExecutorConfig copy() {
return toBuilder().build();
}
/**
* 转换为构建器。
*
* @return 配置构建器
*/
@Override
public OssAsyncExecutorConfigBuilder toBuilder() {
return builder()
.enabledVirtualThread(enabledVirtualThread)
.corePoolSize(corePoolSize);
}
}
@@ -0,0 +1,334 @@
package org.dromara.common.oss.config;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.oss.constant.OssConstant;
import org.dromara.common.oss.enums.AccessPolicy;
import org.dromara.common.oss.exception.S3StorageException;
import org.dromara.common.oss.properties.OssProperties;
import org.dromara.common.oss.util.BucketUrlUtil;
import org.jspecify.annotations.NonNull;
import software.amazon.awssdk.regions.Region;
import java.io.Serial;
import java.io.Serializable;
import java.util.Optional;
/**
* S3存储客户端配置
*
* @author 秋辞未寒
*/
@RequiredArgsConstructor
@Builder
@EqualsAndHashCode
public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.OssClientConfigBuilder>, Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 访问端点
*/
private final String endpoint;
/**
* 自定义域名
*/
private final String domain;
/**
* 是否使用HTTPS协议
*/
private final boolean useHttps;
/**
* 是否使用路径样式访问(使用域名需要启用路径样式访问)
*/
private final boolean usePathStyleAccess;
/**
* ACCESS_KEY
*/
private final String accessKey;
/**
* SECRET_KEY
*/
private final String secretKey;
/**
* 存储桶
*/
private final String bucket;
/**
* 存储区域
*/
private final Region region;
/**
* 前缀
*/
private final String prefix;
/**
* ACL访问策略配置
*/
private final AccessControlPolicyConfig accessControlPolicyConfig;
/**
* 异步调度池配置
*/
private final OssAsyncExecutorConfig asyncExecutorConfig;
/**
* 访问端点
*/
public Optional<String> endpoint() {
return Optional.ofNullable(endpoint);
}
/**
* 自定义域名
*/
public Optional<String> domain() {
return Optional.ofNullable(domain);
}
/**
* 是否使用HTTPS协议
*/
public boolean useHttps() {
return useHttps;
}
/**
* 是否使用路径样式访问(使用域名需要启用路径样式访问)
*/
public boolean usePathStyleAccess() {
return usePathStyleAccess;
}
/**
* ACCESS_KEY
*/
public Optional<String> accessKey() {
return Optional.ofNullable(accessKey);
}
/**
* SECRET_KEY
*/
public Optional<String> secretKey() {
return Optional.ofNullable(secretKey);
}
/**
* 存储桶
*/
public Optional<String> bucket() {
return Optional.ofNullable(bucket);
}
/**
* 存储区域
*/
public Optional<Region> region() {
return Optional.ofNullable(region);
}
/**
* 前缀
*/
public Optional<String> prefix() {
return Optional.ofNullable(prefix);
}
/**
* 根据 OSS 配置属性构建客户端配置。
*
* @param properties OSS 配置属性
* @return 客户端配置
*/
public static OssClientConfig formProperties(OssProperties properties) {
return formPropertiesBuilder(properties).build();
}
/**
* 根据 OSS 配置属性构建客户端配置构造器。
*
* @param properties OSS 配置属性
* @return 客户端配置构造器
*/
public static OssClientConfigBuilder formPropertiesBuilder(OssProperties properties) {
return builder()
.endpoint(properties.getEndpoint())
.domain(properties.getDomainUrl())
.accessKey(properties.getAccessKey())
.secretKey(properties.getSecretKey())
.bucket(properties.getBucketName())
.region(parseRegion(properties.getRegion()))
.prefix(properties.getPrefix())
.useHttps(SystemConstants.YES.equals(properties.getIsHttps()))
.usePathStyleAccess(resolvePathStyleAccess(properties))
.accessControlPolicyConfig(resolveAccessControlPolicy(properties.getAccessPolicy()));
}
/**
* 获取访问站点URL地址
*
* @return 访问站点URL地址
*/
public String getEndpointUrl() {
return BucketUrlUtil.rebuildUrlHeader(useHttps, getEndpoint());
}
/**
* 获取域名URL地址
*
* @return 域名URL地址
*/
public String getDomainUrl() {
return domain()
// 如果已经配置了自定义域名,则优先使用域名
.filter(StringUtils::isNotBlank)
.map(domain -> BucketUrlUtil.rebuildUrlHeader(useHttps, domain.trim()))
// 否则使用站点
.orElseGet(this::getEndpointUrl);
}
/**
* 获取桶URL地址
*
* @return 桶URL地址
*/
public String getBucketUrl() {
// 如果未配置桶,则抛异常
String bucket = bucket()
.filter(s -> !s.isBlank())
.orElseThrow(() -> S3StorageException.form("bucket is not configured."));
return getBucketUrl(bucket);
}
/**
* 获取桶URL地址
*
* @return 桶URL地址
*/
public String getBucketUrl(String bucket) {
String url = getAccessBaseUrl();
// 根据是否使用路径风格配置项决定存储桶的URL风格
return usePathStyleAccess ? BucketUrlUtil.getPathStyleBucketUrl(useHttps, url, bucket) : BucketUrlUtil.getSiteStyleBucketUrl(useHttps, url, bucket);
}
/**
* 解析 S3 Region。
*
* @param regionString Region 字符串
* @return Region 对象
*/
private static Region parseRegion(String regionString) {
if (StringUtils.isBlank(regionString)) {
return Region.US_EAST_1;
}
return Region.of(regionString);
}
/**
* 兼容旧配置推断是否使用路径风格访问。
*
* @param properties OSS 配置属性
* @return 是否使用路径风格访问
*/
private static boolean resolvePathStyleAccess(OssProperties properties) {
// 旧配置没有显式路径风格字段,只能继续按内置云厂商 endpoint 做兼容推断。
return !StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE);
}
/**
* 解析 ACL 访问策略配置。
*
* @param accessPolicyString 访问策略字符串
* @return ACL 访问策略配置
*/
private static AccessControlPolicyConfig resolveAccessControlPolicy(String accessPolicyString) {
// 绝大多数云厂商不允许操作 ACL,默认禁用;当前业务只用访问策略判断是否生成预签名 URL。
if (StringUtils.isBlank(accessPolicyString)) {
return AccessControlPolicyConfig.DEFAULT;
}
return AccessControlPolicyConfig.builder()
.enabled(true)
.accessPolicy(AccessPolicy.formType(accessPolicyString))
.build();
}
/**
* 获取用于访问对象的基础 URL。
*
* @return 基础 URL
*/
private String getAccessBaseUrl() {
return domain()
.filter(StringUtils::isNotBlank)
.map(String::trim)
.orElseGet(this::getEndpoint);
}
/**
* 获取 endpoint 配置。
*
* @return endpoint
*/
private String getEndpoint() {
return endpoint()
.filter(s -> !s.isBlank())
.orElseThrow(() -> S3StorageException.form("endpoint is not configured."));
}
/**
* ACL访问策略配置
*/
public @NonNull AccessControlPolicyConfig accessControlPolicyConfig() {
return Optional.ofNullable(accessControlPolicyConfig)
.orElse(AccessControlPolicyConfig.DEFAULT);
}
/**
* ACL访问策略配置
*/
public @NonNull OssAsyncExecutorConfig asyncExecutorConfig() {
return Optional.ofNullable(asyncExecutorConfig)
.orElse(OssAsyncExecutorConfig.DEFAULT);
}
/**
* 复制S3存储客户端配置对象
*/
@Override
public OssClientConfig copy() {
return toBuilder().build();
}
/**
* 转为S3存储客户端配置构建器对象
*/
@Override
public OssClientConfigBuilder toBuilder() {
return builder()
.endpoint(endpoint)
.domain(domain)
.useHttps(useHttps)
.usePathStyleAccess(usePathStyleAccess)
.accessKey(accessKey)
.secretKey(secretKey)
.bucket(bucket)
.region(region)
.prefix(prefix)
.accessControlPolicyConfig(accessControlPolicyConfig().copy())
.asyncExecutorConfig(asyncExecutorConfig().copy());
}
}
@@ -0,0 +1,40 @@
package org.dromara.common.oss.constant;
import org.dromara.common.core.constant.GlobalConstants;
import java.util.Arrays;
import java.util.List;
/**
* 对象存储常量
*
* @author Lion Li
*/
public interface OssConstant {
/**
* 默认配置KEY
*/
String DEFAULT_CONFIG_KEY = GlobalConstants.GLOBAL_REDIS_KEY + "sys_oss:default_config";
/**
* 预览列表资源开关Key
*/
String PEREVIEW_LIST_RESOURCE_KEY = "sys.oss.previewListResource";
/**
* 系统数据ids
*/
List<Long> SYSTEM_DATA_IDS = Arrays.asList(
1761900000000000001L,
1761900000000000002L,
1761900000000000003L,
1761900000000000004L
);
/**
* 云服务商
*/
String[] CLOUD_SERVICE = new String[]{"aliyun", "qcloud", "qiniu", "obs"};
}
@@ -0,0 +1,62 @@
package org.dromara.common.oss.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.dromara.common.oss.exception.S3StorageException;
import software.amazon.awssdk.services.s3.model.BucketCannedACL;
import software.amazon.awssdk.services.s3.model.ObjectCannedACL;
import java.util.Arrays;
/**
* 访问策略
*
* @author 秋辞未寒
*/
@Getter
@AllArgsConstructor
public enum AccessPolicy {
/**
* 私有
*/
PRIVATE(0, BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE),
/**
* 公有读写
*/
PUBLIC_READ_WRITE(1, BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE),
/**
* 公有只读
*/
PUBLIC_READ(2, BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ);
/**
* 访问策略类型
*/
private final Integer type;
/**
* 桶权限
*/
private final BucketCannedACL bucketCannedACL;
/**
* 文件对象权限
*/
private final ObjectCannedACL objectCannedACL;
/**
* 根据策略类型查找访问策略。
*
* @param type 策略类型
* @return 访问策略
*/
public static AccessPolicy formType(String type) {
return Arrays.stream(values())
.filter(policy -> policy.getType().toString().equals(type))
.findFirst()
.orElseThrow(() -> S3StorageException.form("'type' not found By " + type));
}
}
@@ -0,0 +1,99 @@
package org.dromara.common.oss.exception;
import java.io.Serial;
/**
* S3对象存储异常
*
* @author 秋辞未寒
*/
public class S3StorageException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 使用异常消息构造 S3 对象存储异常。
*
* @param message 异常消息
*/
public S3StorageException(String message) {
super(message);
}
/**
* 使用异常消息和原因构造 S3 对象存储异常。
*
* @param message 异常消息
* @param cause 异常原因
*/
public S3StorageException(String message, Throwable cause) {
super(message, cause);
}
/**
* 使用异常原因构造 S3 对象存储异常。
*
* @param cause 异常原因
*/
public S3StorageException(Throwable cause) {
super(cause);
}
/**
* 使用完整异常参数构造 S3 对象存储异常。
*
* @param message 异常消息
* @param cause 异常原因
* @param enableSuppression 是否启用抑制
* @param writableStackTrace 是否写入堆栈
*/
public S3StorageException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
/**
* 创建 S3 对象存储异常。
*
* @param message 异常消息
* @return S3 对象存储异常
*/
public static S3StorageException form(String message) {
return new S3StorageException(message);
}
/**
* 创建 S3 对象存储异常。
*
* @param message 异常消息
* @param cause 异常原因
* @return S3 对象存储异常
*/
public static S3StorageException form(String message, Throwable cause) {
return new S3StorageException(message, cause);
}
/**
* 创建 S3 对象存储异常。
*
* @param cause 异常原因
* @return S3 对象存储异常
*/
public static S3StorageException form(Throwable cause) {
return new S3StorageException(cause);
}
/**
* 创建 S3 对象存储异常。
*
* @param message 异常消息
* @param cause 异常原因
* @param enableSuppression 是否启用抑制
* @param writableStackTrace 是否写入堆栈
* @return S3 对象存储异常
*/
public static S3StorageException form(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
return new S3StorageException(message, cause, enableSuppression, writableStackTrace);
}
}
@@ -0,0 +1,119 @@
package org.dromara.common.oss.factory;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.CacheNames;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.oss.client.DefaultOssClientImpl;
import org.dromara.common.oss.client.OssClient;
import org.dromara.common.oss.config.OssClientConfig;
import org.dromara.common.oss.constant.OssConstant;
import org.dromara.common.oss.exception.S3StorageException;
import org.dromara.common.oss.properties.OssProperties;
import org.dromara.common.redis.utils.CacheUtils;
import org.dromara.common.redis.utils.RedisUtils;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
/**
* S3存储客户端工厂
*
* @author 秋辞未寒
*/
@Slf4j
public class OssFactory {
private static final Map<String, OssClient> CLIENT_CACHE = new ConcurrentHashMap<>();
private static final Map<String, ReentrantLock> CLIENT_LOCKS = new ConcurrentHashMap<>();
/**
* 获取默认实例
*/
public static OssClient instance() {
// 获取redis 默认类型
String configKey = RedisUtils.getCacheObject(OssConstant.DEFAULT_CONFIG_KEY);
if (StringUtils.isEmpty(configKey)) {
throw S3StorageException.form("文件存储服务类型无法找到!");
}
return instance(configKey);
}
/**
* 根据类型获取实例
*/
public static OssClient instance(String configKey) {
if (StringUtils.isBlank(configKey)) {
throw S3StorageException.form("文件存储服务类型无法找到!");
}
String json = CacheUtils.get(CacheNames.SYS_OSS_CONFIG, configKey);
if (json == null) {
throw S3StorageException.form("系统异常, '" + configKey + "'配置信息不存在!");
}
OssProperties properties = JsonUtils.parseObject(json, OssProperties.class);
OssClientConfig config = OssClientConfig.formProperties(properties);
ReentrantLock lock = getClientLock(configKey);
lock.lock();
try {
OssClient client = CLIENT_CACHE.get(configKey);
if (client != null) {
if (client.verifyConfig(config)) {
return client;
}
closeClient(configKey, client);
}
OssClient newClient = new DefaultOssClientImpl(configKey, config);
CLIENT_CACHE.put(configKey, newClient);
return newClient;
} finally {
lock.unlock();
}
}
/**
* 移除实例
*/
public static boolean remove(String configKey) {
if (StringUtils.isBlank(configKey)) {
return false;
}
ReentrantLock lock = getClientLock(configKey);
lock.lock();
try {
OssClient client = CLIENT_CACHE.remove(configKey);
if (client == null) {
return false;
}
closeClient(configKey, client);
return true;
} finally {
lock.unlock();
}
}
/**
* 获取指定配置键对应的客户端锁。
*
* @param configKey 配置键
* @return 客户端锁
*/
private static ReentrantLock getClientLock(String configKey) {
return CLIENT_LOCKS.computeIfAbsent(configKey, key -> new ReentrantLock());
}
/**
* 关闭 OSS 客户端。
*
* @param configKey 配置键
* @param client OSS 客户端
*/
private static void closeClient(String configKey, OssClient client) {
try {
client.close();
} catch (Exception e) {
log.warn("S3存储客户端 [{}] 关闭异常,错误信息: {}", configKey, e.getMessage(), e);
}
}
}
@@ -0,0 +1,121 @@
package org.dromara.common.oss.io;
import org.dromara.common.oss.exception.S3StorageException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.function.Consumer;
/**
* 输出流下载订阅器
*
* @author 秋辞未寒
*/
public class OutputStreamDownloadSubscriber implements Consumer<ByteBuffer>, AutoCloseable {
private final WritableByteChannel channel;
private final boolean allowAutoClose;
/**
* 创建输出流下载订阅器。
*
* @param channel 可写通道
* @param allowAutoClose 是否允许自动关闭通道
*/
private OutputStreamDownloadSubscriber(WritableByteChannel channel, boolean allowAutoClose) {
this.channel = channel;
this.allowAutoClose = allowAutoClose;
}
/**
* 创建输出流下载订阅器。
*
* @param out 输出流
* @param allowAutoClose 是否允许自动关闭流
*/
private OutputStreamDownloadSubscriber(OutputStream out, boolean allowAutoClose) {
this.allowAutoClose = allowAutoClose;
// 创建可写入的字节通道
if (out instanceof FileOutputStream outputStream) {
// 如果是文件输入流,直接获取文件输出流的 Channel
channel = outputStream.getChannel();
} else {
channel = Channels.newChannel(out);
}
}
/**
* 写入下载到的字节缓冲区。
*
* @param byteBuffer 字节缓冲区
*/
@Override
public void accept(ByteBuffer byteBuffer) {
try {
while (byteBuffer.hasRemaining()) {
channel.write(byteBuffer);
}
} catch (Exception e) {
throw S3StorageException.form(e);
}
}
/**
* 按配置关闭底层通道。
*
* @throws Exception 关闭通道异常
*/
@Override
public void close() throws Exception {
if (channel.isOpen() && allowAutoClose) {
channel.close();
}
}
/**
* 创建一个输出流下载订阅器
*
* @param out 输出流
* @return 输出流下载订阅器
*/
public static OutputStreamDownloadSubscriber create(OutputStream out) {
return create(out, false);
}
/**
* 创建一个输出流下载订阅器
*
* @param out 输出流
* @param allowAutoClose 是否允许自动关闭流
* @return 输出流下载订阅器
*/
public static OutputStreamDownloadSubscriber create(OutputStream out, boolean allowAutoClose) {
return new OutputStreamDownloadSubscriber(out, allowAutoClose);
}
/**
* 创建一个输出流下载订阅器
*
* @param channel 可写字节通道
* @return 输出流下载订阅器
*/
public static OutputStreamDownloadSubscriber create(WritableByteChannel channel) {
return create(channel, false);
}
/**
* 创建一个输出流下载订阅器
*
* @param channel 可写字节通道
* @param allowAutoClose 是否允许自动关闭流
* @return 输出流下载订阅器
*/
public static OutputStreamDownloadSubscriber create(WritableByteChannel channel, boolean allowAutoClose) {
return new OutputStreamDownloadSubscriber(channel, allowAutoClose);
}
}
@@ -0,0 +1,55 @@
package org.dromara.common.oss.model;
import java.time.LocalDateTime;
import java.util.Map;
/**
* Get文件对象结果
*
* @param key
* @param eTag
* @param lastModified
* @param size
* @param contentType
* @param contentDisposition
* @param contentRange
* @param contentEncoding
* @param contentLanguage
* @param metadata
* @author 秋辞未寒
*/
public record GetObjectResult(
String key,
String eTag,
LocalDateTime lastModified,
long size,
String contentType,
String contentDisposition,
String contentRange,
String contentEncoding,
String contentLanguage,
Map<String, String> metadata
) {
/**
* 构建文件对象获取结果。
*
* @param key 对象 Key
* @param eTag 对象 ETag
* @param lastModified 最后修改时间
* @param size 对象大小
* @param contentType 内容类型
* @param contentDisposition 内容处置方式
* @param contentRange 内容范围
* @param contentEncoding 内容编码
* @param contentLanguage 内容语言
* @param metadata 自定义元数据
* @return 文件对象获取结果
*/
public static GetObjectResult form(String key, String eTag, LocalDateTime lastModified, long size
, String contentType, String contentDisposition, String contentRange, String contentEncoding, String contentLanguage
, Map<String, String> metadata) {
return new GetObjectResult(key, eTag, lastModified, size, contentType, contentDisposition, contentRange, contentEncoding, contentLanguage, metadata);
}
}
@@ -0,0 +1,87 @@
package org.dromara.common.oss.model;
import java.util.Optional;
/**
* 处理异步结果
*
* @param result 结果
* @param error 异常错误
* @param <T> 结果类型
* @author 秋辞未寒
*/
public record HandleAsyncResult<T>(
T result,
Throwable error
) {
/**
* 获取异步处理结果。
*
* @return 异步处理结果
*/
public Optional<T> getResult() {
return Optional.ofNullable(result);
}
/**
* 获取异步处理异常。
*
* @return 异步处理异常
*/
public Optional<Throwable> getError() {
return Optional.ofNullable(error);
}
/**
* 是否处理成功。
*
* @return true 成功 false 失败
*/
public boolean isSuccess() {
return getError().isEmpty();
}
/**
* 是否处理失败。
*
* @return true 失败 false 成功
*/
public boolean isFailure() {
return getError().isPresent();
}
/**
* 构建异步处理结果。
*
* @param result 结果
* @param error 异常
* @param <T> 结果类型
* @return 异步处理结果
*/
public static <T> HandleAsyncResult<T> of(T result, Throwable error) {
return new HandleAsyncResult<>(result, error);
}
/**
* 构建成功的异步处理结果。
*
* @param result 结果
* @param <T> 结果类型
* @return 异步处理结果
*/
public static <T> HandleAsyncResult<T> success(T result) {
return new HandleAsyncResult<>(result, null);
}
/**
* 构建失败的异步处理结果。
*
* @param error 异常
* @param <T> 结果类型
* @return 异步处理结果
*/
public static <T> HandleAsyncResult<T> failure(Throwable error) {
return new HandleAsyncResult<>(null, error);
}
}
@@ -0,0 +1,86 @@
package org.dromara.common.oss.model;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import software.amazon.awssdk.transfer.s3.progress.TransferListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* 可选项
*
* @author 秋辞未寒
*/
@Data
@EqualsAndHashCode
@Accessors(chain = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Options {
/**
* 文件长度
*/
private Long length;
/**
* 文件MD5摘要
*/
private String md5Digest;
/**
* 内容类型
*/
private String contentType;
/**
* 元数据
*/
private Map<String, String> metadata;
/**
* 传输监听器
*/
private Collection<TransferListener> transferListeners;
/**
* 添加元数据项
*/
public Options addMetadataItem(String key, String value) {
if (this.metadata == null) {
this.metadata = new HashMap<>();
}
this.metadata.put(key, value);
return this;
}
/**
* 添加监听器
*/
public Options addTransferListener(TransferListener transferListener) {
if (this.transferListeners == null) {
this.transferListeners = new ArrayList<>();
}
this.transferListeners.add(transferListener);
return this;
}
/**
* 创建可选项对象
*/
public static Options builder() {
return create();
}
/**
* 创建可选项对象
*/
public static Options create() {
return new Options();
}
}
@@ -0,0 +1,32 @@
package org.dromara.common.oss.model;
/**
* Put文件对象结果
*
* @param url
* @param key
* @param eTag
* @param size
* @author 秋辞未寒
*/
public record PutObjectResult(
String url,
String key,
String eTag,
long size
) {
/**
* 构建文件上传结果。
*
* @param url 文件地址
* @param key 文件标识
* @param eTag 文件 ETag
* @param size 文件大小
* @return 文件上传结果
*/
public static PutObjectResult form(String url, String key, String eTag, long size) {
return new PutObjectResult(url, key, eTag, size);
}
}
@@ -0,0 +1,58 @@
package org.dromara.common.oss.properties;
import lombok.Data;
/**
* OSS对象存储 配置属性
*
* @author Lion Li
*/
@Data
public class OssProperties {
/**
* 访问站点
*/
private String endpoint;
/**
* 自定义域名
*/
private String domainUrl;
/**
* 前缀
*/
private String prefix;
/**
* ACCESS_KEY
*/
private String accessKey;
/**
* SECRET_KEY
*/
private String secretKey;
/**
* 存储空间名
*/
private String bucketName;
/**
* 存储区域
*/
private String region;
/**
* 是否httpsY=是,N=否)
*/
private String isHttps;
/**
* 桶权限类型(0private 1public 2custom)
*/
private String accessPolicy;
}
@@ -0,0 +1,88 @@
package org.dromara.common.oss.util;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.StringUtils;
/**
* 桶链接工具类
*
* @author 秋辞未寒
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BucketUrlUtil {
public static final String HTTP_PROTOCOL_HEADER = "http://";
public static final String HTTPS_PROTOCOL_HEADER = "https://";
public static final String EMPTY_STRING = "";
// 路径风格 例:https://s3examples.com/images
private static final String PATH_STYLE_HTTP_FORMATE = "http://%s/%s";
private static final String PATH_STYLE_HTTPS_FORMATE = "https://%s/%s";
// 站点风格 例:https://images.oss-cn-beijing.aliyuncs.com
private static final String SITE_STYLE_HTTP_FORMATE = "http://%s.%s";
private static final String SITE_STYLE_HTTPS_FORMATE = "https://%s.%s";
/**
* 重建链接协议头(将IP、域名、站点的协议头改成HTTP或者HTTPS)
*
* @param isHttps 是否为HTTP
* @param base 基础地址(可以是IP、站点或者域名)
* @return 域名地址
*/
public static String rebuildUrlHeader(boolean isHttps, String base) {
String baseUrl = removeHttpProtocolHeader(base);
if (isHttps) {
return HTTPS_PROTOCOL_HEADER + baseUrl;
}
return HTTP_PROTOCOL_HEADER + baseUrl;
}
/**
* 获取路径风格的桶地址 例:https://s3examples.com/images
*
* @param isHttps 是否为HTTP
* @param base 基础地址(可以是IP、站点或者域名)
* @param bucketName 桶名称
* @return 路径风格的桶地址
*/
public static String getPathStyleBucketUrl(boolean isHttps, String base, String bucketName) {
String baseUrl = removeHttpProtocolHeader(base);
if (isHttps) {
return String.format(PATH_STYLE_HTTPS_FORMATE, baseUrl, bucketName);
}
return String.format(PATH_STYLE_HTTP_FORMATE, baseUrl, bucketName);
}
/**
* 获取站点风格的桶地址 例:https://images.oss-cn-beijing.aliyuncs.com
*
* @param isHttps 是否为HTTP
* @param base 基础地址(可以是IP、站点或者域名)
* @param bucketName 桶名称
* @return 站点风格的桶地址
*/
public static String getSiteStyleBucketUrl(boolean isHttps, String base, String bucketName) {
String baseUrl = removeHttpProtocolHeader(base);
if (isHttps) {
return String.format(SITE_STYLE_HTTPS_FORMATE, bucketName, baseUrl);
}
return String.format(SITE_STYLE_HTTP_FORMATE, bucketName, baseUrl);
}
/**
* 移除HTTP/HTTPS协议头(如果有的话)
*
* @param url 链接地址
* @return 移除HTTP/HTTPS协议头后的地址
*/
public static String removeHttpProtocolHeader(String url) {
if (StringUtils.startsWithIgnoreCase(url, HTTP_PROTOCOL_HEADER) || StringUtils.startsWithIgnoreCase(url, HTTPS_PROTOCOL_HEADER)) {
return url.replace(HTTP_PROTOCOL_HEADER, EMPTY_STRING)
.replace(HTTPS_PROTOCOL_HEADER, EMPTY_STRING);
}
return url;
}
}