refactor(storage): 优化存储模块

Co-authored-by: QAQ_Z<958142070@qq.com>



# message auto-generated for no-merge-commit merge:
merge storage into dev

优化存储模块

Created-by: QAQ_Z
Commit-by: QAQ_Z
Merged-by: Charles_7c
Description: <!--
  非常感谢您的 PR!在提交之前,请务必确保您 PR 的代码经过了完整测试,并且通过了代码规范检查。
-->

<!-- 在 [] 中输入 x 来勾选) -->

## PR 类型

<!-- 您的 PR 引入了哪种类型的变更? -->
<!-- 只支持选择一种类型,如果有多种类型,可以在更新日志中增加 “类型” 列。 -->

- [ ] 新 feature
- [ ] Bug 修复
- [x] 功能增强
- [ ] 文档变更
- [ ] 代码样式变更
- [x] 重构
- [ ] 性能改进
- [ ] 单元测试
- [ ] CI/CD
- [ ] 其他

## PR 目的

<!-- 描述一下您的 PR 解决了什么问题。如果可以,请链接到相关 issues。 -->
- 重新设计了文件上传的预处理和后处理流程
- 优化了文件验证、命名、路径生成等环节
- 增加了单文件上传监听逻辑
- 增加了装饰器模式,支持重写指定存储策略实现的具体方法

## 解决方案

<!-- 详细描述您是如何解决的问题 -->

## PR 测试

<!-- 如果可以,请为您的 PR 添加或更新单元测试。 -->
<!-- 请描述一下您是如何测试 PR 的。例如:创建/更新单元测试或添加相关的截图。 -->

## Changelog

| 模块  | Changelog | Related issues |
|-----|-----------| -------------- |
|   continew-starter-storage  |    优化存储模块       |                |

<!-- 如果有多种类型的变更,可以在变更日志表中增加 “类型” 列,该列的值与上方 “PR 类型” 相同。 -->
<!-- Related issues 格式为 Closes #<issue号>,或者 Fixes #<issue号>,或者 Resolves #<issue号>。 -->

## 其他信息

<!-- 请描述一下还有哪些注意事项。例如:如果引入了一个不向下兼容的变更,请描述其影响。 -->

## 提交前确认

- [x] PR 代码经过了完整测试,并且通过了代码规范检查
- [ ] 已经完整填写 Changelog,并链接到了相关 issues
- [x] PR 代码将要提交到 dev 分支

See merge request: continew/continew-starter!4
This commit is contained in:
QAQ_Z
2025-08-16 21:05:19 +08:00
committed by Charles_7c
parent 5ca34eebd1
commit e5002b8bfc
51 changed files with 2066 additions and 1194 deletions

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.annotation;
import java.lang.annotation.*;
/**
* 平台处理器注解
* <p>
* 该注解用于标记文件前置处理器类,以指定其适用的平台范围。
* 主要用于实现平台特定的文件处理逻辑,如文件名生成、路径转换、格式适配等。
* <p>
*
* @author echo
* @since 2.14.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PlatformProcessor {
/**
* 适用的平台列表
*/
String[] platforms();
}

View File

@@ -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 策略列表
*/

View File

