mirror of
				https://github.com/continew-org/continew-starter.git
				synced 2025-10-31 22:57:19 +08:00 
			
		
		
		
	refactor(storage): 优化存储模块
Co-authored-by: QAQ_Z<958142070@qq.com> # message auto-generated for no-merge-commit merge: merge storage into dev 优化存储模块 Created-by: QAQ_Z Commit-by: QAQ_Z Merged-by: Charles_7c Description: <!-- 非常感谢您的 PR!在提交之前,请务必确保您 PR 的代码经过了完整测试,并且通过了代码规范检查。 --> <!-- 在 [] 中输入 x 来勾选) --> ## PR 类型 <!-- 您的 PR 引入了哪种类型的变更? --> <!-- 只支持选择一种类型,如果有多种类型,可以在更新日志中增加 “类型” 列。 --> - [ ] 新 feature - [ ] Bug 修复 - [x] 功能增强 - [ ] 文档变更 - [ ] 代码样式变更 - [x] 重构 - [ ] 性能改进 - [ ] 单元测试 - [ ] CI/CD - [ ] 其他 ## PR 目的 <!-- 描述一下您的 PR 解决了什么问题。如果可以,请链接到相关 issues。 --> - 重新设计了文件上传的预处理和后处理流程 - 优化了文件验证、命名、路径生成等环节 - 增加了单文件上传监听逻辑 - 增加了装饰器模式,支持重写指定存储策略实现的具体方法 ## 解决方案 <!-- 详细描述您是如何解决的问题 --> ## PR 测试 <!-- 如果可以,请为您的 PR 添加或更新单元测试。 --> <!-- 请描述一下您是如何测试 PR 的。例如:创建/更新单元测试或添加相关的截图。 --> ## Changelog | 模块 | Changelog | Related issues | |-----|-----------| -------------- | | continew-starter-storage | 优化存储模块 | | <!-- 如果有多种类型的变更,可以在变更日志表中增加 “类型” 列,该列的值与上方 “PR 类型” 相同。 --> <!-- Related issues 格式为 Closes #<issue号>,或者 Fixes #<issue号>,或者 Resolves #<issue号>。 --> ## 其他信息 <!-- 请描述一下还有哪些注意事项。例如:如果引入了一个不向下兼容的变更,请描述其影响。 --> ## 提交前确认 - [x] PR 代码经过了完整测试,并且通过了代码规范检查 - [ ] 已经完整填写 Changelog,并链接到了相关 issues - [x] PR 代码将要提交到 dev 分支 See merge request: continew/continew-starter!4
This commit is contained in:
		| @@ -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.annotation; | ||||
