zhaojinyu 1 bulan lalu
melakukan
75a9ae9ace
100 mengubah file dengan 6559 tambahan dan 0 penghapusan
  1. 8 0
      .idea/.gitignore
  2. 21 0
      .idea/compiler.xml
  3. 11 0
      .idea/encodings.xml
  4. 20 0
      .idea/jarRepositories.xml
  5. 12 0
      .idea/misc.xml
  6. 4 0
      .idea/vcs.xml
  7. 83 0
      README.md
  8. 27 0
      jnpf-file-core-spring/pom.xml
  9. 13 0
      jnpf-file-core-spring/src/main/java/org/dromara/x/file/storage/spring/EnableFileStorage.java
  10. 190 0
      jnpf-file-core-spring/src/main/java/org/dromara/x/file/storage/spring/FileStorageAutoConfiguration.java
  11. 447 0
      jnpf-file-core-spring/src/main/java/org/dromara/x/file/storage/spring/SpringFileStorageProperties.java
  12. 65 0
      jnpf-file-core-spring/src/main/java/org/dromara/x/file/storage/spring/file/MultipartFileWrapper.java
  13. 33 0
      jnpf-file-core-spring/src/main/java/org/dromara/x/file/storage/spring/file/MultipartFileWrapperAdapter.java
  14. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/EnableFileStorage.class
  15. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/FileStorageAutoConfiguration$FileStorageLocalFileAccessAutoConfiguration$1.class
  16. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/FileStorageAutoConfiguration$FileStorageLocalFileAccessAutoConfiguration.class
  17. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/FileStorageAutoConfiguration.class
  18. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringAliyunOssConfig.class
  19. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringAmazonS3Config.class
  20. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringAzureBlobStorageConfig.class
  21. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringBaiduBosConfig.class
  22. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringFastDfsConfig.class
  23. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringFtpConfig.class
  24. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringGoogleCloudStorageConfig.class
  25. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringHuaweiObsConfig.class
  26. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringLocalConfig.class
  27. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringLocalPlusConfig.class
  28. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringMinioConfig.class
  29. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringQiniuKodoConfig.class
  30. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringSftpConfig.class
  31. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringTencentCosConfig.class
  32. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringUpyunUssConfig.class
  33. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringWebDavConfig.class
  34. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties.class
  35. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/file/MultipartFileWrapper.class
  36. TEMPAT SAMPAH
      jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/file/MultipartFileWrapperAdapter.class
  37. TEMPAT SAMPAH
      jnpf-file-core-spring/target/jnpf-file-core-spring-6.0.0-RELEASE.jar
  38. 3 0
      jnpf-file-core-spring/target/maven-archiver/pom.properties
  39. 23 0
      jnpf-file-core-spring/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
  40. 5 0
      jnpf-file-core-spring/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
  41. 187 0
      jnpf-file-core/pom.xml
  42. 139 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/Downloader.java
  43. 178 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/FileInfo.java
  44. 1081 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/FileStorageProperties.java
  45. 740 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/FileStorageService.java
  46. 619 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/FileStorageServiceBuilder.java
  47. 36 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/IOExceptionConsumer.java
  48. 10 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/IOExceptionFunction.java
  49. 131 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/InputStreamPlus.java
  50. 65 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/ProgressInputStream.java
  51. 70 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/ProgressListener.java
  52. 126 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/ProgressListenerSetter.java
  53. 829 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/UploadPretreatment.java
  54. 37 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/AbortMultipartUploadAspectChain.java
  55. 13 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/AbortMultipartUploadAspectChainCallback.java
  56. 44 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/CompleteMultipartUploadAspectChain.java
  57. 18 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/CompleteMultipartUploadAspectChainCallback.java
  58. 37 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/CopyAspectChain.java
  59. 13 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/CopyAspectChainCallback.java
  60. 35 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/DeleteAspectChain.java
  61. 12 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/DeleteAspectChainCallback.java
  62. 36 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/DownloadAspectChain.java
  63. 13 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/DownloadAspectChainCallback.java
  64. 36 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/DownloadThAspectChain.java
  65. 13 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/DownloadThAspectChainCallback.java
  66. 34 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/ExistsAspectChain.java
  67. 11 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/ExistsAspectChainCallback.java
  68. 271 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/FileStorageAspect.java
  69. 36 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/GeneratePresignedUrlAspectChain.java
  70. 12 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/GeneratePresignedUrlAspectChainCallback.java
  71. 36 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/GenerateThPresignedUrlAspectChain.java
  72. 12 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/GenerateThPresignedUrlAspectChainCallback.java
  73. 35 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/GetFileAspectChain.java
  74. 12 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/GetFileAspectChainCallback.java
  75. 41 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/InitiateMultipartUploadAspectChain.java
  76. 17 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/InitiateMultipartUploadAspectChainCallback.java
  77. 33 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/InvokeAspectChain.java
  78. 10 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/InvokeAspectChainCallback.java
  79. 33 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportAclAspectChain.java
  80. 10 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportAclAspectChainCallback.java
  81. 35 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportListFilesAspectChain.java
  82. 11 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportListFilesChainCallback.java
  83. 34 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportMetadataAspectChain.java
  84. 10 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportMetadataAspectChainCallback.java
  85. 35 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportMultipartUploadAspectChain.java
  86. 11 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportMultipartUploadChainCallback.java
  87. 34 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportPresignedUrlAspectChain.java
  88. 10 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportPresignedUrlAspectChainCallback.java
  89. 34 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportSameCopyAspectChain.java
  90. 10 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportSameCopyAspectChainCallback.java
  91. 34 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportSameMoveAspectChain.java
  92. 10 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportSameMoveAspectChainCallback.java
  93. 35 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/ListFilesAspectChain.java
  94. 12 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/ListFilesAspectChainCallback.java
  95. 35 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/ListPartsAspectChain.java
  96. 12 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/ListPartsAspectChainCallback.java
  97. 37 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/MoveAspectChain.java
  98. 13 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/MoveAspectChainCallback.java
  99. 43 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/SameCopyAspectChain.java
  100. 18 0
      jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/SameCopyAspectChainCallback.java

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 21 - 0
.idea/compiler.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <annotationProcessing>
+      <profile name="Maven default annotation processors profile" enabled="true">
+        <sourceOutputDir name="target/generated-sources/annotations" />
+        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
+        <outputRelativeToContentRoot value="true" />
+        <module name="jnpf-file-core" />
+        <module name="jnpf-file-core-spring" />
+      </profile>
+    </annotationProcessing>
+  </component>
+  <component name="JavacSettings">
+    <option name="ADDITIONAL_OPTIONS_OVERRIDE">
+      <module name="jnpf-file-core" options="-parameters" />
+      <module name="jnpf-file-core-spring" options="-parameters" />
+      <module name="jnpf-file-core-starter" options="-parameters" />
+    </option>
+  </component>
+</project>

+ 11 - 0
.idea/encodings.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding">
+    <file url="file://$PROJECT_DIR$/jnpf-file-core-spring/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/jnpf-file-core-spring/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/jnpf-file-core/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/jnpf-file-core/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
+  </component>
+</project>

+ 20 - 0
.idea/jarRepositories.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="RemoteRepositoriesConfiguration">
+    <remote-repository>
+      <option name="id" value="central" />
+      <option name="name" value="Central Repository" />
+      <option name="url" value="http://127.0.0.1:9999/repository/maven-public/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="central" />
+      <option name="name" value="Maven Central repository" />
+      <option name="url" value="https://repo1.maven.org/maven2" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="jboss.community" />
+      <option name="name" value="JBoss Community repository" />
+      <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
+    </remote-repository>
+  </component>
+</project>

+ 12 - 0
.idea/misc.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ExternalStorageConfigurationManager" enabled="true" />
+  <component name="MavenProjectsManager">
+    <option name="originalFiles">
+      <list>
+        <option value="$PROJECT_DIR$/pom.xml" />
+      </list>
+    </option>
+  </component>
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK" />
+</project>

+ 4 - 0
.idea/vcs.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings" defaultProject="true" />
+</project>

+ 83 - 0
README.md

@@ -0,0 +1,83 @@
+> 特别说明:源码、JDK、数据库、Redis等安装或存放路径禁止包含中文、空格、特殊字符等
+
+## 一 项目结构
+
+```text
+jnpf-file-core-starter
+    ├── aspect- 切面层
+    ├── exception- 自定义异常
+    ├── platform - 存储平台实现层
+    └── recorder- 记录器
+```
+
+## 二 环境要求
+
+| 类目    | 版本或建议  |
+|-------|---------|
+| 硬件    | 开发电脑建议使用I3及以上CPU,16G及以上内存  |
+| 操作系统  | Windows 10/11,MacOS   |
+| JDK   | 默认使用JDK 21,如需要切换JDK 8/11/17版本请参考文档调整代码,推荐使用 `OpenJDK`,如 `Liberica JDK`、`Eclipse Temurin`、`Alibaba Dragonwell`、`BiSheng`等发行版; |
+| Maven | 依赖管理工具,推荐使用 `3.6.3` 及以上版本  |
+| IDE   | 代码集成开发环境,推荐使用 `IDEA2024` 及以上版本,兼容 `Eclipse`、 `Spring Tool Suite` 等IDE工具 |
+
+## 三 关联项目
+> 为以下项目提供基础依赖
+
+| 项目 |  分支 | 说明 |
+| --- |  --- | --- |
+| jnpf-common | v6.0.x-stable  | Java基础依赖项目源码 |
+| jnpf-java-boot  | v6.0.x-stable  | Java单体后端项目源码 |
+| jnpf-java-cloud | v6.0.x-stable  | Java微服务后端项目源码 |
+
+## 四 使用方式
+
+### 4.1 前置条件
+
+#### 4.1.1 本地安装jnpf-common-core
+
+IDEA中打开 `jnpf-common` 项目, 双击右侧 `Maven` 中 `jnpf-common` > `jnpf-boot-common` > `jnpf-common-core` > `Lifecycle` > `install`,将 `jnpf-common-core` 包安装至本地
+
+#### 4.1.2 本地安装dependencies
+
+IDEA中打开 `jnpf-common` 项目,双击右侧 `Maven` 中 `jnpf-common` > `jnpf-dependencies` > `Lifecycle` > `install`,将 `jnpf-dependencies` 包安装至本地
+
+### 4.2 本地安装
+
+在IDEA中,双击右侧 `Maven` 中`jnpf-file-core-starter` > `Lifecycle` > `install`,将`jnpf-file-core-starter`包安装至本地
+
+### 4.3 私服发布
+> 若无Maven私服,忽略本节内容
+
+#### 4.3.1 配置Maven
+
+打开Maven安装目录中的 `conf/setttings.xml` ,
+
+在 `<servers></servers>`节点增加 `<server></server>` ,如下所示:
+
+```xml
+  <!-- 发布版 -->
+  <server>
+    <id>maven-releases</id>
+    <username>jnpf-user(账号,结合私服配置设置)</username>
+    <password>123456(密码,结合私服配置设置)</password>
+  </server>
+```
+#### 4.3.2 配置项目
+
+> 注意:pom.xml里 `<id>` 和 setting.xml 配置里 `<id>` 对应。
+
+IDEA打开 `jnpf-common` 项目, 修改 `jnpf-dependencies/pom.xml` 文件中私服配置
+
+```xml
+<distributionManagement>
+  <repository>
+    <id>maven-releases</id>
+    <name>maven-releases</name>
+    <url>http://nexus.jnpfsoft.com/repository/maven-releases/</url>
+  </repository>
+</distributionManagement>
+```
+
+#### 4.3.3 发布到私服
+
+在IDEA中,双击右侧 `Maven` 中 `jnpf-file-core-starter` > `Lifecycle` > `deploy` 发布至私服。

+ 27 - 0
jnpf-file-core-spring/pom.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.jnpf</groupId>
+        <artifactId>jnpf-file-core-starter</artifactId>
+        <version>6.0.0-RELEASE</version>
+    </parent>
+
+    <artifactId>jnpf-file-core-spring</artifactId>
+    <name>X File Storage Spring</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.jnpf</groupId>
+            <artifactId>jnpf-file-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.jnpf</groupId>
+            <artifactId>jnpf-common-core</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 13 - 0
jnpf-file-core-spring/src/main/java/org/dromara/x/file/storage/spring/EnableFileStorage.java

@@ -0,0 +1,13 @@
+package org.dromara.x.file.storage.spring;
+
+import java.lang.annotation.*;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 启用文件存储,会自动根据配置文件进行加载
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+@Documented
+@Import({FileStorageAutoConfiguration.class, SpringFileStorageProperties.class})
+public @interface EnableFileStorage {}

+ 190 - 0
jnpf-file-core-spring/src/main/java/org/dromara/x/file/storage/spring/FileStorageAutoConfiguration.java

@@ -0,0 +1,190 @@
+package org.dromara.x.file.storage.spring;
+
+import static org.dromara.x.file.storage.core.FileStorageServiceBuilder.doesNotExistClass;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.x.file.storage.core.FileStorageService;
+import org.dromara.x.file.storage.core.FileStorageServiceBuilder;
+import org.dromara.x.file.storage.core.aspect.FileStorageAspect;
+import org.dromara.x.file.storage.core.file.FileWrapperAdapter;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.platform.FileStorageClientFactory;
+import org.dromara.x.file.storage.core.recorder.DefaultFileRecorder;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+import org.dromara.x.file.storage.core.tika.ContentTypeDetect;
+import org.dromara.x.file.storage.core.tika.DefaultTikaFactory;
+import org.dromara.x.file.storage.core.tika.TikaContentTypeDetect;
+import org.dromara.x.file.storage.core.tika.TikaFactory;
+import org.dromara.x.file.storage.spring.SpringFileStorageProperties.SpringLocalConfig;
+import org.dromara.x.file.storage.spring.SpringFileStorageProperties.SpringLocalPlusConfig;
+import org.dromara.x.file.storage.spring.file.MultipartFileWrapperAdapter;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Slf4j
+@Configuration
+@ConditionalOnMissingBean(FileStorageService.class)
+public class FileStorageAutoConfiguration {
+
+    @Autowired
+    private SpringFileStorageProperties properties;
+
+    @Autowired
+    private ApplicationContext applicationContext;
+
+    /**
+     * 当没有找到 FileRecorder 时使用默认的 FileRecorder
+     */
+    @Bean
+    @ConditionalOnMissingBean(FileRecorder.class)
+    public FileRecorder fileRecorder() {
+        log.warn("没有找到 FileRecorder 的实现类,文件上传之外的部分功能无法正常使用,必须实现该接口才能使用完整功能!");
+        return new DefaultFileRecorder();
+    }
+
+    /**
+     * Tika 工厂类型,用于识别上传的文件的 MINE
+     */
+    @Bean
+    @ConditionalOnMissingBean(TikaFactory.class)
+    public TikaFactory tikaFactory() {
+        return new DefaultTikaFactory();
+    }
+
+    /**
+     * 识别文件的 MIME 类型
+     */
+    @Bean
+    @ConditionalOnMissingBean(ContentTypeDetect.class)
+    public ContentTypeDetect contentTypeDetect(TikaFactory tikaFactory) {
+        return new TikaContentTypeDetect(tikaFactory);
+    }
+
+    /**
+     * 文件存储服务
+     */
+    @Bean(destroyMethod = "destroy")
+    public FileStorageService fileStorageService(
+            FileRecorder fileRecorder,
+            @Autowired(required = false) List<List<? extends FileStorage>> fileStorageLists,
+            @Autowired(required = false) List<FileStorageAspect> aspectList,
+            @Autowired(required = false) List<FileWrapperAdapter> fileWrapperAdapterList,
+            ContentTypeDetect contentTypeDetect,
+            @Autowired(required = false) List<List<FileStorageClientFactory<?>>> clientFactoryList) {
+
+        if (fileStorageLists == null) fileStorageLists = new ArrayList<>();
+        if (aspectList == null) aspectList = new ArrayList<>();
+        if (fileWrapperAdapterList == null) fileWrapperAdapterList = new ArrayList<>();
+        if (clientFactoryList == null) clientFactoryList = new ArrayList<>();
+
+        FileStorageServiceBuilder builder = FileStorageServiceBuilder.create(properties.toFileStorageProperties())
+                .setFileRecorder(fileRecorder)
+                .setAspectList(aspectList)
+                .setContentTypeDetect(contentTypeDetect)
+                .setFileWrapperAdapterList(fileWrapperAdapterList)
+                .setClientFactoryList(clientFactoryList);
+
+        fileStorageLists.forEach(builder::addFileStorage);
+
+        if (properties.getEnableByteFileWrapper()) {
+            builder.addByteFileWrapperAdapter();
+        }
+        if (properties.getEnableUriFileWrapper()) {
+            builder.addUriFileWrapperAdapter();
+        }
+        if (properties.getEnableInputStreamFileWrapper()) {
+            builder.addInputStreamFileWrapperAdapter();
+        }
+        if (properties.getEnableLocalFileWrapper()) {
+            builder.addLocalFileWrapperAdapter();
+        }
+        if (properties.getEnableHttpServletRequestFileWrapper()) {
+            if (doesNotExistClass("javax.servlet.http.HttpServletRequest")
+                    && doesNotExistClass("jakarta.servlet.http.HttpServletRequest")) {
+                log.warn(
+                        "当前未检测到 Servlet 环境,无法加载 HttpServletRequest 的文件包装适配器,请将参数【dromara.x-file-storage.enable-http-servlet-request-file-wrapper】设置为 【false】来消除此警告");
+            } else {
+                builder.addHttpServletRequestFileWrapperAdapter();
+            }
+        }
+        if (properties.getEnableMultipartFileWrapper()) {
+            if (doesNotExistClass("org.springframework.web.multipart.MultipartFile")) {
+                log.warn(
+                        "当前未检测到 SpringWeb 环境,无法加载 MultipartFile 的文件包装适配器,请将参数【dromara.x-file-storage.enable-multipart-file-wrapper】设置为 【false】来消除此警告");
+            } else {
+                builder.addFileWrapperAdapter(new MultipartFileWrapperAdapter());
+            }
+        }
+
+        if (doesNotExistClass("org.springframework.web.servlet.config.annotation.WebMvcConfigurer")) {
+            long localAccessNum = properties.getLocal().stream()
+                    .filter(SpringLocalConfig::getEnableStorage)
+                    .filter(SpringLocalConfig::getEnableAccess)
+                    .count();
+            long localPlusAccessNum = properties.getLocalPlus().stream()
+                    .filter(SpringLocalPlusConfig::getEnableStorage)
+                    .filter(SpringLocalPlusConfig::getEnableAccess)
+                    .count();
+
+            if (localAccessNum + localPlusAccessNum > 0) {
+                log.warn("当前未检测到 SpringWeb 环境,无法开启本地存储平台的本地访问功能,请将关闭本地访问来消除此警告");
+            }
+        }
+
+        return builder.build();
+    }
+
+    /**
+     * 对 FileStorageService 注入自己的代理对象,不然会导致针对 FileStorageService 的代理方法不生效
+     */
+    @EventListener(ContextRefreshedEvent.class)
+    public void onContextRefreshedEvent() {
+        FileStorageService service = applicationContext.getBean(FileStorageService.class);
+        service.setSelf(service);
+    }
+
+    /**
+     * 本地存储文件访问自动配置类
+     */
+    @Configuration
+    @ConditionalOnClass(name = "org.springframework.web.servlet.config.annotation.WebMvcConfigurer")
+    public static class FileStorageLocalFileAccessAutoConfiguration {
+        @Autowired
+        private SpringFileStorageProperties properties;
+
+        /**
+         * 配置本地存储的访问地址
+         */
+        @Bean
+        public WebMvcConfigurer fileStorageWebMvcConfigurer() {
+            return new WebMvcConfigurer() {
+                @Override
+                public void addResourceHandlers(@NonNull ResourceHandlerRegistry registry) {
+                    for (SpringLocalConfig local : properties.getLocal()) {
+                        if (local.getEnableStorage() && local.getEnableAccess()) {
+                            registry.addResourceHandler(local.getPathPatterns())
+                                    .addResourceLocations("file:" + local.getBasePath());
+                        }
+                    }
+                    for (SpringLocalPlusConfig local : properties.getLocalPlus()) {
+                        if (local.getEnableStorage() && local.getEnableAccess()) {
+                            registry.addResourceHandler(local.getPathPatterns())
+                                    .addResourceLocations("file:" + local.getStoragePath());
+                        }
+                    }
+                }
+            };
+        }
+    }
+}

+ 447 - 0
jnpf-file-core-spring/src/main/java/org/dromara/x/file/storage/spring/SpringFileStorageProperties.java

