refactor(storage): 新增存储模块 - 本地和 S3 两种存储模式

This commit is contained in:
吴泽威
2024-12-30 21:01:26 +08:00
parent eb2cac54f7
commit bf2e30e560
29 changed files with 2437 additions and 196 deletions

View File

@@ -0,0 +1,63 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter-storage</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-storage-oss</artifactId>
<description>ContiNew Starter 存储模块 - 对象存储</description>
<dependencies>
<!-- S3 SDK for Java 2.x -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<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>
<groupId>top.continew</groupId>
<artifactId>continew-starter-storage-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.autoconfigure;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import top.continew.starter.storage.dao.StorageDao;
import top.continew.starter.storage.dao.impl.StorageDaoDefaultImpl;
/**
* 对象存储 - 存储自动配置
*
* @author echo
* @date 2024/12/17 20:23
*/
@AutoConfiguration
public class OssStorageAutoconfigure {
@Bean
@ConditionalOnMissingBean
public StorageDao storageDao() {
return new StorageDaoDefaultImpl();
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.client;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.crt.S3CrtHttpConfiguration;
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.transfer.s3.S3TransferManager;
import top.continew.starter.core.exception.BusinessException;
import top.continew.starter.storage.model.req.StorageProperties;
import top.continew.starter.storage.util.OssUtils;
import top.continew.starter.storage.util.StorageUtils;
import java.net.URI;
import java.time.Duration;
/**
* S3客户端
*
* @author echo
* @date 2024/12/16
*/
public class OssClient {
private static final Logger log = LoggerFactory.getLogger(OssClient.class);
/**
* 配置属性
*/
private final StorageProperties properties;
/**
* s3 异步客户端
*/
private final S3AsyncClient client;
/**
* S3 数据传输的高级工具
*/
private final S3TransferManager transferManager;
/**
* S3 预签名
*/
private final S3Presigner presigner;
/**
* 获取属性
*
* @return {@link StorageProperties }
*/
public StorageProperties getProperties() {
return properties;
}
/**
* 构造方法
*
* @param s3PropertiesReq 微型性能要求
*/
public OssClient(StorageProperties s3PropertiesReq) {
this.properties = s3PropertiesReq;
// 创建认证信息
StaticCredentialsProvider auth = StaticCredentialsProvider.create(AwsBasicCredentials.create(properties
.getAccessKey(), properties.getSecretKey()));
URI uriWithProtocol = StorageUtils.createUriWithProtocol(properties.getEndpoint());
// 创建 客户端连接
client = S3AsyncClient.crtBuilder()
.credentialsProvider(auth) // 认证信息
.endpointOverride(uriWithProtocol) // 连接端点
.region(OssUtils.getRegion(properties.getRegion()))
.targetThroughputInGbps(20.0) //吞吐量
.minimumPartSizeInBytes(10 * 1025 * 1024L)
.checksumValidationEnabled(false)
.httpConfiguration(S3CrtHttpConfiguration.builder()
.connectionTimeout(Duration.ofSeconds(60)) // 设置连接超时
.build())
.build();
// 基于 CRT 创建 S3 Transfer Manager 的实例
this.transferManager = S3TransferManager.builder().s3Client(this.client).build();
this.presigner = S3Presigner.builder()
.region(OssUtils.getRegion(properties.getRegion()))
.credentialsProvider(auth)
.endpointOverride(uriWithProtocol)
.build();
// 只创建 默认存储的的桶
if (s3PropertiesReq.getIsDefault()) {
try {
// 检查存储桶是否存在
client.headBucket(HeadBucketRequest.builder().bucket(properties.getBucketName()).build());
log.info("默认存储-存储桶 {} 已存在", properties.getBucketName());
} catch (NoSuchBucketException e) {
log.info("默认存储桶 {} 不存在,尝试创建...", properties.getBucketName());
try {
// 创建存储桶
client.createBucket(CreateBucketRequest.builder().bucket(properties.getBucketName()).build());
log.info("默认存储-存储桶 {} 创建成功", properties.getBucketName());
} catch (Exception createException) {
log.error("创建默认存储-存储桶 {} 失败", properties.getBucketName(), createException);
throw new BusinessException("创建默认存储-桶出错", createException);
}
} catch (Exception e) {
log.error("检查默认存储-存储桶 {} 时出错", properties.getBucketName(), e);
throw new BusinessException("检查默认存储-桶时出错", e);
}
}
log.info("加载 S3 存储 => {}", properties.getCode());
}
/**
* 获得客户端
*
* @return {@link S3TransferManager }
*/
public S3AsyncClient getClient() {
return client;
}
/**
* 获得 高效连接客户端 主要用于 上传下载 复制 删除
*
* @return {@link S3TransferManager }
*/
public S3TransferManager getTransferManager() {
return transferManager;
}
/**
* 获得 S3 预签名
*
* @return {@link S3Presigner }
*/
public S3Presigner getPresigner() {
return presigner;
}
}

View File

@@ -0,0 +1,401 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.strategy;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.transfer.s3.model.CompletedUpload;
import software.amazon.awssdk.transfer.s3.model.Download;
import software.amazon.awssdk.transfer.s3.model.DownloadRequest;
import software.amazon.awssdk.transfer.s3.model.Upload;
import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.exception.BusinessException;
import top.continew.starter.core.validation.CheckUtils;
import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.storage.client.OssClient;
import top.continew.starter.storage.constant.StorageConstant;
import top.continew.starter.storage.dao.StorageDao;
import top.continew.starter.storage.enums.FileTypeEnum;
import top.continew.starter.storage.model.req.StorageProperties;
import top.continew.starter.storage.model.resp.ThumbnailResp;
import top.continew.starter.storage.model.resp.UploadResp;
import top.continew.starter.storage.util.ImageThumbnailUtils;
import top.continew.starter.storage.util.OssUtils;
import top.continew.starter.storage.util.StorageUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.CompletionException;
/**
* OSS存储策略
* <p><a href="https://docs.aws.amazon.com/zh_cn/sdk-for-java/latest/developer-guide/home.html">...</a></p>
*
* @author echo
* @date 2024/12/16 20:29
*/
public class OssStorageStrategy implements StorageStrategy<OssClient> {
private final static Logger log = LoggerFactory.getLogger(OssStorageStrategy.class);
private final OssClient client;
private final StorageDao storageDao;
private String etag;
public OssStorageStrategy(OssClient ossClient, StorageDao storageDao) {
this.client = ossClient;
this.storageDao = storageDao;
}
private StorageProperties getStorageProperties() {
return client.getProperties();
}
@Override
public OssClient getClient() {
return client;
}
@Override
public boolean bucketExists(String bucketName) {
try {
// 调用 headBucket 请求,检查桶是否存在
client.getClient().headBucket(HeadBucketRequest.builder().bucket(bucketName).build()).join();
return true; // 桶存在
} catch (Exception e) {
// 捕获异常,详细判断具体原因
if (e.getCause() instanceof NoSuchBucketException) {
// 桶不存在
return false;
} else if (e.getCause() instanceof S3Exception s3Exception) {
// 检查是否是其他人创建的桶403 Forbidden 错误)
if (s3Exception.statusCode() == HttpURLConnection.HTTP_FORBIDDEN) {
throw new BusinessException("全局重复:存储桶名称已被他人创建:" + bucketName);
}
}
// 捕获其他所有异常,并抛出
throw new BusinessException("S3 存储桶查询失败,存储桶名称:" + bucketName, e);
}
}
@Override
public void createBucket(String bucketName) {
try {
if (!this.bucketExists(bucketName)) {
client.getClient().createBucket(CreateBucketRequest.builder().bucket(bucketName).build()).join();
}
} catch (S3Exception e) {
throw new BusinessException("S3 存储桶,创建失败", e);
}
}
@Override
public UploadResp upload(String fileName, InputStream inputStream, String fileType) {
String bucketName = getStorageProperties().getBucketName();
return this.upload(bucketName, fileName, null, inputStream, fileType, false);
}
@Override
public UploadResp upload(String fileName,
String path,
InputStream inputStream,
String fileType,
boolean isThumbnail) {
String bucketName = getStorageProperties().getBucketName();
return this.upload(bucketName, fileName, path, inputStream, fileType, isThumbnail);
}
@Override
public UploadResp upload(String bucketName,
String fileName,
String path,
InputStream inputStream,
String fileType,
boolean isThumbnail) {
try {
// 可重复读流
inputStream = StorageUtils.ensureByteArrayStream(inputStream);
byte[] fileBytes = IoUtil.readBytes(inputStream);
ValidationUtils.throwIf(fileBytes.length == 0, "输入流内容长度不可用或无效");
// 获取文件扩展名
String fileExtension = FileNameUtil.extName(fileName);
// 格式化文件名 防止上传后重复
String formatFileName = StorageUtils.formatFileName(fileName);
// 判断文件路径是否为空 为空给默认路径 格式 2024/12/30/
if (StrUtil.isEmpty(path)) {
path = StorageUtils.defaultPath();
}
ThumbnailResp thumbnailResp = null;
//判断是否需要上传缩略图 前置条件 文件必须为图片
boolean contains = FileTypeEnum.IMAGE.getExtensions().contains(fileExtension);
if (contains && isThumbnail) {
try (InputStream thumbnailStream = new ByteArrayInputStream(fileBytes)) {
thumbnailResp = this.uploadThumbnail(bucketName, formatFileName, path, thumbnailStream, fileType);
}
}
// 上传文件
try (InputStream uploadStream = new ByteArrayInputStream(fileBytes)) {
this.upload(bucketName, formatFileName, path, uploadStream, fileType);
}
String eTag = etag;
// 构建 上传后的文件路径地址 格式 xxx/xxx/xxx.jpg
String filePath = Paths.get(path, formatFileName).toString();
// 构建 文件上传记录 并返回
return buildStorageRecord(bucketName, fileName, filePath, eTag, fileBytes.length, thumbnailResp);
} catch (IOException e) {
throw new BusinessException("文件上传异常", e);
}
}
@Override
public void upload(String bucketName, String fileName, String path, InputStream inputStream, String fileType) {
// 构建 S3 存储 文件路径
String filePath = Paths.get(path, fileName).toString();
try {
long available = inputStream.available();
// 构建异步请求体,指定内容长度
BlockingInputStreamAsyncRequestBody requestBody = BlockingInputStreamAsyncRequestBody.builder()
.contentLength(available)
.subscribeTimeout(Duration.ofSeconds(30))
.build();
// 初始化上传任务
Upload upload = client.getTransferManager()
.upload(u -> u.requestBody(requestBody)
.putObjectRequest(b -> b.bucket(bucketName).key(filePath).contentType(fileType).build())
.build());
// 写入输入流内容到请求体
requestBody.writeInputStream(inputStream);
CompletedUpload uploadResult = upload.completionFuture().join();
etag = uploadResult.response().eTag();
} catch (IOException e) {
throw new BusinessException("文件上传异常", e);
}
}
@Override
public ThumbnailResp uploadThumbnail(String bucketName,
String fileName,
String path,
InputStream inputStream,
String fileType) {
// 获取文件扩展名
String fileExtension = FileNameUtil.extName(fileName);
// 生成缩略图文件名
String thumbnailFileName = StorageUtils.buildThumbnailFileName(fileName, StorageConstant.SMALL_SUFFIX);
// 处理文件为缩略图
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
ImageThumbnailUtils.generateThumbnail(inputStream, outputStream, fileExtension);
inputStream = new ByteArrayInputStream(outputStream.toByteArray());
// 上传文件
this.upload(bucketName, thumbnailFileName, path, inputStream, fileType);
return new ThumbnailResp((long)outputStream.size(), Paths.get(path, thumbnailFileName).toString());
} catch (IOException e) {
throw new BusinessException("缩略图处理异常", e);
}
}
@Override
public InputStream download(String bucketName, String fileName) {
try {
// 构建下载请求
DownloadRequest<ResponseInputStream<GetObjectResponse>> downloadRequest = DownloadRequest.builder()
.getObjectRequest(req -> req.bucket(bucketName).key(fileName).build()) // 设置桶名和对象名
.addTransferListener(LoggingTransferListener.create()) // 添加传输监听器
.responseTransformer(AsyncResponseTransformer.toBlockingInputStream()) // 转换为阻塞输入流
.build();
// 执行下载操作
Download<ResponseInputStream<GetObjectResponse>> download = client.getTransferManager()
.download(downloadRequest);
// 直接等待下载完成并返回 InputStream
// 返回输入流
return download.completionFuture().join().result();
} catch (CompletionException e) {
// 处理异步执行中的异常
throw new BusinessException("文件下载失败,错误信息: " + e.getCause().getMessage(), e.getCause());
} catch (Exception e) {
// 捕获其他异常
throw new BusinessException("文件下载失败,发生未知错误", e);
}
}
@Override
public void delete(String bucketName, String fileName) {
try {
client.getClient().deleteObject(DeleteObjectRequest.builder().bucket(bucketName).key(fileName).build());
} catch (Exception e) {
throw new BusinessException("S3 文件删除失败", e);
}
}
@Override
public String getImageBase64(String bucketName, String fileName) {
try (InputStream inputStream = download(bucketName, fileName)) {
if (ObjectUtil.isEmpty(inputStream)) {
return null;
}
String extName = FileUtil.extName(fileName);
boolean contains = FileTypeEnum.IMAGE.getExtensions().contains(extName);
CheckUtils.throwIf(!contains, "{}非图片格式,无法获取", extName);
return Base64.getEncoder().encodeToString(inputStream.readAllBytes());
} catch (Exception e) {
throw new BusinessException("图片查看失败", e);
}
}
/**
* 构建储存记录
*
* @param bucketName 桶名称
* @param fileName 文件名
* @param filePath 文件路径
* @param eTag e 标记
* @param contentLength 内容长度
* @param thumbnailResp 相应缩略图
* @return {@link UploadResp }
*/
private UploadResp buildStorageRecord(String bucketName,
String fileName,
String filePath,
String eTag,
long contentLength,
ThumbnailResp thumbnailResp) {
// 获取终端地址
String endpoint = client.getProperties().getEndpoint();
// 判断桶策略
boolean isPrivateBucket = this.isPrivate(bucketName);
// 如果是私有桶 则生成私有URL链接 默认 访问时间为 12 小时
String url = isPrivateBucket
? this.getPrivateUrl(bucketName, filePath, 12)
: OssUtils.getUrl(endpoint, bucketName) + StringConstants.SLASH + filePath;
String thumbnailUrl = "";
long thumbnailSize = 0;
// 判断缩略图响应是否为空
if (ObjectUtil.isNotEmpty(thumbnailResp)) {
// 同理按照 访问桶策略构建 缩略图访问地址
thumbnailUrl = isPrivateBucket
? this.getPrivateUrl(bucketName, thumbnailResp.getThumbnailPath(), 12)
: OssUtils.getUrl(endpoint, bucketName) + StringConstants.SLASH + thumbnailResp.getThumbnailPath();
thumbnailSize = thumbnailResp.getThumbnailSize();
}
UploadResp uploadResp = new UploadResp();
uploadResp.setCode(client.getProperties().getCode());
uploadResp.setUrl(url);
uploadResp.setBasePath(filePath);
uploadResp.setOriginalFilename(fileName);
uploadResp.setExt(FileNameUtil.extName(fileName));
uploadResp.setSize(contentLength);
uploadResp.setThumbnailUrl(thumbnailUrl);
uploadResp.setThumbnailSize(thumbnailSize);
uploadResp.seteTag(eTag);
uploadResp.setPath(Paths.get(bucketName, filePath).toString());
uploadResp.setBucketName(bucketName);
uploadResp.setCreateTime(LocalDateTime.now());
storageDao.add(uploadResp);
return uploadResp;
}
/**
* 是否为私有桶
*
* @param bucketName 桶名称
* @return boolean T 是 F 不是
*/
private boolean isPrivate(String bucketName) {
try {
// 尝试获取桶的策略
GetBucketPolicyResponse policyResponse = client.getClient()
.getBucketPolicy(GetBucketPolicyRequest.builder().bucket(bucketName).build())
.join();
//转成 json
String policy = policyResponse.policy();
JSONObject json = new JSONObject(policy);
// 为空则是私有
return ObjectUtil.isEmpty(json.get("Statement"));
} catch (Exception e) {
// 如果 getBucketPolicy 抛出异常,说明不是 MinIO 或不支持策略
log.warn("获取桶策略失败,可能是 MinIO异常信息: {}", e.getMessage());
}
try {
// 获取桶的 ACL 信息
GetBucketAclResponse aclResponse = client.getClient()
.getBucketAcl(GetBucketAclRequest.builder().bucket(bucketName).build())
.join();
List<Grant> grants = aclResponse.grants();
// 只存在 FULL_CONTROL 权限并且只有一个 Grant则认为是私有桶
if (grants.size() == 1 && grants.stream()
.anyMatch(grant -> grant.permission().equals(Permission.FULL_CONTROL))) {
return true;
}
// 如果存在其他权限 (READ 或 WRITE),认为是公开桶
return grants.stream()
.noneMatch(grant -> grant.permission().equals(Permission.READ) || grant.permission()
.equals(Permission.WRITE));
} catch (Exception e) {
// 如果 getBucketAcl 失败,可能是权限或连接问题
log.error("获取桶 ACL 失败: {}", e.getMessage());
return true; // 出现错误时,默认认为桶是私有的
}
}
/**
* 获取私有URL链接
*
* @param bucketName 桶名称
* @param fileName 文件名
* @param second 授权时间
* @return {@link String }
*/
private String getPrivateUrl(String bucketName, String fileName, Integer second) {
try {
return client.getPresigner()
.presignGetObject(GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofHours(second))
.getObjectRequest(GetObjectRequest.builder().bucket(bucketName).key(fileName).build())
.build())
.url()
.toString();
} catch (RuntimeException e) {
throw new BusinessException("获取私有链接异常", e);
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.util;
import cn.hutool.core.util.StrUtil;
import software.amazon.awssdk.regions.Region;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.storage.constant.StorageConstant;
/**
* OSS 工具
*
* @author echo
* @date 2024/12/17 13:48
*/
public class OssUtils {
public OssUtils() {
}
/**
* 获取作用域
* <p>如果 region 参数非空,使用 Region.of 方法创建对应的 S3 区域对象,否则返回默认区域</p>
*
* @param region 区域
* @return {@link Region }
*/
public static Region getRegion(String region) {
return StrUtil.isEmpty(region) ? Region.US_EAST_1 : Region.of(region);
}
/**
* 获取url
*
* @param endpoint 端点
* @param bucketName 桶名称
* @return {@link String }
*/
public static String getUrl(String endpoint, String bucketName) {
// 如果是云服务商,直接返回域名或终端点
if (StrUtil.containsAny(endpoint, StorageConstant.CLOUD_SERVICE_PREFIX)) {
return "http://" + bucketName + StringConstants.DOT + endpoint;
} else {
return "http://" + endpoint + StringConstants.SLASH + bucketName;
}
}
}

View File

@@ -0,0 +1 @@
top.continew.starter.storage.autoconfigure.OssStorageAutoconfigure