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