@@ -0,0 +1,447 @@
+package org.dromara.x.file.storage.spring;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+import org.dromara.x.file.storage.core.FileStorageProperties;
+import org.dromara.x.file.storage.core.FileStorageProperties.AliyunOssConfig;
+import org.dromara.x.file.storage.core.FileStorageProperties.AmazonS3Config;
+import org.dromara.x.file.storage.core.FileStorageProperties.AzureBlobStorageConfig;
+import org.dromara.x.file.storage.core.FileStorageProperties.BaiduBosConfig;
+import org.dromara.x.file.storage.core.FileStorageProperties.FastDfsConfig;
+import org.dromara.x.file.storage.core.FileStorageProperties.FtpConfig;
+import org.dromara.x.file.storage.core.FileStorageProperties.GoogleCloudStorageConfig;
+import org.dromara.x.file.storage.core.FileStorageProperties.HuaweiObsConfig;
+import org.dromara.x.file.storage.core.FileStorageProperties.LocalConfig;
+import org.dromara.x.file.storage.core.FileStorageProperties.LocalPlusConfig;
+import org.dromara.x.file.storage.core.FileStorageProperties.MinioConfig;
+import org.dromara.x.file.storage.core.FileStorageProperties.QiniuKodoConfig;
+import org.dromara.x.file.storage.core.FileStorageProperties.SftpConfig;
+import org.dromara.x.file.storage.core.FileStorageProperties.TencentCosConfig;
+import org.dromara.x.file.storage.core.FileStorageProperties.UpyunUssConfig;
+import org.dromara.x.file.storage.core.FileStorageProperties.WebDavConfig;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Accessors(chain = true)
+@Component
+@ConditionalOnMissingBean(SpringFileStorageProperties.class)
+@ConfigurationProperties(prefix = "config.file-storage")
+public class SpringFileStorageProperties {
+
+    /**
+     * 默认存储平台
+     */
+    private String defaultPlatform = "local";
+    /**
+     * 缩略图后缀,例如【.min.jpg】【.png】
+     */
+    private String thumbnailSuffix = ".min.jpg";
+    /**
+     * 上传时不支持元数据时抛出异常
+     */
+    private Boolean uploadNotSupportMetadataThrowException = true;
+    /**
+     * 上传时不支持 ACL 时抛出异常
+     */
+    private Boolean uploadNotSupportAclThrowException = true;
+    /**
+     * 复制时不支持元数据时抛出异常
+     */
+    private Boolean copyNotSupportMetadataThrowException = true;
+    /**
+     * 复制时不支持 ACL 时抛出异常
+     */
+    private Boolean copyNotSupportAclThrowException = true;
+    /**
+     * 移动时不支持元数据时抛出异常
+     */
+    private Boolean moveNotSupportMetadataThrowException = true;
+    /**
+     * 移动时不支持 ACL 时抛出异常
+     */
+    private Boolean moveNotSupportAclThrowException = true;
+    /**
+     * 启用 byte[] 文件包装适配器
+     */
+    private Boolean enableByteFileWrapper = true;
+    /**
+     * 启用 URI 文件包装适配器,包含 URL 和 String
+     */
+    private Boolean enableUriFileWrapper = true;
+    /**
+     * 启用 InputStream 文件包装适配器
+     */
+    private Boolean enableInputStreamFileWrapper = true;
+    /**
+     * 启用本地文件包装适配器
+     */
+    private Boolean enableLocalFileWrapper = true;
+    /**
+     * 启用 HttpServletRequest 文件包装适配器
+     */
+    private Boolean enableHttpServletRequestFileWrapper = true;
+    /**
+     * 启用 MultipartFile 文件包装适配器
+     */
+    private Boolean enableMultipartFileWrapper = true;
+    /**
+     * 本地存储
+     */
+    @Deprecated
+    private List<? extends SpringLocalConfig> local = new ArrayList<>();
+    /**
+     * 本地存储
+     */
+    private List<? extends SpringLocalPlusConfig> localPlus = new ArrayList<>();
+    /**
+     * 华为云 OBS
+     */
+    private List<? extends SpringHuaweiObsConfig> huaweiObs = new ArrayList<>();
+    /**
+     * 阿里云 OSS
+     */
+    private List<? extends SpringAliyunOssConfig> aliyunOss = new ArrayList<>();
+    /**
+     * 七牛云 Kodo
+     */
+    private List<? extends SpringQiniuKodoConfig> qiniuKodo = new ArrayList<>();
+    /**
+     * 腾讯云 COS
+     */
+    private List<? extends SpringTencentCosConfig> tencentCos = new ArrayList<>();
+    /**
+     * 百度云 BOS
+     */
+    private List<? extends SpringBaiduBosConfig> baiduBos = new ArrayList<>();
+    /**
+     * 又拍云 USS
+     */
+    private List<? extends SpringUpyunUssConfig> upyunUss = new ArrayList<>();
+    /**
+     * MinIO USS
+     */
+    private List<? extends SpringMinioConfig> minio = new ArrayList<>();
+
+    /**
+     * Amazon S3
+     */
+    private List<? extends SpringAmazonS3Config> amazonS3 = new ArrayList<>();
+
+    /**
+     * FTP
+     */
+    private List<? extends SpringFtpConfig> ftp = new ArrayList<>();
+
+    /**
+     * FTP
+     */
+    private List<? extends SpringSftpConfig> sftp = new ArrayList<>();
+
+    /**
+     * WebDAV
+     */
+    private List<? extends SpringWebDavConfig> webdav = new ArrayList<>();
+
+    /**
+     * GoogleCloud Storage
+     */
+    private List<? extends SpringGoogleCloudStorageConfig> googleCloudStorage = new ArrayList<>();
+
+    /**
+     * FastDFS
+     */
+    private List<? extends SpringFastDfsConfig> fastdfs = new ArrayList<>();
+
+    /**
+     * Azure Blob Storage
+     */
+    private List<? extends SpringAzureBlobStorageConfig> azureBlob = new ArrayList<>();
+
+    /**
+     * 转换成 FileStorageProperties ,并过滤掉没有启用的存储平台
+     */
+    public FileStorageProperties toFileStorageProperties() {
+        FileStorageProperties properties = new FileStorageProperties();
+        properties.setDefaultPlatform(defaultPlatform);
+        properties.setThumbnailSuffix(thumbnailSuffix);
+        properties.setUploadNotSupportMetadataThrowException(uploadNotSupportMetadataThrowException);
+        properties.setUploadNotSupportAclThrowException(uploadNotSupportAclThrowException);
+        properties.setCopyNotSupportMetadataThrowException(copyNotSupportMetadataThrowException);
+        properties.setCopyNotSupportAclThrowException(copyNotSupportAclThrowException);
+        properties.setMoveNotSupportMetadataThrowException(moveNotSupportMetadataThrowException);
+        properties.setMoveNotSupportAclThrowException(moveNotSupportAclThrowException);
+        properties.setLocal(
+                local.stream().filter(SpringLocalConfig::getEnableStorage).collect(Collectors.toList()));
+        properties.setLocalPlus(localPlus.stream()
+                .filter(SpringLocalPlusConfig::getEnableStorage)
+                .collect(Collectors.toList()));
+        properties.setHuaweiObs(huaweiObs.stream()
+                .filter(SpringHuaweiObsConfig::getEnableStorage)
+                .collect(Collectors.toList()));
+        properties.setAliyunOss(aliyunOss.stream()
+                .filter(SpringAliyunOssConfig::getEnableStorage)
+                .collect(Collectors.toList()));
+        properties.setQiniuKodo(qiniuKodo.stream()
+                .filter(SpringQiniuKodoConfig::getEnableStorage)
+                .collect(Collectors.toList()));
+        properties.setTencentCos(tencentCos.stream()
+                .filter(SpringTencentCosConfig::getEnableStorage)
+                .collect(Collectors.toList()));
+        properties.setBaiduBos(
+                baiduBos.stream().filter(SpringBaiduBosConfig::getEnableStorage).collect(Collectors.toList()));
+        properties.setUpyunUss(
+                upyunUss.stream().filter(SpringUpyunUssConfig::getEnableStorage).collect(Collectors.toList()));
+        properties.setMinio(
+                minio.stream().filter(SpringMinioConfig::getEnableStorage).collect(Collectors.toList()));
+        properties.setAmazonS3(
+                amazonS3.stream().filter(SpringAmazonS3Config::getEnableStorage).collect(Collectors.toList()));
+        properties.setFtp(ftp.stream().filter(SpringFtpConfig::getEnableStorage).collect(Collectors.toList()));
+        properties.setSftp(
+                sftp.stream().filter(SpringSftpConfig::getEnableStorage).collect(Collectors.toList()));
+        properties.setWebdav(
+                webdav.stream().filter(SpringWebDavConfig::getEnableStorage).collect(Collectors.toList()));
+        properties.setGoogleCloudStorage(googleCloudStorage.stream()
+                .filter(SpringGoogleCloudStorageConfig::getEnableStorage)
+                .collect(Collectors.toList()));
+        properties.setFastdfs(
+                fastdfs.stream().filter(SpringFastDfsConfig::getEnableStorage).collect(Collectors.toList()));
+        properties.setAzureBlob(azureBlob.stream()
+                .filter(SpringAzureBlobStorageConfig::getEnableStorage)
+                .collect(Collectors.toList()));
+
+        return properties;
+    }
+
+    /**
+     * 本地存储
+     */
+    @Deprecated
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringLocalConfig extends LocalConfig {
+        /**
+         * 本地存储访问路径
+         */
+        private String[] pathPatterns = new String[0];
+        /**
+         * 启用本地存储
+         */
+        private Boolean enableStorage = false;
+        /**
+         * 启用本地访问
+         */
+        private Boolean enableAccess = false;
+    }
+
+    /**
+     * 本地存储升级版
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringLocalPlusConfig extends LocalPlusConfig {
+        /**
+         * 本地存储访问路径
+         */
+        private String[] pathPatterns = new String[0];
+        /**
+         * 启用本地存储
+         */
+        private Boolean enableStorage = false;
+        /**
+         * 启用本地访问
+         */
+        private Boolean enableAccess = false;
+    }
+
+    /**
+     * 华为云 OBS
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringHuaweiObsConfig extends HuaweiObsConfig {
+        /**
+         * 启用存储
+         */
+        private Boolean enableStorage = false;
+    }
+
+    /**
+     * 阿里云 OSS
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringAliyunOssConfig extends AliyunOssConfig {
+        /**
+         * 启用存储
+         */
+        private Boolean enableStorage = false;
+    }
+
+    /**
+     * 七牛云 Kodo
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringQiniuKodoConfig extends QiniuKodoConfig {
+        /**
+         * 启用存储
+         */
+        private Boolean enableStorage = false;
+    }
+
+    /**
+     * 腾讯云 COS
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringTencentCosConfig extends TencentCosConfig {
+        /**
+         * 启用存储
+         */
+        private Boolean enableStorage = false;
+    }
+
+    /**
+     * 百度云 BOS
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringBaiduBosConfig extends BaiduBosConfig {
+        /**
+         * 启用存储
+         */
+        private Boolean enableStorage = false;
+    }
+
+    /**
+     * 又拍云 USS
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringUpyunUssConfig extends UpyunUssConfig {
+        /**
+         * 启用存储
+         */
+        private Boolean enableStorage = false;
+    }
+
+    /**
+     * MinIO
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringMinioConfig extends MinioConfig {
+        /**
+         * 启用存储
+         */
+        private Boolean enableStorage = false;
+    }
+
+    /**
+     * Amazon S3
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringAmazonS3Config extends AmazonS3Config {
+        /**
+         * 启用存储
+         */
+        private Boolean enableStorage = false;
+    }
+
+    /**
+     * FTP
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringFtpConfig extends FtpConfig {
+        /**
+         * 启用存储
+         */
+        private Boolean enableStorage = false;
+    }
+
+    /**
+     * SFTP
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringSftpConfig extends SftpConfig {
+        /**
+         * 启用存储
+         */
+        private Boolean enableStorage = false;
+    }
+
+    /**
+     * WebDAV
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringWebDavConfig extends WebDavConfig {
+        /**
+         * 启用存储
+         */
+        private Boolean enableStorage = false;
+    }
+
+    /**
+     * GoogleCloud Storage
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringGoogleCloudStorageConfig extends GoogleCloudStorageConfig {
+        /**
+         * 启用存储
+         */
+        private Boolean enableStorage = false;
+    }
+
+    /**
+     * FastDFS Storage
+     * @author XS <wanghaiqi@beeplay123.com>
+     * @date 2023/10/23
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringFastDfsConfig extends FastDfsConfig {
+        /**
+         * 启用存储
+         */
+        private Boolean enableStorage = false;
+    }
+
+    /**
+     * AzureBlob Storage
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SpringAzureBlobStorageConfig extends AzureBlobStorageConfig {
+        /**
+         * 启用存储
+         */
+        private Boolean enableStorage = false;
+    }
+}

+ 65 - 0
jnpf-file-core-spring/src/main/java/org/dromara/x/file/storage/spring/file/MultipartFileWrapper.java

@@ -0,0 +1,65 @@
+package org.dromara.x.file.storage.spring.file;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IoUtil;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.exception.FileStorageRuntimeException;
+import org.dromara.x.file.storage.core.file.FileWrapper;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * MultipartFile 文件包装类
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+public class MultipartFileWrapper implements FileWrapper {
+    private MultipartFile file;
+    private String name;
+    private String contentType;
+    private InputStream inputStream;
+    private Long size;
+
+    public MultipartFileWrapper(MultipartFile file, String name, String contentType, Long size) {
+        this.file = file;
+        this.name = name;
+        this.contentType = contentType;
+        this.size = size;
+    }
+
+    @Override
+    public InputStream getInputStream() throws IOException {
+        if (inputStream == null) {
+            inputStream = new BufferedInputStream(file.getInputStream());
+        }
+        return inputStream;
+    }
+
+    @Override
+    public void transferTo(File dest) {
+        // 在某些 SpringBoot 版本中,例如 2.4.6,此方法会调用失败,
+        // 此时尝试手动将 InputStream 写入指定文件,
+        // 根据文档来看 MultipartFile 最终都会由框架从临时目录中删除
+        try {
+            file.transferTo(dest);
+            IoUtil.close(inputStream);
+        } catch (Exception ignored) {
+            try {
+                FileUtil.writeFromStream(getInputStream(), dest);
+            } catch (Exception e) {
+                throw new FileStorageRuntimeException("文件移动失败", e);
+            }
+        }
+    }
+
+    @Override
+    public boolean supportTransfer() {
+        return true;
+    }
+}

+ 33 - 0
jnpf-file-core-spring/src/main/java/org/dromara/x/file/storage/spring/file/MultipartFileWrapperAdapter.java

@@ -0,0 +1,33 @@
+package org.dromara.x.file.storage.spring.file;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.file.FileWrapper;
+import org.dromara.x.file.storage.core.file.FileWrapperAdapter;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * MultipartFile 文件包装适配器
+ */
+@Getter
+@Setter
+public class MultipartFileWrapperAdapter implements FileWrapperAdapter {
+
+    @Override
+    public boolean isSupport(Object source) {
+        return source instanceof MultipartFile || source instanceof MultipartFileWrapper;
+    }
+
+    @Override
+    public FileWrapper getFileWrapper(Object source, String name, String contentType, Long size) {
+        if (source instanceof MultipartFileWrapper) {
+            return updateFileWrapper((MultipartFileWrapper) source, name, contentType, size);
+        } else {
+            MultipartFile file = (MultipartFile) source;
+            if (name == null) name = file.getOriginalFilename();
+            if (contentType == null) contentType = file.getContentType();
+            if (size == null) size = file.getSize();
+            return new MultipartFileWrapper(file, name, contentType, size);
+        }
+    }
+}

TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/EnableFileStorage.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/FileStorageAutoConfiguration$FileStorageLocalFileAccessAutoConfiguration$1.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/FileStorageAutoConfiguration$FileStorageLocalFileAccessAutoConfiguration.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/FileStorageAutoConfiguration.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringAliyunOssConfig.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringAmazonS3Config.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringAzureBlobStorageConfig.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringBaiduBosConfig.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringFastDfsConfig.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringFtpConfig.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringGoogleCloudStorageConfig.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringHuaweiObsConfig.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringLocalConfig.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringLocalPlusConfig.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringMinioConfig.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringQiniuKodoConfig.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringSftpConfig.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringTencentCosConfig.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringUpyunUssConfig.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties$SpringWebDavConfig.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/SpringFileStorageProperties.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/file/MultipartFileWrapper.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/classes/org/dromara/x/file/storage/spring/file/MultipartFileWrapperAdapter.class


TEMPAT SAMPAH
jnpf-file-core-spring/target/jnpf-file-core-spring-6.0.0-RELEASE.jar


+ 3 - 0
jnpf-file-core-spring/target/maven-archiver/pom.properties

@@ -0,0 +1,3 @@
+artifactId=jnpf-file-core-spring
+groupId=com.jnpf
+version=6.0.0-RELEASE

+ 23 - 0
jnpf-file-core-spring/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst

@@ -0,0 +1,23 @@
+org\dromara\x\file\storage\spring\file\MultipartFileWrapper.class
+org\dromara\x\file\storage\spring\file\MultipartFileWrapperAdapter.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringAliyunOssConfig.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringSftpConfig.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringQiniuKodoConfig.class
+org\dromara\x\file\storage\spring\FileStorageAutoConfiguration$FileStorageLocalFileAccessAutoConfiguration$1.class
+org\dromara\x\file\storage\spring\FileStorageAutoConfiguration.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringHuaweiObsConfig.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringBaiduBosConfig.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringLocalConfig.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringLocalPlusConfig.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringAmazonS3Config.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringGoogleCloudStorageConfig.class
+org\dromara\x\file\storage\spring\EnableFileStorage.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringUpyunUssConfig.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringFastDfsConfig.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringMinioConfig.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringAzureBlobStorageConfig.class
+org\dromara\x\file\storage\spring\FileStorageAutoConfiguration$FileStorageLocalFileAccessAutoConfiguration.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringWebDavConfig.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringFtpConfig.class
+org\dromara\x\file\storage\spring\SpringFileStorageProperties$SpringTencentCosConfig.class

+ 5 - 0
jnpf-file-core-spring/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst

@@ -0,0 +1,5 @@
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-file-core-starter-v6x\jnpf-file-core-spring\src\main\java\org\dromara\x\file\storage\spring\EnableFileStorage.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-file-core-starter-v6x\jnpf-file-core-spring\src\main\java\org\dromara\x\file\storage\spring\file\MultipartFileWrapper.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-file-core-starter-v6x\jnpf-file-core-spring\src\main\java\org\dromara\x\file\storage\spring\file\MultipartFileWrapperAdapter.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-file-core-starter-v6x\jnpf-file-core-spring\src\main\java\org\dromara\x\file\storage\spring\FileStorageAutoConfiguration.java
+C:\Users\zhaojinyu\Desktop\USKY\jnpf6.0\jnpf-file-core-starter-v6x\jnpf-file-core-spring\src\main\java\org\dromara\x\file\storage\spring\SpringFileStorageProperties.java

+ 187 - 0
jnpf-file-core/pom.xml

@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.jnpf</groupId>
+        <artifactId>jnpf-file-core-starter</artifactId>
+        <version>6.0.0-RELEASE</version>
+    </parent>
+
+    <artifactId>jnpf-file-core</artifactId>
+    <name>X File Storage Core</name>
+
+    <dependencies>
+        <!-- javax.servlet-api -->
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>${javax.servlet-api.version}</version>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- jakarta.servlet-api -->
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+            <version>${jakarata.servlet-api.version}</version>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- Apache commons-pool2 -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- 华为云 OBS -->
+        <dependency>
+            <groupId>com.huaweicloud</groupId>
+            <artifactId>esdk-obs-java</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- Amazon S3 -->
+        <dependency>
+            <groupId>com.amazonaws</groupId>
+            <artifactId>aws-java-sdk-s3</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- 阿里云 OSS -->
+        <dependency>
+            <groupId>com.aliyun.oss</groupId>
+            <artifactId>aliyun-sdk-oss</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- 百度云 BOS -->
+        <dependency>
+            <groupId>com.baidubce</groupId>
+            <artifactId>bce-java-sdk</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>jdk.tools</groupId>
+                    <artifactId>jdk.tools</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <!-- WebDAV -->
+        <dependency>
+            <groupId>com.github.lookfirst</groupId>
+            <artifactId>sardine</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- SFTP -->
+        <dependency>
+            <groupId>com.jcraft</groupId>
+            <artifactId>jsch</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- FTP -->
+        <dependency>
+            <groupId>commons-net</groupId>
+            <artifactId>commons-net</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- MinIO -->
+        <dependency>
+            <groupId>io.minio</groupId>
+            <artifactId>minio</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- 又拍云 USS -->
+        <dependency>
+            <groupId>com.upyun</groupId>
+            <artifactId>java-sdk</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- 腾讯云 COS -->
+        <dependency>
+            <groupId>com.qcloud</groupId>
+            <artifactId>cos_api</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- 七牛云 Kodo -->
+        <dependency>
+            <groupId>com.qiniu</groupId>
+            <artifactId>qiniu-java-sdk</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- 谷歌云 Google Cloud Platform Storage-->
+        <dependency>
+            <groupId>com.google.cloud</groupId>
+            <artifactId>google-cloud-storage</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- FastDFS Storage -->
+        <dependency>
+            <groupId>io.github.rui8832</groupId>
+            <artifactId>fastdfs-client-java</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- Azure Blob -->
+        <dependency>
+            <groupId>com.azure</groupId>
+            <artifactId>azure-storage-blob</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- Azure File Data Lake 用于处理 ACL -->
+        <dependency>
+            <groupId>com.azure</groupId>
+            <artifactId>azure-storage-file-datalake</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+
+        <!--糊涂工具类核心-->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-core</artifactId>
+            <scope>provided</scope>
+            <version>${hutool.version}</version>
+        </dependency>
+        <!--糊涂工具类扩展-->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-extra</artifactId>
+            <version>${hutool.version}</version>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <!-- Tika -->
+        <dependency>
+            <groupId>org.apache.tika</groupId>
+            <artifactId>tika-core</artifactId>
+        </dependency>
+        <!-- 图片处理 https://github.com/coobird/thumbnailator -->
+        <dependency>
+            <groupId>net.coobird</groupId>
+            <artifactId>thumbnailator</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.jnpf</groupId>
+            <artifactId>jnpf-common-core</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 139 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/Downloader.java

@@ -0,0 +1,139 @@
+package org.dromara.x.file.storage.core;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IoUtil;
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.function.Consumer;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import org.dromara.x.file.storage.core.aspect.DownloadAspectChain;
+import org.dromara.x.file.storage.core.aspect.DownloadThAspectChain;
+import org.dromara.x.file.storage.core.aspect.FileStorageAspect;
+import org.dromara.x.file.storage.core.exception.FileStorageRuntimeException;
+import org.dromara.x.file.storage.core.hash.HashCalculator;
+import org.dromara.x.file.storage.core.hash.HashCalculatorManager;
+import org.dromara.x.file.storage.core.hash.HashCalculatorSetter;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 下载器
+ */
+public class Downloader implements ProgressListenerSetter<Downloader>, HashCalculatorSetter<Downloader> {
+    /**
+     * 下载目标:文件
+     */
+    public static final int TARGET_FILE = 1;
+    /**
+     * 下载目标:缩略图文件
+     */
+    public static final int TARGET_TH_FILE = 2;
+
+    private final FileStorage fileStorage;
+    private final List<FileStorageAspect> aspectList;
+    private final FileInfo fileInfo;
+    private final Integer target;
+
+    @Setter
+    @Accessors(chain = true)
+    private ProgressListener progressListener;
+    /**
+     * 哈希计算器管理器
+     */
+    @Getter
+    @Setter
+    @Accessors(chain = true)
+    private HashCalculatorManager hashCalculatorManager = new HashCalculatorManager();
+
+    /**
+     * 构造下载器
+     *
+     * @param target 下载目标:{@link Downloader#TARGET_FILE}下载文件,{@link Downloader#TARGET_TH_FILE}下载缩略图文件
+     */
+    public Downloader(FileInfo fileInfo, List<FileStorageAspect> aspectList, FileStorage fileStorage, Integer target) {
+        this.fileStorage = fileStorage;
+        this.aspectList = aspectList;
+        this.fileInfo = fileInfo;
+        this.target = target;
+    }
+
+    /**
+     * 添加一个哈希计算器
+     * @param hashCalculator 哈希计算器
+     */
+    @Override
+    public Downloader setHashCalculator(HashCalculator hashCalculator) {
+        hashCalculatorManager.setHashCalculator(hashCalculator);
+        return this;
+    }
+
+    /**
+     * 设置哈希计算器管理器(如果条件为 true)
+     * @param flag 条件
+     * @param hashCalculatorManager 哈希计算器管理器
+     */
+    public Downloader setHashCalculatorManager(boolean flag, HashCalculatorManager hashCalculatorManager) {
+        if (flag) setHashCalculatorManager(hashCalculatorManager);
+        return this;
+    }
+
+    /**
+     * 获取 InputStream ,在此方法结束后会自动关闭 InputStream
+     */
+    public void inputStream(Consumer<InputStream> consumer) {
+        if (target == TARGET_FILE) { // 下载文件
+            new DownloadAspectChain(
+                            aspectList,
+                            (_fileInfo, _fileStorage, _consumer) -> _fileStorage.download(_fileInfo, _consumer))
+                    .next(
+                            fileInfo,
+                            fileStorage,
+                            in -> consumer.accept(new InputStreamPlus(
+                                    in, progressListener, fileInfo.getSize(), hashCalculatorManager)));
+        } else if (target == TARGET_TH_FILE) { // 下载缩略图文件
+            new DownloadThAspectChain(
+                            aspectList,
+                            (_fileInfo, _fileStorage, _consumer) -> _fileStorage.downloadTh(_fileInfo, _consumer))
+                    .next(
+                            fileInfo,
+                            fileStorage,
+                            in -> consumer.accept(new InputStreamPlus(
+                                    in, progressListener, fileInfo.getThSize(), hashCalculatorManager)));
+        } else {
+            throw new FileStorageRuntimeException("没找到对应的下载目标,请设置 target 参数!");
+        }
+    }
+
+    /**
+     * 下载 byte 数组
+     */
+    public byte[] bytes() {
+        byte[][] bytes = new byte[1][];
+        inputStream(in -> bytes[0] = IoUtil.readBytes(in));
+        return bytes[0];
+    }
+
+    /**
+     * 下载到指定文件
+     */
+    public void file(File file) {
+        inputStream(in -> FileUtil.writeFromStream(in, file));
+    }
+
+    /**
+     * 下载到指定文件
+     */
+    public void file(String filename) {
+        inputStream(in -> FileUtil.writeFromStream(in, filename));
+    }
+
+    /**
+     * 下载到指定输出流
+     */
+    public void outputStream(OutputStream out) {
+        inputStream(in -> IoUtil.copy(in, out));
+    }
+}

+ 178 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/FileInfo.java

@@ -0,0 +1,178 @@
+package org.dromara.x.file.storage.core;
+
+import cn.hutool.core.lang.Dict;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Map;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.dromara.x.file.storage.core.constant.Constant;
+import org.dromara.x.file.storage.core.hash.HashInfo;
+
+@Data
+@Accessors(chain = true)
+@NoArgsConstructor
+public class FileInfo implements Serializable {
+
+    /**
+     * 文件id
+     */
+    private String id;
+
+    /**
+     * 文件访问地址
+     */
+    private String url;
+
+    /**
+     * 文件大小,单位字节
+     */
+    private Long size;
+
+    /**
+     * 文件名称
+     */
+    private String filename;
+
+    /**
+     * 原始文件名
+     */
+    private String originalFilename;
+
+    /**
+     * 基础存储路径
+     */
+    private String basePath;
+
+    /**
+     * 存储路径
+     */
+    private String path;
+
+    /**
+     * 文件扩展名
+     */
+    private String ext;
+
+    /**
+     * MIME 类型
+     */
+    private String contentType;
+
+    /**
+     * 存储平台
+     */
+    private String platform;
+
+    /**
+     * 缩略图访问路径
+     */
+    private String thUrl;
+
+    /**
+     * 缩略图名称
+     */
+    private String thFilename;
+
+    /**
+     * 缩略图大小,单位字节
+     */
+    private Long thSize;
+
+    /**
+     * 缩略图 MIME 类型
+     */
+    private String thContentType;
+
+    /**
+     * 文件所属对象id
+     */
+    private String objectId;
+
+    /**
+     * 文件所属对象类型,例如用户头像,评价图片
+     */
+    private String objectType;
+
+    /**
+     * 文件元数据
+     */
+    private Map<String, String> metadata;
+
+    /**
+     * 文件用户元数据
+     */
+    private Map<String, String> userMetadata;
+
+    /**
+     * 缩略图元数据
+     */
+    private Map<String, String> thMetadata;
+
+    /**
+     * 缩略图用户元数据
+     */
+    private Map<String, String> thUserMetadata;
+
+    /**
+     * 附加属性字典
+     */
+    private Dict attr;
+
+    /**
+     * 文件的访问控制列表,一般情况下只有对象存储支持该功能,支持 String 或对应存储平台的 ACL 对象
+     * <pre class="code">
+     * //方式一,通过字符串设置通用的 ACL 详情:{@link Constant.ACL }
+     * setFileAcl(ACL.PUBLIC_READ);
+     * //方式二,针对指定存储平台设置更复杂的权限控制,以华为云 OBS 为例
+     * AccessControlList acl = new AccessControlList();
+     * Owner owner = new Owner();
+     * owner.setId("ownerid");
+     * acl.setOwner(owner);
+     * // 保留Owner的完全控制权限(注:如果不设置该权限,该对象Owner自身将没有访问权限)
+     * acl.grantPermission(new CanonicalGrantee("ownerid"), Permission.PERMISSION_FULL_CONTROL);
+     * // 为指定用户设置完全控制权限
+     * acl.grantPermission(new CanonicalGrantee("userid"), Permission.PERMISSION_FULL_CONTROL);
+     * // 为所有用户设置读权限
+     * acl.grantPermission(GroupGrantee.ALL_USERS, Permission.PERMISSION_READ);
+     * setFileAcl(acl);
+     * <pre/>
+     */
+    private Object fileAcl;
+
+    /**
+     * 缩略图的访问控制列表,一般情况下只有对象存储支持该功能
+     * 详情见{@link FileInfo#setFileAcl}
+     */
+    private Object thFileAcl;
+
+    /**
+     * 哈希信息类,用来存储各种哈希值
+     */
+    private HashInfo hashInfo;
+
+    /**
+     * 上传ID,仅在手动分片上传时使用
+     */
+    private String uploadId;
+
+    /**
+     * 上传状态,仅在手动分片上传时使用,1:初始化完成,2:上传完成
+     * {@link org.dromara.x.file.storage.core.constant.Constant.FileInfoUploadStatus}
+     */
+    private Integer uploadStatus;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    private static final long serialVersionUID = 1L;
+
+    public FileInfo(String basePath, String path, String filename) {
+        this.basePath = basePath;
+        this.path = path;
+        this.filename = filename;
+    }
+}