@@ -19,9 +19,9 @@ package top.continew.starter.storage.autoconfigure;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.storage.autoconfigure.properties.S3StorageConfig;
import top.continew.starter.storage.autoconfigure.properties.OssStorageConfig;
import top.continew.starter.storage.autoconfigure.properties.StorageProperties;
import top.continew.starter.storage.router.StorageStrategyRegistrar;
import top.continew.starter.storage.engine.StorageStrategyRegistrar;
import top.continew.starter.storage.strategy.StorageStrategy;
import top.continew.starter.storage.strategy.impl.OssStorageStrategy;
@@ -33,24 +33,24 @@ import java.util.List;
* @author echo
* @since 2.14.0
*/
@ConditionalOnProperty(prefix = PropertiesConstants.STORAGE, name = "s3")
public class S3StorageAutoConfiguration implements StorageStrategyRegistrar {
@ConditionalOnProperty(prefix = PropertiesConstants.STORAGE, name = "oss")
public class OssStorageAutoConfiguration implements StorageStrategyRegistrar {
private final StorageProperties properties;
public S3StorageAutoConfiguration(StorageProperties properties) {
public OssStorageAutoConfiguration(StorageProperties properties) {
this.properties = properties;
}
/**
* 注册配置策略
* 注册 OSS 存储策略
*
* @param strategies 策略列表
*/
@Override
@Bean
public void register(List<StorageStrategy> strategies) {
for (S3StorageConfig config : properties.getS3()) {
for (OssStorageConfig config : properties.getOss()) {
if (config.isEnabled()) {
strategies.add(new OssStorageStrategy(config));
}

View File

@@ -19,29 +19,29 @@ package top.continew.starter.storage.autoconfigure;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import top.continew.starter.storage.annotation.PlatformProcessor;
import top.continew.starter.storage.autoconfigure.properties.StorageProperties;
import top.continew.starter.storage.core.FileStorageService;
import top.continew.starter.storage.core.ProcessorRegistry;
import top.continew.starter.storage.core.StrategyProxyFactory;
import top.continew.starter.storage.prehandle.*;
import top.continew.starter.storage.prehandle.impl.*;
import top.continew.starter.storage.router.StorageStrategyRegistrar;
import top.continew.starter.storage.router.StorageStrategyRouter;
import top.continew.starter.storage.engine.StorageDecoratorManager;
import top.continew.starter.storage.processor.registry.ProcessorRegistry;
import top.continew.starter.storage.processor.preprocess.*;
import top.continew.starter.storage.processor.preprocess.impl.*;
import top.continew.starter.storage.engine.StorageStrategyRegistrar;
import top.continew.starter.storage.engine.StorageStrategyRouter;
import top.continew.starter.storage.service.FileProcessor;
import top.continew.starter.storage.service.FileRecorder;
import top.continew.starter.storage.service.impl.DefaultFileRecorder;
import top.continew.starter.storage.strategy.StorageStrategyOverride;
import java.util.List;
import java.util.Map;
/**
* 存储自动配置
@@ -51,15 +51,17 @@ import java.util.List;
*/
@AutoConfiguration
@EnableConfigurationProperties(StorageProperties.class)
@Import({ProcessorRegistry.class, StrategyProxyFactory.class})
@Import({ProcessorRegistry.class})
public class StorageAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(StorageAutoConfiguration.class);
private final StorageProperties properties;
private final ApplicationContext applicationContext;
public StorageAutoConfiguration(StorageProperties properties) {
public StorageAutoConfiguration(StorageProperties properties, ApplicationContext applicationContext) {
this.properties = properties;
this.applicationContext = applicationContext;
}
/**
@@ -70,17 +72,27 @@ public class StorageAutoConfiguration {
*/
@Bean
public StorageStrategyRouter strategyRouter(List<StorageStrategyRegistrar> registrars) {
return new StorageStrategyRouter(registrars);
return new StorageStrategyRouter(registrars, properties, storageDecoratorManager());
}
/**
* S3存储自动配置
* 存储装饰器管理器
*
* @return {@link S3StorageAutoConfiguration }
* @return {@link StorageDecoratorManager }
*/
@Bean
public S3StorageAutoConfiguration s3StorageAutoConfiguration() {
return new S3StorageAutoConfiguration(properties);
public StorageDecoratorManager storageDecoratorManager() {
return new StorageDecoratorManager(applicationContext);
}
/**
* oss存储自动配置
*
* @return {@link OssStorageAutoConfiguration }
*/
@Bean
public OssStorageAutoConfiguration ossStorageAutoConfiguration() {
return new OssStorageAutoConfiguration(properties);
}
/**
@@ -99,16 +111,15 @@ public class StorageAutoConfiguration {
* @param router 路由
* @param storageProperties 存储属性
* @param processorRegistry 处理器注册表
* @param proxyFactory 代理工厂
* @param fileRecorder 文件记录器
* @return {@link FileStorageService }
*/
@Bean
public FileStorageService fileStorageService(StorageStrategyRouter router,
StorageProperties storageProperties,
ProcessorRegistry processorRegistry,
StrategyProxyFactory proxyFactory,
FileRecorder fileRecorder) {
return new FileStorageService(router, storageProperties, processorRegistry, proxyFactory, fileRecorder);
return new FileStorageService(router, storageProperties, processorRegistry, fileRecorder);
}
/**
@@ -123,124 +134,73 @@ public class StorageAutoConfiguration {
}
/**
* 默认文件名生成器
*
* @param registry 登记处
* @return {@link FileNameGenerator }
* 处理器注册中心
*/
@Bean
@ConditionalOnMissingBean(name = "defaultFileNameGenerator")
public FileNameGenerator defaultFileNameGenerator(ProcessorRegistry registry) {
DefaultFileNameGenerator generator = new DefaultFileNameGenerator();
registry.registerGlobalNameGenerator(generator);
return generator;
public ProcessorRegistry processorRegistry() {
ProcessorRegistry registry = new ProcessorRegistry();
// 自动发现并注册所有 FileProcessor 实现
Map<String, FileProcessor> processors = applicationContext.getBeansOfType(FileProcessor.class);
processors.values().forEach(processor -> {
// 检查是否有平台注解
PlatformProcessor annotation = processor.getClass().getAnnotation(PlatformProcessor.class);
if (annotation != null) {
for (String platform : annotation.platforms()) {
registry.register(processor, platform);
}
} else {
// 注册为全局处理器
registry.register(processor);
}
});
return registry;
}
/**
* 默认文件路径生成器
*
* @param registry 注册
* @return {@link FilePathGenerator }
* 默认文件生成器
*/
@Bean
@ConditionalOnMissingBean(name = "defaultFilePathGenerator")
public FilePathGenerator defaultFilePathGenerator(ProcessorRegistry registry) {
DefaultFilePathGenerator generator = new DefaultFilePathGenerator();
registry.registerGlobalPathGenerator(generator);
return generator;
@ConditionalOnMissingBean(FileNameGenerator.class)
public FileNameGenerator defaultFileNameGenerator() {
return new DefaultFileNameGenerator();
}
/**
* 默认路径生成器
*/
@Bean
@ConditionalOnMissingBean(FilePathGenerator.class)
public FilePathGenerator defaultFilePathGenerator() {
return new DefaultFilePathGenerator();
}
/**
* 默认缩略图处理器
*
* @param registry 注册
* @return {@link ThumbnailProcessor }
*/
@Bean
@ConditionalOnMissingBean(name = "defaultThumbnailProcessor")
@ConditionalOnMissingBean(ThumbnailProcessor.class)
@ConditionalOnClass(name = "net.coobird.thumbnailator.Thumbnails")
public ThumbnailProcessor defaultThumbnailProcessor(ProcessorRegistry registry) {
DefaultThumbnailProcessor processor = new DefaultThumbnailProcessor();
registry.registerGlobalThumbnailProcessor(processor);
return processor;
public ThumbnailProcessor defaultThumbnailProcessor() {
return new DefaultThumbnailProcessor();
}
/**
* 文件大小验证器
*
* @param registry 注册
* @return {@link FileValidator }
*/
@Bean
@ConditionalOnMissingBean(name = "fileSizeValidator")
public FileValidator fileSizeValidator(ProcessorRegistry registry, MultipartProperties multipartProperties) {
FileSizeValidator validator = new FileSizeValidator(multipartProperties);
registry.registerGlobalValidator(validator);
return validator;
public FileValidator fileSizeValidator(MultipartProperties multipartProperties) {
return new FileSizeValidator(multipartProperties);
}
/**
* 文件类型验证器
*
* @param registry 注册
* @return {@link FileValidator }
*/
@Bean
@ConditionalOnMissingBean(name = "fileTypeValidator")
public FileValidator fileTypeValidator(ProcessorRegistry registry) {
FileTypeValidator validator = new FileTypeValidator();
registry.registerGlobalValidator(validator);
return validator;
}
/**
* 策略重写自动注册
*/
@Configuration
@ConditionalOnBean(StorageStrategyOverride.class)
public static class StrategyOverrideConfiguration {
/**
* 注册覆盖
*/
@Autowired
public void registerOverrides(List<StorageStrategyOverride<?>> overrides, StrategyProxyFactory proxyFactory) {
for (StorageStrategyOverride<?> override : overrides) {
proxyFactory.registerOverride(override);
}
}
}
/**
* 处理器自动注册
*/
@Configuration
public static class ProcessorAutoConfiguration {
@Autowired(required = false)
public void registerGlobalProcessors(List<FileNameGenerator> nameGenerators,
List<FilePathGenerator> pathGenerators,
List<ThumbnailProcessor> thumbnailProcessors,
List<FileValidator> validators,
List<UploadCompleteProcessor> completeProcessors,
ProcessorRegistry registry) {
// 注册全局处理器
if (nameGenerators != null) {
nameGenerators.forEach(registry::registerGlobalNameGenerator);
}
if (pathGenerators != null) {
pathGenerators.forEach(registry::registerGlobalPathGenerator);
}
if (thumbnailProcessors != null) {
thumbnailProcessors.forEach(registry::registerGlobalThumbnailProcessor);
}
if (validators != null) {
validators.forEach(registry::registerGlobalValidator);
}
if (completeProcessors != null) {
completeProcessors.forEach(registry::registerGlobalCompleteProcessor);
}
}
public FileValidator fileTypeValidator() {
return new FileTypeValidator();
}
@PostConstruct

View File

@@ -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;
/**
* 是否启用路径样式访问

View File

@@ -18,6 +18,8 @@ package top.continew.starter.storage.autoconfigure.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.storage.common.constant.StorageConstant;
import top.continew.starter.storage.common.enums.DefaultStorageSource;
import java.util.ArrayList;
import java.util.List;
@@ -34,7 +36,12 @@ public class StorageProperties {
/**
* 默认使用的存储平台
*/
private String defaultPlatform = "local";
private String defaultPlatform = StorageConstant.DEFAULT_STORAGE_PLATFORM;
/**
* 默认存储配置来源 (配置文件/动态配置)
*/
private DefaultStorageSource defaultStorageSource = DefaultStorageSource.DYNAMIC;
/**
* 本地存储配置列表
@@ -42,9 +49,9 @@ public class StorageProperties {
private List<LocalStorageConfig> local = new ArrayList<>();
/**
* S3 存储配置列表
* oss 存储配置列表
*/
private List<S3StorageConfig> s3 = new ArrayList<>();
private List<OssStorageConfig> oss = new ArrayList<>();
public String getDefaultPlatform() {
return defaultPlatform;
@@ -54,6 +61,14 @@ public class StorageProperties {
this.defaultPlatform = defaultPlatform;
}
public DefaultStorageSource getDefaultStorageSource() {
return defaultStorageSource;
}
public void setDefaultStorageSource(DefaultStorageSource defaultStorageSource) {
this.defaultStorageSource = defaultStorageSource;
}
public List<LocalStorageConfig> getLocal() {
return local;
}
@@ -62,11 +77,11 @@ public class StorageProperties {
this.local = local;
}
public List<S3StorageConfig> getS3() {
return s3;
public List<OssStorageConfig> getOss() {
return oss;
}
public void setS3(List<S3StorageConfig> s3) {
this.s3 = s3;
public void setOss(List<OssStorageConfig> oss) {
this.oss = oss;
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.common.constant;
import top.continew.starter.core.constant.StringConstants;
/**
* 存储常数
*
* @author echo
* @since 2.14.0
*/
public class StorageConstant {
/**
* 默认存储平台
*/
public static final String DEFAULT_STORAGE_PLATFORM = "local";
/**
* 配置文件
*/
public static final String CONFIG = "CONFIG";
/**
* 动态配置
*/
public static final String DYNAMIC = "DYNAMIC";
/**
* 默认文件大小
*/
public static final Long DEFAULT_FILE_SIZE = 1024 * 1024 * 10L;
/**
* 默认的对象ACL
*/
public static final String DEFAULT_ACL = "private";
/**
* 缩略图后缀
*/
public static final String THUMBNAIL_SUFFIX = StringConstants.DOT + "thumb" + StringConstants.DOT;
/**
* ContentType 图片前缀
*/
public static final String CONTENT_TYPE_IMAGE = "image/";
}

View File

@@ -14,26 +14,25 @@
* limitations under the License.
*/
package top.continew.starter.storage.strategy;
package top.continew.starter.storage.common.enums;
/**
* 存储策略重写接口
* 默认存储配置来源
* 决定默认存储平台配置从哪里加载
*
* @author echo
* @since 2.14.0
*/
public interface StorageStrategyOverride<T extends StorageStrategy> {
public enum DefaultStorageSource {
/**
* 获取目标策略类型
* 从配置文件加载默认存储
*/
Class<T> getTargetType();
CONFIG,
/**
* 获取原始目标对象用于调用原始方法
* 从动态配置加载默认存储
*/
default T getOriginalTarget() {
// 这个方法会在代理创建时被设置
throw new UnsupportedOperationException("原始目标未设置");
}
}
DYNAMIC,
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -16,15 +16,27 @@
package top.continew.starter.storage.core;
import cn.hutool.core.util.StrUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.storage.autoconfigure.properties.StorageProperties;
import top.continew.starter.storage.exception.StorageException;
import top.continew.starter.storage.model.context.UploadContext;
import top.continew.starter.storage.model.resp.*;
import top.continew.starter.storage.router.StorageStrategyRouter;
import top.continew.starter.storage.common.constant.StorageConstant;
import top.continew.starter.storage.common.exception.StorageException;
import top.continew.starter.storage.domain.file.EnhancedMultipartFile;
import top.continew.starter.storage.domain.file.FileWrapper;
import top.continew.starter.storage.domain.file.ProgressAwareMultipartFile;
import top.continew.starter.storage.domain.model.context.UploadContext;
import top.continew.starter.storage.domain.model.req.ThumbnailInfo;
import top.continew.starter.storage.domain.model.resp.*;
import top.continew.starter.storage.processor.preprocess.*;
import top.continew.starter.storage.processor.progress.UploadProgressListener;
import top.continew.starter.storage.processor.registry.ProcessorRegistry;
import top.continew.starter.storage.engine.StorageStrategyRouter;
import top.continew.starter.storage.service.FileProcessor;
import top.continew.starter.storage.service.FileRecorder;
import top.continew.starter.storage.strategy.StorageStrategy;
import top.continew.starter.storage.strategy.impl.LocalStorageStrategy;
import java.io.InputStream;
import java.time.LocalDateTime;
@@ -40,21 +52,22 @@ import java.util.stream.Collectors;
*/
public class FileStorageService {
private static final Logger log = LoggerFactory.getLogger(FileStorageService.class);
private final StorageStrategyRouter router;
private final StorageProperties storageProperties;
private final ProcessorRegistry processorRegistry;
private final StrategyProxyFactory proxyFactory;
private final FileRecorder fileRecorder;
private final ThreadLocal<List<FileProcessor>> tempProcessors = ThreadLocal.withInitial(ArrayList::new);
private final ThreadLocal<UploadProgressListener> progressListener = new ThreadLocal<>();
public FileStorageService(StorageStrategyRouter router,
StorageProperties storageProperties,
ProcessorRegistry processorRegistry,
StrategyProxyFactory proxyFactory,
FileRecorder fileRecorder) {
this.router = router;
this.storageProperties = storageProperties;
this.processorRegistry = processorRegistry;
this.proxyFactory = proxyFactory;
this.fileRecorder = fileRecorder;
}
@@ -62,7 +75,7 @@ public class FileStorageService {
* 获取默认存储平台
*/
public String getDefaultPlatform() {
return storageProperties.getDefaultPlatform();
return router.getDefaultStorage();
}
/**
@@ -73,82 +86,376 @@ public class FileStorageService {
}
/**
* 创建上传预处理器(链式调用入口)
* MultipartFile 直接上传
*/
public UploadPretreatment of(MultipartFile file) {
return new UploadPretreatment(this, file);
return createPretreatment(file, null);
}
/**
* 创建上传预处理器,指定平台
* MultipartFile 指定平台
*/
public UploadPretreatment of(MultipartFile file, String platform) {
return new UploadPretreatment(this, file).setPlatform(platform);
return createPretreatment(file, platform);
}
/**
* 创建上传预处理器(支持 byte[]
* byte[] 上传
*/
public UploadPretreatment of(byte[] bytes, String filename, String contentType) {
FileWrapper wrapper = FileWrapper.of(bytes, filename, contentType);
return new UploadPretreatment(this, wrapper.toMultipartFile());
return createPretreatment(bytes, filename, contentType);
}
/**
* 创建上传预处理器(支持 InputStream
* InputStream 上传
*/
public UploadPretreatment of(InputStream inputStream, String filename, String contentType) {
FileWrapper wrapper = FileWrapper.of(inputStream, filename, contentType);
return new UploadPretreatment(this, wrapper.toMultipartFile());
return createPretreatment(inputStream, filename, contentType);
}
/**
* 创建上传预处理器(支持任意对象
*/
public UploadPretreatment of(Object obj) {
FileWrapper wrapper = FileWrapper.of(obj);
return new UploadPretreatment(this, wrapper.toMultipartFile());
}
/**
* 创建上传预处理器(支持任意对象,指定文件名和类型)
* 任意对象上传
*/
public UploadPretreatment of(Object obj, String filename, String contentType) {
FileWrapper wrapper = FileWrapper.of(obj, filename, contentType);
return new UploadPretreatment(this, wrapper.toMultipartFile());
return createPretreatment(obj, filename, contentType);
}
/**
* 执行上传(内部方法)
* 任意对象智能识别
*/
public FileInfo doUpload(UploadContext context) {
StorageStrategy strategy = getStrategy(context.getPlatform());
public UploadPretreatment of(Object obj) {
return createPretreatment(obj, null, null);
}
// 执行上传
strategy.upload(context.getBucket(), context.getFullPath(), context.getFile());
/**
* 创建预处理
*
* @param file 文件
* @param platform 站台
* @return {@link UploadPretreatment }
*/
private UploadPretreatment createPretreatment(MultipartFile file, String platform) {
UploadPretreatment pretreatment = new UploadPretreatment(this, file);
return platform != null ? pretreatment.platform(platform) : pretreatment;
}
// 构建文件信息
FileInfo fileInfo = strategy.getFileInfo(context.getBucket(), context.getFullPath());
/**
* 创建预处理
*
* @param source 来源
* @param filename 文件名
* @param contentType 内容类型
* @return {@link UploadPretreatment }
*/
private UploadPretreatment createPretreatment(Object source, String filename, String contentType) {
FileWrapper wrapper = (filename != null || contentType != null)
? FileWrapper.of(source, filename, contentType)
: FileWrapper.of(source);
return createPretreatment(wrapper.toMultipartFile(), null);
}
/**
* 添加处理器
*/
public void addProcessor(FileProcessor processor) {
tempProcessors.get().add(processor);
}
/**
* 设置进度监听器
*/
public void onProgress(UploadProgressListener listener) {
progressListener.set(listener);
}
/**
* 执行上传
*/
public FileInfo upload(UploadContext context) {
String platform = context.getPlatform();
try {
// 初始化处理器和监听器
List<FileProcessor> customProcessors = tempProcessors.get();
UploadProgressListener listener = progressListener.get();
// 设置进度监听器到上下文
if (listener != null) {
context.setProgressListener(listener);
}
// 包装文件
prepareFile(context, listener);
// 1. 执行文件验证(验证阶段)
setFileReadPhase(context.getFile(), ProgressAwareMultipartFile.ReadPhase.VALIDATION);
executeValidation(context, platform, customProcessors);
// 2. 处理文件名和路径生成
generateFileNameIfEmpty(context, platform, customProcessors);
generateFilePathIfEmpty(context, platform, customProcessors);
// 3. 设置默认bucket
if (StrUtil.isBlank(context.getBucket())) {
context.setBucket(getDefaultBucket(platform));
}
// 4. 准备缩略图处理
ThumbnailProcessor thumbnailProcessor = prepareThumbnail(context, platform, customProcessors);
// 5. 执行实际上传
setFileReadPhase(context.getFile(), ProgressAwareMultipartFile.ReadPhase.UPLOAD);
upload(platform, context.getBucket(), context.getFullPath(), context.getFile());
// 6. 构建文件信息
FileInfo fileInfo = buildFileInfo(platform, context);
// 7. 处理缩略图生成(缩略图阶段)
if (thumbnailProcessor != null && context.isGenerateThumbnail()) {
setFileReadPhase(context.getFile(), ProgressAwareMultipartFile.ReadPhase.THUMBNAIL);
processThumbnail(fileInfo, thumbnailProcessor, context);
}
// 8. 保存文件记录
if (fileRecorder != null) {
fileRecorder.save(fileInfo);
}
// 9. 触发完成事件
triggerCompleteEvent(fileInfo, context, platform, customProcessors);
return fileInfo;
} finally {
cleanup(context);
}
}
/**
* 准备文件
*/
private void prepareFile(UploadContext context, UploadProgressListener listener) {
MultipartFile file = context.getFile();
// 如果已经是 ProgressAwareMultipartFile不重复包装
if (file instanceof ProgressAwareMultipartFile) {
return;
}
// 判断是否需要缓存
boolean needCache = true;
// 如果有进度监听器,使用 ProgressAwareMultipartFile
if (listener != null) {
context.setFile(new ProgressAwareMultipartFile(file, needCache, listener));
} else if (!(file instanceof EnhancedMultipartFile)) {
// 否则只使用 EnhancedMultipartFile 提供缓存
context.setFile(EnhancedMultipartFile.wrap(file, needCache));
}
}
/**
* 设置文件读取阶段
*/
private void setFileReadPhase(MultipartFile file, ProgressAwareMultipartFile.ReadPhase phase) {
if (file instanceof ProgressAwareMultipartFile) {
((ProgressAwareMultipartFile)file).setReadPhase(phase);
}
}
/**
* 执行文件验证
*/
private void executeValidation(UploadContext context, String platform, List<FileProcessor> customProcessors) {
List<FileValidator> validators = collectProcessors(customProcessors, FileValidator.class, platform, context);
for (FileValidator validator : validators) {
if (validator.support(context)) {
validator.validate(context);
}
}
}
/**
* 仅在文件名为空时生成文件名
*/
private void generateFileNameIfEmpty(UploadContext context, String platform, List<FileProcessor> customProcessors) {
// 如果已有文件名,直接返回
if (StrUtil.isNotBlank(context.getFormatFileName())) {
return;
}
FileNameGenerator nameGenerator = findFirstProcessor(customProcessors, FileNameGenerator.class, platform, context);
if (nameGenerator != null && nameGenerator.support(context)) {
context.setFormatFileName(nameGenerator.generate(context));
}
}
/**
* 仅在路径为空时生成路径
*/
private void generateFilePathIfEmpty(UploadContext context, String platform, List<FileProcessor> customProcessors) {
// 如果已有路径,直接返回
if (StrUtil.isNotBlank(context.getPath())) {
return;
}
FilePathGenerator pathGenerator = findFirstProcessor(customProcessors, FilePathGenerator.class, platform, context);
if (pathGenerator != null && pathGenerator.support(context)) {
context.setPath(pathGenerator.path(context));
}
}
/**
* 准备缩略图处理
*/
private ThumbnailProcessor prepareThumbnail(UploadContext context,
String platform,
List<FileProcessor> customProcessors) {
ThumbnailProcessor thumbnailProcessor = findFirstProcessor(customProcessors, ThumbnailProcessor.class, platform, context);
boolean needThumbnail = thumbnailProcessor != null && thumbnailProcessor.support(context);
context.setGenerateThumbnail(needThumbnail);
return thumbnailProcessor;
}
/**
* 构建文件信息
*/
private FileInfo buildFileInfo(String platform, UploadContext context) {
FileInfo fileInfo = getFileInfo(platform, context.getBucket(), context.getFullPath());
fileInfo.setOriginalFileName(context.getFile().getOriginalFilename());
fileInfo.getMetadata().putAll(context.getMetadata());
// 保存文件记录
if (fileRecorder != null) {
fileRecorder.save(fileInfo);
if (context.getMetadata() != null && !context.getMetadata().isEmpty()) {
fileInfo.getMetadata().putAll(context.getMetadata());
}
return fileInfo;
}
/**
* 触发上传完成事件
*/
private void triggerCompleteEvent(FileInfo fileInfo,
UploadContext context,
String platform,
List<FileProcessor> customProcessors) {
List<UploadCompleteProcessor> completeProcessors = collectProcessors(customProcessors, UploadCompleteProcessor.class, platform, context);
for (UploadCompleteProcessor processor : completeProcessors) {
if (processor.support(context)) {
processor.onComplete(fileInfo);
}
}
}
/**
* 收集指定类型的处理器
*/
private <T extends FileProcessor> List<T> collectProcessors(List<FileProcessor> customProcessors,
Class<T> processorClass,
String platform,
UploadContext context) {
List<T> processors = new ArrayList<>();
// 添加自定义处理器
if (customProcessors != null) {
customProcessors.stream()
.filter(processorClass::isInstance)
.map(processorClass::cast)
.forEach(processors::add);
}
// 添加注册的处理器
processors.addAll(processorRegistry.getProcessors(processorClass, platform, context));
return processors;
}
/**
* 查找第一个匹配的处理器
*/
private <T extends FileProcessor> T findFirstProcessor(List<FileProcessor> customProcessors,
Class<T> processorClass,
String platform,
UploadContext context) {
// 优先从自定义处理器中查找
if (customProcessors != null) {
Optional<T> customProcessor = customProcessors.stream()
.filter(processorClass::isInstance)
.map(processorClass::cast)
.findFirst();
if (customProcessor.isPresent()) {
return customProcessor.get();
}
}
// 从注册的处理器中获取
return processorRegistry.getProcessor(processorClass, platform, context);
}
/**
* 处理缩略图
*/
private void processThumbnail(FileInfo fileInfo, ThumbnailProcessor processor, UploadContext context) {
try {
MultipartFile file = context.getFile();
// 缩略图生成使用普通流
try (InputStream is = file.getInputStream()) {
ThumbnailInfo thumbnailInfo = processor.process(context, is);
// 生成缩略图路径
String filePrefix = StrUtil.subBefore(fileInfo.getPath(), StringConstants.DOT, true);
String thumbnailPath = filePrefix + StorageConstant.THUMBNAIL_SUFFIX + thumbnailInfo.getFormat();
String thumbnailFileName = StrUtil.subAfter(thumbnailPath, StringConstants.SLASH, true);
// 创建缩略图文件
EnhancedMultipartFile thumbnailFile = new EnhancedMultipartFile(thumbnailFileName, thumbnailFileName, StorageConstant.CONTENT_TYPE_IMAGE + thumbnailInfo
.getFormat(), thumbnailInfo.getData());
// 上传缩略图
upload(context.getPlatform(), context.getBucket(), thumbnailPath, thumbnailFile);
fileInfo.setThumbnailPath(thumbnailPath);
fileInfo.setThumbnailSize((long)thumbnailInfo.getData().length);
}
} catch (Exception e) {
log.warn("缩略图处理失败: {}", e.getMessage());
}
}
/**
* 清理资源
*/
private void cleanup(UploadContext context) {
// 清理临时处理器和监听器
tempProcessors.remove();
progressListener.remove();
// 清理文件缓存
cleanupFileCache(context.getFile());
}
/**
* 清理文件缓存
*/
private void cleanupFileCache(MultipartFile file) {
if (file instanceof ProgressAwareMultipartFile) {
((ProgressAwareMultipartFile)file).clearCache();
} else if (file instanceof EnhancedMultipartFile) {
((EnhancedMultipartFile)file).clearCache();
}
}
/**
* 初始化分片上传
*
* @param bucket 存储桶
* @param platform 平台
* @param path 路径
* @param contentType 内容类型
* @param metadata 元数据
* @return {@link MultipartInitResp }
*/
public MultipartInitResp initMultipartUpload(String bucket,
String platform,
@@ -156,8 +463,7 @@ public class FileStorageService {
String contentType,
Map<String, String> metadata) {
bucket = bucket == null ? getDefaultBucket(platform) : bucket;
StorageStrategy strategy = getStrategy(platform);
MultipartInitResp result = strategy.initMultipartUpload(bucket, path, contentType, metadata);
MultipartInitResp result = router.route(platform).initMultipartUpload(bucket, path, contentType, metadata);
// 记录文件信息
if (fileRecorder != null) {
@@ -177,14 +483,6 @@ public class FileStorageService {
/**
* 上传分片
*
* @param platform 平台
* @param bucket 存储桶
* @param path 路径
* @param uploadId 上传id
* @param partNumber 分片编号
* @param data 数据
* @return {@link MultipartUploadResp }
*/
public MultipartUploadResp uploadPart(String platform,
String bucket,
@@ -192,8 +490,7 @@ public class FileStorageService {
String uploadId,
int partNumber,
InputStream data) {
StorageStrategy strategy = getStrategy(platform);
MultipartUploadResp result = strategy.uploadPart(bucket, path, uploadId, partNumber, data);
MultipartUploadResp result = router.route(platform).uploadPart(bucket, path, uploadId, partNumber, data);
// 记录分片信息
if (fileRecorder != null && result.isSuccess()) {
@@ -214,13 +511,6 @@ public class FileStorageService {
/**
* 完成分片上传
*
* @param platform 平台
* @param bucket 存储桶
* @param path 路径
* @param uploadId 上传id
* @param clientParts 分片信息
* @return {@link FileInfo }
*/
public FileInfo completeMultipartUpload(String platform,
String bucket,
@@ -252,13 +542,8 @@ public class FileStorageService {
// 获取策略,判断是否需要验证
boolean needVerify = true;
StorageStrategy strategy = getStrategy(platform);
if (strategy instanceof LocalStorageStrategy) {
needVerify = false;
}
// 完成上传
FileInfo fileInfo = strategy.completeMultipartUpload(bucket, path, uploadId, parts, needVerify);
FileInfo fileInfo = router.route(platform).completeMultipartUpload(bucket, path, uploadId, parts, needVerify);
// 更新文件记录
if (fileRecorder != null) {
@@ -275,15 +560,9 @@ public class FileStorageService {
/**
* 取消分片上传
*
* @param platform 平台
* @param bucket 存储桶
* @param path 路径
* @param uploadId 上传id
*/
public void abortMultipartUpload(String platform, String bucket, String path, String uploadId) {
StorageStrategy strategy = getStrategy(platform);
strategy.abortMultipartUpload(bucket, path, uploadId);
router.route(platform).abortMultipartUpload(bucket, path, uploadId);
// 删除相关记录
if (fileRecorder != null) {
@@ -293,8 +572,6 @@ public class FileStorageService {
/**
* 验证分片完整性
*
* @param parts 分片信息
*/
private void validatePartsCompleteness(List<MultipartUploadResp> parts) {
if (parts.isEmpty()) {
@@ -314,7 +591,7 @@ public class FileStorageService {
List<Integer> failedParts = parts.stream()
.filter(part -> !part.isSuccess())
.map(MultipartUploadResp::getPartNumber)
.collect(Collectors.toList());
.toList();
if (!failedParts.isEmpty()) {
throw new StorageException("存在失败的分片: " + failedParts);
@@ -323,24 +600,9 @@ public class FileStorageService {
/**
* 列出已上传的分片
*
* @param platform 平台
* @param bucket 存储桶
* @param path 路径
* @param uploadId 上传id
* @return {@link List }<{@link MultipartUploadResp }>
*/
public List<MultipartUploadResp> listParts(String platform, String bucket, String path, String uploadId) {
StorageStrategy strategy = router.route(platform);
return strategy.listParts(bucket, path, uploadId);
}
/**
* 获取存储策略(应用代理)
*/
private StorageStrategy getStrategy(String platform) {
StorageStrategy strategy = router.route(platform);
return proxyFactory.createProxy(strategy);
return router.route(platform).listParts(bucket, path, uploadId);
}
/**
@@ -362,7 +624,7 @@ public class FileStorageService {
* @param file 文件
*/
public void upload(String platform, String bucket, String path, MultipartFile file) {
router.route(platform).upload(path, bucket, file);
router.route(platform).upload(bucket, path, file);
}
/**
@@ -458,6 +720,15 @@ public class FileStorageService {
router.registerDynamic(strategy);
}
/**
* 加载动态默认存储
*
* @param platform 站台
*/
public void defaultStorage(String platform) {
router.registerDynamicDefaultStorage(platform);
}
/**
* 卸载动态注册的策略
*/
@@ -499,7 +770,7 @@ public class FileStorageService {
/**
* 获取策略详细信息
*/
public Map<String, StrategyStatus> getStrategyStatus() {
public Map<String, StrategyStatusResp> getStrategyStatus() {
return router.getFullStrategyStatus();
}

View File

@@ -1,215 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.core;
import top.continew.starter.storage.prehandle.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 全局处理器注册表
*
* @author echo
* @since 2.14.0
*/
public class ProcessorRegistry {
// 全局处理器
private final Map<String, FileNameGenerator> globalNameGenerators = new ConcurrentHashMap<>();
private final Map<String, FilePathGenerator> globalPathGenerators = new ConcurrentHashMap<>();
private final Map<String, ThumbnailProcessor> globalThumbnailProcessors = new ConcurrentHashMap<>();
private final List<FileValidator> globalValidators = new CopyOnWriteArrayList<>();
private final List<UploadCompleteProcessor> globalCompleteProcessors = new CopyOnWriteArrayList<>();
// 平台特定处理器
private final Map<String, Map<String, FileNameGenerator>> platformNameGenerators = new ConcurrentHashMap<>();
private final Map<String, Map<String, FilePathGenerator>> platformPathGenerators = new ConcurrentHashMap<>();
private final Map<String, Map<String, ThumbnailProcessor>> platformThumbnailProcessors = new ConcurrentHashMap<>();
private final Map<String, List<FileValidator>> platformValidators = new ConcurrentHashMap<>();
private final Map<String, List<UploadCompleteProcessor>> platformCompleteProcessors = new ConcurrentHashMap<>();
/**
* 注册全局文件名生成器
*/
public void registerGlobalNameGenerator(FileNameGenerator generator) {
globalNameGenerators.put(generator.getName(), generator);
}
public void registerGlobalPathGenerator(FilePathGenerator generator) {
globalPathGenerators.put(generator.getName(), generator);
}
/**
* 注册平台特定的文件名生成器
*/
public void registerPlatformNameGenerator(String platform, FileNameGenerator generator) {
platformNameGenerators.computeIfAbsent(platform, k -> new ConcurrentHashMap<>())
.put(generator.getName(), generator);
}
public void registerPlatformPathGenerator(String platform, FilePathGenerator generator) {
platformPathGenerators.computeIfAbsent(platform, k -> new ConcurrentHashMap<>())
.put(generator.getName(), generator);
}
/**
* 注册全局缩略图处理器
*/
public void registerGlobalThumbnailProcessor(ThumbnailProcessor processor) {
globalThumbnailProcessors.put(processor.getName(), processor);
}
/**
* 注册平台特定的缩略图处理器
*/
public void registerPlatformThumbnailProcessor(String platform, ThumbnailProcessor processor) {
platformThumbnailProcessors.computeIfAbsent(platform, k -> new ConcurrentHashMap<>())
.put(processor.getName(), processor);
}
/**
* 注册全局验证器
*/
public void registerGlobalValidator(FileValidator validator) {
globalValidators.add(validator);
}
/**
* 注册平台特定的验证器
*/
public void registerPlatformValidator(String platform, FileValidator validator) {
platformValidators.computeIfAbsent(platform, k -> new CopyOnWriteArrayList<>()).add(validator);
}
/**
* 注册全局完成处理器
*/
public void registerGlobalCompleteProcessor(UploadCompleteProcessor processor) {
globalCompleteProcessors.add(processor);
}
/**
* 注册平台特定的完成处理器
*/
public void registerPlatformCompleteProcessor(String platform, UploadCompleteProcessor processor) {
platformCompleteProcessors.computeIfAbsent(platform, k -> new CopyOnWriteArrayList<>()).add(processor);
}
/**
* 获取文件名生成器(平台 > 全局)
*/
public FileNameGenerator getNameGenerator(String platform) {
// 先查找平台特定的
Map<String, FileNameGenerator> platformGenerators = platformNameGenerators.get(platform);
if (platformGenerators != null && !platformGenerators.isEmpty()) {
return platformGenerators.values()
.stream()
.max(Comparator.comparingInt(FileNameGenerator::getOrder))
.orElse(null);
}
// 再查找全局的
return globalNameGenerators.values()
.stream()
.max(Comparator.comparingInt(FileNameGenerator::getOrder))
.orElse(null);
}
public FilePathGenerator getPathGenerator(String platform) {
// 先查找平台特定的
Map<String, FilePathGenerator> platformGenerators = platformPathGenerators.get(platform);
if (platformGenerators != null && !platformGenerators.isEmpty()) {
return platformGenerators.values()
.stream()
.max(Comparator.comparingInt(FilePathGenerator::getOrder))
.orElse(null);
}
// 再查找全局的
return globalPathGenerators.values()
.stream()
.max(Comparator.comparingInt(FilePathGenerator::getOrder))
.orElse(null);
}
/**
* 获取缩略图处理器(平台 > 全局)
*/
public ThumbnailProcessor getThumbnailProcessor(String platform) {
// 先查找平台特定的
Map<String, ThumbnailProcessor> platformProcessors = platformThumbnailProcessors.get(platform);
if (platformProcessors != null && !platformProcessors.isEmpty()) {
return platformProcessors.values()
.stream()
.max(Comparator.comparingInt(ThumbnailProcessor::getOrder))
.orElse(null);
}
// 再查找全局的
return globalThumbnailProcessors.values()
.stream()
.max(Comparator.comparingInt(ThumbnailProcessor::getOrder))
.orElse(null);
}
/**
* 获取验证器列表(合并全局和平台)
*/
public List<FileValidator> getValidators(String platform) {
List<FileValidator> validators = new ArrayList<>();
// 先添加全局验证器
validators.addAll(globalValidators);
// 再添加平台特定验证器
List<FileValidator> platformSpecific = platformValidators.get(platform);
if (platformSpecific != null) {
validators.addAll(platformSpecific);
}
// 按优先级排序(优先级高的在前)
validators.sort(Comparator.comparingInt(FileValidator::getOrder).reversed());
return validators;
}
/**
* 获取完成处理器列表(合并全局和平台)
*/
public List<UploadCompleteProcessor> getCompleteProcessors(String platform) {
List<UploadCompleteProcessor> processors = new ArrayList<>();
// 先添加全局处理器
processors.addAll(globalCompleteProcessors);
// 再添加平台特定处理器
List<UploadCompleteProcessor> platformSpecific = platformCompleteProcessors.get(platform);
if (platformSpecific != null) {
processors.addAll(platformSpecific);
}
// 按优先级排序
processors.sort(Comparator.comparingInt(UploadCompleteProcessor::getOrder).reversed());
return processors;
}
}

View File

@@ -1,171 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.core;
import top.continew.starter.storage.strategy.StorageStrategy;
import top.continew.starter.storage.strategy.StorageStrategyOverride;
import top.continew.starter.storage.strategy.impl.AbstractStorageStrategyOverride;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 策略代理工厂
*
* @author echo
* @since 2.14.0
*/
public class StrategyProxyFactory {
private final Map<Class<?>, List<StorageStrategyOverride<?>>> overrides = new ConcurrentHashMap<>();
/**
* 注册重写
*/
public <T extends StorageStrategy> void registerOverride(StorageStrategyOverride<T> override) {
overrides.computeIfAbsent(override.getTargetType(), k -> new CopyOnWriteArrayList<>()).add(override);
}
/**
* 创建代理
*/
@SuppressWarnings("unchecked")
public <T extends StorageStrategy> T createProxy(T target) {
List<StorageStrategyOverride<?>> targetOverrides = overrides.get(target.getClass());
if (targetOverrides == null || targetOverrides.isEmpty()) {
return target;
}
// 为每个重写对象设置原始目标
for (StorageStrategyOverride<?> override : targetOverrides) {
if (override instanceof AbstractStorageStrategyOverride) {
((AbstractStorageStrategyOverride<T>)override).setOriginalTarget(target);
}
}
return (T)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass()
.getInterfaces(), new StrategyInvocationHandler<>(target, targetOverrides));
}
/**
* 改进的调用处理器
*/
private static class StrategyInvocationHandler<T extends StorageStrategy> implements InvocationHandler {
private final T target;
private final List<StorageStrategyOverride<?>> overrides;
private final Map<String, Method> overrideMethodCache = new ConcurrentHashMap<>();
public StrategyInvocationHandler(T target, List<StorageStrategyOverride<?>> overrides) {
this.target = target;
this.overrides = overrides;
cacheOverrideMethods();
}
/**
* 缓存重写方法
*/
private void cacheOverrideMethods() {
for (StorageStrategyOverride<?> override : overrides) {
Class<?> overrideClass = override.getClass();
// 获取目标策略类的所有方法
Class<?> targetClass = override.getTargetType();
Method[] targetMethods = getAllMethods(targetClass);
for (Method targetMethod : targetMethods) {
try {
// 查找重写类中是否有相同签名的方法
Method overrideMethod = overrideClass.getMethod(targetMethod.getName(), targetMethod
.getParameterTypes());
// 检查方法是否真的被重写了(不是从接口继承的默认方法)
if (isMethodOverridden(overrideMethod, overrideClass)) {
overrideMethodCache.put(targetMethod
.getName() + getMethodSignature(targetMethod), overrideMethod);
}
} catch (NoSuchMethodException e) {
// 重写类中没有这个方法,忽略
}
}
}
}
/**
* 获取类及其所有接口的方法
*/
private Method[] getAllMethods(Class<?> clazz) {
// 添加类本身的方法
Set<Method> methods = new HashSet<>(Arrays.asList(clazz.getMethods()));
// 添加所有接口的方法
for (Class<?> iface : clazz.getInterfaces()) {
methods.addAll(Arrays.asList(iface.getMethods()));
}
return methods.toArray(new Method[0]);
}
/**
* 检查方法是否真的被重写了
*/
private boolean isMethodOverridden(Method method, Class<?> overrideClass) {
// 如果方法声明在重写类中(而不是父类或接口),则认为是重写的
return method.getDeclaringClass().equals(overrideClass) || (!method.getDeclaringClass()
.isInterface() && !method.getDeclaringClass().equals(AbstractStorageStrategyOverride.class) && !method
.getDeclaringClass()
.equals(Object.class));
}
/**
* 获取方法签名
*/
private String getMethodSignature(Method method) {
StringBuilder sb = new StringBuilder("(");
for (Class<?> paramType : method.getParameterTypes()) {
sb.append(paramType.getName()).append(",");
}
if (sb.length() > 1) {
sb.setLength(sb.length() - 1);
}
sb.append(")");
return sb.toString();
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodKey = method.getName() + getMethodSignature(method);
Method overrideMethod = overrideMethodCache.get(methodKey);
if (overrideMethod != null) {
// 找到重写方法,调用重写逻辑
for (StorageStrategyOverride<?> override : overrides) {
if (overrideMethod.getDeclaringClass().equals(override.getClass())) {
return overrideMethod.invoke(override, args);
}
}
}
// 没有重写,调用原方法
return method.invoke(target, args);
}
}
}

View File

@@ -17,17 +17,16 @@
package top.continew.starter.storage.core;
import org.springframework.web.multipart.MultipartFile;
import top.continew.starter.storage.model.context.UploadContext;
import top.continew.starter.storage.model.req.MockMultipartFile;
import top.continew.starter.storage.model.req.ThumbnailInfo;
import top.continew.starter.storage.model.req.ThumbnailSize;
import top.continew.starter.storage.model.resp.FileInfo;
import top.continew.starter.storage.prehandle.*;
import top.continew.starter.storage.util.StorageUtils;
import top.continew.starter.storage.domain.model.context.UploadContext;
import top.continew.starter.storage.domain.model.req.ThumbnailSize;
import top.continew.starter.storage.domain.model.resp.FileInfo;
import top.continew.starter.storage.processor.preprocess.*;
import top.continew.starter.storage.processor.progress.UploadProgressListener;
import top.continew.starter.storage.service.FileProcessor;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* 上传预处理器,支持链式调用
@@ -39,257 +38,125 @@ public class UploadPretreatment {
private final FileStorageService storageService;
private final UploadContext context;
private final List<FileValidator> validators = new ArrayList<>();
private FileNameGenerator nameGenerator;
private FilePathGenerator pathGenerator;
private ThumbnailProcessor thumbnailProcessor;
private final List<UploadCompleteProcessor> completeProcessors = new ArrayList<>();
private UploadProgressListener progressListener;
private final List<FileProcessor> processors = new ArrayList<>();
public UploadPretreatment(FileStorageService storageService, MultipartFile file) {
this.storageService = storageService;
this.context = new UploadContext();
this.context.setFile(file);
// 设置默认平台
this.context.setPlatform(storageService.getDefaultPlatform());
}
/**
* 设置存储平台
* 设置平台
*
* @param platform
* @param platform
* @return {@link UploadPretreatment }
*/
public UploadPretreatment setPlatform(String platform) {
public UploadPretreatment platform(String platform) {
context.setPlatform(platform);
return this;
}
/**
* 设置桶
* 设置存储
*
* @param bucket 桶
* @param bucket 存储
* @return {@link UploadPretreatment }
*/
public UploadPretreatment setBucket(String bucket) {
public UploadPretreatment bucket(String bucket) {
context.setBucket(bucket);
return this;
}
/**
* 设置路径
*
* @param path 路径
* @return {@link UploadPretreatment }
*/
public UploadPretreatment setPath(String path) {
public UploadPretreatment path(String path) {
context.setPath(path);
return this;
}
/**
* 设置文件名
* 格式化文件名 - 不传则使用全局格式化
*
* @param fileName 文件名
* @return {@link UploadPretreatment }
*/
public UploadPretreatment setFileName(String fileName) {
context.setFileName(fileName);
public UploadPretreatment fileName(String fileName) {
context.setFormatFileName(fileName);
return this;
}
/**
* 添加元数据
*/
public UploadPretreatment addMetadata(String key, String value) {
public UploadPretreatment metadata(String key, String value) {
context.getMetadata().put(key, value);
return this;
}
/**
* 添加扩展属性
* 添加处理器
*
* @param processor 处理器
* @return {@link UploadPretreatment }
*/
public UploadPretreatment addAttribute(String key, Object value) {
context.getAttributes().put(key, value);
public UploadPretreatment processor(FileProcessor processor) {
processors.add(processor);
return this;
}
/**
* 启用缩略图
* 设置缩略图
*
* @param width 宽度
* @param height 高度
* @return {@link UploadPretreatment }
*/
public UploadPretreatment enableThumbnail(int width, int height) {
public UploadPretreatment thumbnail(int width, int height) {
context.setGenerateThumbnail(true);
context.setThumbnailSize(new ThumbnailSize(width, height));
return this;
}
/**
* 添加验证
* 设置进度监听
*/
public UploadPretreatment addValidator(FileValidator validator) {
validators.add(validator);
public UploadPretreatment onProgress(UploadProgressListener listener) {
this.progressListener = listener;
return this;
}
/**
* 设置文件名生成器
* 设置简单的进度监听(只关心百分比)
*/
public UploadPretreatment setNameGenerator(FileNameGenerator generator) {
this.nameGenerator = generator;
return this;
}
public UploadPretreatment setPathGenerator(FilePathGenerator generator) {
this.pathGenerator = generator;
return this;
}
/**
* 设置缩略图处理器
*/
public UploadPretreatment setThumbnailProcessor(ThumbnailProcessor processor) {
this.thumbnailProcessor = processor;
return this;
}
/**
* 添加上传完成处理器
*/
public UploadPretreatment addCompleteProcessor(UploadCompleteProcessor processor) {
completeProcessors.add(processor);
public UploadPretreatment onProgress(Consumer<Integer> progressConsumer) {
this.progressListener = (bytesRead, totalBytes, percentage) -> progressConsumer.accept(percentage);
return this;
}
/**
* 执行上传
*
* @return {@link FileInfo }
*/
public FileInfo upload() {
// 应用处理器
applyProcessors();
// 执行验证
validate();
// 生成默认存储桶(如果未设置)
if (context.getBucket() == null || context.getBucket().trim().isEmpty()) {
context.setBucket(generateDefaultBucket());
for (FileProcessor processor : processors) {
storageService.addProcessor(processor);
}
// 生成文件名
if (context.getFileName() == null) {
context.setFileName(generateFileName());
}
// 生成文件路径
if (context.getPath() == null) {
context.setPath(generateFilePath());
// 设置进度监听器
if (progressListener != null) {
storageService.onProgress(progressListener);
}
// 执行上传
FileInfo fileInfo = storageService.doUpload(context);
// 处理缩略图
if (context.isGenerateThumbnail()) {
processThumbnail(fileInfo);
}
// 触发完成事件
triggerCompleteEvent(fileInfo);
return fileInfo;
}
/**
* 应用处理器
*/
private void applyProcessors() {
// 从存储服务获取全局处理器
ProcessorRegistry registry = storageService.getProcessorRegistry();
// 合并处理器:自定义 > 平台 > 全局
if (nameGenerator == null) {
nameGenerator = registry.getNameGenerator(context.getPlatform());
}
if (pathGenerator == null) {
pathGenerator = registry.getPathGenerator(context.getPlatform());
}
if (thumbnailProcessor == null && context.isGenerateThumbnail()) {
thumbnailProcessor = registry.getThumbnailProcessor(context.getPlatform());
}
// 合并验证器
validators.addAll(0, registry.getValidators(context.getPlatform()));
// 合并完成处理器
completeProcessors.addAll(0, registry.getCompleteProcessors(context.getPlatform()));
}
/**
* 执行验证
*/
private void validate() {
for (FileValidator validator : validators) {
if (validator.support(context)) {
validator.validate(context);
}
}
}
/**
* 生成文件名
*/
private String generateFileName() {
if (nameGenerator != null && nameGenerator.support(context)) {
return nameGenerator.generate(context);
}
return StorageUtils.generateFileName(context.getFile().getOriginalFilename());
}
private String generateFilePath() {
if (pathGenerator != null && pathGenerator.support(context)) {
return pathGenerator.path(context);
}
// 默认使用时间戳
return StorageUtils.generatePath();
}
/**
* 生成默认存储桶
*/
private String generateDefaultBucket() {
return storageService.getDefaultBucket(context.getPlatform());
}
/**
* 处理缩略图
*
* @param fileInfo 文件信息
*/
private void processThumbnail(FileInfo fileInfo) {
if (thumbnailProcessor != null && thumbnailProcessor.support(context)) {
try (InputStream is = storageService.download(context.getPlatform(), fileInfo.getPath())) {
ThumbnailInfo thumbnailInfo = thumbnailProcessor.process(context, is);
// 上传缩略图
String thumbnailPath = fileInfo.getPath() + "_thumb." + thumbnailInfo.getFormat();
// 创建模拟的文件信息
MockMultipartFile thumbnailFile = new MockMultipartFile("thumbnail", "thumbnail." + thumbnailInfo
.getFormat(), "image/" + thumbnailInfo.getFormat(), thumbnailInfo.getData());
storageService.upload(context.getPlatform(), context.getBucket(), thumbnailPath, thumbnailFile);
fileInfo.setThumbnailPath(thumbnailPath);
} catch (Exception e) {
}
}
}
/**
* 触发完成事件
*/
private void triggerCompleteEvent(FileInfo fileInfo) {
for (UploadCompleteProcessor processor : completeProcessors) {
if (processor.support(context)) {
try {
processor.onComplete(fileInfo);
} catch (Exception e) {
}
}
}
return storageService.upload(context);
}
}

View File

@@ -0,0 +1,247 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.domain.file;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
/**
* 增强版 MultipartFile 实现,支持缓存和包装
* 功能特性:
* 1. 缓存功能:避免重复读取文件内容
* 2. 包装模式:可以包装现有的 MultipartFile
* 3. 创建模式:可以直接从字节数组创建
*
* @author echo
* @since 2.14.0
*/
public class EnhancedMultipartFile implements MultipartFile {
private final MultipartFile originalFile;
private final String name;
private final String originalFilename;
private final String contentType;
private byte[] cachedBytes;
private final boolean cacheEnabled;
private final boolean isWrapped;
/**
* 包装模式构造器 - 包装现有的 MultipartFile
*/
public EnhancedMultipartFile(MultipartFile originalFile, boolean enableCache) {
this.originalFile = originalFile;
this.name = originalFile.getName();
this.originalFilename = originalFile.getOriginalFilename();
this.contentType = originalFile.getContentType();
this.cacheEnabled = enableCache;
this.isWrapped = true;
this.cachedBytes = null;
}
/**
* 创建模式构造器 - 直接从字节数组创建
*/
public EnhancedMultipartFile(String name, String originalFilename, String contentType, byte[] content) {
this.originalFile = null;
this.name = name;
this.originalFilename = originalFilename;
this.contentType = contentType;
this.cachedBytes = content;
this.cacheEnabled = false;
this.isWrapped = false;
}
/**
* 便捷的静态工厂方法 - 包装已有文件并启用缓存
*/
public static EnhancedMultipartFile wrap(MultipartFile file, boolean enableCache) {
if (file instanceof EnhancedMultipartFile) {
return (EnhancedMultipartFile)file;
}
return new EnhancedMultipartFile(file, enableCache);
}
/**
* 便捷的静态工厂方法 - 包装已有文件(不启用缓存)
*/
public static EnhancedMultipartFile wrap(MultipartFile file) {
return wrap(file, false);
}
/**
* 便捷的静态工厂方法 - 创建新文件
*/
public static EnhancedMultipartFile create(String name,
String originalFilename,
String contentType,
byte[] content) {
return new EnhancedMultipartFile(name, originalFilename, contentType, content);
}
@Override
public String getName() {
return name;
}
@Override
public String getOriginalFilename() {
return originalFilename;
}
@Override
public String getContentType() {
return contentType;
}
@Override
public boolean isEmpty() {
if (isWrapped) {
return originalFile.isEmpty();
}
return cachedBytes == null || cachedBytes.length == 0;
}
@Override
public long getSize() {
if (cachedBytes != null) {
return cachedBytes.length;
}
if (isWrapped) {
return originalFile.getSize();
}
return 0;
}
@Override
public byte[] getBytes() throws IOException {
// 缓存模式下的处理
if (cacheEnabled) {
return getBytesWithCache();
}
// 非缓存模式下的处理
return getBytesWithoutCache();
}
@Override
public InputStream getInputStream() throws IOException {
// 如果已有缓存数据,直接使用
if (cachedBytes != null) {
return new ByteArrayInputStream(cachedBytes);
}
// 缓存模式且需要缓存
if (cacheEnabled && isWrapped) {
loadToCache();
return new ByteArrayInputStream(cachedBytes);
}
// 非缓存模式且是包装模式
if (isWrapped) {
return originalFile.getInputStream();
}
// 创建模式理论上不应该到这里因为创建模式下cachedBytes应该有值
return new ByteArrayInputStream(new byte[0]);
}
@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
if (cachedBytes != null) {
// 使用缓存的数据
Files.write(dest.toPath(), cachedBytes);
} else if (isWrapped) {
// 使用原始文件
originalFile.transferTo(dest);
} else {
// 创建模式但没有数据
dest.createNewFile();
}
}
/**
* 清理缓存
*/
public void clearCache() {
if (cacheEnabled && isWrapped) {
cachedBytes = null;
}
}
/**
* 判断是否已缓存
*/
public boolean isCached() {
return cachedBytes != null;
}
/**
* 获取缓存的字节数组(不触发加载)
*/
public byte[] getCachedBytes() {
return cachedBytes;
}
/**
* 判断是否为包装模式
*/
public boolean isWrapped() {
return isWrapped;
}
/**
* 判断是否启用缓存
*/
public boolean isCacheEnabled() {
return cacheEnabled;
}
/**
* 获取字节数组 - 带缓存
*/
private byte[] getBytesWithCache() throws IOException {
if (cachedBytes == null && isWrapped) {
loadToCache();
}
return cachedBytes;
}
/**
* 获取字节数组 - 不带缓存
*/
private byte[] getBytesWithoutCache() throws IOException {
if (isWrapped) {
return originalFile.getBytes();
}
// 创建模式下直接返回内容
return cachedBytes;
}
/**
* 加载文件内容到缓存
*/
private void loadToCache() throws IOException {
if (isWrapped && originalFile != null) {
cachedBytes = originalFile.getBytes();
}
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.domain.file;
import org.springframework.web.multipart.MultipartFile;
import top.continew.starter.storage.processor.progress.ProgressInputStream;
import top.continew.starter.storage.processor.progress.ProgressTracker;
import top.continew.starter.storage.processor.progress.UploadProgressListener;
import java.io.*;
/**
* 进度监听 MultipartFile 包装器
*
* @author echo
* @since 2.14.0
*/
public class ProgressAwareMultipartFile extends EnhancedMultipartFile {
private final UploadProgressListener progressListener;
private volatile ProgressTracker progressTracker;
private volatile boolean progressEnabled = false;
private final Object progressLock = new Object();
// 用于区分不同的读取阶段
public enum ReadPhase {
VALIDATION, // 验证阶段
THUMBNAIL, // 缩略图生成
UPLOAD // 实际上传
}
private volatile ReadPhase currentPhase = ReadPhase.VALIDATION;
public ProgressAwareMultipartFile(MultipartFile originalFile,
boolean enableCache,
UploadProgressListener progressListener) {
super(originalFile, enableCache);
this.progressListener = progressListener;
}
/**
* 设置当前读取阶段
*/
public void setReadPhase(ReadPhase phase) {
synchronized (progressLock) {
this.currentPhase = phase;
// 只在上传阶段启用进度
this.progressEnabled = (phase == ReadPhase.UPLOAD);
// 切换到上传阶段时,重置进度追踪器
if (phase == ReadPhase.UPLOAD && progressListener != null) {
this.progressTracker = new ProgressTracker(getSize(), progressListener);
}
}
}
@Override
public InputStream getInputStream() throws IOException {
InputStream originalStream = super.getInputStream();
synchronized (progressLock) {
// 只有在上传阶段且有监听器时才包装流
if (progressEnabled && progressTracker != null) {
return new ProgressInputStream(originalStream, progressTracker);
}
}
return originalStream;
}
@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
if (progressEnabled && progressTracker != null) {
// 使用带进度的传输
try (InputStream in = getInputStream(); OutputStream out = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
} else {
super.transferTo(dest);
}
}
/**
* 获取不带进度监听的输入流(向后兼容)
*/
public InputStream getInputStreamWithoutProgress() throws IOException {
return super.getInputStream();
}
/**
* 清理资源
*/
@Override
public void clearCache() {
super.clearCache();
synchronized (progressLock) {
progressTracker = null;
}
}
}

View File

@@ -14,10 +14,11 @@
* limitations under the License.
*/
package top.continew.starter.storage.model.context;
package top.continew.starter.storage.domain.model.context;
import org.springframework.web.multipart.MultipartFile;
import top.continew.starter.storage.model.req.ThumbnailSize;
import top.continew.starter.storage.domain.model.req.ThumbnailSize;
import top.continew.starter.storage.processor.progress.UploadProgressListener;
import java.util.HashMap;
import java.util.Map;
@@ -51,9 +52,9 @@ public class UploadContext {
private String path;
/**
* 文件名
* 格式化文件名 为空则默认使用格式化规则格式化
*/
private String fileName;
private String formatFileName;
/**
* 是否生成缩略图
@@ -75,11 +76,16 @@ public class UploadContext {
*/
private Map<String, Object> attributes = new HashMap<>();
/**
* 进度监听器
*/
private UploadProgressListener progressListener;
/**
* 获取完整路径
*/
public String getFullPath() {
return path + fileName;
return path + formatFileName;
}
public MultipartFile getFile() {
@@ -114,12 +120,12 @@ public class UploadContext {
this.path = path;
}
public String getFileName() {
return fileName;
public String getFormatFileName() {
return formatFileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
public void setFormatFileName(String formatFileName) {
this.formatFileName = formatFileName;
}
public boolean isGenerateThumbnail() {
@@ -153,4 +159,12 @@ public class UploadContext {
public void setAttributes(Map<String, Object> attributes) {
this.attributes = attributes;
}
public UploadProgressListener getProgressListener() {
return progressListener;
}
public void setProgressListener(UploadProgressListener progressListener) {
this.progressListener = progressListener;
}
}

View File

@@ -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省略
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.storage.model.req;
package top.continew.starter.storage.domain.model.req;
/**
* 缩略图尺寸

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.storage.model.resp;
package top.continew.starter.storage.domain.model.resp;
/**
* 分片上传初始化结果

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.storage.model.resp;
package top.continew.starter.storage.domain.model.resp;
/**
* 分片上传结果

View File

@@ -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;

View File

@@ -0,0 +1,196 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.engine;
import jakarta.annotation.PostConstruct;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import top.continew.starter.storage.strategy.StorageStrategy;
import top.continew.starter.storage.strategy.impl.StorageStrategyDecorator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 存储装饰器管理器
*
* @author echo
* @since 2.14.0
*/
public class StorageDecoratorManager {
private final ApplicationContext applicationContext;
private final Map<Class<? extends StorageStrategy>, List<StorageStrategyDecorator<?>>> decoratorMap = new ConcurrentHashMap<>();
private volatile boolean initialized = false;
public StorageDecoratorManager(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@PostConstruct
public void init() {
if (initialized) {
return;
}
Map<String, StorageStrategyDecorator> decorators = applicationContext
.getBeansOfType(StorageStrategyDecorator.class);
for (Map.Entry<String, StorageStrategyDecorator> entry : decorators.entrySet()) {
StorageStrategyDecorator<?> decorator = entry.getValue();
Class<?> targetClass = decorator.getTargetStrategyClass();
if (targetClass != null) {
decoratorMap.computeIfAbsent((Class<? extends StorageStrategy>)targetClass, k -> new ArrayList<>())
.add(decorator);
}
}
// 按优先级排序
decoratorMap.values().forEach(list -> list.sort(Comparator.comparingInt(StorageStrategyDecorator::getOrder)));
this.initialized = true;
}
/**
* 应用装饰器到策略实例
*
* @param strategy 存储策略实例
* @return {@link StorageStrategy }
*/
@SuppressWarnings("unchecked")
public StorageStrategy applyDecorators(StorageStrategy strategy) {
if (!initialized) {
init();
}
if (strategy == null) {
return null;
}
Class<? extends StorageStrategy> strategyClass = strategy.getClass();
List<StorageStrategyDecorator<?>> decorators = findApplicableDecorators(strategyClass);
if (decorators.isEmpty()) {
return strategy;
}
// 应用装饰器链
StorageStrategy decorated = strategy;
for (StorageStrategyDecorator decorator : decorators) {
decorator.setDelegate(decorated);
decorated = decorator;
}
return decorated;
}
/**
* 查找适用的装饰器
*
* @param strategyClass 策略类
* @return {@link List }<{@link StorageStrategyDecorator }<{@link ? }>>
*/
private List<StorageStrategyDecorator<?>> findApplicableDecorators(Class<? extends StorageStrategy> strategyClass) {
List<StorageStrategyDecorator<?>> result = new ArrayList<>();
// 精确匹配
List<StorageStrategyDecorator<?>> exactMatch = decoratorMap.get(strategyClass);
if (exactMatch != null) {
result.addAll(exactMatch);
}
// 继承匹配
for (Map.Entry<Class<? extends StorageStrategy>, List<StorageStrategyDecorator<?>>> entry : decoratorMap
.entrySet()) {
if (entry.getKey() != strategyClass && entry.getKey().isAssignableFrom(strategyClass)) {
result.addAll(entry.getValue());
}
}
// 去重并排序
return result.stream()
.distinct()
.sorted(Comparator.comparingInt(StorageStrategyDecorator::getOrder))
.collect(Collectors.toList());
}
/**
* 动态注册装饰器
*
* @param decorator 装饰器
*/
public void registerDecorator(StorageStrategyDecorator<?> decorator) {
Class<?> targetClass = decorator.getTargetStrategyClass();
if (targetClass != null) {
decoratorMap.computeIfAbsent((Class<? extends StorageStrategy>)targetClass, k -> new ArrayList<>())
.add(decorator);
// 重新排序
decoratorMap.get((Class<? extends StorageStrategy>)targetClass)
.sort(Comparator.comparingInt(StorageStrategyDecorator::getOrder));
}
}
/**
* 移除装饰器
*
* @param decorator 装饰器
*/
public void unregisterDecorator(StorageStrategyDecorator<?> decorator) {
Class<?> targetClass = decorator.getTargetStrategyClass();
if (targetClass != null) {
List<StorageStrategyDecorator<?>> decorators = decoratorMap
.get((Class<? extends StorageStrategy>)targetClass);
if (decorators != null) {
decorators.remove(decorator);
}
}
}
/**
* 装饰器注册事件
*/
public static class DecoratorRegisteredEvent extends ApplicationEvent {
private final Class<?> targetClass;
public DecoratorRegisteredEvent(Object source, Class<?> targetClass) {
super(source);
this.targetClass = targetClass;
}
public Class<?> getTargetClass() {
return targetClass;
}
}
/**
* 装饰器注销事件
*/
public static class DecoratorUnregisteredEvent extends ApplicationEvent {
private final Class<?> targetClass;
public DecoratorUnregisteredEvent(Object source, Class<?> targetClass) {
super(source);
this.targetClass = targetClass;
}
public Class<?> getTargetClass() {
return targetClass;
}
}
}

View File

@@ -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;

View File

@@ -0,0 +1,240 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.engine;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import top.continew.starter.storage.autoconfigure.properties.StorageProperties;
import top.continew.starter.storage.common.constant.StorageConstant;
import top.continew.starter.storage.common.enums.DefaultStorageSource;
import top.continew.starter.storage.common.exception.StorageException;
import top.continew.starter.storage.domain.model.resp.StrategyStatusResp;
import top.continew.starter.storage.strategy.StorageStrategy;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 存储策略路由器
*
* @author echo
* @since 2.14.0
*/
public class StorageStrategyRouter implements ApplicationListener<ApplicationEvent> {
private static final Logger log = LoggerFactory.getLogger(StorageStrategyRouter.class);
private final Map<String, StorageStrategy> configStrategies = new ConcurrentHashMap<>();
private final Map<String, StorageStrategy> dynamicStrategies = new ConcurrentHashMap<>();
private final Map<String, StorageStrategy> decoratedStrategies = new ConcurrentHashMap<>();
private final StorageProperties storageProperties;
private final String configDefaultPlatform;
private volatile String dynamicDefaultPlatform;
private final StorageDecoratorManager decoratorManager;
public StorageStrategyRouter(List<StorageStrategyRegistrar> registrars,
StorageProperties storageProperties,
StorageDecoratorManager decoratorManager) {
this.decoratorManager = decoratorManager;
this.storageProperties = storageProperties;
List<StorageStrategy> strategies = new ArrayList<>();
for (StorageStrategyRegistrar registrar : registrars) {
registrar.register(strategies);
}
// 配置文件加载的策略
for (StorageStrategy strategy : strategies) {
configStrategies.put(strategy.getPlatform(), strategy);
}
// 加载配置文件默认存储
this.configDefaultPlatform = storageProperties.getDefaultPlatform();
}
/**
* 存储选择(支持装饰器)
*/
public StorageStrategy route(String platform) {
// 1. 先检查缓存的装饰后策略
StorageStrategy decorated = decoratedStrategies.get(platform);
if (decorated != null) {
return decorated;
}
// 2. 获取原始策略
StorageStrategy strategy = getOriginalStrategy(platform);
// 3. 应用装饰器
StorageStrategy decoratedStrategy = applyDecoratorsIfAvailable(strategy);
// 4. 缓存装饰后的策略
decoratedStrategies.put(platform, decoratedStrategy);
return decoratedStrategy;
}
/**
* 获取原始策略
*/
private StorageStrategy getOriginalStrategy(String platform) {
return Optional.ofNullable(dynamicStrategies.get(platform))
.or(() -> Optional.ofNullable(configStrategies.get(platform)))
.orElseThrow(() -> new StorageException(String.format("不支持的存储平台: %s", platform)));
}
/**
* 应用装饰器
*/
private StorageStrategy applyDecoratorsIfAvailable(StorageStrategy strategy) {
return ObjectUtil.isNotEmpty(decoratorManager) ? decoratorManager.applyDecorators(strategy) : strategy;
}
/**
* 动态注册策略 - 支持装饰器注册
*/
public void registerDynamic(StorageStrategy strategy) {
String platform = strategy.getPlatform();
if (dynamicStrategies.containsKey(platform)) {
throw new StorageException("动态策略 platform 已存在: " + platform);
}
dynamicStrategies.put(platform, strategy);
// 清除装饰器缓存,确保下次获取时重新应用装饰器
decoratedStrategies.remove(platform);
}
/**
* 卸载动态策略
*/
public boolean unloadDynamic(String platform) {
StorageStrategy strategy = dynamicStrategies.remove(platform);
if (strategy == null) {
return false;
}
decoratedStrategies.remove(platform);
try {
strategy.cleanup();
} catch (Exception e) {
log.error("清理存储策略失败: platform={}", platform, e);
}
return true;
}
/**
* 监听装饰器变更事件,自动刷新缓存
*/
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof StorageDecoratorManager.DecoratorRegisteredEvent || event instanceof StorageDecoratorManager.DecoratorUnregisteredEvent) {
decoratedStrategies.clear();
}
}
/**
* 注册动态默认存储
*/
public void registerDynamicDefaultStorage(String platform) {
this.dynamicDefaultPlatform = platform;
}
/**
* 获取默认存储平台
*/
public String getDefaultStorage() {
DefaultStorageSource defaultStorageSource = storageProperties.getDefaultStorageSource();
return switch (defaultStorageSource) {
case DYNAMIC -> {
if (StrUtil.isBlank(dynamicDefaultPlatform)) {
throw new StorageException("动态默认存储平台配置为空");
}
yield dynamicDefaultPlatform;
}
case CONFIG -> {
if (StrUtil.isBlank(configDefaultPlatform)) {
throw new StorageException("配置默认存储平台配置为空");
}
yield configDefaultPlatform;
}
};
}
/**
* 获取所有可用平台
*/
public Set<String> getAllPlatform() {
Set<String> allPlatform = new HashSet<>(configStrategies.keySet());
allPlatform.addAll(dynamicStrategies.keySet());
return allPlatform;
}
/**
* 检查是否为动态注册的策略
*/
public boolean isDynamic(String platform) {
return dynamicStrategies.containsKey(platform);
}
/**
* 检查是否为配置文件策略
*/
public boolean isFromConfig(String platform) {
return configStrategies.containsKey(platform);
}
/**
* 获取简化的策略信息
*/
public Map<String, String> getActiveStrategyInfo() {
Map<String, String> info = new HashMap<>();
// 先添加配置文件策略
configStrategies.keySet().forEach(platform -> info.put(platform, StorageConstant.CONFIG));
// 动态策略会覆盖同名的配置策略
dynamicStrategies.keySet().forEach(platform -> info.put(platform, StorageConstant.DYNAMIC));
return info;
}
/**
* 获取完整的策略状态
*/
public Map<String, StrategyStatusResp> getFullStrategyStatus() {
Map<String, StrategyStatusResp> status = new HashMap<>();
// 所有唯一的 platform
Set<String> appPlatform = new HashSet<>();
appPlatform.addAll(configStrategies.keySet());
appPlatform.addAll(dynamicStrategies.keySet());
for (String platform : appPlatform) {
boolean hasConfig = configStrategies.containsKey(platform);
boolean hasDynamic = dynamicStrategies.containsKey(platform);
boolean hasDecorated = decoratedStrategies.containsKey(platform);
String activeType = hasDynamic ? StorageConstant.DYNAMIC : StorageConstant.CONFIG;
String statusDesc = hasDynamic && hasConfig ? "配置策略被覆盖" : "正常";
if (hasDecorated) {
statusDesc += " (已装饰)";
}
StrategyStatusResp strategyStatusResp = new StrategyStatusResp(platform, hasConfig, hasDynamic, activeType, statusDesc);
status.put(platform, strategyStatusResp);
}
return status;
}
}

View File

@@ -1,89 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.model.req;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
/**
* 内存中的 MultipartFile 实现,适用于无需真实 HTTP 上传场景。
* <p>
* 可用于接口调用中构造文件参数,如将字节数组、输入流包装成 MultipartFile
* 以便复用上传逻辑或兼容已有的文件处理接口。
*
* @author echo
* @since 2.14.0
*/
public class MockMultipartFile implements MultipartFile {
private final String name;
private final String originalFilename;
private final String contentType;
private final byte[] content;
public MockMultipartFile(String name, String originalFilename, String contentType, byte[] content) {
this.name = name;
this.originalFilename = originalFilename;
this.contentType = contentType;
this.content = content;
}
@Override
public String getName() {
return name;
}
@Override
public String getOriginalFilename() {
return originalFilename;
}
@Override
public String getContentType() {
return contentType;
}
@Override
public boolean isEmpty() {
return content == null || content.length == 0;
}
@Override
public long getSize() {
return content != null ? content.length : 0;
}
@Override
public byte[] getBytes() {
return content;
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(content);
}
@Override
public void transferTo(File dest) throws IOException {
Files.write(dest.toPath(), content);
}
}

View File

@@ -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;
/**

View File

@@ -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;
/**

View File

@@ -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;
/**

View File

@@ -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;

View File

@@ -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;
/**

View File

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

View File

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

View File

@@ -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());

View File

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

View File

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

View File

@@ -0,0 +1,62 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.processor.progress;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 进度监听输入流包装器
*
* @author echo
* @since 2.14.0
*/
public class ProgressInputStream extends FilterInputStream {
private final ProgressTracker tracker;
public ProgressInputStream(InputStream in, ProgressTracker tracker) {
super(in);
this.tracker = tracker;
tracker.start();
}
@Override
public int read() throws IOException {
int b = super.read();
if (b != -1) {
tracker.updateProgress(1);
}
return b;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int bytesRead = super.read(b, off, len);
if (bytesRead > 0) {
tracker.updateProgress(bytesRead);
}
return bytesRead;
}
@Override
public void close() throws IOException {
super.close();
tracker.complete();
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.processor.progress;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
/**
* 进度跟踪器
*
* @author echo
* @since 2.14.0
*/
public class ProgressTracker {
private final long totalBytes;
private final UploadProgressListener listener;
private final AtomicLong bytesRead = new AtomicLong(0);
private final AtomicLong lastNotifiedBytes = new AtomicLong(0);
private final AtomicInteger lastPercentage = new AtomicInteger(-1);
private final AtomicBoolean started = new AtomicBoolean(false);
private final AtomicBoolean completed = new AtomicBoolean(false);
// 通知阈值至少变化1%或者达到 1MB 阈值
private static final int PERCENTAGE_THRESHOLD = 1;
private static final long BYTES_THRESHOLD = 1024 * 1024;
public ProgressTracker(long totalBytes, UploadProgressListener listener) {
this.totalBytes = totalBytes;
this.listener = listener;
}
public void start() {
if (started.compareAndSet(false, true) && listener != null) {
listener.onStart();
}
}
public void updateProgress(long bytes) {
if (completed.get() || listener == null) {
return;
}
long currentBytes = bytesRead.addAndGet(bytes);
int currentPercentage = totalBytes > 0 ? (int)((currentBytes * 100L) / totalBytes) : -1;
// 检查是否需要通知
boolean shouldNotify = false;
int lastPct = lastPercentage.get();
if (currentPercentage >= 0) {
// 百分比变化达到阈值
if (currentPercentage - lastPct >= PERCENTAGE_THRESHOLD) {
shouldNotify = true;
}
// 达到100%必须通知
if (currentPercentage == 100 && lastPct != 100) {
shouldNotify = true;
}
}
// 字节数变化达到阈值
if (currentBytes - lastNotifiedBytes.get() >= BYTES_THRESHOLD) {
shouldNotify = true;
}
if (shouldNotify) {
// 使用CAS更新避免并发问题
if (lastPercentage.compareAndSet(lastPct, currentPercentage)) {
lastNotifiedBytes.set(currentBytes);
listener.onProgress(currentBytes, totalBytes, currentPercentage);
// 如果达到100%,标记完成
if (currentPercentage == 100) {
complete();
}
}
}
}
public void complete() {
if (completed.compareAndSet(false, true) && listener != null) {
listener.onComplete();
}
}
public void error(Exception e) {
if (listener != null) {
listener.onError(e);
}
}
}

View File

@@ -14,30 +14,40 @@
* limitations under the License.
*/
package top.continew.starter.storage.strategy.impl;
import top.continew.starter.storage.strategy.StorageStrategy;
import top.continew.starter.storage.strategy.StorageStrategyOverride;
package top.continew.starter.storage.processor.progress;
/**
* 抽象基类提供原始目标对象的访问
* 上传进度监听器
*
* @author echo
* @since 2.14.0
*/
public abstract class AbstractStorageStrategyOverride<T extends StorageStrategy> implements StorageStrategyOverride<T> {
protected T originalTarget;
**/
public interface UploadProgressListener {
/**
* 设置原始目标对象由代理工厂调用
* 进度更新回调
*
* @param bytesRead 已读取字节数
* @param totalBytes 总字节数-1表示未知
* @param percentage 百分比0-100
*/
public void setOriginalTarget(T originalTarget) {
this.originalTarget = originalTarget;
void onProgress(long bytesRead, long totalBytes, int percentage);
/**
* 上传开始
*/
default void onStart() {
}
@Override
public T getOriginalTarget() {
return originalTarget;
/**
* 上传完成
*/
default void onComplete() {
}
}
/**
* 上传失败
*/
default void onError(Exception e) {
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.processor.registry;
import top.continew.starter.storage.domain.model.context.UploadContext;
import top.continew.starter.storage.processor.preprocess.*;
import top.continew.starter.storage.service.FileProcessor;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 全局处理器注册表
*
* @author echo
* @since 2.14.0
*/
public class ProcessorRegistry {
private final Map<Class<?>, List<FileProcessor>> processors = new ConcurrentHashMap<>();
private final Map<String, Map<Class<?>, List<FileProcessor>>> platformProcessors = new ConcurrentHashMap<>();
/**
* 注册处理器(自动识别类型)
*/
public void register(FileProcessor processor) {
register(processor, null);
}
/**
* 注册平台特定处理器
*/
public void register(FileProcessor processor, String platform) {
Class<?> type = getProcessorType(processor);
if (platform == null) {
// 全局处理器
processors.computeIfAbsent(type, k -> new CopyOnWriteArrayList<>()).add(processor);
} else {
// 平台特定处理器
platformProcessors.computeIfAbsent(platform, k -> new ConcurrentHashMap<>())
.computeIfAbsent(type, k -> new CopyOnWriteArrayList<>())
.add(processor);
}
}
/**
* 获取处理器类型
*/
private Class<?> getProcessorType(FileProcessor processor) {
if (processor instanceof ThumbnailProcessor)
return ThumbnailProcessor.class;
if (processor instanceof FileValidator)
return FileValidator.class;
if (processor instanceof FileNameGenerator)
return FileNameGenerator.class;
if (processor instanceof FilePathGenerator)
return FilePathGenerator.class;
if (processor instanceof UploadCompleteProcessor)
return UploadCompleteProcessor.class;
return FileProcessor.class;
}
/**
* 获取指定类型的处理器(支持优先级排序)
*/
@SuppressWarnings("unchecked")
public <T extends FileProcessor> List<T> getProcessors(Class<T> type, String platform, UploadContext context) {
List<T> result = new ArrayList<>();
// 添加全局处理器
List<FileProcessor> globalList = processors.get(type);
if (globalList != null) {
globalList.stream().filter(p -> p.support(context)).map(p -> (T)p).forEach(result::add);
}
// 添加平台特定处理器
if (platform != null) {
Map<Class<?>, List<FileProcessor>> platformMap = platformProcessors.get(platform);
if (platformMap != null) {
List<FileProcessor> platformList = platformMap.get(type);
if (platformList != null) {
platformList.stream().filter(p -> p.support(context)).map(p -> (T)p).forEach(result::add);
}
}
}
// 按优先级排序(优先级高的在前)
result.sort(Comparator.comparingInt(FileProcessor::getOrder).reversed());
return result;
}
/**
* 获取最高优先级的处理器
*/
public <T extends FileProcessor> T getProcessor(Class<T> type, String platform, UploadContext context) {
List<T> processors = getProcessors(type, platform, context);
return processors.isEmpty() ? null : processors.get(0);
}
}

View File

@@ -1,171 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.router;
import top.continew.starter.storage.exception.StorageException;
import top.continew.starter.storage.model.resp.StrategyStatus;
import top.continew.starter.storage.strategy.StorageStrategy;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 存储策略路由器
*
* @author echo
* @since 2.14.0
*/
public class StorageStrategyRouter {
/**
* 配置策略
*/
private final Map<String, StorageStrategy> configStrategies = new ConcurrentHashMap<>();
/**
* 动态策略
*/
private final Map<String, StorageStrategy> dynamicStrategies = new ConcurrentHashMap<>();
public StorageStrategyRouter(List<StorageStrategyRegistrar> registrars) {
List<StorageStrategy> strategies = new ArrayList<>();
for (StorageStrategyRegistrar registrar : registrars) {
registrar.register(strategies);
}
// 配置文件加载的策略
for (StorageStrategy strategy : strategies) {
configStrategies.put(strategy.getPlatform(), strategy);
}
}
/**
* 存储选择
*
* @param platform 代码
* @return {@link StorageStrategy }
*/
public StorageStrategy route(String platform) {
// 动态注册的策略优先级更高
StorageStrategy strategy = dynamicStrategies.get(platform);
if (strategy == null) {
strategy = configStrategies.get(platform);
}
if (strategy == null) {
throw new StorageException("不支持存储编码: " + platform);
}
return strategy;
}
/**
* 动态注册策略
*
* @param strategy 存储策略
* @throws StorageException 如果同一 platform 的动态策略已存在
*/
public void registerDynamic(StorageStrategy strategy) {
String platform = strategy.getPlatform();
if (dynamicStrategies.containsKey(platform)) {
throw new StorageException("动态策略 platform 已存在: " + platform);
}
// 如果配置文件中存在相同 platform动态注册会覆盖但不修改配置策略
dynamicStrategies.put(platform, strategy);
}
/**
* 卸载动态策略
*
* @param platform 策略代码
* @return 是否成功卸载
*/
public boolean unloadDynamic(String platform) {
StorageStrategy strategy = dynamicStrategies.remove(platform);
if (strategy != null) {
try {
strategy.cleanup();
return true;
} catch (Exception e) {
return false;
}
}
return false;
}
/**
* 获取所有可用代码
*/
public Set<String> getAllPlatform() {
Set<String> allPlatform = new HashSet<>(configStrategies.keySet());
allPlatform.addAll(dynamicStrategies.keySet());
return allPlatform;
}
/**
* 检查是否为动态注册的策略
*/
public boolean isDynamic(String platform) {
return dynamicStrategies.containsKey(platform);
}
/**
* 检查是否为配置文件策略
*/
public boolean isFromConfig(String platform) {
return configStrategies.containsKey(platform);
}
/**
* 获取简化的策略信息(当前生效的)
*/
public Map<String, String> getActiveStrategyInfo() {
Map<String, String> info = new HashMap<>();
// 先添加配置文件策略
configStrategies.keySet().forEach(platform -> info.put(platform, "CONFIG"));
// 动态策略会覆盖同名的配置策略
dynamicStrategies.keySet().forEach(platform -> info.put(platform, "DYNAMIC"));
return info;
}
/**
* 获取完整的策略状态
*/
public Map<String, StrategyStatus> getFullStrategyStatus() {
Map<String, StrategyStatus> status = new HashMap<>();
// 所有唯一的 platform
Set<String> appPlatform = new HashSet<>();
appPlatform.addAll(configStrategies.keySet());
appPlatform.addAll(dynamicStrategies.keySet());
for (String platform : appPlatform) {
boolean hasConfig = configStrategies.containsKey(platform);
boolean hasDynamic = dynamicStrategies.containsKey(platform);
StrategyStatus strategyStatus = new StrategyStatus(platform, hasConfig, hasDynamic, hasDynamic
? "DYNAMIC"
: "CONFIG", hasDynamic && hasConfig ? "配置策略被覆盖" : "正常");
status.put(platform, strategyStatus);
}
return status;
}
}

View File

@@ -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;
/**
* 文件处理器接口

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -30,13 +30,13 @@ import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.storage.autoconfigure.properties.S3StorageConfig;
import top.continew.starter.storage.exception.StorageException;
import top.continew.starter.storage.model.resp.FileInfo;
import top.continew.starter.storage.model.resp.MultipartInitResp;
import top.continew.starter.storage.model.resp.MultipartUploadResp;
import top.continew.starter.storage.autoconfigure.properties.OssStorageConfig;
import top.continew.starter.storage.common.exception.StorageException;
import top.continew.starter.storage.domain.model.resp.FileInfo;
import top.continew.starter.storage.domain.model.resp.MultipartInitResp;
import top.continew.starter.storage.domain.model.resp.MultipartUploadResp;
import top.continew.starter.storage.strategy.StorageStrategy;
import top.continew.starter.storage.util.StorageUtils;
import top.continew.starter.storage.common.util.StorageUtils;
import java.io.InputStream;
import java.net.URI;
@@ -57,9 +57,9 @@ public class OssStorageStrategy implements StorageStrategy {
private final S3Client s3Client;
private final S3Presigner s3Presigner;
private final S3StorageConfig config;
private final OssStorageConfig config;
public OssStorageStrategy(S3StorageConfig config) {
public OssStorageStrategy(OssStorageConfig config) {
this.config = config;
this.s3Client = createS3Client(config);
this.s3Presigner = createS3Presigner(config);
@@ -89,7 +89,7 @@ public class OssStorageStrategy implements StorageStrategy {
* @param config 配置
* @return {@link S3Client }
*/
private S3Client createS3Client(S3StorageConfig config) {
private S3Client createS3Client(OssStorageConfig config) {
// 登录认证账户密码
StaticCredentialsProvider auth = StaticCredentialsProvider.create(AwsBasicCredentials.create(config
.getAccessKey(), config.getSecretKey()));
@@ -107,7 +107,7 @@ public class OssStorageStrategy implements StorageStrategy {
* @param config 配置
* @return {@link S3Presigner }
*/
private S3Presigner createS3Presigner(S3StorageConfig config) {
private S3Presigner createS3Presigner(OssStorageConfig config) {
StaticCredentialsProvider auth = StaticCredentialsProvider.create(AwsBasicCredentials.create(config
.getAccessKey(), config.getSecretKey()));

View File

@@ -0,0 +1,174 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.storage.strategy.impl;
import org.springframework.web.multipart.MultipartFile;
import top.continew.starter.storage.domain.model.resp.FileInfo;
import top.continew.starter.storage.domain.model.resp.MultipartInitResp;
import top.continew.starter.storage.domain.model.resp.MultipartUploadResp;
import top.continew.starter.storage.strategy.StorageStrategy;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
/**
* 存储策略装饰器基类
* 支持对特定存储策略进行选择性重写
*
* @author echo
* @since 2.14.0
*/
public abstract class StorageStrategyDecorator<T extends StorageStrategy> implements StorageStrategy {
protected T delegate;
/**
* 获取被装饰的策略类型
*/
public abstract Class<T> getTargetStrategyClass();
/**
* 设置被装饰的策略实例
*/
public void setDelegate(T delegate) {
this.delegate = delegate;
}
/**
* 获取被装饰的策略实例
*/
protected T getDelegate() {
if (delegate == null) {
throw new IllegalStateException("装饰器未初始化请先设置delegate");
}
return delegate;
}
/**
* 获取装饰器优先级(数值越小优先级越高)
*/
public int getOrder() {
return 0;
}
@Override
public void upload(String bucket, String path, MultipartFile file) {
getDelegate().upload(bucket, path, file);
}
@Override
public InputStream download(String bucket, String path) {
return getDelegate().download(bucket, path);
}
@Override
public InputStream batchDownload(String bucket, List<String> paths) {
return getDelegate().batchDownload(bucket, paths);
}
@Override
public void delete(String bucket, String path) {
getDelegate().delete(bucket, path);
}
@Override
public void batchDelete(String bucket, List<String> paths) {
getDelegate().batchDelete(bucket, paths);
}
@Override
public boolean exists(String bucket, String path) {
return getDelegate().exists(bucket, path);
}
@Override
public FileInfo getFileInfo(String bucket, String path) {
return getDelegate().getFileInfo(bucket, path);
}
@Override
public List<FileInfo> list(String bucket, String prefix, int maxKeys) {
return getDelegate().list(bucket, prefix, maxKeys);
}
@Override
public void copy(String sourceBucket, String targetBucket, String sourcePath, String targetPath) {
getDelegate().copy(sourceBucket, targetBucket, sourcePath, targetPath);
}
@Override
public void move(String sourceBucket, String targetBucket, String sourcePath, String targetPath) {
getDelegate().move(sourceBucket, targetBucket, sourcePath, targetPath);
}
@Override
public String getPlatform() {
return getDelegate().getPlatform();
}
@Override
public String defaultBucket() {
return getDelegate().defaultBucket();
}
@Override
public String generatePresignedUrl(String bucket, String path, long expireSeconds) {
return getDelegate().generatePresignedUrl(bucket, path, expireSeconds);
}
@Override
public MultipartInitResp initMultipartUpload(String bucket,
String path,
String contentType,
Map<String, String> metadata) {
return getDelegate().initMultipartUpload(bucket, path, contentType, metadata);
}
@Override
public MultipartUploadResp uploadPart(String bucket,
String path,
String uploadId,
int partNumber,
InputStream data) {
return getDelegate().uploadPart(bucket, path, uploadId, partNumber, data);
}
@Override
public FileInfo completeMultipartUpload(String bucket,
String path,
String uploadId,
List<MultipartUploadResp> parts,
boolean verifyParts) {
return getDelegate().completeMultipartUpload(bucket, path, uploadId, parts, verifyParts);
}
@Override
public void abortMultipartUpload(String bucket, String path, String uploadId) {
getDelegate().abortMultipartUpload(bucket, path, uploadId);
}
@Override
public List<MultipartUploadResp> listParts(String bucket, String path, String uploadId) {
return getDelegate().listParts(bucket, path, uploadId);
}
@Override
public void cleanup() {
getDelegate().cleanup();
}
}