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 extends StorageStrategy>)targetClass, k -> new ArrayList<>())
+ .add(decorator);
+ }
+ }
+
+ // 按优先级排序
+ decoratorMap.values().forEach(list -> list.sort(Comparator.comparingInt(StorageStrategyDecorator::getOrder)));
+ this.initialized = true;
+ }
+
+ /**
+ * 应用装饰器到策略实例
+ *
+ * @param strategy 存储策略实例
+ * @return {@link StorageStrategy }
+ */
+ @SuppressWarnings("unchecked")
+ public StorageStrategy applyDecorators(StorageStrategy strategy) {
+ if (!initialized) {
+ init();
+ }
+ if (strategy == null) {
+ return null;
+ }
+ Class extends StorageStrategy> strategyClass = strategy.getClass();
+ List> 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 extends StorageStrategy> 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 extends StorageStrategy>)targetClass, k -> new ArrayList<>())
+ .add(decorator);
+ // 重新排序
+ decoratorMap.get((Class extends StorageStrategy>)targetClass)
+ .sort(Comparator.comparingInt(StorageStrategyDecorator::getOrder));
+ }
+ }
+
+ /**
+ * 移除装饰器
+ *
+ * @param decorator 装饰器
+ */
+ public void unregisterDecorator(StorageStrategyDecorator> decorator) {
+ Class> targetClass = decorator.getTargetStrategyClass();
+ if (targetClass != null) {
+ List> decorators = decoratorMap
+ .get((Class extends StorageStrategy>)targetClass);
+ if (decorators != null) {
+ decorators.remove(decorator);
+ }
+ }
+ }
+
+ /**
+ * 装饰器注册事件
+ */
+ public static class DecoratorRegisteredEvent extends ApplicationEvent {
+ private final Class> targetClass;
+
+ public DecoratorRegisteredEvent(Object source, Class> targetClass) {
+ super(source);
+ this.targetClass = targetClass;
+ }
+
+ public Class> getTargetClass() {
+ return targetClass;
+ }
+ }
+
+ /**
+ * 装饰器注销事件
+ */
+ public static class DecoratorUnregisteredEvent extends ApplicationEvent {
+ private final Class> targetClass;
+
+ public DecoratorUnregisteredEvent(Object source, Class> targetClass) {
+ super(source);
+ this.targetClass = targetClass;
+ }
+
+ public Class> getTargetClass() {
+ return targetClass;
+ }
+ }
+}
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