mirror of
https://github.com/continew-org/continew-starter.git
synced 2025-09-11 06:57:14 +08:00
refactor(storage): 新增存储模块 - 本地和 S3 两种存储模式
This commit is contained in:
@@ -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>
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1 @@
|
||||
top.continew.starter.storage.autoconfigure.OssStorageAutoconfigure
|
Reference in New Issue
Block a user