mirror of
https://gitee.com/dromara/RuoYi-Vue-Plus.git
synced 2026-03-26 07:14:33 +08:00
remove 移除 旧的S3客户端
update 更新 文件上传使用新的S3客户端
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
package org.dromara.common.oss.s3.client;
|
||||
package org.dromara.common.oss.client;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.oss.s3.config.S3StorageClientConfig;
|
||||
import org.dromara.common.oss.s3.domain.GetObjectResult;
|
||||
import org.dromara.common.oss.s3.domain.HandleAsyncResult;
|
||||
import org.dromara.common.oss.s3.domain.PutObjectResult;
|
||||
import org.dromara.common.oss.s3.exception.S3StorageException;
|
||||
import org.dromara.common.oss.s3.io.OutputStreamDownloadSubscriber;
|
||||
import org.dromara.common.oss.config.S3StorageClientConfig;
|
||||
import org.dromara.common.oss.model.GetObjectResult;
|
||||
import org.dromara.common.oss.model.HandleAsyncResult;
|
||||
import org.dromara.common.oss.model.PutObjectResult;
|
||||
import org.dromara.common.oss.exception.S3StorageException;
|
||||
import org.dromara.common.oss.io.OutputStreamDownloadSubscriber;
|
||||
import software.amazon.awssdk.core.async.AsyncRequestBody;
|
||||
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
|
||||
import software.amazon.awssdk.core.async.ResponsePublisher;
|
||||
@@ -44,6 +45,7 @@ import java.util.function.Function;
|
||||
*
|
||||
* @author 秋辞未寒
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractS3StorageClientImpl implements S3StorageClient {
|
||||
|
||||
private final AtomicBoolean initialized = new AtomicBoolean(false);
|
||||
@@ -182,12 +184,22 @@ public abstract class AbstractS3StorageClientImpl implements S3StorageClient {
|
||||
|
||||
@Override
|
||||
public HandleAsyncResult<PutObjectResponse> doCustomUpload(AsyncRequestBody body, Consumer<PutObjectRequest.Builder> putObjectRequestBuilderConsumer, Collection<TransferListener> transferListeners) {
|
||||
return doCustomUpload(body, putObjectRequestBuilderConsumer, transferListeners, (completedUpload, throwable) -> HandleAsyncResult.of(completedUpload.response(), throwable));
|
||||
return doCustomUpload(body, putObjectRequestBuilderConsumer, transferListeners, (completedUpload, throwable) -> {
|
||||
if (completedUpload == null) {
|
||||
return HandleAsyncResult.of(null, throwable);
|
||||
}
|
||||
return HandleAsyncResult.of(completedUpload.response(), throwable);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandleAsyncResult<PutObjectResponse> doCustomUpload(AsyncRequestBody body, Consumer<PutObjectRequest.Builder> putObjectRequestBuilderConsumer) {
|
||||
return doCustomUpload(body, putObjectRequestBuilderConsumer, null, (completedUpload, throwable) -> HandleAsyncResult.of(completedUpload.response(), throwable));
|
||||
return doCustomUpload(body, putObjectRequestBuilderConsumer, null, (completedUpload, throwable) -> {
|
||||
if (completedUpload == null) {
|
||||
return HandleAsyncResult.of(null, throwable);
|
||||
}
|
||||
return HandleAsyncResult.of(completedUpload.response(), throwable);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -232,9 +244,7 @@ public abstract class AbstractS3StorageClientImpl implements S3StorageClient {
|
||||
|
||||
@Override
|
||||
public PutObjectResult bucketUpload(String bucket, String key, InputStream in, long contentLength) {
|
||||
AsyncRequestBody body = AsyncRequestBody.fromInputStream(builder -> builder.inputStream(in)
|
||||
.contentLength(contentLength)
|
||||
.executor(asyncExecutor));
|
||||
AsyncRequestBody body = AsyncRequestBody.fromInputStream(in,contentLength,asyncExecutor);
|
||||
return bucketUpload(bucket, key, body);
|
||||
}
|
||||
|
||||
@@ -252,7 +262,13 @@ public abstract class AbstractS3StorageClientImpl implements S3StorageClient {
|
||||
|
||||
|
||||
private PutObjectResult bucketUpload(String bucket, String key, AsyncRequestBody body) {
|
||||
HandleAsyncResult<PutObjectResponse> result = doCustomUpload(body, builder -> builder.bucket(bucket).key(key));
|
||||
Long contentLength = body.contentLength().orElse(null);
|
||||
HandleAsyncResult<PutObjectResponse> result = doCustomUpload(body, builder -> {
|
||||
builder.bucket(bucket)
|
||||
.key(key)
|
||||
.contentLength(contentLength)
|
||||
;
|
||||
});
|
||||
if (result.isFailure()) {
|
||||
throw S3StorageException.form(result.error());
|
||||
}
|
||||
@@ -261,7 +277,12 @@ public abstract class AbstractS3StorageClientImpl implements S3StorageClient {
|
||||
throw S3StorageException.form("response is empty.");
|
||||
}
|
||||
PutObjectResponse response = opt.get();
|
||||
return PutObjectResult.form(null, key, response.eTag(), response.size());
|
||||
String bucketUrl = config.getBucketUrl(bucket);
|
||||
// 不知道什么原因导致 response.size() 返回了一个 null size ,此处做一个适配...
|
||||
Long size = response.size();
|
||||
size = size == null ? contentLength : size;
|
||||
log.info("response size:{}", size);
|
||||
return PutObjectResult.form("%s/%s".formatted(bucketUrl,key), key, response.eTag(), size == null?0:size);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.dromara.common.oss.s3.client;
|
||||
package org.dromara.common.oss.client;
|
||||
|
||||
import org.dromara.common.oss.s3.config.S3AsyncExecutorConfig;
|
||||
import org.dromara.common.oss.s3.config.S3StorageClientConfig;
|
||||
import org.dromara.common.oss.s3.exception.S3StorageException;
|
||||
import org.dromara.common.oss.config.S3AsyncExecutorConfig;
|
||||
import org.dromara.common.oss.config.S3StorageClientConfig;
|
||||
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.http.nio.netty.NettyNioAsyncHttpClient;
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.dromara.common.oss.s3.client;
|
||||
package org.dromara.common.oss.client;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import org.dromara.common.oss.s3.config.S3StorageClientConfig;
|
||||
import org.dromara.common.oss.s3.domain.GetObjectResult;
|
||||
import org.dromara.common.oss.s3.domain.HandleAsyncResult;
|
||||
import org.dromara.common.oss.s3.domain.PutObjectResult;
|
||||
import org.dromara.common.oss.s3.io.OutputStreamDownloadSubscriber;
|
||||
import org.dromara.common.oss.config.S3StorageClientConfig;
|
||||
import org.dromara.common.oss.model.GetObjectResult;
|
||||
import org.dromara.common.oss.model.HandleAsyncResult;
|
||||
import org.dromara.common.oss.model.PutObjectResult;
|
||||
import org.dromara.common.oss.io.OutputStreamDownloadSubscriber;
|
||||
import software.amazon.awssdk.core.async.AsyncRequestBody;
|
||||
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.dromara.common.oss.s3.config;
|
||||
package org.dromara.common.oss.config;
|
||||
|
||||
/**
|
||||
* 配置对象接口
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.dromara.common.oss.s3.config;
|
||||
package org.dromara.common.oss.config;
|
||||
|
||||
import lombok.Builder;
|
||||
import org.dromara.common.oss.s3.enums.AccessPolicy;
|
||||
import org.dromara.common.oss.enums.AccessPolicy;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
import java.io.Serial;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.dromara.common.oss.s3.config;
|
||||
package org.dromara.common.oss.config;
|
||||
|
||||
import lombok.Builder;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.dromara.common.oss.s3.config;
|
||||
package org.dromara.common.oss.config;
|
||||
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import lombok.Builder;
|
||||
@@ -7,9 +7,10 @@ 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.s3.exception.S3StorageException;
|
||||
import org.dromara.common.oss.s3.util.BucketUrlUtil;
|
||||
import org.dromara.common.oss.util.BucketUrlUtil;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
|
||||
@@ -200,6 +201,15 @@ public class S3StorageClientConfig implements Config<S3StorageClientConfig, S3St
|
||||
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 = domain()
|
||||
// 检查携带协议头
|
||||
@@ -256,16 +266,17 @@ public class S3StorageClientConfig implements Config<S3StorageClientConfig, S3St
|
||||
// MinIO 使用 HTTPS 限制使用域名访问,站点填域名。需要启用路径样式访问
|
||||
boolean usePathStyleAccess = !StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE);
|
||||
|
||||
// // 目前自定义实现的 Client 中并没有实际使用到ACL相关配置,只是作为一个扩展点保留,有需要ACL的自行实现调用逻辑
|
||||
// String accessPolicyString = properties.getAccessPolicy();
|
||||
// // 绝大多数的云厂商都是不允许操作ACL的,所以此处的默认配置也是禁用ACL的
|
||||
// S3AccessControlPolicyConfig accessControlPolicyConfig = S3AccessControlPolicyConfig.DEFAULT;
|
||||
// if (StringUtils.isNotBlank(accessPolicyString)) {
|
||||
// accessControlPolicyConfig = S3AccessControlPolicyConfig.builder()
|
||||
// .enabled(true)
|
||||
// .accessPolicy(AccessPolicy.formType(accessPolicyString))
|
||||
// .build();
|
||||
// }
|
||||
// 绝大多数的云厂商都是不允许操作ACL的,所以此处的默认配置也是禁用ACL的
|
||||
S3AccessControlPolicyConfig accessControlPolicyConfig = S3AccessControlPolicyConfig.DEFAULT;
|
||||
// 目前自定义实现的 Client 上传/下载/删除中并没有实际使用到ACL相关配置
|
||||
// 仅有业务中的链接预签名使用到(SysOssServiceImpl#matchingUrl),更多只是作为一个扩展点保留,如有需要ACL的自行实现调用逻辑
|
||||
String accessPolicyString = properties.getAccessPolicy();
|
||||
if (StringUtils.isNotBlank(accessPolicyString)) {
|
||||
accessControlPolicyConfig = S3AccessControlPolicyConfig.builder()
|
||||
.enabled(true)
|
||||
.accessPolicy(AccessPolicy.formType(accessPolicyString))
|
||||
.build();
|
||||
}
|
||||
return builder()
|
||||
.endpoint(properties.getEndpoint())
|
||||
.domain(properties.getDomainUrl())
|
||||
@@ -274,7 +285,7 @@ public class S3StorageClientConfig implements Config<S3StorageClientConfig, S3St
|
||||
.bucket(properties.getBucketName())
|
||||
.region(region)
|
||||
.useHttps(SystemConstants.YES.equals(properties.getIsHttps()))
|
||||
.usePathStyleAccess(usePathStyleAccess);
|
||||
// .accessControlPolicyConfig(accessControlPolicyConfig);
|
||||
.usePathStyleAccess(usePathStyleAccess)
|
||||
.accessControlPolicyConfig(accessControlPolicyConfig);
|
||||
}
|
||||
}
|
||||
@@ -1,568 +0,0 @@
|
||||
package org.dromara.common.oss.core;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
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.enums.AccessPolicyType;
|
||||
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.AsyncResponseTransformer;
|
||||
import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody;
|
||||
import software.amazon.awssdk.core.async.ResponsePublisher;
|
||||
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.model.GetObjectResponse;
|
||||
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.*;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* S3 存储协议 所有兼容S3协议的云厂商均支持
|
||||
* 阿里云 腾讯云 七牛云 minio
|
||||
*
|
||||
* @author AprilWind
|
||||
*/
|
||||
@Slf4j
|
||||
public class OssClient {
|
||||
|
||||
/**
|
||||
* 服务商
|
||||
*/
|
||||
private final String configKey;
|
||||
|
||||
/**
|
||||
* 配置属性
|
||||
*/
|
||||
private final OssProperties properties;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// 创建 AWS 认证信息
|
||||
StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()));
|
||||
|
||||
// MinIO 使用 HTTPS 限制使用域名访问,站点填域名。需要启用路径样式访问
|
||||
boolean isStyle = !StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE);
|
||||
|
||||
// 创建AWS基于 Netty 的 S3 客户端
|
||||
this.client = S3AsyncClient.builder()
|
||||
.credentialsProvider(credentialsProvider)
|
||||
.endpointOverride(URI.create(getEndpoint()))
|
||||
.region(of())
|
||||
.forcePathStyle(isStyle)
|
||||
.httpClient(NettyNioAsyncHttpClient.builder()
|
||||
.connectionTimeout(Duration.ofSeconds(60))
|
||||
.connectionAcquisitionTimeout(Duration.ofSeconds(30))
|
||||
.maxConcurrency(100)
|
||||
.maxPendingConnectionAcquires(1000)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
//AWS基于 CRT 的 S3 AsyncClient 实例用作 S3 传输管理器的底层客户端
|
||||
this.transferManager = S3TransferManager.builder().s3Client(this.client).build();
|
||||
|
||||
// 创建 S3 配置对象
|
||||
S3Configuration config = S3Configuration.builder().chunkedEncodingEnabled(false)
|
||||
.pathStyleAccessEnabled(isStyle).build();
|
||||
|
||||
// 创建 预签名 URL 的生成器 实例,用于生成 S3 预签名 URL
|
||||
this.presigner = S3Presigner.builder()
|
||||
.region(of())
|
||||
.credentialsProvider(credentialsProvider)
|
||||
.endpointOverride(URI.create(getDomain()))
|
||||
.serviceConfiguration(config)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
if (e instanceof OssException) {
|
||||
throw e;
|
||||
}
|
||||
throw new OssException("配置错误! 请检查系统配置:[" + e.getMessage() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到 Amazon S3,并返回上传结果
|
||||
*
|
||||
* @param filePath 本地文件路径
|
||||
* @param key 在 Amazon S3 中的对象键
|
||||
* @param md5Digest 本地文件的 MD5 哈希值(可选)
|
||||
* @param contentType 文件内容类型
|
||||
* @return UploadResult 包含上传后的文件信息
|
||||
* @throws OssException 如果上传失败,抛出自定义异常
|
||||
*/
|
||||
public UploadResult upload(Path filePath, String key, String md5Digest, String contentType) {
|
||||
try {
|
||||
// 构建上传请求对象
|
||||
FileUpload fileUpload = transferManager.uploadFile(
|
||||
x -> {
|
||||
x.source(filePath).putObjectRequest(
|
||||
y -> y.bucket(properties.getBucketName())
|
||||
.key(key)
|
||||
.contentMD5(StringUtils.isNotEmpty(md5Digest) ? md5Digest : null)
|
||||
.contentType(contentType)
|
||||
// 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同,
|
||||
// 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置)
|
||||
//.acl(getAccessPolicy().getObjectCannedACL())
|
||||
.build()
|
||||
);
|
||||
if (log.isDebugEnabled()) {
|
||||
x.addTransferListener(LoggingTransferListener.create());
|
||||
}
|
||||
}
|
||||
);
|
||||
// 等待上传完成并获取上传结果
|
||||
CompletedFileUpload uploadResult = fileUpload.completionFuture().join();
|
||||
String eTag = uploadResult.response().eTag();
|
||||
|
||||
// 提取上传结果中的 ETag,并构建一个自定义的 UploadResult 对象
|
||||
return new UploadResult(getUrl() + StringUtils.SLASH + key, key, eTag);
|
||||
} catch (Exception e) {
|
||||
// 捕获异常并抛出自定义异常
|
||||
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
|
||||
} finally {
|
||||
// 无论上传是否成功,最终都会删除临时文件
|
||||
FileUtils.del(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传 InputStream 到 Amazon S3
|
||||
*
|
||||
* @param inputStream 要上传的输入流
|
||||
* @param key 在 Amazon S3 中的对象键
|
||||
* @param length 输入流的长度
|
||||
* @param contentType 文件内容类型
|
||||
* @return UploadResult 包含上传后的文件信息
|
||||
* @throws OssException 如果上传失败,抛出自定义异常
|
||||
*/
|
||||
public UploadResult upload(InputStream inputStream, String key, Long length, String contentType) {
|
||||
// 如果输入流不是 ByteArrayInputStream,则将其读取为字节数组再创建 ByteArrayInputStream
|
||||
if (!(inputStream instanceof ByteArrayInputStream)) {
|
||||
inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream));
|
||||
}
|
||||
try {
|
||||
// 创建异步请求体(length如果为空会报错)
|
||||
BlockingInputStreamAsyncRequestBody body = BlockingInputStreamAsyncRequestBody.builder()
|
||||
.contentLength(length)
|
||||
.subscribeTimeout(Duration.ofSeconds(120))
|
||||
.build();
|
||||
|
||||
// 使用 transferManager 进行上传
|
||||
Upload upload = transferManager.upload(
|
||||
x -> {
|
||||
x.requestBody(body).putObjectRequest(
|
||||
y -> y.bucket(properties.getBucketName())
|
||||
.key(key)
|
||||
.contentType(contentType)
|
||||
// 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同,
|
||||
// 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置)
|
||||
//.acl(getAccessPolicy().getObjectCannedACL())
|
||||
.build()
|
||||
);
|
||||
if (log.isDebugEnabled()) {
|
||||
x.addTransferListener(LoggingTransferListener.create());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 将输入流写入请求体
|
||||
body.writeInputStream(inputStream);
|
||||
|
||||
// 等待文件上传操作完成
|
||||
CompletedUpload uploadResult = upload.completionFuture().join();
|
||||
String eTag = uploadResult.response().eTag();
|
||||
|
||||
// 提取上传结果中的 ETag,并构建一个自定义的 UploadResult 对象
|
||||
return new UploadResult(getUrl() + StringUtils.SLASH + key, key, eTag);
|
||||
} catch (Exception e) {
|
||||
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件从 Amazon S3 到临时目录
|
||||
*
|
||||
* @param path 文件在 Amazon S3 中的对象键
|
||||
* @return 下载后的文件在本地的临时路径
|
||||
* @throws OssException 如果下载失败,抛出自定义异常
|
||||
*/
|
||||
public Path fileDownload(String path) {
|
||||
// 构建临时文件
|
||||
Path tempFilePath = FileUtils.createTempFile().toPath();
|
||||
// 使用 S3TransferManager 下载文件
|
||||
FileDownload downloadFile = transferManager.downloadFile(
|
||||
x -> {
|
||||
x.destination(tempFilePath).getObjectRequest(
|
||||
y -> y.bucket(properties.getBucketName())
|
||||
.key(removeBaseUrl(path))
|
||||
.build()
|
||||
);
|
||||
if (log.isDebugEnabled()) {
|
||||
x.addTransferListener(LoggingTransferListener.create());
|
||||
}
|
||||
}
|
||||
);
|
||||
// 等待文件下载操作完成
|
||||
downloadFile.completionFuture().join();
|
||||
return tempFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件从 Amazon S3 到 输出流
|
||||
*
|
||||
* @param key 文件在 Amazon S3 中的对象键
|
||||
* @param out 输出流
|
||||
* @param consumer 自定义处理逻辑
|
||||
* @throws OssException 如果下载失败,抛出自定义异常
|
||||
*/
|
||||
public void download(String key, OutputStream out, Consumer<Long> consumer) {
|
||||
try {
|
||||
this.download(key, consumer).writeTo(out);
|
||||
} catch (Exception e) {
|
||||
throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件从 Amazon S3 到 输出流
|
||||
*
|
||||
* @param key 文件在 Amazon S3 中的对象键
|
||||
* @param contentLengthConsumer 文件大小消费者函数
|
||||
* @return 写出订阅器
|
||||
* @throws OssException 如果下载失败,抛出自定义异常
|
||||
*/
|
||||
public WriteOutSubscriber<OutputStream> download(String key, Consumer<Long> contentLengthConsumer) {
|
||||
try {
|
||||
DownloadRequest.TypedBuilder<ResponsePublisher<GetObjectResponse>> typedBuilder = DownloadRequest.builder()
|
||||
// 使用发布订阅转换器
|
||||
.responseTransformer(AsyncResponseTransformer.toPublisher())
|
||||
// 文件对象
|
||||
.getObjectRequest(y -> y.bucket(properties.getBucketName()).key(key).build());
|
||||
if (log.isDebugEnabled()) {
|
||||
typedBuilder.addTransferListener(LoggingTransferListener.create());
|
||||
}
|
||||
|
||||
// 使用 S3TransferManager 下载文件
|
||||
Download<ResponsePublisher<GetObjectResponse>> publisherDownload = transferManager.download(typedBuilder.build());
|
||||
// 获取下载发布订阅转换器
|
||||
ResponsePublisher<GetObjectResponse> publisher = publisherDownload.completionFuture().join().result();
|
||||
// 执行文件大小消费者函数
|
||||
Optional.ofNullable(contentLengthConsumer)
|
||||
.ifPresent(lengthConsumer -> lengthConsumer.accept(publisher.response().contentLength()));
|
||||
|
||||
// 构建写出订阅器对象
|
||||
return out -> {
|
||||
// 创建可写入的字节通道
|
||||
try (WritableByteChannel channel = Channels.newChannel(out)) {
|
||||
// 订阅数据
|
||||
publisher.subscribe(byteBuffer -> {
|
||||
while (byteBuffer.hasRemaining()) {
|
||||
try {
|
||||
channel.write(byteBuffer);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}).join();
|
||||
}
|
||||
};
|
||||
} catch (Exception e) {
|
||||
throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除云存储服务中指定路径下文件
|
||||
*
|
||||
* @param path 指定路径
|
||||
*/
|
||||
public void delete(String path) {
|
||||
try {
|
||||
client.deleteObject(
|
||||
x -> x.bucket(properties.getBucketName())
|
||||
.key(removeBaseUrl(path))
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建下载请求的预签名URL
|
||||
*
|
||||
* @param objectKey 对象KEY
|
||||
* @param expiredTime 链接授权到期时间
|
||||
*/
|
||||
public String createPresignedGetUrl(String objectKey, Duration expiredTime) {
|
||||
// 使用 AWS S3 预签名 URL 的生成器 获取下载对象的预签名 URL
|
||||
URL url = presigner.presignGetObject(
|
||||
x -> x.signatureDuration(expiredTime)
|
||||
.getObjectRequest(
|
||||
y -> y.bucket(properties.getBucketName())
|
||||
.key(objectKey)
|
||||
.build())
|
||||
.build())
|
||||
.url();
|
||||
return url.toExternalForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建上传请求的预签名URL
|
||||
*
|
||||
* @param objectKey 对象KEY
|
||||
* @param expiredTime 链接授权到期时间
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
public String createPresignedPutUrl(String objectKey, Duration expiredTime, Map<String, String> metadata) {
|
||||
// 使用 AWS S3 预签名 URL 的生成器 获取上传文件对象的预签名 URL
|
||||
URL url = presigner.presignPutObject(
|
||||
x -> x.signatureDuration(expiredTime)
|
||||
.putObjectRequest(
|
||||
y -> y.bucket(properties.getBucketName())
|
||||
.key(objectKey)
|
||||
.metadata(metadata)
|
||||
.build())
|
||||
.build())
|
||||
.url();
|
||||
return url.toExternalForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传 byte[] 数据到 Amazon S3,使用指定的后缀构造对象键。
|
||||
*
|
||||
* @param data 要上传的 byte[] 数据
|
||||
* @param suffix 对象键的后缀
|
||||
* @return UploadResult 包含上传后的文件信息
|
||||
* @throws OssException 如果上传失败,抛出自定义异常
|
||||
*/
|
||||
public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
|
||||
return upload(new ByteArrayInputStream(data), getPath(properties.getPrefix(), suffix), Long.valueOf(data.length), contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传 InputStream 到 Amazon S3,使用指定的后缀构造对象键。
|
||||
*
|
||||
* @param inputStream 要上传的输入流
|
||||
* @param suffix 对象键的后缀
|
||||
* @param length 输入流的长度
|
||||
* @return UploadResult 包含上传后的文件信息
|
||||
* @throws OssException 如果上传失败,抛出自定义异常
|
||||
*/
|
||||
public UploadResult uploadSuffix(InputStream inputStream, String suffix, Long length, String contentType) {
|
||||
return upload(inputStream, getPath(properties.getPrefix(), suffix), length, contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到 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, FileUtils.getMimeType(suffix));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件输入流
|
||||
*
|
||||
* @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.getDomainUrl();
|
||||
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.getDomainUrl();
|
||||
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 SystemConstants.YES.equals(properties.getIsHttps()) ? Constants.HTTPS : Constants.HTTP;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查配置是否相同
|
||||
*
|
||||
* @param properties OSS 配置
|
||||
* @return 是否与当前客户端配置一致
|
||||
*/
|
||||
public boolean checkPropertiesSame(OssProperties properties) {
|
||||
return this.properties.equals(properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前桶权限类型
|
||||
*
|
||||
* @return 当前桶权限类型code
|
||||
*/
|
||||
public AccessPolicyType getAccessPolicy() {
|
||||
return AccessPolicyType.getByType(properties.getAccessPolicy());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.dromara.common.oss.core;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 写出订阅器
|
||||
*
|
||||
* @author 秋辞未寒
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface WriteOutSubscriber<T> {
|
||||
|
||||
/**
|
||||
* 将订阅到的数据写出到目标对象。
|
||||
*
|
||||
* @param out 写出目标
|
||||
* @throws IOException 写出异常
|
||||
*/
|
||||
void writeTo(T out) throws IOException;
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.dromara.common.oss.entity;
|
||||
|
||||
/**
|
||||
* 上传返回体
|
||||
*
|
||||
* @param url 文件访问地址
|
||||
* @param filename 文件名
|
||||
* @param eTag 存储服务返回的 ETag
|
||||
* @author Lion Li
|
||||
*/
|
||||
public record UploadResult(
|
||||
String url,
|
||||
String filename,
|
||||
String eTag
|
||||
) {
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.dromara.common.oss.s3.enums;
|
||||
package org.dromara.common.oss.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.dromara.common.oss.s3.exception.S3StorageException;
|
||||
import org.dromara.common.oss.exception.S3StorageException;
|
||||
import software.amazon.awssdk.services.s3.model.BucketCannedACL;
|
||||
import software.amazon.awssdk.services.s3.model.ObjectCannedACL;
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package org.dromara.common.oss.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import software.amazon.awssdk.services.s3.model.BucketCannedACL;
|
||||
import software.amazon.awssdk.services.s3.model.ObjectCannedACL;
|
||||
|
||||
/**
|
||||
* 桶访问策略配置
|
||||
*
|
||||
* @author 陈賝
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum AccessPolicyType {
|
||||
|
||||
/**
|
||||
* private
|
||||
*/
|
||||
PRIVATE("0", BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE),
|
||||
|
||||
/**
|
||||
* public
|
||||
*/
|
||||
PUBLIC("1", BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE),
|
||||
|
||||
/**
|
||||
* custom
|
||||
*/
|
||||
CUSTOM("2", BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ);
|
||||
|
||||
/**
|
||||
* 桶 权限类型(数据库值)
|
||||
*/
|
||||
private final String type;
|
||||
|
||||
/**
|
||||
* 桶 权限类型
|
||||
*/
|
||||
private final BucketCannedACL bucketCannedACL;
|
||||
|
||||
/**
|
||||
* 文件对象 权限类型
|
||||
*/
|
||||
private final ObjectCannedACL objectCannedACL;
|
||||
|
||||
public static AccessPolicyType getByType(String type) {
|
||||
for (AccessPolicyType value : values()) {
|
||||
if (value.getType().equals(type)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("'type' not found By " + type);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.dromara.common.oss.exception;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* OSS异常类
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class OssException extends RuntimeException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 创建 OSS 业务异常。
|
||||
*
|
||||
* @param msg 异常消息
|
||||
*/
|
||||
public OssException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.dromara.common.oss.s3.exception;
|
||||
package org.dromara.common.oss.exception;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
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.constant.OssConstant;
|
||||
import org.dromara.common.oss.core.OssClient;
|
||||
import org.dromara.common.oss.exception.OssException;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 文件上传Factory
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
public class OssFactory {
|
||||
|
||||
private static final Map<String, OssClient> CLIENT_CACHE = new ConcurrentHashMap<>();
|
||||
private static final ReentrantLock LOCK = new ReentrantLock();
|
||||
|
||||
/**
|
||||
* 获取默认 OSS 客户端实例。
|
||||
*
|
||||
* @return 默认 OSS 客户端
|
||||
*/
|
||||
public static OssClient instance() {
|
||||
// 获取redis 默认类型
|
||||
String configKey = RedisUtils.getCacheObject(OssConstant.DEFAULT_CONFIG_KEY);
|
||||
if (StringUtils.isEmpty(configKey)) {
|
||||
throw new OssException("文件存储服务类型无法找到!");
|
||||
}
|
||||
return instance(configKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置键获取 OSS 客户端实例。
|
||||
*
|
||||
* @param configKey 配置键
|
||||
* @return OSS 客户端
|
||||
*/
|
||||
public static OssClient instance(String configKey) {
|
||||
String json = CacheUtils.get(CacheNames.SYS_OSS_CONFIG, configKey);
|
||||
if (json == null) {
|
||||
throw new OssException("系统异常, '" + configKey + "'配置信息不存在!");
|
||||
}
|
||||
OssProperties properties = JsonUtils.parseObject(json, OssProperties.class);
|
||||
// 使用租户标识避免多个租户相同key实例覆盖
|
||||
OssClient client = CLIENT_CACHE.get(configKey);
|
||||
// 客户端不存在或配置不相同则重新构建
|
||||
if (client == null || !client.checkPropertiesSame(properties)) {
|
||||
LOCK.lock();
|
||||
try {
|
||||
client = CLIENT_CACHE.get(configKey);
|
||||
if (client == null || !client.checkPropertiesSame(properties)) {
|
||||
CLIENT_CACHE.put(configKey, new OssClient(configKey, properties));
|
||||
log.info("创建OSS实例 key => {}", configKey);
|
||||
return CLIENT_CACHE.get(configKey);
|
||||
}
|
||||
} finally {
|
||||
LOCK.unlock();
|
||||
}
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.dromara.common.oss.s3.factory;
|
||||
package org.dromara.common.oss.factory;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.CacheNames;
|
||||
@@ -6,10 +6,10 @@ import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.oss.constant.OssConstant;
|
||||
import org.dromara.common.oss.properties.OssProperties;
|
||||
import org.dromara.common.oss.s3.client.DefaultS3StorageClientImpl;
|
||||
import org.dromara.common.oss.s3.client.S3StorageClient;
|
||||
import org.dromara.common.oss.s3.config.S3StorageClientConfig;
|
||||
import org.dromara.common.oss.s3.exception.S3StorageException;
|
||||
import org.dromara.common.oss.client.DefaultS3StorageClientImpl;
|
||||
import org.dromara.common.oss.client.S3StorageClient;
|
||||
import org.dromara.common.oss.config.S3StorageClientConfig;
|
||||
import org.dromara.common.oss.exception.S3StorageException;
|
||||
import org.dromara.common.redis.utils.CacheUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.dromara.common.oss.s3.io;
|
||||
package org.dromara.common.oss.io;
|
||||
|
||||
import org.dromara.common.oss.s3.exception.S3StorageException;
|
||||
import org.dromara.common.oss.exception.S3StorageException;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.dromara.common.oss.s3.domain;
|
||||
package org.dromara.common.oss.model;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.dromara.common.oss.s3.domain;
|
||||
package org.dromara.common.oss.model;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.dromara.common.oss.s3.domain;
|
||||
package org.dromara.common.oss.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.dromara.common.oss.s3.domain;
|
||||
package org.dromara.common.oss.model;
|
||||
|
||||
/**
|
||||
* Put文件对象结果
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.dromara.common.oss.s3.util;
|
||||
package org.dromara.common.oss.util;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.dromara.common.oss.s3.util;
|
||||
package org.dromara.common.oss.util;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import lombok.AccessLevel;
|
||||
Reference in New Issue
Block a user