mirror of
				https://github.com/continew-org/continew-starter.git
				synced 2025-10-31 22:57:19 +08:00 
			
		
		
		
	refactor(storage): 新增存储模块 - 本地和 S3 两种存储模式
This commit is contained in:
		| @@ -70,6 +70,11 @@ | |||||||
|         <ttl.version>2.14.5</ttl.version> |         <ttl.version>2.14.5</ttl.version> | ||||||
|         <ip2region.version>3.2.12</ip2region.version> |         <ip2region.version>3.2.12</ip2region.version> | ||||||
|         <hutool.version>5.8.34</hutool.version> |         <hutool.version>5.8.34</hutool.version> | ||||||
|  |         <!--对象存储版本--> | ||||||
|  |         <s3.version>2.29.23</s3.version> | ||||||
|  |         <s3-crt.version>0.33.5</s3-crt.version> | ||||||
|  |         <!--缩略图处理版本--> | ||||||
|  |         <thumbnails.version>0.4.20</thumbnails.version> | ||||||
|         <!-- Maven Plugin Versions --> |         <!-- Maven Plugin Versions --> | ||||||
|         <flatten.version>1.6.0</flatten.version> |         <flatten.version>1.6.0</flatten.version> | ||||||
|         <spotless.version>2.43.0</spotless.version> |         <spotless.version>2.43.0</spotless.version> | ||||||
| @@ -253,18 +258,32 @@ | |||||||
|                 <version>${easy-excel.version}</version> |                 <version>${easy-excel.version}</version> | ||||||
|             </dependency> |             </dependency> | ||||||
|  |  | ||||||
|             <!-- X File Storage(一行代码将文件存储到本地、FTP、SFTP、WebDAV、阿里云 OSS、华为云 OBS...等其它兼容 S3 协议的存储平台) --> |             <!--  S3  for Java 2.x  --> | ||||||
|             <dependency> |             <dependency> | ||||||
|                 <groupId>org.dromara.x-file-storage</groupId> |                 <groupId>software.amazon.awssdk</groupId> | ||||||
|                 <artifactId>x-file-storage-spring</artifactId> |                 <artifactId>s3</artifactId> | ||||||
|                 <version>${x-file-storage.version}</version> |                 <version>${s3.version}</version> | ||||||
|             </dependency> |             </dependency> | ||||||
|  |  | ||||||
|             <!-- Amazon S3(Amazon Simple Storage Service,亚马逊简单存储服务,通用存储协议 S3,兼容主流云厂商对象存储) --> |             <!-- 使用AWS基于 CRT 的 S3 客户端 --> | ||||||
|             <dependency> |             <dependency> | ||||||
|                 <groupId>com.amazonaws</groupId> |                 <groupId>software.amazon.awssdk.crt</groupId> | ||||||
|                 <artifactId>aws-java-sdk-s3</artifactId> |                 <artifactId>aws-crt</artifactId> | ||||||
|                 <version>${aws-s3.version}</version> |                 <version>${s3-crt.version}</version> | ||||||
|  |             </dependency> | ||||||
|  |  | ||||||
|  |             <!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 --> | ||||||
|  |             <dependency> | ||||||
|  |                 <groupId>software.amazon.awssdk</groupId> | ||||||
|  |                 <artifactId>s3-transfer-manager</artifactId> | ||||||
|  |                 <version>${s3.version}</version> | ||||||
|  |             </dependency> | ||||||
|  |  | ||||||
|  |             <!--图片处理工具-主要用做图片缩略处理--> | ||||||
|  |             <dependency> | ||||||
|  |                 <groupId>net.coobird</groupId> | ||||||
|  |                 <artifactId>thumbnailator</artifactId> | ||||||
|  |                 <version>${thumbnails.version}</version> | ||||||
|             </dependency> |             </dependency> | ||||||
|  |  | ||||||
|             <!-- Graceful Response(一个Spring Boot技术栈下的优雅响应处理组件,可以帮助开发者完成响应数据封装、异常处理、错误码填充等过程,提高开发效率,提高代码质量) --> |             <!-- Graceful Response(一个Spring Boot技术栈下的优雅响应处理组件,可以帮助开发者完成响应数据封装、异常处理、错误码填充等过程,提高开发效率,提高代码质量) --> | ||||||
| @@ -476,6 +495,13 @@ | |||||||
|                 <version>${revision}</version> |                 <version>${revision}</version> | ||||||
|             </dependency> |             </dependency> | ||||||
|  |  | ||||||
|  |             <!-- 存储模块 - 核心模块 --> | ||||||
|  |             <dependency> | ||||||
|  |                 <groupId>top.continew</groupId> | ||||||
|  |                 <artifactId>continew-starter-storage-core</artifactId> | ||||||
|  |                 <version>${revision}</version> | ||||||
|  |             </dependency> | ||||||
|  |  | ||||||
|             <!-- 存储模块 - 本地存储 --> |             <!-- 存储模块 - 本地存储 --> | ||||||
|             <dependency> |             <dependency> | ||||||
|                 <groupId>top.continew</groupId> |                 <groupId>top.continew</groupId> | ||||||
| @@ -483,6 +509,13 @@ | |||||||
|                 <version>${revision}</version> |                 <version>${revision}</version> | ||||||
|             </dependency> |             </dependency> | ||||||
|  |  | ||||||
|  |             <!-- 存储模块 - 对象存储 --> | ||||||
|  |             <dependency> | ||||||
|  |                 <groupId>top.continew</groupId> | ||||||
|  |                 <artifactId>continew-starter-storage-oss</artifactId> | ||||||
|  |                 <version>${revision}</version> | ||||||
|  |             </dependency> | ||||||
|  |  | ||||||
|             <!-- 日志模块 - 基于拦截器实现(Spring Boot Actuator HttpTrace 增强版) --> |             <!-- 日志模块 - 基于拦截器实现(Spring Boot Actuator HttpTrace 增强版) --> | ||||||
|             <dependency> |             <dependency> | ||||||
|                 <groupId>top.continew</groupId> |                 <groupId>top.continew</groupId> | ||||||
|   | |||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | <?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-core</artifactId> | ||||||
|  |     <description>ContiNew Starter 存储模块 - 核心模块</description> | ||||||
|  |  | ||||||
|  |     <dependencies> | ||||||
|  |  | ||||||
|  |         <!--redisson 缓存模块--> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>top.continew</groupId> | ||||||
|  |             <artifactId>continew-starter-cache-redisson</artifactId> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|  |         <!--图片处理工具-主要用做图片缩略处理--> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>net.coobird</groupId> | ||||||
|  |             <artifactId>thumbnailator</artifactId> | ||||||
|  |         </dependency> | ||||||
|  |     </dependencies> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | </project> | ||||||
| @@ -0,0 +1,42 @@ | |||||||
|  | /* | ||||||
|  |  * 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.constant; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 存储 常量 | ||||||
|  |  * | ||||||
|  |  * @author echo | ||||||
|  |  * @date 2024/12/16 19:09 | ||||||
|  |  */ | ||||||
|  | public class StorageConstant { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 默认存储 Key | ||||||
|  |      */ | ||||||
|  |     public static final String DEFAULT_KEY = "storage:default_config"; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 云服务商 域名前缀 | ||||||
|  |      * <p>目前只支持 阿里云-oss 华为云-obs 腾讯云-cos</p> | ||||||
|  |      */ | ||||||
|  |     public static final String[] CLOUD_SERVICE_PREFIX = new String[] {"oss", "cos", "obs"}; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 缩略图后缀 | ||||||
|  |      */ | ||||||
|  |     public static final String SMALL_SUFFIX = "small"; | ||||||
|  | } | ||||||
| @@ -0,0 +1,35 @@ | |||||||
|  | /* | ||||||
|  |  * 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.dao; | ||||||
|  |  | ||||||
|  | import top.continew.starter.storage.model.resp.UploadResp; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 存储记录持久层接口 | ||||||
|  |  * | ||||||
|  |  * @author echo | ||||||
|  |  * @date 2024/12/17 16:49 | ||||||
|  |  */ | ||||||
|  | public interface StorageDao { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 记录上传信息 | ||||||
|  |      * | ||||||
|  |      * @param uploadResp 上传信息 | ||||||
|  |      */ | ||||||
|  |     void add(UploadResp uploadResp); | ||||||
|  | } | ||||||
| @@ -0,0 +1,33 @@ | |||||||
|  | /* | ||||||
|  |  * 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.dao.impl; | ||||||
|  |  | ||||||
|  | import top.continew.starter.storage.dao.StorageDao; | ||||||
|  | import top.continew.starter.storage.model.resp.UploadResp; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 默认记录实现,此类并不能真正保存记录,只是用来脱离数据库运行,保证文件上传功能可以正常使用 | ||||||
|  |  * | ||||||
|  |  * @author echo | ||||||
|  |  * @date 2024/12/18 08:48 | ||||||
|  |  **/ | ||||||
|  | public class StorageDaoDefaultImpl implements StorageDao { | ||||||
|  |     @Override | ||||||
|  |     public void add(UploadResp uploadResp) { | ||||||
|  |  | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,106 @@ | |||||||
|  | /* | ||||||
|  |  * 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.decorator; | ||||||
|  |  | ||||||
|  | import top.continew.starter.storage.model.resp.ThumbnailResp; | ||||||
|  | import top.continew.starter.storage.model.resp.UploadResp; | ||||||
|  | import top.continew.starter.storage.strategy.StorageStrategy; | ||||||
|  |  | ||||||
|  | import java.io.InputStream; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 装饰器基类 - 用于重写 | ||||||
|  |  * | ||||||
|  |  * @author echo | ||||||
|  |  * @date 2024/12/30 19:33 | ||||||
|  |  */ | ||||||
|  | public abstract class AbstractStorageDecorator<C> implements StorageStrategy<C> { | ||||||
|  |  | ||||||
|  |     protected StorageStrategy<C> delegate; | ||||||
|  |  | ||||||
|  |     protected AbstractStorageDecorator(StorageStrategy<C> delegate) { | ||||||
|  |         this.delegate = delegate; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public C getClient() { | ||||||
|  |         return delegate.getClient(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean bucketExists(String bucketName) { | ||||||
|  |         return delegate.bucketExists(bucketName); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void createBucket(String bucketName) { | ||||||
|  |         delegate.createBucket(bucketName); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public UploadResp upload(String fileName, InputStream inputStream, String fileType) { | ||||||
|  |         return delegate.upload(fileName, inputStream, fileType); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public UploadResp upload(String fileName, | ||||||
|  |                              String path, | ||||||
|  |                              InputStream inputStream, | ||||||
|  |                              String fileType, | ||||||
|  |                              boolean isThumbnail) { | ||||||
|  |         return delegate.upload(fileName, path, inputStream, fileType, isThumbnail); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public UploadResp upload(String bucketName, | ||||||
|  |                              String fileName, | ||||||
|  |                              String path, | ||||||
|  |                              InputStream inputStream, | ||||||
|  |                              String fileType, | ||||||
|  |                              boolean isThumbnail) { | ||||||
|  |         return delegate.upload(bucketName, fileName, path, inputStream, fileType, isThumbnail); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void upload(String bucketName, String fileName, String path, InputStream inputStream, String fileType) { | ||||||
|  |         delegate.upload(bucketName, fileName, path, inputStream, fileType); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public ThumbnailResp uploadThumbnail(String bucketName, | ||||||
|  |                                          String fileName, | ||||||
|  |                                          String path, | ||||||
|  |                                          InputStream inputStream, | ||||||
|  |                                          String fileType) { | ||||||
|  |         return delegate.uploadThumbnail(bucketName, fileName, path, inputStream, fileType); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public InputStream download(String bucketName, String fileName) { | ||||||
|  |         return delegate.download(bucketName, fileName); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void delete(String bucketName, String fileName) { | ||||||
|  |         delegate.delete(bucketName, fileName); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public String getImageBase64(String bucketName, String fileName) { | ||||||
|  |         return delegate.getImageBase64(bucketName, fileName); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,102 @@ | |||||||
|  | /* | ||||||
|  |  * 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.enums; | ||||||
|  |  | ||||||
|  | import cn.hutool.core.util.StrUtil; | ||||||
|  | import top.continew.starter.core.enums.BaseEnum; | ||||||
|  |  | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 文件类型枚举 | ||||||
|  |  * | ||||||
|  |  * @author Charles7c | ||||||
|  |  * @since 2023/12/23 13:38 | ||||||
|  |  */ | ||||||
|  | public enum FileTypeEnum implements BaseEnum<Integer> { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 其他 | ||||||
|  |      */ | ||||||
|  |     UNKNOWN(1, "其他", Collections.emptyList()), | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 图片 | ||||||
|  |      */ | ||||||
|  |     IMAGE(2, "图片", List | ||||||
|  |         .of("jpg", "jpeg", "png", "gif", "bmp", "webp", "ico", "psd", "tiff", "dwg", "jxr", "apng", "xcf")), | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 文档 | ||||||
|  |      */ | ||||||
|  |     DOC(3, "文档", List.of("txt", "pdf", "doc", "xls", "ppt", "docx", "xlsx", "pptx")), | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 视频 | ||||||
|  |      */ | ||||||
|  |     VIDEO(4, "视频", List.of("mp4", "avi", "mkv", "flv", "webm", "wmv", "m4v", "mov", "mpg", "rmvb", "3gp")), | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 音频 | ||||||
|  |      */ | ||||||
|  |     AUDIO(5, "音频", List.of("mp3", "flac", "wav", "ogg", "midi", "m4a", "aac", "amr", "ac3", "aiff")),; | ||||||
|  |  | ||||||
|  |     private final Integer value; | ||||||
|  |     private final String description; | ||||||
|  |     private final List<String> extensions; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 根据扩展名查询 | ||||||
|  |      * | ||||||
|  |      * @param extension 扩展名 | ||||||
|  |      * @return 文件类型 | ||||||
|  |      */ | ||||||
|  |     public static FileTypeEnum getByExtension(String extension) { | ||||||
|  |         return Arrays.stream(FileTypeEnum.values()) | ||||||
|  |             .filter(t -> t.getExtensions().contains(StrUtil.emptyIfNull(extension).toLowerCase())) | ||||||
|  |             .findFirst() | ||||||
|  |             .orElse(FileTypeEnum.UNKNOWN); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     FileTypeEnum(Integer value, String description, List<String> extensions) { | ||||||
|  |         this.value = value; | ||||||
|  |         this.description = description; | ||||||
|  |         this.extensions = extensions; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public List<String> getExtensions() { | ||||||
|  |         return this.extensions; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public Integer getValue() { | ||||||
|  |         return this.value; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public String getDescription() { | ||||||
|  |         return this.description; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public String getColor() { | ||||||
|  |         return BaseEnum.super.getColor(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,80 @@ | |||||||
|  | /* | ||||||
|  |  * 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.manger; | ||||||
|  |  | ||||||
|  | import top.continew.starter.cache.redisson.util.RedisUtils; | ||||||
|  | import top.continew.starter.core.validation.ValidationUtils; | ||||||
|  | import top.continew.starter.storage.constant.StorageConstant; | ||||||
|  | import top.continew.starter.storage.strategy.StorageStrategy; | ||||||
|  |  | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.concurrent.ConcurrentHashMap; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 存储策略管理器 | ||||||
|  |  * | ||||||
|  |  * @author echo | ||||||
|  |  * @date 2024/12/16 | ||||||
|  |  */ | ||||||
|  | public class StorageManager { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 存储策略连接信息 | ||||||
|  |      */ | ||||||
|  |     private static final Map<String, StorageStrategy<?>> STORAGE_STRATEGY = new ConcurrentHashMap<>(); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 加载存储策略 | ||||||
|  |      * | ||||||
|  |      * @param code     存储码 | ||||||
|  |      * @param strategy 对应存储策略 | ||||||
|  |      */ | ||||||
|  |     public static void load(String code, StorageStrategy<?> strategy) { | ||||||
|  |         STORAGE_STRATEGY.put(code, strategy); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 卸载存储策略 | ||||||
|  |      * | ||||||
|  |      * @param code 存储码 | ||||||
|  |      */ | ||||||
|  |     public static void unload(String code) { | ||||||
|  |         STORAGE_STRATEGY.remove(code); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 根据 存储 code 获取对应存储策略 | ||||||
|  |      * | ||||||
|  |      * @param code 代码 | ||||||
|  |      * @return {@link StorageStrategy } | ||||||
|  |      */ | ||||||
|  |     public static StorageStrategy<?> instance(String code) { | ||||||
|  |         StorageStrategy<?> strategy = STORAGE_STRATEGY.get(code); | ||||||
|  |         ValidationUtils.throwIfEmpty(strategy, "未找到存储配置:" + code); | ||||||
|  |         return strategy; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 获取默认存储策略 | ||||||
|  |      * | ||||||
|  |      * @return {@link StorageStrategy } | ||||||
|  |      */ | ||||||
|  |     public static StorageStrategy<?> instance() { | ||||||
|  |         return instance(RedisUtils.get(StorageConstant.DEFAULT_KEY)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,152 @@ | |||||||
|  | /* | ||||||
|  |  * 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.model.req; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 存储配置信息 | ||||||
|  |  * | ||||||
|  |  * @author echo | ||||||
|  |  * @date 2024/11/04 15:13 | ||||||
|  |  **/ | ||||||
|  | public class StorageProperties { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 编码 | ||||||
|  |      */ | ||||||
|  |     private String code; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 访问密钥 | ||||||
|  |      */ | ||||||
|  |     private String accessKey; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 私有密钥 | ||||||
|  |      */ | ||||||
|  |     private String secretKey; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 终端节点 | ||||||
|  |      */ | ||||||
|  |     private String endpoint; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 桶名称 | ||||||
|  |      */ | ||||||
|  |     private String bucketName; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 域名 | ||||||
|  |      */ | ||||||
|  |     private String domain; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 作用域 | ||||||
|  |      */ | ||||||
|  |     private String region; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 是否是默认存储 | ||||||
|  |      */ | ||||||
|  |     private Boolean isDefault; | ||||||
|  |  | ||||||
|  |     public StorageProperties() { | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getCode() { | ||||||
|  |         return code; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setCode(String code) { | ||||||
|  |         this.code = code; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getAccessKey() { | ||||||
|  |         return accessKey; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setAccessKey(String accessKey) { | ||||||
|  |         this.accessKey = accessKey; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getSecretKey() { | ||||||
|  |         return secretKey; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setSecretKey(String secretKey) { | ||||||
|  |         this.secretKey = secretKey; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getEndpoint() { | ||||||
|  |         return endpoint; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setEndpoint(String endpoint) { | ||||||
|  |         this.endpoint = endpoint; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getBucketName() { | ||||||
|  |         return bucketName; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setBucketName(String bucketName) { | ||||||
|  |         this.bucketName = bucketName; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getDomain() { | ||||||
|  |         return domain; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setDomain(String domain) { | ||||||
|  |         this.domain = domain; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getRegion() { | ||||||
|  |         return region; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setRegion(String region) { | ||||||
|  |         this.region = region; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public Boolean getIsDefault() { | ||||||
|  |         return isDefault; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setIsDefault(Boolean isDefault) { | ||||||
|  |         this.isDefault = isDefault; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public StorageProperties(String code, | ||||||
|  |                              String accessKey, | ||||||
|  |                              String secretKey, | ||||||
|  |                              String endpoint, | ||||||
|  |                              String bucketName, | ||||||
|  |                              String domain, | ||||||
|  |                              String region, | ||||||
|  |                              Boolean isDefault) { | ||||||
|  |         this.code = code; | ||||||
|  |         this.accessKey = accessKey; | ||||||
|  |         this.secretKey = secretKey; | ||||||
|  |         this.endpoint = endpoint; | ||||||
|  |         this.bucketName = bucketName; | ||||||
|  |         this.domain = domain; | ||||||
|  |         this.region = region; | ||||||
|  |         this.isDefault = isDefault; | ||||||
|  |  | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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.model.resp; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 缩略图 | ||||||
|  |  * | ||||||
|  |  * @author echo | ||||||
|  |  * @date 2024/12/20 17:00 | ||||||
|  |  */ | ||||||
|  | public class ThumbnailResp { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 缩略图大小(字节) | ||||||
|  |      */ | ||||||
|  |     private Long thumbnailSize; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 缩略图地址 格式 xxx/xxx/xxx.small.jpg | ||||||
|  |      */ | ||||||
|  |     private String thumbnailPath; | ||||||
|  |  | ||||||
|  |     public ThumbnailResp() { | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public ThumbnailResp(Long thumbnailSize, String thumbnailPath) { | ||||||
|  |         this.thumbnailSize = thumbnailSize; | ||||||
|  |         this.thumbnailPath = thumbnailPath; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public Long getThumbnailSize() { | ||||||
|  |         return thumbnailSize; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setThumbnailSize(Long thumbnailSize) { | ||||||
|  |         this.thumbnailSize = thumbnailSize; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getThumbnailPath() { | ||||||
|  |         return thumbnailPath; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setThumbnailPath(String thumbnailPath) { | ||||||
|  |         this.thumbnailPath = thumbnailPath; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,215 @@ | |||||||
|  | /* | ||||||
|  |  * 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.model.resp; | ||||||
|  |  | ||||||
|  | import java.time.LocalDateTime; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 上传结果 | ||||||
|  |  * | ||||||
|  |  * @author echo | ||||||
|  |  * @date 2024/12/10 | ||||||
|  |  */ | ||||||
|  | public class UploadResp { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 存储 code | ||||||
|  |      */ | ||||||
|  |     private String code; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 访问地址 | ||||||
|  |      * <p>如果桶为私有,则提供临时链接,时间默认为 12 小时</p> | ||||||
|  |      */ | ||||||
|  |     private String url; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 文件基础路径 | ||||||
|  |      */ | ||||||
|  |     private String basePath; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 原始 文件名 | ||||||
|  |      */ | ||||||
|  |     private String originalFilename; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 扩展名 | ||||||
|  |      */ | ||||||
|  |     private String ext; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 文件大小(字节) | ||||||
|  |      */ | ||||||
|  |     private long size; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 已上传对象的实体标记(用来校验文件)-S3 | ||||||
|  |      */ | ||||||
|  |     private String eTag; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 存储路径 | ||||||
|  |      * <p></p> 格式 桶/文件名 continew/2024/12/24/1234.jpg | ||||||
|  |      */ | ||||||
|  |     private String path; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 存储桶 | ||||||
|  |      */ | ||||||
|  |     private String bucketName; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 缩略图大小(字节) | ||||||
|  |      */ | ||||||
|  |     private Long thumbnailSize; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 缩略图URL | ||||||
|  |      */ | ||||||
|  |     private String thumbnailUrl; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 上传时间 | ||||||
|  |      */ | ||||||
|  |     private LocalDateTime createTime; | ||||||
|  |  | ||||||
|  |     public UploadResp() { | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public UploadResp(String code, | ||||||
|  |                       String url, | ||||||
|  |                       String basePath, | ||||||
|  |                       String originalFilename, | ||||||
|  |                       String ext, | ||||||
|  |                       long size, | ||||||
|  |                       String eTag, | ||||||
|  |                       String path, | ||||||
|  |                       String bucketName, | ||||||
|  |                       Long thumbnailSize, | ||||||
|  |                       String thumbnailUrl, | ||||||
|  |                       LocalDateTime createTime) { | ||||||
|  |         this.code = code; | ||||||
|  |         this.url = url; | ||||||
|  |         this.basePath = basePath; | ||||||
|  |         this.originalFilename = originalFilename; | ||||||
|  |         this.ext = ext; | ||||||
|  |         this.size = size; | ||||||
|  |         this.eTag = eTag; | ||||||
|  |         this.path = path; | ||||||
|  |         this.bucketName = bucketName; | ||||||
|  |         this.thumbnailSize = thumbnailSize; | ||||||
|  |         this.thumbnailUrl = thumbnailUrl; | ||||||
|  |         this.createTime = createTime; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getCode() { | ||||||
|  |         return code; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setCode(String code) { | ||||||
|  |         this.code = code; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getUrl() { | ||||||
|  |         return url; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setUrl(String url) { | ||||||
|  |         this.url = url; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getBasePath() { | ||||||
|  |         return basePath; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setBasePath(String basePath) { | ||||||
|  |         this.basePath = basePath; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getOriginalFilename() { | ||||||
|  |         return originalFilename; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setOriginalFilename(String originalFilename) { | ||||||
|  |         this.originalFilename = originalFilename; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getExt() { | ||||||
|  |         return ext; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setExt(String ext) { | ||||||
|  |         this.ext = ext; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public long getSize() { | ||||||
|  |         return size; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setSize(long size) { | ||||||
|  |         this.size = size; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String geteTag() { | ||||||
|  |         return eTag; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void seteTag(String eTag) { | ||||||
|  |         this.eTag = eTag; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getPath() { | ||||||
|  |         return path; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setPath(String path) { | ||||||
|  |         this.path = path; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getBucketName() { | ||||||
|  |         return bucketName; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setBucketName(String bucketName) { | ||||||
|  |         this.bucketName = bucketName; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public Long getThumbnailSize() { | ||||||
|  |         return thumbnailSize; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setThumbnailSize(Long thumbnailSize) { | ||||||
|  |         this.thumbnailSize = thumbnailSize; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getThumbnailUrl() { | ||||||
|  |         return thumbnailUrl; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setThumbnailUrl(String thumbnailUrl) { | ||||||
|  |         this.thumbnailUrl = thumbnailUrl; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public LocalDateTime getCreateTime() { | ||||||
|  |         return createTime; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setCreateTime(LocalDateTime createTime) { | ||||||
|  |         this.createTime = createTime; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,151 @@ | |||||||
|  | /* | ||||||
|  |  * 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 top.continew.starter.storage.model.resp.ThumbnailResp; | ||||||
|  | import top.continew.starter.storage.model.resp.UploadResp; | ||||||
|  |  | ||||||
|  | import java.io.InputStream; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 存储策略接口 | ||||||
|  |  * | ||||||
|  |  * @author echo | ||||||
|  |  * @date 2024/12/16 11:19 | ||||||
|  |  */ | ||||||
|  | public interface StorageStrategy<C> { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 获得客户端 - 用于重写时 获取对应存储 code 客户端 | ||||||
|  |      * | ||||||
|  |      * @return {@link Object } | ||||||
|  |      */ | ||||||
|  |     C getClient(); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 检查桶是否存在 | ||||||
|  |      * <p> S3: 检查桶是否存在 </p> | ||||||
|  |      * <p>local: 检查 默认路径 是否存在 </p> | ||||||
|  |      * | ||||||
|  |      * @param bucketName 桶名称 | ||||||
|  |      * @return true 存在 false 不存在 | ||||||
|  |      */ | ||||||
|  |     boolean bucketExists(String bucketName); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 创建桶 | ||||||
|  |      * <p> S3: 创建桶 </p> | ||||||
|  |      * <p> local: 创建 默认路径下 指定文件夹 </p> | ||||||
|  |      * | ||||||
|  |      * @param bucketName 桶名称 | ||||||
|  |      */ | ||||||
|  |     void createBucket(String bucketName); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 上传文件 - 默认桶 | ||||||
|  |      * | ||||||
|  |      * @param fileName    文件名 | ||||||
|  |      * @param inputStream 输入流 | ||||||
|  |      * @param fileType    文件类型 | ||||||
|  |      * @return 上传响应 | ||||||
|  |      */ | ||||||
|  |     UploadResp upload(String fileName, InputStream inputStream, String fileType); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 上传文件 - 默认桶 | ||||||
|  |      * | ||||||
|  |      * @param fileName    文件名 | ||||||
|  |      * @param path        路径 | ||||||
|  |      * @param inputStream 输入流 | ||||||
|  |      * @param fileType    文件类型 | ||||||
|  |      * @param isThumbnail 是缩略图 | ||||||
|  |      * @return {@link UploadResp } | ||||||
|  |      */ | ||||||
|  |     UploadResp upload(String fileName, String path, InputStream inputStream, String fileType, boolean isThumbnail); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 上传文件 | ||||||
|  |      * | ||||||
|  |      * @param bucketName  桶名称 | ||||||
|  |      * @param fileName    文件名 | ||||||
|  |      * @param path        路径 | ||||||
|  |      * @param inputStream 输入流 | ||||||
|  |      * @param fileType    文件类型 | ||||||
|  |      * @param isThumbnail 是缩略图 | ||||||
|  |      * @return 上传响应 | ||||||
|  |      */ | ||||||
|  |     UploadResp upload(String bucketName, | ||||||
|  |                       String fileName, | ||||||
|  |                       String path, | ||||||
|  |                       InputStream inputStream, | ||||||
|  |                       String fileType, | ||||||
|  |                       boolean isThumbnail); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 文件上传-基础上传 | ||||||
|  |      * | ||||||
|  |      * @param bucketName  桶名称 - 基础上传不做处理 | ||||||
|  |      * @param fileName    文件名 - 基础上传不做处理 | ||||||
|  |      * @param path        路径 - 基础上传不做处理 | ||||||
|  |      * @param inputStream 输入流 | ||||||
|  |      * @param fileType    文件类型 | ||||||
|  |      * @return {@link UploadResp } | ||||||
|  |      */ | ||||||
|  |     void upload(String bucketName, String fileName, String path, InputStream inputStream, String fileType); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 上传缩略图 | ||||||
|  |      * | ||||||
|  |      * @param bucketName  桶名称 | ||||||
|  |      * @param fileName    文件名 | ||||||
|  |      * @param inputStream 输入流 | ||||||
|  |      * @param fileType    文件类型 | ||||||
|  |      * @return {@link UploadResp } | ||||||
|  |      */ | ||||||
|  |     ThumbnailResp uploadThumbnail(String bucketName, | ||||||
|  |                                   String fileName, | ||||||
|  |                                   String path, | ||||||
|  |                                   InputStream inputStream, | ||||||
|  |                                   String fileType); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 下载文件 | ||||||
|  |      * | ||||||
|  |      * @param bucketName 桶名称 | ||||||
|  |      * @param fileName   文件名 | ||||||
|  |      * @return 文件输入流 | ||||||
|  |      */ | ||||||
|  |     InputStream download(String bucketName, String fileName); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 删除文件 | ||||||
|  |      * | ||||||
|  |      * @param bucketName 桶名称 | ||||||
|  |      * @param fileName   文件名 | ||||||
|  |      */ | ||||||
|  |     void delete(String bucketName, String fileName); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 获取图像Base64 | ||||||
|  |      * | ||||||
|  |      * @param bucketName 桶名称 | ||||||
|  |      * @param fileName   文件名 | ||||||
|  |      * @return Base64编码的图像 | ||||||
|  |      */ | ||||||
|  |     String getImageBase64(String bucketName, String fileName); | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,81 @@ | |||||||
|  | /* | ||||||
|  |  * 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 javax.imageio.ImageIO; | ||||||
|  | import java.awt.*; | ||||||
|  | import java.awt.image.BufferedImage; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.io.InputStream; | ||||||
|  | import java.io.OutputStream; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 图像缩略图工具 | ||||||
|  |  * | ||||||
|  |  * @author echo | ||||||
|  |  * @date 2024/12/20 16:49 | ||||||
|  |  */ | ||||||
|  | public class ImageThumbnailUtils { | ||||||
|  |  | ||||||
|  |     // 默认缩略图尺寸:100x100 | ||||||
|  |     private static final int DEFAULT_WIDTH = 100; | ||||||
|  |     private static final int DEFAULT_HEIGHT = 100; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 根据输入流生成默认大小(100x100)的缩略图并写入输出流 | ||||||
|  |      * | ||||||
|  |      * @param inputStream  原始图片的输入流 | ||||||
|  |      * @param outputStream 缩略图输出流 | ||||||
|  |      * @param suffix       后缀 | ||||||
|  |      * @throws IOException IOException | ||||||
|  |      */ | ||||||
|  |     public static void generateThumbnail(InputStream inputStream, | ||||||
|  |                                          OutputStream outputStream, | ||||||
|  |                                          String suffix) throws IOException { | ||||||
|  |         generateThumbnail(inputStream, outputStream, DEFAULT_WIDTH, DEFAULT_HEIGHT, suffix); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 根据输入流和自定义尺寸生成缩略图并写入输出流 | ||||||
|  |      * | ||||||
|  |      * @param inputStream  原始图片的输入流 | ||||||
|  |      * @param outputStream 缩略图输出流 | ||||||
|  |      * @param width        缩略图宽度 | ||||||
|  |      * @param height       缩略图高度 | ||||||
|  |      * @param suffix       后缀 | ||||||
|  |      * @throws IOException IOException | ||||||
|  |      */ | ||||||
|  |     public static void generateThumbnail(InputStream inputStream, | ||||||
|  |                                          OutputStream outputStream, | ||||||
|  |                                          int width, | ||||||
|  |                                          int height, | ||||||
|  |                                          String suffix) throws IOException { | ||||||
|  |         // 读取原始图片 | ||||||
|  |         BufferedImage originalImage = ImageIO.read(inputStream); | ||||||
|  |  | ||||||
|  |         // 调整图片大小 | ||||||
|  |         Image tmp = originalImage.getScaledInstance(width, height, Image.SCALE_SMOOTH); | ||||||
|  |         BufferedImage thumbnail = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); | ||||||
|  |  | ||||||
|  |         // 画出缩略图 | ||||||
|  |         Graphics2D g2d = thumbnail.createGraphics(); | ||||||
|  |         g2d.drawImage(tmp, 0, 0, null); | ||||||
|  |         g2d.dispose(); | ||||||
|  |         // 写入输出流 | ||||||
|  |         ImageIO.write(thumbnail, suffix, outputStream); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,128 @@ | |||||||
|  | /* | ||||||
|  |  * 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.io.FileUtil; | ||||||
|  | import cn.hutool.core.io.IoUtil; | ||||||
|  | import cn.hutool.core.io.file.FileNameUtil; | ||||||
|  | import cn.hutool.core.util.StrUtil; | ||||||
|  | import top.continew.starter.core.constant.StringConstants; | ||||||
|  |  | ||||||
|  | import java.io.ByteArrayInputStream; | ||||||
|  | import java.io.InputStream; | ||||||
|  | import java.net.URI; | ||||||
|  | import java.nio.file.Paths; | ||||||
|  | import java.time.LocalDate; | ||||||
|  | import java.time.LocalDateTime; | ||||||
|  | import java.time.format.DateTimeFormatter; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 储存工具 | ||||||
|  |  * | ||||||
|  |  * @author echo | ||||||
|  |  * @date 2024/12/16 19:55 | ||||||
|  |  */ | ||||||
|  | public class StorageUtils { | ||||||
|  |     public StorageUtils() { | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 格式文件名 | ||||||
|  |      * | ||||||
|  |      * @param fileName 文件名 | ||||||
|  |      * @return {@link String } | ||||||
|  |      */ | ||||||
|  |     public static String formatFileName(String fileName) { | ||||||
|  |         // 获取文件后缀名 | ||||||
|  |         String suffix = FileUtil.extName(fileName); | ||||||
|  |         // 获取当前时间的年月日时分秒格式 | ||||||
|  |         DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); | ||||||
|  |         String datetime = LocalDateTime.now().format(formatter); | ||||||
|  |         // 获取当前时间戳 | ||||||
|  |         String timestamp = String.valueOf(System.currentTimeMillis()); | ||||||
|  |         // 生成新的文件名 | ||||||
|  |         return datetime + timestamp + "." + suffix; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 默认文件目录 | ||||||
|  |      * | ||||||
|  |      * @param fileName 文件名 | ||||||
|  |      * @return {@link String } | ||||||
|  |      */ | ||||||
|  |     public static String defaultFileDir(String fileName) { | ||||||
|  |         LocalDate today = LocalDate.now(); | ||||||
|  |         return Paths.get(String.valueOf(today.getYear()), String.valueOf(today.getMonthValue()), String.valueOf(today | ||||||
|  |             .getDayOfMonth()), fileName).toString(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 默认路径地址 格式 2024/03/10/ | ||||||
|  |      * | ||||||
|  |      * @return {@link String } | ||||||
|  |      */ | ||||||
|  |     public static String defaultPath() { | ||||||
|  |         LocalDate today = LocalDate.now(); | ||||||
|  |         return Paths.get(String.valueOf(today.getYear()), String.valueOf(today.getMonthValue()), String.valueOf(today | ||||||
|  |             .getDayOfMonth())) + StringConstants.SLASH; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 根据 endpoint 判断是否带有 http 或 https,如果没有则加上 http 前缀。 | ||||||
|  |      * | ||||||
|  |      * @param endpoint 输入的 endpoint 字符串 | ||||||
|  |      * @return URI 对象 | ||||||
|  |      */ | ||||||
|  |     public static URI createUriWithProtocol(String endpoint) { | ||||||
|  |         // 判断 endpoint 是否包含 http:// 或 https:// 前缀 | ||||||
|  |         if (!endpoint.startsWith("http://") && !endpoint.startsWith("https://")) { | ||||||
|  |             // 如果没有协议前缀,则加上 http:// | ||||||
|  |             endpoint = "http://" + endpoint; | ||||||
|  |         } | ||||||
|  |         // 返回 URI 对象 | ||||||
|  |         return URI.create(endpoint); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 生成缩略图文件名 | ||||||
|  |      * | ||||||
|  |      * @param fileName 文件名 | ||||||
|  |      * @param suffix   后缀 | ||||||
|  |      * @return {@link String } | ||||||
|  |      */ | ||||||
|  |     public static String buildThumbnailFileName(String fileName, String suffix) { | ||||||
|  |         // 获取文件的扩展名 | ||||||
|  |         String extName = FileNameUtil.extName(fileName); | ||||||
|  |         // 去掉扩展名 | ||||||
|  |         String baseName = StrUtil.subBefore(fileName, StringConstants.DOT, true); | ||||||
|  |         // 拼接新的路径:原始路径 + .缩略图后缀 + .扩展名 | ||||||
|  |         return baseName + "." + suffix + "." + extName; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 可重复读流 | ||||||
|  |      * | ||||||
|  |      * @param inputStream 输入流 | ||||||
|  |      * @return {@link InputStream } | ||||||
|  |      */ | ||||||
|  |     public static InputStream ensureByteArrayStream(InputStream inputStream) { | ||||||
|  |         return (inputStream instanceof ByteArrayInputStream) | ||||||
|  |             ? inputStream | ||||||
|  |             : new ByteArrayInputStream(IoUtil.readBytes(inputStream)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -13,10 +13,10 @@ | |||||||
|     <description>ContiNew Starter 存储模块 - 本地存储</description> |     <description>ContiNew Starter 存储模块 - 本地存储</description> | ||||||
|  |  | ||||||
|     <dependencies> |     <dependencies> | ||||||
|         <!-- Spring Web MVC 模块 --> |         <!--存储 - 核心模块--> | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>org.springframework</groupId> |             <groupId>top.continew</groupId> | ||||||
|             <artifactId>spring-webmvc</artifactId> |             <artifactId>continew-starter-storage-core</artifactId> | ||||||
|         </dependency> |         </dependency> | ||||||
|     </dependencies> |     </dependencies> | ||||||
| </project> | </project> | ||||||
| @@ -0,0 +1,39 @@ | |||||||
|  | /* | ||||||
|  |  * 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 LocalStorageAutoconfigure { | ||||||
|  |  | ||||||
|  |     @Bean | ||||||
|  |     @ConditionalOnMissingBean | ||||||
|  |     public StorageDao storageDao() { | ||||||
|  |         return new StorageDaoDefaultImpl(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,85 @@ | |||||||
|  | /* | ||||||
|  |  * 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 top.continew.starter.storage.model.req.StorageProperties; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.nio.file.Files; | ||||||
|  | import java.nio.file.Path; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 本地客户端 | ||||||
|  |  * | ||||||
|  |  * @author echo | ||||||
|  |  * @date 2024/12/16 19:37 | ||||||
|  |  */ | ||||||
|  | public class LocalClient { | ||||||
|  |     private static final Logger log = LoggerFactory.getLogger(LocalClient.class); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 配置属性 | ||||||
|  |      */ | ||||||
|  |     private final StorageProperties properties; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 构造函数 | ||||||
|  |      * | ||||||
|  |      * @param properties 配置属性 | ||||||
|  |      */ | ||||||
|  |     public LocalClient(StorageProperties properties) { | ||||||
|  |         this.properties = properties; | ||||||
|  |         // 判断是否是默认存储,若不存在桶目录,则创建 | ||||||
|  |         if (Boolean.TRUE.equals(properties.getIsDefault())) { | ||||||
|  |             String bucketName = properties.getBucketName(); | ||||||
|  |             if (bucketName != null && !bucketName.isEmpty()) { | ||||||
|  |                 createBucketDirectory(bucketName); | ||||||
|  |             } else { | ||||||
|  |                 log.info("默认存储-存储桶已存在 => {}", bucketName); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         log.info("加载 Local 存储 => {}", properties.getCode()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 获取属性 | ||||||
|  |      * | ||||||
|  |      * @return {@link StorageProperties } | ||||||
|  |      */ | ||||||
|  |     public StorageProperties getProperties() { | ||||||
|  |         return properties; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 创建桶目录 | ||||||
|  |      * | ||||||
|  |      * @param bucketName 桶名称 | ||||||
|  |      */ | ||||||
|  |     private void createBucketDirectory(String bucketName) { | ||||||
|  |         Path bucketPath = Path.of(bucketName); | ||||||
|  |         try { | ||||||
|  |             if (Files.notExists(bucketPath)) { | ||||||
|  |                 Files.createDirectories(bucketPath); | ||||||
|  |                 log.info("默认存储-存储桶创建成功 : {}", bucketPath.toAbsolutePath()); | ||||||
|  |             } | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             log.error("创建默认存储-存储桶失败 => 路径: {}", bucketPath.toAbsolutePath(), e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,79 +0,0 @@ | |||||||
| /* |  | ||||||
|  * 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.local.autoconfigure; |  | ||||||
|  |  | ||||||
| import cn.hutool.core.text.CharSequenceUtil; |  | ||||||
| import jakarta.annotation.PostConstruct; |  | ||||||
| import org.slf4j.Logger; |  | ||||||
| import org.slf4j.LoggerFactory; |  | ||||||
| import org.springframework.boot.autoconfigure.AutoConfiguration; |  | ||||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |  | ||||||
| import org.springframework.boot.context.properties.EnableConfigurationProperties; |  | ||||||
| import org.springframework.web.servlet.config.annotation.EnableWebMvc; |  | ||||||
| import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; |  | ||||||
| import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; |  | ||||||
| import top.continew.starter.core.constant.PropertiesConstants; |  | ||||||
| import top.continew.starter.core.constant.StringConstants; |  | ||||||
|  |  | ||||||
| import java.util.Map; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * 本地文件自动配置 |  | ||||||
|  * |  | ||||||
|  * @author Charles7c |  | ||||||
|  * @since 1.1.0 |  | ||||||
|  */ |  | ||||||
| @EnableWebMvc |  | ||||||
| @AutoConfiguration |  | ||||||
| @EnableConfigurationProperties(LocalStorageProperties.class) |  | ||||||
| @ConditionalOnProperty(prefix = PropertiesConstants.STORAGE_LOCAL, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true) |  | ||||||
| public class LocalStorageAutoConfiguration implements WebMvcConfigurer { |  | ||||||
|  |  | ||||||
|     private static final Logger log = LoggerFactory.getLogger(LocalStorageAutoConfiguration.class); |  | ||||||
|     private final LocalStorageProperties properties; |  | ||||||
|  |  | ||||||
|     public LocalStorageAutoConfiguration(LocalStorageProperties properties) { |  | ||||||
|         this.properties = properties; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void addResourceHandlers(ResourceHandlerRegistry registry) { |  | ||||||
|         Map<String, LocalStorageProperties.LocalStorageMapping> mappingMap = properties.getMapping(); |  | ||||||
|         for (Map.Entry<String, LocalStorageProperties.LocalStorageMapping> mappingEntry : mappingMap.entrySet()) { |  | ||||||
|             LocalStorageProperties.LocalStorageMapping mapping = mappingEntry.getValue(); |  | ||||||
|             String pathPattern = mapping.getPathPattern(); |  | ||||||
|             String location = mapping.getLocation(); |  | ||||||
|             if (CharSequenceUtil.isBlank(location)) { |  | ||||||
|                 throw new IllegalArgumentException("Path pattern [%s] location is null.".formatted(pathPattern)); |  | ||||||
|             } |  | ||||||
|             registry.addResourceHandler(CharSequenceUtil.appendIfMissing(pathPattern, StringConstants.PATH_PATTERN)) |  | ||||||
|                 .addResourceLocations(!location.startsWith("file:") |  | ||||||
|                     ? "file:%s".formatted(this.format(location)) |  | ||||||
|                     : this.format(location)) |  | ||||||
|                 .setCachePeriod(0); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private String format(String location) { |  | ||||||
|         return location.replace(StringConstants.BACKSLASH, StringConstants.SLASH); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @PostConstruct |  | ||||||
|     public void postConstruct() { |  | ||||||
|         log.debug("[ContiNew Starter] - Auto Configuration 'Storage-Local' completed initialization."); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,105 +0,0 @@ | |||||||
| /* |  | ||||||
|  * 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.local.autoconfigure; |  | ||||||
|  |  | ||||||
| import org.springframework.boot.context.properties.ConfigurationProperties; |  | ||||||
| import org.springframework.util.unit.DataSize; |  | ||||||
| import top.continew.starter.core.constant.PropertiesConstants; |  | ||||||
|  |  | ||||||
| import java.util.HashMap; |  | ||||||
| import java.util.Map; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * 本地存储配置属性 |  | ||||||
|  * |  | ||||||
|  * @author Charles7c |  | ||||||
|  * @since 1.1.0 |  | ||||||
|  */ |  | ||||||
| @ConfigurationProperties(PropertiesConstants.STORAGE_LOCAL) |  | ||||||
| public class LocalStorageProperties { |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 是否启用本地存储 |  | ||||||
|      */ |  | ||||||
|     private boolean enabled = true; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 存储映射 |  | ||||||
|      */ |  | ||||||
|     private Map<String, LocalStorageMapping> mapping = new HashMap<>(); |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 本地存储映射 |  | ||||||
|      */ |  | ||||||
|     public static class LocalStorageMapping { |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * 路径模式 |  | ||||||
|          */ |  | ||||||
|         private String pathPattern; |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * 资源路径 |  | ||||||
|          */ |  | ||||||
|         private String location; |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * 单文件上传大小限制 |  | ||||||
|          */ |  | ||||||
|         private DataSize maxFileSize = DataSize.ofMegabytes(1); |  | ||||||
|  |  | ||||||
|         public String getPathPattern() { |  | ||||||
|             return pathPattern; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         public void setPathPattern(String pathPattern) { |  | ||||||
|             this.pathPattern = pathPattern; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         public String getLocation() { |  | ||||||
|             return location; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         public void setLocation(String location) { |  | ||||||
|             this.location = location; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         public DataSize getMaxFileSize() { |  | ||||||
|             return maxFileSize; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         public void setMaxFileSize(DataSize maxFileSize) { |  | ||||||
|             this.maxFileSize = maxFileSize; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public boolean isEnabled() { |  | ||||||
|         return enabled; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void setEnabled(boolean enabled) { |  | ||||||
|         this.enabled = enabled; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public Map<String, LocalStorageMapping> getMapping() { |  | ||||||
|         return mapping; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void setMapping(Map<String, LocalStorageMapping> mapping) { |  | ||||||
|         this.mapping = mapping; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,275 @@ | |||||||
|  | /* | ||||||
|  |  * 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 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.LocalClient; | ||||||
|  | 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.LocalUtils; | ||||||
|  | import top.continew.starter.storage.util.StorageUtils; | ||||||
|  |  | ||||||
|  | import java.io.*; | ||||||
|  | import java.nio.file.Files; | ||||||
|  | import java.nio.file.Path; | ||||||
|  | import java.nio.file.Paths; | ||||||
|  | import java.security.NoSuchAlgorithmException; | ||||||
|  | import java.time.LocalDateTime; | ||||||
|  | import java.util.Base64; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 本地存储策略 | ||||||
|  |  * | ||||||
|  |  * @author echo | ||||||
|  |  * @date 2024/12/16 19:48 | ||||||
|  |  */ | ||||||
|  | public class LocalStorageStrategy implements StorageStrategy<LocalClient> { | ||||||
|  |  | ||||||
|  |     private final LocalClient client; | ||||||
|  |     private final StorageDao storageDao; | ||||||
|  |  | ||||||
|  |     public LocalStorageStrategy(LocalClient client, StorageDao storageDao) { | ||||||
|  |         this.client = client; | ||||||
|  |         this.storageDao = storageDao; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private StorageProperties getStorageProperties() { | ||||||
|  |         return client.getProperties(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public LocalClient getClient() { | ||||||
|  |         return client; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean bucketExists(String bucketName) { | ||||||
|  |         try { | ||||||
|  |             return Files.exists(Path.of(bucketName)); | ||||||
|  |         } catch (RuntimeException e) { | ||||||
|  |             throw new BusinessException("local存储 查询桶 失败", e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void createBucket(String bucketName) { | ||||||
|  |         if (!bucketExists(bucketName)) { | ||||||
|  |             try { | ||||||
|  |                 Files.createDirectories(Path.of(bucketName)); | ||||||
|  |             } catch (IOException e) { | ||||||
|  |                 throw new BusinessException("local存储 创建桶 失败", 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[] originalBytes = IoUtil.readBytes(inputStream); | ||||||
|  |             ValidationUtils.throwIf(originalBytes.length == 0, "输入流内容长度不可用或无效"); | ||||||
|  |  | ||||||
|  |             // 获取文件扩展名 | ||||||
|  |             String fileExtension = FileNameUtil.extName(fileName); | ||||||
|  |             // 格式化文件名 防止上传后重复 | ||||||
|  |             String formatFileName = StorageUtils.formatFileName(fileName); | ||||||
|  |             // 判断文件路径是否为空  为空给默认路径 格式 2024/12/30/ | ||||||
|  |             if (StrUtil.isEmpty(path)) { | ||||||
|  |                 path = StorageUtils.defaultPath(); | ||||||
|  |             } | ||||||
|  |             // 判断文件夹是否存在 不存在则创建 | ||||||
|  |             Path folderPath = Paths.get(bucketName, path); | ||||||
|  |             if (!Files.exists(folderPath)) { | ||||||
|  |                 Files.createDirectories(folderPath); | ||||||
|  |             } | ||||||
|  |             ThumbnailResp thumbnailResp = null; | ||||||
|  |             //判断是否需要上传缩略图 前置条件 文件必须为图片 | ||||||
|  |             boolean contains = FileTypeEnum.IMAGE.getExtensions().contains(fileExtension); | ||||||
|  |             if (contains && isThumbnail) { | ||||||
|  |                 try (InputStream thumbnailStream = new ByteArrayInputStream(originalBytes)) { | ||||||
|  |                     thumbnailResp = this.uploadThumbnail(bucketName, formatFileName, path, thumbnailStream, fileType); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // 上传文件 | ||||||
|  |             try (InputStream uploadStream = new ByteArrayInputStream(originalBytes)) { | ||||||
|  |                 this.upload(bucketName, formatFileName, path, uploadStream, fileType); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // 构建文件 md5 | ||||||
|  |             String eTag = LocalUtils.calculateMD5(inputStream); | ||||||
|  |             // 构建 上传后的文件路径地址 格式 xxx/xxx/xxx.jpg | ||||||
|  |             String filePath = Paths.get(path, formatFileName).toString(); | ||||||
|  |             // 构建 文件上传记录 并返回 | ||||||
|  |             return buildStorageRecord(bucketName, fileName, filePath, eTag, originalBytes.length, thumbnailResp); | ||||||
|  |         } catch (NoSuchAlgorithmException | IOException e) { | ||||||
|  |             throw new BusinessException("文件上传异常", e); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void upload(String bucketName, String fileName, String path, InputStream inputStream, String fileType) { | ||||||
|  |         byte[] fileBytes = IoUtil.readBytes(inputStream); | ||||||
|  |         // 拼接完整地址 | ||||||
|  |         String filePath = Paths.get(bucketName, path, fileName).toString(); | ||||||
|  |         try { | ||||||
|  |             //上传文件 | ||||||
|  |             File targetFile = new File(filePath); | ||||||
|  |             try (FileOutputStream fos = new FileOutputStream(targetFile)) { | ||||||
|  |                 fos.write(fileBytes); | ||||||
|  |             } | ||||||
|  |         } 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, null); | ||||||
|  |  | ||||||
|  |             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) { | ||||||
|  |         String fullPath = Paths.get(bucketName, fileName).toString(); | ||||||
|  |         File file = new File(fullPath); | ||||||
|  |         try { | ||||||
|  |             return new FileInputStream(file); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             throw new BusinessException("下载文件异常", e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void delete(String bucketName, String fileName) { | ||||||
|  |         try { | ||||||
|  |             String fullPath = Paths.get(bucketName, fileName).toString(); | ||||||
|  |             Files.delete(Paths.get(fullPath)); | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             throw new BusinessException("删除文件异常", 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); | ||||||
|  |             CheckUtils.throwIf(!FileTypeEnum.IMAGE.getExtensions().contains(extName), "{} 不是图像格式", extName); | ||||||
|  |             return Base64.getEncoder().encodeToString(inputStream.readAllBytes()); | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             throw new BusinessException("无法查看图片", e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 构建存储记录 | ||||||
|  |      * | ||||||
|  |      * @param bucketName    桶名称 | ||||||
|  |      * @param fileName      原始文件名 | ||||||
|  |      * @param filePath      文件路径 xx/xx/xxx.jpg | ||||||
|  |      * @param eTag          标签 - md5 | ||||||
|  |      * @param size          文件大小 | ||||||
|  |      * @param thumbnailResp 缩略图信息 | ||||||
|  |      * @return {@link UploadResp } | ||||||
|  |      */ | ||||||
|  |     private UploadResp buildStorageRecord(String bucketName, | ||||||
|  |                                           String fileName, | ||||||
|  |                                           String filePath, | ||||||
|  |                                           String eTag, | ||||||
|  |                                           long size, | ||||||
|  |                                           ThumbnailResp thumbnailResp) { | ||||||
|  |         // 获取当前存储 code | ||||||
|  |         String code = client.getProperties().getCode(); | ||||||
|  |         // 构建访问地址前缀 | ||||||
|  |         String baseUrl = "http://" + getStorageProperties().getEndpoint() + StringConstants.SLASH; | ||||||
|  |  | ||||||
|  |         UploadResp resp = new UploadResp(); | ||||||
|  |         resp.setCode(code); | ||||||
|  |         resp.setUrl(baseUrl + filePath); | ||||||
|  |         resp.setBasePath(filePath); | ||||||
|  |         resp.setOriginalFilename(fileName); | ||||||
|  |         resp.setExt(FileNameUtil.extName(fileName)); | ||||||
|  |         resp.setSize(size); | ||||||
|  |         resp.seteTag(eTag); | ||||||
|  |         resp.setPath(filePath); | ||||||
|  |         resp.setBucketName(bucketName); | ||||||
|  |         resp.setCreateTime(LocalDateTime.now()); | ||||||
|  |         if (ObjectUtil.isNotEmpty(thumbnailResp)) { | ||||||
|  |             resp.setThumbnailUrl(baseUrl + thumbnailResp.getThumbnailPath()); | ||||||
|  |             resp.setThumbnailSize(thumbnailResp.getThumbnailSize()); | ||||||
|  |         } | ||||||
|  |         storageDao.add(resp); | ||||||
|  |         return resp; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,46 @@ | |||||||
|  | /* | ||||||
|  |  * 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.io.IoUtil; | ||||||
|  | import net.dreamlu.mica.core.utils.DigestUtil; | ||||||
|  |  | ||||||
|  | import java.io.InputStream; | ||||||
|  | import java.security.NoSuchAlgorithmException; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 本地存储工具 | ||||||
|  |  * | ||||||
|  |  * @author echo | ||||||
|  |  * @date 2024/12/27 11:58 | ||||||
|  |  */ | ||||||
|  | public class LocalUtils { | ||||||
|  |     public LocalUtils() { | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 计算MD5 | ||||||
|  |      * | ||||||
|  |      * @param inputStream 输入流 | ||||||
|  |      * @return {@link String } | ||||||
|  |      * @throws NoSuchAlgorithmException 没有这样算法例外 | ||||||
|  |      */ | ||||||
|  |     public static String calculateMD5(InputStream inputStream) throws NoSuchAlgorithmException { | ||||||
|  |         byte[] fileBytes = IoUtil.readBytes(inputStream); | ||||||
|  |         return DigestUtil.md5Hex(fileBytes); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1 +1 @@ | |||||||
| top.continew.starter.storage.local.autoconfigure.LocalStorageAutoConfiguration | top.continew.starter.storage.autoconfigure.LocalStorageAutoconfigure | ||||||
| @@ -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 | ||||||
| @@ -14,7 +14,9 @@ | |||||||
|     <description>ContiNew Starter 存储模块</description> |     <description>ContiNew Starter 存储模块</description> | ||||||
|  |  | ||||||
|     <modules> |     <modules> | ||||||
|  |         <module>continew-starter-storage-core</module> | ||||||
|         <module>continew-starter-storage-local</module> |         <module>continew-starter-storage-local</module> | ||||||
|  |         <module>continew-starter-storage-oss</module> | ||||||
|     </modules> |     </modules> | ||||||
|  |  | ||||||
|     <dependencies> |     <dependencies> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 吴泽威
					吴泽威