+ 1081 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/FileStorageProperties.java

@@ -0,0 +1,1081 @@
+package org.dromara.x.file.storage.core;
+
+import cn.hutool.core.map.MapBuilder;
+import cn.hutool.core.util.StrUtil;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+import org.dromara.x.file.storage.core.constant.Constant;
+
+@Data
+@Accessors(chain = true)
+public class FileStorageProperties {
+
+    /**
+     * 默认存储平台
+     */
+    private String defaultPlatform = "local";
+
+    /**
+     * 缩略图后缀,例如【.min.jpg】【.png】
+     */
+    private String thumbnailSuffix = ".min.jpg";
+
+    /**
+     * 上传时不支持元数据时抛出异常
+     */
+    private Boolean uploadNotSupportMetadataThrowException = true;
+
+    /**
+     * 上传时不支持 ACL 时抛出异常
+     */
+    private Boolean uploadNotSupportAclThrowException = true;
+
+    /**
+     * 复制时不支持元数据时抛出异常
+     */
+    private Boolean copyNotSupportMetadataThrowException = true;
+    /**
+     * 复制时不支持 ACL 时抛出异常
+     */
+    private Boolean copyNotSupportAclThrowException = true;
+
+    /**
+     * 移动时不支持元数据时抛出异常
+     */
+    private Boolean moveNotSupportMetadataThrowException = true;
+
+    /**
+     * 移动时不支持 ACL 时抛出异常
+     */
+    private Boolean moveNotSupportAclThrowException = true;
+
+    /**
+     * 本地存储
+     */
+    private List<? extends LocalConfig> local = new ArrayList<>();
+
+    /**
+     * 本地存储
+     */
+    private List<? extends LocalPlusConfig> localPlus = new ArrayList<>();
+
+    /**
+     * 华为云 OBS
+     */
+    private List<? extends HuaweiObsConfig> huaweiObs = new ArrayList<>();
+
+    /**
+     * 阿里云 OSS
+     */
+    private List<? extends AliyunOssConfig> aliyunOss = new ArrayList<>();
+
+    /**
+     * 七牛云 Kodo
+     */
+    private List<? extends QiniuKodoConfig> qiniuKodo = new ArrayList<>();
+
+    /**
+     * 腾讯云 COS
+     */
+    private List<? extends TencentCosConfig> tencentCos = new ArrayList<>();
+
+    /**
+     * 百度云 BOS
+     */
+    private List<? extends BaiduBosConfig> baiduBos = new ArrayList<>();
+
+    /**
+     * 又拍云 USS
+     */
+    private List<? extends UpyunUssConfig> upyunUss = new ArrayList<>();
+
+    /**
+     * MinIO USS
+     */
+    private List<? extends MinioConfig> minio = new ArrayList<>();
+
+    /**
+     * Amazon S3
+     */
+    private List<? extends AmazonS3Config> amazonS3 = new ArrayList<>();
+
+    /**
+     * FTP
+     */
+    private List<? extends FtpConfig> ftp = new ArrayList<>();
+
+    /**
+     * FTP
+     */
+    private List<? extends SftpConfig> sftp = new ArrayList<>();
+
+    /**
+     * WebDAV
+     */
+    private List<? extends WebDavConfig> webdav = new ArrayList<>();
+
+    /**
+     * 谷歌云存储
+     */
+    private List<? extends GoogleCloudStorageConfig> googleCloudStorage = new ArrayList<>();
+
+    /**
+     * FastDFS
+     */
+    private List<? extends FastDfsConfig> fastdfs = new ArrayList<>();
+
+    /**
+     * Azure Blob Storage
+     */
+    private List<? extends AzureBlobStorageConfig> azureBlob = new ArrayList<>();
+
+    /**
+     * 基本的存储平台配置
+     */
+    @Data
+    @Accessors(chain = true)
+    public static class BaseConfig {
+
+        /**
+         * 存储平台
+         */
+        private String platform = "";
+    }
+
+    /**
+     * 本地存储
+     */
+    @Deprecated
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class LocalConfig extends BaseConfig {
+
+        /**
+         * 本地存储路径
+         */
+        private String basePath = "";
+
+        /**
+         * 访问域名
+         */
+        private String domain = "";
+
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+    }
+
+    /**
+     * 本地存储升级版
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class LocalPlusConfig extends BaseConfig {
+
+        /**
+         * 基础路径
+         */
+        private String basePath = "";
+
+        /**
+         * 存储路径,上传的文件都会存储在这个路径下面,默认“/”,注意“/”结尾
+         */
+        private String storagePath = "/";
+
+        /**
+         * 访问域名
+         */
+        private String domain = "";
+
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+    }
+
+    /**
+     * 华为云 OBS
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class HuaweiObsConfig extends BaseConfig {
+
+        private String accessKey;
+
+        private String secretKey;
+
+        private String endPoint;
+
+        private String bucketName;
+
+        /**
+         * 访问域名
+         */
+        private String domain = "";
+
+        /**
+         * 基础路径
+         */
+        private String basePath = "";
+
+        /**
+         * 默认的 ACL,详情 {@link Constant.HuaweiObsACL}
+         */
+        private String defaultAcl;
+
+        /**
+         * 自动分片上传阈值,达到此大小则使用分片上传,默认 128MB
+         */
+        private int multipartThreshold = 128 * 1024 * 1024;
+
+        /**
+         * 自动分片上传时每个分片大小,默认 32MB
+         */
+        private int multipartPartSize = 32 * 1024 * 1024;
+
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+    }
+
+    /**
+     * 阿里云 OSS
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class AliyunOssConfig extends BaseConfig {
+
+        private String accessKey;
+
+        private String secretKey;
+
+        private String endPoint;
+
+        private String bucketName;
+
+        /**
+         * 访问域名
+         */
+        private String domain = "";
+
+        /**
+         * 基础路径
+         */
+        private String basePath = "";
+
+        /**
+         * 默认的 ACL,详情 {@link Constant.AliyunOssACL}
+         */
+        private String defaultAcl;
+
+        /**
+         * 自动分片上传阈值,达到此大小则使用分片上传,默认 128MB
+         */
+        private int multipartThreshold = 128 * 1024 * 1024;
+
+        /**
+         * 自动分片上传时每个分片大小,默认 32MB
+         */
+        private int multipartPartSize = 32 * 1024 * 1024;
+
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+    }
+
+    /**
+     * 七牛云 Kodo
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class QiniuKodoConfig extends BaseConfig {
+
+        private String accessKey;
+
+        private String secretKey;
+
+        private String bucketName;
+
+        /**
+         * 访问域名
+         */
+        private String domain = "";
+
+        /**
+         * 基础路径
+         */
+        private String basePath = "";
+
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+    }
+
+    /**
+     * 腾讯云 COS
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class TencentCosConfig extends BaseConfig {
+
+        private String secretId;
+
+        private String secretKey;
+
+        private String region;
+
+        private String bucketName;
+
+        /**
+         * 访问域名
+         */
+        private String domain = "";
+
+        /**
+         * 基础路径
+         */
+        private String basePath = "";
+
+        /**
+         * 默认的 ACL,详情 {@link Constant.TencentCosACL}
+         */
+        private String defaultAcl;
+
+        /**
+         * 自动分片上传阈值,达到此大小则使用分片上传,默认 128MB
+         */
+        private int multipartThreshold = 128 * 1024 * 1024;
+
+        /**
+         * 自动分片上传时每个分片大小,默认 32MB
+         */
+        private int multipartPartSize = 32 * 1024 * 1024;
+
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+    }
+
+    /**
+     * 百度云 BOS
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class BaiduBosConfig extends BaseConfig {
+
+        private String accessKey;
+
+        private String secretKey;
+
+        private String endPoint;
+
+        private String bucketName;
+
+        /**
+         * 访问域名
+         */
+        private String domain = "";
+
+        /**
+         * 基础路径
+         */
+        private String basePath = "";
+
+        /**
+         * 默认的 ACL,详情 {@link Constant.BaiduBosACL}
+         */
+        private String defaultAcl;
+
+        /**
+         * 自动分片上传阈值,达到此大小则使用分片上传,默认 128MB
+         */
+        private int multipartThreshold = 128 * 1024 * 1024;
+
+        /**
+         * 自动分片上传时每个分片大小,默认 32MB
+         */
+        private int multipartPartSize = 32 * 1024 * 1024;
+
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+    }
+
+    /**
+     * 又拍云 USS
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class UpyunUssConfig extends BaseConfig {
+
+        private String username;
+
+        private String password;
+
+        private String bucketName;
+
+        /**
+         * 访问域名
+         */
+        private String domain = "";
+
+        /**
+         * 基础路径
+         */
+        private String basePath = "";
+
+        /**
+         * 手动分片上传时,每个分片大小,单位字节,最小 1MB,最大 50MB,必须是 1MB 的整数倍,默认 1MB。
+         * 又拍云 USS 比较特殊,必须提前传入分片大小(最后一个分片可以小于此大小,但不能超过)
+         * 你可以在初始化文件时使用 putMetadata("X-Upyun-Multi-Part-Size", "1048576") 方法传入分片大小
+         */
+        private Integer multipartUploadPartSize = 1024 * 1024;
+
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+    }
+
+    /**
+     * MinIO
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class MinioConfig extends BaseConfig {
+
+        private String accessKey;
+
+        private String secretKey;
+
+        private String endPoint;
+
+        private String bucketName;
+
+        /**
+         * 访问域名
+         */
+        private String domain = "";
+
+        /**
+         * 基础路径
+         */
+        private String basePath = "";
+
+        /**
+         * 自动分片上传阈值,达到此大小则使用分片上传,默认 128MB。
+         * 在获取不到文件大小或达到这个阈值的情况下,会使用这里提供的分片大小,否则 MinIO 会自动分片大小
+         */
+        private int multipartThreshold = 128 * 1024 * 1024;
+        /**
+         * 自动分片上传时每个分片大小,默认 32MB
+         */
+        private int multipartPartSize = 32 * 1024 * 1024;
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+    }
+
+    /**
+     * Amazon S3
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class AmazonS3Config extends BaseConfig {
+
+        private String accessKey;
+
+        private String secretKey;
+
+        private String region;
+
+        private String endPoint;
+
+        private String bucketName;
+
+        /**
+         * 访问域名
+         */
+        private String domain = "";
+
+        /**
+         * 基础路径
+         */
+        private String basePath = "";
+
+        /**
+         * 默认的 ACL,详情 {@link Constant.AwsS3ACL}
+         */
+        private String defaultAcl;
+
+        /**
+         * 自动分片上传阈值,达到此大小则使用分片上传,默认 128MB
+         */
+        private int multipartThreshold = 128 * 1024 * 1024;
+
+        /**
+         * 自动分片上传时每个分片大小,默认 32MB
+         */
+        private int multipartPartSize = 32 * 1024 * 1024;
+
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+    }
+
+    /**
+     * FTP
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class FtpConfig extends BaseConfig {
+
+        /**
+         * 主机
+         */
+        private String host;
+
+        /**
+         * 端口,默认21
+         */
+        private int port = 21;
+
+        /**
+         * 用户名,默认 anonymous(匿名)
+         */
+        private String user = "anonymous";
+
+        /**
+         * 密码,默认空
+         */
+        private String password = "";
+
+        /**
+         * 编码,默认UTF-8
+         */
+        private Charset charset = StandardCharsets.UTF_8;
+
+        /**
+         * 连接超时时长,单位毫秒,默认10秒 {@link org.apache.commons.net.SocketClient#setConnectTimeout(int)}
+         */
+        private long connectionTimeout = 10 * 1000;
+
+        /**
+         * Socket连接超时时长,单位毫秒,默认10秒 {@link org.apache.commons.net.SocketClient#setSoTimeout(int)}
+         */
+        private long soTimeout = 10 * 1000;
+
+        /**
+         * 设置服务器语言,默认空,{@link org.apache.commons.net.ftp.FTPClientConfig#setServerLanguageCode(String)}
+         */
+        private String serverLanguageCode;
+
+        /**
+         * 服务器标识,默认空,{@link org.apache.commons.net.ftp.FTPClientConfig#FTPClientConfig(String)}
+         * 例如:org.apache.commons.net.ftp.FTPClientConfig.SYST_NT
+         */
+        private String systemKey;
+
+        /**
+         * 是否主动模式,默认被动模式
+         */
+        private Boolean isActive = false;
+
+        /**
+         * 访问域名
+         */
+        private String domain = "";
+
+        /**
+         * 基础路径
+         */
+        private String basePath = "";
+
+        /**
+         * 存储路径,上传的文件都会存储在这个路径下面,默认“/”,注意“/”结尾
+         */
+        private String storagePath = "/";
+
+        /**
+         * Client 对象池配置
+         */
+        private CommonClientPoolConfig pool = new CommonClientPoolConfig();
+
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+    }
+
+    /**
+     * SFTP
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class SftpConfig extends BaseConfig {
+
+        /**
+         * 主机
+         */
+        private String host;
+
+        /**
+         * 端口,默认22
+         */
+        private int port = 22;
+
+        /**
+         * 用户名
+         */
+        private String user;
+
+        /**
+         * 密码
+         */
+        private String password;
+
+        /**
+         * 私钥路径
+         */
+        private String privateKeyPath;
+
+        /**
+         * 编码,默认UTF-8
+         */
+        private Charset charset = StandardCharsets.UTF_8;
+
+        /**
+         * 连接超时时长,单位毫秒,默认10秒
+         */
+        private int connectionTimeout = 10 * 1000;
+
+        /**
+         * 访问域名
+         */
+        private String domain = "";
+
+        /**
+         * 基础路径
+         */
+        private String basePath = "";
+
+        /**
+         * 存储路径,上传的文件都会存储在这个路径下面,默认“/”,注意“/”结尾
+         */
+        private String storagePath = "/";
+
+        /**
+         * Client 对象池配置
+         */
+        private CommonClientPoolConfig pool = new CommonClientPoolConfig();
+
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+    }
+
+    /**
+     * WebDAV
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class WebDavConfig extends BaseConfig {
+
+        /**
+         * 服务器地址,注意“/”结尾,例如:http://192.168.1.105:8405/
+         */
+        private String server;
+
+        /**
+         * 用户名
+         */
+        private String user;
+
+        /**
+         * 密码
+         */
+        private String password;
+
+        /**
+         * 访问域名
+         */
+        private String domain = "";
+
+        /**
+         * 基础路径
+         */
+        private String basePath = "";
+
+        /**
+         * 存储路径,上传的文件都会存储在这个路径下面,默认“/”,注意“/”结尾
+         */
+        private String storagePath = "/";
+
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+    }
+
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class GoogleCloudStorageConfig extends BaseConfig {
+
+        private String projectId;
+
+        /**
+         * 证书路径,兼容Spring的ClassPath路径、文件路径、HTTP路径等
+         */
+        private String credentialsPath;
+
+        private String bucketName;
+
+        /**
+         * 访问域名
+         */
+        private String domain = "";
+
+        /**
+         * 基础路径
+         */
+        private String basePath = "";
+
+        /**
+         * 默认的 ACL,详情 {@link Constant.GoogleCloudStorageACL}
+         */
+        private String defaultAcl;
+
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+    }
+
+    /**
+     * FastDFS
+     * 兼容性说明:https://x-file-storage.xuyanwu.cn/2.2.0/#/%E5%AD%98%E5%82%A8%E5%B9%B3%E5%8F%B0?id=OCI_FastDFS
+     */
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class FastDfsConfig extends BaseConfig {
+        /**
+         * 运行模式,由于 FastDFS 比较特殊,不支持自定义文件名及路径,所以使用运行模式来解决这个问题。
+         * 详情请查看:https://x-file-storage.xuyanwu.cn/2.2.0/#/%E5%AD%98%E5%82%A8%E5%B9%B3%E5%8F%B0?id=OCI_FastDFS
+         */
+        private RunMod runMod = RunMod.COVER;
+
+        /**
+         * Tracker Server 配置
+         */
+        private FastDfsTrackerServer trackerServer;
+
+        /**
+         * Storage Server 配置(当不使用 Tracker Server 时使用)
+         */
+        private FastDfsStorageServer storageServer;
+
+        /**
+         * 额外扩展配置
+         */
+        private FastDfsExtra extra;
+
+        /**
+         * 访问域名
+         */
+        private String domain = "";
+
+        /**
+         * 基础路径,强烈建议留空
+         * 仅在上传成功时和获取文件时原样传到 FileInfo 及 RemoteFileInfo 中,可以用来保存到数据库中使用,
+         * 实际上作用也不大,还会破坏 url 约定(url:实际上就是 domain + basePath + path + filename),
+         * 约定详情见文档 https://x-file-storage.xuyanwu.cn/2.2.0/#/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98?id=%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E5%8F%8A-fileinfo-%E4%B8%AD%E5%90%84%E7%A7%8D%E8%B7%AF%E5%BE%84%EF%BC%88path%EF%BC%89%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F
+         * FastDFS 兼容性说明:https://x-file-storage.xuyanwu.cn/2.2.0/#/%E5%AD%98%E5%82%A8%E5%B9%B3%E5%8F%B0?id=OCI_FastDFS
+         */
+        private String basePath = "";
+
+        /**
+         * 自动分片上传阈值,达到此大小则使用分片上传,默认 128MB
+         */
+        private int multipartThreshold = 128 * 1024 * 1024;
+
+        /**
+         * 自动分片上传时每个分片大小,默认 32MB
+         */
+        private int multipartPartSize = 32 * 1024 * 1024;
+
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+
+        public String getGroupName() {
+            return Optional.ofNullable(extra).map(FastDfsExtra::getGroupName).orElse(StrUtil.EMPTY);
+        }
+
+        /**
+         * 运行模式
+         */
+        public enum RunMod {
+            /**
+             * 覆盖模式,强制用 FastDFS 返回的路径及文件名覆盖 FileInfo 中的 path 及 filename。
+             * 详情请查看:https://x-file-storage.xuyanwu.cn/2.2.0/#/%E5%AD%98%E5%82%A8%E5%B9%B3%E5%8F%B0?id=OCI_FastDFS
+             */
+            COVER,
+            /**
+             * URL模式,不覆盖 FileInfo 中的 path 及 filename。通过 url 解析 FastDFS 支持的路径及文件名
+             * 详情请查看:https://x-file-storage.xuyanwu.cn/2.2.0/#/%E5%AD%98%E5%82%A8%E5%B9%B3%E5%8F%B0?id=OCI_FastDFS
+             */
+            URL;
+        }
+
+        @Data
+        @Accessors(chain = true)
+        @EqualsAndHashCode
+        public static class FastDfsTrackerServer {
+
+            /**
+             * Tracker Server 地址(IP:PORT),多个用英文逗号隔开
+             */
+            private String serverAddr;
+
+            /**
+             * HTTP端口,默认:80
+             */
+            private Integer httpPort = 80;
+        }
+
+        @Data
+        @Accessors(chain = true)
+        @EqualsAndHashCode
+        public static class FastDfsStorageServer {
+
+            /**
+             * Storage Server 地址:IP:PORT
+             */
+            private String serverAddr;
+
+            /**
+             * Store path,默认 0
+             */
+            private Integer storePath = 0;
+        }
+
+        @Data
+        @Accessors(chain = true)
+        @EqualsAndHashCode
+        public static class FastDfsExtra {
+
+            /**
+             * 组名,可以为空
+             */
+            private String groupName = "";
+
+            /**
+             * 连接超时,单位:秒,默认:5s
+             */
+            private Integer connectTimeoutInSeconds = 5;
+
+            /**
+             * 套接字超时,单位:秒,默认:30s
+             */
+            private Integer networkTimeoutInSeconds = 30;
+
+            /**
+             * 字符编码,默认:UTF-8
+             */
+            private Charset charset = StandardCharsets.UTF_8;
+
+            /**
+             * token 防盗链 默认:false
+             */
+            private Boolean httpAntiStealToken = false;
+
+            /**
+             * 安全密钥,默认:FastDFS1234567890
+             */
+            private String httpSecretKey = "FastDFS1234567890";
+
+            /**
+             * 是否启用连接池。默认:true
+             */
+            private Boolean connectionPoolEnabled = true;
+
+            /**
+             * #每一个IP:Port的最大连接数,0为没有限制,默认:100
+             */
+            private Integer connectionPoolMaxCountPerEntry = 100;
+
+            /**
+             * 连接池最大空闲时间。单位:秒,默认:3600
+             */
+            private Integer connectionPoolMaxIdleTime = 3600;
+
+            /**
+             * 连接池最大等待时间。单位:毫秒,默认:1000
+             */
+            private Integer connectionPoolMaxWaitTimeInMs = 1000;
+        }
+    }
+
+    @Data
+    @Accessors(chain = true)
+    @EqualsAndHashCode(callSuper = true)
+    public static class AzureBlobStorageConfig extends BaseConfig {
+
+        /**
+         * 终结点 AzureBlob控制台-设置-终结点-主终结点-Blob服务
+         */
+        private String endPoint;
+
+        /**
+         * 访问域名,注意“/”结尾,与 end-point 保持一致
+         */
+        private String domain = "";
+
+        /**
+         * 容器名称,类似于 s3 的 bucketName,AzureBlob控制台-数据存储-容器
+         */
+        private String containerName;
+
+        /**
+         * 基础路径
+         */
+        private String basePath = "";
+
+        /**
+         * 默认的 ACL,详情 {@link Constant.AzureBlobStorageACL}
+         */
+        private String defaultAcl;
+
+        /**
+         * 连接字符串,AzureBlob控制台-安全性和网络-访问秘钥-连接字符串
+         */
+        private String connectionString;
+
+        /**
+         * 自动分片上传阈值,超过此大小则使用分片上传,默认值256M
+         */
+        private long multipartThreshold = 256 * 1024 * 1024L;
+        /**
+         * 自动分片上传时每个分片大小,默认 4MB
+         */
+        private long multipartPartSize = 4 * 1024 * 1024L;
+
+        /**
+         * 最大上传并行度
+         * 分片后 同时进行上传的 数量
+         * 数量太大会占用大量缓冲区
+         * 默认 8
+         */
+        private int maxConcurrency = 8;
+
+        /**
+         * 预签名 URL 时,传入的 HTTP method 与 Azure Blob Storage 中的 SAS 权限映射表,
+         * 目前默认支持 GET (获取),PUT(上传),DELETE(删除),
+         * 其它可以自行扩展,例如你想自定义一个 ALL 的 method,赋予所有权限,可以写为 .put("ALL", "racwdxytlmei")
+         * {@link com.azure.storage.blob.sas.BlobSasPermission}
+         */
+        private Map<String, String> methodToPermissionMap = MapBuilder.create(new HashMap<String, String>())
+                .put(Constant.GeneratePresignedUrl.Method.GET, "r") // 获取
+                .put(Constant.GeneratePresignedUrl.Method.PUT, "w") // 上传
+                .put(Constant.GeneratePresignedUrl.Method.DELETE, "d") // 删除
+                // .put("ALL", "racwdxytlmei")    //自定义一个名为 ALL 的 method,赋予所有权限
+                .build();
+
+        /**
+         * 其它自定义配置
+         */
+        private Map<String, Object> attr = new LinkedHashMap<>();
+    }
+
+    /**
+     * 通用的 Client 对象池配置,详情见 {@link org.apache.commons.pool2.impl.GenericObjectPoolConfig}
+     */
+    @Data
+    @Accessors(chain = true)
+    public static class CommonClientPoolConfig {
+
+        /**
+         * 取出对象前进行校验,默认开启
+         */
+        private Boolean testOnBorrow = true;
+
+        /**
+         * 空闲检测,默认开启
+         */
+        private Boolean testWhileIdle = true;
+
+        /**
+         * 最大总数量,超过此数量会进行阻塞等待,默认 16
+         */
+        private Integer maxTotal = 16;
+
+        /**
+         * 最大空闲数量,默认 4
+         */
+        private Integer maxIdle = 4;
+
+        /**
+         * 最小空闲数量,默认 1
+         */
+        private Integer minIdle = 1;
+
+        /**
+         * 空闲对象逐出(销毁)运行间隔时间,默认 30 秒
+         */
+        private Duration timeBetweenEvictionRuns = Duration.ofSeconds(30);
+
+        /**
+         * 对象空闲超过此时间将逐出(销毁),为负数则关闭此功能,默认 -1
+         */
+        private Duration minEvictableIdleDuration = Duration.ofMillis(-1);
+
+        /**
+         * 对象空闲超过此时间且当前对象池的空闲对象数大于最小空闲数量,将逐出(销毁),为负数则关闭此功能,默认 30 分钟
+         */
+        private Duration softMinEvictableIdleDuration = Duration.ofMillis(30);
+
+        public <T> GenericObjectPoolConfig<T> toGenericObjectPoolConfig() {
+            GenericObjectPoolConfig<T> config = new GenericObjectPoolConfig<>();
+            config.setTestOnBorrow(testOnBorrow);
+            config.setTestWhileIdle(testWhileIdle);
+            config.setMaxTotal(maxTotal);
+            config.setMinIdle(minIdle);
+            config.setMaxIdle(maxIdle);
+            config.setTimeBetweenEvictionRuns(timeBetweenEvictionRuns);
+            config.setMinEvictableIdleTime(minEvictableIdleDuration);
+            config.setSoftMinEvictableIdleTime(softMinEvictableIdleDuration);
+            return config;
+        }
+    }
+}