|  | ||||
| import java.lang.annotation.*; | ||||
|  | ||||
| /** | ||||
|  * 平台处理器注解 | ||||
|  * <p> | ||||
|  * 该注解用于标记文件前置处理器类,以指定其适用的平台范围。 | ||||
|  * 主要用于实现平台特定的文件处理逻辑,如文件名生成、路径转换、格式适配等。 | ||||
|  * <p> | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| @Target(ElementType.TYPE) | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| @Documented | ||||
| public @interface PlatformProcessor { | ||||
|     /** | ||||
|      * 适用的平台列表 | ||||
|      */ | ||||
|     String[] platforms(); | ||||
| } | ||||
| @@ -21,7 +21,7 @@ import org.springframework.context.annotation.Bean; | ||||
| import top.continew.starter.core.constant.PropertiesConstants; | ||||
| import top.continew.starter.storage.autoconfigure.properties.LocalStorageConfig; | ||||
| import top.continew.starter.storage.autoconfigure.properties.StorageProperties; | ||||
| import top.continew.starter.storage.router.StorageStrategyRegistrar; | ||||
| import top.continew.starter.storage.engine.StorageStrategyRegistrar; | ||||
| import top.continew.starter.storage.strategy.StorageStrategy; | ||||
| import top.continew.starter.storage.strategy.impl.LocalStorageStrategy; | ||||
|  | ||||
| @@ -43,7 +43,7 @@ public class LocalStorageAutoConfiguration implements StorageStrategyRegistrar { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 注册配置策略 | ||||
|      * 注册本地存储策略 | ||||
|      * | ||||
|      * @param strategies 策略列表 | ||||
|      */ | ||||
|   | ||||
| @@ -19,9 +19,9 @@ package top.continew.starter.storage.autoconfigure; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import top.continew.starter.core.constant.PropertiesConstants; | ||||
| import top.continew.starter.storage.autoconfigure.properties.S3StorageConfig; | ||||
| import top.continew.starter.storage.autoconfigure.properties.OssStorageConfig; | ||||
| import top.continew.starter.storage.autoconfigure.properties.StorageProperties; | ||||
| import top.continew.starter.storage.router.StorageStrategyRegistrar; | ||||
| import top.continew.starter.storage.engine.StorageStrategyRegistrar; | ||||
| import top.continew.starter.storage.strategy.StorageStrategy; | ||||
| import top.continew.starter.storage.strategy.impl.OssStorageStrategy; | ||||
| 
 | ||||
| @@ -33,24 +33,24 @@ import java.util.List; | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| @ConditionalOnProperty(prefix = PropertiesConstants.STORAGE, name = "s3") | ||||
| public class S3StorageAutoConfiguration implements StorageStrategyRegistrar { | ||||
| @ConditionalOnProperty(prefix = PropertiesConstants.STORAGE, name = "oss") | ||||
| public class OssStorageAutoConfiguration implements StorageStrategyRegistrar { | ||||
| 
 | ||||
|     private final StorageProperties properties; | ||||
| 
 | ||||
|     public S3StorageAutoConfiguration(StorageProperties properties) { | ||||
|     public OssStorageAutoConfiguration(StorageProperties properties) { | ||||
|         this.properties = properties; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 注册配置策略 | ||||
|      * 注册 OSS 存储策略 | ||||
|      * | ||||
|      * @param strategies 策略列表 | ||||
|      */ | ||||
|     @Override | ||||
|     @Bean | ||||
|     public void register(List<StorageStrategy> strategies) { | ||||
|         for (S3StorageConfig config : properties.getS3()) { | ||||
|         for (OssStorageConfig config : properties.getOss()) { | ||||
|             if (config.isEnabled()) { | ||||
|                 strategies.add(new OssStorageStrategy(config)); | ||||
|             } | ||||
| @@ -19,29 +19,29 @@ package top.continew.starter.storage.autoconfigure; | ||||
| import jakarta.annotation.PostConstruct; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.boot.autoconfigure.AutoConfiguration; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; | ||||
| import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties; | ||||
| import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||||
| import org.springframework.context.ApplicationContext; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.Configuration; | ||||
| import org.springframework.context.annotation.Import; | ||||
| import top.continew.starter.storage.annotation.PlatformProcessor; | ||||
| import top.continew.starter.storage.autoconfigure.properties.StorageProperties; | ||||
| import top.continew.starter.storage.core.FileStorageService; | ||||
| import top.continew.starter.storage.core.ProcessorRegistry; | ||||
| import top.continew.starter.storage.core.StrategyProxyFactory; | ||||
| import top.continew.starter.storage.prehandle.*; | ||||
| import top.continew.starter.storage.prehandle.impl.*; | ||||
| import top.continew.starter.storage.router.StorageStrategyRegistrar; | ||||
| import top.continew.starter.storage.router.StorageStrategyRouter; | ||||
| import top.continew.starter.storage.engine.StorageDecoratorManager; | ||||
| import top.continew.starter.storage.processor.registry.ProcessorRegistry; | ||||
| import top.continew.starter.storage.processor.preprocess.*; | ||||
| import top.continew.starter.storage.processor.preprocess.impl.*; | ||||
| import top.continew.starter.storage.engine.StorageStrategyRegistrar; | ||||
| import top.continew.starter.storage.engine.StorageStrategyRouter; | ||||
| import top.continew.starter.storage.service.FileProcessor; | ||||
| import top.continew.starter.storage.service.FileRecorder; | ||||
| import top.continew.starter.storage.service.impl.DefaultFileRecorder; | ||||
| import top.continew.starter.storage.strategy.StorageStrategyOverride; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * 存储自动配置 | ||||
| @@ -51,15 +51,17 @@ import java.util.List; | ||||
|  */ | ||||
| @AutoConfiguration | ||||
| @EnableConfigurationProperties(StorageProperties.class) | ||||
| @Import({ProcessorRegistry.class, StrategyProxyFactory.class}) | ||||
| @Import({ProcessorRegistry.class}) | ||||
| public class StorageAutoConfiguration { | ||||
|  | ||||
|     private static final Logger log = LoggerFactory.getLogger(StorageAutoConfiguration.class); | ||||
|  | ||||
|     private final StorageProperties properties; | ||||
|     private final ApplicationContext applicationContext; | ||||
|  | ||||
|     public StorageAutoConfiguration(StorageProperties properties) { | ||||
|     public StorageAutoConfiguration(StorageProperties properties, ApplicationContext applicationContext) { | ||||
|         this.properties = properties; | ||||
|         this.applicationContext = applicationContext; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -70,17 +72,27 @@ public class StorageAutoConfiguration { | ||||
|      */ | ||||
|     @Bean | ||||
|     public StorageStrategyRouter strategyRouter(List<StorageStrategyRegistrar> registrars) { | ||||
|         return new StorageStrategyRouter(registrars); | ||||
|         return new StorageStrategyRouter(registrars, properties, storageDecoratorManager()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * S3存储自动配置 | ||||
|      * 存储装饰器管理器 | ||||
|      * | ||||
|      * @return {@link S3StorageAutoConfiguration } | ||||
|      * @return {@link StorageDecoratorManager } | ||||
|      */ | ||||
|     @Bean | ||||
|     public S3StorageAutoConfiguration s3StorageAutoConfiguration() { | ||||
|         return new S3StorageAutoConfiguration(properties); | ||||
|     public StorageDecoratorManager storageDecoratorManager() { | ||||
|         return new StorageDecoratorManager(applicationContext); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * oss存储自动配置 | ||||
|      * | ||||
|      * @return {@link OssStorageAutoConfiguration } | ||||
|      */ | ||||
|     @Bean | ||||
|     public OssStorageAutoConfiguration ossStorageAutoConfiguration() { | ||||
|         return new OssStorageAutoConfiguration(properties); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -99,16 +111,15 @@ public class StorageAutoConfiguration { | ||||
|      * @param router            路由 | ||||
|      * @param storageProperties 存储属性 | ||||
|      * @param processorRegistry 处理器注册表 | ||||
|      * @param proxyFactory      代理工厂 | ||||
|      * @param fileRecorder      文件记录器 | ||||
|      * @return {@link FileStorageService } | ||||
|      */ | ||||
|     @Bean | ||||
|     public FileStorageService fileStorageService(StorageStrategyRouter router, | ||||
|                                                  StorageProperties storageProperties, | ||||
|                                                  ProcessorRegistry processorRegistry, | ||||
|                                                  StrategyProxyFactory proxyFactory, | ||||
|                                                  FileRecorder fileRecorder) { | ||||
|         return new FileStorageService(router, storageProperties, processorRegistry, proxyFactory, fileRecorder); | ||||
|         return new FileStorageService(router, storageProperties, processorRegistry, fileRecorder); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -123,124 +134,73 @@ public class StorageAutoConfiguration { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 默认文件名生成器 | ||||
|      * | ||||
|      * @param registry 登记处 | ||||
|      * @return {@link FileNameGenerator } | ||||
|      * 处理器注册中心 | ||||
|      */ | ||||
|     @Bean | ||||
|     @ConditionalOnMissingBean(name = "defaultFileNameGenerator") | ||||
|     public FileNameGenerator defaultFileNameGenerator(ProcessorRegistry registry) { | ||||
|         DefaultFileNameGenerator generator = new DefaultFileNameGenerator(); | ||||
|         registry.registerGlobalNameGenerator(generator); | ||||
|         return generator; | ||||
|     public ProcessorRegistry processorRegistry() { | ||||
|         ProcessorRegistry registry = new ProcessorRegistry(); | ||||
|  | ||||
|         // 自动发现并注册所有 FileProcessor 实现 | ||||
|         Map<String, FileProcessor> processors = applicationContext.getBeansOfType(FileProcessor.class); | ||||
|         processors.values().forEach(processor -> { | ||||
|             // 检查是否有平台注解 | ||||
|             PlatformProcessor annotation = processor.getClass().getAnnotation(PlatformProcessor.class); | ||||
|             if (annotation != null) { | ||||
|                 for (String platform : annotation.platforms()) { | ||||
|                     registry.register(processor, platform); | ||||
|                 } | ||||
|             } else { | ||||
|                 // 注册为全局处理器 | ||||
|                 registry.register(processor); | ||||
|             } | ||||
|         }); | ||||
|         return registry; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 默认文件路径生成器 | ||||
|      * | ||||
|      * @param registry 注册 | ||||
|      * @return {@link FilePathGenerator } | ||||
|      * 默认文件名生成器 | ||||
|      */ | ||||
|     @Bean | ||||
|     @ConditionalOnMissingBean(name = "defaultFilePathGenerator") | ||||
|     public FilePathGenerator defaultFilePathGenerator(ProcessorRegistry registry) { | ||||
|         DefaultFilePathGenerator generator = new DefaultFilePathGenerator(); | ||||
|         registry.registerGlobalPathGenerator(generator); | ||||
|         return generator; | ||||
|     @ConditionalOnMissingBean(FileNameGenerator.class) | ||||
|     public FileNameGenerator defaultFileNameGenerator() { | ||||
|         return new DefaultFileNameGenerator(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 默认路径生成器 | ||||
|      */ | ||||
|     @Bean | ||||
|     @ConditionalOnMissingBean(FilePathGenerator.class) | ||||
|     public FilePathGenerator defaultFilePathGenerator() { | ||||
|         return new DefaultFilePathGenerator(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 默认缩略图处理器 | ||||
|      * | ||||
|      * @param registry 注册 | ||||
|      * @return {@link ThumbnailProcessor } | ||||
|      */ | ||||
|     @Bean | ||||
|     @ConditionalOnMissingBean(name = "defaultThumbnailProcessor") | ||||
|     @ConditionalOnMissingBean(ThumbnailProcessor.class) | ||||
|     @ConditionalOnClass(name = "net.coobird.thumbnailator.Thumbnails") | ||||
|     public ThumbnailProcessor defaultThumbnailProcessor(ProcessorRegistry registry) { | ||||
|         DefaultThumbnailProcessor processor = new DefaultThumbnailProcessor(); | ||||
|         registry.registerGlobalThumbnailProcessor(processor); | ||||
|         return processor; | ||||
|     public ThumbnailProcessor defaultThumbnailProcessor() { | ||||
|         return new DefaultThumbnailProcessor(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 文件大小验证器 | ||||
|      * | ||||
|      * @param registry 注册 | ||||
|      * @return {@link FileValidator } | ||||
|      */ | ||||
|     @Bean | ||||
|     @ConditionalOnMissingBean(name = "fileSizeValidator") | ||||
|     public FileValidator fileSizeValidator(ProcessorRegistry registry, MultipartProperties multipartProperties) { | ||||
|         FileSizeValidator validator = new FileSizeValidator(multipartProperties); | ||||
|         registry.registerGlobalValidator(validator); | ||||
|         return validator; | ||||
|     public FileValidator fileSizeValidator(MultipartProperties multipartProperties) { | ||||
|         return new FileSizeValidator(multipartProperties); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 文件类型验证器 | ||||
|      * | ||||
|      * @param registry 注册 | ||||
|      * @return {@link FileValidator } | ||||
|      */ | ||||
|     @Bean | ||||
|     @ConditionalOnMissingBean(name = "fileTypeValidator") | ||||
|     public FileValidator fileTypeValidator(ProcessorRegistry registry) { | ||||
|         FileTypeValidator validator = new FileTypeValidator(); | ||||
|         registry.registerGlobalValidator(validator); | ||||
|         return validator; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 策略重写自动注册 | ||||
|      */ | ||||
|     @Configuration | ||||
|     @ConditionalOnBean(StorageStrategyOverride.class) | ||||
|     public static class StrategyOverrideConfiguration { | ||||
|  | ||||
|         /** | ||||
|          * 注册覆盖 | ||||
|          */ | ||||
|         @Autowired | ||||
|         public void registerOverrides(List<StorageStrategyOverride<?>> overrides, StrategyProxyFactory proxyFactory) { | ||||
|             for (StorageStrategyOverride<?> override : overrides) { | ||||
|                 proxyFactory.registerOverride(override); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理器自动注册 | ||||
|      */ | ||||
|     @Configuration | ||||
|     public static class ProcessorAutoConfiguration { | ||||
|         @Autowired(required = false) | ||||
|         public void registerGlobalProcessors(List<FileNameGenerator> nameGenerators, | ||||
|                                              List<FilePathGenerator> pathGenerators, | ||||
|                                              List<ThumbnailProcessor> thumbnailProcessors, | ||||
|                                              List<FileValidator> validators, | ||||
|                                              List<UploadCompleteProcessor> completeProcessors, | ||||
|                                              ProcessorRegistry registry) { | ||||
|  | ||||
|             // 注册全局处理器 | ||||
|             if (nameGenerators != null) { | ||||
|                 nameGenerators.forEach(registry::registerGlobalNameGenerator); | ||||
|             } | ||||
|             if (pathGenerators != null) { | ||||
|                 pathGenerators.forEach(registry::registerGlobalPathGenerator); | ||||
|             } | ||||
|             if (thumbnailProcessors != null) { | ||||
|                 thumbnailProcessors.forEach(registry::registerGlobalThumbnailProcessor); | ||||
|             } | ||||
|             if (validators != null) { | ||||
|                 validators.forEach(registry::registerGlobalValidator); | ||||
|             } | ||||
|             if (completeProcessors != null) { | ||||
|                 completeProcessors.forEach(registry::registerGlobalCompleteProcessor); | ||||
|             } | ||||
|         } | ||||
|     public FileValidator fileTypeValidator() { | ||||
|         return new FileTypeValidator(); | ||||
|     } | ||||
|  | ||||
|     @PostConstruct | ||||
|   | ||||
| @@ -16,13 +16,15 @@ | ||||
| 
 | ||||
| package top.continew.starter.storage.autoconfigure.properties; | ||||
| 
 | ||||
| import top.continew.starter.storage.common.constant.StorageConstant; | ||||
| 
 | ||||
| /** | ||||
|  * s3存储配置 | ||||
|  * oss 存储配置 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public class S3StorageConfig { | ||||
| public class OssStorageConfig { | ||||
| 
 | ||||
|     /** | ||||
|      * 是否启用 | ||||
| @@ -30,7 +32,7 @@ public class S3StorageConfig { | ||||
|     private boolean enabled; | ||||
| 
 | ||||
|     /** | ||||
|      * 唯一存储码 | ||||
|      * 存储平台 | ||||
|      */ | ||||
|     private String platform; | ||||
| 
 | ||||
| @@ -72,12 +74,12 @@ public class S3StorageConfig { | ||||
|     /** | ||||
|      * 多部分上传阈值(字节) | ||||
|      */ | ||||
|     private long multipartUploadThreshold = 5 * 1024 * 1024; // 5MB | ||||
|     private long multipartUploadThreshold = StorageConstant.DEFAULT_FILE_SIZE; | ||||
| 
 | ||||
|     /** | ||||
|      * 多部分上传的部分大小(字节) | ||||
|      */ | ||||
|     private long multipartUploadPartSize = 5 * 1024 * 1024; // 5MB | ||||
|     private long multipartUploadPartSize = StorageConstant.DEFAULT_FILE_SIZE; | ||||
| 
 | ||||
|     /** | ||||
|      * 请求超时时间(秒) | ||||
| @@ -87,7 +89,7 @@ public class S3StorageConfig { | ||||
|     /** | ||||
|      * 默认的对象ACL | ||||
|      */ | ||||
|     private String defaultAcl = "private"; | ||||
|     private String defaultAcl = StorageConstant.DEFAULT_ACL; | ||||
| 
 | ||||
|     /** | ||||
|      * 是否启用路径样式访问 | ||||
| @@ -18,6 +18,8 @@ package top.continew.starter.storage.autoconfigure.properties; | ||||
|  | ||||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||||
| import top.continew.starter.core.constant.PropertiesConstants; | ||||
| import top.continew.starter.storage.common.constant.StorageConstant; | ||||
| import top.continew.starter.storage.common.enums.DefaultStorageSource; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| @@ -34,7 +36,12 @@ public class StorageProperties { | ||||
|     /** | ||||
|      * 默认使用的存储平台 | ||||
|      */ | ||||
|     private String defaultPlatform = "local"; | ||||
|     private String defaultPlatform = StorageConstant.DEFAULT_STORAGE_PLATFORM; | ||||
|  | ||||
|     /** | ||||
|      * 默认存储配置来源 (配置文件/动态配置) | ||||
|      */ | ||||
|     private DefaultStorageSource defaultStorageSource = DefaultStorageSource.DYNAMIC; | ||||
|  | ||||
|     /** | ||||
|      * 本地存储配置列表 | ||||
| @@ -42,9 +49,9 @@ public class StorageProperties { | ||||
|     private List<LocalStorageConfig> local = new ArrayList<>(); | ||||
|  | ||||
|     /** | ||||
|      * S3 存储配置列表 | ||||
|      * oss 存储配置列表 | ||||
|      */ | ||||
|     private List<S3StorageConfig> s3 = new ArrayList<>(); | ||||
|     private List<OssStorageConfig> oss = new ArrayList<>(); | ||||
|  | ||||
|     public String getDefaultPlatform() { | ||||
|         return defaultPlatform; | ||||
| @@ -54,6 +61,14 @@ public class StorageProperties { | ||||
|         this.defaultPlatform = defaultPlatform; | ||||
|     } | ||||
|  | ||||
|     public DefaultStorageSource getDefaultStorageSource() { | ||||
|         return defaultStorageSource; | ||||
|     } | ||||
|  | ||||
|     public void setDefaultStorageSource(DefaultStorageSource defaultStorageSource) { | ||||
|         this.defaultStorageSource = defaultStorageSource; | ||||
|     } | ||||
|  | ||||
|     public List<LocalStorageConfig> getLocal() { | ||||
|         return local; | ||||
|     } | ||||
| @@ -62,11 +77,11 @@ public class StorageProperties { | ||||
|         this.local = local; | ||||
|     } | ||||
|  | ||||
|     public List<S3StorageConfig> getS3() { | ||||
|         return s3; | ||||
|     public List<OssStorageConfig> getOss() { | ||||
|         return oss; | ||||
|     } | ||||
|  | ||||
|     public void setS3(List<S3StorageConfig> s3) { | ||||
|         this.s3 = s3; | ||||
|     public void setOss(List<OssStorageConfig> oss) { | ||||
|         this.oss = oss; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,64 @@ | ||||
| /* | ||||
|  * 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.common.constant; | ||||
|  | ||||
| import top.continew.starter.core.constant.StringConstants; | ||||
|  | ||||
| /** | ||||
|  * 存储常数 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public class StorageConstant { | ||||
|  | ||||
|     /** | ||||
|      * 默认存储平台 | ||||
|      */ | ||||
|     public static final String DEFAULT_STORAGE_PLATFORM = "local"; | ||||
|  | ||||
|     /** | ||||
|      * 配置文件 | ||||
|      */ | ||||
|     public static final String CONFIG = "CONFIG"; | ||||
|  | ||||
|     /** | ||||
|      * 动态配置 | ||||
|      */ | ||||
|     public static final String DYNAMIC = "DYNAMIC"; | ||||
|  | ||||
|     /** | ||||
|      * 默认文件大小 | ||||
|      */ | ||||
|     public static final Long DEFAULT_FILE_SIZE = 1024 * 1024 * 10L; | ||||
|  | ||||
|     /** | ||||
|      * 默认的对象ACL | ||||
|      */ | ||||
|     public static final String DEFAULT_ACL = "private"; | ||||
|  | ||||
|     /** | ||||
|      * 缩略图后缀 | ||||
|      */ | ||||
|     public static final String THUMBNAIL_SUFFIX = StringConstants.DOT + "thumb" + StringConstants.DOT; | ||||
|  | ||||
|     /** | ||||
|      * ContentType 图片前缀 | ||||
|      */ | ||||
|     public static final String CONTENT_TYPE_IMAGE = "image/"; | ||||
|  | ||||
| } | ||||
| @@ -14,26 +14,25 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.strategy; | ||||
| package top.continew.starter.storage.common.enums; | ||||
| 
 | ||||
| /** | ||||
|  * 存储策略重写接口 | ||||
|  * 默认存储配置来源 | ||||
|  * 决定默认存储平台配置从哪里加载 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public interface StorageStrategyOverride<T extends StorageStrategy> { | ||||
| public enum DefaultStorageSource { | ||||
| 
 | ||||
|     /** | ||||
|      * 获取目标策略类型 | ||||
|      * 从配置文件加载默认存储 | ||||
|      */ | ||||
|     Class<T> getTargetType(); | ||||
|     CONFIG, | ||||
| 
 | ||||
|     /** | ||||
|      * 获取原始目标对象(用于调用原始方法) | ||||
|      * 从动态配置加载默认存储 | ||||
|      */ | ||||
|     default T getOriginalTarget() { | ||||
|         // 这个方法会在代理创建时被设置 | ||||
|         throw new UnsupportedOperationException("原始目标未设置"); | ||||
|     } | ||||
| } | ||||
|     DYNAMIC, | ||||
| 
 | ||||
| } | ||||
| @@ -14,7 +14,7 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.exception; | ||||
| package top.continew.starter.storage.common.exception; | ||||
| 
 | ||||
| import top.continew.starter.core.exception.BaseException; | ||||
| 
 | ||||
| @@ -14,7 +14,7 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.util; | ||||
| package top.continew.starter.storage.common.util; | ||||
| 
 | ||||
| import cn.hutool.core.date.DateUtil; | ||||
| import cn.hutool.core.io.FileUtil; | ||||
| @@ -16,15 +16,27 @@ | ||||
|  | ||||
| package top.continew.starter.storage.core; | ||||
|  | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
| import top.continew.starter.core.constant.StringConstants; | ||||
| import top.continew.starter.storage.autoconfigure.properties.StorageProperties; | ||||
| import top.continew.starter.storage.exception.StorageException; | ||||
| import top.continew.starter.storage.model.context.UploadContext; | ||||
| import top.continew.starter.storage.model.resp.*; | ||||
| import top.continew.starter.storage.router.StorageStrategyRouter; | ||||
| import top.continew.starter.storage.common.constant.StorageConstant; | ||||
| import top.continew.starter.storage.common.exception.StorageException; | ||||
| import top.continew.starter.storage.domain.file.EnhancedMultipartFile; | ||||
| import top.continew.starter.storage.domain.file.FileWrapper; | ||||
| import top.continew.starter.storage.domain.file.ProgressAwareMultipartFile; | ||||
| import top.continew.starter.storage.domain.model.context.UploadContext; | ||||
| import top.continew.starter.storage.domain.model.req.ThumbnailInfo; | ||||
| import top.continew.starter.storage.domain.model.resp.*; | ||||
| import top.continew.starter.storage.processor.preprocess.*; | ||||
| import top.continew.starter.storage.processor.progress.UploadProgressListener; | ||||
| import top.continew.starter.storage.processor.registry.ProcessorRegistry; | ||||
| import top.continew.starter.storage.engine.StorageStrategyRouter; | ||||
| import top.continew.starter.storage.service.FileProcessor; | ||||
| import top.continew.starter.storage.service.FileRecorder; | ||||
| import top.continew.starter.storage.strategy.StorageStrategy; | ||||
| import top.continew.starter.storage.strategy.impl.LocalStorageStrategy; | ||||
|  | ||||
| import java.io.InputStream; | ||||
| import java.time.LocalDateTime; | ||||
| @@ -40,21 +52,22 @@ import java.util.stream.Collectors; | ||||
|  */ | ||||
| public class FileStorageService { | ||||
|  | ||||
|     private static final Logger log = LoggerFactory.getLogger(FileStorageService.class); | ||||
|  | ||||
|     private final StorageStrategyRouter router; | ||||
|     private final StorageProperties storageProperties; | ||||
|     private final ProcessorRegistry processorRegistry; | ||||
|     private final StrategyProxyFactory proxyFactory; | ||||
|     private final FileRecorder fileRecorder; | ||||
|     private final ThreadLocal<List<FileProcessor>> tempProcessors = ThreadLocal.withInitial(ArrayList::new); | ||||
|     private final ThreadLocal<UploadProgressListener> progressListener = new ThreadLocal<>(); | ||||
|  | ||||
|     public FileStorageService(StorageStrategyRouter router, | ||||
|                               StorageProperties storageProperties, | ||||
|                               ProcessorRegistry processorRegistry, | ||||
|                               StrategyProxyFactory proxyFactory, | ||||
|                               FileRecorder fileRecorder) { | ||||
|         this.router = router; | ||||
|         this.storageProperties = storageProperties; | ||||
|         this.processorRegistry = processorRegistry; | ||||
|         this.proxyFactory = proxyFactory; | ||||
|         this.fileRecorder = fileRecorder; | ||||
|     } | ||||
|  | ||||
| @@ -62,7 +75,7 @@ public class FileStorageService { | ||||
|      * 获取默认存储平台 | ||||
|      */ | ||||
|     public String getDefaultPlatform() { | ||||
|         return storageProperties.getDefaultPlatform(); | ||||
|         return router.getDefaultStorage(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -73,82 +86,376 @@ public class FileStorageService { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 创建上传预处理器(链式调用入口) | ||||
|      * MultipartFile 直接上传 | ||||
|      */ | ||||
|     public UploadPretreatment of(MultipartFile file) { | ||||
|         return new UploadPretreatment(this, file); | ||||
|         return createPretreatment(file, null); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 创建上传预处理器,指定平台 | ||||
|      * MultipartFile 指定平台 | ||||
|      */ | ||||
|     public UploadPretreatment of(MultipartFile file, String platform) { | ||||
|         return new UploadPretreatment(this, file).setPlatform(platform); | ||||
|         return createPretreatment(file, platform); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 创建上传预处理器(支持 byte[]) | ||||
|      * byte[] 上传 | ||||
|      */ | ||||
|     public UploadPretreatment of(byte[] bytes, String filename, String contentType) { | ||||
|         FileWrapper wrapper = FileWrapper.of(bytes, filename, contentType); | ||||
|         return new UploadPretreatment(this, wrapper.toMultipartFile()); | ||||
|         return createPretreatment(bytes, filename, contentType); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 创建上传预处理器(支持 InputStream) | ||||
|      * InputStream 上传 | ||||
|      */ | ||||
|     public UploadPretreatment of(InputStream inputStream, String filename, String contentType) { | ||||
|         FileWrapper wrapper = FileWrapper.of(inputStream, filename, contentType); | ||||
|         return new UploadPretreatment(this, wrapper.toMultipartFile()); | ||||
|         return createPretreatment(inputStream, filename, contentType); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 创建上传预处理器(支持任意对象) | ||||
|      */ | ||||
|     public UploadPretreatment of(Object obj) { | ||||
|         FileWrapper wrapper = FileWrapper.of(obj); | ||||
|         return new UploadPretreatment(this, wrapper.toMultipartFile()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 创建上传预处理器(支持任意对象,指定文件名和类型) | ||||
|      * 任意对象上传 | ||||
|      */ | ||||
|     public UploadPretreatment of(Object obj, String filename, String contentType) { | ||||
|         FileWrapper wrapper = FileWrapper.of(obj, filename, contentType); | ||||
|         return new UploadPretreatment(this, wrapper.toMultipartFile()); | ||||
|         return createPretreatment(obj, filename, contentType); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 执行上传(内部方法) | ||||
|      * 任意对象智能识别 | ||||
|      */ | ||||
|     public FileInfo doUpload(UploadContext context) { | ||||
|         StorageStrategy strategy = getStrategy(context.getPlatform()); | ||||
|     public UploadPretreatment of(Object obj) { | ||||
|         return createPretreatment(obj, null, null); | ||||
|     } | ||||
|  | ||||
|         // 执行上传 | ||||
|         strategy.upload(context.getBucket(), context.getFullPath(), context.getFile()); | ||||
|     /** | ||||
|      * 创建预处理 | ||||
|      * | ||||
|      * @param file     文件 | ||||
|      * @param platform 站台 | ||||
|      * @return {@link UploadPretreatment } | ||||
|      */ | ||||
|     private UploadPretreatment createPretreatment(MultipartFile file, String platform) { | ||||
|         UploadPretreatment pretreatment = new UploadPretreatment(this, file); | ||||
|         return platform != null ? pretreatment.platform(platform) : pretreatment; | ||||
|     } | ||||
|  | ||||
|         // 构建文件信息 | ||||
|         FileInfo fileInfo = strategy.getFileInfo(context.getBucket(), context.getFullPath()); | ||||
|     /** | ||||
|      * 创建预处理 | ||||
|      * | ||||
|      * @param source      来源 | ||||
|      * @param filename    文件名 | ||||
|      * @param contentType 内容类型 | ||||
|      * @return {@link UploadPretreatment } | ||||
|      */ | ||||
|     private UploadPretreatment createPretreatment(Object source, String filename, String contentType) { | ||||
|         FileWrapper wrapper = (filename != null || contentType != null) | ||||
|             ? FileWrapper.of(source, filename, contentType) | ||||
|             : FileWrapper.of(source); | ||||
|  | ||||
|         return createPretreatment(wrapper.toMultipartFile(), null); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 添加处理器 | ||||
|      */ | ||||
|     public void addProcessor(FileProcessor processor) { | ||||
|         tempProcessors.get().add(processor); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 设置进度监听器 | ||||
|      */ | ||||
|     public void onProgress(UploadProgressListener listener) { | ||||
|         progressListener.set(listener); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 执行上传 | ||||
|      */ | ||||
|     public FileInfo upload(UploadContext context) { | ||||
|         String platform = context.getPlatform(); | ||||
|         try { | ||||
|             // 初始化处理器和监听器 | ||||
|             List<FileProcessor> customProcessors = tempProcessors.get(); | ||||
|             UploadProgressListener listener = progressListener.get(); | ||||
|  | ||||
|             // 设置进度监听器到上下文 | ||||
|             if (listener != null) { | ||||
|                 context.setProgressListener(listener); | ||||
|             } | ||||
|  | ||||
|             // 包装文件 | ||||
|             prepareFile(context, listener); | ||||
|  | ||||
|             // 1. 执行文件验证(验证阶段) | ||||
|             setFileReadPhase(context.getFile(), ProgressAwareMultipartFile.ReadPhase.VALIDATION); | ||||
|             executeValidation(context, platform, customProcessors); | ||||
|  | ||||
|             // 2. 处理文件名和路径生成 | ||||
|             generateFileNameIfEmpty(context, platform, customProcessors); | ||||
|             generateFilePathIfEmpty(context, platform, customProcessors); | ||||
|  | ||||
|             // 3. 设置默认bucket | ||||
|             if (StrUtil.isBlank(context.getBucket())) { | ||||
|                 context.setBucket(getDefaultBucket(platform)); | ||||
|             } | ||||
|  | ||||
|             // 4. 准备缩略图处理 | ||||
|             ThumbnailProcessor thumbnailProcessor = prepareThumbnail(context, platform, customProcessors); | ||||
|  | ||||
|             // 5. 执行实际上传 | ||||
|             setFileReadPhase(context.getFile(), ProgressAwareMultipartFile.ReadPhase.UPLOAD); | ||||
|             upload(platform, context.getBucket(), context.getFullPath(), context.getFile()); | ||||
|  | ||||
|             // 6. 构建文件信息 | ||||
|             FileInfo fileInfo = buildFileInfo(platform, context); | ||||
|  | ||||
|             // 7. 处理缩略图生成(缩略图阶段) | ||||
|             if (thumbnailProcessor != null && context.isGenerateThumbnail()) { | ||||
|                 setFileReadPhase(context.getFile(), ProgressAwareMultipartFile.ReadPhase.THUMBNAIL); | ||||
|                 processThumbnail(fileInfo, thumbnailProcessor, context); | ||||
|             } | ||||
|  | ||||
|             // 8. 保存文件记录 | ||||
|             if (fileRecorder != null) { | ||||
|                 fileRecorder.save(fileInfo); | ||||
|             } | ||||
|  | ||||
|             // 9. 触发完成事件 | ||||
|             triggerCompleteEvent(fileInfo, context, platform, customProcessors); | ||||
|  | ||||
|             return fileInfo; | ||||
|  | ||||
|         } finally { | ||||
|             cleanup(context); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 准备文件 | ||||
|      */ | ||||
|     private void prepareFile(UploadContext context, UploadProgressListener listener) { | ||||
|         MultipartFile file = context.getFile(); | ||||
|  | ||||
|         // 如果已经是 ProgressAwareMultipartFile,不重复包装 | ||||
|         if (file instanceof ProgressAwareMultipartFile) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // 判断是否需要缓存 | ||||
|         boolean needCache = true; | ||||
|  | ||||
|         // 如果有进度监听器,使用 ProgressAwareMultipartFile | ||||
|         if (listener != null) { | ||||
|             context.setFile(new ProgressAwareMultipartFile(file, needCache, listener)); | ||||
|         } else if (!(file instanceof EnhancedMultipartFile)) { | ||||
|             // 否则只使用 EnhancedMultipartFile 提供缓存 | ||||
|             context.setFile(EnhancedMultipartFile.wrap(file, needCache)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 设置文件读取阶段 | ||||
|      */ | ||||
|     private void setFileReadPhase(MultipartFile file, ProgressAwareMultipartFile.ReadPhase phase) { | ||||
|         if (file instanceof ProgressAwareMultipartFile) { | ||||
|             ((ProgressAwareMultipartFile)file).setReadPhase(phase); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 执行文件验证 | ||||
|      */ | ||||
|     private void executeValidation(UploadContext context, String platform, List<FileProcessor> customProcessors) { | ||||
|         List<FileValidator> validators = collectProcessors(customProcessors, FileValidator.class, platform, context); | ||||
|  | ||||
|         for (FileValidator validator : validators) { | ||||
|             if (validator.support(context)) { | ||||
|                 validator.validate(context); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 仅在文件名为空时生成文件名 | ||||
|      */ | ||||
|     private void generateFileNameIfEmpty(UploadContext context, String platform, List<FileProcessor> customProcessors) { | ||||
|         // 如果已有文件名,直接返回 | ||||
|         if (StrUtil.isNotBlank(context.getFormatFileName())) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         FileNameGenerator nameGenerator = findFirstProcessor(customProcessors, FileNameGenerator.class, platform, context); | ||||
|  | ||||
|         if (nameGenerator != null && nameGenerator.support(context)) { | ||||
|             context.setFormatFileName(nameGenerator.generate(context)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 仅在路径为空时生成路径 | ||||
|      */ | ||||
|     private void generateFilePathIfEmpty(UploadContext context, String platform, List<FileProcessor> customProcessors) { | ||||
|         // 如果已有路径,直接返回 | ||||
|         if (StrUtil.isNotBlank(context.getPath())) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         FilePathGenerator pathGenerator = findFirstProcessor(customProcessors, FilePathGenerator.class, platform, context); | ||||
|  | ||||
|         if (pathGenerator != null && pathGenerator.support(context)) { | ||||
|             context.setPath(pathGenerator.path(context)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 准备缩略图处理 | ||||
|      */ | ||||
|     private ThumbnailProcessor prepareThumbnail(UploadContext context, | ||||
|                                                 String platform, | ||||
|                                                 List<FileProcessor> customProcessors) { | ||||
|         ThumbnailProcessor thumbnailProcessor = findFirstProcessor(customProcessors, ThumbnailProcessor.class, platform, context); | ||||
|  | ||||
|         boolean needThumbnail = thumbnailProcessor != null && thumbnailProcessor.support(context); | ||||
|         context.setGenerateThumbnail(needThumbnail); | ||||
|  | ||||
|         return thumbnailProcessor; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 构建文件信息 | ||||
|      */ | ||||
|     private FileInfo buildFileInfo(String platform, UploadContext context) { | ||||
|         FileInfo fileInfo = getFileInfo(platform, context.getBucket(), context.getFullPath()); | ||||
|         fileInfo.setOriginalFileName(context.getFile().getOriginalFilename()); | ||||
|         fileInfo.getMetadata().putAll(context.getMetadata()); | ||||
|  | ||||
|         // 保存文件记录 | ||||
|         if (fileRecorder != null) { | ||||
|             fileRecorder.save(fileInfo); | ||||
|         if (context.getMetadata() != null && !context.getMetadata().isEmpty()) { | ||||
|             fileInfo.getMetadata().putAll(context.getMetadata()); | ||||
|         } | ||||
|  | ||||
|         return fileInfo; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 触发上传完成事件 | ||||
|      */ | ||||
|     private void triggerCompleteEvent(FileInfo fileInfo, | ||||
|                                       UploadContext context, | ||||
|                                       String platform, | ||||
|                                       List<FileProcessor> customProcessors) { | ||||
|         List<UploadCompleteProcessor> completeProcessors = collectProcessors(customProcessors, UploadCompleteProcessor.class, platform, context); | ||||
|  | ||||
|         for (UploadCompleteProcessor processor : completeProcessors) { | ||||
|             if (processor.support(context)) { | ||||
|                 processor.onComplete(fileInfo); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 收集指定类型的处理器 | ||||
|      */ | ||||
|     private <T extends FileProcessor> List<T> collectProcessors(List<FileProcessor> customProcessors, | ||||
|                                                                 Class<T> processorClass, | ||||
|                                                                 String platform, | ||||
|                                                                 UploadContext context) { | ||||
|  | ||||
|         List<T> processors = new ArrayList<>(); | ||||
|  | ||||
|         // 添加自定义处理器 | ||||
|         if (customProcessors != null) { | ||||
|             customProcessors.stream() | ||||
|                 .filter(processorClass::isInstance) | ||||
|                 .map(processorClass::cast) | ||||
|                 .forEach(processors::add); | ||||
|         } | ||||
|  | ||||
|         // 添加注册的处理器 | ||||
|         processors.addAll(processorRegistry.getProcessors(processorClass, platform, context)); | ||||
|  | ||||
|         return processors; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 查找第一个匹配的处理器 | ||||
|      */ | ||||
|     private <T extends FileProcessor> T findFirstProcessor(List<FileProcessor> customProcessors, | ||||
|                                                            Class<T> processorClass, | ||||
|                                                            String platform, | ||||
|                                                            UploadContext context) { | ||||
|  | ||||
|         // 优先从自定义处理器中查找 | ||||
|         if (customProcessors != null) { | ||||
|             Optional<T> customProcessor = customProcessors.stream() | ||||
|                 .filter(processorClass::isInstance) | ||||
|                 .map(processorClass::cast) | ||||
|                 .findFirst(); | ||||
|  | ||||
|             if (customProcessor.isPresent()) { | ||||
|                 return customProcessor.get(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // 从注册的处理器中获取 | ||||
|         return processorRegistry.getProcessor(processorClass, platform, context); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理缩略图 | ||||
|      */ | ||||
|     private void processThumbnail(FileInfo fileInfo, ThumbnailProcessor processor, UploadContext context) { | ||||
|         try { | ||||
|             MultipartFile file = context.getFile(); | ||||
|  | ||||
|             // 缩略图生成使用普通流 | ||||
|             try (InputStream is = file.getInputStream()) { | ||||
|                 ThumbnailInfo thumbnailInfo = processor.process(context, is); | ||||
|  | ||||
|                 // 生成缩略图路径 | ||||
|                 String filePrefix = StrUtil.subBefore(fileInfo.getPath(), StringConstants.DOT, true); | ||||
|                 String thumbnailPath = filePrefix + StorageConstant.THUMBNAIL_SUFFIX + thumbnailInfo.getFormat(); | ||||
|                 String thumbnailFileName = StrUtil.subAfter(thumbnailPath, StringConstants.SLASH, true); | ||||
|  | ||||
|                 // 创建缩略图文件 | ||||
|                 EnhancedMultipartFile thumbnailFile = new EnhancedMultipartFile(thumbnailFileName, thumbnailFileName, StorageConstant.CONTENT_TYPE_IMAGE + thumbnailInfo | ||||
|                     .getFormat(), thumbnailInfo.getData()); | ||||
|  | ||||
|                 // 上传缩略图 | ||||
|                 upload(context.getPlatform(), context.getBucket(), thumbnailPath, thumbnailFile); | ||||
|  | ||||
|                 fileInfo.setThumbnailPath(thumbnailPath); | ||||
|                 fileInfo.setThumbnailSize((long)thumbnailInfo.getData().length); | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|             log.warn("缩略图处理失败: {}", e.getMessage()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 清理资源 | ||||
|      */ | ||||
|     private void cleanup(UploadContext context) { | ||||
|         // 清理临时处理器和监听器 | ||||
|         tempProcessors.remove(); | ||||
|         progressListener.remove(); | ||||
|  | ||||
|         // 清理文件缓存 | ||||
|         cleanupFileCache(context.getFile()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 清理文件缓存 | ||||
|      */ | ||||
|     private void cleanupFileCache(MultipartFile file) { | ||||
|         if (file instanceof ProgressAwareMultipartFile) { | ||||
|             ((ProgressAwareMultipartFile)file).clearCache(); | ||||
|         } else if (file instanceof EnhancedMultipartFile) { | ||||
|             ((EnhancedMultipartFile)file).clearCache(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 初始化分片上传 | ||||
|      * | ||||
|      * @param bucket      存储桶 | ||||
|      * @param platform    平台 | ||||
|      * @param path        路径 | ||||
|      * @param contentType 内容类型 | ||||
|      * @param metadata    元数据 | ||||
|      * @return {@link MultipartInitResp } | ||||
|      */ | ||||
|     public MultipartInitResp initMultipartUpload(String bucket, | ||||
|                                                  String platform, | ||||
| @@ -156,8 +463,7 @@ public class FileStorageService { | ||||
|                                                  String contentType, | ||||
|                                                  Map<String, String> metadata) { | ||||
|         bucket = bucket == null ? getDefaultBucket(platform) : bucket; | ||||
|         StorageStrategy strategy = getStrategy(platform); | ||||
|         MultipartInitResp result = strategy.initMultipartUpload(bucket, path, contentType, metadata); | ||||
|         MultipartInitResp result = router.route(platform).initMultipartUpload(bucket, path, contentType, metadata); | ||||
|  | ||||
|         // 记录文件信息 | ||||
|         if (fileRecorder != null) { | ||||
| @@ -177,14 +483,6 @@ public class FileStorageService { | ||||
|  | ||||
|     /** | ||||
|      * 上传分片 | ||||
|      * | ||||
|      * @param platform   平台 | ||||
|      * @param bucket     存储桶 | ||||
|      * @param path       路径 | ||||
|      * @param uploadId   上传id | ||||
|      * @param partNumber 分片编号 | ||||
|      * @param data       数据 | ||||
|      * @return {@link MultipartUploadResp } | ||||
|      */ | ||||
|     public MultipartUploadResp uploadPart(String platform, | ||||
|                                           String bucket, | ||||
| @@ -192,8 +490,7 @@ public class FileStorageService { | ||||
|                                           String uploadId, | ||||
|                                           int partNumber, | ||||
|                                           InputStream data) { | ||||
|         StorageStrategy strategy = getStrategy(platform); | ||||
|         MultipartUploadResp result = strategy.uploadPart(bucket, path, uploadId, partNumber, data); | ||||
|         MultipartUploadResp result = router.route(platform).uploadPart(bucket, path, uploadId, partNumber, data); | ||||
|  | ||||
|         // 记录分片信息 | ||||
|         if (fileRecorder != null && result.isSuccess()) { | ||||
| @@ -214,13 +511,6 @@ public class FileStorageService { | ||||
|  | ||||
|     /** | ||||
|      * 完成分片上传 | ||||
|      * | ||||
|      * @param platform    平台 | ||||
|      * @param bucket      存储桶 | ||||
|      * @param path        路径 | ||||
|      * @param uploadId    上传id | ||||
|      * @param clientParts 分片信息 | ||||
|      * @return {@link FileInfo } | ||||
|      */ | ||||
|     public FileInfo completeMultipartUpload(String platform, | ||||
|                                             String bucket, | ||||
| @@ -252,13 +542,8 @@ public class FileStorageService { | ||||
|  | ||||
|         // 获取策略,判断是否需要验证 | ||||
|         boolean needVerify = true; | ||||
|         StorageStrategy strategy = getStrategy(platform); | ||||
|         if (strategy instanceof LocalStorageStrategy) { | ||||
|             needVerify = false; | ||||
|         } | ||||
|  | ||||
|         // 完成上传 | ||||
|         FileInfo fileInfo = strategy.completeMultipartUpload(bucket, path, uploadId, parts, needVerify); | ||||
|         FileInfo fileInfo = router.route(platform).completeMultipartUpload(bucket, path, uploadId, parts, needVerify); | ||||
|  | ||||
|         // 更新文件记录 | ||||
|         if (fileRecorder != null) { | ||||
| @@ -275,15 +560,9 @@ public class FileStorageService { | ||||
|  | ||||
|     /** | ||||
|      * 取消分片上传 | ||||
|      * | ||||
|      * @param platform 平台 | ||||
|      * @param bucket   存储桶 | ||||
|      * @param path     路径 | ||||
|      * @param uploadId 上传id | ||||
|      */ | ||||
|     public void abortMultipartUpload(String platform, String bucket, String path, String uploadId) { | ||||
|         StorageStrategy strategy = getStrategy(platform); | ||||
|         strategy.abortMultipartUpload(bucket, path, uploadId); | ||||
|         router.route(platform).abortMultipartUpload(bucket, path, uploadId); | ||||
|  | ||||
|         // 删除相关记录 | ||||
|         if (fileRecorder != null) { | ||||
| @@ -293,8 +572,6 @@ public class FileStorageService { | ||||
|  | ||||
|     /** | ||||
|      * 验证分片完整性 | ||||
|      * | ||||
|      * @param parts 分片信息 | ||||
|      */ | ||||
|     private void validatePartsCompleteness(List<MultipartUploadResp> parts) { | ||||
|         if (parts.isEmpty()) { | ||||
| @@ -314,7 +591,7 @@ public class FileStorageService { | ||||
|         List<Integer> failedParts = parts.stream() | ||||
|             .filter(part -> !part.isSuccess()) | ||||
|             .map(MultipartUploadResp::getPartNumber) | ||||
|             .collect(Collectors.toList()); | ||||
|             .toList(); | ||||
|  | ||||
|         if (!failedParts.isEmpty()) { | ||||
|             throw new StorageException("存在失败的分片: " + failedParts); | ||||
| @@ -323,24 +600,9 @@ public class FileStorageService { | ||||
|  | ||||
|     /** | ||||
|      * 列出已上传的分片 | ||||
|      * | ||||
|      * @param platform 平台 | ||||
|      * @param bucket   存储桶 | ||||
|      * @param path     路径 | ||||
|      * @param uploadId 上传id | ||||
|      * @return {@link List }<{@link MultipartUploadResp }> | ||||
|      */ | ||||
|     public List<MultipartUploadResp> listParts(String platform, String bucket, String path, String uploadId) { | ||||
|         StorageStrategy strategy = router.route(platform); | ||||
|         return strategy.listParts(bucket, path, uploadId); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取存储策略(应用代理) | ||||
|      */ | ||||
|     private StorageStrategy getStrategy(String platform) { | ||||
|         StorageStrategy strategy = router.route(platform); | ||||
|         return proxyFactory.createProxy(strategy); | ||||
|         return router.route(platform).listParts(bucket, path, uploadId); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -362,7 +624,7 @@ public class FileStorageService { | ||||
|      * @param file     文件 | ||||
|      */ | ||||
|     public void upload(String platform, String bucket, String path, MultipartFile file) { | ||||
|         router.route(platform).upload(path, bucket, file); | ||||
|         router.route(platform).upload(bucket, path, file); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -458,6 +720,15 @@ public class FileStorageService { | ||||
|         router.registerDynamic(strategy); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 加载动态默认存储 | ||||
|      * | ||||
|      * @param platform 站台 | ||||
|      */ | ||||
|     public void defaultStorage(String platform) { | ||||
|         router.registerDynamicDefaultStorage(platform); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 卸载动态注册的策略 | ||||
|      */ | ||||
| @@ -499,7 +770,7 @@ public class FileStorageService { | ||||
|     /** | ||||
|      * 获取策略详细信息 | ||||
|      */ | ||||
|     public Map<String, StrategyStatus> getStrategyStatus() { | ||||
|     public Map<String, StrategyStatusResp> getStrategyStatus() { | ||||
|         return router.getFullStrategyStatus(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,215 +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.core; | ||||
|  | ||||
| import top.continew.starter.storage.prehandle.*; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Comparator; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
| import java.util.concurrent.CopyOnWriteArrayList; | ||||
|  | ||||
| /** | ||||
|  * 全局处理器注册表 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public class ProcessorRegistry { | ||||
|  | ||||
|     // 全局处理器 | ||||
|     private final Map<String, FileNameGenerator> globalNameGenerators = new ConcurrentHashMap<>(); | ||||
|     private final Map<String, FilePathGenerator> globalPathGenerators = new ConcurrentHashMap<>(); | ||||
|     private final Map<String, ThumbnailProcessor> globalThumbnailProcessors = new ConcurrentHashMap<>(); | ||||
|     private final List<FileValidator> globalValidators = new CopyOnWriteArrayList<>(); | ||||
|     private final List<UploadCompleteProcessor> globalCompleteProcessors = new CopyOnWriteArrayList<>(); | ||||
|  | ||||
|     // 平台特定处理器 | ||||
|     private final Map<String, Map<String, FileNameGenerator>> platformNameGenerators = new ConcurrentHashMap<>(); | ||||
|     private final Map<String, Map<String, FilePathGenerator>> platformPathGenerators = new ConcurrentHashMap<>(); | ||||
|     private final Map<String, Map<String, ThumbnailProcessor>> platformThumbnailProcessors = new ConcurrentHashMap<>(); | ||||
|     private final Map<String, List<FileValidator>> platformValidators = new ConcurrentHashMap<>(); | ||||
|     private final Map<String, List<UploadCompleteProcessor>> platformCompleteProcessors = new ConcurrentHashMap<>(); | ||||
|  | ||||
|     /** | ||||
|      * 注册全局文件名生成器 | ||||
|      */ | ||||
|     public void registerGlobalNameGenerator(FileNameGenerator generator) { | ||||
|         globalNameGenerators.put(generator.getName(), generator); | ||||
|     } | ||||
|  | ||||
|     public void registerGlobalPathGenerator(FilePathGenerator generator) { | ||||
|         globalPathGenerators.put(generator.getName(), generator); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 注册平台特定的文件名生成器 | ||||
|      */ | ||||
|     public void registerPlatformNameGenerator(String platform, FileNameGenerator generator) { | ||||
|         platformNameGenerators.computeIfAbsent(platform, k -> new ConcurrentHashMap<>()) | ||||
|             .put(generator.getName(), generator); | ||||
|     } | ||||
|  | ||||
|     public void registerPlatformPathGenerator(String platform, FilePathGenerator generator) { | ||||
|         platformPathGenerators.computeIfAbsent(platform, k -> new ConcurrentHashMap<>()) | ||||
|             .put(generator.getName(), generator); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 注册全局缩略图处理器 | ||||
|      */ | ||||
|     public void registerGlobalThumbnailProcessor(ThumbnailProcessor processor) { | ||||
|         globalThumbnailProcessors.put(processor.getName(), processor); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 注册平台特定的缩略图处理器 | ||||
|      */ | ||||
|     public void registerPlatformThumbnailProcessor(String platform, ThumbnailProcessor processor) { | ||||
|         platformThumbnailProcessors.computeIfAbsent(platform, k -> new ConcurrentHashMap<>()) | ||||
|             .put(processor.getName(), processor); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 注册全局验证器 | ||||
|      */ | ||||
|     public void registerGlobalValidator(FileValidator validator) { | ||||
|         globalValidators.add(validator); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 注册平台特定的验证器 | ||||
|      */ | ||||
|     public void registerPlatformValidator(String platform, FileValidator validator) { | ||||
|         platformValidators.computeIfAbsent(platform, k -> new CopyOnWriteArrayList<>()).add(validator); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 注册全局完成处理器 | ||||
|      */ | ||||
|     public void registerGlobalCompleteProcessor(UploadCompleteProcessor processor) { | ||||
|         globalCompleteProcessors.add(processor); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 注册平台特定的完成处理器 | ||||
|      */ | ||||
|     public void registerPlatformCompleteProcessor(String platform, UploadCompleteProcessor processor) { | ||||
|         platformCompleteProcessors.computeIfAbsent(platform, k -> new CopyOnWriteArrayList<>()).add(processor); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取文件名生成器(平台 > 全局) | ||||
|      */ | ||||
|     public FileNameGenerator getNameGenerator(String platform) { | ||||
|         // 先查找平台特定的 | ||||
|         Map<String, FileNameGenerator> platformGenerators = platformNameGenerators.get(platform); | ||||
|         if (platformGenerators != null && !platformGenerators.isEmpty()) { | ||||
|             return platformGenerators.values() | ||||
|                 .stream() | ||||
|                 .max(Comparator.comparingInt(FileNameGenerator::getOrder)) | ||||
|                 .orElse(null); | ||||
|         } | ||||
|  | ||||
|         // 再查找全局的 | ||||
|         return globalNameGenerators.values() | ||||
|             .stream() | ||||
|             .max(Comparator.comparingInt(FileNameGenerator::getOrder)) | ||||
|             .orElse(null); | ||||
|     } | ||||
|  | ||||
|     public FilePathGenerator getPathGenerator(String platform) { | ||||
|         // 先查找平台特定的 | ||||
|         Map<String, FilePathGenerator> platformGenerators = platformPathGenerators.get(platform); | ||||
|         if (platformGenerators != null && !platformGenerators.isEmpty()) { | ||||
|             return platformGenerators.values() | ||||
|                 .stream() | ||||
|                 .max(Comparator.comparingInt(FilePathGenerator::getOrder)) | ||||
|                 .orElse(null); | ||||
|         } | ||||
|  | ||||
|         // 再查找全局的 | ||||
|         return globalPathGenerators.values() | ||||
|             .stream() | ||||
|             .max(Comparator.comparingInt(FilePathGenerator::getOrder)) | ||||
|             .orElse(null); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取缩略图处理器(平台 > 全局) | ||||
|      */ | ||||
|     public ThumbnailProcessor getThumbnailProcessor(String platform) { | ||||
|         // 先查找平台特定的 | ||||
|         Map<String, ThumbnailProcessor> platformProcessors = platformThumbnailProcessors.get(platform); | ||||
|         if (platformProcessors != null && !platformProcessors.isEmpty()) { | ||||
|             return platformProcessors.values() | ||||
|                 .stream() | ||||
|                 .max(Comparator.comparingInt(ThumbnailProcessor::getOrder)) | ||||
|                 .orElse(null); | ||||
|         } | ||||
|  | ||||
|         // 再查找全局的 | ||||
|         return globalThumbnailProcessors.values() | ||||
|             .stream() | ||||
|             .max(Comparator.comparingInt(ThumbnailProcessor::getOrder)) | ||||
|             .orElse(null); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取验证器列表(合并全局和平台) | ||||
|      */ | ||||
|     public List<FileValidator> getValidators(String platform) { | ||||
|         List<FileValidator> validators = new ArrayList<>(); | ||||
|  | ||||
|         // 先添加全局验证器 | ||||
|         validators.addAll(globalValidators); | ||||
|  | ||||
|         // 再添加平台特定验证器 | ||||
|         List<FileValidator> platformSpecific = platformValidators.get(platform); | ||||
|         if (platformSpecific != null) { | ||||
|             validators.addAll(platformSpecific); | ||||
|         } | ||||
|  | ||||
|         // 按优先级排序(优先级高的在前) | ||||
|         validators.sort(Comparator.comparingInt(FileValidator::getOrder).reversed()); | ||||
|  | ||||
|         return validators; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取完成处理器列表(合并全局和平台) | ||||
|      */ | ||||
|     public List<UploadCompleteProcessor> getCompleteProcessors(String platform) { | ||||
|         List<UploadCompleteProcessor> processors = new ArrayList<>(); | ||||
|  | ||||
|         // 先添加全局处理器 | ||||
|         processors.addAll(globalCompleteProcessors); | ||||
|  | ||||
|         // 再添加平台特定处理器 | ||||
|         List<UploadCompleteProcessor> platformSpecific = platformCompleteProcessors.get(platform); | ||||
|         if (platformSpecific != null) { | ||||
|             processors.addAll(platformSpecific); | ||||
|         } | ||||
|  | ||||
|         // 按优先级排序 | ||||
|         processors.sort(Comparator.comparingInt(UploadCompleteProcessor::getOrder).reversed()); | ||||
|  | ||||
|         return processors; | ||||
|     } | ||||
| } | ||||
| @@ -1,171 +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.core; | ||||
|  | ||||
| import top.continew.starter.storage.strategy.StorageStrategy; | ||||
| import top.continew.starter.storage.strategy.StorageStrategyOverride; | ||||
| import top.continew.starter.storage.strategy.impl.AbstractStorageStrategyOverride; | ||||
|  | ||||
| import java.lang.reflect.InvocationHandler; | ||||
| import java.lang.reflect.Method; | ||||
| import java.lang.reflect.Proxy; | ||||
| import java.util.*; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
| import java.util.concurrent.CopyOnWriteArrayList; | ||||
|  | ||||
| /** | ||||
|  * 策略代理工厂 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public class StrategyProxyFactory { | ||||
|  | ||||
|     private final Map<Class<?>, List<StorageStrategyOverride<?>>> overrides = new ConcurrentHashMap<>(); | ||||
|  | ||||
|     /** | ||||
|      * 注册重写 | ||||
|      */ | ||||
|     public <T extends StorageStrategy> void registerOverride(StorageStrategyOverride<T> override) { | ||||
|         overrides.computeIfAbsent(override.getTargetType(), k -> new CopyOnWriteArrayList<>()).add(override); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 创建代理 | ||||
|      */ | ||||
|     @SuppressWarnings("unchecked") | ||||
|     public <T extends StorageStrategy> T createProxy(T target) { | ||||
|         List<StorageStrategyOverride<?>> targetOverrides = overrides.get(target.getClass()); | ||||
|         if (targetOverrides == null || targetOverrides.isEmpty()) { | ||||
|             return target; | ||||
|         } | ||||
|  | ||||
|         // 为每个重写对象设置原始目标 | ||||
|         for (StorageStrategyOverride<?> override : targetOverrides) { | ||||
|             if (override instanceof AbstractStorageStrategyOverride) { | ||||
|                 ((AbstractStorageStrategyOverride<T>)override).setOriginalTarget(target); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return (T)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass() | ||||
|             .getInterfaces(), new StrategyInvocationHandler<>(target, targetOverrides)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 改进的调用处理器 | ||||
|      */ | ||||
|     private static class StrategyInvocationHandler<T extends StorageStrategy> implements InvocationHandler { | ||||
|         private final T target; | ||||
|         private final List<StorageStrategyOverride<?>> overrides; | ||||
|         private final Map<String, Method> overrideMethodCache = new ConcurrentHashMap<>(); | ||||
|  | ||||
|         public StrategyInvocationHandler(T target, List<StorageStrategyOverride<?>> overrides) { | ||||
|             this.target = target; | ||||
|             this.overrides = overrides; | ||||
|             cacheOverrideMethods(); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * 缓存重写方法 | ||||
|          */ | ||||
|         private void cacheOverrideMethods() { | ||||
|             for (StorageStrategyOverride<?> override : overrides) { | ||||
|                 Class<?> overrideClass = override.getClass(); | ||||
|  | ||||
|                 // 获取目标策略类的所有方法 | ||||
|                 Class<?> targetClass = override.getTargetType(); | ||||
|                 Method[] targetMethods = getAllMethods(targetClass); | ||||
|  | ||||
|                 for (Method targetMethod : targetMethods) { | ||||
|                     try { | ||||
|                         // 查找重写类中是否有相同签名的方法 | ||||
|                         Method overrideMethod = overrideClass.getMethod(targetMethod.getName(), targetMethod | ||||
|                             .getParameterTypes()); | ||||
|  | ||||
|                         // 检查方法是否真的被重写了(不是从接口继承的默认方法) | ||||
|                         if (isMethodOverridden(overrideMethod, overrideClass)) { | ||||
|                             overrideMethodCache.put(targetMethod | ||||
|                                 .getName() + getMethodSignature(targetMethod), overrideMethod); | ||||
|                         } | ||||
|                     } catch (NoSuchMethodException e) { | ||||
|                         // 重写类中没有这个方法,忽略 | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * 获取类及其所有接口的方法 | ||||
|          */ | ||||
|         private Method[] getAllMethods(Class<?> clazz) { | ||||
|  | ||||
|             // 添加类本身的方法 | ||||
|             Set<Method> methods = new HashSet<>(Arrays.asList(clazz.getMethods())); | ||||
|  | ||||
|             // 添加所有接口的方法 | ||||
|             for (Class<?> iface : clazz.getInterfaces()) { | ||||
|                 methods.addAll(Arrays.asList(iface.getMethods())); | ||||
|             } | ||||
|  | ||||
|             return methods.toArray(new Method[0]); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * 检查方法是否真的被重写了 | ||||
|          */ | ||||
|         private boolean isMethodOverridden(Method method, Class<?> overrideClass) { | ||||
|             // 如果方法声明在重写类中(而不是父类或接口),则认为是重写的 | ||||
|             return method.getDeclaringClass().equals(overrideClass) || (!method.getDeclaringClass() | ||||
|                 .isInterface() && !method.getDeclaringClass().equals(AbstractStorageStrategyOverride.class) && !method | ||||
|                     .getDeclaringClass() | ||||
|                     .equals(Object.class)); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * 获取方法签名 | ||||
|          */ | ||||
|         private String getMethodSignature(Method method) { | ||||
|             StringBuilder sb = new StringBuilder("("); | ||||
|             for (Class<?> paramType : method.getParameterTypes()) { | ||||
|                 sb.append(paramType.getName()).append(","); | ||||
|             } | ||||
|             if (sb.length() > 1) { | ||||
|                 sb.setLength(sb.length() - 1); | ||||
|             } | ||||
|             sb.append(")"); | ||||
|             return sb.toString(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { | ||||
|             String methodKey = method.getName() + getMethodSignature(method); | ||||
|             Method overrideMethod = overrideMethodCache.get(methodKey); | ||||
|  | ||||
|             if (overrideMethod != null) { | ||||
|                 // 找到重写方法,调用重写逻辑 | ||||
|                 for (StorageStrategyOverride<?> override : overrides) { | ||||
|                     if (overrideMethod.getDeclaringClass().equals(override.getClass())) { | ||||
|                         return overrideMethod.invoke(override, args); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // 没有重写,调用原方法 | ||||
|             return method.invoke(target, args); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -17,17 +17,16 @@ | ||||
| package top.continew.starter.storage.core; | ||||
|  | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
| import top.continew.starter.storage.model.context.UploadContext; | ||||
| import top.continew.starter.storage.model.req.MockMultipartFile; | ||||
| import top.continew.starter.storage.model.req.ThumbnailInfo; | ||||
| import top.continew.starter.storage.model.req.ThumbnailSize; | ||||
| import top.continew.starter.storage.model.resp.FileInfo; | ||||
| import top.continew.starter.storage.prehandle.*; | ||||
| import top.continew.starter.storage.util.StorageUtils; | ||||
| import top.continew.starter.storage.domain.model.context.UploadContext; | ||||
| import top.continew.starter.storage.domain.model.req.ThumbnailSize; | ||||
| import top.continew.starter.storage.domain.model.resp.FileInfo; | ||||
| import top.continew.starter.storage.processor.preprocess.*; | ||||
| import top.continew.starter.storage.processor.progress.UploadProgressListener; | ||||
| import top.continew.starter.storage.service.FileProcessor; | ||||
|  | ||||
| import java.io.InputStream; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.function.Consumer; | ||||
|  | ||||
| /** | ||||
|  * 上传预处理器,支持链式调用 | ||||
| @@ -39,257 +38,125 @@ public class UploadPretreatment { | ||||
|  | ||||
|     private final FileStorageService storageService; | ||||
|     private final UploadContext context; | ||||
|     private final List<FileValidator> validators = new ArrayList<>(); | ||||
|     private FileNameGenerator nameGenerator; | ||||
|     private FilePathGenerator pathGenerator; | ||||
|     private ThumbnailProcessor thumbnailProcessor; | ||||
|     private final List<UploadCompleteProcessor> completeProcessors = new ArrayList<>(); | ||||
|     private UploadProgressListener progressListener; | ||||
|     private final List<FileProcessor> processors = new ArrayList<>(); | ||||
|  | ||||
|     public UploadPretreatment(FileStorageService storageService, MultipartFile file) { | ||||
|         this.storageService = storageService; | ||||
|         this.context = new UploadContext(); | ||||
|         this.context.setFile(file); | ||||
|         // 设置默认平台 | ||||
|         this.context.setPlatform(storageService.getDefaultPlatform()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 设置存储平台 | ||||
|      * 设置平台 | ||||
|      * | ||||
|      * @param platform 站台 | ||||
|      * @param platform 平台 | ||||
|      * @return {@link UploadPretreatment } | ||||
|      */ | ||||
|     public UploadPretreatment setPlatform(String platform) { | ||||
|     public UploadPretreatment platform(String platform) { | ||||
|         context.setPlatform(platform); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 设置桶 | ||||
|      * 设置存储桶 | ||||
|      * | ||||
|      * @param bucket 桶 | ||||
|      * @param bucket 存储桶 | ||||
|      * @return {@link UploadPretreatment } | ||||
|      */ | ||||
|     public UploadPretreatment setBucket(String bucket) { | ||||
|     public UploadPretreatment bucket(String bucket) { | ||||
|         context.setBucket(bucket); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 设置路径 | ||||
|      * | ||||
|      * @param path 路径 | ||||
|      * @return {@link UploadPretreatment } | ||||
|      */ | ||||
|     public UploadPretreatment setPath(String path) { | ||||
|     public UploadPretreatment path(String path) { | ||||
|         context.setPath(path); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 设置文件名 | ||||
|      * 格式化文件名 - 不传则使用全局格式化 | ||||
|      * | ||||
|      * @param fileName 文件名 | ||||
|      * @return {@link UploadPretreatment } | ||||
|      */ | ||||
|     public UploadPretreatment setFileName(String fileName) { | ||||
|         context.setFileName(fileName); | ||||
|     public UploadPretreatment fileName(String fileName) { | ||||
|         context.setFormatFileName(fileName); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 添加元数据 | ||||
|      */ | ||||
|     public UploadPretreatment addMetadata(String key, String value) { | ||||
|     public UploadPretreatment metadata(String key, String value) { | ||||
|         context.getMetadata().put(key, value); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 添加扩展属性 | ||||
|      * 添加处理器 | ||||
|      * | ||||
|      * @param processor 处理器 | ||||
|      * @return {@link UploadPretreatment } | ||||
|      */ | ||||
|     public UploadPretreatment addAttribute(String key, Object value) { | ||||
|         context.getAttributes().put(key, value); | ||||
|     public UploadPretreatment processor(FileProcessor processor) { | ||||
|         processors.add(processor); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 启用缩略图 | ||||
|      * 设置缩略图 | ||||
|      * | ||||
|      * @param width  宽度 | ||||
|      * @param height 高度 | ||||
|      * @return {@link UploadPretreatment } | ||||
|      */ | ||||
|     public UploadPretreatment enableThumbnail(int width, int height) { | ||||
|     public UploadPretreatment thumbnail(int width, int height) { | ||||
|         context.setGenerateThumbnail(true); | ||||
|         context.setThumbnailSize(new ThumbnailSize(width, height)); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 添加验证器 | ||||
|      * 设置进度监听器 | ||||
|      */ | ||||
|     public UploadPretreatment addValidator(FileValidator validator) { | ||||
|         validators.add(validator); | ||||
|     public UploadPretreatment onProgress(UploadProgressListener listener) { | ||||
|         this.progressListener = listener; | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 设置文件名生成器 | ||||
|      * 设置简单的进度监听(只关心百分比) | ||||
|      */ | ||||
|     public UploadPretreatment setNameGenerator(FileNameGenerator generator) { | ||||
|         this.nameGenerator = generator; | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     public UploadPretreatment setPathGenerator(FilePathGenerator generator) { | ||||
|         this.pathGenerator = generator; | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 设置缩略图处理器 | ||||
|      */ | ||||
|     public UploadPretreatment setThumbnailProcessor(ThumbnailProcessor processor) { | ||||
|         this.thumbnailProcessor = processor; | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 添加上传完成处理器 | ||||
|      */ | ||||
|     public UploadPretreatment addCompleteProcessor(UploadCompleteProcessor processor) { | ||||
|         completeProcessors.add(processor); | ||||
|     public UploadPretreatment onProgress(Consumer<Integer> progressConsumer) { | ||||
|         this.progressListener = (bytesRead, totalBytes, percentage) -> progressConsumer.accept(percentage); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 执行上传 | ||||
|      * | ||||
|      * @return {@link FileInfo } | ||||
|      */ | ||||
|     public FileInfo upload() { | ||||
|         // 应用处理器 | ||||
|         applyProcessors(); | ||||
|  | ||||
|         // 执行验证 | ||||
|         validate(); | ||||
|  | ||||
|         // 生成默认存储桶(如果未设置) | ||||
|         if (context.getBucket() == null || context.getBucket().trim().isEmpty()) { | ||||
|             context.setBucket(generateDefaultBucket()); | ||||
|         for (FileProcessor processor : processors) { | ||||
|             storageService.addProcessor(processor); | ||||
|         } | ||||
|  | ||||
|         // 生成文件名 | ||||
|         if (context.getFileName() == null) { | ||||
|             context.setFileName(generateFileName()); | ||||
|         } | ||||
|  | ||||
|         // 生成文件路径 | ||||
|         if (context.getPath() == null) { | ||||
|             context.setPath(generateFilePath()); | ||||
|         // 设置进度监听器 | ||||
|         if (progressListener != null) { | ||||
|             storageService.onProgress(progressListener); | ||||
|         } | ||||
|  | ||||
|         // 执行上传 | ||||
|         FileInfo fileInfo = storageService.doUpload(context); | ||||
|  | ||||
|         // 处理缩略图 | ||||
|         if (context.isGenerateThumbnail()) { | ||||
|             processThumbnail(fileInfo); | ||||
|         } | ||||
|  | ||||
|         // 触发完成事件 | ||||
|         triggerCompleteEvent(fileInfo); | ||||
|  | ||||
|         return fileInfo; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 应用处理器 | ||||
|      */ | ||||
|     private void applyProcessors() { | ||||
|         // 从存储服务获取全局处理器 | ||||
|         ProcessorRegistry registry = storageService.getProcessorRegistry(); | ||||
|  | ||||
|         // 合并处理器:自定义 > 平台 > 全局 | ||||
|         if (nameGenerator == null) { | ||||
|             nameGenerator = registry.getNameGenerator(context.getPlatform()); | ||||
|         } | ||||
|  | ||||
|         if (pathGenerator == null) { | ||||
|             pathGenerator = registry.getPathGenerator(context.getPlatform()); | ||||
|         } | ||||
|  | ||||
|         if (thumbnailProcessor == null && context.isGenerateThumbnail()) { | ||||
|             thumbnailProcessor = registry.getThumbnailProcessor(context.getPlatform()); | ||||
|         } | ||||
|  | ||||
|         // 合并验证器 | ||||
|         validators.addAll(0, registry.getValidators(context.getPlatform())); | ||||
|  | ||||
|         // 合并完成处理器 | ||||
|         completeProcessors.addAll(0, registry.getCompleteProcessors(context.getPlatform())); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 执行验证 | ||||
|      */ | ||||
|     private void validate() { | ||||
|         for (FileValidator validator : validators) { | ||||
|             if (validator.support(context)) { | ||||
|                 validator.validate(context); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 生成文件名 | ||||
|      */ | ||||
|     private String generateFileName() { | ||||
|         if (nameGenerator != null && nameGenerator.support(context)) { | ||||
|             return nameGenerator.generate(context); | ||||
|         } | ||||
|         return StorageUtils.generateFileName(context.getFile().getOriginalFilename()); | ||||
|     } | ||||
|  | ||||
|     private String generateFilePath() { | ||||
|         if (pathGenerator != null && pathGenerator.support(context)) { | ||||
|             return pathGenerator.path(context); | ||||
|         } | ||||
|         // 默认使用时间戳 | ||||
|         return StorageUtils.generatePath(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 生成默认存储桶 | ||||
|      */ | ||||
|     private String generateDefaultBucket() { | ||||
|         return storageService.getDefaultBucket(context.getPlatform()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理缩略图 | ||||
|      * | ||||
|      * @param fileInfo 文件信息 | ||||
|      */ | ||||
|     private void processThumbnail(FileInfo fileInfo) { | ||||
|         if (thumbnailProcessor != null && thumbnailProcessor.support(context)) { | ||||
|             try (InputStream is = storageService.download(context.getPlatform(), fileInfo.getPath())) { | ||||
|                 ThumbnailInfo thumbnailInfo = thumbnailProcessor.process(context, is); | ||||
|                 // 上传缩略图 | ||||
|                 String thumbnailPath = fileInfo.getPath() + "_thumb." + thumbnailInfo.getFormat(); | ||||
|                 // 创建模拟的文件信息 | ||||
|                 MockMultipartFile thumbnailFile = new MockMultipartFile("thumbnail", "thumbnail." + thumbnailInfo | ||||
|                     .getFormat(), "image/" + thumbnailInfo.getFormat(), thumbnailInfo.getData()); | ||||
|  | ||||
|                 storageService.upload(context.getPlatform(), context.getBucket(), thumbnailPath, thumbnailFile); | ||||
|                 fileInfo.setThumbnailPath(thumbnailPath); | ||||
|             } catch (Exception e) { | ||||
|  | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 触发完成事件 | ||||
|      */ | ||||
|     private void triggerCompleteEvent(FileInfo fileInfo) { | ||||
|         for (UploadCompleteProcessor processor : completeProcessors) { | ||||
|             if (processor.support(context)) { | ||||
|                 try { | ||||
|                     processor.onComplete(fileInfo); | ||||
|                 } catch (Exception e) { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return storageService.upload(context); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,247 @@ | ||||
| /* | ||||
|  * 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.domain.file; | ||||
|  | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
|  | ||||
| import java.io.ByteArrayInputStream; | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.nio.file.Files; | ||||
|  | ||||
| /** | ||||
|  * 增强版 MultipartFile 实现,支持缓存和包装 | ||||
|  * 功能特性: | ||||
|  * 1. 缓存功能:避免重复读取文件内容 | ||||
|  * 2. 包装模式:可以包装现有的 MultipartFile | ||||
|  * 3. 创建模式:可以直接从字节数组创建 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public class EnhancedMultipartFile implements MultipartFile { | ||||
|  | ||||
|     private final MultipartFile originalFile; | ||||
|     private final String name; | ||||
|     private final String originalFilename; | ||||
|     private final String contentType; | ||||
|     private byte[] cachedBytes; | ||||
|     private final boolean cacheEnabled; | ||||
|     private final boolean isWrapped; | ||||
|  | ||||
|     /** | ||||
|      * 包装模式构造器 - 包装现有的 MultipartFile | ||||
|      */ | ||||
|     public EnhancedMultipartFile(MultipartFile originalFile, boolean enableCache) { | ||||
|         this.originalFile = originalFile; | ||||
|         this.name = originalFile.getName(); | ||||
|         this.originalFilename = originalFile.getOriginalFilename(); | ||||
|         this.contentType = originalFile.getContentType(); | ||||
|         this.cacheEnabled = enableCache; | ||||
|         this.isWrapped = true; | ||||
|         this.cachedBytes = null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 创建模式构造器 - 直接从字节数组创建 | ||||
|      */ | ||||
|     public EnhancedMultipartFile(String name, String originalFilename, String contentType, byte[] content) { | ||||
|         this.originalFile = null; | ||||
|         this.name = name; | ||||
|         this.originalFilename = originalFilename; | ||||
|         this.contentType = contentType; | ||||
|         this.cachedBytes = content; | ||||
|         this.cacheEnabled = false; | ||||
|         this.isWrapped = false; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 便捷的静态工厂方法 - 包装已有文件并启用缓存 | ||||
|      */ | ||||
|     public static EnhancedMultipartFile wrap(MultipartFile file, boolean enableCache) { | ||||
|         if (file instanceof EnhancedMultipartFile) { | ||||
|             return (EnhancedMultipartFile)file; | ||||
|         } | ||||
|         return new EnhancedMultipartFile(file, enableCache); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 便捷的静态工厂方法 - 包装已有文件(不启用缓存) | ||||
|      */ | ||||
|     public static EnhancedMultipartFile wrap(MultipartFile file) { | ||||
|         return wrap(file, false); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 便捷的静态工厂方法 - 创建新文件 | ||||
|      */ | ||||
|     public static EnhancedMultipartFile create(String name, | ||||
|                                                String originalFilename, | ||||
|                                                String contentType, | ||||
|                                                byte[] content) { | ||||
|         return new EnhancedMultipartFile(name, originalFilename, contentType, content); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getName() { | ||||
|         return name; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getOriginalFilename() { | ||||
|         return originalFilename; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getContentType() { | ||||
|         return contentType; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean isEmpty() { | ||||
|         if (isWrapped) { | ||||
|             return originalFile.isEmpty(); | ||||
|         } | ||||
|         return cachedBytes == null || cachedBytes.length == 0; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long getSize() { | ||||
|         if (cachedBytes != null) { | ||||
|             return cachedBytes.length; | ||||
|         } | ||||
|         if (isWrapped) { | ||||
|             return originalFile.getSize(); | ||||
|         } | ||||
|         return 0; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public byte[] getBytes() throws IOException { | ||||
|         // 缓存模式下的处理 | ||||
|         if (cacheEnabled) { | ||||
|             return getBytesWithCache(); | ||||
|         } | ||||
|  | ||||
|         // 非缓存模式下的处理 | ||||
|         return getBytesWithoutCache(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public InputStream getInputStream() throws IOException { | ||||
|         // 如果已有缓存数据,直接使用 | ||||
|         if (cachedBytes != null) { | ||||
|             return new ByteArrayInputStream(cachedBytes); | ||||
|         } | ||||
|  | ||||
|         // 缓存模式且需要缓存 | ||||
|         if (cacheEnabled && isWrapped) { | ||||
|             loadToCache(); | ||||
|             return new ByteArrayInputStream(cachedBytes); | ||||
|         } | ||||
|  | ||||
|         // 非缓存模式且是包装模式 | ||||
|         if (isWrapped) { | ||||
|             return originalFile.getInputStream(); | ||||
|         } | ||||
|  | ||||
|         // 创建模式(理论上不应该到这里,因为创建模式下cachedBytes应该有值) | ||||
|         return new ByteArrayInputStream(new byte[0]); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void transferTo(File dest) throws IOException, IllegalStateException { | ||||
|         if (cachedBytes != null) { | ||||
|             // 使用缓存的数据 | ||||
|             Files.write(dest.toPath(), cachedBytes); | ||||
|         } else if (isWrapped) { | ||||
|             // 使用原始文件 | ||||
|             originalFile.transferTo(dest); | ||||
|         } else { | ||||
|             // 创建模式但没有数据 | ||||
|             dest.createNewFile(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 清理缓存 | ||||
|      */ | ||||
|     public void clearCache() { | ||||
|         if (cacheEnabled && isWrapped) { | ||||
|             cachedBytes = null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 判断是否已缓存 | ||||
|      */ | ||||
|     public boolean isCached() { | ||||
|         return cachedBytes != null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取缓存的字节数组(不触发加载) | ||||
|      */ | ||||
|     public byte[] getCachedBytes() { | ||||
|         return cachedBytes; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 判断是否为包装模式 | ||||
|      */ | ||||
|     public boolean isWrapped() { | ||||
|         return isWrapped; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 判断是否启用缓存 | ||||
|      */ | ||||
|     public boolean isCacheEnabled() { | ||||
|         return cacheEnabled; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取字节数组 - 带缓存 | ||||
|      */ | ||||
|     private byte[] getBytesWithCache() throws IOException { | ||||
|         if (cachedBytes == null && isWrapped) { | ||||
|             loadToCache(); | ||||
|         } | ||||
|         return cachedBytes; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取字节数组 - 不带缓存 | ||||
|      */ | ||||
|     private byte[] getBytesWithoutCache() throws IOException { | ||||
|         if (isWrapped) { | ||||
|             return originalFile.getBytes(); | ||||
|         } | ||||
|         // 创建模式下直接返回内容 | ||||
|         return cachedBytes; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 加载文件内容到缓存 | ||||
|      */ | ||||
|     private void loadToCache() throws IOException { | ||||
|         if (isWrapped && originalFile != null) { | ||||
|             cachedBytes = originalFile.getBytes(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -14,12 +14,12 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.core; | ||||
| package top.continew.starter.storage.domain.file; | ||||
| 
 | ||||
| import cn.hutool.json.JSONUtil; | ||||
| import org.springframework.http.MediaType; | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
| import top.continew.starter.json.jackson.util.JSONUtils; | ||||
| import top.continew.starter.storage.exception.StorageException; | ||||
| import top.continew.starter.storage.model.req.MockMultipartFile; | ||||
| import top.continew.starter.storage.common.exception.StorageException; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| @@ -133,7 +133,7 @@ public class FileWrapper { | ||||
|         String json = convertToJson(obj); | ||||
|         byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8); | ||||
|         String finalFilename = filename != null ? filename : "data.json"; | ||||
|         String finalContentType = contentType != null ? contentType : "application/json"; | ||||
|         String finalContentType = contentType != null ? contentType : MediaType.APPLICATION_JSON_VALUE; | ||||
| 
 | ||||
|         return of(jsonBytes, finalFilename, finalContentType); | ||||
|     } | ||||
| @@ -147,13 +147,13 @@ public class FileWrapper { | ||||
|         } | ||||
| 
 | ||||
|         if (bytes != null) { | ||||
|             return new MockMultipartFile(getFilenameWithoutExtension(originalFilename), originalFilename, contentType, bytes); | ||||
|             return new EnhancedMultipartFile(getFilenameWithoutExtension(originalFilename), originalFilename, contentType, bytes); | ||||
|         } | ||||
| 
 | ||||
|         if (inputStream != null) { | ||||
|             try { | ||||
|                 byte[] data = inputStream.readAllBytes(); | ||||
|                 return new MockMultipartFile(getFilenameWithoutExtension(originalFilename), originalFilename, contentType, data); | ||||
|                 return new EnhancedMultipartFile(getFilenameWithoutExtension(originalFilename), originalFilename, contentType, data); | ||||
|             } catch (IOException e) { | ||||
|                 throw new StorageException("读取输入流失败", e); | ||||
|             } | ||||
| @@ -169,7 +169,7 @@ public class FileWrapper { | ||||
| 
 | ||||
|     private static String convertToJson(Object obj) { | ||||
|         try { | ||||
|             return JSONUtils.toJsonStr(obj); | ||||
|             return JSONUtil.toJsonStr(obj); | ||||
|         } catch (Exception e) { | ||||
|             throw new StorageException("对象转换为 JSON 失败", e); | ||||
|         } | ||||
| @@ -0,0 +1,118 @@ | ||||
| /* | ||||
|  * 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.domain.file; | ||||
|  | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
| import top.continew.starter.storage.processor.progress.ProgressInputStream; | ||||
| import top.continew.starter.storage.processor.progress.ProgressTracker; | ||||
| import top.continew.starter.storage.processor.progress.UploadProgressListener; | ||||
|  | ||||
| import java.io.*; | ||||
|  | ||||
| /** | ||||
|  * 进度监听 MultipartFile 包装器 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public class ProgressAwareMultipartFile extends EnhancedMultipartFile { | ||||
|  | ||||
|     private final UploadProgressListener progressListener; | ||||
|     private volatile ProgressTracker progressTracker; | ||||
|     private volatile boolean progressEnabled = false; | ||||
|     private final Object progressLock = new Object(); | ||||
|  | ||||
|     // 用于区分不同的读取阶段 | ||||
|     public enum ReadPhase { | ||||
|         VALIDATION,     // 验证阶段 | ||||
|         THUMBNAIL,      // 缩略图生成 | ||||
|         UPLOAD          // 实际上传 | ||||
|     } | ||||
|  | ||||
|     private volatile ReadPhase currentPhase = ReadPhase.VALIDATION; | ||||
|  | ||||
|     public ProgressAwareMultipartFile(MultipartFile originalFile, | ||||
|                                       boolean enableCache, | ||||
|                                       UploadProgressListener progressListener) { | ||||
|         super(originalFile, enableCache); | ||||
|         this.progressListener = progressListener; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 设置当前读取阶段 | ||||
|      */ | ||||
|     public void setReadPhase(ReadPhase phase) { | ||||
|         synchronized (progressLock) { | ||||
|             this.currentPhase = phase; | ||||
|             // 只在上传阶段启用进度 | ||||
|             this.progressEnabled = (phase == ReadPhase.UPLOAD); | ||||
|  | ||||
|             // 切换到上传阶段时,重置进度追踪器 | ||||
|             if (phase == ReadPhase.UPLOAD && progressListener != null) { | ||||
|                 this.progressTracker = new ProgressTracker(getSize(), progressListener); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public InputStream getInputStream() throws IOException { | ||||
|         InputStream originalStream = super.getInputStream(); | ||||
|  | ||||
|         synchronized (progressLock) { | ||||
|             // 只有在上传阶段且有监听器时才包装流 | ||||
|             if (progressEnabled && progressTracker != null) { | ||||
|                 return new ProgressInputStream(originalStream, progressTracker); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return originalStream; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void transferTo(File dest) throws IOException, IllegalStateException { | ||||
|         if (progressEnabled && progressTracker != null) { | ||||
|             // 使用带进度的传输 | ||||
|             try (InputStream in = getInputStream(); OutputStream out = new FileOutputStream(dest)) { | ||||
|                 byte[] buffer = new byte[8192]; | ||||
|                 int bytesRead; | ||||
|                 while ((bytesRead = in.read(buffer)) != -1) { | ||||
|                     out.write(buffer, 0, bytesRead); | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             super.transferTo(dest); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取不带进度监听的输入流(向后兼容) | ||||
|      */ | ||||
|     public InputStream getInputStreamWithoutProgress() throws IOException { | ||||
|         return super.getInputStream(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 清理资源 | ||||
|      */ | ||||
|     @Override | ||||
|     public void clearCache() { | ||||
|         super.clearCache(); | ||||
|         synchronized (progressLock) { | ||||
|             progressTracker = null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -14,10 +14,11 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.model.context; | ||||
| package top.continew.starter.storage.domain.model.context; | ||||
| 
 | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
| import top.continew.starter.storage.model.req.ThumbnailSize; | ||||
| import top.continew.starter.storage.domain.model.req.ThumbnailSize; | ||||
| import top.continew.starter.storage.processor.progress.UploadProgressListener; | ||||
| 
 | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| @@ -51,9 +52,9 @@ public class UploadContext { | ||||
|     private String path; | ||||
| 
 | ||||
|     /** | ||||
|      * 文件名 | ||||
|      * 格式化文件名 为空则默认使用格式化规则格式化 | ||||
|      */ | ||||
|     private String fileName; | ||||
|     private String formatFileName; | ||||
| 
 | ||||
|     /** | ||||
|      * 是否生成缩略图 | ||||
| @@ -75,11 +76,16 @@ public class UploadContext { | ||||
|      */ | ||||
|     private Map<String, Object> attributes = new HashMap<>(); | ||||
| 
 | ||||
|     /** | ||||
|      * 进度监听器 | ||||
|      */ | ||||
|     private UploadProgressListener progressListener; | ||||
| 
 | ||||
|     /** | ||||
|      * 获取完整路径 | ||||
|      */ | ||||
|     public String getFullPath() { | ||||
|         return path + fileName; | ||||
|         return path + formatFileName; | ||||
|     } | ||||
| 
 | ||||
|     public MultipartFile getFile() { | ||||
| @@ -114,12 +120,12 @@ public class UploadContext { | ||||
|         this.path = path; | ||||
|     } | ||||
| 
 | ||||
|     public String getFileName() { | ||||
|         return fileName; | ||||
|     public String getFormatFileName() { | ||||
|         return formatFileName; | ||||
|     } | ||||
| 
 | ||||
|     public void setFileName(String fileName) { | ||||
|         this.fileName = fileName; | ||||
|     public void setFormatFileName(String formatFileName) { | ||||
|         this.formatFileName = formatFileName; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isGenerateThumbnail() { | ||||
| @@ -153,4 +159,12 @@ public class UploadContext { | ||||
|     public void setAttributes(Map<String, Object> attributes) { | ||||
|         this.attributes = attributes; | ||||
|     } | ||||
| 
 | ||||
|     public UploadProgressListener getProgressListener() { | ||||
|         return progressListener; | ||||
|     } | ||||
| 
 | ||||
|     public void setProgressListener(UploadProgressListener progressListener) { | ||||
|         this.progressListener = progressListener; | ||||
|     } | ||||
| } | ||||
| @@ -14,7 +14,7 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.model.req; | ||||
| package top.continew.starter.storage.domain.model.req; | ||||
| 
 | ||||
| /** | ||||
|  * 缩略图信息 | ||||
| @@ -59,6 +59,4 @@ public class ThumbnailInfo { | ||||
|     public void setHeight(int height) { | ||||
|         this.height = height; | ||||
|     } | ||||
| 
 | ||||
|     // getter/setter省略 | ||||
| } | ||||
| @@ -14,7 +14,7 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.model.req; | ||||
| package top.continew.starter.storage.domain.model.req; | ||||
| 
 | ||||
| /** | ||||
|  * 缩略图尺寸 | ||||
| @@ -14,7 +14,7 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.model.resp; | ||||
| package top.continew.starter.storage.domain.model.resp; | ||||
| 
 | ||||
| import java.time.LocalDateTime; | ||||
| import java.util.Map; | ||||
| @@ -62,6 +62,11 @@ public class FileInfo { | ||||
|      */ | ||||
|     private String thumbnailPath; | ||||
| 
 | ||||
|     /** | ||||
|      * 缩略图大小 | ||||
|      */ | ||||
|     private Long thumbnailSize; | ||||
| 
 | ||||
|     /** | ||||
|      * 完整路径 | ||||
|      */ | ||||
| @@ -172,6 +177,14 @@ public class FileInfo { | ||||
|         this.thumbnailPath = thumbnailPath; | ||||
|     } | ||||
| 
 | ||||
|     public Long getThumbnailSize() { | ||||
|         return thumbnailSize; | ||||
|     } | ||||
| 
 | ||||
|     public void setThumbnailSize(Long thumbnailSize) { | ||||
|         this.thumbnailSize = thumbnailSize; | ||||
|     } | ||||
| 
 | ||||
|     public String getUrl() { | ||||
|         return url; | ||||
|     } | ||||
| @@ -14,7 +14,7 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.model.resp; | ||||
| package top.continew.starter.storage.domain.model.resp; | ||||
| 
 | ||||
| import java.time.LocalDateTime; | ||||
| 
 | ||||
| @@ -14,7 +14,7 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.model.resp; | ||||
| package top.continew.starter.storage.domain.model.resp; | ||||
| 
 | ||||
| /** | ||||
|  * 分片上传初始化结果 | ||||
| @@ -14,7 +14,7 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.model.resp; | ||||
| package top.continew.starter.storage.domain.model.resp; | ||||
| 
 | ||||
| /** | ||||
|  * 分片上传结果 | ||||
| @@ -14,7 +14,7 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.model.resp; | ||||
| package top.continew.starter.storage.domain.model.resp; | ||||
| 
 | ||||
| /** | ||||
|  * 存储策略状态 | ||||
| @@ -22,7 +22,7 @@ package top.continew.starter.storage.model.resp; | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public class StrategyStatus { | ||||
| public class StrategyStatusResp { | ||||
| 
 | ||||
|     /** | ||||
|      * 平台 | ||||
| @@ -48,11 +48,11 @@ public class StrategyStatus { | ||||
|      */ | ||||
|     private String description; | ||||
| 
 | ||||
|     public StrategyStatus(String platform, | ||||
|                           boolean hasConfig, | ||||
|                           boolean hasDynamic, | ||||
|                           String activeType, | ||||
|                           String description) { | ||||
|     public StrategyStatusResp(String platform, | ||||
|                               boolean hasConfig, | ||||
|                               boolean hasDynamic, | ||||
|                               String activeType, | ||||
|                               String description) { | ||||
|         this.platform = platform; | ||||
|         this.hasConfig = hasConfig; | ||||
|         this.hasDynamic = hasDynamic; | ||||
| @@ -0,0 +1,196 @@ | ||||
| /* | ||||
|  * 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.engine; | ||||
|  | ||||
| import jakarta.annotation.PostConstruct; | ||||
| import org.springframework.context.ApplicationContext; | ||||
| import org.springframework.context.ApplicationEvent; | ||||
| import top.continew.starter.storage.strategy.StorageStrategy; | ||||
| import top.continew.starter.storage.strategy.impl.StorageStrategyDecorator; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Comparator; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| /** | ||||
|  * 存储装饰器管理器 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public class StorageDecoratorManager { | ||||
|  | ||||
|     private final ApplicationContext applicationContext; | ||||
|  | ||||
|     private final Map<Class<? extends StorageStrategy>, List<StorageStrategyDecorator<?>>> decoratorMap = new ConcurrentHashMap<>(); | ||||
|  | ||||
|     private volatile boolean initialized = false; | ||||
|  | ||||
|     public StorageDecoratorManager(ApplicationContext applicationContext) { | ||||
|         this.applicationContext = applicationContext; | ||||
|     } | ||||
|  | ||||
|     @PostConstruct | ||||
|     public void init() { | ||||
|         if (initialized) { | ||||
|             return; | ||||
|         } | ||||
|         Map<String, StorageStrategyDecorator> decorators = applicationContext | ||||
|             .getBeansOfType(StorageStrategyDecorator.class); | ||||
|  | ||||
|         for (Map.Entry<String, StorageStrategyDecorator> entry : decorators.entrySet()) { | ||||
|             StorageStrategyDecorator<?> decorator = entry.getValue(); | ||||
|             Class<?> targetClass = decorator.getTargetStrategyClass(); | ||||
|  | ||||
|             if (targetClass != null) { | ||||
|                 decoratorMap.computeIfAbsent((Class<? extends StorageStrategy>)targetClass, k -> new ArrayList<>()) | ||||
|                     .add(decorator); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // 按优先级排序 | ||||
|         decoratorMap.values().forEach(list -> list.sort(Comparator.comparingInt(StorageStrategyDecorator::getOrder))); | ||||
|         this.initialized = true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 应用装饰器到策略实例 | ||||
|      * | ||||
|      * @param strategy 存储策略实例 | ||||
|      * @return {@link StorageStrategy } | ||||
|      */ | ||||
|     @SuppressWarnings("unchecked") | ||||
|     public StorageStrategy applyDecorators(StorageStrategy strategy) { | ||||
|         if (!initialized) { | ||||
|             init(); | ||||
|         } | ||||
|         if (strategy == null) { | ||||
|             return null; | ||||
|         } | ||||
|         Class<? extends StorageStrategy> strategyClass = strategy.getClass(); | ||||
|         List<StorageStrategyDecorator<?>> decorators = findApplicableDecorators(strategyClass); | ||||
|         if (decorators.isEmpty()) { | ||||
|             return strategy; | ||||
|         } | ||||
|  | ||||
|         // 应用装饰器链 | ||||
|         StorageStrategy decorated = strategy; | ||||
|         for (StorageStrategyDecorator decorator : decorators) { | ||||
|             decorator.setDelegate(decorated); | ||||
|             decorated = decorator; | ||||
|         } | ||||
|  | ||||
|         return decorated; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 查找适用的装饰器 | ||||
|      * | ||||
|      * @param strategyClass 策略类 | ||||
|      * @return {@link List }<{@link StorageStrategyDecorator }<{@link ? }>> | ||||
|      */ | ||||
|     private List<StorageStrategyDecorator<?>> findApplicableDecorators(Class<? extends StorageStrategy> strategyClass) { | ||||
|         List<StorageStrategyDecorator<?>> result = new ArrayList<>(); | ||||
|  | ||||
|         // 精确匹配 | ||||
|         List<StorageStrategyDecorator<?>> exactMatch = decoratorMap.get(strategyClass); | ||||
|         if (exactMatch != null) { | ||||
|             result.addAll(exactMatch); | ||||
|         } | ||||
|  | ||||
|         // 继承匹配 | ||||
|         for (Map.Entry<Class<? extends StorageStrategy>, List<StorageStrategyDecorator<?>>> entry : decoratorMap | ||||
|             .entrySet()) { | ||||
|             if (entry.getKey() != strategyClass && entry.getKey().isAssignableFrom(strategyClass)) { | ||||
|                 result.addAll(entry.getValue()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // 去重并排序 | ||||
|         return result.stream() | ||||
|             .distinct() | ||||
|             .sorted(Comparator.comparingInt(StorageStrategyDecorator::getOrder)) | ||||
|             .collect(Collectors.toList()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 动态注册装饰器 | ||||
|      * | ||||
|      * @param decorator 装饰器 | ||||
|      */ | ||||
|     public void registerDecorator(StorageStrategyDecorator<?> decorator) { | ||||
|         Class<?> targetClass = decorator.getTargetStrategyClass(); | ||||
|         if (targetClass != null) { | ||||
|             decoratorMap.computeIfAbsent((Class<? extends StorageStrategy>)targetClass, k -> new ArrayList<>()) | ||||
|                 .add(decorator); | ||||
|             // 重新排序 | ||||
|             decoratorMap.get((Class<? extends StorageStrategy>)targetClass) | ||||
|                 .sort(Comparator.comparingInt(StorageStrategyDecorator::getOrder)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 移除装饰器 | ||||
|      * | ||||
|      * @param decorator 装饰器 | ||||
|      */ | ||||
|     public void unregisterDecorator(StorageStrategyDecorator<?> decorator) { | ||||
|         Class<?> targetClass = decorator.getTargetStrategyClass(); | ||||
|         if (targetClass != null) { | ||||
|             List<StorageStrategyDecorator<?>> decorators = decoratorMap | ||||
|                 .get((Class<? extends StorageStrategy>)targetClass); | ||||
|             if (decorators != null) { | ||||
|                 decorators.remove(decorator); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 装饰器注册事件 | ||||
|      */ | ||||
|     public static class DecoratorRegisteredEvent extends ApplicationEvent { | ||||
|         private final Class<?> targetClass; | ||||
|  | ||||
|         public DecoratorRegisteredEvent(Object source, Class<?> targetClass) { | ||||
|             super(source); | ||||
|             this.targetClass = targetClass; | ||||
|         } | ||||
|  | ||||
|         public Class<?> getTargetClass() { | ||||
|             return targetClass; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 装饰器注销事件 | ||||
|      */ | ||||
|     public static class DecoratorUnregisteredEvent extends ApplicationEvent { | ||||
|         private final Class<?> targetClass; | ||||
|  | ||||
|         public DecoratorUnregisteredEvent(Object source, Class<?> targetClass) { | ||||
|             super(source); | ||||
|             this.targetClass = targetClass; | ||||
|         } | ||||
|  | ||||
|         public Class<?> getTargetClass() { | ||||
|             return targetClass; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -14,7 +14,7 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.router; | ||||
| package top.continew.starter.storage.engine; | ||||
| 
 | ||||
| import top.continew.starter.storage.strategy.StorageStrategy; | ||||
| 
 | ||||
| @@ -0,0 +1,240 @@ | ||||
| /* | ||||
|  * 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.engine; | ||||
|  | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.springframework.context.ApplicationEvent; | ||||
| import org.springframework.context.ApplicationListener; | ||||
| import top.continew.starter.storage.autoconfigure.properties.StorageProperties; | ||||
| import top.continew.starter.storage.common.constant.StorageConstant; | ||||
| import top.continew.starter.storage.common.enums.DefaultStorageSource; | ||||
| import top.continew.starter.storage.common.exception.StorageException; | ||||
| import top.continew.starter.storage.domain.model.resp.StrategyStatusResp; | ||||
| import top.continew.starter.storage.strategy.StorageStrategy; | ||||
|  | ||||
| import java.util.*; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
|  | ||||
| /** | ||||
|  * 存储策略路由器 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public class StorageStrategyRouter implements ApplicationListener<ApplicationEvent> { | ||||
|  | ||||
|     private static final Logger log = LoggerFactory.getLogger(StorageStrategyRouter.class); | ||||
|  | ||||
|     private final Map<String, StorageStrategy> configStrategies = new ConcurrentHashMap<>(); | ||||
|     private final Map<String, StorageStrategy> dynamicStrategies = new ConcurrentHashMap<>(); | ||||
|     private final Map<String, StorageStrategy> decoratedStrategies = new ConcurrentHashMap<>(); | ||||
|  | ||||
|     private final StorageProperties storageProperties; | ||||
|     private final String configDefaultPlatform; | ||||
|     private volatile String dynamicDefaultPlatform; | ||||
|     private final StorageDecoratorManager decoratorManager; | ||||
|  | ||||
|     public StorageStrategyRouter(List<StorageStrategyRegistrar> registrars, | ||||
|                                  StorageProperties storageProperties, | ||||
|                                  StorageDecoratorManager decoratorManager) { | ||||
|         this.decoratorManager = decoratorManager; | ||||
|         this.storageProperties = storageProperties; | ||||
|         List<StorageStrategy> strategies = new ArrayList<>(); | ||||
|         for (StorageStrategyRegistrar registrar : registrars) { | ||||
|             registrar.register(strategies); | ||||
|         } | ||||
|  | ||||
|         // 配置文件加载的策略 | ||||
|         for (StorageStrategy strategy : strategies) { | ||||
|             configStrategies.put(strategy.getPlatform(), strategy); | ||||
|         } | ||||
|  | ||||
|         // 加载配置文件默认存储 | ||||
|         this.configDefaultPlatform = storageProperties.getDefaultPlatform(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 存储选择(支持装饰器) | ||||
|      */ | ||||
|     public StorageStrategy route(String platform) { | ||||
|         // 1. 先检查缓存的装饰后策略 | ||||
|         StorageStrategy decorated = decoratedStrategies.get(platform); | ||||
|         if (decorated != null) { | ||||
|             return decorated; | ||||
|         } | ||||
|         // 2. 获取原始策略 | ||||
|         StorageStrategy strategy = getOriginalStrategy(platform); | ||||
|         // 3. 应用装饰器 | ||||
|         StorageStrategy decoratedStrategy = applyDecoratorsIfAvailable(strategy); | ||||
|         // 4. 缓存装饰后的策略 | ||||
|         decoratedStrategies.put(platform, decoratedStrategy); | ||||
|         return decoratedStrategy; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取原始策略 | ||||
|      */ | ||||
|     private StorageStrategy getOriginalStrategy(String platform) { | ||||
|         return Optional.ofNullable(dynamicStrategies.get(platform)) | ||||
|                 .or(() -> Optional.ofNullable(configStrategies.get(platform))) | ||||
|                 .orElseThrow(() -> new StorageException(String.format("不支持的存储平台: %s", platform))); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 应用装饰器 | ||||
|      */ | ||||
|     private StorageStrategy applyDecoratorsIfAvailable(StorageStrategy strategy) { | ||||
|         return ObjectUtil.isNotEmpty(decoratorManager) ? decoratorManager.applyDecorators(strategy) : strategy; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 动态注册策略 - 支持装饰器注册 | ||||
|      */ | ||||
|     public void registerDynamic(StorageStrategy strategy) { | ||||
|         String platform = strategy.getPlatform(); | ||||
|         if (dynamicStrategies.containsKey(platform)) { | ||||
|             throw new StorageException("动态策略 platform 已存在: " + platform); | ||||
|         } | ||||
|         dynamicStrategies.put(platform, strategy); | ||||
|         // 清除装饰器缓存,确保下次获取时重新应用装饰器 | ||||
|         decoratedStrategies.remove(platform); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 卸载动态策略 | ||||
|      */ | ||||
|     public boolean unloadDynamic(String platform) { | ||||
|         StorageStrategy strategy = dynamicStrategies.remove(platform); | ||||
|         if (strategy == null) { | ||||
|             return false; | ||||
|         } | ||||
|         decoratedStrategies.remove(platform); | ||||
|         try { | ||||
|             strategy.cleanup(); | ||||
|         } catch (Exception e) { | ||||
|             log.error("清理存储策略失败: platform={}", platform, e); | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 监听装饰器变更事件,自动刷新缓存 | ||||
|      */ | ||||
|     @Override | ||||
|     public void onApplicationEvent(ApplicationEvent event) { | ||||
|         if (event instanceof StorageDecoratorManager.DecoratorRegisteredEvent || event instanceof StorageDecoratorManager.DecoratorUnregisteredEvent) { | ||||
|             decoratedStrategies.clear(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 注册动态默认存储 | ||||
|      */ | ||||
|     public void registerDynamicDefaultStorage(String platform) { | ||||
|         this.dynamicDefaultPlatform = platform; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取默认存储平台 | ||||
|      */ | ||||
|     public String getDefaultStorage() { | ||||
|         DefaultStorageSource defaultStorageSource = storageProperties.getDefaultStorageSource(); | ||||
|         return switch (defaultStorageSource) { | ||||
|             case DYNAMIC -> { | ||||
|                 if (StrUtil.isBlank(dynamicDefaultPlatform)) { | ||||
|                     throw new StorageException("动态默认存储平台配置为空"); | ||||
|                 } | ||||
|                 yield dynamicDefaultPlatform; | ||||
|             } | ||||
|             case CONFIG -> { | ||||
|                 if (StrUtil.isBlank(configDefaultPlatform)) { | ||||
|                     throw new StorageException("配置默认存储平台配置为空"); | ||||
|                 } | ||||
|                 yield configDefaultPlatform; | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取所有可用平台 | ||||
|      */ | ||||
|     public Set<String> getAllPlatform() { | ||||
|         Set<String> allPlatform = new HashSet<>(configStrategies.keySet()); | ||||
|         allPlatform.addAll(dynamicStrategies.keySet()); | ||||
|         return allPlatform; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 检查是否为动态注册的策略 | ||||
|      */ | ||||
|     public boolean isDynamic(String platform) { | ||||
|         return dynamicStrategies.containsKey(platform); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 检查是否为配置文件策略 | ||||
|      */ | ||||
|     public boolean isFromConfig(String platform) { | ||||
|         return configStrategies.containsKey(platform); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取简化的策略信息 | ||||
|      */ | ||||
|     public Map<String, String> getActiveStrategyInfo() { | ||||
|         Map<String, String> info = new HashMap<>(); | ||||
|         // 先添加配置文件策略 | ||||
|         configStrategies.keySet().forEach(platform -> info.put(platform, StorageConstant.CONFIG)); | ||||
|         // 动态策略会覆盖同名的配置策略 | ||||
|         dynamicStrategies.keySet().forEach(platform -> info.put(platform, StorageConstant.DYNAMIC)); | ||||
|         return info; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取完整的策略状态 | ||||
|      */ | ||||
|     public Map<String, StrategyStatusResp> getFullStrategyStatus() { | ||||
|         Map<String, StrategyStatusResp> status = new HashMap<>(); | ||||
|  | ||||
|         // 所有唯一的 platform | ||||
|         Set<String> appPlatform = new HashSet<>(); | ||||
|         appPlatform.addAll(configStrategies.keySet()); | ||||
|         appPlatform.addAll(dynamicStrategies.keySet()); | ||||
|  | ||||
|         for (String platform : appPlatform) { | ||||
|             boolean hasConfig = configStrategies.containsKey(platform); | ||||
|             boolean hasDynamic = dynamicStrategies.containsKey(platform); | ||||
|             boolean hasDecorated = decoratedStrategies.containsKey(platform); | ||||
|  | ||||
|             String activeType = hasDynamic ? StorageConstant.DYNAMIC : StorageConstant.CONFIG; | ||||
|             String statusDesc = hasDynamic && hasConfig ? "配置策略被覆盖" : "正常"; | ||||
|  | ||||
|             if (hasDecorated) { | ||||
|                 statusDesc += " (已装饰)"; | ||||
|             } | ||||
|  | ||||
|             StrategyStatusResp strategyStatusResp = new StrategyStatusResp(platform, hasConfig, hasDynamic, activeType, statusDesc); | ||||
|  | ||||
|             status.put(platform, strategyStatusResp); | ||||
|         } | ||||
|  | ||||
|         return status; | ||||
|     } | ||||
| } | ||||
| @@ -1,89 +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.model.req; | ||||
|  | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
|  | ||||
| import java.io.ByteArrayInputStream; | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.nio.file.Files; | ||||
|  | ||||
| /** | ||||
|  * 内存中的 MultipartFile 实现,适用于无需真实 HTTP 上传场景。 | ||||
|  * <p> | ||||
|  * 可用于接口调用中构造文件参数,如将字节数组、输入流包装成 MultipartFile, | ||||
|  * 以便复用上传逻辑或兼容已有的文件处理接口。 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public class MockMultipartFile implements MultipartFile { | ||||
|     private final String name; | ||||
|     private final String originalFilename; | ||||
|     private final String contentType; | ||||
|     private final byte[] content; | ||||
|  | ||||
|     public MockMultipartFile(String name, String originalFilename, String contentType, byte[] content) { | ||||
|         this.name = name; | ||||
|         this.originalFilename = originalFilename; | ||||
|         this.contentType = contentType; | ||||
|         this.content = content; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getName() { | ||||
|         return name; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getOriginalFilename() { | ||||
|         return originalFilename; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getContentType() { | ||||
|         return contentType; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean isEmpty() { | ||||
|         return content == null || content.length == 0; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long getSize() { | ||||
|         return content != null ? content.length : 0; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public byte[] getBytes() { | ||||
|         return content; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public InputStream getInputStream() { | ||||
|         return new ByteArrayInputStream(content); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void transferTo(File dest) throws IOException { | ||||
|         Files.write(dest.toPath(), content); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -14,9 +14,9 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.prehandle; | ||||
| package top.continew.starter.storage.processor.preprocess; | ||||
| 
 | ||||
| import top.continew.starter.storage.model.context.UploadContext; | ||||
| import top.continew.starter.storage.domain.model.context.UploadContext; | ||||
| import top.continew.starter.storage.service.FileProcessor; | ||||
| 
 | ||||
| /** | ||||
| @@ -14,9 +14,9 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.prehandle; | ||||
| package top.continew.starter.storage.processor.preprocess; | ||||
| 
 | ||||
| import top.continew.starter.storage.model.context.UploadContext; | ||||
| import top.continew.starter.storage.domain.model.context.UploadContext; | ||||
| import top.continew.starter.storage.service.FileProcessor; | ||||
| 
 | ||||
| /** | ||||
| @@ -14,10 +14,10 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.prehandle; | ||||
| package top.continew.starter.storage.processor.preprocess; | ||||
| 
 | ||||
| import top.continew.starter.storage.exception.StorageException; | ||||
| import top.continew.starter.storage.model.context.UploadContext; | ||||
| import top.continew.starter.storage.common.exception.StorageException; | ||||
| import top.continew.starter.storage.domain.model.context.UploadContext; | ||||
| import top.continew.starter.storage.service.FileProcessor; | ||||
| 
 | ||||
| /** | ||||
| @@ -14,10 +14,10 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.prehandle; | ||||
| package top.continew.starter.storage.processor.preprocess; | ||||
| 
 | ||||
| import top.continew.starter.storage.model.context.UploadContext; | ||||
| import top.continew.starter.storage.model.req.ThumbnailInfo; | ||||
| import top.continew.starter.storage.domain.model.context.UploadContext; | ||||
| import top.continew.starter.storage.domain.model.req.ThumbnailInfo; | ||||
| import top.continew.starter.storage.service.FileProcessor; | ||||
| 
 | ||||
| import java.io.InputStream; | ||||
| @@ -14,9 +14,9 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.prehandle; | ||||
| package top.continew.starter.storage.processor.preprocess; | ||||
| 
 | ||||
| import top.continew.starter.storage.model.resp.FileInfo; | ||||
| import top.continew.starter.storage.domain.model.resp.FileInfo; | ||||
| import top.continew.starter.storage.service.FileProcessor; | ||||
| 
 | ||||
| /** | ||||
| @@ -14,11 +14,12 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.prehandle.impl; | ||||
| package top.continew.starter.storage.processor.preprocess.impl; | ||||
| 
 | ||||
| import top.continew.starter.storage.model.context.UploadContext; | ||||
| import top.continew.starter.storage.prehandle.FileNameGenerator; | ||||
| import top.continew.starter.storage.util.StorageUtils; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import top.continew.starter.storage.domain.model.context.UploadContext; | ||||
| import top.continew.starter.storage.processor.preprocess.FileNameGenerator; | ||||
| import top.continew.starter.storage.common.util.StorageUtils; | ||||
| 
 | ||||
| /** | ||||
|  * 默认文件名生成器 | ||||
| @@ -30,12 +31,12 @@ public class DefaultFileNameGenerator implements FileNameGenerator { | ||||
| 
 | ||||
|     @Override | ||||
|     public String getName() { | ||||
|         return "defaultFileName"; | ||||
|         return DefaultFilePathGenerator.class.getSimpleName(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean support(UploadContext context) { | ||||
|         return true; | ||||
|         return StrUtil.isBlank(context.getFormatFileName()); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @@ -14,12 +14,12 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.prehandle.impl; | ||||
| package top.continew.starter.storage.processor.preprocess.impl; | ||||
| 
 | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import top.continew.starter.storage.model.context.UploadContext; | ||||
| import top.continew.starter.storage.prehandle.FilePathGenerator; | ||||
| import top.continew.starter.storage.util.StorageUtils; | ||||
| import top.continew.starter.storage.domain.model.context.UploadContext; | ||||
| import top.continew.starter.storage.processor.preprocess.FilePathGenerator; | ||||
| import top.continew.starter.storage.common.util.StorageUtils; | ||||
| 
 | ||||
| /** | ||||
|  * 默认文件路径生成器 | ||||
| @@ -31,12 +31,12 @@ public class DefaultFilePathGenerator implements FilePathGenerator { | ||||
| 
 | ||||
|     @Override | ||||
|     public String getName() { | ||||
|         return "defaultFilePath"; | ||||
|         return DefaultFilePathGenerator.class.getSimpleName(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean support(UploadContext context) { | ||||
|         return true; | ||||
|         return StrUtil.isBlank(context.getPath()); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @@ -14,14 +14,16 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.prehandle.impl; | ||||
| package top.continew.starter.storage.processor.preprocess.impl; | ||||
| 
 | ||||
| import cn.hutool.core.io.FileUtil; | ||||
| import net.coobird.thumbnailator.Thumbnails; | ||||
| import top.continew.starter.storage.exception.StorageException; | ||||
| import top.continew.starter.storage.model.context.UploadContext; | ||||
| import top.continew.starter.storage.model.req.ThumbnailInfo; | ||||
| import top.continew.starter.storage.model.req.ThumbnailSize; | ||||
| import top.continew.starter.storage.prehandle.ThumbnailProcessor; | ||||
| import top.continew.starter.storage.common.constant.StorageConstant; | ||||
| import top.continew.starter.storage.common.exception.StorageException; | ||||
| import top.continew.starter.storage.domain.model.context.UploadContext; | ||||
| import top.continew.starter.storage.domain.model.req.ThumbnailInfo; | ||||
| import top.continew.starter.storage.domain.model.req.ThumbnailSize; | ||||
| import top.continew.starter.storage.processor.preprocess.ThumbnailProcessor; | ||||
| 
 | ||||
| import javax.imageio.ImageIO; | ||||
| import java.awt.image.BufferedImage; | ||||
| @@ -38,18 +40,19 @@ public class DefaultThumbnailProcessor implements ThumbnailProcessor { | ||||
| 
 | ||||
|     @Override | ||||
|     public String getName() { | ||||
|         return "defaultThumbnail"; | ||||
|         return DefaultThumbnailProcessor.class.getSimpleName(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean support(UploadContext context) { | ||||
|         String contentType = context.getFile().getContentType(); | ||||
|         return contentType != null && contentType.startsWith("image/"); | ||||
|         return contentType != null && contentType.startsWith(StorageConstant.CONTENT_TYPE_IMAGE); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public ThumbnailInfo process(UploadContext context, InputStream sourceInputStream) { | ||||
|         try { | ||||
|             String suffix = FileUtil.getSuffix(context.getFormatFileName()); | ||||
|             ThumbnailSize size = context.getThumbnailSize(); | ||||
|             BufferedImage thumbnail = Thumbnails.of(sourceInputStream) | ||||
|                 .size(size.getWidth(), size.getHeight()) | ||||
| @@ -57,11 +60,11 @@ public class DefaultThumbnailProcessor implements ThumbnailProcessor { | ||||
|                 .asBufferedImage(); | ||||
| 
 | ||||
|             ByteArrayOutputStream baos = new ByteArrayOutputStream(); | ||||
|             ImageIO.write(thumbnail, "jpg", baos); | ||||
|             ImageIO.write(thumbnail, suffix, baos); | ||||
| 
 | ||||
|             ThumbnailInfo info = new ThumbnailInfo(); | ||||
|             info.setData(baos.toByteArray()); | ||||
|             info.setFormat("jpg"); | ||||
|             info.setFormat(suffix); | ||||
|             info.setWidth(thumbnail.getWidth()); | ||||
|             info.setHeight(thumbnail.getHeight()); | ||||
| 
 | ||||
| @@ -14,13 +14,14 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.prehandle.impl; | ||||
| package top.continew.starter.storage.processor.preprocess.impl; | ||||
| 
 | ||||
| import cn.hutool.core.io.FileUtil; | ||||
| import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties; | ||||
| import top.continew.starter.storage.exception.StorageException; | ||||
| import top.continew.starter.storage.model.context.UploadContext; | ||||
| import top.continew.starter.storage.prehandle.FileValidator; | ||||
| import top.continew.starter.storage.common.constant.StorageConstant; | ||||
| import top.continew.starter.storage.common.exception.StorageException; | ||||
| import top.continew.starter.storage.domain.model.context.UploadContext; | ||||
| import top.continew.starter.storage.processor.preprocess.FileValidator; | ||||
| 
 | ||||
| /** | ||||
|  * 文件大小验证器 | ||||
| @@ -37,8 +38,7 @@ public class FileSizeValidator implements FileValidator { | ||||
|     } | ||||
| 
 | ||||
|     public FileSizeValidator() { | ||||
|         // 提供默认大小 10MB | ||||
|         this(10 * 1024 * 1024L); | ||||
|         this(StorageConstant.DEFAULT_FILE_SIZE); | ||||
|     } | ||||
| 
 | ||||
|     public FileSizeValidator(long maxSize) { | ||||
| @@ -47,7 +47,7 @@ public class FileSizeValidator implements FileValidator { | ||||
| 
 | ||||
|     @Override | ||||
|     public String getName() { | ||||
|         return "fileSize"; | ||||
|         return FileSizeValidator.class.getSimpleName(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @@ -14,12 +14,12 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.prehandle.impl; | ||||
| package top.continew.starter.storage.processor.preprocess.impl; | ||||
| 
 | ||||
| import cn.hutool.core.io.FileUtil; | ||||
| import top.continew.starter.storage.exception.StorageException; | ||||
| import top.continew.starter.storage.model.context.UploadContext; | ||||
| import top.continew.starter.storage.prehandle.FileValidator; | ||||
| import top.continew.starter.storage.common.exception.StorageException; | ||||
| import top.continew.starter.storage.domain.model.context.UploadContext; | ||||
| import top.continew.starter.storage.processor.preprocess.FileValidator; | ||||
| 
 | ||||
| import java.util.Arrays; | ||||
| import java.util.HashSet; | ||||
| @@ -53,7 +53,7 @@ public class FileTypeValidator implements FileValidator { | ||||
| 
 | ||||
|     @Override | ||||
|     public String getName() { | ||||
|         return "fileType"; | ||||
|         return FileTypeValidator.class.getSimpleName(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @@ -0,0 +1,62 @@ | ||||
| /* | ||||
|  * 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.processor.progress; | ||||
|  | ||||
| import java.io.FilterInputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
|  | ||||
| /** | ||||
|  * 进度监听输入流包装器 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public class ProgressInputStream extends FilterInputStream { | ||||
|  | ||||
|     private final ProgressTracker tracker; | ||||
|  | ||||
|     public ProgressInputStream(InputStream in, ProgressTracker tracker) { | ||||
|         super(in); | ||||
|         this.tracker = tracker; | ||||
|         tracker.start(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int read() throws IOException { | ||||
|         int b = super.read(); | ||||
|         if (b != -1) { | ||||
|             tracker.updateProgress(1); | ||||
|         } | ||||
|         return b; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int read(byte[] b, int off, int len) throws IOException { | ||||
|         int bytesRead = super.read(b, off, len); | ||||
|         if (bytesRead > 0) { | ||||
|             tracker.updateProgress(bytesRead); | ||||
|         } | ||||
|         return bytesRead; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void close() throws IOException { | ||||
|         super.close(); | ||||
|         tracker.complete(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,107 @@ | ||||
| /* | ||||
|  * 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.processor.progress; | ||||
|  | ||||
| import java.util.concurrent.atomic.AtomicBoolean; | ||||
| import java.util.concurrent.atomic.AtomicInteger; | ||||
| import java.util.concurrent.atomic.AtomicLong; | ||||
|  | ||||
| /** | ||||
|  * 进度跟踪器 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public class ProgressTracker { | ||||
|  | ||||
|     private final long totalBytes; | ||||
|     private final UploadProgressListener listener; | ||||
|     private final AtomicLong bytesRead = new AtomicLong(0); | ||||
|     private final AtomicLong lastNotifiedBytes = new AtomicLong(0); | ||||
|     private final AtomicInteger lastPercentage = new AtomicInteger(-1); | ||||
|     private final AtomicBoolean started = new AtomicBoolean(false); | ||||
|     private final AtomicBoolean completed = new AtomicBoolean(false); | ||||
|  | ||||
|     // 通知阈值:至少变化1%或者达到 1MB 阈值 | ||||
|     private static final int PERCENTAGE_THRESHOLD = 1; | ||||
|     private static final long BYTES_THRESHOLD = 1024 * 1024; | ||||
|  | ||||
|     public ProgressTracker(long totalBytes, UploadProgressListener listener) { | ||||
|         this.totalBytes = totalBytes; | ||||
|         this.listener = listener; | ||||
|     } | ||||
|  | ||||
|     public void start() { | ||||
|         if (started.compareAndSet(false, true) && listener != null) { | ||||
|             listener.onStart(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void updateProgress(long bytes) { | ||||
|         if (completed.get() || listener == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         long currentBytes = bytesRead.addAndGet(bytes); | ||||
|         int currentPercentage = totalBytes > 0 ? (int)((currentBytes * 100L) / totalBytes) : -1; | ||||
|  | ||||
|         // 检查是否需要通知 | ||||
|         boolean shouldNotify = false; | ||||
|         int lastPct = lastPercentage.get(); | ||||
|  | ||||
|         if (currentPercentage >= 0) { | ||||
|             // 百分比变化达到阈值 | ||||
|             if (currentPercentage - lastPct >= PERCENTAGE_THRESHOLD) { | ||||
|                 shouldNotify = true; | ||||
|             } | ||||
|             // 达到100%必须通知 | ||||
|             if (currentPercentage == 100 && lastPct != 100) { | ||||
|                 shouldNotify = true; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // 字节数变化达到阈值 | ||||
|         if (currentBytes - lastNotifiedBytes.get() >= BYTES_THRESHOLD) { | ||||
|             shouldNotify = true; | ||||
|         } | ||||
|  | ||||
|         if (shouldNotify) { | ||||
|             // 使用CAS更新,避免并发问题 | ||||
|             if (lastPercentage.compareAndSet(lastPct, currentPercentage)) { | ||||
|                 lastNotifiedBytes.set(currentBytes); | ||||
|                 listener.onProgress(currentBytes, totalBytes, currentPercentage); | ||||
|  | ||||
|                 // 如果达到100%,标记完成 | ||||
|                 if (currentPercentage == 100) { | ||||
|                     complete(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void complete() { | ||||
|         if (completed.compareAndSet(false, true) && listener != null) { | ||||
|             listener.onComplete(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void error(Exception e) { | ||||
|         if (listener != null) { | ||||
|             listener.onError(e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -14,30 +14,40 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.starter.storage.strategy.impl; | ||||
| 
 | ||||
| import top.continew.starter.storage.strategy.StorageStrategy; | ||||
| import top.continew.starter.storage.strategy.StorageStrategyOverride; | ||||
| package top.continew.starter.storage.processor.progress; | ||||
| 
 | ||||
| /** | ||||
|  * 抽象基类,提供原始目标对象的访问 | ||||
|  * 上传进度监听器 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public abstract class AbstractStorageStrategyOverride<T extends StorageStrategy> implements StorageStrategyOverride<T> { | ||||
| 
 | ||||
|     protected T originalTarget; | ||||
|  **/ | ||||
| public interface UploadProgressListener { | ||||
| 
 | ||||
|     /** | ||||
|      * 设置原始目标对象(由代理工厂调用) | ||||
|      * 进度更新回调 | ||||
|      * | ||||
|      * @param bytesRead  已读取字节数 | ||||
|      * @param totalBytes 总字节数(-1表示未知) | ||||
|      * @param percentage 百分比(0-100) | ||||
|      */ | ||||
|     public void setOriginalTarget(T originalTarget) { | ||||
|         this.originalTarget = originalTarget; | ||||
|     void onProgress(long bytesRead, long totalBytes, int percentage); | ||||
| 
 | ||||
|     /** | ||||
|      * 上传开始 | ||||
|      */ | ||||
|     default void onStart() { | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public T getOriginalTarget() { | ||||
|         return originalTarget; | ||||
|     /** | ||||
|      * 上传完成 | ||||
|      */ | ||||
|     default void onComplete() { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|     /** | ||||
|      * 上传失败 | ||||
|      */ | ||||
|     default void onError(Exception e) { | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,118 @@ | ||||
| /* | ||||
|  * 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.processor.registry; | ||||
|  | ||||
| import top.continew.starter.storage.domain.model.context.UploadContext; | ||||
| import top.continew.starter.storage.processor.preprocess.*; | ||||
| import top.continew.starter.storage.service.FileProcessor; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Comparator; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
| import java.util.concurrent.CopyOnWriteArrayList; | ||||
|  | ||||
| /** | ||||
|  * 全局处理器注册表 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public class ProcessorRegistry { | ||||
|  | ||||
|     private final Map<Class<?>, List<FileProcessor>> processors = new ConcurrentHashMap<>(); | ||||
|     private final Map<String, Map<Class<?>, List<FileProcessor>>> platformProcessors = new ConcurrentHashMap<>(); | ||||
|  | ||||
|     /** | ||||
|      * 注册处理器(自动识别类型) | ||||
|      */ | ||||
|     public void register(FileProcessor processor) { | ||||
|         register(processor, null); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 注册平台特定处理器 | ||||
|      */ | ||||
|     public void register(FileProcessor processor, String platform) { | ||||
|         Class<?> type = getProcessorType(processor); | ||||
|  | ||||
|         if (platform == null) { | ||||
|             // 全局处理器 | ||||
|             processors.computeIfAbsent(type, k -> new CopyOnWriteArrayList<>()).add(processor); | ||||
|         } else { | ||||
|             // 平台特定处理器 | ||||
|             platformProcessors.computeIfAbsent(platform, k -> new ConcurrentHashMap<>()) | ||||
|                 .computeIfAbsent(type, k -> new CopyOnWriteArrayList<>()) | ||||
|                 .add(processor); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取处理器类型 | ||||
|      */ | ||||
|     private Class<?> getProcessorType(FileProcessor processor) { | ||||
|         if (processor instanceof ThumbnailProcessor) | ||||
|             return ThumbnailProcessor.class; | ||||
|         if (processor instanceof FileValidator) | ||||
|             return FileValidator.class; | ||||
|         if (processor instanceof FileNameGenerator) | ||||
|             return FileNameGenerator.class; | ||||
|         if (processor instanceof FilePathGenerator) | ||||
|             return FilePathGenerator.class; | ||||
|         if (processor instanceof UploadCompleteProcessor) | ||||
|             return UploadCompleteProcessor.class; | ||||
|         return FileProcessor.class; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取指定类型的处理器(支持优先级排序) | ||||
|      */ | ||||
|     @SuppressWarnings("unchecked") | ||||
|     public <T extends FileProcessor> List<T> getProcessors(Class<T> type, String platform, UploadContext context) { | ||||
|         List<T> result = new ArrayList<>(); | ||||
|  | ||||
|         // 添加全局处理器 | ||||
|         List<FileProcessor> globalList = processors.get(type); | ||||
|         if (globalList != null) { | ||||
|             globalList.stream().filter(p -> p.support(context)).map(p -> (T)p).forEach(result::add); | ||||
|         } | ||||
|  | ||||
|         // 添加平台特定处理器 | ||||
|         if (platform != null) { | ||||
|             Map<Class<?>, List<FileProcessor>> platformMap = platformProcessors.get(platform); | ||||
|             if (platformMap != null) { | ||||
|                 List<FileProcessor> platformList = platformMap.get(type); | ||||
|                 if (platformList != null) { | ||||
|                     platformList.stream().filter(p -> p.support(context)).map(p -> (T)p).forEach(result::add); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // 按优先级排序(优先级高的在前) | ||||
|         result.sort(Comparator.comparingInt(FileProcessor::getOrder).reversed()); | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取最高优先级的处理器 | ||||
|      */ | ||||
|     public <T extends FileProcessor> T getProcessor(Class<T> type, String platform, UploadContext context) { | ||||
|         List<T> processors = getProcessors(type, platform, context); | ||||
|         return processors.isEmpty() ? null : processors.get(0); | ||||
|     } | ||||
| } | ||||
| @@ -1,171 +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.router; | ||||
|  | ||||
| import top.continew.starter.storage.exception.StorageException; | ||||
| import top.continew.starter.storage.model.resp.StrategyStatus; | ||||
| import top.continew.starter.storage.strategy.StorageStrategy; | ||||
|  | ||||
| import java.util.*; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
|  | ||||
| /** | ||||
|  * 存储策略路由器 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public class StorageStrategyRouter { | ||||
|  | ||||
|     /** | ||||
|      * 配置策略 | ||||
|      */ | ||||
|     private final Map<String, StorageStrategy> configStrategies = new ConcurrentHashMap<>(); | ||||
|  | ||||
|     /** | ||||
|      * 动态策略 | ||||
|      */ | ||||
|     private final Map<String, StorageStrategy> dynamicStrategies = new ConcurrentHashMap<>(); | ||||
|  | ||||
|     public StorageStrategyRouter(List<StorageStrategyRegistrar> registrars) { | ||||
|         List<StorageStrategy> strategies = new ArrayList<>(); | ||||
|         for (StorageStrategyRegistrar registrar : registrars) { | ||||
|             registrar.register(strategies); | ||||
|         } | ||||
|  | ||||
|         // 配置文件加载的策略 | ||||
|         for (StorageStrategy strategy : strategies) { | ||||
|             configStrategies.put(strategy.getPlatform(), strategy); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 存储选择 | ||||
|      * | ||||
|      * @param platform 代码 | ||||
|      * @return {@link StorageStrategy } | ||||
|      */ | ||||
|     public StorageStrategy route(String platform) { | ||||
|         // 动态注册的策略优先级更高 | ||||
|         StorageStrategy strategy = dynamicStrategies.get(platform); | ||||
|         if (strategy == null) { | ||||
|             strategy = configStrategies.get(platform); | ||||
|         } | ||||
|  | ||||
|         if (strategy == null) { | ||||
|             throw new StorageException("不支持存储编码: " + platform); | ||||
|         } | ||||
|         return strategy; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 动态注册策略 | ||||
|      * | ||||
|      * @param strategy 存储策略 | ||||
|      * @throws StorageException 如果同一 platform 的动态策略已存在 | ||||
|      */ | ||||
|     public void registerDynamic(StorageStrategy strategy) { | ||||
|         String platform = strategy.getPlatform(); | ||||
|         if (dynamicStrategies.containsKey(platform)) { | ||||
|             throw new StorageException("动态策略 platform 已存在: " + platform); | ||||
|         } | ||||
|         // 如果配置文件中存在相同 platform,动态注册会覆盖(但不修改配置策略) | ||||
|         dynamicStrategies.put(platform, strategy); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 卸载动态策略 | ||||
|      * | ||||
|      * @param platform 策略代码 | ||||
|      * @return 是否成功卸载 | ||||
|      */ | ||||
|     public boolean unloadDynamic(String platform) { | ||||
|         StorageStrategy strategy = dynamicStrategies.remove(platform); | ||||
|         if (strategy != null) { | ||||
|             try { | ||||
|                 strategy.cleanup(); | ||||
|                 return true; | ||||
|             } catch (Exception e) { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取所有可用代码 | ||||
|      */ | ||||
|     public Set<String> getAllPlatform() { | ||||
|         Set<String> allPlatform = new HashSet<>(configStrategies.keySet()); | ||||
|         allPlatform.addAll(dynamicStrategies.keySet()); | ||||
|         return allPlatform; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 检查是否为动态注册的策略 | ||||
|      */ | ||||
|     public boolean isDynamic(String platform) { | ||||
|         return dynamicStrategies.containsKey(platform); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 检查是否为配置文件策略 | ||||
|      */ | ||||
|     public boolean isFromConfig(String platform) { | ||||
|         return configStrategies.containsKey(platform); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取简化的策略信息(当前生效的) | ||||
|      */ | ||||
|     public Map<String, String> getActiveStrategyInfo() { | ||||
|         Map<String, String> info = new HashMap<>(); | ||||
|  | ||||
|         // 先添加配置文件策略 | ||||
|         configStrategies.keySet().forEach(platform -> info.put(platform, "CONFIG")); | ||||
|  | ||||
|         // 动态策略会覆盖同名的配置策略 | ||||
|         dynamicStrategies.keySet().forEach(platform -> info.put(platform, "DYNAMIC")); | ||||
|  | ||||
|         return info; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取完整的策略状态 | ||||
|      */ | ||||
|     public Map<String, StrategyStatus> getFullStrategyStatus() { | ||||
|         Map<String, StrategyStatus> status = new HashMap<>(); | ||||
|  | ||||
|         // 所有唯一的 platform | ||||
|         Set<String> appPlatform = new HashSet<>(); | ||||
|         appPlatform.addAll(configStrategies.keySet()); | ||||
|         appPlatform.addAll(dynamicStrategies.keySet()); | ||||
|  | ||||
|         for (String platform : appPlatform) { | ||||
|             boolean hasConfig = configStrategies.containsKey(platform); | ||||
|             boolean hasDynamic = dynamicStrategies.containsKey(platform); | ||||
|  | ||||
|             StrategyStatus strategyStatus = new StrategyStatus(platform, hasConfig, hasDynamic, hasDynamic | ||||
|                 ? "DYNAMIC" | ||||
|                 : "CONFIG", hasDynamic && hasConfig ? "配置策略被覆盖" : "正常"); | ||||
|  | ||||
|             status.put(platform, strategyStatus); | ||||
|         } | ||||
|  | ||||
|         return status; | ||||
|     } | ||||
| } | ||||
| @@ -16,7 +16,7 @@ | ||||
|  | ||||
| package top.continew.starter.storage.service; | ||||
|  | ||||
| import top.continew.starter.storage.model.context.UploadContext; | ||||
| import top.continew.starter.storage.domain.model.context.UploadContext; | ||||
|  | ||||
| /** | ||||
|  * 文件处理器接口 | ||||
|   | ||||
| @@ -16,8 +16,8 @@ | ||||
|  | ||||
| package top.continew.starter.storage.service; | ||||
|  | ||||
| import top.continew.starter.storage.model.resp.FileInfo; | ||||
| import top.continew.starter.storage.model.resp.FilePartInfo; | ||||
| import top.continew.starter.storage.domain.model.resp.FileInfo; | ||||
| import top.continew.starter.storage.domain.model.resp.FilePartInfo; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
|   | ||||
| @@ -16,8 +16,8 @@ | ||||
|  | ||||
| package top.continew.starter.storage.service.impl; | ||||
|  | ||||
| import top.continew.starter.storage.model.resp.FileInfo; | ||||
| import top.continew.starter.storage.model.resp.FilePartInfo; | ||||
| import top.continew.starter.storage.domain.model.resp.FileInfo; | ||||
| import top.continew.starter.storage.domain.model.resp.FilePartInfo; | ||||
| import top.continew.starter.storage.service.FileRecorder; | ||||
|  | ||||
| import java.util.List; | ||||
|   | ||||
| @@ -17,9 +17,9 @@ | ||||
| package top.continew.starter.storage.strategy; | ||||
|  | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
| import top.continew.starter.storage.model.resp.FileInfo; | ||||
| import top.continew.starter.storage.model.resp.MultipartInitResp; | ||||
| import top.continew.starter.storage.model.resp.MultipartUploadResp; | ||||
| import top.continew.starter.storage.domain.model.resp.FileInfo; | ||||
| import top.continew.starter.storage.domain.model.resp.MultipartInitResp; | ||||
| import top.continew.starter.storage.domain.model.resp.MultipartUploadResp; | ||||
|  | ||||
| import java.io.InputStream; | ||||
| import java.util.List; | ||||
|   | ||||
| @@ -24,10 +24,10 @@ import org.springframework.web.multipart.MultipartFile; | ||||
| import top.continew.starter.core.constant.StringConstants; | ||||
| import top.continew.starter.core.util.SpringWebUtils; | ||||
| import top.continew.starter.storage.autoconfigure.properties.LocalStorageConfig; | ||||
| import top.continew.starter.storage.exception.StorageException; | ||||
| import top.continew.starter.storage.model.resp.FileInfo; | ||||
| import top.continew.starter.storage.model.resp.MultipartInitResp; | ||||
| import top.continew.starter.storage.model.resp.MultipartUploadResp; | ||||
| import top.continew.starter.storage.common.exception.StorageException; | ||||
| import top.continew.starter.storage.domain.model.resp.FileInfo; | ||||
| import top.continew.starter.storage.domain.model.resp.MultipartInitResp; | ||||
| import top.continew.starter.storage.domain.model.resp.MultipartUploadResp; | ||||
| import top.continew.starter.storage.strategy.StorageStrategy; | ||||
|  | ||||
| import java.io.File; | ||||
|   | ||||
| @@ -30,13 +30,13 @@ import software.amazon.awssdk.services.s3.presigner.S3Presigner; | ||||
| import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; | ||||
| import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest; | ||||
| import top.continew.starter.core.constant.StringConstants; | ||||
| import top.continew.starter.storage.autoconfigure.properties.S3StorageConfig; | ||||
| import top.continew.starter.storage.exception.StorageException; | ||||
| import top.continew.starter.storage.model.resp.FileInfo; | ||||
| import top.continew.starter.storage.model.resp.MultipartInitResp; | ||||
| import top.continew.starter.storage.model.resp.MultipartUploadResp; | ||||
| import top.continew.starter.storage.autoconfigure.properties.OssStorageConfig; | ||||
| import top.continew.starter.storage.common.exception.StorageException; | ||||
| import top.continew.starter.storage.domain.model.resp.FileInfo; | ||||
| import top.continew.starter.storage.domain.model.resp.MultipartInitResp; | ||||
| import top.continew.starter.storage.domain.model.resp.MultipartUploadResp; | ||||
| import top.continew.starter.storage.strategy.StorageStrategy; | ||||
| import top.continew.starter.storage.util.StorageUtils; | ||||
| import top.continew.starter.storage.common.util.StorageUtils; | ||||
|  | ||||
| import java.io.InputStream; | ||||
| import java.net.URI; | ||||
| @@ -57,9 +57,9 @@ public class OssStorageStrategy implements StorageStrategy { | ||||
|  | ||||
|     private final S3Client s3Client; | ||||
|     private final S3Presigner s3Presigner; | ||||
|     private final S3StorageConfig config; | ||||
|     private final OssStorageConfig config; | ||||
|  | ||||
|     public OssStorageStrategy(S3StorageConfig config) { | ||||
|     public OssStorageStrategy(OssStorageConfig config) { | ||||
|         this.config = config; | ||||
|         this.s3Client = createS3Client(config); | ||||
|         this.s3Presigner = createS3Presigner(config); | ||||
| @@ -89,7 +89,7 @@ public class OssStorageStrategy implements StorageStrategy { | ||||
|      * @param config 配置 | ||||
|      * @return {@link S3Client } | ||||
|      */ | ||||
|     private S3Client createS3Client(S3StorageConfig config) { | ||||
|     private S3Client createS3Client(OssStorageConfig config) { | ||||
|         // 登录认证账户密码 | ||||
|         StaticCredentialsProvider auth = StaticCredentialsProvider.create(AwsBasicCredentials.create(config | ||||
|             .getAccessKey(), config.getSecretKey())); | ||||
| @@ -107,7 +107,7 @@ public class OssStorageStrategy implements StorageStrategy { | ||||
|      * @param config 配置 | ||||
|      * @return {@link S3Presigner } | ||||
|      */ | ||||
|     private S3Presigner createS3Presigner(S3StorageConfig config) { | ||||
|     private S3Presigner createS3Presigner(OssStorageConfig config) { | ||||
|         StaticCredentialsProvider auth = StaticCredentialsProvider.create(AwsBasicCredentials.create(config | ||||
|             .getAccessKey(), config.getSecretKey())); | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,174 @@ | ||||
| /* | ||||
|  * 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.impl; | ||||
|  | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
| import top.continew.starter.storage.domain.model.resp.FileInfo; | ||||
| import top.continew.starter.storage.domain.model.resp.MultipartInitResp; | ||||
| import top.continew.starter.storage.domain.model.resp.MultipartUploadResp; | ||||
| import top.continew.starter.storage.strategy.StorageStrategy; | ||||
|  | ||||
| import java.io.InputStream; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * 存储策略装饰器基类 | ||||
|  * 支持对特定存储策略进行选择性重写 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2.14.0 | ||||
|  */ | ||||
| public abstract class StorageStrategyDecorator<T extends StorageStrategy> implements StorageStrategy { | ||||
|  | ||||
|     protected T delegate; | ||||
|  | ||||
|     /** | ||||
|      * 获取被装饰的策略类型 | ||||
|      */ | ||||
|     public abstract Class<T> getTargetStrategyClass(); | ||||
|  | ||||
|     /** | ||||
|      * 设置被装饰的策略实例 | ||||
|      */ | ||||
|     public void setDelegate(T delegate) { | ||||
|         this.delegate = delegate; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取被装饰的策略实例 | ||||
|      */ | ||||
|     protected T getDelegate() { | ||||
|         if (delegate == null) { | ||||
|             throw new IllegalStateException("装饰器未初始化,请先设置delegate"); | ||||
|         } | ||||
|         return delegate; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取装饰器优先级(数值越小优先级越高) | ||||
|      */ | ||||
|     public int getOrder() { | ||||
|         return 0; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void upload(String bucket, String path, MultipartFile file) { | ||||
|         getDelegate().upload(bucket, path, file); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public InputStream download(String bucket, String path) { | ||||
|         return getDelegate().download(bucket, path); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public InputStream batchDownload(String bucket, List<String> paths) { | ||||
|         return getDelegate().batchDownload(bucket, paths); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void delete(String bucket, String path) { | ||||
|         getDelegate().delete(bucket, path); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void batchDelete(String bucket, List<String> paths) { | ||||
|         getDelegate().batchDelete(bucket, paths); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean exists(String bucket, String path) { | ||||
|         return getDelegate().exists(bucket, path); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public FileInfo getFileInfo(String bucket, String path) { | ||||
|         return getDelegate().getFileInfo(bucket, path); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public List<FileInfo> list(String bucket, String prefix, int maxKeys) { | ||||
|         return getDelegate().list(bucket, prefix, maxKeys); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void copy(String sourceBucket, String targetBucket, String sourcePath, String targetPath) { | ||||
|         getDelegate().copy(sourceBucket, targetBucket, sourcePath, targetPath); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void move(String sourceBucket, String targetBucket, String sourcePath, String targetPath) { | ||||
|         getDelegate().move(sourceBucket, targetBucket, sourcePath, targetPath); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getPlatform() { | ||||
|         return getDelegate().getPlatform(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String defaultBucket() { | ||||
|         return getDelegate().defaultBucket(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String generatePresignedUrl(String bucket, String path, long expireSeconds) { | ||||
|         return getDelegate().generatePresignedUrl(bucket, path, expireSeconds); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public MultipartInitResp initMultipartUpload(String bucket, | ||||
|                                                  String path, | ||||
|                                                  String contentType, | ||||
|                                                  Map<String, String> metadata) { | ||||
|         return getDelegate().initMultipartUpload(bucket, path, contentType, metadata); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public MultipartUploadResp uploadPart(String bucket, | ||||
|                                           String path, | ||||
|                                           String uploadId, | ||||
|                                           int partNumber, | ||||
|                                           InputStream data) { | ||||
|         return getDelegate().uploadPart(bucket, path, uploadId, partNumber, data); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public FileInfo completeMultipartUpload(String bucket, | ||||
|                                             String path, | ||||
|                                             String uploadId, | ||||
|                                             List<MultipartUploadResp> parts, | ||||
|                                             boolean verifyParts) { | ||||
|         return getDelegate().completeMultipartUpload(bucket, path, uploadId, parts, verifyParts); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void abortMultipartUpload(String bucket, String path, String uploadId) { | ||||
|         getDelegate().abortMultipartUpload(bucket, path, uploadId); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public List<MultipartUploadResp> listParts(String bucket, String path, String uploadId) { | ||||
|         return getDelegate().listParts(bucket, path, uploadId); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void cleanup() { | ||||
|         getDelegate().cleanup(); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 QAQ_Z
					QAQ_Z