diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/LocalStorageAutoConfiguration.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/LocalStorageAutoConfiguration.java index 91447b91a4cdcb722fad7dbc4ad1ea829e733b9f..aadeaa5c76fc7ba79c7dab9aeb4bac32543f9eec 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/LocalStorageAutoConfiguration.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/LocalStorageAutoConfiguration.java @@ -17,15 +17,14 @@ package top.continew.starter.storage.autoconfigure; import cn.hutool.core.util.StrUtil; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import top.continew.starter.core.constant.PropertiesConstants; +import org.springframework.context.annotation.Configuration; import top.continew.starter.storage.autoconfigure.properties.LocalStorageConfig; import top.continew.starter.storage.autoconfigure.properties.StorageProperties; import top.continew.starter.storage.engine.StorageStrategyRegistrar; import top.continew.starter.storage.strategy.StorageStrategy; import top.continew.starter.storage.strategy.impl.LocalStorageStrategy; +import java.util.Collections; import java.util.List; /** @@ -34,7 +33,7 @@ import java.util.List; * @author echo * @since 2.14.0 */ -@ConditionalOnProperty(prefix = PropertiesConstants.STORAGE, name = "local") +@Configuration(proxyBeanMethods = false) public class LocalStorageAutoConfiguration implements StorageStrategyRegistrar { private final StorageProperties storageProperties; @@ -48,10 +47,12 @@ public class LocalStorageAutoConfiguration implements StorageStrategyRegistrar { * * @param strategies 策略列表 */ - @Bean @Override public void register(List strategies) { - for (LocalStorageConfig config : storageProperties.getLocal()) { + List localConfigs = storageProperties.getLocal() == null + ? Collections.emptyList() + : storageProperties.getLocal(); + for (LocalStorageConfig config : localConfigs) { if (config.isEnabled()) { if (config.getMultipartUploadThreshold() == null || config.getMultipartUploadThreshold() <= 0) { config.setMultipartUploadThreshold(storageProperties.getMultipartUploadThreshold()); diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/OssStorageAutoConfiguration.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/OssStorageAutoConfiguration.java index 64fc44293e06da892c20f6ba7997a40a4f07135a..e7ef756261b771adba158cb69895632444633d2b 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/OssStorageAutoConfiguration.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/OssStorageAutoConfiguration.java @@ -16,15 +16,14 @@ 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 org.springframework.context.annotation.Configuration; import top.continew.starter.storage.autoconfigure.properties.OssStorageConfig; import top.continew.starter.storage.autoconfigure.properties.StorageProperties; import top.continew.starter.storage.engine.StorageStrategyRegistrar; import top.continew.starter.storage.strategy.StorageStrategy; import top.continew.starter.storage.strategy.impl.OssStorageStrategy; +import java.util.Collections; import java.util.List; /** @@ -33,7 +32,7 @@ import java.util.List; * @author echo * @since 2.14.0 */ -@ConditionalOnProperty(prefix = PropertiesConstants.STORAGE, name = "oss") +@Configuration(proxyBeanMethods = false) public class OssStorageAutoConfiguration implements StorageStrategyRegistrar { private final StorageProperties properties; @@ -48,9 +47,9 @@ public class OssStorageAutoConfiguration implements StorageStrategyRegistrar { * @param strategies 策略列表 */ @Override - @Bean public void register(List strategies) { - for (OssStorageConfig config : properties.getOss()) { + List ossConfigs = properties.getOss() == null ? Collections.emptyList() : properties.getOss(); + for (OssStorageConfig config : ossConfigs) { if (config.isEnabled()) { if (config.getMultipartUploadThreshold() == null || config.getMultipartUploadThreshold() <= 0) { config.setMultipartUploadThreshold(properties.getMultipartUploadThreshold()); diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/StorageAutoConfiguration.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/StorageAutoConfiguration.java index 60ca7aa8d34134742294b709b79e78167dd66541..09b91a9249495398835f3d7f2eedac790a380490 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/StorageAutoConfiguration.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/StorageAutoConfiguration.java @@ -19,6 +19,7 @@ package top.continew.starter.storage.autoconfigure; import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.aop.support.AopUtils; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -27,6 +28,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AnnotationUtils; import top.continew.starter.storage.annotation.PlatformProcessor; import top.continew.starter.storage.autoconfigure.properties.StorageProperties; import top.continew.starter.storage.core.FileStorageService; @@ -51,7 +53,7 @@ import java.util.Map; */ @AutoConfiguration @EnableConfigurationProperties(StorageProperties.class) -@Import({ProcessorRegistry.class}) +@Import({OssStorageAutoConfiguration.class, LocalStorageAutoConfiguration.class}) public class StorageAutoConfiguration { private static final Logger log = LoggerFactory.getLogger(StorageAutoConfiguration.class); @@ -71,8 +73,9 @@ public class StorageAutoConfiguration { * @return {@link StorageStrategyRouter } */ @Bean - public StorageStrategyRouter strategyRouter(List registrars) { - return new StorageStrategyRouter(registrars, properties, storageDecoratorManager()); + public StorageStrategyRouter strategyRouter(List registrars, + StorageDecoratorManager storageDecoratorManager) { + return new StorageStrategyRouter(registrars, properties, storageDecoratorManager); } /** @@ -85,41 +88,19 @@ public class StorageAutoConfiguration { return new StorageDecoratorManager(applicationContext); } - /** - * oss存储自动配置 - * - * @return {@link OssStorageAutoConfiguration } - */ - @Bean - public OssStorageAutoConfiguration ossStorageAutoConfiguration() { - return new OssStorageAutoConfiguration(properties); - } - - /** - * 本地存储自动配置 - * - * @return {@link LocalStorageAutoConfiguration } - */ - @Bean - public LocalStorageAutoConfiguration localStorageAutoConfiguration() { - return new LocalStorageAutoConfiguration(properties); - } - /** * 文件存储服务 * * @param router 路由 - * @param storageProperties 存储属性 * @param processorRegistry 处理器注册表 * @param fileRecorder 文件记录器 * @return {@link FileStorageService } */ @Bean public FileStorageService fileStorageService(StorageStrategyRouter router, - StorageProperties storageProperties, ProcessorRegistry processorRegistry, FileRecorder fileRecorder) { - return new FileStorageService(router, storageProperties, processorRegistry, fileRecorder); + return new FileStorageService(router, processorRegistry, fileRecorder); } /** @@ -143,8 +124,9 @@ public class StorageAutoConfiguration { // 自动发现并注册所有 FileProcessor 实现 Map processors = applicationContext.getBeansOfType(FileProcessor.class); processors.values().forEach(processor -> { - // 检查是否有平台注解 - PlatformProcessor annotation = processor.getClass().getAnnotation(PlatformProcessor.class); + // 检查是否有平台注解(兼容代理类) + Class targetClass = AopUtils.getTargetClass(processor); + PlatformProcessor annotation = AnnotationUtils.findAnnotation(targetClass, PlatformProcessor.class); if (annotation != null) { for (String platform : annotation.platforms()) { registry.register(processor, platform); diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/OssStorageConfig.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/OssStorageConfig.java index f0fee8c5e443cfae24f6b51017d7eb0ade493098..384eab147146df2104326dfffe3558d6a6d1f920 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/OssStorageConfig.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/OssStorageConfig.java @@ -81,6 +81,11 @@ public class OssStorageConfig { */ private Long multipartUploadPartSize; + /** + * 分片落盘临时目录(可选) + */ + private String multipartTempDir; + /** * 请求超时时间(秒) */ @@ -184,6 +189,14 @@ public class OssStorageConfig { this.multipartUploadPartSize = multipartUploadPartSize; } + public String getMultipartTempDir() { + return multipartTempDir; + } + + public void setMultipartTempDir(String multipartTempDir) { + this.multipartTempDir = multipartTempDir; + } + public int getRequestTimeout() { return requestTimeout; } diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/core/FileStorageService.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/core/FileStorageService.java index aac9c803740d98959ff226261385d5f0ded5c255..4040e3d5ef65206f1f2dd94fc5dae6d791c1d7d3 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/core/FileStorageService.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/core/FileStorageService.java @@ -21,7 +21,6 @@ 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.common.constant.StorageConstant; import top.continew.starter.storage.common.exception.StorageException; import top.continew.starter.storage.domain.file.EnhancedMultipartFile; @@ -56,18 +55,15 @@ 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 FileRecorder fileRecorder; private final ThreadLocal> tempProcessors = ThreadLocal.withInitial(ArrayList::new); private final ThreadLocal progressListener = new ThreadLocal<>(); public FileStorageService(StorageStrategyRouter router, - StorageProperties storageProperties, ProcessorRegistry processorRegistry, FileRecorder fileRecorder) { this.router = router; - this.storageProperties = storageProperties; this.processorRegistry = processorRegistry; this.fileRecorder = fileRecorder; } @@ -600,8 +596,6 @@ public class FileStorageService { .getOriginalFileName())); fileInfo.getMetadata() .put("fileMd5", StrUtil.blankToDefault(session.getFileMd5(), StringConstants.EMPTY)); - fileInfo.getMetadata() - .put("sha256", StrUtil.blankToDefault(session.getFileMd5(), StringConstants.EMPTY)); } fileInfo.getMetadata().put("uploadId", uploadId); fileInfo.getMetadata().put("status", "COMPLETED"); diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/context/UploadContext.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/context/UploadContext.java index 84c72ac89408c076a64871116bbad00f65b3e602..a2446a29c7bd3283efbf8a5c5b720f5b3b728c39 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/context/UploadContext.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/domain/model/context/UploadContext.java @@ -17,6 +17,7 @@ package top.continew.starter.storage.domain.model.context; import org.springframework.web.multipart.MultipartFile; +import top.continew.starter.core.constant.StringConstants; import top.continew.starter.storage.domain.model.req.ThumbnailSize; import top.continew.starter.storage.processor.progress.UploadProgressListener; @@ -85,7 +86,18 @@ public class UploadContext { * 获取完整路径 */ public String getFullPath() { - return path + formatFileName; + String safePath = path == null ? "" : path.trim(); + String safeFileName = formatFileName == null ? "" : formatFileName.trim(); + + if (safePath.isEmpty()) { + return safeFileName; + } + if (safeFileName.isEmpty()) { + return safePath; + } + return safePath.endsWith(StringConstants.SLASH) + ? safePath + safeFileName + : safePath + StringConstants.SLASH + safeFileName; } public MultipartFile getFile() { diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/engine/StorageStrategyRouter.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/engine/StorageStrategyRouter.java index 21b7038d01cee19711e31238fb62de666a1e2af0..bcc0a8a01f7171177ae78c506af2da126a5e8625 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/engine/StorageStrategyRouter.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/engine/StorageStrategyRouter.java @@ -148,28 +148,66 @@ public class StorageStrategyRouter implements ApplicationListener { - if (StrUtil.isBlank(dynamicDefaultPlatform)) { - throw new StorageException("动态默认存储平台配置为空"); - } - yield dynamicDefaultPlatform; + DefaultStorageSource defaultStorageSource = ObjectUtil.defaultIfNull(storageProperties + .getDefaultStorageSource(), DefaultStorageSource.DYNAMIC); + List candidates = resolveDefaultStorageCandidates(defaultStorageSource); + for (String candidate : candidates) { + if (hasStrategy(candidate)) { + return candidate; } - case CONFIG -> { - if (StrUtil.isBlank(configDefaultPlatform)) { - throw new StorageException("配置默认存储平台配置为空"); - } - yield configDefaultPlatform; - } - }; + } + + throw new StorageException(String + .format("未找到可用默认存储平台: source=%s, candidates=%s, available=%s", defaultStorageSource, candidates, getAllPlatform())); + } + + /** + * 解决默认存储候选 + * + * @param defaultStorageSource 默认存储源 + * @return {@link List }<{@link String }> + */ + private List resolveDefaultStorageCandidates(DefaultStorageSource defaultStorageSource) { + Set orderedCandidates = new LinkedHashSet<>(); + if (defaultStorageSource == DefaultStorageSource.DYNAMIC) { + addCandidate(orderedCandidates, dynamicDefaultPlatform); + addCandidate(orderedCandidates, configDefaultPlatform); + } else { + addCandidate(orderedCandidates, configDefaultPlatform); + addCandidate(orderedCandidates, dynamicDefaultPlatform); + } + addCandidate(orderedCandidates, StorageConstant.DEFAULT_STORAGE_PLATFORM); + return new ArrayList<>(orderedCandidates); + } + + /** + * 添加候选人 + * + * @param candidates 候选人 + * @param platform 站台 + */ + private void addCandidate(Set candidates, String platform) { + String normalizedPlatform = StrUtil.trim(platform); + if (StrUtil.isNotBlank(normalizedPlatform)) { + candidates.add(normalizedPlatform); + } + } + + /** + * 有策略 + * + * @param platform 站台 + * @return boolean + */ + private boolean hasStrategy(String platform) { + return dynamicStrategies.containsKey(platform) || configStrategies.containsKey(platform); } /** diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/DefaultFileNameGenerator.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/DefaultFileNameGenerator.java index 2b58910b2dc4baf32b0e9bc554e4d286b8f486e2..1406253edc0880c2183e35f797f99126f9ea7754 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/DefaultFileNameGenerator.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/processor/preprocess/impl/DefaultFileNameGenerator.java @@ -31,7 +31,7 @@ public class DefaultFileNameGenerator implements FileNameGenerator { @Override public String getName() { - return DefaultFilePathGenerator.class.getSimpleName(); + return DefaultFileNameGenerator.class.getSimpleName(); } @Override diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/LocalStorageStrategy.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/LocalStorageStrategy.java index 3a3a555b077a25291b9ed7891900dc05f72ea5f6..90474ecceb99eb33d3451b4b839a4a800c030f6e 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/LocalStorageStrategy.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/LocalStorageStrategy.java @@ -65,6 +65,11 @@ public class LocalStorageStrategy implements StorageStrategy { */ private final String multipartTempDir; + /** + * 动态注册的静态资源路径模式 + */ + private String resourceHandlerPath; + public LocalStorageStrategy(LocalStorageConfig config) { this.config = config; this.multipartUploadPartSize = resolveMultipartUploadPartSize(config); @@ -79,9 +84,21 @@ public class LocalStorageStrategy implements StorageStrategy { * @param config 配置 */ public void registerResources(LocalStorageConfig config) { - // 注册资源映射 - SpringUtils.registerResourceHandler(MapUtil.of(URLUtil.url(config.getEndpoint()).getPath(), config - .getBucketName())); + if (StrUtil.hasBlank(config.getEndpoint(), config.getBucketName())) { + return; + } + try { + String endpointPath = URLUtil.url(config.getEndpoint()).getPath(); + if (StrUtil.isBlank(endpointPath)) { + return; + } + // 注册资源映射 + SpringUtils.registerResourceHandler(MapUtil.of(endpointPath, config.getBucketName())); + this.resourceHandlerPath = endpointPath; + } catch (Exception e) { + // 避免因运行环境差异导致启动失败 + log.warn("注册本地存储静态资源映射失败,将继续使用存储功能: platform={}", config.getPlatform(), e); + } } /** @@ -480,7 +497,13 @@ public class LocalStorageStrategy implements StorageStrategy { while (normalized.startsWith(StringConstants.SLASH)) { normalized = normalized.substring(1); } - return normalized; + + Path normalizedPath = Paths.get(normalized).normalize(); + String safePath = normalizedPath.toString().replace("\\", StringConstants.SLASH); + if (StringConstants.DOUBLE_DOT.equals(safePath) || safePath.startsWith("../")) { + throw new StorageException("非法路径,包含目录穿越风险: " + rawPath); + } + return safePath; } private long resolveMultipartUploadPartSize(LocalStorageConfig localStorageConfig) { @@ -520,9 +543,13 @@ public class LocalStorageStrategy implements StorageStrategy { @Override public void cleanup() { // 清理静态资源映射 - if (config != null) { - SpringUtils.deRegisterResourceHandler(MapUtil.of(URLUtil.url(config.getEndpoint()).getPath(), config - .getBucketName())); + if (StrUtil.hasBlank(resourceHandlerPath, config.getBucketName())) { + return; + } + try { + SpringUtils.deRegisterResourceHandler(MapUtil.of(resourceHandlerPath, config.getBucketName())); + } catch (Exception e) { + log.warn("清理本地存储静态资源映射失败: platform={}", config.getPlatform(), e); } } diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/OssStorageStrategy.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/OssStorageStrategy.java index 83d5ad77d890407574aee409615bce17d237c6e5..ad3d4d9ae54f0d9320d839f9b266f90d43557580 100644 --- a/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/OssStorageStrategy.java +++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/strategy/impl/OssStorageStrategy.java @@ -22,6 +22,7 @@ import org.slf4j.LoggerFactory; import org.springframework.web.multipart.MultipartFile; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.S3Configuration; @@ -41,8 +42,13 @@ import top.continew.starter.storage.domain.model.resp.MultipartUploadResp; import top.continew.starter.storage.strategy.StorageStrategy; import top.continew.starter.storage.common.util.StorageUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Duration; import java.time.LocalDateTime; import java.util.*; @@ -61,11 +67,17 @@ public class OssStorageStrategy implements StorageStrategy { private final S3Client s3Client; private final S3Presigner s3Presigner; private final OssStorageConfig config; + private final long multipartUploadThreshold; private final long multipartUploadPartSize; + private final String multipartTempDir; + private final long uploadPartInMemoryThreshold; public OssStorageStrategy(OssStorageConfig config) { this.config = config; + this.multipartUploadThreshold = resolveMultipartUploadThreshold(config); this.multipartUploadPartSize = resolveMultipartUploadPartSize(config); + this.multipartTempDir = resolveMultipartTempDir(config); + this.uploadPartInMemoryThreshold = resolveUploadPartInMemoryThreshold(); this.s3Client = createS3Client(config); this.s3Presigner = createS3Presigner(config); } @@ -103,7 +115,8 @@ public class OssStorageStrategy implements StorageStrategy { .credentialsProvider(auth) .endpointOverride(URI.create(config.getEndpoint())) .region(StorageUtils.getRegion(config.getRegion())) - .serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(true).build()) + .serviceConfiguration(buildS3Configuration(config)) + .overrideConfiguration(buildTimeoutConfig(config)) .build(); } @@ -123,9 +136,7 @@ public class OssStorageStrategy implements StorageStrategy { .credentialsProvider(auth) .endpointOverride(URI.create(domain)) .region(StorageUtils.getRegion(config.getRegion())) - .serviceConfiguration(S3Configuration.builder() - .pathStyleAccessEnabled(config.isPathStyleAccessEnabled()) - .build()) + .serviceConfiguration(buildS3Configuration(config)) .build(); } @@ -216,8 +227,11 @@ public class OssStorageStrategy implements StorageStrategy { try { s3Client.headObject(HeadObjectRequest.builder().bucket(bucket).key(normalizeKey(path)).build()); return true; - } catch (NoSuchKeyException e) { - return false; + } catch (S3Exception e) { + if (e.statusCode() == 404) { + return false; + } + throw new StorageException("S3检查文件存在性失败: " + e.getMessage(), e); } catch (Exception e) { throw new StorageException("S3检查文件存在性失败: " + e.getMessage(), e); } @@ -257,8 +271,11 @@ public class OssStorageStrategy implements StorageStrategy { return fileInfo; - } catch (NoSuchKeyException e) { - return null; + } catch (S3Exception e) { + if (e.statusCode() == 404) { + return null; + } + throw new StorageException("S3获取文件信息失败: " + e.getMessage(), e); } catch (Exception e) { throw new StorageException("S3获取文件信息失败: " + e.getMessage(), e); } @@ -340,9 +357,6 @@ public class OssStorageStrategy implements StorageStrategy { return config.getBucketName(); } - /** - * 生成预签名URL - */ @Override public String generatePresignedUrl(String bucket, String path, long expireSeconds) { try { @@ -369,14 +383,14 @@ public class OssStorageStrategy implements StorageStrategy { public String generateUploadPresignedUrl(String bucket, String path, long expireSeconds) { try { PutObjectRequest putObjectRequest = PutObjectRequest.builder() - .bucket(bucket) - .key(normalizeKey(path)) - .build(); + .bucket(bucket) + .key(normalizeKey(path)) + .build(); PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder() - .signatureDuration(Duration.ofSeconds(expireSeconds)) - .putObjectRequest(putObjectRequest) - .build(); + .signatureDuration(Duration.ofSeconds(expireSeconds)) + .putObjectRequest(putObjectRequest) + .build(); PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest); return presignedRequest.url().toString(); @@ -389,11 +403,15 @@ public class OssStorageStrategy implements StorageStrategy { * 获取文件URL */ private String getFileUrl(String path) { - if (config.getEndpoint() != null && !config.getEndpoint().isEmpty()) { - return config.getEndpoint() + StringConstants.SLASH + path; - } else { - return String.format("%s/%s/%s", config.getEndpoint(), config.getBucketName(), path); + String baseUrl = StrUtil.isNotBlank(config.getDomain()) ? config.getDomain() : config.getEndpoint(); + if (StrUtil.isBlank(baseUrl)) { + return StringConstants.SLASH + path; } + baseUrl = StrUtil.removeSuffix(baseUrl, StringConstants.SLASH); + if (StrUtil.isNotBlank(config.getDomain())) { + return baseUrl + StringConstants.SLASH + path; + } + return String.format("%s/%s/%s", baseUrl, config.getBucketName(), path); } /** @@ -468,6 +486,9 @@ public class OssStorageStrategy implements StorageStrategy { String uploadId, int partNumber, InputStream data) { + byte[] partBytes = null; + Path tempFile = null; + long partSize = 0L; try { String key = normalizeKey(path); @@ -475,8 +496,11 @@ public class OssStorageStrategy implements StorageStrategy { throw new StorageException("无效的uploadId: " + uploadId); } - // 读取数据到内存(注意:实际使用时可能需要优化大文件处理) - byte[] bytes = data.readAllBytes(); + // 小分片优先走内存,超过阈值自动切换到临时文件,兼顾吞吐和内存控制 + PartPayload payload = readPartPayload(data); + partBytes = payload.bytes(); + tempFile = payload.tempFile(); + partSize = payload.size(); // 构建请求 UploadPartRequest request = UploadPartRequest.builder() @@ -484,16 +508,20 @@ public class OssStorageStrategy implements StorageStrategy { .key(key) .uploadId(uploadId) .partNumber(partNumber) - .contentLength((long)bytes.length) + .contentLength(partSize) .build(); // 执行上传 - UploadPartResponse response = s3Client.uploadPart(request, RequestBody.fromBytes(bytes)); + RequestBody requestBody = partBytes != null + ? RequestBody.fromBytes(partBytes) + : RequestBody.fromFile(tempFile); + UploadPartResponse response = s3Client.uploadPart(request, requestBody); // 构建返回结果 MultipartUploadResp result = new MultipartUploadResp(); result.setPartNumber(partNumber); result.setPartETag(response.eTag()); + result.setPartSize(partSize); result.setSuccess(true); return result; @@ -503,6 +531,14 @@ public class OssStorageStrategy implements StorageStrategy { result.setSuccess(false); result.setErrorMessage(e.getMessage()); return result; + } finally { + if (tempFile != null) { + try { + Files.deleteIfExists(tempFile); + } catch (Exception e) { + log.warn("删除临时分片文件失败: {}", tempFile, e); + } + } } } @@ -665,6 +701,14 @@ public class OssStorageStrategy implements StorageStrategy { return key; } + private long resolveMultipartUploadThreshold(OssStorageConfig ossStorageConfig) { + Long threshold = ossStorageConfig.getMultipartUploadThreshold(); + if (threshold == null || threshold <= 0) { + return StorageConstant.DEFAULT_MULTIPART_UPLOAD_THRESHOLD; + } + return threshold; + } + private long resolveMultipartUploadPartSize(OssStorageConfig ossStorageConfig) { Long partSize = ossStorageConfig.getMultipartUploadPartSize(); if (partSize == null || partSize <= 0) { @@ -672,4 +716,80 @@ public class OssStorageStrategy implements StorageStrategy { } return partSize; } + + private long resolveUploadPartInMemoryThreshold() { + // 复用现有配置:以内存分片阈值=min(分片阈值, 分片大小) + long threshold = Math.min(multipartUploadThreshold, multipartUploadPartSize); + return Math.max(1L, threshold); + } + + private String resolveMultipartTempDir(OssStorageConfig ossStorageConfig) { + if (StrUtil.isBlank(ossStorageConfig.getMultipartTempDir())) { + return StorageConstant.DEFAULT_LOCAL_MULTIPART_TEMP_DIR; + } + return ossStorageConfig.getMultipartTempDir().trim(); + } + + private S3Configuration buildS3Configuration(OssStorageConfig ossStorageConfig) { + return S3Configuration.builder() + .pathStyleAccessEnabled(ossStorageConfig.isPathStyleAccessEnabled()) + .accelerateModeEnabled(ossStorageConfig.isTransferAccelerationEnabled()) + .build(); + } + + private ClientOverrideConfiguration buildTimeoutConfig(OssStorageConfig ossStorageConfig) { + int requestTimeoutSeconds = Math.max(1, ossStorageConfig.getRequestTimeout()); + Duration timeout = Duration.ofSeconds(requestTimeoutSeconds); + return ClientOverrideConfiguration.builder() + .apiCallAttemptTimeout(timeout) + .apiCallTimeout(timeout.multipliedBy(2)) + .build(); + } + + private PartPayload readPartPayload(InputStream data) throws IOException { + byte[] buffer = new byte[8192]; + ByteArrayOutputStream memoryBuffer = new ByteArrayOutputStream(); + OutputStream fileOutput = null; + Path tempFile = null; + long totalRead = 0L; + + try { + int bytesRead; + while ((bytesRead = data.read(buffer)) != -1) { + totalRead += bytesRead; + if (fileOutput != null) { + fileOutput.write(buffer, 0, bytesRead); + continue; + } + if (totalRead <= uploadPartInMemoryThreshold) { + memoryBuffer.write(buffer, 0, bytesRead); + continue; + } + + // 超过阈值后,切换到文件模式,先写入已缓存内存数据 + tempFile = createMultipartTempFile(); + fileOutput = Files.newOutputStream(tempFile); + memoryBuffer.writeTo(fileOutput); + fileOutput.write(buffer, 0, bytesRead); + } + } finally { + if (fileOutput != null) { + fileOutput.close(); + } + } + + if (tempFile != null) { + return new PartPayload(null, tempFile, totalRead); + } + return new PartPayload(memoryBuffer.toByteArray(), null, totalRead); + } + + private Path createMultipartTempFile() throws IOException { + Path tempDir = Path.of(multipartTempDir); + Files.createDirectories(tempDir); + return Files.createTempFile(tempDir, "storage-part-", ".tmp"); + } + + private record PartPayload(byte[] bytes, Path tempFile, long size) { + } }