+ 740 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/FileStorageService.java

@@ -0,0 +1,740 @@
+package org.dromara.x.file.storage.core;
+
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
+import java.io.IOException;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Predicate;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.x.file.storage.core.aspect.*;
+import org.dromara.x.file.storage.core.constant.Constant;
+import org.dromara.x.file.storage.core.copy.CopyPretreatment;
+import org.dromara.x.file.storage.core.exception.FileStorageRuntimeException;
+import org.dromara.x.file.storage.core.file.FileWrapper;
+import org.dromara.x.file.storage.core.file.FileWrapperAdapter;
+import org.dromara.x.file.storage.core.file.HttpServletRequestFileWrapper;
+import org.dromara.x.file.storage.core.file.MultipartFormDataReader;
+import org.dromara.x.file.storage.core.get.GetFilePretreatment;
+import org.dromara.x.file.storage.core.get.ListFilesPretreatment;
+import org.dromara.x.file.storage.core.get.ListFilesSupportInfo;
+import org.dromara.x.file.storage.core.get.RemoteFileInfo;
+import org.dromara.x.file.storage.core.move.MovePretreatment;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.presigned.GeneratePresignedUrlPretreatment;
+import org.dromara.x.file.storage.core.presigned.GeneratePresignedUrlResult;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+import org.dromara.x.file.storage.core.tika.ContentTypeDetect;
+import org.dromara.x.file.storage.core.upload.*;
+import org.dromara.x.file.storage.core.upload.MultipartUploadSupportInfo;
+import org.dromara.x.file.storage.core.upload.UploadPretreatment;
+import org.dromara.x.file.storage.core.util.Tools;
+
+/**
+ * 用来处理文件存储,对接多个平台
+ */
+@Slf4j
+@Getter
+@Setter
+public class FileStorageService {
+
+    private FileStorageService self;
+    private FileStorageProperties properties;
+    private FileRecorder fileRecorder;
+    private CopyOnWriteArrayList<FileStorage> fileStorageList;
+    private CopyOnWriteArrayList<FileStorageAspect> aspectList;
+    private CopyOnWriteArrayList<FileWrapperAdapter> fileWrapperAdapterList;
+    private ContentTypeDetect contentTypeDetect;
+
+    /**
+     * 获取默认的存储平台,请使用 getProperties().getDefaultPlatform() 代替
+     */
+    @Deprecated
+    public String getDefaultPlatform() {
+        return properties.getDefaultPlatform();
+    }
+
+    /**
+     * 缩略图后缀,例如【.min.jpg】【.png】,请使用 getProperties().getThumbnailSuffix() 代替
+     */
+    @Deprecated
+    public String getThumbnailSuffix() {
+        return properties.getThumbnailSuffix();
+    }
+
+    /**
+     * 获取默认的存储平台
+     */
+    public <T extends FileStorage> T getFileStorage() {
+        return self.getFileStorage(properties.getDefaultPlatform());
+    }
+
+    /**
+     * 获取对应的存储平台
+     */
+    public <T extends FileStorage> T getFileStorage(String platform) {
+        for (FileStorage fileStorage : fileStorageList) {
+            if (fileStorage.getPlatform().equals(platform)) {
+                return Tools.cast(fileStorage);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 获取对应的存储平台,如果存储平台不存在则抛出异常
+     */
+    public <T extends FileStorage> T getFileStorageVerify(FileInfo fileInfo) {
+        return self.getFileStorageVerify(fileInfo.getPlatform());
+    }
+
+    /**
+     * 获取对应的存储平台,如果存储平台不存在则抛出异常
+     */
+    public <T extends FileStorage> T getFileStorageVerify(String platform) {
+        T fileStorage = self.getFileStorage(platform);
+        if (fileStorage == null)
+            throw new FileStorageRuntimeException(StrUtil.format("没有找到对应的存储平台!platform:{}", platform));
+        return fileStorage;
+    }
+
+    /**
+     * 上传文件,成功返回文件信息,失败返回 null,请使用 pre.upload() 代替
+     */
+    @Deprecated
+    public FileInfo upload(UploadPretreatment pre) {
+        return pre.upload();
+    }
+
+    /**
+     * 根据 url 获取 FileInfo
+     */
+    public FileInfo getFileInfoByUrl(String url) {
+        return fileRecorder.getByUrl(url);
+    }
+
+    /**
+     * 根据 url 删除文件
+     */
+    public boolean delete(String url) {
+        return self.delete(getFileInfoByUrl(url));
+    }
+
+    /**
+     * 根据 url 删除文件
+     */
+    public boolean delete(String url, Predicate<FileInfo> predicate) {
+        return self.delete(getFileInfoByUrl(url), predicate);
+    }
+
+    /**
+     * 根据条件
+     */
+    public boolean delete(FileInfo fileInfo) {
+        return self.delete(fileInfo, null);
+    }
+
+    /**
+     * 根据条件删除文件
+     */
+    public boolean delete(FileInfo fileInfo, Predicate<FileInfo> predicate) {
+        if (fileInfo == null) return true;
+        if (predicate != null && !predicate.test(fileInfo)) return false;
+        FileStorage fileStorage = self.getFileStorage(fileInfo.getPlatform());
+        if (fileStorage == null) throw new FileStorageRuntimeException("没有找到对应的存储平台!");
+        return self.delete(fileInfo, fileStorage, fileRecorder, aspectList);
+    }
+
+    /**
+     * 删除文件,仅限内部使用
+     */
+    public boolean delete(
+            FileInfo fileInfo, FileStorage fileStorage, FileRecorder fileRecorder, List<FileStorageAspect> aspectList) {
+        return new DeleteAspectChain(aspectList, (_fileInfo, _fileStorage, _fileRecorder) -> {
+                    if (_fileStorage.delete(_fileInfo)) { // 删除文件
+                        return _fileRecorder.delete(_fileInfo.getUrl()); // 删除文件记录
+                    }
+                    return false;
+                })
+                .next(fileInfo, fileStorage, fileRecorder);
+    }
+
+    /**
+     * 文件是否存在
+     */
+    public boolean exists(String url) {
+        return self.exists(getFileInfoByUrl(url));
+    }
+
+    /**
+     * 文件是否存在
+     */
+    public boolean exists(FileInfo fileInfo) {
+        if (fileInfo == null) return false;
+        return new ExistsAspectChain(aspectList, (_fileInfo, _fileStorage) -> _fileStorage.exists(_fileInfo))
+                .next(fileInfo, getFileStorageVerify(fileInfo));
+    }
+
+    /**
+     * 获取文件下载器
+     */
+    public Downloader download(FileInfo fileInfo) {
+        return new Downloader(fileInfo, aspectList, getFileStorageVerify(fileInfo), Downloader.TARGET_FILE);
+    }
+
+    /**
+     * 获取文件下载器
+     */
+    public Downloader download(String url) {
+        return self.download(getFileInfoByUrl(url));
+    }
+
+    /**
+     * 获取缩略图文件下载器
+     */
+    public Downloader downloadTh(FileInfo fileInfo) {
+        return new Downloader(fileInfo, aspectList, getFileStorageVerify(fileInfo), Downloader.TARGET_TH_FILE);
+    }
+
+    /**
+     * 获取缩略图文件下载器
+     */
+    public Downloader downloadTh(String url) {
+        return self.downloadTh(getFileInfoByUrl(url));
+    }
+
+    /**
+     * 是否支持对文件生成可以签名访问的 URL
+     */
+    public boolean isSupportPresignedUrl() {
+        return self.isSupportPresignedUrl(properties.getDefaultPlatform());
+    }
+
+    /**
+     * 是否支持对文件生成可以签名访问的 URL
+     */
+    public boolean isSupportPresignedUrl(String platform) {
+        FileStorage storage = self.getFileStorageVerify(platform);
+        return self.isSupportPresignedUrl(storage);
+    }
+
+    /**
+     * 是否支持对文件生成可以签名访问的 URL
+     */
+    public boolean isSupportPresignedUrl(FileStorage fileStorage) {
+        if (fileStorage == null) return false;
+        return new IsSupportPresignedUrlAspectChain(aspectList, FileStorage::isSupportPresignedUrl).next(fileStorage);
+    }
+
+    /**
+     * 生成预签名 URL
+     * @return 生成预签名 URL 预处理器
+     */
+    public GeneratePresignedUrlPretreatment generatePresignedUrl() {
+        return new GeneratePresignedUrlPretreatment()
+                .setFileStorageService(self)
+                .setPlatform(properties.getDefaultPlatform());
+    }
+
+    /**
+     * 对文件生成可以签名访问的 URL,无法生成则返回 null
+     *
+     * @param expiration 到期时间
+     */
+    public String generatePresignedUrl(FileInfo fileInfo, Date expiration) {
+        if (fileInfo == null) return null;
+        GeneratePresignedUrlResult result = generatePresignedUrl()
+                .setExpiration(expiration)
+                .setPath(fileInfo.getPath())
+                .setFilename(fileInfo.getFilename())
+                .setMethod(Constant.GeneratePresignedUrl.Method.GET)
+                .generatePresignedUrl();
+        return result == null ? null : result.getUrl();
+    }
+
+    /**
+     * 对缩略图文件生成可以签名访问的 URL,无法生成则返回 null
+     *
+     * @param expiration 到期时间
+     */
+    public String generateThPresignedUrl(FileInfo fileInfo, Date expiration) {
+        if (fileInfo == null) return null;
+        GeneratePresignedUrlResult result = generatePresignedUrl()
+                .setExpiration(expiration)
+                .setPath(fileInfo.getPath())
+                .setFilename(fileInfo.getThFilename())
+                .setMethod(Constant.GeneratePresignedUrl.Method.GET)
+                .generatePresignedUrl();
+        return result == null ? null : result.getUrl();
+    }
+
+    /**
+     * 是否支持对文件的访问控制列表
+     */
+    public boolean isSupportAcl() {
+        return self.isSupportAcl(properties.getDefaultPlatform());
+    }
+
+    /**
+     * 是否支持对文件的访问控制列表
+     */
+    public boolean isSupportAcl(String platform) {
+        FileStorage storage = self.getFileStorageVerify(platform);
+        return self.isSupportAcl(storage);
+    }
+
+    /**
+     * 是否支持对文件的访问控制列表
+     */
+    public boolean isSupportAcl(FileStorage fileStorage) {
+        if (fileStorage == null) return false;
+        return new IsSupportAclAspectChain(aspectList, FileStorage::isSupportAcl).next(fileStorage);
+    }
+
+    /**
+     * 设置文件的访问控制列表,一般情况下只有对象存储支持该功能
+     * 详情见{@link FileInfo#setFileAcl}
+     */
+    public boolean setFileAcl(FileInfo fileInfo, Object acl) {
+        if (fileInfo == null) return false;
+        return new SetFileAclAspectChain(
+                        aspectList, (_fileInfo, _acl, _fileStorage) -> _fileStorage.setFileAcl(_fileInfo, _acl))
+                .next(fileInfo, acl, self.getFileStorageVerify(fileInfo));
+    }
+
+    /**
+     * 设置缩略图文件的访问控制列表,一般情况下只有对象存储支持该功能
+     * 详情见{@link FileInfo#setFileAcl}
+     */
+    public boolean setThFileAcl(FileInfo fileInfo, Object acl) {
+        if (fileInfo == null) return false;
+        return new SetThFileAclAspectChain(
+                        aspectList, (_fileInfo, _acl, _fileStorage) -> _fileStorage.setThFileAcl(_fileInfo, _acl))
+                .next(fileInfo, acl, self.getFileStorageVerify(fileInfo));
+    }
+
+    /**
+     * 是否支持 Metadata
+     */
+    public boolean isSupportMetadata() {
+        return self.isSupportMetadata(properties.getDefaultPlatform());
+    }
+
+    /**
+     * 是否支持 Metadata
+     */
+    public boolean isSupportMetadata(String platform) {
+        FileStorage storage = self.getFileStorageVerify(platform);
+        return self.isSupportMetadata(storage);
+    }
+
+    /**
+     * 是否支持 Metadata
+     */
+    public boolean isSupportMetadata(FileStorage fileStorage) {
+        if (fileStorage == null) return false;
+        return new IsSupportMetadataAspectChain(aspectList, FileStorage::isSupportMetadata).next(fileStorage);
+    }
+
+    /**
+     * 创建上传预处理器
+     */
+    public UploadPretreatment of() {
+        UploadPretreatment pre = new UploadPretreatment();
+        pre.setFileStorageService(self);
+        pre.setPlatform(properties.getDefaultPlatform());
+        pre.setThumbnailSuffix(properties.getThumbnailSuffix());
+        pre.setNotSupportMetadataThrowException(properties.getUploadNotSupportMetadataThrowException());
+        pre.setNotSupportAclThrowException(properties.getUploadNotSupportAclThrowException());
+        return pre;
+    }
+
+    /**
+     * 创建上传预处理器
+     *
+     * @param source 源
+     */
+    public UploadPretreatment of(Object source) {
+        return self.of(source, null, null);
+    }
+
+    /**
+     * 创建上传预处理器
+     *
+     * @param source 源
+     * @param name   文件名
+     */
+    public UploadPretreatment of(Object source, String name) {
+        return self.of(source, name, null);
+    }
+
+    /**
+     * 创建上传预处理器
+     *
+     * @param source      源
+     * @param name        文件名
+     * @param contentType 文件的 MIME 类型
+     */
+    public UploadPretreatment of(Object source, String name, String contentType) {
+        return self.of(source, name, contentType, null);
+    }
+
+    /**
+     * 默认使用的存储平台是否支持手动分片上传
+     */
+    public MultipartUploadSupportInfo isSupportMultipartUpload() {
+        return self.isSupportMultipartUpload(properties.getDefaultPlatform());
+    }
+
+    /**
+     * 是否支持手动分片上传
+     */
+    public MultipartUploadSupportInfo isSupportMultipartUpload(String platform) {
+        FileStorage storage = self.getFileStorageVerify(platform);
+        return self.isSupportMultipartUpload(storage);
+    }
+
+    /**
+     * 是否支持手动分片上传
+     */
+    public MultipartUploadSupportInfo isSupportMultipartUpload(FileStorage fileStorage) {
+        if (fileStorage == null) return MultipartUploadSupportInfo.notSupport();
+        return new IsSupportMultipartUploadAspectChain(aspectList, FileStorage::isSupportMultipartUpload)
+                .next(fileStorage);
+    }
+
+    /**
+     * 手动分片上传-初始化
+     */
+    public InitiateMultipartUploadPretreatment initiateMultipartUpload() {
+        InitiateMultipartUploadPretreatment pre = new InitiateMultipartUploadPretreatment();
+        pre.setFileStorageService(self);
+        pre.setPlatform(properties.getDefaultPlatform());
+        pre.setNotSupportMetadataThrowException(properties.getUploadNotSupportMetadataThrowException());
+        pre.setNotSupportAclThrowException(properties.getUploadNotSupportAclThrowException());
+        return pre;
+    }
+
+    /**
+     * 手动分片上传-上传分片
+     * @param fileInfo 文件信息
+     * @param partNumber 分片号。每一个上传的分片都有一个分片号,一般情况下取值范围是1~10000
+     * @param source      源
+     * @return 手动分片上传-上传分片预处理器
+     */
+    public UploadPartPretreatment uploadPart(FileInfo fileInfo, int partNumber, Object source) {
+        return self.uploadPart(fileInfo, partNumber, source, null);
+    }
+
+    /**
+     * 手动分片上传-上传分片
+     * @param fileInfo 文件信息
+     * @param partNumber 分片号。每一个上传的分片都有一个分片号,一般情况下取值范围是1~10000
+     * @param source      源
+     * @param size        源的文件大小
+     * @return 手动分片上传-上传分片预处理器
+     */
+    public UploadPartPretreatment uploadPart(FileInfo fileInfo, int partNumber, Object source, Long size) {
+        UploadPartPretreatment pre = new UploadPartPretreatment();
+        pre.setFileStorageService(self);
+        pre.setFileInfo(fileInfo);
+        pre.setPartNumber(partNumber);
+        // 这是是个优化,如果是 FileWrapper 对象就直接使用,否则就创建 FileWrapper 并指定 contentType 避免自动识别造成性能浪费
+        if (source instanceof FileWrapper) {
+            pre.setPartFileWrapper((FileWrapper) source);
+        } else {
+            pre.setPartFileWrapper(self.wrapper(source, null, "application/octet-stream", size));
+        }
+        return pre;
+    }
+
+    /**
+     * 手动分片上传-完成
+     * @param fileInfo 文件信息,如果在初始化时传入了 ACL 访问控制列表、Metadata 元数据等信息,
+     *                 一定要保证这里的 fileInfo 也有相同的信息,否则有些存储平台会不生效,
+     *                 这是因为每个存储平台的逻辑不一样,有些是初始化时传入的,有些是完成时传入的,
+     *                 建议将 FileInfo 保存到数据库中,这样就可以使用 fileStorageService.getFileInfoByUrl("https://abc.def.com/xxx.png")
+     *                 来获取 FileInfo 方便操作,详情请阅读 https://x-file-storage.xuyanwu.cn/2.2.0/#/%E5%9F%BA%E7%A1%80%E5%8A%9F%E8%83%BD?id=%E4%BF%9D%E5%AD%98%E4%B8%8A%E4%BC%A0%E8%AE%B0%E5%BD%95
+     */
+    public CompleteMultipartUploadPretreatment completeMultipartUpload(FileInfo fileInfo) {
+        CompleteMultipartUploadPretreatment pre = new CompleteMultipartUploadPretreatment();
+        pre.setFileStorageService(self);
+        pre.setFileInfo(fileInfo);
+        return pre;
+    }
+
+    /**
+     * 手动分片上传-取消
+     */
+    public AbortMultipartUploadPretreatment abortMultipartUpload(FileInfo fileInfo) {
+        AbortMultipartUploadPretreatment pre = new AbortMultipartUploadPretreatment();
+        pre.setFileStorageService(self);
+        pre.setFileInfo(fileInfo);
+        return pre;
+    }
+
+    /**
+     * 手动分片上传-列举已上传的分片
+     */
+    public ListPartsPretreatment listParts(FileInfo fileInfo) {
+        ListPartsPretreatment pre = new ListPartsPretreatment();
+        pre.setFileStorageService(self);
+        pre.setFileInfo(fileInfo);
+        return pre;
+    }
+
+    /**
+     * 默认使用的存储平台是否支持列举文件
+     */
+    public ListFilesSupportInfo isSupportListFiles() {
+        return self.isSupportListFiles(properties.getDefaultPlatform());
+    }
+
+    /**
+     * 是否支持列举文件
+     */
+    public ListFilesSupportInfo isSupportListFiles(String platform) {
+        FileStorage storage = self.getFileStorageVerify(platform);
+        return self.isSupportListFiles(storage);
+    }
+
+    /**
+     * 是否支持列举文件
+     */
+    public ListFilesSupportInfo isSupportListFiles(FileStorage fileStorage) {
+        if (fileStorage == null) return ListFilesSupportInfo.notSupport();
+        return new IsSupportListFilesAspectChain(aspectList, FileStorage::isSupportListFiles).next(fileStorage);
+    }
+
+    /**
+     * 列举文件
+     */
+    public ListFilesPretreatment listFiles() {
+        ListFilesPretreatment pre = new ListFilesPretreatment();
+        pre.setPlatform(properties.getDefaultPlatform());
+        pre.setFileStorageService(self);
+        return pre;
+    }
+
+    /**
+     * 获取文件
+     */
+    public GetFilePretreatment getFile() {
+        return new GetFilePretreatment()
+                .setPlatform(properties.getDefaultPlatform())
+                .setFileStorageService(self);
+    }
+
+    /**
+     * 获取文件
+     * @param fileInfo 文件信息
+     * @return 远程文件信息
+     */
+    public RemoteFileInfo getFile(FileInfo fileInfo) {
+        return getFile()
+                .setPlatform(fileInfo.getPlatform())
+                .setPath(fileInfo.getPath() != null, fileInfo.getPath())
+                .setFilename(fileInfo.getFilename() != null, fileInfo.getFilename())
+                .setUrl(fileInfo.getUrl() != null, fileInfo.getUrl())
+                .getFile();
+    }
+
+    /**
+     * 获取缩略图文件
+     * @param fileInfo 文件信息
+     * @return 远程文件信息
+     */
+    public RemoteFileInfo getThFile(FileInfo fileInfo) {
+        return getFile()
+                .setPlatform(fileInfo.getPlatform())
+                .setPath(fileInfo.getPath() != null, fileInfo.getPath())
+                .setFilename(fileInfo.getThFilename() != null, fileInfo.getThFilename())
+                .setUrl(fileInfo.getThUrl() != null, fileInfo.getThUrl())
+                .getFile();
+    }
+
+    /**
+     * 创建上传预处理器
+     *
+     * @param source      源
+     * @param name        文件名
+     * @param contentType 文件的 MIME 类型
+     * @param size        文件大小
+     */
+    public UploadPretreatment of(Object source, String name, String contentType, Long size) {
+        FileWrapper wrapper = self.wrapper(source, name, contentType, size);
+        UploadPretreatment up = self.of().setFileWrapper(wrapper);
+        // 这里针对 HttpServletRequestFileWrapper 特殊处理,加载读取到的缩略图文件
+        if (wrapper instanceof HttpServletRequestFileWrapper) {
+            MultipartFormDataReader.MultipartFormData data =
+                    ((HttpServletRequestFileWrapper) wrapper).getMultipartFormData();
+            if (data.getThFileBytes() != null) {
+                FileWrapper thWrapper = self.wrapper(
+                        data.getThFileBytes(), data.getThFileOriginalFilename(), data.getThFileContentType());
+                up.thumbnailOf(thWrapper);
+            }
+        }
+        return up;
+    }
+
+    /**
+     * 对要上传的文件进行包装
+     *
+     * @param source 源
+     */
+    public FileWrapper wrapper(Object source) {
+        return self.wrapper(source, null);
+    }
+
+    /**
+     * 对要上传的文件进行包装
+     *
+     * @param source 源
+     * @param name   文件名
+     */
+    public FileWrapper wrapper(Object source, String name) {
+        return self.wrapper(source, name, null);
+    }
+
+    /**
+     * 对要上传的文件进行包装
+     *
+     * @param source 源
+     * @param name   文件名
+     */
+    public FileWrapper wrapper(Object source, String name, String contentType) {
+        return self.wrapper(source, name, contentType, null);
+    }
+
+    /**
+     * 对要上传的文件进行包装
+     *
+     * @param source      源
+     * @param name        文件名
+     * @param contentType 文件的 MIME 类型
+     * @param size        文件大小
+     */
+    public FileWrapper wrapper(Object source, String name, String contentType, Long size) {
+        if (source == null) {
+            throw new FileStorageRuntimeException("要包装的文件不能是 null");
+        }
+        try {
+            for (FileWrapperAdapter adapter : fileWrapperAdapterList) {
+                if (adapter.isSupport(source)) {
+                    return adapter.getFileWrapper(source, name, contentType, size);
+                }
+            }
+        } catch (IOException e) {
+            throw new FileStorageRuntimeException("文件包装失败", e);
+        }
+        throw new FileStorageRuntimeException("不支持此文件");
+    }
+
+    /**
+     * 是否支持同存储平台复制文件
+     */
+    public boolean isSupportSameCopy() {
+        return self.isSupportSameCopy(properties.getDefaultPlatform());
+    }
+
+    /**
+     * 是否支持同存储平台复制文件
+     */
+    public boolean isSupportSameCopy(String platform) {
+        FileStorage storage = self.getFileStorageVerify(platform);
+        return self.isSupportSameCopy(storage);
+    }
+
+    /**
+     * 是否支持同存储平台复制文件
+     */
+    public boolean isSupportSameCopy(FileStorage fileStorage) {
+        if (fileStorage == null) return false;
+        return new IsSupportSameCopyAspectChain(aspectList, FileStorage::isSupportSameCopy).next(fileStorage);
+    }
+
+    /**
+     * 复制文件
+     */
+    public CopyPretreatment copy(FileInfo fileInfo) {
+        return new CopyPretreatment(fileInfo, self)
+                .setNotSupportMetadataThrowException(properties.getCopyNotSupportMetadataThrowException())
+                .setNotSupportAclThrowException(properties.getCopyNotSupportAclThrowException());
+    }
+
+    /**
+     * 复制文件
+     */
+    public CopyPretreatment copy(String url) {
+        return self.copy(self.getFileInfoByUrl(url));
+    }
+
+    /**
+     * 是否支持同存储平台移动文件
+     */
+    public boolean isSupportSameMove() {
+        return self.isSupportSameMove(properties.getDefaultPlatform());
+    }
+
+    /**
+     * 是否支持同存储平台移动文件
+     */
+    public boolean isSupportSameMove(String platform) {
+        FileStorage storage = self.getFileStorageVerify(platform);
+        return self.isSupportSameMove(storage);
+    }
+
+    /**
+     * 是否支持同存储平台移动文件
+     */
+    public boolean isSupportSameMove(FileStorage fileStorage) {
+        if (fileStorage == null) return false;
+        return new IsSupportSameMoveAspectChain(aspectList, FileStorage::isSupportSameMove).next(fileStorage);
+    }
+
+    /**
+     * 移动文件
+     */
+    public MovePretreatment move(FileInfo fileInfo) {
+        return new MovePretreatment(fileInfo, self)
+                .setNotSupportMetadataThrowException(properties.getMoveNotSupportMetadataThrowException())
+                .setNotSupportAclThrowException(properties.getMoveNotSupportAclThrowException());
+    }
+
+    /**
+     * 移动文件
+     */
+    public MovePretreatment move(String url) {
+        return self.move(self.getFileInfoByUrl(url));
+    }
+
+    /**
+     * 通过反射调用指定存储平台的方法
+     * 详情见{@link ReflectUtil#invoke(Object,String,Object...)}
+     */
+    public <T> T invoke(String platform, String method, Object... args) {
+        return self.invoke((FileStorage) self.getFileStorageVerify(platform), method, args);
+    }
+
+    /**
+     * 通过反射调用指定存储平台的方法
+     * 详情见{@link ReflectUtil#invoke(Object,String,Object...)}
+     */
+    public <T> T invoke(FileStorage platform, String method, Object... args) {
+        return new InvokeAspectChain(aspectList, ReflectUtil::invoke).next(platform, method, args);
+    }
+
+    public void destroy() {
+        for (FileStorage fileStorage : fileStorageList) {
+            try {
+                fileStorage.close();
+                log.info("销毁存储平台 {} 成功", fileStorage.getPlatform());
+            } catch (Exception e) {
+                log.error("销毁存储平台 {} 失败,{}", fileStorage.getPlatform(), e.getMessage(), e);
+            }
+        }
+    }
+}

+ 619 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/FileStorageServiceBuilder.java

@@ -0,0 +1,619 @@
+package org.dromara.x.file.storage.core;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.extra.ftp.Ftp;
+import cn.hutool.extra.ssh.Sftp;
+import com.aliyun.oss.OSS;
+import com.amazonaws.services.s3.AmazonS3;
+import com.baidubce.services.bos.BosClient;
+import com.github.sardine.Sardine;
+import com.google.cloud.storage.Storage;
+import com.obs.services.ObsClient;
+import com.qcloud.cos.COSClient;
+import com.upyun.RestManager;
+import io.minio.MinioClient;
+import java.util.*;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import org.csource.fastdfs.StorageClient;
+import org.dromara.x.file.storage.core.FileStorageProperties.*;
+import org.dromara.x.file.storage.core.aspect.FileStorageAspect;
+import org.dromara.x.file.storage.core.exception.FileStorageRuntimeException;
+import org.dromara.x.file.storage.core.file.*;
+import org.dromara.x.file.storage.core.platform.*;
+import org.dromara.x.file.storage.core.platform.AzureBlobStorageFileStorageClientFactory.AzureBlobStorageClient;
+import org.dromara.x.file.storage.core.platform.QiniuKodoFileStorageClientFactory.QiniuKodoClient;
+import org.dromara.x.file.storage.core.recorder.DefaultFileRecorder;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+import org.dromara.x.file.storage.core.tika.ContentTypeDetect;
+import org.dromara.x.file.storage.core.tika.DefaultTikaFactory;
+import org.dromara.x.file.storage.core.tika.TikaContentTypeDetect;
+import org.dromara.x.file.storage.core.tika.TikaFactory;
+import org.dromara.x.file.storage.core.util.Tools;
+
+@Slf4j
+@Getter
+@Setter
+@Accessors(chain = true)
+public class FileStorageServiceBuilder {
+    /**
+     * 配置参数
+     */
+    private FileStorageProperties properties;
+    /**
+     * 文件记录记录者
+     */
+    private FileRecorder fileRecorder;
+    /**
+     * Tika 工厂类
+     */
+    private TikaFactory tikaFactory;
+    /**
+     * 识别文件的 MIME 类型
+     */
+    private ContentTypeDetect contentTypeDetect;
+    /**
+     * 切面
+     */
+    private List<FileStorageAspect> aspectList = new ArrayList<>();
+    /**
+     * 文件包装适配器
+     */
+    private List<FileWrapperAdapter> fileWrapperAdapterList = new ArrayList<>();
+    /**
+     * Client 工厂
+     */
+    private List<List<FileStorageClientFactory<?>>> clientFactoryList = new ArrayList<>();
+    /**
+     * 存储平台
+     */
+    private List<FileStorage> fileStorageList = new ArrayList<>();
+
+    public FileStorageServiceBuilder(FileStorageProperties properties) {
+        this.properties = properties;
+    }
+
+    /**
+     * 设置默认的文件记录者
+     */
+    public FileStorageServiceBuilder setDefaultFileRecorder() {
+        fileRecorder = new DefaultFileRecorder();
+        return this;
+    }
+
+    /**
+     * 设置默认的 Tika 工厂类
+     */
+    public FileStorageServiceBuilder setDefaultTikaFactory() {
+        tikaFactory = new DefaultTikaFactory();
+        return this;
+    }
+
+    /**
+     * 设置基于 Tika 识别文件的 MIME 类型
+     */
+    public FileStorageServiceBuilder setTikaContentTypeDetect() {
+        if (tikaFactory == null) throw new FileStorageRuntimeException("请先设置 TikaFactory");
+        contentTypeDetect = new TikaContentTypeDetect(tikaFactory);
+        return this;
+    }
+
+    /**
+     * 添加切面
+     */
+    public FileStorageServiceBuilder addAspect(FileStorageAspect aspect) {
+        aspectList.add(aspect);
+        return this;
+    }
+
+    /**
+     * 添加文件包装适配器
+     */
+    public FileStorageServiceBuilder addFileWrapperAdapter(FileWrapperAdapter adapter) {
+        fileWrapperAdapterList.add(adapter);
+        return this;
+    }
+
+    /**
+     * 添加 byte[] 文件包装适配器
+     */
+    public FileStorageServiceBuilder addByteFileWrapperAdapter() {
+        if (contentTypeDetect == null) throw new FileStorageRuntimeException("请先设置 TikaFactory 和 ContentTypeDetect");
+        fileWrapperAdapterList.add(new ByteFileWrapperAdapter(contentTypeDetect));
+        return this;
+    }
+
+    /**
+     * 添加 InputStream 文件包装适配器
+     */
+    public FileStorageServiceBuilder addInputStreamFileWrapperAdapter() {
+        if (contentTypeDetect == null) throw new FileStorageRuntimeException("请先设置 TikaFactory 和 ContentTypeDetect");
+        fileWrapperAdapterList.add(new InputStreamFileWrapperAdapter(contentTypeDetect));
+        return this;
+    }
+
+    /**
+     * 添加本地文件包装适配器
+     */
+    public FileStorageServiceBuilder addLocalFileWrapperAdapter() {
+        if (contentTypeDetect == null) throw new FileStorageRuntimeException("请先设置 TikaFactory 和 ContentTypeDetect");
+        fileWrapperAdapterList.add(new LocalFileWrapperAdapter(contentTypeDetect));
+        return this;
+    }
+
+    /**
+     * 添加 URI 文件包装适配器
+     */
+    public FileStorageServiceBuilder addUriFileWrapperAdapter() {
+        if (contentTypeDetect == null) throw new FileStorageRuntimeException("请先设置 TikaFactory 和 ContentTypeDetect");
+        fileWrapperAdapterList.add(new UriFileWrapperAdapter(contentTypeDetect));
+        return this;
+    }
+
+    /**
+     * 添加 HttpServletRequest 文件包装适配器
+     */
+    public FileStorageServiceBuilder addHttpServletRequestFileWrapperAdapter() {
+        if (!doesNotExistClass("javax.servlet.http.HttpServletRequest")) {
+            fileWrapperAdapterList.add(new JavaxHttpServletRequestFileWrapperAdapter());
+        }
+        if (!doesNotExistClass("jakarta.servlet.http.HttpServletRequest")) {
+            fileWrapperAdapterList.add(new JakartaHttpServletRequestFileWrapperAdapter());
+        }
+        return this;
+    }
+
+    /**
+     * 添加全部的文件包装适配器
+     */
+    public FileStorageServiceBuilder addAllFileWrapperAdapter() {
+        addByteFileWrapperAdapter();
+        addInputStreamFileWrapperAdapter();
+        addLocalFileWrapperAdapter();
+        addUriFileWrapperAdapter();
+        addHttpServletRequestFileWrapperAdapter();
+        return this;
+    }
+
+    /**
+     * 添加 Client 工厂
+     */
+    public FileStorageServiceBuilder addFileStorageClientFactory(List<FileStorageClientFactory<?>> list) {
+        clientFactoryList.add(list);
+        return this;
+    }
+
+    /**
+     * 添加 Client 工厂
+     */
+    public FileStorageServiceBuilder addFileStorageClientFactory(FileStorageClientFactory<?> factory) {
+        clientFactoryList.add(Collections.singletonList(factory));
+        return this;
+    }
+
+    /**
+     * 添加存储平台
+     */
+    public FileStorageServiceBuilder addFileStorage(List<? extends FileStorage> storageList) {
+        fileStorageList.addAll(storageList);
+        return this;
+    }
+
+    /**
+     * 添加存储平台
+     */
+    public FileStorageServiceBuilder addFileStorage(FileStorage storage) {
+        fileStorageList.add(storage);
+        return this;
+    }
+
+    /**
+     * 使用默认配置
+     */
+    public FileStorageServiceBuilder useDefault() {
+        setDefaultFileRecorder();
+        setDefaultTikaFactory();
+        setTikaContentTypeDetect();
+        addAllFileWrapperAdapter();
+        return this;
+    }
+
+    /**
+     * 创建
+     */
+    public FileStorageService build() {
+        if (properties == null) throw new FileStorageRuntimeException("properties 不能为 null");
+
+        // 初始化各个存储平台
+        fileStorageList.addAll(buildLocalFileStorage(properties.getLocal()));
+        fileStorageList.addAll(buildLocalPlusFileStorage(properties.getLocalPlus()));
+        fileStorageList.addAll(buildHuaweiObsFileStorage(properties.getHuaweiObs(), clientFactoryList));
+        fileStorageList.addAll(buildAliyunOssFileStorage(properties.getAliyunOss(), clientFactoryList));
+        fileStorageList.addAll(buildQiniuKodoFileStorage(properties.getQiniuKodo(), clientFactoryList));
+        fileStorageList.addAll(buildTencentCosFileStorage(properties.getTencentCos(), clientFactoryList));
+        fileStorageList.addAll(buildBaiduBosFileStorage(properties.getBaiduBos(), clientFactoryList));
+        fileStorageList.addAll(buildUpyunUssFileStorage(properties.getUpyunUss(), clientFactoryList));
+        fileStorageList.addAll(buildMinioFileStorage(properties.getMinio(), clientFactoryList));
+        fileStorageList.addAll(buildAmazonS3FileStorage(properties.getAmazonS3(), clientFactoryList));
+        fileStorageList.addAll(buildFtpFileStorage(properties.getFtp(), clientFactoryList));
+        fileStorageList.addAll(buildSftpFileStorage(properties.getSftp(), clientFactoryList));
+        fileStorageList.addAll(buildWebDavFileStorage(properties.getWebdav(), clientFactoryList));
+        fileStorageList.addAll(
+                buildGoogleCloudStorageFileStorage(properties.getGoogleCloudStorage(), clientFactoryList));
+        fileStorageList.addAll(buildFastDfsFileStorage(properties.getFastdfs(), clientFactoryList));
+        fileStorageList.addAll(buildAzureBlobFileStorage(properties.getAzureBlob(), clientFactoryList));
+
+        // 本体
+        FileStorageService service = new FileStorageService();
+        service.setSelf(service);
+        service.setProperties(properties);
+        service.setFileStorageList(new CopyOnWriteArrayList<>(fileStorageList));
+        service.setFileRecorder(fileRecorder);
+        service.setAspectList(new CopyOnWriteArrayList<>(aspectList));
+        service.setFileWrapperAdapterList(new CopyOnWriteArrayList<>(fileWrapperAdapterList));
+        service.setContentTypeDetect(contentTypeDetect);
+
+        return service;
+    }
+
+    /**
+     * 创建一个 FileStorageService 的构造器
+     */
+    public static FileStorageServiceBuilder create(FileStorageProperties properties) {
+        return new FileStorageServiceBuilder(properties);
+    }
+
+    /**
+     * 根据配置文件创建本地文件存储平台
+     */
+    public static List<LocalFileStorage> buildLocalFileStorage(List<? extends LocalConfig> list) {
+        if (CollUtil.isEmpty(list)) return Collections.emptyList();
+        return list.stream()
+                .map(config -> {
+                    log.info("加载本地存储平台:{},此存储平台已不推荐使用,新项目请使用 本地升级版存储平台(LocalPlusFileStorage)", config.getPlatform());
+                    return new LocalFileStorage(config);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据配置文件创建本地文件升级版存储平台
+     */
+    public static List<LocalPlusFileStorage> buildLocalPlusFileStorage(List<? extends LocalPlusConfig> list) {
+        if (CollUtil.isEmpty(list)) return Collections.emptyList();
+        return list.stream()
+                .map(config -> {
+                    log.info("加载本地升级版存储平台:{}", config.getPlatform());
+                    return new LocalPlusFileStorage(config);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据配置文件创建华为云 OBS 存储平台
+     */
+    public static List<HuaweiObsFileStorage> buildHuaweiObsFileStorage(
+            List<? extends HuaweiObsConfig> list, List<List<FileStorageClientFactory<?>>> clientFactoryList) {
+        if (CollUtil.isEmpty(list)) return Collections.emptyList();
+        buildFileStorageDetect(list, "华为云 OBS", "com.obs.services.ObsClient");
+        return list.stream()
+                .map(config -> {
+                    log.info("加载华为云 OBS 存储平台:{}", config.getPlatform());
+                    FileStorageClientFactory<ObsClient> clientFactory = getFactory(
+                            config.getPlatform(),
+                            clientFactoryList,
+                            () -> new HuaweiObsFileStorageClientFactory(config));
+                    return new HuaweiObsFileStorage(config, clientFactory);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据配置文件创建阿里云 OSS 存储平台
+     */
+    public static List<AliyunOssFileStorage> buildAliyunOssFileStorage(
+            List<? extends AliyunOssConfig> list, List<List<FileStorageClientFactory<?>>> clientFactoryList) {
+        if (CollUtil.isEmpty(list)) return Collections.emptyList();
+        buildFileStorageDetect(list, "阿里云 OSS", "com.aliyun.oss.OSS");
+        return list.stream()
+                .map(config -> {
+                    log.info("加载阿里云 OSS 存储平台:{}", config.getPlatform());
+                    FileStorageClientFactory<OSS> clientFactory = getFactory(
+                            config.getPlatform(),
+                            clientFactoryList,
+                            () -> new AliyunOssFileStorageClientFactory(config));
+                    return new AliyunOssFileStorage(config, clientFactory);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据配置文件创建七牛云 Kodo 存储平台
+     */
+    public static List<QiniuKodoFileStorage> buildQiniuKodoFileStorage(
+            List<? extends QiniuKodoConfig> list, List<List<FileStorageClientFactory<?>>> clientFactoryList) {
+        if (CollUtil.isEmpty(list)) return Collections.emptyList();
+        buildFileStorageDetect(list, "七牛云 Kodo", "com.qiniu.storage.UploadManager");
+        return list.stream()
+                .map(config -> {
+                    log.info("加载七牛云 Kodo 存储平台:{}", config.getPlatform());
+                    FileStorageClientFactory<QiniuKodoClient> clientFactory = getFactory(
+                            config.getPlatform(),
+                            clientFactoryList,
+                            () -> new QiniuKodoFileStorageClientFactory(config));
+                    return new QiniuKodoFileStorage(config, clientFactory);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据配置文件创建腾讯云 COS 存储平台
+     */
+    public static List<TencentCosFileStorage> buildTencentCosFileStorage(
+            List<? extends TencentCosConfig> list, List<List<FileStorageClientFactory<?>>> clientFactoryList) {
+        if (CollUtil.isEmpty(list)) return Collections.emptyList();
+        buildFileStorageDetect(list, "腾讯云 COS", "com.qcloud.cos.COSClient");
+        return list.stream()
+                .map(config -> {
+                    log.info("加载腾讯云 COS 存储平台:{}", config.getPlatform());
+                    FileStorageClientFactory<COSClient> clientFactory = getFactory(
+                            config.getPlatform(),
+                            clientFactoryList,
+                            () -> new TencentCosFileStorageClientFactory(config));
+                    return new TencentCosFileStorage(config, clientFactory);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据配置文件创建百度云 BOS 存储平台
+     */
+    public static List<BaiduBosFileStorage> buildBaiduBosFileStorage(
+            List<? extends BaiduBosConfig> list, List<List<FileStorageClientFactory<?>>> clientFactoryList) {
+        if (CollUtil.isEmpty(list)) return Collections.emptyList();
+        buildFileStorageDetect(list, "百度云 BOS", "com.baidubce.services.bos.BosClient");
+        return list.stream()
+                .map(config -> {
+                    log.info("加载百度云 BOS 存储平台:{}", config.getPlatform());
+                    FileStorageClientFactory<BosClient> clientFactory = getFactory(
+                            config.getPlatform(),
+                            clientFactoryList,
+                            () -> new BaiduBosFileStorageClientFactory(config));
+                    return new BaiduBosFileStorage(config, clientFactory);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据配置文件创建又拍云 USS 存储平台
+     */
+    public static List<UpyunUssFileStorage> buildUpyunUssFileStorage(
+            List<? extends UpyunUssConfig> list, List<List<FileStorageClientFactory<?>>> clientFactoryList) {
+        if (CollUtil.isEmpty(list)) return Collections.emptyList();
+        buildFileStorageDetect(list, "又拍云 USS", "com.upyun.RestManager");
+        return list.stream()
+                .map(config -> {
+                    log.info("加载又拍云 USS 存储平台:{}", config.getPlatform());
+                    FileStorageClientFactory<RestManager> clientFactory = getFactory(
+                            config.getPlatform(),
+                            clientFactoryList,
+                            () -> new UpyunUssFileStorageClientFactory(config));
+                    return new UpyunUssFileStorage(config, clientFactory);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据配置文件创建 MinIO 存储平台
+     */
+    public static List<MinioFileStorage> buildMinioFileStorage(
+            List<? extends MinioConfig> list, List<List<FileStorageClientFactory<?>>> clientFactoryList) {
+        if (CollUtil.isEmpty(list)) return Collections.emptyList();
+        buildFileStorageDetect(list, "MinIO", "io.minio.MinioClient");
+        return list.stream()
+                .map(config -> {
+                    log.info("加载 MinIO 存储平台:{}", config.getPlatform());
+                    FileStorageClientFactory<MinioClient> clientFactory = getFactory(
+                            config.getPlatform(), clientFactoryList, () -> new MinioFileStorageClientFactory(config));
+                    return new MinioFileStorage(config, clientFactory);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据配置文件创建又 Amazon S3 存储平台
+     */
+    public static List<AmazonS3FileStorage> buildAmazonS3FileStorage(
+            List<? extends AmazonS3Config> list, List<List<FileStorageClientFactory<?>>> clientFactoryList) {
+        if (CollUtil.isEmpty(list)) return Collections.emptyList();
+        buildFileStorageDetect(list, "Amazon S3", "com.amazonaws.services.s3.AmazonS3");
+        return list.stream()
+                .map(config -> {
+                    log.info("加载 Amazon S3 存储平台:{}", config.getPlatform());
+                    FileStorageClientFactory<AmazonS3> clientFactory = getFactory(
+                            config.getPlatform(),
+                            clientFactoryList,
+                            () -> new AmazonS3FileStorageClientFactory(config));
+                    return new AmazonS3FileStorage(config, clientFactory);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据配置文件创建 FTP 存储平台
+     */
+    public static List<FtpFileStorage> buildFtpFileStorage(
+            List<? extends FtpConfig> list, List<List<FileStorageClientFactory<?>>> clientFactoryList) {
+        if (CollUtil.isEmpty(list)) return Collections.emptyList();
+        buildFileStorageDetect(
+                list,
+                "FTP",
+                "org.apache.commons.net.ftp.FTPClient",
+                "cn.hutool.extra.ftp.Ftp",
+                "org.apache.commons.pool2.impl.GenericObjectPool");
+        return list.stream()
+                .map(config -> {
+                    log.info("加载 FTP 存储平台:{}", config.getPlatform());
+                    FileStorageClientFactory<Ftp> clientFactory = getFactory(
+                            config.getPlatform(), clientFactoryList, () -> new FtpFileStorageClientFactory(config));
+                    return new FtpFileStorage(config, clientFactory);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据配置文件创建 SFTP 存储平台
+     */
+    public static List<SftpFileStorage> buildSftpFileStorage(
+            List<? extends SftpConfig> list, List<List<FileStorageClientFactory<?>>> clientFactoryList) {
+        if (CollUtil.isEmpty(list)) return Collections.emptyList();
+        buildFileStorageDetect(
+                list,
+                "SFTP",
+                "com.jcraft.jsch.ChannelSftp",
+                "cn.hutool.extra.ftp.Ftp",
+                "org.apache.commons.pool2.impl.GenericObjectPool");
+        return list.stream()
+                .map(config -> {
+                    log.info("加载 SFTP 存储平台:{}", config.getPlatform());
+                    FileStorageClientFactory<Sftp> clientFactory = getFactory(
+                            config.getPlatform(), clientFactoryList, () -> new SftpFileStorageClientFactory(config));
+                    return new SftpFileStorage(config, clientFactory);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据配置文件创建 WebDAV 存储平台
+     */
+    public static List<WebDavFileStorage> buildWebDavFileStorage(
+            List<? extends WebDavConfig> list, List<List<FileStorageClientFactory<?>>> clientFactoryList) {
+        if (CollUtil.isEmpty(list)) return Collections.emptyList();
+        buildFileStorageDetect(list, "WebDAV", "com.github.sardine.Sardine");
+        return list.stream()
+                .map(config -> {
+                    log.info("加载 WebDAV 存储平台:{}", config.getPlatform());
+                    FileStorageClientFactory<Sardine> clientFactory = getFactory(
+                            config.getPlatform(), clientFactoryList, () -> new WebDavFileStorageClientFactory(config));
+                    return new WebDavFileStorage(config, clientFactory);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据配置文件创建 GoogleCloud Storage 存储平台
+     */
+    public static List<GoogleCloudStorageFileStorage> buildGoogleCloudStorageFileStorage(
+            List<? extends GoogleCloudStorageConfig> list, List<List<FileStorageClientFactory<?>>> clientFactoryList) {
+        if (CollUtil.isEmpty(list)) return Collections.emptyList();
+        buildFileStorageDetect(list, "GoogleCloud Storage ", "com.google.cloud.storage.Storage");
+        return list.stream()
+                .map(config -> {
+                    log.info("加载 GoogleCloud Storage 存储平台:{}", config.getPlatform());
+                    FileStorageClientFactory<Storage> clientFactory = getFactory(
+                            config.getPlatform(),
+                            clientFactoryList,
+                            () -> new GoogleCloudStorageFileStorageClientFactory(config));
+                    return new GoogleCloudStorageFileStorage(config, clientFactory);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 构建 FastDFS 客户端
+     * @param fastdfs FastDFS 配置列表
+     * @param clientFactoryList 客户端工厂
+     * @return {@link Collection}<{@link ?} {@link extends} {@link FileStorage}>
+     */
+    private Collection<? extends FileStorage> buildFastDfsFileStorage(
+            List<? extends FastDfsConfig> fastdfs, List<List<FileStorageClientFactory<?>>> clientFactoryList) {
+        if (CollUtil.isEmpty(fastdfs)) {
+            return Collections.emptyList();
+        }
+
+        buildFileStorageDetect(fastdfs, "FastDFS", "org.csource.fastdfs.StorageClient");
+
+        return fastdfs.stream()
+                .map(config -> {
+                    log.info("加载 FastDFS 存储平台:{}", config.getPlatform());
+                    FileStorageClientFactory<StorageClient> clientFactory = getFactory(
+                            config.getPlatform(), clientFactoryList, () -> new FastDfsFileStorageClientFactory(config));
+                    return new FastDfsFileStorage(config, clientFactory);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据配置文件创建 Azure Blob Storage 存储平台
+     */
+    public static List<AzureBlobStorageFileStorage> buildAzureBlobFileStorage(
+            List<? extends AzureBlobStorageConfig> list, List<List<FileStorageClientFactory<?>>> clientFactoryList) {
+        if (CollUtil.isEmpty(list)) return Collections.emptyList();
+        buildFileStorageDetect(list, "microsoft azure blob ", "com.azure.storage.blob.BlobServiceClient");
+        return list.stream()
+                .map(config -> {
+                    log.info("加载 microsoft azure blob 存储平台:{}", config.getPlatform());
+                    FileStorageClientFactory<AzureBlobStorageClient> clientFactory = getFactory(
+                            config.getPlatform(),
+                            clientFactoryList,
+                            () -> new AzureBlobStorageFileStorageClientFactory(config));
+                    return new AzureBlobStorageFileStorage(config, clientFactory);
+                })
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 获取或创建指定存储平台的 Client 工厂对象
+     */
+    public static <Client> FileStorageClientFactory<Client> getFactory(
+            String platform,
+            List<List<FileStorageClientFactory<?>>> list,
+            Supplier<FileStorageClientFactory<Client>> defaultSupplier) {
+        if (list != null) {
+            for (List<FileStorageClientFactory<?>> factoryList : list) {
+                for (FileStorageClientFactory<?> factory : factoryList) {
+                    if (Objects.equals(platform, factory.getPlatform())) {
+                        try {
+                            return Tools.cast(factory);
+                        } catch (Exception e) {
+                            throw new FileStorageRuntimeException(
+                                    "获取 FileStorageClientFactory 失败,类型不匹配,platform:" + platform, e);
+                        }
+                    }
+                }
+            }
+        }
+        return defaultSupplier.get();
+    }
+
+    /**
+     * 判断是否没有引入指定 Class
+     */
+    public static boolean doesNotExistClass(String name) {
+        try {
+            Class.forName(name);
+            return false;
+        } catch (ClassNotFoundException e) {
+            return true;
+        }
+    }
+
+    /**
+     * 创建存储平台时的依赖检查
+     */
+    public static void buildFileStorageDetect(List<?> list, String platformName, String... classNames) {
+        if (CollUtil.isEmpty(list)) return;
+        for (String className : classNames) {
+            if (doesNotExistClass(className)) {
+                throw new FileStorageRuntimeException(
+                        "检测到【" + platformName + "】配置,但是没有找到对应的依赖类:【" + className
+                                + "】,所以无法加载此存储平台!配置参考地址:https://x-file-storage.xuyanwu.cn/2.2.0/#/%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8");
+            }
+        }
+    }
+}

+ 36 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/IOExceptionConsumer.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
+ * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+package org.dromara.x.file.storage.core;
+
+import java.io.IOException;
+
+/**
+ * 带 IOException 异常的 Consumer
+ */
+@FunctionalInterface
+public interface IOExceptionConsumer<T> {
+
+    void accept(T t) throws IOException;
+}

+ 10 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/IOExceptionFunction.java

@@ -0,0 +1,10 @@
+package org.dromara.x.file.storage.core;
+
+import java.io.IOException;
+
+/**
+ * 带 IOException 异常的 Function
+ */
+public interface IOExceptionFunction<T, R> {
+    R apply(T t) throws IOException;
+}

+ 131 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/InputStreamPlus.java

@@ -0,0 +1,131 @@
+package org.dromara.x.file.storage.core;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.function.Consumer;
+import lombok.Getter;
+import org.dromara.x.file.storage.core.hash.HashCalculatorManager;
+
+/**
+ * 增强版本的 InputStream ,可以带进度监听、计算哈希等功能
+ */
+@Getter
+public class InputStreamPlus extends FilterInputStream {
+    protected boolean readFlag;
+    protected boolean finishFlag;
+    protected long progressSize;
+    protected final Long allSize;
+    protected final ProgressListener listener;
+    protected final HashCalculatorManager hashCalculatorManager;
+    protected int markFlag;
+
+    public InputStreamPlus(InputStream in, Consumer<Long> listener) {
+        this(
+                in,
+                new ProgressListener() {
+                    @Override
+                    public void start() {}
+
+                    @Override
+                    public void progress(long progressSize, Long allSize) {
+                        listener.accept(progressSize);
+                    }
+
+                    @Override
+                    public void finish() {}
+                },
+                null);
+    }
+
+    public InputStreamPlus(InputStream in, ProgressListener listener, Long allSize) {
+        this(in, listener, allSize, null);
+    }
+
+    public InputStreamPlus(
+            InputStream in, ProgressListener listener, Long allSize, HashCalculatorManager hashCalculatorManager) {
+        super(in);
+        this.listener = listener;
+        this.allSize = allSize;
+        this.hashCalculatorManager = hashCalculatorManager;
+    }
+
+    @Override
+    public long skip(long n) throws IOException {
+        long skip = super.skip(n);
+        onProgress(skip);
+        return skip;
+    }
+
+    @Override
+    public int read() throws IOException {
+        int b = super.read();
+        onProgress(b == -1 ? -1 : 1);
+        if (this.markFlag == 0 && hashCalculatorManager != null && b > -1) {
+            hashCalculatorManager.update(new byte[] {(byte) b});
+        }
+        return b;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        onStart();
+        int bytes = super.read(b, off, len);
+        if (this.markFlag == 0 && hashCalculatorManager != null && bytes > 0) {
+            hashCalculatorManager.update(Arrays.copyOfRange(b, off, off + bytes));
+        }
+        onProgress(bytes);
+        return bytes;
+    }
+
+    @Override
+    public boolean markSupported() {
+        return false;
+    }
+
+    @Override
+    public synchronized void mark(int readlimit) {
+        super.mark(readlimit);
+        this.markFlag++;
+    }
+
+    @Override
+    public synchronized void reset() throws IOException {
+        super.reset();
+        this.markFlag--;
+    }
+
+    /**
+     * 触发开始
+     */
+    private void onStart() {
+        if (this.markFlag > 0) return;
+        if (this.readFlag) return;
+        this.readFlag = true;
+        if (this.listener != null) this.listener.start();
+    }
+
+    /**
+     * 触发进度变动
+     */
+    protected void onProgress(long size) {
+        if (this.markFlag > 0) return;
+        if (size > 0) {
+            progressSize += size;
+            if (this.listener != null) this.listener.progress(progressSize, allSize);
+        } else if (size < 0) {
+            onFinish();
+        }
+    }
+
+    /**
+     * 触发读取完毕
+     */
+    private void onFinish() {
+        if (this.markFlag > 0) return;
+        if (this.finishFlag) return;
+        this.finishFlag = true;
+        if (this.listener != null) this.listener.finish();
+    }
+}

+ 65 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/ProgressInputStream.java

@@ -0,0 +1,65 @@
+package org.dromara.x.file.storage.core;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * 带进度通知的 InputStream 包装类,将在后续版本中删除,请使用 InputStreamPlus 代替此类
+ */
+@Deprecated
+public class ProgressInputStream extends FilterInputStream {
+
+    private boolean readFlag;
+    private boolean finishFlag;
+    private long progressSize;
+    private final Long allSize;
+    private final ProgressListener listener;
+
+    public ProgressInputStream(InputStream in, ProgressListener listener, Long allSize) {
+        super(in);
+        this.listener = listener;
+        this.allSize = allSize;
+    }
+
+    @Override
+    public long skip(long n) throws IOException {
+        long skip = super.skip(n);
+        progress(skip);
+        return skip;
+    }
+
+    @Override
+    public int read() throws IOException {
+        int b = super.read();
+        progress(b == -1 ? -1 : 1);
+        return b;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        if (!this.readFlag) {
+            this.readFlag = true;
+            this.listener.start();
+        }
+        int bytes = super.read(b, off, len);
+        progress(bytes);
+        return bytes;
+    }
+
+    @Override
+    public boolean markSupported() {
+        return false;
+    }
+
+    protected void progress(long size) {
+        if (size > 0) {
+            this.listener.progress(progressSize += size, allSize);
+        } else if (size < 0) {
+            if (!this.finishFlag) {
+                this.finishFlag = true;
+                this.listener.finish();
+            }
+        }
+    }
+}

+ 70 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/ProgressListener.java

@@ -0,0 +1,70 @@
+package org.dromara.x.file.storage.core;
+
+import java.util.function.LongSupplier;
+
+/**
+ * 进度监听器
+ */
+public interface ProgressListener {
+
+    /**
+     * 开始
+     */
+    void start();
+
+    /**
+     * 进行中
+     *
+     * @param progressSize 已经进行的大小
+     * @param allSize      总大小,来自 fileInfo.getSize(),未知大小的流可能会导致此参数为 null
+     */
+    void progress(long progressSize, Long allSize);
+
+    /**
+     * 结束
+     */
+    void finish();
+
+    /**
+     * 快速触发开始
+     */
+    static void quickStart(ProgressListener progressListener, Long size) {
+        if (progressListener == null) return;
+        progressListener.start();
+        progressListener.progress(0, size);
+    }
+
+    /**
+     * 快速触发进行中
+     */
+    static void quickProgress(ProgressListener progressListener, long progressSize, Long size) {
+        if (progressListener == null) return;
+        progressListener.progress(progressSize, size);
+    }
+
+    /**
+     * 快速触发结束
+     */
+    static void quickFinish(ProgressListener progressListener, Long size, LongSupplier progressSizeSupplier) {
+        if (progressListener == null) return;
+        progressListener.progress(progressSizeSupplier.getAsLong(), size);
+        progressListener.finish();
+    }
+
+    /**
+     * 快速触发结束
+     */
+    static void quickFinish(ProgressListener progressListener, Long size) {
+        if (progressListener == null) return;
+        progressListener.progress(size, size);
+        progressListener.finish();
+    }
+
+    /**
+     * 快速触发结束
+     */
+    static void quickFinish(ProgressListener progressListener) {
+        if (progressListener == null) return;
+        progressListener.finish();
+    }
+}

+ 126 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/ProgressListenerSetter.java

@@ -0,0 +1,126 @@
+package org.dromara.x.file.storage.core;
+
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import org.dromara.x.file.storage.core.util.Tools;
+
+public interface ProgressListenerSetter<T extends ProgressListenerSetter<?>> {
+
+    /**
+     * 设置进度监听器,请使用 setProgressListener 代替
+     *
+     * @param progressMonitor 提供一个参数,表示已传输字节数
+     */
+    @Deprecated
+    default T setProgressMonitor(boolean flag, Consumer<Long> progressMonitor) {
+        return setProgressListener(flag, progressMonitor);
+    }
+
+    /**
+     * 设置进度监听器,请使用 setProgressListener 代替
+     *
+     * @param progressMonitor 提供一个参数,表示已传输字节数
+     */
+    @Deprecated
+    default T setProgressMonitor(Consumer<Long> progressMonitor) {
+        return setProgressListener(progressMonitor);
+    }
+
+    /**
+     * 设置进度监听器,请使用 setProgressListener 代替
+     *
+     * @param progressMonitor 提供两个参数,第一个是 progressSize已传输字节数,第二个是 allSize总字节数
+     */
+    @Deprecated
+    default T setProgressMonitor(boolean flag, BiConsumer<Long, Long> progressMonitor) {
+        return setProgressListener(flag, progressMonitor);
+    }
+
+    /**
+     * 设置进度监听器,请使用 setProgressListener 代替
+     *
+     * @param progressMonitor 提供两个参数,第一个是 progressSize已传输字节数,第二个是 allSize总字节数
+     */
+    @Deprecated
+    default T setProgressMonitor(BiConsumer<Long, Long> progressMonitor) {
+        return setProgressListener(progressMonitor);
+    }
+
+    /**
+     * 设置进度监听器,请使用 setProgressListener 代替
+     */
+    @Deprecated
+    default T setProgressMonitor(boolean flag, ProgressListener progressMonitor) {
+        return setProgressListener(flag, progressMonitor);
+    }
+
+    /**
+     * 设置进度监听器,请使用 setProgressListener 代替
+     */
+    @Deprecated
+    default T setProgressMonitor(ProgressListener progressMonitor) {
+        return setProgressListener(progressMonitor);
+    }
+
+    /**
+     * 设置进度监听器
+     *
+     * @param progressListener 提供一个参数,表示已传输字节数
+     */
+    default T setProgressListener(boolean flag, Consumer<Long> progressListener) {
+        if (flag) setProgressListener(progressListener);
+        return Tools.cast(this);
+    }
+
+    /**
+     * 设置进度监听器
+     *
+     * @param progressListener 提供一个参数,表示已传输字节数
+     */
+    default T setProgressListener(Consumer<Long> progressListener) {
+        return setProgressListener((progressSize, allSize) -> progressListener.accept(progressSize));
+    }
+
+    /**
+     * 设置进度监听器
+     *
+     * @param progressListener 提供两个参数,第一个是 progressSize已传输字节数,第二个是 allSize总字节数
+     */
+    default T setProgressListener(boolean flag, BiConsumer<Long, Long> progressListener) {
+        if (flag) setProgressListener(progressListener);
+        return Tools.cast(this);
+    }
+
+    /**
+     * 设置进度监听器
+     *
+     * @param progressListener 提供两个参数,第一个是 progressSize已传输字节数,第二个是 allSize总字节数
+     */
+    default T setProgressListener(BiConsumer<Long, Long> progressListener) {
+        return setProgressListener(new ProgressListener() {
+            @Override
+            public void start() {}
+
+            @Override
+            public void progress(long progressSize, Long allSize) {
+                progressListener.accept(progressSize, allSize);
+            }
+
+            @Override
+            public void finish() {}
+        });
+    }
+
+    /**
+     * 设置进度监听器
+     */
+    default T setProgressListener(boolean flag, ProgressListener progressListener) {
+        if (flag) setProgressListener(progressListener);
+        return Tools.cast(this);
+    }
+
+    /**
+     * 设置进度监听器
+     */
+    T setProgressListener(ProgressListener progressListener);
+}

+ 829 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/UploadPretreatment.java

@@ -0,0 +1,829 @@
+package org.dromara.x.file.storage.core;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.io.file.FileNameUtil;
+import cn.hutool.core.lang.Dict;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import net.coobird.thumbnailator.Thumbnails;
+import org.dromara.x.file.storage.core.aspect.FileStorageAspect;
+import org.dromara.x.file.storage.core.exception.FileStorageRuntimeException;
+import org.dromara.x.file.storage.core.file.FileWrapper;
+import org.dromara.x.file.storage.core.hash.HashCalculator;
+import org.dromara.x.file.storage.core.hash.HashCalculatorManager;
+import org.dromara.x.file.storage.core.hash.HashCalculatorSetter;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+import org.dromara.x.file.storage.core.upload.UploadActuator;
+
+/**
+ * 文件上传预处理对象,请使用 org.dromara.x.file.storage.core.upload.UploadPretreatment 代替
+ */
+@Deprecated
+@Getter
+@Setter
+@Accessors(chain = true)
+public class UploadPretreatment
+        implements ProgressListenerSetter<UploadPretreatment>, HashCalculatorSetter<UploadPretreatment> {
+    private FileStorageService fileStorageService;
+    /**
+     * 要上传到的平台
+     */
+    private String platform;
+    /**
+     * 要上传的文件包装类
+     */
+    private FileWrapper fileWrapper;
+    /**
+     * 要上传文件的缩略图
+     */
+    private byte[] thumbnailBytes;
+    /**
+     * 缩略图后缀,不是扩展名但包含扩展名,例如【.min.jpg】【.png】。
+     * 只能在缩略图生成前进行修改后缀中的扩展名部分。
+     * 例如当前是【.min.jpg】那么扩展名就是【jpg】,当缩略图未生成的情况下可以随意修改(扩展名必须是 thumbnailator 支持的图片格式),
+     * 一旦缩略图生成后,扩展名之外的部分可以随意改变 ,扩展名部分不能改变,除非你在 {@link UploadPretreatment#thumbnail} 方法中修改了输出格式。
+     */
+    private String thumbnailSuffix;
+    /**
+     * 忽略生成缩略图时的错误,需要在生成缩略图之前设置
+     */
+    private boolean ignoreThumbnailException = false;
+    /**
+     * 文件所属对象id
+     */
+    private String objectId;
+    /**
+     * 文件所属对象类型
+     */
+    private String objectType;
+    /**
+     * 文件存储路径
+     */
+    private String path = "";
+
+    /**
+     * 保存文件名,如果不设置则自动生成
+     */
+    private String saveFilename;
+
+    /**
+     * 缩略图的保存文件名,注意此文件名不含后缀,后缀用 {@link UploadPretreatment#thumbnailSuffix} 属性控制
+     */
+    private String saveThFilename;
+
+    /**
+     * 缩略图 MIME 类型,如果不设置则在上传文件根据缩略图文件名自动识别
+     */
+    private String thContentType;
+
+    /**
+     * 文件元数据
+     */
+    private Map<String, String> metadata;
+
+    /**
+     * 文件用户元数据
+     */
+    private Map<String, String> userMetadata;
+
+    /**
+     * 缩略图元数据
+     */
+    private Map<String, String> thMetadata;
+
+    /**
+     * 缩略图用户元数据
+     */
+    private Map<String, String> thUserMetadata;
+
+    /**
+     * 不支持元数据时抛出异常
+     */
+    private Boolean notSupportMetadataThrowException;
+
+    /**
+     * 不支持 ACL 时抛出异常
+     */
+    private Boolean notSupportAclThrowException;
+
+    /**
+     * 附加属性字典
+     */
+    private Dict attr;
+
+    /**
+     * 上传进度监听
+     */
+    private ProgressListener progressListener;
+
+    /**
+     * 传时用的增强版本的 InputStream ,可以带进度监听、计算哈希等功能,仅内部使用
+     */
+    private InputStreamPlus inputStreamPlus;
+
+    /**
+     * 文件的访问控制列表,一般情况下只有对象存储支持该功能
+     * 详情见{@link FileInfo#setFileAcl}
+     */
+    private Object fileAcl;
+
+    /**
+     * 缩略图的访问控制列表,一般情况下只有对象存储支持该功能
+     * 详情见{@link FileInfo#setFileAcl}
+     */
+    private Object thFileAcl;
+
+    /**
+     * 哈希计算器管理器
+     */
+    private HashCalculatorManager hashCalculatorManager = new HashCalculatorManager();
+
+    /**
+     * 设置要上传到的平台
+     */
+    public UploadPretreatment setPlatform(boolean flag, String platform) {
+        if (flag) setPlatform(platform);
+        return this;
+    }
+
+    /**
+     * 设置要上传的文件包装类
+     */
+    public UploadPretreatment setFileWrapper(boolean flag, FileWrapper fileWrapper) {
+        if (flag) setFileWrapper(fileWrapper);
+        return this;
+    }
+
+    /**
+     * 设置要上传文件的缩略图
+     */
+    public UploadPretreatment setThumbnailBytes(boolean flag, byte[] thumbnailBytes) {
+        if (flag) setThumbnailBytes(thumbnailBytes);
+        return this;
+    }
+
+    /**
+     * 设置缩略图后缀,不是扩展名但包含扩展名,例如【.min.jpg】【.png】。
+     * 只能在缩略图生成前进行修改后缀中的扩展名部分。
+     * 例如当前是【.min.jpg】那么扩展名就是【jpg】,当缩略图未生成的情况下可以随意修改(扩展名必须是 thumbnailator 支持的图片格式),
+     * 一旦缩略图生成后,扩展名之外的部分可以随意改变 ,扩展名部分不能改变,除非你在 {@link UploadPretreatment#thumbnail} 方法中修改了输出格式。
+     */
+    public UploadPretreatment setThumbnailSuffix(boolean flag, String thumbnailSuffix) {
+        if (flag) setThumbnailSuffix(thumbnailSuffix);
+        return this;
+    }
+
+    /**
+     * 忽略生成缩略图时的错误,需要在生成缩略图之前设置
+     */
+    public UploadPretreatment setIgnoreThumbnailException(boolean flag, boolean ignoreThumbnailException) {
+        if (flag) setIgnoreThumbnailException(ignoreThumbnailException);
+        return this;
+    }
+
+    /**
+     * 设置文件所属对象id
+     *
+     * @param objectId 如果不是 String 类型会自动调用 toString() 方法
+     */
+    public UploadPretreatment setObjectId(boolean flag, Object objectId) {
+        if (flag) setObjectId(objectId);
+        return this;
+    }
+
+    /**
+     * 设置文件所属对象id
+     *
+     * @param objectId 如果不是 String 类型会自动调用 toString() 方法
+     */
+    public UploadPretreatment setObjectId(Object objectId) {
+        this.objectId = objectId == null ? null : objectId.toString();
+        return this;
+    }
+
+    /**
+     * 设置文件所属对象类型
+     */
+    public UploadPretreatment setObjectType(boolean flag, String objectType) {
+        if (flag) setObjectType(objectType);
+        return this;
+    }
+
+    /**
+     * 设置文文件存储路径
+     */
+    public UploadPretreatment setPath(boolean flag, String path) {
+        if (flag) setPath(path);
+        return this;
+    }
+
+    /**
+     * 设置保存文件名,如果不设置则自动生成
+     */
+    public UploadPretreatment setSaveFilename(boolean flag, String saveFilename) {
+        if (flag) setSaveFilename(saveFilename);
+        return this;
+    }
+
+    /**
+     * 设置缩略图的保存文件名,注意此文件名不含后缀,后缀用 {@link UploadPretreatment#thumbnailSuffix} 属性控制
+     */
+    public UploadPretreatment setSaveThFilename(boolean flag, String saveThFilename) {
+        if (flag) setSaveThFilename(saveThFilename);
+        return this;
+    }
+
+    /**
+     * 缩略图 MIME 类型,如果不设置则在上传文件根据缩略图文件名自动识别
+     */
+    public UploadPretreatment setThContentType(boolean flag, String thContentType) {
+        if (flag) setThContentType(thContentType);
+        return this;
+    }
+
+    /**
+     * 获取文件名
+     */
+    public String getName() {
+        return fileWrapper.getName();
+    }
+
+    /**
+     * 设置文件名
+     */
+    public UploadPretreatment setName(boolean flag, String name) {
+        if (flag) setName(name);
+        return this;
+    }
+
+    /**
+     * 设置文件名
+     */
+    public UploadPretreatment setName(String name) {
+        fileWrapper.setName(name);
+        return this;
+    }
+
+    /**
+     * 获取文件的 MIME 类型
+     */
+    public String getContentType() {
+        return fileWrapper.getContentType();
+    }
+
+    /**
+     * 设置文件的 MIME 类型
+     */
+    public UploadPretreatment setContentType(boolean flag, String contentType) {
+        if (flag) setContentType(contentType);
+        return this;
+    }
+
+    /**
+     * 设置文件的 MIME 类型
+     */
+    public UploadPretreatment setContentType(String contentType) {
+        fileWrapper.setContentType(contentType);
+        return this;
+    }
+
+    /**
+     * 获取原始文件名
+     */
+    public String getOriginalFilename() {
+        return fileWrapper.getName();
+    }
+
+    /**
+     * 设置原始文件名
+     */
+    public UploadPretreatment setOriginalFilename(boolean flag, String originalFilename) {
+        if (flag) setOriginalFilename(originalFilename);
+        return this;
+    }
+
+    /**
+     * 设置原始文件名
+     */
+    public UploadPretreatment setOriginalFilename(String originalFilename) {
+        fileWrapper.setName(originalFilename);
+        return this;
+    }
+
+    /**
+     * 获取文件元数据
+     */
+    public Map<String, String> getMetadata() {
+        if (metadata == null) metadata = new LinkedHashMap<>();
+        return metadata;
+    }
+
+    /**
+     * 设置文件元数据
+     */
+    public UploadPretreatment putMetadata(boolean flag, String key, String value) {
+        if (flag) putMetadata(key, value);
+        return this;
+    }
+
+    /**
+     * 设置文件元数据
+     */
+    public UploadPretreatment putMetadata(String key, String value) {
+        getMetadata().put(key, value);
+        return this;
+    }
+
+    /**
+     * 设置文件元数据
+     */
+    public UploadPretreatment putMetadataAll(boolean flag, Map<String, String> metadata) {
+        if (flag) putMetadataAll(metadata);
+        return this;
+    }
+
+    /**
+     * 设置文件元数据
+     */
+    public UploadPretreatment putMetadataAll(Map<String, String> metadata) {
+        getMetadata().putAll(metadata);
+        return this;
+    }
+
+    /**
+     * 获取文件用户元数据
+     */
+    public Map<String, String> getUserMetadata() {
+        if (userMetadata == null) userMetadata = new LinkedHashMap<>();
+        return userMetadata;
+    }
+
+    /**
+     * 设置文件用户元数据
+     */
+    public UploadPretreatment putUserMetadata(boolean flag, String key, String value) {
+        if (flag) putUserMetadata(key, value);
+        return this;
+    }
+
+    /**
+     * 设置文件用户元数据
+     */
+    public UploadPretreatment putUserMetadata(String key, String value) {
+        getUserMetadata().put(key, value);
+        return this;
+    }
+
+    /**
+     * 设置文件用户元数据
+     */
+    public UploadPretreatment putUserMetadataAll(boolean flag, Map<String, String> metadata) {
+        if (flag) putUserMetadataAll(metadata);
+        return this;
+    }
+
+    /**
+     * 设置文件用户元数据
+     */
+    public UploadPretreatment putUserMetadataAll(Map<String, String> metadata) {
+        getUserMetadata().putAll(metadata);
+        return this;
+    }
+
+    /**
+     * 获取缩略图元数据
+     */
+    public Map<String, String> getThMetadata() {
+        if (thMetadata == null) thMetadata = new LinkedHashMap<>();
+        return thMetadata;
+    }
+
+    /**
+     * 设置缩略图元数据
+     */
+    public UploadPretreatment putThMetadata(boolean flag, String key, String value) {
+        if (flag) putThMetadata(key, value);
+        return this;
+    }
+
+    /**
+     * 设置缩略图元数据
+     */
+    public UploadPretreatment putThMetadata(String key, String value) {
+        getThMetadata().put(key, value);
+        return this;
+    }
+
+    /**
+     * 设置缩略图元数据
+     */
+    public UploadPretreatment putThMetadataAll(boolean flag, Map<String, String> metadata) {
+        if (flag) putThMetadataAll(metadata);
+        return this;
+    }
+
+    /**
+     * 设置缩略图元数据
+     */
+    public UploadPretreatment putThMetadataAll(Map<String, String> metadata) {
+        getThMetadata().putAll(metadata);
+        return this;
+    }
+
+    /**
+     * 获取缩略图用户元数据
+     */
+    public Map<String, String> getThUserMetadata() {
+        if (thUserMetadata == null) thUserMetadata = new LinkedHashMap<>();
+        return thUserMetadata;
+    }
+
+    /**
+     * 设置缩略图用户元数据
+     */
+    public UploadPretreatment putThUserMetadata(boolean flag, String key, String value) {
+        if (flag) putThUserMetadata(key, value);
+        return this;
+    }
+
+    /**
+     * 设置缩略图用户元数据
+     */
+    public UploadPretreatment putThUserMetadata(String key, String value) {
+        getThUserMetadata().put(key, value);
+        return this;
+    }
+
+    /**
+     * 设置缩略图用户元数据
+     */
+    public UploadPretreatment putThUserMetadataAll(boolean flag, Map<String, String> metadata) {
+        if (flag) putThUserMetadataAll(metadata);
+        return this;
+    }
+
+    /**
+     * 设置缩略图用户元数据
+     */
+    public UploadPretreatment putThUserMetadataAll(Map<String, String> metadata) {
+        getThUserMetadata().putAll(metadata);
+        return this;
+    }
+
+    /**
+     * 设置不支持元数据时抛出异常
+     */
+    public UploadPretreatment setNotSupportMetadataThrowException(
+            boolean flag, Boolean notSupportMetadataThrowException) {
+        if (flag) this.notSupportMetadataThrowException = notSupportMetadataThrowException;
+        return this;
+    }
+
+    /**
+     * 设置不支持 ACL 时抛出异常
+     */
+    public UploadPretreatment setNotSupportAclThrowException(boolean flag, Boolean notSupportAclThrowException) {
+        if (flag) this.notSupportAclThrowException = notSupportAclThrowException;
+        return this;
+    }
+
+    /**
+     * 获取附加属性字典
+     */
+    public Dict getAttr() {
+        if (attr == null) attr = new Dict();
+        return attr;
+    }
+
+    /**
+     * 设置附加属性
+     */
+    public UploadPretreatment putAttr(boolean flag, String key, Object value) {
+        if (flag) putAttr(key, value);
+        return this;
+    }
+
+    /**
+     * 设置附加属性
+     */
+    public UploadPretreatment putAttr(String key, Object value) {
+        getAttr().put(key, value);
+        return this;
+    }
+
+    /**
+     * 设置附加属性
+     */
+    public UploadPretreatment putAttrAll(boolean flag, Map<String, Object> attr) {
+        if (flag) putAttrAll(attr);
+        return this;
+    }
+
+    /**
+     * 设置附加属性
+     */
+    public UploadPretreatment putAttrAll(Map<String, Object> attr) {
+        getAttr().putAll(attr);
+        return this;
+    }
+
+    /**
+     * 进行图片处理,可以进行裁剪、旋转、缩放、水印等操作
+     */
+    public UploadPretreatment image(boolean flag, Consumer<Thumbnails.Builder<? extends InputStream>> consumer) {
+        if (flag) image(consumer);
+        return this;
+    }
+
+    /**
+     * 进行图片处理,可以进行裁剪、旋转、缩放、水印等操作
+     */
+    public UploadPretreatment image(Consumer<Thumbnails.Builder<? extends InputStream>> consumer) {
+        try (InputStream in = fileWrapper.getInputStream()) {
+            Thumbnails.Builder<? extends InputStream> builder = Thumbnails.of(in);
+            consumer.accept(builder);
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            builder.toOutputStream(out);
+            fileWrapper = fileStorageService.wrapper(out.toByteArray(), fileWrapper.getName());
+            return this;
+        } catch (IOException e) {
+            throw new FileStorageRuntimeException("图片处理失败!", e);
+        }
+    }
+
+    /**
+     * 缩放到指定大小
+     */
+    public UploadPretreatment image(boolean flag, int width, int height) {
+        if (flag) image(width, height);
+        return this;
+    }
+
+    /**
+     * 缩放到指定大小
+     */
+    public UploadPretreatment image(int width, int height) {
+        return image(th -> th.size(width, height));
+    }
+
+    /**
+     * 缩放到 200*200 大小
+     */
+    public UploadPretreatment image(boolean flag) {
+        if (flag) image();
+        return this;
+    }
+
+    /**
+     * 缩放到 200*200 大小
+     */
+    public UploadPretreatment image() {
+        return image(th -> th.size(200, 200));
+    }
+
+    /**
+     * 清空缩略图
+     */
+    public UploadPretreatment clearThumbnail(boolean flag) {
+        if (flag) clearThumbnail();
+        return this;
+    }
+
+    /**
+     * 清空缩略图
+     */
+    public UploadPretreatment clearThumbnail() {
+        thumbnailBytes = null;
+        return this;
+    }
+
+    /**
+     * 通过指定 file 生成缩略图,
+     * 如果 file 是 InputStream、FileWrapper 等可以自动关闭的对象,操作完成后会自动关闭
+     */
+    public UploadPretreatment thumbnailOf(boolean flag, Object file) {
+        if (flag) thumbnailOf(file);
+        return this;
+    }
+
+    /**
+     * 通过指定 file 生成缩略图,
+     * 如果 file 是 InputStream、FileWrapper 等可以自动关闭的对象,操作完成后会自动关闭
+     */
+    public UploadPretreatment thumbnailOf(Object file) {
+        try {
+            thumbnailBytes = IoUtil.readBytes(fileStorageService.wrapper(file).getInputStream());
+        } catch (IOException e) {
+            if (!ignoreThumbnailException) throw new FileStorageRuntimeException("生成缩略图失败!", e);
+        }
+        return this;
+    }
+
+    /**
+     * 通过指定 file 生成缩略图并进行图片处理,
+     * 可以进行裁剪、旋转、缩放、水印等操作,默认输出图片格式通过 thumbnailSuffix 获取,
+     * 如果 file 是 InputStream、FileWrapper 等可以自动关闭的对象,操作完成后会自动关闭
+     */
+    public UploadPretreatment thumbnailOf(
+            boolean flag, Object file, Consumer<Thumbnails.Builder<? extends InputStream>> consumer) {
+        if (flag) thumbnailOf(file, consumer);
+        return this;
+    }
+
+    /**
+     * 通过指定 file 生成缩略图并进行图片处理,
+     * 可以进行裁剪、旋转、缩放、水印等操作,默认输出图片格式通过 thumbnailSuffix 获取,
+     * 如果 file 是 InputStream、FileWrapper 等可以自动关闭的对象,操作完成后会自动关闭
+     */
+    public UploadPretreatment thumbnailOf(Object file, Consumer<Thumbnails.Builder<? extends InputStream>> consumer) {
+        try {
+            return thumbnail(consumer, fileStorageService.wrapper(file).getInputStream());
+        } catch (IOException e) {
+            if (!ignoreThumbnailException) throw new FileStorageRuntimeException("生成缩略图失败!", e);
+        }
+        return this;
+    }
+
+    /**
+     * 生成缩略图并进行图片处理,如果缩略图已存在则使用已有的缩略图进行处理,
+     * 可以进行裁剪、旋转、缩放、水印等操作,默认输出图片格式通过 thumbnailSuffix 获取
+     */
+    public UploadPretreatment thumbnail(boolean flag, Consumer<Thumbnails.Builder<? extends InputStream>> consumer) {
+        if (flag) thumbnail(consumer);
+        return this;
+    }
+
+    /**
+     * 生成缩略图并进行图片处理,如果缩略图已存在则使用已有的缩略图进行处理,
+     * 可以进行裁剪、旋转、缩放、水印等操作,默认输出图片格式通过 thumbnailSuffix 获取
+     */
+    public UploadPretreatment thumbnail(Consumer<Thumbnails.Builder<? extends InputStream>> consumer) {
+        try {
+            if (thumbnailBytes == null) {
+                return fileWrapper.getInputStreamMaskResetReturn(in -> thumbnail(consumer, in));
+            } else {
+                return thumbnail(consumer, new ByteArrayInputStream(thumbnailBytes));
+            }
+        } catch (IOException e) {
+            if (!ignoreThumbnailException) throw new FileStorageRuntimeException("生成缩略图失败!", e);
+        }
+        return this;
+    }
+
+    /**
+     * 通过指定 InputStream 生成缩略图并进行图片处理,
+     * 可以进行裁剪、旋转、缩放、水印等操作,默认输出图片格式通过 thumbnailSuffix 获取,
+     * 操作完成后不会自动关闭 InputStream
+     */
+    private UploadPretreatment thumbnail(Consumer<Thumbnails.Builder<? extends InputStream>> consumer, InputStream in) {
+        try {
+            Thumbnails.Builder<? extends InputStream> builder = Thumbnails.of(in);
+            builder.outputFormat(FileNameUtil.extName(thumbnailSuffix));
+            consumer.accept(builder);
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            builder.toOutputStream(out);
+            thumbnailBytes = out.toByteArray();
+        } catch (IOException e) {
+            if (!ignoreThumbnailException) throw new FileStorageRuntimeException("生成缩略图失败!", e);
+        }
+        return this;
+    }
+
+    /**
+     * 生成缩略图并缩放到指定大小,默认输出图片格式通过 thumbnailSuffix 获取
+     */
+    public UploadPretreatment thumbnail(boolean flag, int width, int height) {
+        if (flag) thumbnail(width, height);
+        return this;
+    }
+
+    /**
+     * 生成缩略图并缩放到指定大小,默认输出图片格式通过 thumbnailSuffix 获取
+     */
+    public UploadPretreatment thumbnail(int width, int height) {
+        return thumbnail(th -> th.size(width, height));
+    }
+
+    /**
+     * 生成缩略图并缩放到 200*200 大小,默认输出图片格式通过 thumbnailSuffix 获取
+     */
+    public UploadPretreatment thumbnail(boolean flag) {
+        if (flag) thumbnail();
+        return this;
+    }
+
+    /**
+     * 生成缩略图并缩放到 200*200 大小,默认输出图片格式通过 thumbnailSuffix 获取
+     */
+    public UploadPretreatment thumbnail() {
+        return thumbnail(200, 200);
+    }
+
+    /**
+     * 设置文件的访问控制列表,一般情况下只有对象存储支持该功能
+     */
+    public UploadPretreatment setFileAcl(boolean flag, Object acl) {
+        if (flag) setFileAcl(acl);
+        return this;
+    }
+
+    /**
+     * 设置文件的访问控制列表,一般情况下只有对象存储支持该功能
+     */
+    public UploadPretreatment setThFileAcl(boolean flag, Object acl) {
+        if (flag) setThFileAcl(acl);
+        return this;
+    }
+
+    /**
+     * 同时设置 fileAcl 和 thFileAcl 两个属性
+     * 详情见{@link FileInfo#setFileAcl}
+     */
+    public UploadPretreatment setAcl(boolean flag, Object acl) {
+        if (flag) setAcl(acl);
+        return this;
+    }
+
+    /**
+     * 同时设置 fileAcl 和 thFileAcl 两个属性
+     * 详情见{@link FileInfo#setFileAcl}
+     */
+    public UploadPretreatment setAcl(Object acl) {
+        this.fileAcl = acl;
+        this.thFileAcl = acl;
+        return this;
+    }
+
+    /**
+     * 添加一个哈希计算器
+     * @param hashCalculator 哈希计算器
+     */
+    @Override
+    public UploadPretreatment setHashCalculator(HashCalculator hashCalculator) {
+        hashCalculatorManager.setHashCalculator(hashCalculator);
+        return this;
+    }
+
+    /**
+     * 设置哈希计算器管理器(如果条件为 true)
+     * @param flag 条件
+     * @param hashCalculatorManager 哈希计算器管理器
+     */
+    public UploadPretreatment setHashCalculatorManager(boolean flag, HashCalculatorManager hashCalculatorManager) {
+        if (flag) setHashCalculatorManager(hashCalculatorManager);
+        return this;
+    }
+
+    /**
+     * 上传文件,成功返回文件信息,失败返回null
+     */
+    public FileInfo upload() {
+        return new UploadActuator(this).execute();
+    }
+
+    /**
+     * 上传文件,成功返回文件信息,失败返回null。此方法仅限内部使用
+     */
+    public FileInfo upload(FileStorage fileStorage, FileRecorder fileRecorder, List<FileStorageAspect> aspectList) {
+        return new UploadActuator(this).execute(fileStorage, fileRecorder, aspectList);
+    }
+
+    /**
+     * 获取增强版本的 InputStream ,可以带进度监听、计算哈希等功能
+     */
+    public InputStreamPlus getInputStreamPlus() throws IOException {
+        return getInputStreamPlus(true);
+    }
+
+    /**
+     * 获取增强版本的 InputStream ,可以带进度监听、计算哈希等功能
+     */
+    public InputStreamPlus getInputStreamPlus(boolean hasListener) throws IOException {
+        if (inputStreamPlus == null) {
+            inputStreamPlus = new InputStreamPlus(
+                    fileWrapper.getInputStream(),
+                    hasListener ? progressListener : null,
+                    fileWrapper.getSize(),
+                    hashCalculatorManager);
+        }
+        return inputStreamPlus;
+    }
+
+    /**
+     * 直接获取 InputStreamPlus,仅限内部使用
+     */
+    @Deprecated
+    public InputStreamPlus getInputStreamPlusDirect() {
+        return inputStreamPlus;
+    }
+}

+ 37 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/AbortMultipartUploadAspectChain.java

@@ -0,0 +1,37 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+import org.dromara.x.file.storage.core.upload.AbortMultipartUploadPretreatment;
+
+/**
+ * 手动分片上传-取消的切面调用链
+ */
+@Getter
+@Setter
+public class AbortMultipartUploadAspectChain {
+
+    private AbortMultipartUploadAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public AbortMultipartUploadAspectChain(
+            Iterable<FileStorageAspect> aspects, AbortMultipartUploadAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public FileInfo next(AbortMultipartUploadPretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().abortMultipartUploadAround(this, pre, fileStorage, fileRecorder);
+        } else {
+            return callback.run(pre, fileStorage, fileRecorder);
+        }
+    }
+}

+ 13 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/AbortMultipartUploadAspectChainCallback.java

@@ -0,0 +1,13 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+import org.dromara.x.file.storage.core.upload.AbortMultipartUploadPretreatment;
+
+/**
+ * 手动分片上传-取消切面调用链结束回调
+ */
+public interface AbortMultipartUploadAspectChainCallback {
+    FileInfo run(AbortMultipartUploadPretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder);
+}

+ 44 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/CompleteMultipartUploadAspectChain.java

@@ -0,0 +1,44 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+import org.dromara.x.file.storage.core.tika.ContentTypeDetect;
+import org.dromara.x.file.storage.core.upload.CompleteMultipartUploadPretreatment;
+
+/**
+ * 手动分片上传-完成的切面调用链
+ */
+@Getter
+@Setter
+public class CompleteMultipartUploadAspectChain {
+
+    private CompleteMultipartUploadAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public CompleteMultipartUploadAspectChain(
+            Iterable<FileStorageAspect> aspects, CompleteMultipartUploadAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public FileInfo next(
+            CompleteMultipartUploadPretreatment pre,
+            FileStorage fileStorage,
+            FileRecorder fileRecorder,
+            ContentTypeDetect contentTypeDetect) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator
+                    .next()
+                    .completeMultipartUploadAround(this, pre, fileStorage, fileRecorder, contentTypeDetect);
+        } else {
+            return callback.run(pre, fileStorage, fileRecorder, contentTypeDetect);
+        }
+    }
+}

+ 18 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/CompleteMultipartUploadAspectChainCallback.java

@@ -0,0 +1,18 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+import org.dromara.x.file.storage.core.tika.ContentTypeDetect;
+import org.dromara.x.file.storage.core.upload.CompleteMultipartUploadPretreatment;
+
+/**
+ * 手动分片上传-完成切面调用链结束回调
+ */
+public interface CompleteMultipartUploadAspectChainCallback {
+    FileInfo run(
+            CompleteMultipartUploadPretreatment pre,
+            FileStorage fileStorage,
+            FileRecorder fileRecorder,
+            ContentTypeDetect contentTypeDetect);
+}

+ 37 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/CopyAspectChain.java

@@ -0,0 +1,37 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.copy.CopyPretreatment;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+
+/**
+ * 复制的切面调用链
+ */
+@Getter
+@Setter
+public class CopyAspectChain {
+
+    private CopyAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public CopyAspectChain(Iterable<FileStorageAspect> aspects, CopyAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public FileInfo next(
+            FileInfo srcFileInfo, CopyPretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().copyAround(this, srcFileInfo, pre, fileStorage, fileRecorder);
+        } else {
+            return callback.run(srcFileInfo, pre, fileStorage, fileRecorder);
+        }
+    }
+}

+ 13 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/CopyAspectChainCallback.java

@@ -0,0 +1,13 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.copy.CopyPretreatment;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+
+/**
+ * 复制切面调用链结束回调
+ */
+public interface CopyAspectChainCallback {
+    FileInfo run(FileInfo srcFileInfo, CopyPretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder);
+}

+ 35 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/DeleteAspectChain.java

@@ -0,0 +1,35 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+
+/**
+ * 删除的切面调用链
+ */
+@Getter
+@Setter
+public class DeleteAspectChain {
+
+    private DeleteAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public DeleteAspectChain(Iterable<FileStorageAspect> aspects, DeleteAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public boolean next(FileInfo fileInfo, FileStorage fileStorage, FileRecorder fileRecorder) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().deleteAround(this, fileInfo, fileStorage, fileRecorder);
+        } else {
+            return callback.run(fileInfo, fileStorage, fileRecorder);
+        }
+    }
+}

+ 12 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/DeleteAspectChainCallback.java

@@ -0,0 +1,12 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+
+/**
+ * 删除切面调用链结束回调
+ */
+public interface DeleteAspectChainCallback {
+    boolean run(FileInfo fileInfo, FileStorage fileStorage, FileRecorder fileRecorder);
+}

+ 36 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/DownloadAspectChain.java

@@ -0,0 +1,36 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.function.Consumer;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 下载的切面调用链
+ */
+@Getter
+@Setter
+public class DownloadAspectChain {
+
+    private DownloadAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public DownloadAspectChain(Iterable<FileStorageAspect> aspects, DownloadAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public void next(FileInfo fileInfo, FileStorage fileStorage, Consumer<InputStream> consumer) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            aspectIterator.next().downloadAround(this, fileInfo, fileStorage, consumer);
+        } else {
+            callback.run(fileInfo, fileStorage, consumer);
+        }
+    }
+}

+ 13 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/DownloadAspectChainCallback.java

@@ -0,0 +1,13 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.io.InputStream;
+import java.util.function.Consumer;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 下载切面调用链结束回调
+ */
+public interface DownloadAspectChainCallback {
+    void run(FileInfo fileInfo, FileStorage fileStorage, Consumer<InputStream> consumer);
+}

+ 36 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/DownloadThAspectChain.java

@@ -0,0 +1,36 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.function.Consumer;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 下载缩略图的切面调用链
+ */
+@Getter
+@Setter
+public class DownloadThAspectChain {
+
+    private DownloadThAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public DownloadThAspectChain(Iterable<FileStorageAspect> aspects, DownloadThAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public void next(FileInfo fileInfo, FileStorage fileStorage, Consumer<InputStream> consumer) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            aspectIterator.next().downloadThAround(this, fileInfo, fileStorage, consumer);
+        } else {
+            callback.run(fileInfo, fileStorage, consumer);
+        }
+    }
+}

+ 13 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/DownloadThAspectChainCallback.java

@@ -0,0 +1,13 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.io.InputStream;
+import java.util.function.Consumer;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 下载缩略图切面调用链结束回调
+ */
+public interface DownloadThAspectChainCallback {
+    void run(FileInfo fileInfo, FileStorage fileStorage, Consumer<InputStream> consumer);
+}

+ 34 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/ExistsAspectChain.java

@@ -0,0 +1,34 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 文件是否存在的切面调用链
+ */
+@Getter
+@Setter
+public class ExistsAspectChain {
+
+    private ExistsAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public ExistsAspectChain(Iterable<FileStorageAspect> aspects, ExistsAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public boolean next(FileInfo fileInfo, FileStorage fileStorage) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().existsAround(this, fileInfo, fileStorage);
+        } else {
+            return callback.run(fileInfo, fileStorage);
+        }
+    }
+}

+ 11 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/ExistsAspectChainCallback.java

@@ -0,0 +1,11 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 文件是否存在切面调用链结束回调
+ */
+public interface ExistsAspectChainCallback {
+    boolean run(FileInfo fileInfo, FileStorage fileStorage);
+}

+ 271 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/FileStorageAspect.java

@@ -0,0 +1,271 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.io.InputStream;
+import java.util.Date;
+import java.util.function.Consumer;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.UploadPretreatment;
+import org.dromara.x.file.storage.core.copy.CopyPretreatment;
+import org.dromara.x.file.storage.core.get.*;
+import org.dromara.x.file.storage.core.move.MovePretreatment;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.presigned.GeneratePresignedUrlPretreatment;
+import org.dromara.x.file.storage.core.presigned.GeneratePresignedUrlResult;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+import org.dromara.x.file.storage.core.tika.ContentTypeDetect;
+import org.dromara.x.file.storage.core.upload.*;
+import org.dromara.x.file.storage.core.upload.MultipartUploadSupportInfo;
+
+/**
+ * 文件服务切面接口,用来干预文件上传,删除等
+ */
+public interface FileStorageAspect {
+
+    /**
+     * 上传,成功返回文件信息,失败返回 null
+     */
+    default FileInfo uploadAround(
+            UploadAspectChain chain,
+            FileInfo fileInfo,
+            UploadPretreatment pre,
+            FileStorage fileStorage,
+            FileRecorder fileRecorder) {
+        return chain.next(fileInfo, pre, fileStorage, fileRecorder);
+    }
+
+    /**
+     * 是否支持手动分片上传
+     */
+    default MultipartUploadSupportInfo isSupportMultipartUpload(
+            IsSupportMultipartUploadAspectChain chain, FileStorage fileStorage) {
+        return chain.next(fileStorage);
+    }
+
+    /**
+     * 手动分片上传-初始化,成功返回文件信息,失败返回 null
+     */
+    default FileInfo initiateMultipartUploadAround(
+            InitiateMultipartUploadAspectChain chain,
+            FileInfo fileInfo,
+            InitiateMultipartUploadPretreatment pre,
+            FileStorage fileStorage,
+            FileRecorder fileRecorder) {
+        return chain.next(fileInfo, pre, fileStorage, fileRecorder);
+    }
+
+    /**
+     * 手动分片上传-上传分片,成功返回文件信息
+     */
+    default FilePartInfo uploadPart(
+            UploadPartAspectChain chain,
+            UploadPartPretreatment pre,
+            FileStorage fileStorage,
+            FileRecorder fileRecorder) {
+        return chain.next(pre, fileStorage, fileRecorder);
+    }
+
+    /**
+     * 手动分片上传-完成
+     */
+    default FileInfo completeMultipartUploadAround(
+            CompleteMultipartUploadAspectChain chain,
+            CompleteMultipartUploadPretreatment pre,
+            FileStorage fileStorage,
+            FileRecorder fileRecorder,
+            ContentTypeDetect contentTypeDetect) {
+        return chain.next(pre, fileStorage, fileRecorder, contentTypeDetect);
+    }
+
+    /**
+     * 手动分片上传-取消
+     */
+    default FileInfo abortMultipartUploadAround(
+            AbortMultipartUploadAspectChain chain,
+            AbortMultipartUploadPretreatment pre,
+            FileStorage fileStorage,
+            FileRecorder fileRecorder) {
+        return chain.next(pre, fileStorage, fileRecorder);
+    }
+
+    /**
+     * 手动分片上传-列举已上传的分片
+     */
+    default FilePartInfoList listParts(ListPartsAspectChain chain, ListPartsPretreatment pre, FileStorage fileStorage) {
+        return chain.next(pre, fileStorage);
+    }
+
+    /**
+     * 是否支持列举文件
+     */
+    default ListFilesSupportInfo isSupportListFiles(IsSupportListFilesAspectChain chain, FileStorage fileStorage) {
+        return chain.next(fileStorage);
+    }
+
+    /**
+     * 列举文件
+     */
+    default ListFilesResult listFiles(ListFilesAspectChain chain, ListFilesPretreatment pre, FileStorage fileStorage) {
+        return chain.next(pre, fileStorage);
+    }
+
+    /**
+     * 获取文件
+     */
+    default RemoteFileInfo getFile(GetFileAspectChain chain, GetFilePretreatment pre, FileStorage fileStorage) {
+        return chain.next(pre, fileStorage);
+    }
+
+    /**
+     * 删除文件,成功返回 true
+     */
+    default boolean deleteAround(
+            DeleteAspectChain chain, FileInfo fileInfo, FileStorage fileStorage, FileRecorder fileRecorder) {
+        return chain.next(fileInfo, fileStorage, fileRecorder);
+    }
+
+    /**
+     * 文件是否存在,成功返回 true
+     */
+    default boolean existsAround(ExistsAspectChain chain, FileInfo fileInfo, FileStorage fileStorage) {
+        return chain.next(fileInfo, fileStorage);
+    }
+
+    /**
+     * 下载文件,成功返回文件内容
+     */
+    default void downloadAround(
+            DownloadAspectChain chain, FileInfo fileInfo, FileStorage fileStorage, Consumer<InputStream> consumer) {
+        chain.next(fileInfo, fileStorage, consumer);
+    }
+
+    /**
+     * 下载缩略图文件,成功返回文件内容
+     */
+    default void downloadThAround(
+            DownloadThAspectChain chain, FileInfo fileInfo, FileStorage fileStorage, Consumer<InputStream> consumer) {
+        chain.next(fileInfo, fileStorage, consumer);
+    }
+
+    /**
+     * 是否支持对文件生成可以签名访问的 URL
+     */
+    default boolean isSupportPresignedUrlAround(IsSupportPresignedUrlAspectChain chain, FileStorage fileStorage) {
+        return chain.next(fileStorage);
+    }
+
+    /**
+     * 对文件生成可以签名访问的 URL,无法生成则返回 null
+     */
+    default GeneratePresignedUrlResult generatePresignedUrlAround(
+            GeneratePresignedUrlAspectChain chain, GeneratePresignedUrlPretreatment pre, FileStorage fileStorage) {
+        return chain.next(pre, fileStorage);
+    }
+
+    /**
+     * 对缩略图文件生成可以签名访问的 URL,无法生成则返回 null
+     */
+    default String generateThPresignedUrlAround(
+            GenerateThPresignedUrlAspectChain chain, FileInfo fileInfo, Date expiration, FileStorage fileStorage) {
+        return chain.next(fileInfo, expiration, fileStorage);
+    }
+
+    /**
+     * 是否支持文件的访问控制列表
+     */
+    default boolean isSupportAclAround(IsSupportAclAspectChain chain, FileStorage fileStorage) {
+        return chain.next(fileStorage);
+    }
+
+    /**
+     * 设置文件的访问控制列表,一般情况下只有对象存储支持该功能
+     */
+    default boolean setFileAcl(SetFileAclAspectChain chain, FileInfo fileInfo, Object acl, FileStorage fileStorage) {
+        return chain.next(fileInfo, acl, fileStorage);
+    }
+
+    /**
+     * 设置缩略图文件的访问控制列表,一般情况下只有对象存储支持该功能
+     */
+    default boolean setThFileAcl(
+            SetThFileAclAspectChain chain, FileInfo fileInfo, Object acl, FileStorage fileStorage) {
+        return chain.next(fileInfo, acl, fileStorage);
+    }
+
+    /**
+     * 是否支持 Metadata
+     */
+    default boolean isSupportMetadataAround(IsSupportMetadataAspectChain chain, FileStorage fileStorage) {
+        return chain.next(fileStorage);
+    }
+
+    /**
+     * 是否支持同存储平台复制
+     */
+    default boolean isSupportSameCopyAround(IsSupportSameCopyAspectChain chain, FileStorage fileStorage) {
+        return chain.next(fileStorage);
+    }
+
+    /**
+     * 复制,成功返回文件信息
+     */
+    default FileInfo copyAround(
+            CopyAspectChain chain,
+            FileInfo srcFileInfo,
+            CopyPretreatment pre,
+            FileStorage fileStorage,
+            FileRecorder fileRecorder) {
+        return chain.next(srcFileInfo, pre, fileStorage, fileRecorder);
+    }
+
+    /**
+     * 同存储平台复制,成功返回文件信息
+     */
+    default FileInfo sameCopyAround(
+            SameCopyAspectChain chain,
+            FileInfo srcFileInfo,
+            FileInfo destFileInfo,
+            CopyPretreatment pre,
+            FileStorage fileStorage,
+            FileRecorder fileRecorder) {
+        return chain.next(srcFileInfo, destFileInfo, pre, fileStorage, fileRecorder);
+    }
+
+    /**
+     * 是否支持同存储平台移动
+     */
+    default boolean isSupportSameMoveAround(IsSupportSameMoveAspectChain chain, FileStorage fileStorage) {
+        return chain.next(fileStorage);
+    }
+
+    /**
+     * 移动,成功返回文件信息
+     */
+    default FileInfo moveAround(
+            MoveAspectChain chain,
+            FileInfo srcFileInfo,
+            MovePretreatment pre,
+            FileStorage fileStorage,
+            FileRecorder fileRecorder) {
+        return chain.next(srcFileInfo, pre, fileStorage, fileRecorder);
+    }
+
+    /**
+     * 同存储平台移动,成功返回文件信息
+     */
+    default FileInfo sameMoveAround(
+            SameMoveAspectChain chain,
+            FileInfo srcFileInfo,
+            FileInfo destFileInfo,
+            MovePretreatment pre,
+            FileStorage fileStorage,
+            FileRecorder fileRecorder) {
+        return chain.next(srcFileInfo, destFileInfo, pre, fileStorage, fileRecorder);
+    }
+
+    /**
+     * 通过反射调用指定存储平台的方法
+     */
+    default <T> T invoke(InvokeAspectChain chain, FileStorage fileStorage, String method, Object[] args) {
+        return chain.next(fileStorage, method, args);
+    }
+}

+ 36 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/GeneratePresignedUrlAspectChain.java

@@ -0,0 +1,36 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.presigned.GeneratePresignedUrlPretreatment;
+import org.dromara.x.file.storage.core.presigned.GeneratePresignedUrlResult;
+
+/**
+ * 对文件生成可以签名访问的 URL 的切面调用链
+ */
+@Getter
+@Setter
+public class GeneratePresignedUrlAspectChain {
+
+    private GeneratePresignedUrlAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public GeneratePresignedUrlAspectChain(
+            Iterable<FileStorageAspect> aspects, GeneratePresignedUrlAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public GeneratePresignedUrlResult next(GeneratePresignedUrlPretreatment pre, FileStorage fileStorage) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().generatePresignedUrlAround(this, pre, fileStorage);
+        } else {
+            return callback.run(pre, fileStorage);
+        }
+    }
+}

+ 12 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/GeneratePresignedUrlAspectChainCallback.java

@@ -0,0 +1,12 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.presigned.GeneratePresignedUrlPretreatment;
+import org.dromara.x.file.storage.core.presigned.GeneratePresignedUrlResult;
+
+/**
+ * 对文件生成可以签名访问的 URL 切面调用链结束回调
+ */
+public interface GeneratePresignedUrlAspectChainCallback {
+    GeneratePresignedUrlResult run(GeneratePresignedUrlPretreatment pre, FileStorage fileStorage);
+}

+ 36 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/GenerateThPresignedUrlAspectChain.java

@@ -0,0 +1,36 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Date;
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 对缩略图文件生成可以签名访问的 URL 的切面调用链
+ */
+@Getter
+@Setter
+public class GenerateThPresignedUrlAspectChain {
+
+    private GenerateThPresignedUrlAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public GenerateThPresignedUrlAspectChain(
+            Iterable<FileStorageAspect> aspects, GenerateThPresignedUrlAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public String next(FileInfo fileInfo, Date expiration, FileStorage fileStorage) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().generateThPresignedUrlAround(this, fileInfo, expiration, fileStorage);
+        } else {
+            return callback.run(fileInfo, expiration, fileStorage);
+        }
+    }
+}

+ 12 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/GenerateThPresignedUrlAspectChainCallback.java

@@ -0,0 +1,12 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Date;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 对缩略图文件生成可以签名访问的 URL 切面调用链结束回调
+ */
+public interface GenerateThPresignedUrlAspectChainCallback {
+    String run(FileInfo fileInfo, Date expiration, FileStorage fileStorage);
+}

+ 35 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/GetFileAspectChain.java

@@ -0,0 +1,35 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.get.GetFilePretreatment;
+import org.dromara.x.file.storage.core.get.RemoteFileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 获取文件的切面调用链
+ */
+@Getter
+@Setter
+public class GetFileAspectChain {
+
+    private GetFileAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public GetFileAspectChain(Iterable<FileStorageAspect> aspects, GetFileAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public RemoteFileInfo next(GetFilePretreatment pre, FileStorage fileStorage) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().getFile(this, pre, fileStorage);
+        } else {
+            return callback.run(pre, fileStorage);
+        }
+    }
+}

+ 12 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/GetFileAspectChainCallback.java

@@ -0,0 +1,12 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.get.GetFilePretreatment;
+import org.dromara.x.file.storage.core.get.RemoteFileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 获取文件切面调用链结束回调
+ */
+public interface GetFileAspectChainCallback {
+    RemoteFileInfo run(GetFilePretreatment pre, FileStorage fileStorage);
+}

+ 41 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/InitiateMultipartUploadAspectChain.java

@@ -0,0 +1,41 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+import org.dromara.x.file.storage.core.upload.InitiateMultipartUploadPretreatment;
+
+/**
+ * 手动分片上传-初始化的切面调用链
+ */
+@Getter
+@Setter
+public class InitiateMultipartUploadAspectChain {
+
+    private InitiateMultipartUploadAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public InitiateMultipartUploadAspectChain(
+            Iterable<FileStorageAspect> aspects, InitiateMultipartUploadAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public FileInfo next(
+            FileInfo fileInfo,
+            InitiateMultipartUploadPretreatment pre,
+            FileStorage fileStorage,
+            FileRecorder fileRecorder) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().initiateMultipartUploadAround(this, fileInfo, pre, fileStorage, fileRecorder);
+        } else {
+            return callback.run(fileInfo, pre, fileStorage, fileRecorder);
+        }
+    }
+}

+ 17 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/InitiateMultipartUploadAspectChainCallback.java

@@ -0,0 +1,17 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+import org.dromara.x.file.storage.core.upload.InitiateMultipartUploadPretreatment;
+
+/**
+ * 手动分片上传-初始化切面调用链结束回调
+ */
+public interface InitiateMultipartUploadAspectChainCallback {
+    FileInfo run(
+            FileInfo fileInfo,
+            InitiateMultipartUploadPretreatment pre,
+            FileStorage fileStorage,
+            FileRecorder fileRecorder);
+}

+ 33 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/InvokeAspectChain.java

@@ -0,0 +1,33 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 通过反射调用指定存储平台的方法的切面调用链
+ */
+@Getter
+@Setter
+public class InvokeAspectChain {
+
+    private InvokeAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public InvokeAspectChain(Iterable<FileStorageAspect> aspects, InvokeAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public <T> T next(FileStorage fileStorage, String method, Object[] args) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().invoke(this, fileStorage, method, args);
+        } else {
+            return callback.run(fileStorage, method, args);
+        }
+    }
+}

+ 10 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/InvokeAspectChainCallback.java

@@ -0,0 +1,10 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 通过反射调用指定存储平台的方法的切面调用链结束回调
+ */
+public interface InvokeAspectChainCallback {
+    <T> T run(FileStorage fileStorage, String method, Object[] args);
+}

+ 33 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportAclAspectChain.java

@@ -0,0 +1,33 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 是否支持文件的访问控制列表 的切面调用链
+ */
+@Getter
+@Setter
+public class IsSupportAclAspectChain {
+
+    private IsSupportAclAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public IsSupportAclAspectChain(Iterable<FileStorageAspect> aspects, IsSupportAclAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public boolean next(FileStorage fileStorage) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().isSupportAclAround(this, fileStorage);
+        } else {
+            return callback.run(fileStorage);
+        }
+    }
+}

+ 10 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportAclAspectChainCallback.java

@@ -0,0 +1,10 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 是否支持文件的访问控制列表 切面调用链结束回调
+ */
+public interface IsSupportAclAspectChainCallback {
+    boolean run(FileStorage fileStorage);
+}

+ 35 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportListFilesAspectChain.java

@@ -0,0 +1,35 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.get.ListFilesSupportInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 是否支持手动分片上传的切面调用链
+ */
+@Getter
+@Setter
+public class IsSupportListFilesAspectChain {
+
+    private IsSupportListFilesChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public IsSupportListFilesAspectChain(
+            Iterable<FileStorageAspect> aspects, IsSupportListFilesChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public ListFilesSupportInfo next(FileStorage fileStorage) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().isSupportListFiles(this, fileStorage);
+        } else {
+            return callback.run(fileStorage);
+        }
+    }
+}

+ 11 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportListFilesChainCallback.java

@@ -0,0 +1,11 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.get.ListFilesSupportInfo;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 是否支持手动分片上传的切面调用链结束回调
+ */
+public interface IsSupportListFilesChainCallback {
+    ListFilesSupportInfo run(FileStorage fileStorage);
+}

+ 34 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportMetadataAspectChain.java

@@ -0,0 +1,34 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 是否支持 Metadata 的切面调用链
+ */
+@Getter
+@Setter
+public class IsSupportMetadataAspectChain {
+
+    private IsSupportMetadataAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public IsSupportMetadataAspectChain(
+            Iterable<FileStorageAspect> aspects, IsSupportMetadataAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public boolean next(FileStorage fileStorage) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().isSupportMetadataAround(this, fileStorage);
+        } else {
+            return callback.run(fileStorage);
+        }
+    }
+}

+ 10 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportMetadataAspectChainCallback.java

@@ -0,0 +1,10 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 是否支持 Metadata 切面调用链结束回调
+ */
+public interface IsSupportMetadataAspectChainCallback {
+    boolean run(FileStorage fileStorage);
+}

+ 35 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportMultipartUploadAspectChain.java

@@ -0,0 +1,35 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.upload.MultipartUploadSupportInfo;
+
+/**
+ * 是否支持手动分片上传的切面调用链
+ */
+@Getter
+@Setter
+public class IsSupportMultipartUploadAspectChain {
+
+    private IsSupportMultipartUploadChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public IsSupportMultipartUploadAspectChain(
+            Iterable<FileStorageAspect> aspects, IsSupportMultipartUploadChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public MultipartUploadSupportInfo next(FileStorage fileStorage) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().isSupportMultipartUpload(this, fileStorage);
+        } else {
+            return callback.run(fileStorage);
+        }
+    }
+}

+ 11 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportMultipartUploadChainCallback.java

@@ -0,0 +1,11 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.upload.MultipartUploadSupportInfo;
+
+/**
+ * 是否支持手动分片上传的切面调用链结束回调
+ */
+public interface IsSupportMultipartUploadChainCallback {
+    MultipartUploadSupportInfo run(FileStorage fileStorage);
+}

+ 34 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportPresignedUrlAspectChain.java

@@ -0,0 +1,34 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 是否支持对文件生成可以签名访问的 URL 的切面调用链
+ */
+@Getter
+@Setter
+public class IsSupportPresignedUrlAspectChain {
+
+    private IsSupportPresignedUrlAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public IsSupportPresignedUrlAspectChain(
+            Iterable<FileStorageAspect> aspects, IsSupportPresignedUrlAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public boolean next(FileStorage fileStorage) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().isSupportPresignedUrlAround(this, fileStorage);
+        } else {
+            return callback.run(fileStorage);
+        }
+    }
+}

+ 10 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportPresignedUrlAspectChainCallback.java

@@ -0,0 +1,10 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 是否支持对文件生成可以签名访问的 URL 切面调用链结束回调
+ */
+public interface IsSupportPresignedUrlAspectChainCallback {
+    boolean run(FileStorage fileStorage);
+}

+ 34 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportSameCopyAspectChain.java

@@ -0,0 +1,34 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 是否支持同存储平台复制的切面调用链
+ */
+@Getter
+@Setter
+public class IsSupportSameCopyAspectChain {
+
+    private IsSupportSameCopyAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public IsSupportSameCopyAspectChain(
+            Iterable<FileStorageAspect> aspects, IsSupportSameCopyAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public boolean next(FileStorage fileStorage) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().isSupportSameCopyAround(this, fileStorage);
+        } else {
+            return callback.run(fileStorage);
+        }
+    }
+}

+ 10 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportSameCopyAspectChainCallback.java

@@ -0,0 +1,10 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 是否支持同存储平台复制的切面调用链结束回调
+ */
+public interface IsSupportSameCopyAspectChainCallback {
+    boolean run(FileStorage fileStorage);
+}

+ 34 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportSameMoveAspectChain.java

@@ -0,0 +1,34 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 是否支持同存储平台移动的切面调用链
+ */
+@Getter
+@Setter
+public class IsSupportSameMoveAspectChain {
+
+    private IsSupportSameMoveAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public IsSupportSameMoveAspectChain(
+            Iterable<FileStorageAspect> aspects, IsSupportSameMoveAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public boolean next(FileStorage fileStorage) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().isSupportSameMoveAround(this, fileStorage);
+        } else {
+            return callback.run(fileStorage);
+        }
+    }
+}

+ 10 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportSameMoveAspectChainCallback.java

@@ -0,0 +1,10 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 是否支持同存储平台移动的切面调用链结束回调
+ */
+public interface IsSupportSameMoveAspectChainCallback {
+    boolean run(FileStorage fileStorage);
+}

+ 35 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/ListFilesAspectChain.java

@@ -0,0 +1,35 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.get.ListFilesPretreatment;
+import org.dromara.x.file.storage.core.get.ListFilesResult;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 列举文件的切面调用链
+ */
+@Getter
+@Setter
+public class ListFilesAspectChain {
+
+    private ListFilesAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public ListFilesAspectChain(Iterable<FileStorageAspect> aspects, ListFilesAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public ListFilesResult next(ListFilesPretreatment pre, FileStorage fileStorage) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().listFiles(this, pre, fileStorage);
+        } else {
+            return callback.run(pre, fileStorage);
+        }
+    }
+}

+ 12 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/ListFilesAspectChainCallback.java

@@ -0,0 +1,12 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.get.ListFilesPretreatment;
+import org.dromara.x.file.storage.core.get.ListFilesResult;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+
+/**
+ * 列举文件切面调用链结束回调
+ */
+public interface ListFilesAspectChainCallback {
+    ListFilesResult run(ListFilesPretreatment pre, FileStorage fileStorage);
+}

+ 35 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/ListPartsAspectChain.java

@@ -0,0 +1,35 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.upload.FilePartInfoList;
+import org.dromara.x.file.storage.core.upload.ListPartsPretreatment;
+
+/**
+ * 手动分片上传-列举已上传的分片的切面调用链
+ */
+@Getter
+@Setter
+public class ListPartsAspectChain {
+
+    private ListPartsAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public ListPartsAspectChain(Iterable<FileStorageAspect> aspects, ListPartsAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public FilePartInfoList next(ListPartsPretreatment pre, FileStorage fileStorage) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().listParts(this, pre, fileStorage);
+        } else {
+            return callback.run(pre, fileStorage);
+        }
+    }
+}

+ 12 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/ListPartsAspectChainCallback.java

@@ -0,0 +1,12 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.upload.FilePartInfoList;
+import org.dromara.x.file.storage.core.upload.ListPartsPretreatment;
+
+/**
+ * 手动分片上传-列举已上传的分片切面调用链结束回调
+ */
+public interface ListPartsAspectChainCallback {
+    FilePartInfoList run(ListPartsPretreatment pre, FileStorage fileStorage);
+}

+ 37 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/MoveAspectChain.java

@@ -0,0 +1,37 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.move.MovePretreatment;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+
+/**
+ * 移动的切面调用链
+ */
+@Getter
+@Setter
+public class MoveAspectChain {
+
+    private MoveAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public MoveAspectChain(Iterable<FileStorageAspect> aspects, MoveAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public FileInfo next(
+            FileInfo srcFileInfo, MovePretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator.next().moveAround(this, srcFileInfo, pre, fileStorage, fileRecorder);
+        } else {
+            return callback.run(srcFileInfo, pre, fileStorage, fileRecorder);
+        }
+    }
+}

+ 13 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/MoveAspectChainCallback.java

@@ -0,0 +1,13 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.move.MovePretreatment;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+
+/**
+ * 移动切面调用链结束回调
+ */
+public interface MoveAspectChainCallback {
+    FileInfo run(FileInfo srcFileInfo, MovePretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder);
+}

+ 43 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/SameCopyAspectChain.java

@@ -0,0 +1,43 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import java.util.Iterator;
+import lombok.Getter;
+import lombok.Setter;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.copy.CopyPretreatment;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+
+/**
+ * 同存储平台复制的切面调用链
+ */
+@Getter
+@Setter
+public class SameCopyAspectChain {
+
+    private SameCopyAspectChainCallback callback;
+    private Iterator<FileStorageAspect> aspectIterator;
+
+    public SameCopyAspectChain(Iterable<FileStorageAspect> aspects, SameCopyAspectChainCallback callback) {
+        this.aspectIterator = aspects.iterator();
+        this.callback = callback;
+    }
+
+    /**
+     * 调用下一个切面
+     */
+    public FileInfo next(
+            FileInfo srcFileInfo,
+            FileInfo destFileInfo,
+            CopyPretreatment pre,
+            FileStorage fileStorage,
+            FileRecorder fileRecorder) {
+        if (aspectIterator.hasNext()) { // 还有下一个
+            return aspectIterator
+                    .next()
+                    .sameCopyAround(this, srcFileInfo, destFileInfo, pre, fileStorage, fileRecorder);
+        } else {
+            return callback.run(srcFileInfo, destFileInfo, pre, fileStorage, fileRecorder);
+        }
+    }
+}

+ 18 - 0
jnpf-file-core/src/main/java/org/dromara/x/file/storage/core/aspect/SameCopyAspectChainCallback.java

@@ -0,0 +1,18 @@
+package org.dromara.x.file.storage.core.aspect;
+
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.copy.CopyPretreatment;
+import org.dromara.x.file.storage.core.platform.FileStorage;
+import org.dromara.x.file.storage.core.recorder.FileRecorder;
+
+/**
+ * 同存储平台复制切面调用链结束回调
+ */
+public interface SameCopyAspectChainCallback {
+    FileInfo run(
+            FileInfo srcFileInfo,
+            FileInfo destFileInfo,
+            CopyPretreatment pre,
+            FileStorage fileStorage,
+            FileRecorder fileRecorder);
+}

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini