diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/annotation/PlatformProcessor.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/annotation/PlatformProcessor.java new file mode 100644 index 00000000..78676487 --- /dev/null +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/annotation/PlatformProcessor.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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.*; + +/** + * 平台处理器注解 + *

+ * 该注解用于标记文件前置处理器类,以指定其适用的平台范围。 + * 主要用于实现平台特定的文件处理逻辑,如文件名生成、路径转换、格式适配等。 + *

+ * + * @author echo + * @since 2.14.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface PlatformProcessor { + /** + * 适用的平台列表 + */ + String[] platforms(); +} \ No newline at end of file diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/LocalStorageAutoConfiguration.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/LocalStorageAutoConfiguration.java index 54316511..6938c510 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/LocalStorageAutoConfiguration.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/LocalStorageAutoConfiguration.java @@ -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 策略列表 */ diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/S3StorageAutoConfiguration.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/OssStorageAutoConfiguration.java similarity index 80% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/S3StorageAutoConfiguration.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/OssStorageAutoConfiguration.java index dd48da85..1f2df7dc 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/S3StorageAutoConfiguration.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/OssStorageAutoConfiguration.java @@ -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 strategies) { - for (S3StorageConfig config : properties.getS3()) { + for (OssStorageConfig config : properties.getOss()) { if (config.isEnabled()) { strategies.add(new OssStorageStrategy(config)); } diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/StorageAutoConfiguration.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/StorageAutoConfiguration.java index d91c1886..60ca7aa8 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/StorageAutoConfiguration.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/StorageAutoConfiguration.java @@ -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 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 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> overrides, StrategyProxyFactory proxyFactory) { - for (StorageStrategyOverride override : overrides) { - proxyFactory.registerOverride(override); - } - } - } - - /** - * 处理器自动注册 - */ - @Configuration - public static class ProcessorAutoConfiguration { - @Autowired(required = false) - public void registerGlobalProcessors(List nameGenerators, - List pathGenerators, - List thumbnailProcessors, - List validators, - List 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 diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/S3StorageConfig.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/OssStorageConfig.java similarity index 92% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/S3StorageConfig.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/OssStorageConfig.java index 9088c068..23e369ec 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/S3StorageConfig.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/OssStorageConfig.java @@ -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; /** * 是否启用路径样式访问 diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/StorageProperties.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/StorageProperties.java index 41ed9a3f..26403d60 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/StorageProperties.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/StorageProperties.java @@ -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 local = new ArrayList<>(); /** - * S3 存储配置列表 + * oss 存储配置列表 */ - private List s3 = new ArrayList<>(); + private List 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 getLocal() { return local; } @@ -62,11 +77,11 @@ public class StorageProperties { this.local = local; } - public List getS3() { - return s3; + public List getOss() { + return oss; } - public void setS3(List s3) { - this.s3 = s3; + public void setOss(List oss) { + this.oss = oss; } } diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/common/constant/StorageConstant.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/common/constant/StorageConstant.java new file mode 100644 index 00000000..0890568a --- /dev/null +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/common/constant/StorageConstant.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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/"; + +} diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/StorageStrategyOverride.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/common/enums/DefaultStorageSource.java similarity index 61% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/StorageStrategyOverride.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/common/enums/DefaultStorageSource.java index e5222f27..a8ada1cd 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/StorageStrategyOverride.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/common/enums/DefaultStorageSource.java @@ -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 { +public enum DefaultStorageSource { /** - * 获取目标策略类型 + * 从配置文件加载默认存储 */ - Class getTargetType(); + CONFIG, /** - * 获取原始目标对象(用于调用原始方法) + * 从动态配置加载默认存储 */ - default T getOriginalTarget() { - // 这个方法会在代理创建时被设置 - throw new UnsupportedOperationException("原始目标未设置"); - } -} \ No newline at end of file + DYNAMIC, + +} diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/exception/StorageException.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/common/exception/StorageException.java similarity index 95% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/exception/StorageException.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/common/exception/StorageException.java index b78b43c5..03d985eb 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/exception/StorageException.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/common/exception/StorageException.java @@ -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; diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/util/StorageUtils.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/common/util/StorageUtils.java similarity index 98% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/util/StorageUtils.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/common/util/StorageUtils.java index ba02f5ed..278d0ffa 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/util/StorageUtils.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/common/util/StorageUtils.java @@ -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; diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/core/FileStorageService.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/core/FileStorageService.java index 5a7e0840..bc1165c0 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/core/FileStorageService.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/core/FileStorageService.java @@ -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> tempProcessors = ThreadLocal.withInitial(ArrayList::new); + private final ThreadLocal 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 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 customProcessors) { + List 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 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 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 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 customProcessors) { + List completeProcessors = collectProcessors(customProcessors, UploadCompleteProcessor.class, platform, context); + + for (UploadCompleteProcessor processor : completeProcessors) { + if (processor.support(context)) { + processor.onComplete(fileInfo); + } + } + } + + /** + * 收集指定类型的处理器 + */ + private List collectProcessors(List customProcessors, + Class processorClass, + String platform, + UploadContext context) { + + List 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 findFirstProcessor(List customProcessors, + Class processorClass, + String platform, + UploadContext context) { + + // 优先从自定义处理器中查找 + if (customProcessors != null) { + Optional 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 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 parts) { if (parts.isEmpty()) { @@ -314,7 +591,7 @@ public class FileStorageService { List 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 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 getStrategyStatus() { + public Map getStrategyStatus() { return router.getFullStrategyStatus(); } diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/core/ProcessorRegistry.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/core/ProcessorRegistry.java deleted file mode 100644 index dd63d6b3..00000000 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/core/ProcessorRegistry.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. - *

- * 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 - *

- * http://www.gnu.org/licenses/lgpl.html - *

- * 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 globalNameGenerators = new ConcurrentHashMap<>(); - private final Map globalPathGenerators = new ConcurrentHashMap<>(); - private final Map globalThumbnailProcessors = new ConcurrentHashMap<>(); - private final List globalValidators = new CopyOnWriteArrayList<>(); - private final List globalCompleteProcessors = new CopyOnWriteArrayList<>(); - - // 平台特定处理器 - private final Map> platformNameGenerators = new ConcurrentHashMap<>(); - private final Map> platformPathGenerators = new ConcurrentHashMap<>(); - private final Map> platformThumbnailProcessors = new ConcurrentHashMap<>(); - private final Map> platformValidators = new ConcurrentHashMap<>(); - private final Map> 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 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 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 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 getValidators(String platform) { - List validators = new ArrayList<>(); - - // 先添加全局验证器 - validators.addAll(globalValidators); - - // 再添加平台特定验证器 - List platformSpecific = platformValidators.get(platform); - if (platformSpecific != null) { - validators.addAll(platformSpecific); - } - - // 按优先级排序(优先级高的在前) - validators.sort(Comparator.comparingInt(FileValidator::getOrder).reversed()); - - return validators; - } - - /** - * 获取完成处理器列表(合并全局和平台) - */ - public List getCompleteProcessors(String platform) { - List processors = new ArrayList<>(); - - // 先添加全局处理器 - processors.addAll(globalCompleteProcessors); - - // 再添加平台特定处理器 - List platformSpecific = platformCompleteProcessors.get(platform); - if (platformSpecific != null) { - processors.addAll(platformSpecific); - } - - // 按优先级排序 - processors.sort(Comparator.comparingInt(UploadCompleteProcessor::getOrder).reversed()); - - return processors; - } -} \ No newline at end of file diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/core/StrategyProxyFactory.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/core/StrategyProxyFactory.java deleted file mode 100644 index 88fcaae6..00000000 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/core/StrategyProxyFactory.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. - *

- * 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 - *

- * http://www.gnu.org/licenses/lgpl.html - *

- * 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, List>> overrides = new ConcurrentHashMap<>(); - - /** - * 注册重写 - */ - public void registerOverride(StorageStrategyOverride override) { - overrides.computeIfAbsent(override.getTargetType(), k -> new CopyOnWriteArrayList<>()).add(override); - } - - /** - * 创建代理 - */ - @SuppressWarnings("unchecked") - public T createProxy(T target) { - List> targetOverrides = overrides.get(target.getClass()); - if (targetOverrides == null || targetOverrides.isEmpty()) { - return target; - } - - // 为每个重写对象设置原始目标 - for (StorageStrategyOverride override : targetOverrides) { - if (override instanceof AbstractStorageStrategyOverride) { - ((AbstractStorageStrategyOverride)override).setOriginalTarget(target); - } - } - - return (T)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass() - .getInterfaces(), new StrategyInvocationHandler<>(target, targetOverrides)); - } - - /** - * 改进的调用处理器 - */ - private static class StrategyInvocationHandler implements InvocationHandler { - private final T target; - private final List> overrides; - private final Map overrideMethodCache = new ConcurrentHashMap<>(); - - public StrategyInvocationHandler(T target, List> 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 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); - } - } -} \ No newline at end of file diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/core/UploadPretreatment.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/core/UploadPretreatment.java index 324409db..fa2f8cda 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/core/UploadPretreatment.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/core/UploadPretreatment.java @@ -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 validators = new ArrayList<>(); - private FileNameGenerator nameGenerator; - private FilePathGenerator pathGenerator; - private ThumbnailProcessor thumbnailProcessor; - private final List completeProcessors = new ArrayList<>(); + private UploadProgressListener progressListener; + private final List 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 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); } } \ No newline at end of file diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/file/EnhancedMultipartFile.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/file/EnhancedMultipartFile.java new file mode 100644 index 00000000..582da151 --- /dev/null +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/file/EnhancedMultipartFile.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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(); + } + } +} diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/core/FileWrapper.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/file/FileWrapper.java similarity index 90% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/core/FileWrapper.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/domain/file/FileWrapper.java index a4604746..8d9b35b2 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/core/FileWrapper.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/file/FileWrapper.java @@ -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); } diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/file/ProgressAwareMultipartFile.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/file/ProgressAwareMultipartFile.java new file mode 100644 index 00000000..6d6af598 --- /dev/null +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/file/ProgressAwareMultipartFile.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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; + } + } +} diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/context/UploadContext.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/context/UploadContext.java similarity index 77% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/model/context/UploadContext.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/context/UploadContext.java index 5ebb18be..84c72ac8 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/context/UploadContext.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/context/UploadContext.java @@ -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 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 attributes) { this.attributes = attributes; } + + public UploadProgressListener getProgressListener() { + return progressListener; + } + + public void setProgressListener(UploadProgressListener progressListener) { + this.progressListener = progressListener; + } } \ No newline at end of file diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/req/ThumbnailInfo.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/req/ThumbnailInfo.java similarity index 94% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/model/req/ThumbnailInfo.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/req/ThumbnailInfo.java index a3fd3c0a..675e654b 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/req/ThumbnailInfo.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/req/ThumbnailInfo.java @@ -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省略 } \ No newline at end of file diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/req/ThumbnailSize.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/req/ThumbnailSize.java similarity index 96% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/model/req/ThumbnailSize.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/req/ThumbnailSize.java index a6414bca..8e39829e 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/req/ThumbnailSize.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/req/ThumbnailSize.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package top.continew.starter.storage.model.req; +package top.continew.starter.storage.domain.model.req; /** * 缩略图尺寸 diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/FileInfo.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/resp/FileInfo.java similarity index 92% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/FileInfo.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/resp/FileInfo.java index bc5de7f5..7618cc56 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/FileInfo.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/resp/FileInfo.java @@ -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; } diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/FilePartInfo.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/resp/FilePartInfo.java similarity index 98% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/FilePartInfo.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/resp/FilePartInfo.java index 85dc39dd..3aaaf4cb 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/FilePartInfo.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/resp/FilePartInfo.java @@ -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; diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/MultipartInitResp.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/resp/MultipartInitResp.java similarity index 97% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/MultipartInitResp.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/resp/MultipartInitResp.java index 124bd124..272fbba7 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/MultipartInitResp.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/resp/MultipartInitResp.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package top.continew.starter.storage.model.resp; +package top.continew.starter.storage.domain.model.resp; /** * 分片上传初始化结果 diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/MultipartUploadResp.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/resp/MultipartUploadResp.java similarity index 97% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/MultipartUploadResp.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/resp/MultipartUploadResp.java index e5e7d676..4e735d27 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/MultipartUploadResp.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/resp/MultipartUploadResp.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package top.continew.starter.storage.model.resp; +package top.continew.starter.storage.domain.model.resp; /** * 分片上传结果 diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/StrategyStatus.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/resp/StrategyStatusResp.java similarity index 86% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/StrategyStatus.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/resp/StrategyStatusResp.java index 1e3592e1..157c843e 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/StrategyStatus.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/resp/StrategyStatusResp.java @@ -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; diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/engine/StorageDecoratorManager.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/engine/StorageDecoratorManager.java new file mode 100644 index 00000000..7e3c640d --- /dev/null +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/engine/StorageDecoratorManager.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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, List>> decoratorMap = new ConcurrentHashMap<>(); + + private volatile boolean initialized = false; + + public StorageDecoratorManager(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @PostConstruct + public void init() { + if (initialized) { + return; + } + Map decorators = applicationContext + .getBeansOfType(StorageStrategyDecorator.class); + + for (Map.Entry entry : decorators.entrySet()) { + StorageStrategyDecorator decorator = entry.getValue(); + Class targetClass = decorator.getTargetStrategyClass(); + + if (targetClass != null) { + decoratorMap.computeIfAbsent((Class)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 strategyClass = strategy.getClass(); + List> 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> findApplicableDecorators(Class strategyClass) { + List> result = new ArrayList<>(); + + // 精确匹配 + List> exactMatch = decoratorMap.get(strategyClass); + if (exactMatch != null) { + result.addAll(exactMatch); + } + + // 继承匹配 + for (Map.Entry, List>> 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)targetClass, k -> new ArrayList<>()) + .add(decorator); + // 重新排序 + decoratorMap.get((Class)targetClass) + .sort(Comparator.comparingInt(StorageStrategyDecorator::getOrder)); + } + } + + /** + * 移除装饰器 + * + * @param decorator 装饰器 + */ + public void unregisterDecorator(StorageStrategyDecorator decorator) { + Class targetClass = decorator.getTargetStrategyClass(); + if (targetClass != null) { + List> decorators = decoratorMap + .get((Class)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; + } + } +} diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/router/StorageStrategyRegistrar.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/engine/StorageStrategyRegistrar.java similarity index 95% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/router/StorageStrategyRegistrar.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/engine/StorageStrategyRegistrar.java index 6c1c9cff..e42eba5c 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/router/StorageStrategyRegistrar.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/engine/StorageStrategyRegistrar.java @@ -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; diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/engine/StorageStrategyRouter.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/engine/StorageStrategyRouter.java new file mode 100644 index 00000000..6dcfb86a --- /dev/null +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/engine/StorageStrategyRouter.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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 { + + private static final Logger log = LoggerFactory.getLogger(StorageStrategyRouter.class); + + private final Map configStrategies = new ConcurrentHashMap<>(); + private final Map dynamicStrategies = new ConcurrentHashMap<>(); + private final Map decoratedStrategies = new ConcurrentHashMap<>(); + + private final StorageProperties storageProperties; + private final String configDefaultPlatform; + private volatile String dynamicDefaultPlatform; + private final StorageDecoratorManager decoratorManager; + + public StorageStrategyRouter(List registrars, + StorageProperties storageProperties, + StorageDecoratorManager decoratorManager) { + this.decoratorManager = decoratorManager; + this.storageProperties = storageProperties; + List 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 getAllPlatform() { + Set 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 getActiveStrategyInfo() { + Map 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 getFullStrategyStatus() { + Map status = new HashMap<>(); + + // 所有唯一的 platform + Set 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; + } +} \ No newline at end of file diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/req/MockMultipartFile.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/model/req/MockMultipartFile.java deleted file mode 100644 index 314cf11b..00000000 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/req/MockMultipartFile.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. - *

- * 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 - *

- * http://www.gnu.org/licenses/lgpl.html - *

- * 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 上传场景。 - *

- * 可用于接口调用中构造文件参数,如将字节数组、输入流包装成 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); - } - -} diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/FileNameGenerator.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/FileNameGenerator.java similarity index 88% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/FileNameGenerator.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/FileNameGenerator.java index 9f5a4180..5fabd7c7 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/FileNameGenerator.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/FileNameGenerator.java @@ -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; /** diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/FilePathGenerator.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/FilePathGenerator.java similarity index 88% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/FilePathGenerator.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/FilePathGenerator.java index a1c7e86e..ece4fbe4 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/FilePathGenerator.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/FilePathGenerator.java @@ -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; /** diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/FileValidator.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/FileValidator.java similarity index 83% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/FileValidator.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/FileValidator.java index d08347a6..c5c1dea4 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/FileValidator.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/FileValidator.java @@ -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; /** diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/ThumbnailProcessor.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/ThumbnailProcessor.java similarity index 84% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/ThumbnailProcessor.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/ThumbnailProcessor.java index c66717ae..67171d27 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/ThumbnailProcessor.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/ThumbnailProcessor.java @@ -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; diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/UploadCompleteProcessor.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/UploadCompleteProcessor.java similarity index 88% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/UploadCompleteProcessor.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/UploadCompleteProcessor.java index ac34bb4e..c3324120 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/UploadCompleteProcessor.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/UploadCompleteProcessor.java @@ -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; /** diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultFileNameGenerator.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/DefaultFileNameGenerator.java similarity index 70% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultFileNameGenerator.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/DefaultFileNameGenerator.java index fc32f683..2b58910b 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultFileNameGenerator.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/DefaultFileNameGenerator.java @@ -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 diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultFilePathGenerator.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/DefaultFilePathGenerator.java similarity index 74% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultFilePathGenerator.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/DefaultFilePathGenerator.java index 6bcf3dd5..4a98d815 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultFilePathGenerator.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/DefaultFilePathGenerator.java @@ -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 diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultThumbnailProcessor.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/DefaultThumbnailProcessor.java similarity index 69% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultThumbnailProcessor.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/DefaultThumbnailProcessor.java index 9a6393c1..9f020201 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultThumbnailProcessor.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/DefaultThumbnailProcessor.java @@ -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()); diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/FileSizeValidator.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/FileSizeValidator.java similarity index 84% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/FileSizeValidator.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/FileSizeValidator.java index cf42c357..30dfd8f7 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/FileSizeValidator.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/FileSizeValidator.java @@ -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 diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/FileTypeValidator.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/FileTypeValidator.java similarity index 86% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/FileTypeValidator.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/FileTypeValidator.java index 30bdfc86..b18fa5de 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/FileTypeValidator.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/FileTypeValidator.java @@ -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 diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/progress/ProgressInputStream.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/progress/ProgressInputStream.java new file mode 100644 index 00000000..dfa5ffa3 --- /dev/null +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/progress/ProgressInputStream.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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(); + } +} diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/progress/ProgressTracker.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/progress/ProgressTracker.java new file mode 100644 index 00000000..e1287aad --- /dev/null +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/progress/ProgressTracker.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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); + } + } +} diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/AbstractStorageStrategyOverride.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/progress/UploadProgressListener.java similarity index 52% rename from continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/AbstractStorageStrategyOverride.java rename to continew-starter-storage/src/main/java/top/continew/starter/storage/processor/progress/UploadProgressListener.java index ae3b7950..6f49426c 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/AbstractStorageStrategyOverride.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/progress/UploadProgressListener.java @@ -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 implements StorageStrategyOverride { - - 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() { } -} \ No newline at end of file + + /** + * 上传失败 + */ + default void onError(Exception e) { + } +} diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/registry/ProcessorRegistry.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/registry/ProcessorRegistry.java new file mode 100644 index 00000000..75b9f803 --- /dev/null +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/registry/ProcessorRegistry.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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, List> processors = new ConcurrentHashMap<>(); + private final Map, List>> 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 List getProcessors(Class type, String platform, UploadContext context) { + List result = new ArrayList<>(); + + // 添加全局处理器 + List 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, List> platformMap = platformProcessors.get(platform); + if (platformMap != null) { + List 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 getProcessor(Class type, String platform, UploadContext context) { + List processors = getProcessors(type, platform, context); + return processors.isEmpty() ? null : processors.get(0); + } +} \ No newline at end of file diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/router/StorageStrategyRouter.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/router/StorageStrategyRouter.java deleted file mode 100644 index 4d439b30..00000000 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/router/StorageStrategyRouter.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. - *

- * 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 - *

- * http://www.gnu.org/licenses/lgpl.html - *

- * 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 configStrategies = new ConcurrentHashMap<>(); - - /** - * 动态策略 - */ - private final Map dynamicStrategies = new ConcurrentHashMap<>(); - - public StorageStrategyRouter(List registrars) { - List 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 getAllPlatform() { - Set 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 getActiveStrategyInfo() { - Map info = new HashMap<>(); - - // 先添加配置文件策略 - configStrategies.keySet().forEach(platform -> info.put(platform, "CONFIG")); - - // 动态策略会覆盖同名的配置策略 - dynamicStrategies.keySet().forEach(platform -> info.put(platform, "DYNAMIC")); - - return info; - } - - /** - * 获取完整的策略状态 - */ - public Map getFullStrategyStatus() { - Map status = new HashMap<>(); - - // 所有唯一的 platform - Set 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; - } -} \ No newline at end of file diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/service/FileProcessor.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/service/FileProcessor.java index fd3a277e..f229a051 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/service/FileProcessor.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/service/FileProcessor.java @@ -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; /** * 文件处理器接口 diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/service/FileRecorder.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/service/FileRecorder.java index 83481741..a87fac1f 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/service/FileRecorder.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/service/FileRecorder.java @@ -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; diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/service/impl/DefaultFileRecorder.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/service/impl/DefaultFileRecorder.java index 51f7ff90..3dc6f7fc 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/service/impl/DefaultFileRecorder.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/service/impl/DefaultFileRecorder.java @@ -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; diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/StorageStrategy.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/StorageStrategy.java index 361d6586..8f6f96a0 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/StorageStrategy.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/StorageStrategy.java @@ -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; diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/LocalStorageStrategy.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/LocalStorageStrategy.java index d50bb5a0..ced72357 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/LocalStorageStrategy.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/LocalStorageStrategy.java @@ -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; diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/OssStorageStrategy.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/OssStorageStrategy.java index e1676bcf..0b84ae8d 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/OssStorageStrategy.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/OssStorageStrategy.java @@ -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())); diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/StorageStrategyDecorator.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/StorageStrategyDecorator.java new file mode 100644 index 00000000..70549d0b --- /dev/null +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/StorageStrategyDecorator.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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 implements StorageStrategy { + + protected T delegate; + + /** + * 获取被装饰的策略类型 + */ + public abstract Class 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 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 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 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 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 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 listParts(String bucket, String path, String uploadId) { + return getDelegate().listParts(bucket, path, uploadId); + } + + @Override + public void cleanup() { + getDelegate().cleanup(); + } +} \ No newline at end of file