diff --git a/continew-starter-bom/pom.xml b/continew-starter-bom/pom.xml
index c6df4851..457ca5ed 100644
--- a/continew-starter-bom/pom.xml
+++ b/continew-starter-bom/pom.xml
@@ -216,22 +216,10 @@
- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *
- * http://www.gnu.org/licenses/lgpl.html - *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package top.continew.starter.storage.decorator;
-
-import top.continew.starter.storage.model.resp.ThumbnailResp;
-import top.continew.starter.storage.model.resp.UploadResp;
-import top.continew.starter.storage.strategy.StorageStrategy;
-
-import java.io.InputStream;
-
-/**
- * 装饰器基类
- *
- * @author echo
- * @since 2.9.0
- */
-public abstract class AbstractStorageDecorator
- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.gnu.org/licenses/lgpl.html
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package top.continew.starter.storage.enums;
-
-import cn.hutool.core.text.CharSequenceUtil;
-import top.continew.starter.core.enums.BaseEnum;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * 文件类型枚举
- *
- * @author Charles7c
- * @since 2.9.0
- */
-public enum FileType implements BaseEnum
- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.gnu.org/licenses/lgpl.html
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package top.continew.starter.storage.manger;
-
-import top.continew.starter.cache.redisson.util.RedisUtils;
-import top.continew.starter.core.util.validation.ValidationUtils;
-import top.continew.starter.storage.constant.StorageConstant;
-import top.continew.starter.storage.strategy.StorageStrategy;
-
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * 存储策略管理器
- *
- * @author echo
- * @since 2.9.0
- */
-public class StorageManager {
-
- /**
- * 存储策略连接信息
- */
- private static final Map
- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.gnu.org/licenses/lgpl.html
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package top.continew.starter.storage.model.req;
-
-/**
- * 存储配置信息
- *
- * @author echo
- * @date 2024/11/04 15:13
- */
-public class StorageProperties {
-
- /**
- * 编码
- */
- private String code;
-
- /**
- * 访问密钥
- */
- private String accessKey;
-
- /**
- * 私有密钥
- */
- private String secretKey;
-
- /**
- * 终端节点
- */
- private String endpoint;
-
- /**
- * 桶名称
- */
- private String bucketName;
-
- /**
- * 域名
- */
- private String domain;
-
- /**
- * 作用域
- */
- private String region;
-
- /**
- * 是否是默认存储
- */
- private Boolean isDefault;
-
- public StorageProperties() {
- }
-
- public String getCode() {
- return code;
- }
-
- public void setCode(String code) {
- this.code = code;
- }
-
- public String getAccessKey() {
- return accessKey;
- }
-
- public void setAccessKey(String accessKey) {
- this.accessKey = accessKey;
- }
-
- public String getSecretKey() {
- return secretKey;
- }
-
- public void setSecretKey(String secretKey) {
- this.secretKey = secretKey;
- }
-
- public String getEndpoint() {
- return endpoint;
- }
-
- public void setEndpoint(String endpoint) {
- this.endpoint = endpoint;
- }
-
- public String getBucketName() {
- return bucketName;
- }
-
- public void setBucketName(String bucketName) {
- this.bucketName = bucketName;
- }
-
- public String getDomain() {
- return domain;
- }
-
- public void setDomain(String domain) {
- this.domain = domain;
- }
-
- public String getRegion() {
- return region;
- }
-
- public void setRegion(String region) {
- this.region = region;
- }
-
- public Boolean getIsDefault() {
- return isDefault;
- }
-
- public void setIsDefault(Boolean isDefault) {
- this.isDefault = isDefault;
- }
-
- public StorageProperties(String code,
- String accessKey,
- String secretKey,
- String endpoint,
- String bucketName,
- String domain,
- String region,
- Boolean isDefault) {
- this.code = code;
- this.accessKey = accessKey;
- this.secretKey = secretKey;
- this.endpoint = endpoint;
- this.bucketName = bucketName;
- this.domain = domain;
- this.region = region;
- this.isDefault = isDefault;
-
- }
-}
diff --git a/continew-starter-storage/continew-starter-storage-core/src/main/java/top/continew/starter/storage/model/resp/ThumbnailResp.java b/continew-starter-storage/continew-starter-storage-core/src/main/java/top/continew/starter/storage/model/resp/ThumbnailResp.java
deleted file mode 100644
index 9b149786..00000000
--- a/continew-starter-storage/continew-starter-storage-core/src/main/java/top/continew/starter/storage/model/resp/ThumbnailResp.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
- *
- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.gnu.org/licenses/lgpl.html
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package top.continew.starter.storage.model.resp;
-
-/**
- * 缩略图
- *
- * @author echo
- * @since 2.9.0
- */
-public class ThumbnailResp {
-
- /**
- * 缩略图大小(字节)
- */
- private Long thumbnailSize;
-
- /**
- * 缩略图地址 格式 xxx/xxx/xxx.small.jpg
- */
- private String thumbnailPath;
-
- public ThumbnailResp() {
- }
-
- public ThumbnailResp(Long thumbnailSize, String thumbnailPath) {
- this.thumbnailSize = thumbnailSize;
- this.thumbnailPath = thumbnailPath;
- }
-
- public Long getThumbnailSize() {
- return thumbnailSize;
- }
-
- public void setThumbnailSize(Long thumbnailSize) {
- this.thumbnailSize = thumbnailSize;
- }
-
- public String getThumbnailPath() {
- return thumbnailPath;
- }
-
- public void setThumbnailPath(String thumbnailPath) {
- this.thumbnailPath = thumbnailPath;
- }
-
-}
diff --git a/continew-starter-storage/continew-starter-storage-core/src/main/java/top/continew/starter/storage/model/resp/UploadResp.java b/continew-starter-storage/continew-starter-storage-core/src/main/java/top/continew/starter/storage/model/resp/UploadResp.java
deleted file mode 100644
index a2990f6b..00000000
--- a/continew-starter-storage/continew-starter-storage-core/src/main/java/top/continew/starter/storage/model/resp/UploadResp.java
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
- *
- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.gnu.org/licenses/lgpl.html
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package top.continew.starter.storage.model.resp;
-
-import java.time.LocalDateTime;
-
-/**
- * 上传结果
- *
- * @author echo
- * @since 2.9.0
- */
-public class UploadResp {
-
- /**
- * 存储 code
- */
- private String code;
-
- /**
- * 访问地址
- * 如果桶为私有,则提供临时链接,时间默认为 12 小时
- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.gnu.org/licenses/lgpl.html
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package top.continew.starter.storage.strategy;
-
-import top.continew.starter.storage.model.resp.ThumbnailResp;
-import top.continew.starter.storage.model.resp.UploadResp;
-
-import java.io.InputStream;
-
-/**
- * 存储策略接口
- *
- * @author echo
- * @since 2.9.0
- */
-public interface StorageStrategy S3: 检查桶是否存在 local: 检查 默认路径 是否存在 S3: 创建桶 local: 在默认路径下创建指定文件夹
- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.gnu.org/licenses/lgpl.html
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package top.continew.starter.storage.util;
-
-import javax.imageio.ImageIO;
-import java.awt.*;
-import java.awt.image.BufferedImage;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * 图像缩略图工具
- *
- * @author echo
- * @since 2.9.0
- */
-public class ImageThumbnailUtils {
-
- // 默认缩略图尺寸:100x100
- private static final int DEFAULT_WIDTH = 100;
- private static final int DEFAULT_HEIGHT = 100;
-
- /**
- * 根据输入流生成默认大小(100x100)的缩略图并写入输出流
- *
- * @param inputStream 原始图片的输入流
- * @param outputStream 缩略图输出流
- * @param suffix 后缀
- * @throws IOException IOException
- */
- public static void generateThumbnail(InputStream inputStream,
- OutputStream outputStream,
- String suffix) throws IOException {
- generateThumbnail(inputStream, outputStream, DEFAULT_WIDTH, DEFAULT_HEIGHT, suffix);
- }
-
- /**
- * 根据输入流和自定义尺寸生成缩略图并写入输出流
- *
- * @param inputStream 原始图片的输入流
- * @param outputStream 缩略图输出流
- * @param width 缩略图宽度
- * @param height 缩略图高度
- * @param suffix 后缀
- * @throws IOException IOException
- */
- public static void generateThumbnail(InputStream inputStream,
- OutputStream outputStream,
- int width,
- int height,
- String suffix) throws IOException {
- // 读取原始图片
- BufferedImage originalImage = ImageIO.read(inputStream);
-
- // 调整图片大小
- Image tmp = originalImage.getScaledInstance(width, height, Image.SCALE_SMOOTH);
- BufferedImage thumbnail = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
-
- // 画出缩略图
- Graphics2D g2d = thumbnail.createGraphics();
- g2d.drawImage(tmp, 0, 0, null);
- g2d.dispose();
- // 写入输出流
- ImageIO.write(thumbnail, suffix, outputStream);
- }
-}
diff --git a/continew-starter-storage/continew-starter-storage-core/src/main/java/top/continew/starter/storage/util/StorageUtils.java b/continew-starter-storage/continew-starter-storage-core/src/main/java/top/continew/starter/storage/util/StorageUtils.java
deleted file mode 100644
index 565c15d3..00000000
--- a/continew-starter-storage/continew-starter-storage-core/src/main/java/top/continew/starter/storage/util/StorageUtils.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
- *
- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.gnu.org/licenses/lgpl.html
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package top.continew.starter.storage.util;
-
-import cn.hutool.core.io.FileUtil;
-import cn.hutool.core.io.IoUtil;
-import cn.hutool.core.io.file.FileNameUtil;
-import cn.hutool.core.text.CharSequenceUtil;
-import top.continew.starter.core.constant.StringConstants;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.net.URI;
-import java.nio.file.Paths;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-
-/**
- * 储存工具
- *
- * @author echo
- * @since 2.9.0
- */
-public class StorageUtils {
- public StorageUtils() {
- }
-
- /**
- * 格式文件名
- *
- * @param fileName 文件名
- * @return {@link String }
- */
- public static String formatFileName(String fileName) {
- // 获取文件后缀名
- String suffix = FileUtil.extName(fileName);
- // 获取当前时间的年月日时分秒格式
- DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
- String datetime = LocalDateTime.now().format(formatter);
- // 获取当前时间戳
- String timestamp = String.valueOf(System.currentTimeMillis());
- // 生成新的文件名
- return datetime + timestamp + "." + suffix;
- }
-
- /**
- * 默认文件目录
- *
- * @param fileName 文件名
- * @return {@link String }
- */
- public static String defaultFileDir(String fileName) {
- LocalDate today = LocalDate.now();
- return Paths.get(String.valueOf(today.getYear()), String.valueOf(today.getMonthValue()), String.valueOf(today
- .getDayOfMonth()), fileName).toString();
- }
-
- /**
- * 本地存储默认路径地址 格式
- * mac/linux : 2024/03/10/>
- * windows : 2024\03\10\>
- *
- * @return {@link String }
- */
- public static String localDefaultPath() {
- LocalDate today = LocalDate.now();
- return Paths.get(String.valueOf(today.getYear()), String.valueOf(today.getMonthValue()), String.valueOf(today
- .getDayOfMonth())) + StringConstants.SLASH;
- }
-
- /**
- * 对象存储默认路径 格式 2024/03/10/
- *
- * @return {@link String }
- */
- public static String ossDefaultPath() {
- LocalDate today = LocalDate.now();
- return today.getYear() + StringConstants.SLASH + today.getMonthValue() + StringConstants.SLASH + today
- .getDayOfMonth() + StringConstants.SLASH;
- }
-
- /**
- * 根据 endpoint 判断是否带有 http 或 https,如果没有则加上 http 前缀。
- *
- * @param endpoint 输入的 endpoint 字符串
- * @return URI 对象
- */
- public static URI createUriWithProtocol(String endpoint) {
- // 判断 endpoint 是否包含 http:// 或 https:// 前缀
- if (!endpoint.startsWith("http://") && !endpoint.startsWith("https://")) {
- // 如果没有协议前缀,则加上 http://
- endpoint = "http://" + endpoint;
- }
- // 返回 URI 对象
- return URI.create(endpoint);
- }
-
- /**
- * 生成缩略图文件名
- *
- * @param fileName 文件名
- * @param suffix 后缀
- * @return {@link String }
- */
- public static String buildThumbnailFileName(String fileName, String suffix) {
- // 获取文件的扩展名
- String extName = FileNameUtil.extName(fileName);
- // 去掉扩展名
- String baseName = CharSequenceUtil.subBefore(fileName, StringConstants.DOT, true);
- // 拼接新的路径:原始路径 + .缩略图后缀 + .扩展名
- return baseName + "." + suffix + "." + extName;
- }
-
- /**
- * 可重复读流
- *
- * @param inputStream 输入流
- * @return {@link InputStream }
- */
- public static InputStream ensureByteArrayStream(InputStream inputStream) {
- return (inputStream instanceof ByteArrayInputStream)
- ? inputStream
- : new ByteArrayInputStream(IoUtil.readBytes(inputStream));
- }
-
-}
diff --git a/continew-starter-storage/continew-starter-storage-local/pom.xml b/continew-starter-storage/continew-starter-storage-local/pom.xml
deleted file mode 100644
index 5ed3880b..00000000
--- a/continew-starter-storage/continew-starter-storage-local/pom.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.gnu.org/licenses/lgpl.html
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package top.continew.starter.storage.client;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import top.continew.starter.storage.model.req.StorageProperties;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-/**
- * 本地客户端
- *
- * @author echo
- * @since 2.9.0
- */
-public class LocalClient {
-
- private static final Logger log = LoggerFactory.getLogger(LocalClient.class);
-
- /**
- * 配置属性
- */
- private final StorageProperties properties;
-
- /**
- * 构造函数
- *
- * @param properties 配置属性
- */
- public LocalClient(StorageProperties properties) {
- this.properties = properties;
- // 判断是否是默认存储,若不存在桶目录,则创建
- if (Boolean.TRUE.equals(properties.getIsDefault())) {
- String bucketName = properties.getBucketName();
- if (bucketName != null && !bucketName.isEmpty()) {
- createBucketDirectory(bucketName);
- } else {
- log.info("默认存储-存储桶已存在 => {}", bucketName);
- }
- }
- log.info("加载 Local 存储 => {}", properties.getCode());
- }
-
- /**
- * 获取属性
- *
- * @return {@link StorageProperties }
- */
- public StorageProperties getProperties() {
- return properties;
- }
-
- /**
- * 创建桶目录
- *
- * @param bucketName 桶名称
- */
- private void createBucketDirectory(String bucketName) {
- Path bucketPath = Path.of(bucketName);
- try {
- if (Files.notExists(bucketPath)) {
- Files.createDirectories(bucketPath);
- log.info("默认存储-存储桶创建成功 : {}", bucketPath.toAbsolutePath());
- }
- } catch (IOException e) {
- log.error("创建默认存储-存储桶失败 => 路径: {}", bucketPath.toAbsolutePath(), e);
- }
- }
-}
diff --git a/continew-starter-storage/continew-starter-storage-local/src/main/java/top/continew/starter/storage/strategy/LocalStorageStrategy.java b/continew-starter-storage/continew-starter-storage-local/src/main/java/top/continew/starter/storage/strategy/LocalStorageStrategy.java
deleted file mode 100644
index 42b5d05f..00000000
--- a/continew-starter-storage/continew-starter-storage-local/src/main/java/top/continew/starter/storage/strategy/LocalStorageStrategy.java
+++ /dev/null
@@ -1,274 +0,0 @@
-/*
- * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
- *
- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.gnu.org/licenses/lgpl.html
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package top.continew.starter.storage.strategy;
-
-import cn.hutool.core.io.FileUtil;
-import cn.hutool.core.io.IoUtil;
-import cn.hutool.core.io.file.FileNameUtil;
-import cn.hutool.core.text.CharSequenceUtil;
-import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.crypto.digest.DigestUtil;
-import top.continew.starter.core.constant.StringConstants;
-import top.continew.starter.core.exception.BusinessException;
-import top.continew.starter.core.util.validation.CheckUtils;
-import top.continew.starter.core.util.validation.ValidationUtils;
-import top.continew.starter.storage.client.LocalClient;
-import top.continew.starter.storage.constant.StorageConstant;
-import top.continew.starter.storage.dao.StorageDao;
-import top.continew.starter.storage.enums.FileType;
-import top.continew.starter.storage.model.req.StorageProperties;
-import top.continew.starter.storage.model.resp.ThumbnailResp;
-import top.continew.starter.storage.model.resp.UploadResp;
-import top.continew.starter.storage.util.ImageThumbnailUtils;
-import top.continew.starter.storage.util.StorageUtils;
-
-import java.io.*;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.time.LocalDateTime;
-import java.util.Base64;
-
-/**
- * 本地存储策略
- *
- * @author echo
- * @since 2.9.0
- */
-public class LocalStorageStrategy implements StorageStrategy
- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.gnu.org/licenses/lgpl.html
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package top.continew.starter.storage.client;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
-import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
-import software.amazon.awssdk.services.s3.S3AsyncClient;
-import software.amazon.awssdk.services.s3.crt.S3CrtHttpConfiguration;
-import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
-import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
-import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
-import software.amazon.awssdk.services.s3.presigner.S3Presigner;
-import software.amazon.awssdk.transfer.s3.S3TransferManager;
-import top.continew.starter.core.exception.BusinessException;
-import top.continew.starter.storage.model.req.StorageProperties;
-import top.continew.starter.storage.util.OssUtils;
-import top.continew.starter.storage.util.StorageUtils;
-
-import java.net.URI;
-import java.time.Duration;
-
-/**
- * S3客户端
- *
- * @author echo
- * @since 2.9.0
- */
-public class OssClient {
-
- private static final Logger log = LoggerFactory.getLogger(OssClient.class);
-
- /**
- * 配置属性
- */
- private final StorageProperties properties;
-
- /**
- * s3 异步客户端
- */
- private final S3AsyncClient client;
-
- /**
- * S3 数据传输的高级工具
- */
- private final S3TransferManager transferManager;
-
- /**
- * S3 预签名
- */
- private final S3Presigner presigner;
-
- /**
- * 获取属性
- *
- * @return {@link StorageProperties }
- */
- public StorageProperties getProperties() {
- return properties;
- }
-
- /**
- * 构造方法
- *
- * @param s3PropertiesReq 微型性能要求
- */
- public OssClient(StorageProperties s3PropertiesReq) {
- this.properties = s3PropertiesReq;
-
- // 创建认证信息
- StaticCredentialsProvider auth = StaticCredentialsProvider.create(AwsBasicCredentials.create(properties
- .getAccessKey(), properties.getSecretKey()));
-
- URI uriWithProtocol = StorageUtils.createUriWithProtocol(properties.getEndpoint());
-
- // 创建 客户端连接
- client = S3AsyncClient.crtBuilder()
- .credentialsProvider(auth) // 认证信息
- .endpointOverride(uriWithProtocol) // 连接端点
- .region(OssUtils.getRegion(properties.getRegion()))
- .targetThroughputInGbps(20.0) //吞吐量
- .minimumPartSizeInBytes(10 * 1025 * 1024L)
- .checksumValidationEnabled(false)
- .httpConfiguration(S3CrtHttpConfiguration.builder()
- .connectionTimeout(Duration.ofSeconds(60)) // 设置连接超时
- .build())
- .build();
-
- // 基于 CRT 创建 S3 Transfer Manager 的实例
- this.transferManager = S3TransferManager.builder().s3Client(this.client).build();
-
- this.presigner = S3Presigner.builder()
- .region(OssUtils.getRegion(properties.getRegion()))
- .credentialsProvider(auth)
- .endpointOverride(uriWithProtocol)
- .build();
-
- // 只创建 默认存储的的桶
- if (s3PropertiesReq.getIsDefault()) {
- try {
- // 检查存储桶是否存在
- client.headBucket(HeadBucketRequest.builder().bucket(properties.getBucketName()).build());
- log.info("默认存储-存储桶 {} 已存在", properties.getBucketName());
- } catch (NoSuchBucketException e) {
- log.info("默认存储桶 {} 不存在,尝试创建...", properties.getBucketName());
- try {
- // 创建存储桶
- client.createBucket(CreateBucketRequest.builder().bucket(properties.getBucketName()).build());
- log.info("默认存储-存储桶 {} 创建成功", properties.getBucketName());
- } catch (Exception createException) {
- log.error("创建默认存储-存储桶 {} 失败", properties.getBucketName(), createException);
- throw new BusinessException("创建默认存储-桶出错", createException);
- }
- } catch (Exception e) {
- log.error("检查默认存储-存储桶 {} 时出错", properties.getBucketName(), e);
- throw new BusinessException("检查默认存储-桶时出错", e);
- }
- }
- log.info("加载 S3 存储 => {}", properties.getCode());
- }
-
- /**
- * 获得客户端
- *
- * @return {@link S3TransferManager }
- */
- public S3AsyncClient getClient() {
- return client;
- }
-
- /**
- * 获得 高效连接客户端 主要用于 上传下载 复制 删除
- *
- * @return {@link S3TransferManager }
- */
- public S3TransferManager getTransferManager() {
- return transferManager;
- }
-
- /**
- * 获得 S3 预签名
- *
- * @return {@link S3Presigner }
- */
- public S3Presigner getPresigner() {
- return presigner;
- }
-}
diff --git a/continew-starter-storage/continew-starter-storage-oss/src/main/java/top/continew/starter/storage/strategy/OssStorageStrategy.java b/continew-starter-storage/continew-starter-storage-oss/src/main/java/top/continew/starter/storage/strategy/OssStorageStrategy.java
deleted file mode 100644
index 39a2edcd..00000000
--- a/continew-starter-storage/continew-starter-storage-oss/src/main/java/top/continew/starter/storage/strategy/OssStorageStrategy.java
+++ /dev/null
@@ -1,403 +0,0 @@
-/*
- * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
- *
- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.gnu.org/licenses/lgpl.html
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package top.continew.starter.storage.strategy;
-
-import cn.hutool.core.io.FileUtil;
-import cn.hutool.core.io.IoUtil;
-import cn.hutool.core.io.file.FileNameUtil;
-import cn.hutool.core.text.CharSequenceUtil;
-import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.json.JSONObject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import software.amazon.awssdk.core.ResponseInputStream;
-import software.amazon.awssdk.core.async.AsyncResponseTransformer;
-import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody;
-import software.amazon.awssdk.services.s3.model.*;
-import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
-import software.amazon.awssdk.transfer.s3.model.CompletedUpload;
-import software.amazon.awssdk.transfer.s3.model.Download;
-import software.amazon.awssdk.transfer.s3.model.DownloadRequest;
-import software.amazon.awssdk.transfer.s3.model.Upload;
-import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener;
-import top.continew.starter.core.constant.StringConstants;
-import top.continew.starter.core.exception.BusinessException;
-import top.continew.starter.core.util.validation.CheckUtils;
-import top.continew.starter.core.util.validation.ValidationUtils;
-import top.continew.starter.storage.client.OssClient;
-import top.continew.starter.storage.constant.StorageConstant;
-import top.continew.starter.storage.dao.StorageDao;
-import top.continew.starter.storage.enums.FileType;
-import top.continew.starter.storage.model.req.StorageProperties;
-import top.continew.starter.storage.model.resp.ThumbnailResp;
-import top.continew.starter.storage.model.resp.UploadResp;
-import top.continew.starter.storage.util.ImageThumbnailUtils;
-import top.continew.starter.storage.util.OssUtils;
-import top.continew.starter.storage.util.StorageUtils;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.time.Duration;
-import java.time.LocalDateTime;
-import java.util.Base64;
-import java.util.List;
-import java.util.concurrent.CompletionException;
-
-/**
- * OSS存储策略
- *
- * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.gnu.org/licenses/lgpl.html
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package top.continew.starter.storage.util;
-
-import cn.hutool.core.text.CharSequenceUtil;
-import software.amazon.awssdk.regions.Region;
-import top.continew.starter.core.constant.StringConstants;
-import top.continew.starter.storage.constant.StorageConstant;
-
-/**
- * OSS 工具
- *
- * @author echo
- * @since 2.9.0
- */
-public class OssUtils {
- public OssUtils() {
- }
-
- /**
- * 获取作用域
- * 如果 region 参数非空,使用 Region.of 方法创建对应的 S3 区域对象,否则返回默认区域
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.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.LocalStorageConfig;
+import top.continew.starter.storage.autoconfigure.properties.StorageProperties;
+import top.continew.starter.storage.router.StorageStrategyRegistrar;
+import top.continew.starter.storage.strategy.StorageStrategy;
+import top.continew.starter.storage.strategy.impl.LocalStorageStrategy;
+
+import java.util.List;
+
+/**
+ * 本地存储自动配置
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+@ConditionalOnProperty(prefix = PropertiesConstants.STORAGE, name = "local")
+public class LocalStorageAutoConfiguration implements StorageStrategyRegistrar {
+
+ private final StorageProperties storageProperties;
+
+ public LocalStorageAutoConfiguration(StorageProperties storageProperties) {
+ this.storageProperties = storageProperties;
+ }
+
+ /**
+ * 注册配置策略
+ *
+ * @param strategies 策略列表
+ */
+ @Bean
+ @Override
+ public void register(List
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.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.StorageProperties;
+import top.continew.starter.storage.router.StorageStrategyRegistrar;
+import top.continew.starter.storage.strategy.StorageStrategy;
+import top.continew.starter.storage.strategy.impl.OssStorageStrategy;
+
+import java.util.List;
+
+/**
+ * s3存储自动配置
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+@ConditionalOnProperty(prefix = PropertiesConstants.STORAGE, name = "s3")
+public class S3StorageAutoConfiguration implements StorageStrategyRegistrar {
+
+ private final StorageProperties properties;
+
+ public S3StorageAutoConfiguration(StorageProperties properties) {
+ this.properties = properties;
+ }
+
+ /**
+ * 注册配置策略
+ *
+ * @param strategies 策略列表
+ */
+ @Override
+ @Bean
+ public void register(List
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.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.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+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.service.FileRecorder;
+import top.continew.starter.storage.service.impl.DefaultFileRecorder;
+import top.continew.starter.storage.strategy.StorageStrategyOverride;
+
+import java.util.List;
+
+/**
+ * 存储自动配置
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(StorageProperties.class)
+@Import({ProcessorRegistry.class, StrategyProxyFactory.class})
+public class StorageAutoConfiguration {
+
+ private static final Logger log = LoggerFactory.getLogger(StorageAutoConfiguration.class);
+
+ private final StorageProperties properties;
+
+ public StorageAutoConfiguration(StorageProperties properties) {
+ this.properties = properties;
+ }
+
+ /**
+ * 策略路由器
+ *
+ * @param registrars 注册
+ * @return {@link StorageStrategyRouter }
+ */
+ @Bean
+ public StorageStrategyRouter strategyRouter(List
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.autoconfigure.properties;
+
+/**
+ * 本地存储配置
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class LocalStorageConfig {
+
+ /**
+ * 是否启用
+ */
+ private boolean enabled;
+
+ /**
+ * 存储平台
+ */
+ private String platform;
+
+ /**
+ * 存储路径
+ */
+ private String bucketName;
+
+ /**
+ * 访问路径
+ */
+ private String endpoint;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getPlatform() {
+ return platform;
+ }
+
+ public void setPlatform(String platform) {
+ this.platform = platform;
+ }
+
+ public String getBucketName() {
+ return bucketName;
+ }
+
+ public void setBucketName(String bucketName) {
+ this.bucketName = bucketName;
+ }
+
+ public String getEndpoint() {
+ return endpoint;
+ }
+
+ public void setEndpoint(String endpoint) {
+ this.endpoint = endpoint;
+ }
+}
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/S3StorageConfig.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/S3StorageConfig.java
new file mode 100644
index 00000000..9088c068
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/S3StorageConfig.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.autoconfigure.properties;
+
+/**
+ * s3存储配置
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class S3StorageConfig {
+
+ /**
+ * 是否启用
+ */
+ private boolean enabled;
+
+ /**
+ * 唯一存储码
+ */
+ private String platform;
+
+ /**
+ * 连接地址 带 http://
+ */
+ private String endpoint;
+
+ /**
+ * 访问密钥
+ */
+ private String accessKey;
+
+ /**
+ * 密钥
+ */
+ private String secretKey;
+
+ /**
+ * 默认桶
+ */
+ private String bucketName;
+
+ /**
+ * 区域
+ */
+ private String region;
+
+ /**
+ * 自定义域名
+ */
+ private String domain;
+
+ /**
+ * 是否启用传输加速
+ */
+ private boolean transferAccelerationEnabled = false;
+
+ /**
+ * 多部分上传阈值(字节)
+ */
+ private long multipartUploadThreshold = 5 * 1024 * 1024; // 5MB
+
+ /**
+ * 多部分上传的部分大小(字节)
+ */
+ private long multipartUploadPartSize = 5 * 1024 * 1024; // 5MB
+
+ /**
+ * 请求超时时间(秒)
+ */
+ private int requestTimeout = 30;
+
+ /**
+ * 默认的对象ACL
+ */
+ private String defaultAcl = "private";
+
+ /**
+ * 是否启用路径样式访问
+ */
+ private boolean pathStyleAccessEnabled = false;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getPlatform() {
+ return platform;
+ }
+
+ public void setPlatform(String platform) {
+ this.platform = platform;
+ }
+
+ public String getEndpoint() {
+ return endpoint;
+ }
+
+ public void setEndpoint(String endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ public String getAccessKey() {
+ return accessKey;
+ }
+
+ public void setAccessKey(String accessKey) {
+ this.accessKey = accessKey;
+ }
+
+ public String getSecretKey() {
+ return secretKey;
+ }
+
+ public void setSecretKey(String secretKey) {
+ this.secretKey = secretKey;
+ }
+
+ public String getBucketName() {
+ return bucketName;
+ }
+
+ public void setBucketName(String bucketName) {
+ this.bucketName = bucketName;
+ }
+
+ public String getRegion() {
+ return region;
+ }
+
+ public void setRegion(String region) {
+ this.region = region;
+ }
+
+ public String getDomain() {
+ return domain;
+ }
+
+ public void setDomain(String domain) {
+ this.domain = domain;
+ }
+
+ public boolean isTransferAccelerationEnabled() {
+ return transferAccelerationEnabled;
+ }
+
+ public void setTransferAccelerationEnabled(boolean transferAccelerationEnabled) {
+ this.transferAccelerationEnabled = transferAccelerationEnabled;
+ }
+
+ public long getMultipartUploadThreshold() {
+ return multipartUploadThreshold;
+ }
+
+ public void setMultipartUploadThreshold(long multipartUploadThreshold) {
+ this.multipartUploadThreshold = multipartUploadThreshold;
+ }
+
+ public long getMultipartUploadPartSize() {
+ return multipartUploadPartSize;
+ }
+
+ public void setMultipartUploadPartSize(long multipartUploadPartSize) {
+ this.multipartUploadPartSize = multipartUploadPartSize;
+ }
+
+ public int getRequestTimeout() {
+ return requestTimeout;
+ }
+
+ public void setRequestTimeout(int requestTimeout) {
+ this.requestTimeout = requestTimeout;
+ }
+
+ public String getDefaultAcl() {
+ return defaultAcl;
+ }
+
+ public void setDefaultAcl(String defaultAcl) {
+ this.defaultAcl = defaultAcl;
+ }
+
+ public boolean isPathStyleAccessEnabled() {
+ return pathStyleAccessEnabled;
+ }
+
+ public void setPathStyleAccessEnabled(boolean pathStyleAccessEnabled) {
+ this.pathStyleAccessEnabled = pathStyleAccessEnabled;
+ }
+}
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/StorageProperties.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/StorageProperties.java
new file mode 100644
index 00000000..41ed9a3f
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/autoconfigure/properties/StorageProperties.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.autoconfigure.properties;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import top.continew.starter.core.constant.PropertiesConstants;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 存储属性
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+@ConfigurationProperties(prefix = PropertiesConstants.STORAGE)
+public class StorageProperties {
+
+ /**
+ * 默认使用的存储平台
+ */
+ private String defaultPlatform = "local";
+
+ /**
+ * 本地存储配置列表
+ */
+ private List
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.core;
+
+import org.springframework.web.multipart.MultipartFile;
+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.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;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 增强版文件存储服务
+ * 支持链式调用和更多功能
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class FileStorageService {
+
+ private final StorageStrategyRouter router;
+ private final StorageProperties storageProperties;
+ private final ProcessorRegistry processorRegistry;
+ private final StrategyProxyFactory proxyFactory;
+ private final FileRecorder fileRecorder;
+
+ 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;
+ }
+
+ /**
+ * 获取默认存储平台
+ */
+ public String getDefaultPlatform() {
+ return storageProperties.getDefaultPlatform();
+ }
+
+ /**
+ * 获取处理器注册表
+ */
+ public ProcessorRegistry getProcessorRegistry() {
+ return processorRegistry;
+ }
+
+ /**
+ * 创建上传预处理器(链式调用入口)
+ */
+ public UploadPretreatment of(MultipartFile file) {
+ return new UploadPretreatment(this, file);
+ }
+
+ /**
+ * 创建上传预处理器,指定平台
+ */
+ public UploadPretreatment of(MultipartFile file, String platform) {
+ return new UploadPretreatment(this, file).setPlatform(platform);
+ }
+
+ /**
+ * 创建上传预处理器(支持 byte[])
+ */
+ public UploadPretreatment of(byte[] bytes, String filename, String contentType) {
+ FileWrapper wrapper = FileWrapper.of(bytes, filename, contentType);
+ return new UploadPretreatment(this, wrapper.toMultipartFile());
+ }
+
+ /**
+ * 创建上传预处理器(支持 InputStream)
+ */
+ public UploadPretreatment of(InputStream inputStream, String filename, String contentType) {
+ FileWrapper wrapper = FileWrapper.of(inputStream, filename, contentType);
+ return new UploadPretreatment(this, wrapper.toMultipartFile());
+ }
+
+ /**
+ * 创建上传预处理器(支持任意对象)
+ */
+ 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());
+ }
+
+ /**
+ * 执行上传(内部方法)
+ */
+ public FileInfo doUpload(UploadContext context) {
+ StorageStrategy strategy = getStrategy(context.getPlatform());
+
+ // 执行上传
+ strategy.upload(context.getBucket(), context.getFullPath(), context.getFile());
+
+ // 构建文件信息
+ FileInfo fileInfo = strategy.getFileInfo(context.getBucket(), context.getFullPath());
+ fileInfo.setOriginalFileName(context.getFile().getOriginalFilename());
+ fileInfo.getMetadata().putAll(context.getMetadata());
+
+ // 保存文件记录
+ if (fileRecorder != null) {
+ fileRecorder.save(fileInfo);
+ }
+
+ return fileInfo;
+ }
+
+ /**
+ * 初始化分片上传
+ *
+ * @param bucket 存储桶
+ * @param platform 平台
+ * @param path 路径
+ * @param contentType 内容类型
+ * @param metadata 元数据
+ * @return {@link MultipartInitResp }
+ */
+ public MultipartInitResp initMultipartUpload(String bucket,
+ String platform,
+ String path,
+ String contentType,
+ Map
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.core;
+
+import 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 java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 文件包装器,用于统一处理不同类型的输入
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class FileWrapper {
+
+ private MultipartFile multipartFile;
+ private byte[] bytes;
+ private InputStream inputStream;
+ private String originalFilename;
+ private String contentType;
+ private long size;
+
+ private FileWrapper() {
+ }
+
+ /**
+ * 从 MultipartFile 创建
+ */
+ public static FileWrapper of(MultipartFile file) {
+ FileWrapper wrapper = new FileWrapper();
+ wrapper.multipartFile = file;
+ wrapper.originalFilename = file.getOriginalFilename();
+ wrapper.contentType = file.getContentType();
+ wrapper.size = file.getSize();
+ return wrapper;
+ }
+
+ /**
+ * 从 byte[] 创建
+ */
+ public static FileWrapper of(byte[] bytes, String filename, String contentType) {
+ if (filename == null || filename.trim().isEmpty()) {
+ throw new StorageException("文件名不能为空");
+ }
+ if (contentType == null || contentType.trim().isEmpty()) {
+ throw new StorageException("文件类型不能为空");
+ }
+
+ FileWrapper wrapper = new FileWrapper();
+ wrapper.bytes = bytes;
+ wrapper.originalFilename = filename;
+ wrapper.contentType = contentType;
+ wrapper.size = bytes.length;
+ return wrapper;
+ }
+
+ /**
+ * 从 InputStream 创建
+ */
+ public static FileWrapper of(InputStream inputStream, String filename, String contentType) {
+ if (filename == null || filename.trim().isEmpty()) {
+ throw new StorageException("文件名不能为空");
+ }
+ if (contentType == null || contentType.trim().isEmpty()) {
+ throw new StorageException("文件类型不能为空");
+ }
+
+ FileWrapper wrapper = new FileWrapper();
+ wrapper.inputStream = inputStream;
+ wrapper.originalFilename = filename;
+ wrapper.contentType = contentType;
+ wrapper.size = -1;
+ return wrapper;
+ }
+
+ /**
+ * 从 Object 创建(智能识别)
+ */
+ public static FileWrapper of(Object obj) {
+ return of(obj, null, null);
+ }
+
+ /**
+ * 从 Object 创建,可指定文件名和类型
+ */
+ public static FileWrapper of(Object obj, String filename, String contentType) {
+ if (obj == null) {
+ throw new StorageException("对象不能为空");
+ }
+
+ // 如果是 MultipartFile,直接处理
+ if (obj instanceof MultipartFile) {
+ return of((MultipartFile)obj);
+ }
+
+ // 如果是 byte[]
+ if (obj instanceof byte[]) {
+ if (filename == null || contentType == null) {
+ throw new StorageException("byte[] 类型必须指定文件名和文件类型");
+ }
+ return of((byte[])obj, filename, contentType);
+ }
+
+ // 如果是 InputStream
+ if (obj instanceof InputStream) {
+ if (filename == null || contentType == null) {
+ throw new StorageException("InputStream 类型必须指定文件名和文件类型");
+ }
+ return of((InputStream)obj, filename, contentType);
+ }
+
+ // 其他对象,转换为 JSON
+ 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";
+
+ return of(jsonBytes, finalFilename, finalContentType);
+ }
+
+ /**
+ * 转换为 MultipartFile
+ */
+ public MultipartFile toMultipartFile() {
+ if (multipartFile != null) {
+ return multipartFile;
+ }
+
+ if (bytes != null) {
+ return new MockMultipartFile(getFilenameWithoutExtension(originalFilename), originalFilename, contentType, bytes);
+ }
+
+ if (inputStream != null) {
+ try {
+ byte[] data = inputStream.readAllBytes();
+ return new MockMultipartFile(getFilenameWithoutExtension(originalFilename), originalFilename, contentType, data);
+ } catch (IOException e) {
+ throw new StorageException("读取输入流失败", e);
+ }
+ }
+
+ throw new IllegalStateException("无法转换为 MultipartFile");
+ }
+
+ private static String getFilenameWithoutExtension(String filename) {
+ int lastDotIndex = filename.lastIndexOf('.');
+ return lastDotIndex > 0 ? filename.substring(0, lastDotIndex) : filename;
+ }
+
+ private static String convertToJson(Object obj) {
+ try {
+ return JSONUtils.toJsonStr(obj);
+ } catch (Exception e) {
+ throw new StorageException("对象转换为 JSON 失败", e);
+ }
+ }
+
+ public String getOriginalFilename() {
+ return originalFilename;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public long getSize() {
+ return size;
+ }
+}
\ No newline at end of file
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/core/ProcessorRegistry.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/core/ProcessorRegistry.java
new file mode 100644
index 00000000..dd63d6b3
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/core/ProcessorRegistry.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.core;
+
+import top.continew.starter.storage.prehandle.*;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * 全局处理器注册表
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class ProcessorRegistry {
+
+ // 全局处理器
+ private final Map
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.core;
+
+import top.continew.starter.storage.strategy.StorageStrategy;
+import top.continew.starter.storage.strategy.StorageStrategyOverride;
+import top.continew.starter.storage.strategy.impl.AbstractStorageStrategyOverride;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * 策略代理工厂
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class StrategyProxyFactory {
+
+ private final Map
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.core;
+
+import 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 java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 上传预处理器,支持链式调用
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class UploadPretreatment {
+
+ private final FileStorageService storageService;
+ private final UploadContext context;
+ private final List
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.exception;
+
+import top.continew.starter.core.exception.BaseException;
+
+import java.io.Serial;
+
+/**
+ * 存储异常
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class StorageException extends BaseException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public StorageException() {
+ }
+
+ public StorageException(String message) {
+ super(message);
+ }
+
+ public StorageException(Throwable cause) {
+ super(cause);
+ }
+
+ public StorageException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/context/UploadContext.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/model/context/UploadContext.java
new file mode 100644
index 00000000..5ebb18be
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/model/context/UploadContext.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.model.context;
+
+import org.springframework.web.multipart.MultipartFile;
+import top.continew.starter.storage.model.req.ThumbnailSize;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 上传上下文,包含上传过程中的所有信息
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class UploadContext {
+
+ /**
+ * 原始文件
+ */
+ private MultipartFile file;
+
+ /**
+ * 存储平台代码
+ */
+ private String platform;
+
+ /**
+ * 桶名
+ */
+ private String bucket;
+
+ /**
+ * 文件路径(不含文件名)
+ */
+ private String path;
+
+ /**
+ * 文件名
+ */
+ private String fileName;
+
+ /**
+ * 是否生成缩略图
+ */
+ private boolean generateThumbnail;
+
+ /**
+ * 缩略图尺寸
+ */
+ private ThumbnailSize thumbnailSize;
+
+ /**
+ * 元数据
+ */
+ private Map
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.model.req;
+
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+
+/**
+ * 内存中的 MultipartFile 实现,适用于无需真实 HTTP 上传场景。
+ *
+ * 可用于接口调用中构造文件参数,如将字节数组、输入流包装成 MultipartFile,
+ * 以便复用上传逻辑或兼容已有的文件处理接口。
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class MockMultipartFile implements MultipartFile {
+ private final String name;
+ private final String originalFilename;
+ private final String contentType;
+ private final byte[] content;
+
+ public MockMultipartFile(String name, String originalFilename, String contentType, byte[] content) {
+ this.name = name;
+ this.originalFilename = originalFilename;
+ this.contentType = contentType;
+ this.content = content;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getOriginalFilename() {
+ return originalFilename;
+ }
+
+ @Override
+ public String getContentType() {
+ return contentType;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return content == null || content.length == 0;
+ }
+
+ @Override
+ public long getSize() {
+ return content != null ? content.length : 0;
+ }
+
+ @Override
+ public byte[] getBytes() {
+ return content;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new ByteArrayInputStream(content);
+ }
+
+ @Override
+ public void transferTo(File dest) throws IOException {
+ Files.write(dest.toPath(), content);
+ }
+
+}
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/req/ThumbnailInfo.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/model/req/ThumbnailInfo.java
new file mode 100644
index 00000000..a3fd3c0a
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/model/req/ThumbnailInfo.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.model.req;
+
+/**
+ * 缩略图信息
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class ThumbnailInfo {
+ private byte[] data;
+ private String format;
+ private int width;
+ private int height;
+
+ public byte[] getData() {
+ return data;
+ }
+
+ public void setData(byte[] data) {
+ this.data = data;
+ }
+
+ public String getFormat() {
+ return format;
+ }
+
+ public void setFormat(String format) {
+ this.format = format;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public void setHeight(int height) {
+ this.height = height;
+ }
+
+ // getter/setter省略
+}
\ No newline at end of file
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/req/ThumbnailSize.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/model/req/ThumbnailSize.java
new file mode 100644
index 00000000..a6414bca
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/model/req/ThumbnailSize.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.model.req;
+
+/**
+ * 缩略图尺寸
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class ThumbnailSize {
+ private int width;
+ private int height;
+ private boolean keepAspectRatio = true;
+
+ public ThumbnailSize(int width, int height) {
+ this.width = width;
+ this.height = height;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public void setHeight(int height) {
+ this.height = height;
+ }
+
+ public boolean isKeepAspectRatio() {
+ return keepAspectRatio;
+ }
+
+ public void setKeepAspectRatio(boolean keepAspectRatio) {
+ this.keepAspectRatio = keepAspectRatio;
+ }
+}
\ No newline at end of file
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/FileInfo.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/FileInfo.java
new file mode 100644
index 00000000..bc5de7f5
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/FileInfo.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.model.resp;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+/**
+ * 文件信息
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class FileInfo {
+
+ /**
+ * 平台
+ */
+ private String platform;
+
+ /**
+ * 文件id
+ */
+ private String fileId;
+
+ /**
+ * 存储桶
+ */
+ private String bucket;
+
+ /**
+ * 路径
+ */
+ private String path;
+
+ /**
+ * 原始文件名
+ */
+ private String originalFileName;
+
+ /**
+ * 文件名
+ */
+ private String name;
+
+ /**
+ * 缩略图路径
+ */
+ private String thumbnailPath;
+
+ /**
+ * 完整路径
+ */
+ private String fullPath;
+
+ /**
+ * 文件大小
+ */
+ private Long size;
+
+ /**
+ * 内容类型
+ */
+ private String contentType;
+
+ /**
+ * 访问 url
+ */
+ private String url;
+
+ /**
+ * 上传时间
+ */
+ private LocalDateTime uploadTime;
+
+ /**
+ * 元数据
+ */
+ private Map
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.model.resp;
+
+import java.time.LocalDateTime;
+
+/**
+ * 文件分片信息
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class FilePartInfo {
+ /**
+ * 文件ID
+ */
+ private String fileId;
+
+ /**
+ * 分片编号(从1开始)
+ */
+ private Integer partNumber;
+
+ /**
+ * 分片大小
+ */
+ private Long partSize;
+
+ /**
+ * 分片MD5
+ */
+ private String partMd5;
+
+ /**
+ * 分片ETag(S3返回的标识)
+ */
+ private String partETag;
+
+ /**
+ * 上传ID(S3分片上传标识)
+ */
+ private String uploadId;
+
+ /**
+ * 上传时间
+ */
+ private LocalDateTime uploadTime;
+
+ /**
+ * 状态:UPLOADING, SUCCESS, FAILED
+ */
+ private String status;
+
+ /**
+ * 存储桶
+ */
+ private String bucket;
+
+ /**
+ * 文件路径
+ */
+ private String path;
+
+ public String getFileId() {
+ return fileId;
+ }
+
+ public void setFileId(String fileId) {
+ this.fileId = fileId;
+ }
+
+ public Integer getPartNumber() {
+ return partNumber;
+ }
+
+ public void setPartNumber(Integer partNumber) {
+ this.partNumber = partNumber;
+ }
+
+ public Long getPartSize() {
+ return partSize;
+ }
+
+ public void setPartSize(Long partSize) {
+ this.partSize = partSize;
+ }
+
+ public String getPartMd5() {
+ return partMd5;
+ }
+
+ public void setPartMd5(String partMd5) {
+ this.partMd5 = partMd5;
+ }
+
+ public String getPartETag() {
+ return partETag;
+ }
+
+ public void setPartETag(String partETag) {
+ this.partETag = partETag;
+ }
+
+ public String getUploadId() {
+ return uploadId;
+ }
+
+ public void setUploadId(String uploadId) {
+ this.uploadId = uploadId;
+ }
+
+ public LocalDateTime getUploadTime() {
+ return uploadTime;
+ }
+
+ public void setUploadTime(LocalDateTime uploadTime) {
+ this.uploadTime = uploadTime;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getBucket() {
+ return bucket;
+ }
+
+ public void setBucket(String bucket) {
+ this.bucket = bucket;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+}
\ No newline at end of file
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/MultipartInitResp.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/MultipartInitResp.java
new file mode 100644
index 00000000..124bd124
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/MultipartInitResp.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.model.resp;
+
+/**
+ * 分片上传初始化结果
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class MultipartInitResp {
+ /**
+ * 文件ID
+ */
+ private String fileId;
+
+ /**
+ * 上传ID(S3返回的uploadId)
+ */
+ private String uploadId;
+
+ /**
+ * 存储桶
+ */
+ private String bucket;
+
+ /**
+ * 存储平台
+ */
+ private String platform;
+
+ /**
+ * 文件路径
+ */
+ private String path;
+
+ /**
+ * 分片大小
+ */
+ private Long partSize;
+
+ /**
+ * 总分片数
+ */
+ private Integer totalParts;
+
+ public String getFileId() {
+ return fileId;
+ }
+
+ public void setFileId(String fileId) {
+ this.fileId = fileId;
+ }
+
+ public String getUploadId() {
+ return uploadId;
+ }
+
+ public void setUploadId(String uploadId) {
+ this.uploadId = uploadId;
+ }
+
+ public String getBucket() {
+ return bucket;
+ }
+
+ public void setBucket(String bucket) {
+ this.bucket = bucket;
+ }
+
+ public String getPlatform() {
+ return platform;
+ }
+
+ public void setPlatform(String platform) {
+ this.platform = platform;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public Long getPartSize() {
+ return partSize;
+ }
+
+ public void setPartSize(Long partSize) {
+ this.partSize = partSize;
+ }
+
+ public Integer getTotalParts() {
+ return totalParts;
+ }
+
+ public void setTotalParts(Integer totalParts) {
+ this.totalParts = totalParts;
+ }
+}
\ No newline at end of file
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/MultipartUploadResp.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/MultipartUploadResp.java
new file mode 100644
index 00000000..e5e7d676
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/MultipartUploadResp.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.model.resp;
+
+/**
+ * 分片上传结果
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class MultipartUploadResp {
+ /**
+ * 分片编号
+ */
+ private Integer partNumber;
+
+ /**
+ * 分片ETag
+ */
+ private String partETag;
+
+ /**
+ * 分片大小
+ */
+ private Long partSize;
+
+ /**
+ * 是否成功
+ */
+ private boolean success;
+
+ /**
+ * 错误信息
+ */
+ private String errorMessage;
+
+ public Integer getPartNumber() {
+ return partNumber;
+ }
+
+ public void setPartNumber(Integer partNumber) {
+ this.partNumber = partNumber;
+ }
+
+ public String getPartETag() {
+ return partETag;
+ }
+
+ public void setPartETag(String partETag) {
+ this.partETag = partETag;
+ }
+
+ public Long getPartSize() {
+ return partSize;
+ }
+
+ public void setPartSize(Long partSize) {
+ this.partSize = partSize;
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public void setSuccess(boolean success) {
+ this.success = success;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+}
\ No newline at end of file
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/StrategyStatus.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/StrategyStatus.java
new file mode 100644
index 00000000..1e3592e1
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/model/resp/StrategyStatus.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.model.resp;
+
+/**
+ * 存储策略状态
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class StrategyStatus {
+
+ /**
+ * 平台
+ */
+ private String platform;
+
+ /**
+ * 是否有配置文件策略
+ */
+ private boolean hasConfig;
+ /**
+ * 是否有动态策略
+ */
+ private boolean hasDynamic;
+
+ /**
+ * 当前生效的类型:"CONFIG" 或 "DYNAMIC"
+ */
+ private String activeType;
+
+ /**
+ * 描述
+ */
+ private String description;
+
+ public StrategyStatus(String platform,
+ boolean hasConfig,
+ boolean hasDynamic,
+ String activeType,
+ String description) {
+ this.platform = platform;
+ this.hasConfig = hasConfig;
+ this.hasDynamic = hasDynamic;
+ this.activeType = activeType;
+ this.description = description;
+ }
+
+ public String getPlatform() {
+ return platform;
+ }
+
+ public void setPlatform(String platform) {
+ this.platform = platform;
+ }
+
+ public boolean isHasConfig() {
+ return hasConfig;
+ }
+
+ public void setHasConfig(boolean hasConfig) {
+ this.hasConfig = hasConfig;
+ }
+
+ public boolean isHasDynamic() {
+ return hasDynamic;
+ }
+
+ public void setHasDynamic(boolean hasDynamic) {
+ this.hasDynamic = hasDynamic;
+ }
+
+ public String getActiveType() {
+ return activeType;
+ }
+
+ public void setActiveType(String activeType) {
+ this.activeType = activeType;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+}
diff --git a/continew-starter-storage/continew-starter-storage-core/src/main/java/top/continew/starter/storage/dao/impl/StorageDaoDefaultImpl.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/FileNameGenerator.java
similarity index 58%
rename from continew-starter-storage/continew-starter-storage-core/src/main/java/top/continew/starter/storage/dao/impl/StorageDaoDefaultImpl.java
rename to continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/FileNameGenerator.java
index 8114b02b..9f5a4180 100644
--- a/continew-starter-storage/continew-starter-storage-core/src/main/java/top/continew/starter/storage/dao/impl/StorageDaoDefaultImpl.java
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/FileNameGenerator.java
@@ -14,21 +14,24 @@
* limitations under the License.
*/
-package top.continew.starter.storage.dao.impl;
+package top.continew.starter.storage.prehandle;
-import top.continew.starter.storage.dao.StorageDao;
-import top.continew.starter.storage.model.resp.UploadResp;
+import top.continew.starter.storage.model.context.UploadContext;
+import top.continew.starter.storage.service.FileProcessor;
/**
- * 存储记录持久层接口默认实现
- * 此类并不能真正保存记录,只是用来脱离数据库运行,保证文件上传功能可以正常使用
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.prehandle;
+
+import top.continew.starter.storage.model.context.UploadContext;
+import top.continew.starter.storage.service.FileProcessor;
+
+/**
+ * 文件路径生成器
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public interface FilePathGenerator extends FileProcessor {
+
+ /**
+ * 生成路径
+ *
+ * @param context 上下文
+ * @return {@link String }
+ */
+ String path(UploadContext context);
+}
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/FileValidator.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/FileValidator.java
new file mode 100644
index 00000000..d08347a6
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/FileValidator.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.prehandle;
+
+import top.continew.starter.storage.exception.StorageException;
+import top.continew.starter.storage.model.context.UploadContext;
+import top.continew.starter.storage.service.FileProcessor;
+
+/**
+ * 文件验证器
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public interface FileValidator extends FileProcessor {
+
+ /**
+ * 验证文件
+ *
+ * @param context 上传上下文
+ * @throws StorageException 验证失败时抛出异常
+ */
+ void validate(UploadContext context) throws StorageException;
+}
\ No newline at end of file
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/ThumbnailProcessor.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/ThumbnailProcessor.java
new file mode 100644
index 00000000..c66717ae
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/ThumbnailProcessor.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.prehandle;
+
+import top.continew.starter.storage.model.context.UploadContext;
+import top.continew.starter.storage.model.req.ThumbnailInfo;
+import top.continew.starter.storage.service.FileProcessor;
+
+import java.io.InputStream;
+
+/**
+ * 缩略图处理器
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public interface ThumbnailProcessor extends FileProcessor {
+
+ /**
+ * 生成缩略图
+ *
+ * @param context 上传上下文
+ * @param sourceInputStream 原始文件流
+ * @return 缩略图信息
+ */
+ ThumbnailInfo process(UploadContext context, InputStream sourceInputStream);
+}
\ No newline at end of file
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/UploadCompleteProcessor.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/UploadCompleteProcessor.java
new file mode 100644
index 00000000..ac34bb4e
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/UploadCompleteProcessor.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.prehandle;
+
+import top.continew.starter.storage.model.resp.FileInfo;
+import top.continew.starter.storage.service.FileProcessor;
+
+/**
+ * 上传完成处理器
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public interface UploadCompleteProcessor extends FileProcessor {
+
+ /**
+ * 处理上传完成事件
+ *
+ * @param fileInfo 文件信息
+ */
+ void onComplete(FileInfo fileInfo);
+}
\ No newline at end of file
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultFileNameGenerator.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultFileNameGenerator.java
new file mode 100644
index 00000000..fc32f683
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultFileNameGenerator.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.prehandle.impl;
+
+import top.continew.starter.storage.model.context.UploadContext;
+import top.continew.starter.storage.prehandle.FileNameGenerator;
+import top.continew.starter.storage.util.StorageUtils;
+
+/**
+ * 默认文件名生成器
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class DefaultFileNameGenerator implements FileNameGenerator {
+
+ @Override
+ public String getName() {
+ return "defaultFileName";
+ }
+
+ @Override
+ public boolean support(UploadContext context) {
+ return true;
+ }
+
+ @Override
+ public String generate(UploadContext context) {
+ return StorageUtils.generateFileName(context.getFile().getOriginalFilename());
+ }
+}
\ No newline at end of file
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultFilePathGenerator.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultFilePathGenerator.java
new file mode 100644
index 00000000..6bcf3dd5
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultFilePathGenerator.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.prehandle.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;
+
+/**
+ * 默认文件路径生成器
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class DefaultFilePathGenerator implements FilePathGenerator {
+
+ @Override
+ public String getName() {
+ return "defaultFilePath";
+ }
+
+ @Override
+ public boolean support(UploadContext context) {
+ return true;
+ }
+
+ @Override
+ public String path(UploadContext context) {
+ String path = context.getPath();
+ return StrUtil.isNotBlank(path) ? path : StorageUtils.generatePath();
+ }
+}
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultThumbnailProcessor.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultThumbnailProcessor.java
new file mode 100644
index 00000000..9a6393c1
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/DefaultThumbnailProcessor.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.prehandle.impl;
+
+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 javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+
+/**
+ * 默认缩略图处理器
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class DefaultThumbnailProcessor implements ThumbnailProcessor {
+
+ @Override
+ public String getName() {
+ return "defaultThumbnail";
+ }
+
+ @Override
+ public boolean support(UploadContext context) {
+ String contentType = context.getFile().getContentType();
+ return contentType != null && contentType.startsWith("image/");
+ }
+
+ @Override
+ public ThumbnailInfo process(UploadContext context, InputStream sourceInputStream) {
+ try {
+ ThumbnailSize size = context.getThumbnailSize();
+ BufferedImage thumbnail = Thumbnails.of(sourceInputStream)
+ .size(size.getWidth(), size.getHeight())
+ .keepAspectRatio(size.isKeepAspectRatio())
+ .asBufferedImage();
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ImageIO.write(thumbnail, "jpg", baos);
+
+ ThumbnailInfo info = new ThumbnailInfo();
+ info.setData(baos.toByteArray());
+ info.setFormat("jpg");
+ info.setWidth(thumbnail.getWidth());
+ info.setHeight(thumbnail.getHeight());
+
+ return info;
+ } catch (Exception e) {
+ throw new StorageException("生成缩略图失败", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/FileSizeValidator.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/FileSizeValidator.java
new file mode 100644
index 00000000..cf42c357
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/FileSizeValidator.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.prehandle.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;
+
+/**
+ * 文件大小验证器
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class FileSizeValidator implements FileValidator {
+
+ private final long maxSize;
+
+ public FileSizeValidator(MultipartProperties multipartProperties) {
+ this.maxSize = multipartProperties.getMaxFileSize().toBytes();
+ }
+
+ public FileSizeValidator() {
+ // 提供默认大小 10MB
+ this(10 * 1024 * 1024L);
+ }
+
+ public FileSizeValidator(long maxSize) {
+ this.maxSize = maxSize;
+ }
+
+ @Override
+ public String getName() {
+ return "fileSize";
+ }
+
+ @Override
+ public boolean support(UploadContext context) {
+ return true;
+ }
+
+ @Override
+ public void validate(UploadContext context) throws StorageException {
+ long fileSize = context.getFile().getSize();
+ if (fileSize > maxSize) {
+ throw new StorageException(String.format("文件大小超过限制: %s (当前: %s)", FileUtil
+ .readableFileSize(maxSize), FileUtil.readableFileSize(fileSize)));
+ }
+ }
+
+ /**
+ * 创建默认的文件大小验证器(10MB)
+ */
+ public FileSizeValidator create() {
+ return new FileSizeValidator(maxSize);
+ }
+
+ /**
+ * 创建指定大小的验证器
+ */
+ public static FileSizeValidator maxSize(long bytes) {
+ return new FileSizeValidator(bytes);
+ }
+
+ /**
+ * 创建指定KB大小的验证器
+ */
+ public static FileSizeValidator maxKB(long kb) {
+ return new FileSizeValidator(kb * 1024);
+ }
+
+ /**
+ * 创建指定MB大小的验证器
+ */
+ public static FileSizeValidator maxMB(long mb) {
+ return new FileSizeValidator(mb * 1024 * 1024);
+ }
+
+ /**
+ * 创建指定GB大小的验证器
+ */
+ public static FileSizeValidator maxGB(long gb) {
+ return new FileSizeValidator(gb * 1024L * 1024 * 1024);
+ }
+}
diff --git a/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/FileTypeValidator.java b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/FileTypeValidator.java
new file mode 100644
index 00000000..30bdfc86
--- /dev/null
+++ b/continew-starter-storage/src/main/java/top/continew/starter/storage/prehandle/impl/FileTypeValidator.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.prehandle.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 java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 文件类型验证器
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class FileTypeValidator implements FileValidator {
+
+ private final Set
+ * 主要针对配置文件
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.router;
+
+import top.continew.starter.storage.exception.StorageException;
+import top.continew.starter.storage.model.resp.StrategyStatus;
+import top.continew.starter.storage.strategy.StorageStrategy;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 存储策略路由器
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class StorageStrategyRouter {
+
+ /**
+ * 配置策略
+ */
+ private final Map
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.service;
+
+import top.continew.starter.storage.model.resp.FileInfo;
+import top.continew.starter.storage.model.resp.FilePartInfo;
+
+import java.util.List;
+
+/**
+ * 文件记录器接口,用于保存文件上传记录
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public interface FileRecorder {
+
+ /**
+ * 保存文件记录
+ *
+ * @param fileInfo 文件信息
+ * @return 是否保存成功
+ */
+ boolean save(FileInfo fileInfo);
+
+ /**
+ * 更新文件记录
+ *
+ * @param fileInfo 文件信息
+ * @return 是否更新成功
+ */
+ boolean update(FileInfo fileInfo);
+
+ /**
+ * 删除文件记录
+ *
+ * @param platform 存储平台
+ * @param path 文件路径
+ * @return 是否删除成功
+ */
+ boolean delete(String platform, String path);
+
+ /**
+ * 保存文件分片信息
+ *
+ * @param filePartInfo 文件分片信息
+ */
+ void saveFilePart(FilePartInfo filePartInfo);
+
+ /**
+ * 获取文件所有分片信息
+ *
+ * @param fileId 文件ID
+ * @return 分片信息列表
+ */
+ List
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.service.impl;
+
+import top.continew.starter.storage.model.resp.FileInfo;
+import top.continew.starter.storage.model.resp.FilePartInfo;
+import top.continew.starter.storage.service.FileRecorder;
+
+import java.util.List;
+
+/**
+ * 默认文件记录器
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class DefaultFileRecorder implements FileRecorder {
+ @Override
+ public boolean save(FileInfo fileInfo) {
+ return false;
+ }
+
+ @Override
+ public boolean update(FileInfo fileInfo) {
+ return false;
+ }
+
+ @Override
+ public boolean delete(String platform, String path) {
+ return false;
+ }
+
+ @Override
+ public void saveFilePart(FilePartInfo filePartInfo) {
+
+ }
+
+ @Override
+ public List
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.strategy;
+
+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 java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 存储策略
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public interface StorageStrategy {
+
+ /**
+ * 上传
+ *
+ * @param bucket 存储桶
+ * @param path 路径
+ * @param file 文件
+ */
+ void upload(String bucket, String path, MultipartFile file);
+
+ /**
+ * 下载
+ *
+ * @param bucket 存储桶
+ * @param path 路径
+ * @return {@link InputStream }
+ */
+ InputStream download(String bucket, String path);
+
+ /**
+ * 批量下载到zip
+ */
+ InputStream batchDownload(String bucket, List 目前只支持 阿里云-oss 华为云-obs 腾讯云-cos
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.strategy.impl;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.URLUtil;
+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.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.strategy.StorageStrategy;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.*;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.security.MessageDigest;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * 本地存储策略
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class LocalStorageStrategy implements StorageStrategy {
+
+ private static final Logger log = LoggerFactory.getLogger(LocalStorageStrategy.class);
+
+ private final LocalStorageConfig config;
+
+ // 分片上传临时目录
+ private final String TEMP_DIR = ".multipart";
+
+ public LocalStorageStrategy(LocalStorageConfig config) {
+ this.config = config;
+ initTempDir(config.getBucketName());
+ registerResources(config);
+ }
+
+ /**
+ * 注册资源
+ *
+ * @param config 配置
+ */
+ public void registerResources(LocalStorageConfig config) {
+ // 注册资源映射
+ SpringWebUtils.registerResourceHandler(MapUtil.of(URLUtil.url(config.getEndpoint()).getPath(), config
+ .getBucketName()));
+ }
+
+ /**
+ * 初始化临时目录
+ */
+ private void initTempDir(String bucket) {
+ Path tempPath = Paths.get(bucket, TEMP_DIR);
+ try {
+ Files.createDirectories(tempPath);
+ } catch (IOException e) {
+ log.error("创建临时目录失败", e);
+ }
+ }
+
+ @Override
+ public void upload(String bucket, String path, MultipartFile file) {
+ Path filePath = Paths.get(bucket, path);
+ try {
+ // 创建目录
+ Files.createDirectories(filePath.getParent());
+
+ // 复制文件
+ Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
+ } catch (Exception e) {
+ throw new StorageException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 下载文件
+ */
+ @Override
+ public InputStream download(String bucket, String path) {
+ Path filePath = Paths.get(bucket, path);
+ try {
+ return Files.newInputStream(filePath);
+ } catch (IOException e) {
+ throw new StorageException("本地读取失败: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public InputStream batchDownload(String bucket, List
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.strategy.impl;
+
+import cn.hutool.core.util.StrUtil;
+import org.slf4j.Logger;
+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.sync.RequestBody;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3Configuration;
+import software.amazon.awssdk.services.s3.model.*;
+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.strategy.StorageStrategy;
+import top.continew.starter.storage.util.StorageUtils;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * oss存储策略
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class OssStorageStrategy implements StorageStrategy {
+
+ private static final Logger log = LoggerFactory.getLogger(OssStorageStrategy.class);
+
+ private final S3Client s3Client;
+ private final S3Presigner s3Presigner;
+ private final S3StorageConfig config;
+
+ public OssStorageStrategy(S3StorageConfig config) {
+ this.config = config;
+ this.s3Client = createS3Client(config);
+ this.s3Presigner = createS3Presigner(config);
+ }
+
+ /**
+ * 获取客户端
+ *
+ * @return {@link S3Client }
+ */
+ public S3Client getClient() {
+ return s3Client;
+ }
+
+ /**
+ * 获取预签名者
+ *
+ * @return {@link S3Presigner }
+ */
+ public S3Presigner getPresigner() {
+ return s3Presigner;
+ }
+
+ /**
+ * 创建S3客户端
+ *
+ * @param config 配置
+ * @return {@link S3Client }
+ */
+ private S3Client createS3Client(S3StorageConfig config) {
+ // 登录认证账户密码
+ StaticCredentialsProvider auth = StaticCredentialsProvider.create(AwsBasicCredentials.create(config
+ .getAccessKey(), config.getSecretKey()));
+
+ return S3Client.builder()
+ .credentialsProvider(auth)
+ .endpointOverride(URI.create(config.getEndpoint()))
+ .region(StorageUtils.getRegion(config.getRegion()))
+ .build();
+ }
+
+ /**
+ * 创建S3预签名器
+ *
+ * @param config 配置
+ * @return {@link S3Presigner }
+ */
+ private S3Presigner createS3Presigner(S3StorageConfig config) {
+ StaticCredentialsProvider auth = StaticCredentialsProvider.create(AwsBasicCredentials.create(config
+ .getAccessKey(), config.getSecretKey()));
+
+ String domain = StrUtil.isNotBlank(config.getDomain()) ? config.getDomain() : config.getEndpoint();
+
+ return S3Presigner.builder()
+ .credentialsProvider(auth)
+ .endpointOverride(URI.create(domain))
+ .region(StorageUtils.getRegion(config.getRegion()))
+ .serviceConfiguration(S3Configuration.builder()
+ .pathStyleAccessEnabled(config.isPathStyleAccessEnabled())
+ .build())
+ .build();
+ }
+
+ @Override
+ public void upload(String bucket, String path, MultipartFile file) {
+ // 构建上传请求
+ PutObjectRequest.Builder requestBuilder = PutObjectRequest.builder()
+ .bucket(bucket)
+ .key(path)
+ .contentType(file.getContentType())
+ .contentLength(file.getSize());
+
+ try {
+ // 执行上传
+ s3Client.putObject(requestBuilder.build(), RequestBody.fromInputStream(file.getInputStream(), file
+ .getSize()));
+
+ } catch (Exception e) {
+ throw new StorageException("S3上传异常" + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 下载文件
+ */
+ @Override
+ public InputStream download(String bucket, String path) {
+ try {
+ return s3Client.getObject(GetObjectRequest.builder().bucket(bucket).key(path).build());
+ } catch (Exception e) {
+ throw new StorageException("S3下载失败: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public InputStream batchDownload(String bucket, List
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.starter.storage.util;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.StrUtil;
+import software.amazon.awssdk.regions.Region;
+
+import java.time.LocalDate;
+
+/**
+ * 存储工具
+ *
+ * @author echo
+ * @since 2.14.0
+ */
+public class StorageUtils {
+
+ /**
+ * 获取区域
+ */
+ public static Region getRegion(String region) {
+ return StrUtil.isEmpty(region) ? Region.US_EAST_1 : Region.of(region);
+ }
+
+ /**
+ * 生成默认路径:年/月/日/
+ */
+ public static String generatePath() {
+ LocalDate date = LocalDate.now();
+ return String.format("%d/%d/%d/", date.getYear(), date.getMonthValue(), date.getDayOfMonth());
+ }
+
+ /**
+ * 生成文件名:时间戳.扩展名
+ */
+ public static String generateFileName(String originalFilename) {
+ return generateFileName(originalFilename, null, false);
+ }
+
+ /**
+ * 生成文件名:前缀_时间戳.扩展名
+ */
+ public static String generateFileName(String originalFilename, String prefix) {
+ return generateFileName(originalFilename, prefix, false);
+ }
+
+ /**
+ * 生成文件名
+ *
+ * @param originalFilename 原始文件名
+ * @param prefix 前缀(可为null)
+ * @param useMillis 是否使用毫秒时间戳,false则使用格式化时间戳
+ */
+ public static String generateFileName(String originalFilename, String prefix, boolean useMillis) {
+ String extension = FileUtil.getSuffix(originalFilename);
+ String timestamp = useMillis
+ ? String.valueOf(System.currentTimeMillis())
+ : DateUtil.format(DateUtil.date(), "yyyyMMddHHmmssSSS");
+
+ String fileName = StrUtil.isNotBlank(prefix) ? prefix + "_" + timestamp : timestamp;
+ return fileName + (StrUtil.isNotBlank(extension) ? "." + extension : "");
+ }
+}
diff --git a/continew-starter-storage/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/continew-starter-storage/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000..d88d912f
--- /dev/null
+++ b/continew-starter-storage/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+top.continew.starter.storage.autoconfigure.StorageAutoConfiguration
\ No newline at end of file
> batches = partition(paths, 1000);
+
+ for (List
> partition(List
> partitions = new ArrayList<>();
+ for (int i = 0; i < list.size(); i += size) {
+ partitions.add(list.subList(i, Math.min(i + size, list.size())));
+ }
+ return partitions;
+ }
+
+ /**
+ * 初始化分片上传
+ */
+ @Override
+ public MultipartInitResp initMultipartUpload(String bucket,
+ String path,
+ String contentType,
+ Map