├── docs ├── .nojekyll ├── CNAME ├── _footer.md ├── _sidebar.md ├── _navbar.md ├── 脱离SpringBoot单独使用.md ├── 在Solon中使用.md ├── Metadata.md ├── 识别文件的MIME类型.md ├── 文件适配器.md └── index.html ├── .mvn ├── jvm.config ├── maven.config ├── wrapper │ └── maven-wrapper.properties └── settings.xml ├── x-file-storage-tests ├── x-file-storage-fastdfs-test │ ├── docker │ │ ├── .env │ │ ├── docker-compose.yaml │ │ └── nginx.conf │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ ├── fastdfs.txt │ │ │ │ └── application.yaml │ │ │ └── java │ │ │ │ └── org │ │ │ │ └── dromara │ │ │ │ └── x │ │ │ │ └── file │ │ │ │ └── storage │ │ │ │ └── fastdfs │ │ │ │ └── test │ │ │ │ ├── package-info.java │ │ │ │ └── FastDfsTestApplication.java │ │ └── test │ │ │ ├── java │ │ │ └── org │ │ │ │ └── dromara │ │ │ │ └── x │ │ │ │ └── file │ │ │ │ └── storage │ │ │ │ └── fastdfs │ │ │ │ └── test │ │ │ │ ├── package-info.java │ │ │ │ └── FastDfsFileStorageTests.java │ │ │ └── resources │ │ │ └── application.yaml │ └── pom.xml ├── x-file-storage-general-test │ └── src │ │ ├── test │ │ ├── resources │ │ │ ├── image.jpg │ │ │ └── image2.jpg │ │ └── java │ │ │ └── org │ │ │ └── dromara │ │ │ └── x │ │ │ └── file │ │ │ └── storage │ │ │ └── test │ │ │ ├── DirectUseFileStorageTest.java │ │ │ ├── HttpServletRequestFileTest.java │ │ │ ├── FileStorageServiceBigFileTest.java │ │ │ └── FileStorageServicePoolTest.java │ │ └── main │ │ ├── java │ │ └── org │ │ │ └── dromara │ │ │ └── x │ │ │ └── file │ │ │ └── storage │ │ │ └── test │ │ │ ├── mapper │ │ │ ├── FileDetailMapper.java │ │ │ ├── FilePartDetailMapper.java │ │ │ └── xml │ │ │ │ ├── FilePartDetailMapper.xml │ │ │ │ └── FileDetailMapper.xml │ │ │ ├── SpringFileStorageTestApplication.java │ │ │ ├── resolver │ │ │ └── LazyStandardServletMultipartResolver.java │ │ │ ├── model │ │ │ └── FilePartDetail.java │ │ │ └── service │ │ │ └── FilePartDetailService.java │ │ └── resources │ │ └── db │ │ └── schema-mysql.sql ├── README.md └── pom.xml ├── .sdkmanrc ├── lombok.config ├── x-file-storage-solon ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── solon │ │ │ └── solon.dromara.x-file-storage.properties │ │ └── java │ │ └── org │ │ └── dromara │ │ └── x │ │ └── file │ │ └── storage │ │ └── solon │ │ ├── file │ │ ├── UploadedFileWrapperAdapter.java │ │ └── UploadedFileWrapper.java │ │ └── DromaraXFileStoragePluginImpl.java └── pom.xml ├── x-file-storage-core └── src │ └── main │ └── java │ └── org │ └── dromara │ └── x │ └── file │ └── storage │ └── core │ ├── tika │ ├── TikaFactory.java │ ├── ContentTypeDetect.java │ ├── DefaultTikaFactory.java │ └── TikaContentTypeDetect.java │ ├── IOExceptionFunction.java │ ├── aspect │ ├── IsSupportAclAspectChainCallback.java │ ├── IsSupportMetadataAspectChainCallback.java │ ├── IsSupportSameCopyAspectChainCallback.java │ ├── IsSupportSameMoveAspectChainCallback.java │ ├── InvokeAspectChainCallback.java │ ├── IsSupportPresignedUrlAspectChainCallback.java │ ├── ExistsAspectChainCallback.java │ ├── SetFileAclAspectChainCallback.java │ ├── IsSupportListFilesChainCallback.java │ ├── SetThFileAclAspectChainCallback.java │ ├── IsSupportMultipartUploadChainCallback.java │ ├── DeleteAspectChainCallback.java │ ├── GenerateThPresignedUrlAspectChainCallback.java │ ├── GetFileAspectChainCallback.java │ ├── DownloadAspectChainCallback.java │ ├── ListFilesAspectChainCallback.java │ ├── DownloadThAspectChainCallback.java │ ├── ListPartsAspectChainCallback.java │ ├── CopyAspectChainCallback.java │ ├── MoveAspectChainCallback.java │ ├── UploadAspectChainCallback.java │ ├── GeneratePresignedUrlAspectChainCallback.java │ ├── UploadPartAspectChainCallback.java │ ├── AbortMultipartUploadAspectChainCallback.java │ ├── SameCopyAspectChainCallback.java │ ├── SameMoveAspectChainCallback.java │ ├── InitiateMultipartUploadAspectChainCallback.java │ ├── CompleteMultipartUploadAspectChainCallback.java │ ├── IsSupportAclAspectChain.java │ ├── InvokeAspectChain.java │ ├── IsSupportSameCopyAspectChain.java │ ├── IsSupportSameMoveAspectChain.java │ ├── ExistsAspectChain.java │ ├── IsSupportMetadataAspectChain.java │ ├── IsSupportPresignedUrlAspectChain.java │ ├── SetFileAclAspectChain.java │ ├── SetThFileAclAspectChain.java │ ├── IsSupportListFilesAspectChain.java │ ├── GetFileAspectChain.java │ ├── ListFilesAspectChain.java │ ├── DownloadAspectChain.java │ ├── IsSupportMultipartUploadAspectChain.java │ ├── ListPartsAspectChain.java │ ├── DeleteAspectChain.java │ ├── DownloadThAspectChain.java │ ├── GenerateThPresignedUrlAspectChain.java │ ├── CopyAspectChain.java │ ├── MoveAspectChain.java │ ├── UploadAspectChain.java │ ├── UploadPartAspectChain.java │ ├── GeneratePresignedUrlAspectChain.java │ ├── AbortMultipartUploadAspectChain.java │ ├── SameCopyAspectChain.java │ ├── SameMoveAspectChain.java │ ├── InitiateMultipartUploadAspectChain.java │ └── CompleteMultipartUploadAspectChain.java │ ├── hash │ ├── HashCalculator.java │ ├── HashInfo.java │ ├── HashCalculatorManager.java │ └── MessageDigestHashCalculator.java │ ├── IOExceptionConsumer.java │ ├── platform │ ├── FileStorageClientFactory.java │ ├── UpyunUssFileStorageClientFactory.java │ ├── HuaweiObsFileStorageClientFactory.java │ ├── MinioFileStorageClientFactory.java │ ├── AliyunOssFileStorageClientFactory.java │ ├── VolcengineTosFileStorageClientFactory.java │ ├── WebDavFileStorageClientFactory.java │ ├── TencentCosFileStorageClientFactory.java │ ├── BaiduBosFileStorageClientFactory.java │ ├── AmazonS3FileStorageClientFactory.java │ ├── GoogleCloudStorageFileStorageClientFactory.java │ └── AzureBlobStorageFileStorageClientFactory.java │ ├── exception │ └── FileStorageRuntimeException.java │ ├── file │ ├── FileWrapperAdapter.java │ ├── UriFileWrapper.java │ ├── InputStreamFileWrapper.java │ ├── ByteFileWrapper.java │ ├── LocalFileWrapper.java │ ├── ByteFileWrapperAdapter.java │ ├── LocalFileWrapperAdapter.java │ ├── HttpServletRequestFileWrapper.java │ ├── InputStreamFileWrapperAdapter.java │ ├── JavaxHttpServletRequestFileWrapperAdapter.java │ ├── JakartaHttpServletRequestFileWrapperAdapter.java │ └── FileWrapper.java │ ├── get │ ├── ListFilesSupportInfo.java │ ├── ListFilesResult.java │ ├── GetFileActuator.java │ └── GetFilePretreatment.java │ ├── upload │ ├── FilePartInfoList.java │ ├── FilePartInfo.java │ ├── MultipartUploadSupportInfo.java │ ├── AbortMultipartUploadPretreatment.java │ ├── UploadPartActuator.java │ ├── AbortMultipartUploadActuator.java │ └── CompleteMultipartUploadPretreatment.java │ ├── recorder │ ├── FileRecorder.java │ └── DefaultFileRecorder.java │ ├── presigned │ ├── GeneratePresignedUrlResult.java │ └── GeneratePresignedUrlActuator.java │ ├── ProgressInputStream.java │ ├── ProgressListener.java │ └── util │ └── KebabCaseInsensitiveMap.java ├── .gitee └── ISSUE_TEMPLATE │ ├── config.yaml │ ├── feature.yml │ └── question.yml ├── x-file-storage-spring ├── src │ └── main │ │ └── java │ │ └── org │ │ └── dromara │ │ └── x │ │ └── file │ │ └── storage │ │ └── spring │ │ ├── EnableFileStorage.java │ │ └── file │ │ ├── MultipartFileWrapperAdapter.java │ │ └── MultipartFileWrapper.java └── pom.xml └── .gitignore /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.mvn/jvm.config: -------------------------------------------------------------------------------- 1 | -XX:+UseG1GC -Xmx2g -Xms2g -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | x-file-storage.xuyanwu.cn 2 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -s=.mvn/settings.xml 2 | -Dmaven.test.skip=true -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-fastdfs-test/docker/.env: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT_NAME=fastdfs-test -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-fastdfs-test/src/main/resources/fastdfs.txt: -------------------------------------------------------------------------------- 1 | This is a fastDFS test file. 2 | 3 | Storage server test. -------------------------------------------------------------------------------- /.sdkmanrc: -------------------------------------------------------------------------------- 1 | # Enable auto-env through the sdkman_auto_env config 2 | # Add key=value pairs of SDKs to use below 3 | maven=3.9.9 4 | java=8.0.302-open -------------------------------------------------------------------------------- /docs/_footer.md: -------------------------------------------------------------------------------- 1 | [我的博客(常年不更新)](https://xuyanwu.cn) 2 |        3 | [用 JavaScript 在浏览器中控制台观看 BadApple!!](https://app.xuyanwu.cn/BadApple/) 4 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.singular.useGuava=true 2 | lombok.toString.doNotUseGetters=true 3 | lombok.equalsAndHashCode.callSuper=call 4 | lombok.toString.callSuper=call 5 | #lombok.accessors.chain=true -------------------------------------------------------------------------------- /x-file-storage-solon/src/main/resources/META-INF/solon/solon.dromara.x-file-storage.properties: -------------------------------------------------------------------------------- 1 | solon.plugin=org.dromara.x.file.storage.solon.DromaraXFileStoragePluginImpl 2 | solon.plugin.priority=1 -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-general-test/src/test/resources/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/x-file-storage/HEAD/x-file-storage-tests/x-file-storage-general-test/src/test/resources/image.jpg -------------------------------------------------------------------------------- /x-file-storage-tests/README.md: -------------------------------------------------------------------------------- 1 | ```shell 2 | #测试模块需要在项目根目录下执行以下命令,通过 Profile 来切换,默认不包含测试模块 3 | mvn clean package -P test 4 | ``` 5 | 6 | # IDEA 开发时包含测试模块 7 | ![](https://cdn.0512.host/images/20231026/101354393-x7d.png) -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-general-test/src/test/resources/image2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/x-file-storage/HEAD/x-file-storage-tests/x-file-storage-general-test/src/test/resources/image2.jpg -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/tika/TikaFactory.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.tika; 2 | 3 | import org.apache.tika.Tika; 4 | 5 | /** 6 | * Tika 工厂类接口 7 | */ 8 | public interface TikaFactory { 9 | 10 | Tika getTika(); 11 | } 12 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-fastdfs-test/src/main/java/org/dromara/x/file/storage/fastdfs/test/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * There is no description. 3 | * 4 | * @author XS 5 | * @version 1.0 6 | * @date 2023/10/23 9:58 7 | */ 8 | package org.dromara.x.file.storage.fastdfs.test; 9 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-fastdfs-test/src/test/java/org/dromara/x/file/storage/fastdfs/test/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * There is no description. 3 | * 4 | * @author XS 5 | * @version 1.0 6 | * @date 2023/10/23 9:59 7 | */ 8 | package org.dromara.x.file.storage.fastdfs.test; 9 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/IOExceptionFunction.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * 带 IOException 异常的 Function 7 | */ 8 | public interface IOExceptionFunction { 9 | R apply(T t) throws IOException; 10 | } 11 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-general-test/src/main/java/org/dromara/x/file/storage/test/mapper/FileDetailMapper.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.test.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import org.dromara.x.file.storage.test.model.FileDetail; 5 | 6 | public interface FileDetailMapper extends BaseMapper {} 7 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-general-test/src/main/java/org/dromara/x/file/storage/test/mapper/FilePartDetailMapper.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.test.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import org.dromara.x.file.storage.test.model.FilePartDetail; 5 | 6 | public interface FilePartDetailMapper extends BaseMapper {} 7 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportAclAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.platform.FileStorage; 4 | 5 | /** 6 | * 是否支持文件的访问控制列表 切面调用链结束回调 7 | */ 8 | public interface IsSupportAclAspectChainCallback { 9 | boolean run(FileStorage fileStorage); 10 | } 11 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportMetadataAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.platform.FileStorage; 4 | 5 | /** 6 | * 是否支持 Metadata 切面调用链结束回调 7 | */ 8 | public interface IsSupportMetadataAspectChainCallback { 9 | boolean run(FileStorage fileStorage); 10 | } 11 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportSameCopyAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.platform.FileStorage; 4 | 5 | /** 6 | * 是否支持同存储平台复制的切面调用链结束回调 7 | */ 8 | public interface IsSupportSameCopyAspectChainCallback { 9 | boolean run(FileStorage fileStorage); 10 | } 11 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportSameMoveAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.platform.FileStorage; 4 | 5 | /** 6 | * 是否支持同存储平台移动的切面调用链结束回调 7 | */ 8 | public interface IsSupportSameMoveAspectChainCallback { 9 | boolean run(FileStorage fileStorage); 10 | } 11 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/InvokeAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.platform.FileStorage; 4 | 5 | /** 6 | * 通过反射调用指定存储平台的方法的切面调用链结束回调 7 | */ 8 | public interface InvokeAspectChainCallback { 9 | T run(FileStorage fileStorage, String method, Object[] args); 10 | } 11 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportPresignedUrlAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.platform.FileStorage; 4 | 5 | /** 6 | * 是否支持对文件生成可以签名访问的 URL 切面调用链结束回调 7 | */ 8 | public interface IsSupportPresignedUrlAspectChainCallback { 9 | boolean run(FileStorage fileStorage); 10 | } 11 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/ExistsAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.FileInfo; 4 | import org.dromara.x.file.storage.core.platform.FileStorage; 5 | 6 | /** 7 | * 文件是否存在切面调用链结束回调 8 | */ 9 | public interface ExistsAspectChainCallback { 10 | boolean run(FileInfo fileInfo, FileStorage fileStorage); 11 | } 12 | -------------------------------------------------------------------------------- /.gitee/ISSUE_TEMPLATE/config.yaml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 官方文档 4 | url: https://x-file-storage.xuyanwu.cn/ 5 | about: 提供使用指南、教程、功能使用、介绍和常见问题解答 6 | - name: 常见问题 7 | url: https%3A%2F%2Fx-file-storage.xuyanwu.cn%2F%23%2F%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98 8 | about: 提供常见问题解答 9 | - name: 加入QQ群 515706495 10 | url: "https%3A%2F%2Fjq.qq.com%2F%3F_wv%3D1027%26k%3DeGfeNqka" 11 | about: 一起交流 -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/SetFileAclAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.FileInfo; 4 | import org.dromara.x.file.storage.core.platform.FileStorage; 5 | 6 | /** 7 | * 获取文件的访问控制列表调用链结束回调 8 | */ 9 | public interface SetFileAclAspectChainCallback { 10 | boolean run(FileInfo fileInfo, Object acl, FileStorage fileStorage); 11 | } 12 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportListFilesChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.get.ListFilesSupportInfo; 4 | import org.dromara.x.file.storage.core.platform.FileStorage; 5 | 6 | /** 7 | * 是否支持手动分片上传的切面调用链结束回调 8 | */ 9 | public interface IsSupportListFilesChainCallback { 10 | ListFilesSupportInfo run(FileStorage fileStorage); 11 | } 12 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/SetThFileAclAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.FileInfo; 4 | import org.dromara.x.file.storage.core.platform.FileStorage; 5 | 6 | /** 7 | * 设置缩略图文件的访问控制列表调用链结束回调 8 | */ 9 | public interface SetThFileAclAspectChainCallback { 10 | boolean run(FileInfo fileInfo, Object acl, FileStorage fileStorage); 11 | } 12 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportMultipartUploadChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.platform.FileStorage; 4 | import org.dromara.x.file.storage.core.upload.MultipartUploadSupportInfo; 5 | 6 | /** 7 | * 是否支持手动分片上传的切面调用链结束回调 8 | */ 9 | public interface IsSupportMultipartUploadChainCallback { 10 | MultipartUploadSupportInfo run(FileStorage fileStorage); 11 | } 12 | -------------------------------------------------------------------------------- /x-file-storage-spring/src/main/java/org/dromara/x/file/storage/spring/EnableFileStorage.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.spring; 2 | 3 | import java.lang.annotation.*; 4 | import org.springframework.context.annotation.Import; 5 | 6 | /** 7 | * 启用文件存储,会自动根据配置文件进行加载 8 | */ 9 | @Retention(RetentionPolicy.RUNTIME) 10 | @Target({ElementType.TYPE}) 11 | @Documented 12 | @Import({FileStorageAutoConfiguration.class, SpringFileStorageProperties.class}) 13 | public @interface EnableFileStorage {} 14 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/DeleteAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.FileInfo; 4 | import org.dromara.x.file.storage.core.platform.FileStorage; 5 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 6 | 7 | /** 8 | * 删除切面调用链结束回调 9 | */ 10 | public interface DeleteAspectChainCallback { 11 | boolean run(FileInfo fileInfo, FileStorage fileStorage, FileRecorder fileRecorder); 12 | } 13 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/GenerateThPresignedUrlAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Date; 4 | import org.dromara.x.file.storage.core.FileInfo; 5 | import org.dromara.x.file.storage.core.platform.FileStorage; 6 | 7 | /** 8 | * 对缩略图文件生成可以签名访问的 URL 切面调用链结束回调 9 | */ 10 | public interface GenerateThPresignedUrlAspectChainCallback { 11 | String run(FileInfo fileInfo, Date expiration, FileStorage fileStorage); 12 | } 13 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/GetFileAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.get.GetFilePretreatment; 4 | import org.dromara.x.file.storage.core.get.RemoteFileInfo; 5 | import org.dromara.x.file.storage.core.platform.FileStorage; 6 | 7 | /** 8 | * 获取文件切面调用链结束回调 9 | */ 10 | public interface GetFileAspectChainCallback { 11 | RemoteFileInfo run(GetFilePretreatment pre, FileStorage fileStorage); 12 | } 13 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/DownloadAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.io.InputStream; 4 | import java.util.function.Consumer; 5 | import org.dromara.x.file.storage.core.FileInfo; 6 | import org.dromara.x.file.storage.core.platform.FileStorage; 7 | 8 | /** 9 | * 下载切面调用链结束回调 10 | */ 11 | public interface DownloadAspectChainCallback { 12 | void run(FileInfo fileInfo, FileStorage fileStorage, Consumer consumer); 13 | } 14 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/ListFilesAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.get.ListFilesPretreatment; 4 | import org.dromara.x.file.storage.core.get.ListFilesResult; 5 | import org.dromara.x.file.storage.core.platform.FileStorage; 6 | 7 | /** 8 | * 列举文件切面调用链结束回调 9 | */ 10 | public interface ListFilesAspectChainCallback { 11 | ListFilesResult run(ListFilesPretreatment pre, FileStorage fileStorage); 12 | } 13 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/DownloadThAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.io.InputStream; 4 | import java.util.function.Consumer; 5 | import org.dromara.x.file.storage.core.FileInfo; 6 | import org.dromara.x.file.storage.core.platform.FileStorage; 7 | 8 | /** 9 | * 下载缩略图切面调用链结束回调 10 | */ 11 | public interface DownloadThAspectChainCallback { 12 | void run(FileInfo fileInfo, FileStorage fileStorage, Consumer consumer); 13 | } 14 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/ListPartsAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.platform.FileStorage; 4 | import org.dromara.x.file.storage.core.upload.FilePartInfoList; 5 | import org.dromara.x.file.storage.core.upload.ListPartsPretreatment; 6 | 7 | /** 8 | * 手动分片上传-列举已上传的分片切面调用链结束回调 9 | */ 10 | public interface ListPartsAspectChainCallback { 11 | FilePartInfoList run(ListPartsPretreatment pre, FileStorage fileStorage); 12 | } 13 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/tika/ContentTypeDetect.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.tika; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | 7 | /** 8 | * 识别文件的 MIME 类型 9 | */ 10 | public interface ContentTypeDetect { 11 | 12 | String detect(File file) throws IOException; 13 | 14 | String detect(byte[] bytes); 15 | 16 | String detect(byte[] bytes, String filename); 17 | 18 | String detect(InputStream in, String filename) throws IOException; 19 | } 20 | -------------------------------------------------------------------------------- /.mvn/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | aliyun 8 | Aliyun Maven Mirror 9 | https://maven.aliyun.com/repository/central 10 | central 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [📚简介](/ "简介") 2 | * [🍭快速入门](快速入门 "快速入门") 3 | * [🍬基础功能](基础功能 "基础功能") 4 | * [🚚迁移文件](迁移文件 "迁移文件") 5 | * [🍉ACL 访问控制列表](acl "ACL 访问控制列表") 6 | * [🥑预签名 URL](预签名URL "预签名 URL") 7 | * [🌼Metadata 元数据](Metadata "Metadata 元数据") 8 | * [🥦存储平台](存储平台 "存储平台") 9 | * [🌽文件适配器](文件适配器 "文件适配器") 10 | * [🔍️识别文件的 MIME 类型](识别文件的MIME类型 "识别文件的 MIME 类型") 11 | * [🍵️计算哈希](hash "hash") 12 | * [🧪切面](切面 "切面") 13 | * [🌱脱离 SpringBoot 单独使用](脱离SpringBoot单独使用 "脱离 SpringBoot 单独使用") 14 | * [🍩在 Solon 中使用](在Solon中使用 "在 Solon 中使用") 15 | * [🙋‍♂️常见问题](常见问题 "常见问题") 16 | * [📜更新记录](更新记录 "更新记录") 17 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/hash/HashCalculator.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.hash; 2 | 3 | /** 4 | * 哈希计算器接口 5 | */ 6 | public interface HashCalculator { 7 | 8 | /** 9 | * 获取哈希名称,例如 MD5、SHA1、SHA256等,详情{@link org.dromara.x.file.storage.core.constant.Constant.Hash} 10 | */ 11 | String getName(); 12 | 13 | /** 14 | * 获取哈希值,一般情况下获取后将不能继续增量计算哈希 15 | */ 16 | String getValue(); 17 | 18 | /** 19 | * 增量计算哈希 20 | * @param bytes 字节数组 21 | */ 22 | void update(byte[] bytes); 23 | } 24 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-fastdfs-test/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | dromara: 2 | x-file-storage: #文件存储配置,不使用的情况下可以不写 3 | default-platform: fastdfs-1 #默认使用的存储平台 4 | fastdfs: # 谷歌云存储 5 | - platform: fastdfs-1 # 存储平台标识 6 | enable-storage: true # 启用存储 7 | # tracker-server: 8 | # server-addr: 172.28.133.14:22122 9 | storage-server: 10 | server-addr: 172.28.133.14:23000 11 | extra: 12 | group-name: group2 13 | http-anti-steal-token: false 14 | http-secret-key: FastDFS1234567890 15 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/CopyAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.FileInfo; 4 | import org.dromara.x.file.storage.core.copy.CopyPretreatment; 5 | import org.dromara.x.file.storage.core.platform.FileStorage; 6 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 7 | 8 | /** 9 | * 复制切面调用链结束回调 10 | */ 11 | public interface CopyAspectChainCallback { 12 | FileInfo run(FileInfo srcFileInfo, CopyPretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder); 13 | } 14 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/MoveAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.FileInfo; 4 | import org.dromara.x.file.storage.core.move.MovePretreatment; 5 | import org.dromara.x.file.storage.core.platform.FileStorage; 6 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 7 | 8 | /** 9 | * 移动切面调用链结束回调 10 | */ 11 | public interface MoveAspectChainCallback { 12 | FileInfo run(FileInfo srcFileInfo, MovePretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder); 13 | } 14 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/UploadAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.FileInfo; 4 | import org.dromara.x.file.storage.core.UploadPretreatment; 5 | import org.dromara.x.file.storage.core.platform.FileStorage; 6 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 7 | 8 | /** 9 | * 上传切面调用链结束回调 10 | */ 11 | public interface UploadAspectChainCallback { 12 | FileInfo run(FileInfo fileInfo, UploadPretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder); 13 | } 14 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/GeneratePresignedUrlAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.platform.FileStorage; 4 | import org.dromara.x.file.storage.core.presigned.GeneratePresignedUrlPretreatment; 5 | import org.dromara.x.file.storage.core.presigned.GeneratePresignedUrlResult; 6 | 7 | /** 8 | * 对文件生成可以签名访问的 URL 切面调用链结束回调 9 | */ 10 | public interface GeneratePresignedUrlAspectChainCallback { 11 | GeneratePresignedUrlResult run(GeneratePresignedUrlPretreatment pre, FileStorage fileStorage); 12 | } 13 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/tika/DefaultTikaFactory.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.tika; 2 | 3 | import org.apache.tika.Tika; 4 | 5 | /** 6 | * 默认的 Tika 工厂类 7 | */ 8 | public class DefaultTikaFactory implements TikaFactory { 9 | private volatile Tika tika; 10 | 11 | @Override 12 | public Tika getTika() { 13 | if (tika == null) { 14 | synchronized (this) { 15 | if (tika == null) { 16 | tika = new Tika(); 17 | } 18 | } 19 | } 20 | return tika; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-fastdfs-test/src/test/resources/application.yaml: -------------------------------------------------------------------------------- 1 | dromara: 2 | x-file-storage: #文件存储配置,不使用的情况下可以不写 3 | default-platform: fastdfs-1 #默认使用的存储平台 4 | fastdfs: # 谷歌云存储 5 | - platform: fastdfs-1 # 存储平台标识 6 | enable-storage: true # 启用存储 7 | domain: http://172.28.133.14:8888 8 | # tracker-server: 9 | # server-addr: 172.28.133.14:22122 10 | storage-server: 11 | server-addr: 172.28.133.14:23000 12 | extra: 13 | group-name: group2 14 | http-anti-steal-token: false 15 | http-secret-key: FastDFS1234567890 -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/UploadPartAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.platform.FileStorage; 4 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 5 | import org.dromara.x.file.storage.core.upload.FilePartInfo; 6 | import org.dromara.x.file.storage.core.upload.UploadPartPretreatment; 7 | 8 | /** 9 | * 手动分片上传-上传分片切面调用链结束回调 10 | */ 11 | public interface UploadPartAspectChainCallback { 12 | FilePartInfo run(UploadPartPretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder); 13 | } 14 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/AbortMultipartUploadAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.FileInfo; 4 | import org.dromara.x.file.storage.core.platform.FileStorage; 5 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 6 | import org.dromara.x.file.storage.core.upload.AbortMultipartUploadPretreatment; 7 | 8 | /** 9 | * 手动分片上传-取消切面调用链结束回调 10 | */ 11 | public interface AbortMultipartUploadAspectChainCallback { 12 | FileInfo run(AbortMultipartUploadPretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder); 13 | } 14 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/IOExceptionConsumer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. 3 | * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. 4 | * 5 | * 6 | * 7 | * 8 | * 9 | * 10 | * 11 | * 12 | * 13 | * 14 | * 15 | * 16 | * 17 | * 18 | * 19 | * 20 | * 21 | * 22 | * 23 | * 24 | */ 25 | package org.dromara.x.file.storage.core; 26 | 27 | import java.io.IOException; 28 | 29 | /** 30 | * 带 IOException 异常的 Consumer 31 | */ 32 | @FunctionalInterface 33 | public interface IOExceptionConsumer { 34 | 35 | void accept(T t) throws IOException; 36 | } 37 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/platform/FileStorageClientFactory.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.platform; 2 | 3 | /** 4 | * 存储平台的 Client 的对象的工厂接口 5 | */ 6 | public interface FileStorageClientFactory extends AutoCloseable { 7 | 8 | /** 9 | * 获取平台 10 | */ 11 | String getPlatform(); 12 | 13 | /** 14 | * 获取 Client ,部分存储平台例如 FTP 、 SFTP 使用完后需要归还 15 | */ 16 | Client getClient(); 17 | 18 | /** 19 | * 归还 Client 20 | */ 21 | default void returnClient(Client client) {} 22 | 23 | /** 24 | * 释放相关资源 25 | */ 26 | @Override 27 | default void close() {} 28 | } 29 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/SameCopyAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.FileInfo; 4 | import org.dromara.x.file.storage.core.copy.CopyPretreatment; 5 | import org.dromara.x.file.storage.core.platform.FileStorage; 6 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 7 | 8 | /** 9 | * 同存储平台复制切面调用链结束回调 10 | */ 11 | public interface SameCopyAspectChainCallback { 12 | FileInfo run( 13 | FileInfo srcFileInfo, 14 | FileInfo destFileInfo, 15 | CopyPretreatment pre, 16 | FileStorage fileStorage, 17 | FileRecorder fileRecorder); 18 | } 19 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/SameMoveAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.FileInfo; 4 | import org.dromara.x.file.storage.core.move.MovePretreatment; 5 | import org.dromara.x.file.storage.core.platform.FileStorage; 6 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 7 | 8 | /** 9 | * 同存储平台移动切面调用链结束回调 10 | */ 11 | public interface SameMoveAspectChainCallback { 12 | FileInfo run( 13 | FileInfo srcFileInfo, 14 | FileInfo destFileInfo, 15 | MovePretreatment pre, 16 | FileStorage fileStorage, 17 | FileRecorder fileRecorder); 18 | } 19 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-general-test/src/main/java/org/dromara/x/file/storage/test/SpringFileStorageTestApplication.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.test; 2 | 3 | import org.dromara.x.file.storage.spring.EnableFileStorage; 4 | import org.mybatis.spring.annotation.MapperScan; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | 8 | @SpringBootApplication 9 | @EnableFileStorage 10 | @MapperScan("org.dromara.x.file.storage.test.mapper") 11 | public class SpringFileStorageTestApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(SpringFileStorageTestApplication.class, args); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/InitiateMultipartUploadAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.FileInfo; 4 | import org.dromara.x.file.storage.core.platform.FileStorage; 5 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 6 | import org.dromara.x.file.storage.core.upload.InitiateMultipartUploadPretreatment; 7 | 8 | /** 9 | * 手动分片上传-初始化切面调用链结束回调 10 | */ 11 | public interface InitiateMultipartUploadAspectChainCallback { 12 | FileInfo run( 13 | FileInfo fileInfo, 14 | InitiateMultipartUploadPretreatment pre, 15 | FileStorage fileStorage, 16 | FileRecorder fileRecorder); 17 | } 18 | -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | * 🌟文档版本 2.3.0 2 | 3 | * [2.3.0](https://x-file-storage.xuyanwu.cn/2.3.0/) 4 | * [2.2.1](https://x-file-storage.xuyanwu.cn/2.2.1/) 5 | * [2.2.0](https://x-file-storage.xuyanwu.cn/2.2.0/) 6 | * [2.1.0](https://x-file-storage.xuyanwu.cn/2.1.0/) 7 | * [2.0.0](https://x-file-storage.xuyanwu.cn/2.0.0/) 8 | * [1.0.3](https://x-file-storage.xuyanwu.cn/1.0.3/) 9 | * [1.0.2](https://x-file-storage.xuyanwu.cn/1.0.2/) 10 | * [1.0.1](https://x-file-storage.xuyanwu.cn/1.0.1/) 11 | * [1.0.0](https://x-file-storage.xuyanwu.cn/1.0.0/) 12 | * [0.7.0](https://x-file-storage.xuyanwu.cn/0.7.0/) 13 | * [0.6.1](https://x-file-storage.xuyanwu.cn/0.6.1/) 14 | * [0.6.0](https://x-file-storage.xuyanwu.cn/0.6.0/) 15 | * [0.5.0](https://x-file-storage.xuyanwu.cn/0.5.0/) 16 | * [0.4.0](https://x-file-storage.xuyanwu.cn/0.4.0/) 17 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/CompleteMultipartUploadAspectChainCallback.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import org.dromara.x.file.storage.core.FileInfo; 4 | import org.dromara.x.file.storage.core.platform.FileStorage; 5 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 6 | import org.dromara.x.file.storage.core.tika.ContentTypeDetect; 7 | import org.dromara.x.file.storage.core.upload.CompleteMultipartUploadPretreatment; 8 | 9 | /** 10 | * 手动分片上传-完成切面调用链结束回调 11 | */ 12 | public interface CompleteMultipartUploadAspectChainCallback { 13 | FileInfo run( 14 | CompleteMultipartUploadPretreatment pre, 15 | FileStorage fileStorage, 16 | FileRecorder fileRecorder, 17 | ContentTypeDetect contentTypeDetect); 18 | } 19 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/exception/FileStorageRuntimeException.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.exception; 2 | 3 | /** 4 | * FileStorage 运行时异常 5 | */ 6 | public class FileStorageRuntimeException extends RuntimeException { 7 | 8 | public FileStorageRuntimeException() {} 9 | 10 | public FileStorageRuntimeException(String message) { 11 | super(message); 12 | } 13 | 14 | public FileStorageRuntimeException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | 18 | public FileStorageRuntimeException(Throwable cause) { 19 | super(cause); 20 | } 21 | 22 | public FileStorageRuntimeException( 23 | String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 24 | super(message, cause, enableSuppression, writableStackTrace); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/file/FileWrapperAdapter.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.file; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * 文件包装适配器接口 7 | */ 8 | public interface FileWrapperAdapter { 9 | 10 | /** 11 | * 是否支持 12 | */ 13 | boolean isSupport(Object source); 14 | 15 | /** 16 | * 获取文件包装 17 | */ 18 | FileWrapper getFileWrapper(Object source, String name, String contentType, Long size) throws IOException; 19 | 20 | /** 21 | * 更新文件包装参数 22 | */ 23 | default FileWrapper updateFileWrapper(FileWrapper fileWrapper, String name, String contentType, Long size) { 24 | if (name != null) fileWrapper.setName(name); 25 | if (contentType != null) fileWrapper.setContentType(contentType); 26 | if (size != null) fileWrapper.setSize(size); 27 | return fileWrapper; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-general-test/src/main/java/org/dromara/x/file/storage/test/resolver/LazyStandardServletMultipartResolver.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.test.resolver; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import org.springframework.web.multipart.MultipartException; 5 | import org.springframework.web.multipart.MultipartHttpServletRequest; 6 | import org.springframework.web.multipart.MultipartResolver; 7 | 8 | public class LazyStandardServletMultipartResolver implements MultipartResolver { 9 | 10 | @Override 11 | public boolean isMultipart(HttpServletRequest request) { 12 | return false; 13 | } 14 | 15 | @Override 16 | public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException { 17 | return null; 18 | } 19 | 20 | @Override 21 | public void cleanupMultipart(MultipartHttpServletRequest request) {} 22 | } 23 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/file/UriFileWrapper.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.file; 2 | 3 | import cn.hutool.core.io.IoUtil; 4 | import java.io.InputStream; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | /** 10 | * URI文件包装类 11 | */ 12 | @Getter 13 | @Setter 14 | @NoArgsConstructor 15 | public class UriFileWrapper implements FileWrapper { 16 | private String name; 17 | private String contentType; 18 | private InputStream inputStream; 19 | private Long size; 20 | 21 | public UriFileWrapper(InputStream inputStream, String name, String contentType, Long size) { 22 | this.name = name; 23 | this.contentType = contentType; 24 | this.inputStream = IoUtil.toMarkSupportStream(inputStream); 25 | this.size = size; 26 | } 27 | 28 | @Override 29 | public InputStream getInputStream() { 30 | return inputStream; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/get/ListFilesSupportInfo.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.get; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.experimental.Accessors; 7 | 8 | /** 9 | * 列举文件支持信息 10 | */ 11 | @Data 12 | @Accessors(chain = true) 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class ListFilesSupportInfo { 16 | /** 17 | * 是否支持列举文件,正常情况下判断此参数就行了 18 | */ 19 | private Boolean isSupport; 20 | 21 | /** 22 | * 每次获取的最大文件数,对象存储一般是 1000 23 | */ 24 | private Integer supportMaxFiles; 25 | 26 | /** 27 | * 不支持列举文件 28 | */ 29 | public static ListFilesSupportInfo notSupport() { 30 | return new ListFilesSupportInfo(false, null); 31 | } 32 | 33 | /** 34 | * 支持全部的列举文件功能 35 | */ 36 | public static ListFilesSupportInfo supportAll() { 37 | return new ListFilesSupportInfo(true, 1000); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/upload/FilePartInfoList.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.upload; 2 | 3 | import java.util.List; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.experimental.Accessors; 7 | import org.dromara.x.file.storage.core.FileInfo; 8 | 9 | /** 10 | * 文件分片信息列出结果 11 | */ 12 | @Data 13 | @NoArgsConstructor 14 | @Accessors(chain = true) 15 | public class FilePartInfoList { 16 | /** 17 | * 文件信息 18 | */ 19 | private FileInfo fileInfo; 20 | /** 21 | * 分片列表 22 | */ 23 | private List list; 24 | /** 25 | * 本次列出的最大分片数量 26 | */ 27 | private Integer maxParts; 28 | /** 29 | * 列表是否被截断,就是当前 uploadId下还有其它分片超出最大分片数量未被列出 30 | */ 31 | private Boolean isTruncated; 32 | /** 33 | * 本次列举的起始位置 34 | */ 35 | private Integer partNumberMarker; 36 | /** 37 | * 下次列举的起始位置 38 | */ 39 | private Integer nextPartNumberMarker; 40 | } 41 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/file/InputStreamFileWrapper.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.file; 2 | 3 | import cn.hutool.core.io.IoUtil; 4 | import java.io.InputStream; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | /** 10 | * InputStream 文件包装类 11 | */ 12 | @Getter 13 | @Setter 14 | @NoArgsConstructor 15 | public class InputStreamFileWrapper implements FileWrapper { 16 | private String name; 17 | private String contentType; 18 | private InputStream inputStream; 19 | private Long size; 20 | 21 | public InputStreamFileWrapper(InputStream inputStream, String name, String contentType, Long size) { 22 | this.name = name; 23 | this.contentType = contentType; 24 | this.inputStream = IoUtil.toMarkSupportStream(inputStream); 25 | this.size = size; 26 | } 27 | 28 | @Override 29 | public InputStream getInputStream() { 30 | return inputStream; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-fastdfs-test/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.dromara.x-file-storage 6 | x-file-storage-tests 7 | ${revision} 8 | 9 | 10 | x-file-storage-fastdfs-test 11 | X File Storage FastDFS Test 12 | 13 | 14 | 15 | io.github.rui8832 16 | fastdfs-client-java 17 | 18 | 19 | cn.hutool 20 | hutool-json 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/file/ByteFileWrapper.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.file; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.InputStream; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | /** 10 | * byte[] 文件包装类 11 | */ 12 | @Getter 13 | @Setter 14 | @NoArgsConstructor 15 | public class ByteFileWrapper implements FileWrapper { 16 | private byte[] bytes; 17 | private String name; 18 | private String contentType; 19 | private InputStream inputStream; 20 | private Long size; 21 | 22 | public ByteFileWrapper(byte[] bytes, String name, String contentType, Long size) { 23 | this.bytes = bytes; 24 | this.name = name; 25 | this.contentType = contentType; 26 | this.size = size; 27 | } 28 | 29 | @Override 30 | public InputStream getInputStream() { 31 | if (inputStream == null) { 32 | inputStream = new ByteArrayInputStream(bytes); 33 | } 34 | return inputStream; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportAclAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.platform.FileStorage; 7 | 8 | /** 9 | * 是否支持文件的访问控制列表 的切面调用链 10 | */ 11 | @Getter 12 | @Setter 13 | public class IsSupportAclAspectChain { 14 | 15 | private IsSupportAclAspectChainCallback callback; 16 | private Iterator aspectIterator; 17 | 18 | public IsSupportAclAspectChain(Iterable aspects, IsSupportAclAspectChainCallback callback) { 19 | this.aspectIterator = aspects.iterator(); 20 | this.callback = callback; 21 | } 22 | 23 | /** 24 | * 调用下一个切面 25 | */ 26 | public boolean next(FileStorage fileStorage) { 27 | if (aspectIterator.hasNext()) { // 还有下一个 28 | return aspectIterator.next().isSupportAclAround(this, fileStorage); 29 | } else { 30 | return callback.run(fileStorage); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-fastdfs-test/src/main/java/org/dromara/x/file/storage/fastdfs/test/FastDfsTestApplication.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.fastdfs.test; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.dromara.x.file.storage.spring.EnableFileStorage; 5 | import org.springframework.boot.ApplicationArguments; 6 | import org.springframework.boot.ApplicationRunner; 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.boot.autoconfigure.SpringBootApplication; 9 | 10 | /** 11 | * There is no description. 12 | * 13 | * @author XS 14 | * @version 1.0 15 | * @date 2023/10/23 9:58 16 | */ 17 | @Slf4j 18 | @EnableFileStorage 19 | @SpringBootApplication 20 | public class FastDfsTestApplication implements ApplicationRunner { 21 | 22 | public static void main(String[] args) { 23 | SpringApplication.run(FastDfsTestApplication.class, args); 24 | } 25 | 26 | @Override 27 | public void run(ApplicationArguments args) throws Exception { 28 | log.info("💦 FastDFS test boot successful."); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-fastdfs-test/docker/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | tracker: 4 | image: season/fastdfs:1.2 5 | restart: always 6 | network_mode: host 7 | ports: 8 | - "22122:22122" 9 | environment: 10 | - TZ=Asia/Shanghai 11 | - LANG=C.UTF-8 12 | - LC_ALL=C.UTF-8 13 | command: tracker 14 | storage: 15 | image: season/fastdfs:1.2 16 | restart: always 17 | network_mode: host 18 | ports: 19 | - "23000:23000" 20 | - "8888:8888" 21 | volumes: 22 | - "./storage.conf:/fdfs_conf/storage.conf" 23 | - "./storage_base_path:/fastdfs/storage/data" 24 | - "./store_path0:/fastdfs/store_path" 25 | environment: 26 | - TZ=Asia/Shanghai 27 | - LANG=C.UTF-8 28 | - LC_ALL=C.UTF-8 29 | command: storage 30 | nginx: 31 | image: season/fastdfs:1.2 32 | restart: always 33 | network_mode: host 34 | ports: 35 | - "8088:8088" 36 | volumes: 37 | - "./nginx.conf:/etc/nginx/conf/nginx.conf" 38 | - "./store_path0:/fastdfs/store_path" 39 | command: "nginx" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | # maven ignore 26 | target/ 27 | bin/ 28 | .sts4-cache/ 29 | *.versionsBackup 30 | pom.xml.tag 31 | pom.xml.releaseBackup 32 | pom.xml.versionsBackup 33 | pom.xml.next 34 | release.properties 35 | dependency-reduced-pom.xml 36 | buildNumber.properties 37 | .mvn/timing.properties 38 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 39 | .mvn/wrapper/maven-wrapper.jar 40 | 41 | # Eclipse m2e generated files 42 | # Eclipse Core 43 | .project 44 | # JDT-specific (Eclipse Java Development Tools) 45 | .classpath 46 | 47 | # eclipse ignore 48 | .settings/ 49 | .project 50 | .classpath 51 | .factorypath 52 | 53 | # idea ignore 54 | .idea/ 55 | *.ipr 56 | *.iml 57 | *.iws 58 | 59 | # system ignore 60 | .DS_Store 61 | Thumbs.db -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/InvokeAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.platform.FileStorage; 7 | 8 | /** 9 | * 通过反射调用指定存储平台的方法的切面调用链 10 | */ 11 | @Getter 12 | @Setter 13 | public class InvokeAspectChain { 14 | 15 | private InvokeAspectChainCallback callback; 16 | private Iterator aspectIterator; 17 | 18 | public InvokeAspectChain(Iterable aspects, InvokeAspectChainCallback callback) { 19 | this.aspectIterator = aspects.iterator(); 20 | this.callback = callback; 21 | } 22 | 23 | /** 24 | * 调用下一个切面 25 | */ 26 | public T next(FileStorage fileStorage, String method, Object[] args) { 27 | if (aspectIterator.hasNext()) { // 还有下一个 28 | return aspectIterator.next().invoke(this, fileStorage, method, args); 29 | } else { 30 | return callback.run(fileStorage, method, args); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportSameCopyAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.platform.FileStorage; 7 | 8 | /** 9 | * 是否支持同存储平台复制的切面调用链 10 | */ 11 | @Getter 12 | @Setter 13 | public class IsSupportSameCopyAspectChain { 14 | 15 | private IsSupportSameCopyAspectChainCallback callback; 16 | private Iterator aspectIterator; 17 | 18 | public IsSupportSameCopyAspectChain( 19 | Iterable aspects, IsSupportSameCopyAspectChainCallback callback) { 20 | this.aspectIterator = aspects.iterator(); 21 | this.callback = callback; 22 | } 23 | 24 | /** 25 | * 调用下一个切面 26 | */ 27 | public boolean next(FileStorage fileStorage) { 28 | if (aspectIterator.hasNext()) { // 还有下一个 29 | return aspectIterator.next().isSupportSameCopyAround(this, fileStorage); 30 | } else { 31 | return callback.run(fileStorage); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportSameMoveAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.platform.FileStorage; 7 | 8 | /** 9 | * 是否支持同存储平台移动的切面调用链 10 | */ 11 | @Getter 12 | @Setter 13 | public class IsSupportSameMoveAspectChain { 14 | 15 | private IsSupportSameMoveAspectChainCallback callback; 16 | private Iterator aspectIterator; 17 | 18 | public IsSupportSameMoveAspectChain( 19 | Iterable aspects, IsSupportSameMoveAspectChainCallback callback) { 20 | this.aspectIterator = aspects.iterator(); 21 | this.callback = callback; 22 | } 23 | 24 | /** 25 | * 调用下一个切面 26 | */ 27 | public boolean next(FileStorage fileStorage) { 28 | if (aspectIterator.hasNext()) { // 还有下一个 29 | return aspectIterator.next().isSupportSameMoveAround(this, fileStorage); 30 | } else { 31 | return callback.run(fileStorage); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/ExistsAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.FileInfo; 7 | import org.dromara.x.file.storage.core.platform.FileStorage; 8 | 9 | /** 10 | * 文件是否存在的切面调用链 11 | */ 12 | @Getter 13 | @Setter 14 | public class ExistsAspectChain { 15 | 16 | private ExistsAspectChainCallback callback; 17 | private Iterator aspectIterator; 18 | 19 | public ExistsAspectChain(Iterable aspects, ExistsAspectChainCallback callback) { 20 | this.aspectIterator = aspects.iterator(); 21 | this.callback = callback; 22 | } 23 | 24 | /** 25 | * 调用下一个切面 26 | */ 27 | public boolean next(FileInfo fileInfo, FileStorage fileStorage) { 28 | if (aspectIterator.hasNext()) { // 还有下一个 29 | return aspectIterator.next().existsAround(this, fileInfo, fileStorage); 30 | } else { 31 | return callback.run(fileInfo, fileStorage); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportMetadataAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.platform.FileStorage; 7 | 8 | /** 9 | * 是否支持 Metadata 的切面调用链 10 | */ 11 | @Getter 12 | @Setter 13 | public class IsSupportMetadataAspectChain { 14 | 15 | private IsSupportMetadataAspectChainCallback callback; 16 | private Iterator aspectIterator; 17 | 18 | public IsSupportMetadataAspectChain( 19 | Iterable aspects, IsSupportMetadataAspectChainCallback callback) { 20 | this.aspectIterator = aspects.iterator(); 21 | this.callback = callback; 22 | } 23 | 24 | /** 25 | * 调用下一个切面 26 | */ 27 | public boolean next(FileStorage fileStorage) { 28 | if (aspectIterator.hasNext()) { // 还有下一个 29 | return aspectIterator.next().isSupportMetadataAround(this, fileStorage); 30 | } else { 31 | return callback.run(fileStorage); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/脱离SpringBoot单独使用.md: -------------------------------------------------------------------------------- 1 | # 脱离 SpringBoot 单独使用 2 | 3 | 从 `1.0.0` 版本开始支持脱离 `SpringBoot` 单独使用 4 | 5 | 先引入本项目,注意这里是 `x-file-storage-core`,之后再参考 [快速入门](快速入门) 引入对应平台的依赖 6 | 7 | ```xml 8 | 9 | org.dromara.x-file-storage 10 | x-file-storage-core 11 | 2.3.0 12 | 13 | ``` 14 | 15 | 最后手动初始化即可 16 | 17 | ```java 18 | //配置文件定义存储平台 19 | FileStorageProperties properties = new FileStorageProperties(); 20 | properties.setDefaultPlatform("ftp-1"); 21 | FtpConfig ftp = new FtpConfig(); 22 | ftp.setPlatform("ftp-1"); 23 | ftp.setHost("192.168.3.100"); 24 | ftp.setPort(2121); 25 | ftp.setUser("root"); 26 | ftp.setPassword("123456"); 27 | ftp.setDomain("ftp://192.168.3.100:2121/"); 28 | ftp.setBasePath("ftp/"); 29 | ftp.setStoragePath("/"); 30 | properties.setFtp(Collections.singletonList(ftp)); 31 | 32 | //创建,自定义存储平台、 Client 工厂、切面等功能都有对应的添加方法 33 | FileStorageService service = FileStorageServiceBuilder.create(properties).useDefault().build(); 34 | 35 | //初始化完毕,开始上传吧 36 | FileInfo fileInfo = service.of(new File("D:\\Desktop\\a.png")).upload(); 37 | System.out.println(fileInfo); 38 | ``` 39 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/recorder/FileRecorder.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.recorder; 2 | 3 | import org.dromara.x.file.storage.core.FileInfo; 4 | import org.dromara.x.file.storage.core.upload.FilePartInfo; 5 | 6 | /** 7 | * 文件记录记录者接口,参考文档:https://x-file-storage.xuyanwu.cn/2.3.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 8 | */ 9 | public interface FileRecorder { 10 | 11 | /** 12 | * 保存文件记录 13 | */ 14 | boolean save(FileInfo fileInfo); 15 | 16 | /** 17 | * 更新文件记录,可以根据文件 ID 或 URL 来更新文件记录, 18 | * 主要用在手动分片上传文件-完成上传,作用是更新文件信息 19 | */ 20 | void update(FileInfo fileInfo); 21 | 22 | /** 23 | * 根据 url 获取文件记录 24 | */ 25 | FileInfo getByUrl(String url); 26 | 27 | /** 28 | * 根据 url 删除文件记录 29 | */ 30 | boolean delete(String url); 31 | 32 | /** 33 | * 保存文件分片信息 34 | * @param filePartInfo 文件分片信息 35 | */ 36 | void saveFilePart(FilePartInfo filePartInfo); 37 | 38 | /** 39 | * 删除文件分片信息 40 | */ 41 | void deleteFilePartByUploadId(String uploadId); 42 | } 43 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportPresignedUrlAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.platform.FileStorage; 7 | 8 | /** 9 | * 是否支持对文件生成可以签名访问的 URL 的切面调用链 10 | */ 11 | @Getter 12 | @Setter 13 | public class IsSupportPresignedUrlAspectChain { 14 | 15 | private IsSupportPresignedUrlAspectChainCallback callback; 16 | private Iterator aspectIterator; 17 | 18 | public IsSupportPresignedUrlAspectChain( 19 | Iterable aspects, IsSupportPresignedUrlAspectChainCallback callback) { 20 | this.aspectIterator = aspects.iterator(); 21 | this.callback = callback; 22 | } 23 | 24 | /** 25 | * 调用下一个切面 26 | */ 27 | public boolean next(FileStorage fileStorage) { 28 | if (aspectIterator.hasNext()) { // 还有下一个 29 | return aspectIterator.next().isSupportPresignedUrlAround(this, fileStorage); 30 | } else { 31 | return callback.run(fileStorage); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/SetFileAclAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.FileInfo; 7 | import org.dromara.x.file.storage.core.platform.FileStorage; 8 | 9 | /** 10 | * 获取文件的访问控制列表的切面调用链 11 | */ 12 | @Getter 13 | @Setter 14 | public class SetFileAclAspectChain { 15 | 16 | private SetFileAclAspectChainCallback callback; 17 | private Iterator aspectIterator; 18 | 19 | public SetFileAclAspectChain(Iterable aspects, SetFileAclAspectChainCallback callback) { 20 | this.aspectIterator = aspects.iterator(); 21 | this.callback = callback; 22 | } 23 | 24 | /** 25 | * 调用下一个切面 26 | */ 27 | public boolean next(FileInfo fileInfo, Object acl, FileStorage fileStorage) { 28 | if (aspectIterator.hasNext()) { // 还有下一个 29 | return aspectIterator.next().setFileAcl(this, fileInfo, acl, fileStorage); 30 | } else { 31 | return callback.run(fileInfo, acl, fileStorage); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/presigned/GeneratePresignedUrlResult.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.presigned; 2 | 3 | import java.util.Map; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.experimental.Accessors; 7 | 8 | /** 9 | * 生成预签名 URL 结果 10 | */ 11 | @Data 12 | @NoArgsConstructor 13 | @Accessors(chain = true) 14 | public class GeneratePresignedUrlResult { 15 | /** 16 | * 存储平台 17 | */ 18 | private String platform; 19 | /** 20 | * 基础存储路径 21 | */ 22 | private String basePath; 23 | /** 24 | * 存储路径 25 | */ 26 | private String path; 27 | /** 28 | * 文件名称 29 | */ 30 | private String filename; 31 | /** 32 | * URL 33 | */ 34 | private String url; 35 | /** 36 | * 访问 URL 时需要附带的请求头 37 | */ 38 | private Map headers; 39 | 40 | public GeneratePresignedUrlResult(String platform, String basePath, GeneratePresignedUrlPretreatment pre) { 41 | this.platform = platform; 42 | this.basePath = basePath; 43 | this.path = pre.getPath(); 44 | this.filename = pre.getFilename(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/SetThFileAclAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.FileInfo; 7 | import org.dromara.x.file.storage.core.platform.FileStorage; 8 | 9 | /** 10 | * 获取缩略图文件的访问控制列表的切面调用链 11 | */ 12 | @Getter 13 | @Setter 14 | public class SetThFileAclAspectChain { 15 | 16 | private SetThFileAclAspectChainCallback callback; 17 | private Iterator aspectIterator; 18 | 19 | public SetThFileAclAspectChain(Iterable aspects, SetThFileAclAspectChainCallback callback) { 20 | this.aspectIterator = aspects.iterator(); 21 | this.callback = callback; 22 | } 23 | 24 | /** 25 | * 调用下一个切面 26 | */ 27 | public boolean next(FileInfo fileInfo, Object acl, FileStorage fileStorage) { 28 | if (aspectIterator.hasNext()) { // 还有下一个 29 | return aspectIterator.next().setThFileAcl(this, fileInfo, acl, fileStorage); 30 | } else { 31 | return callback.run(fileInfo, acl, fileStorage); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/file/LocalFileWrapper.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.file; 2 | 3 | import java.io.BufferedInputStream; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.nio.file.Files; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.Setter; 11 | 12 | /** 13 | * 本地文件包装类 14 | */ 15 | @Getter 16 | @Setter 17 | @NoArgsConstructor 18 | public class LocalFileWrapper implements FileWrapper { 19 | private File file; 20 | private String name; 21 | private String contentType; 22 | private InputStream inputStream; 23 | private Long size; 24 | 25 | public LocalFileWrapper(File file, String name, String contentType, Long size) { 26 | this.file = file; 27 | this.name = name; 28 | this.contentType = contentType; 29 | this.size = size; 30 | } 31 | 32 | @Override 33 | public InputStream getInputStream() throws IOException { 34 | if (inputStream == null) { 35 | inputStream = new BufferedInputStream(Files.newInputStream(file.toPath())); 36 | } 37 | return inputStream; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportListFilesAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.get.ListFilesSupportInfo; 7 | import org.dromara.x.file.storage.core.platform.FileStorage; 8 | 9 | /** 10 | * 是否支持手动分片上传的切面调用链 11 | */ 12 | @Getter 13 | @Setter 14 | public class IsSupportListFilesAspectChain { 15 | 16 | private IsSupportListFilesChainCallback callback; 17 | private Iterator aspectIterator; 18 | 19 | public IsSupportListFilesAspectChain( 20 | Iterable aspects, IsSupportListFilesChainCallback callback) { 21 | this.aspectIterator = aspects.iterator(); 22 | this.callback = callback; 23 | } 24 | 25 | /** 26 | * 调用下一个切面 27 | */ 28 | public ListFilesSupportInfo next(FileStorage fileStorage) { 29 | if (aspectIterator.hasNext()) { // 还有下一个 30 | return aspectIterator.next().isSupportListFiles(this, fileStorage); 31 | } else { 32 | return callback.run(fileStorage); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitee/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: 功能建议 2 | description: 对本项目提出一个功能建议 3 | title: "[功能建议]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 感谢提出功能建议,我们将仔细考虑! 10 | - type: textarea 11 | id: related-problem 12 | attributes: 13 | label: 你的功能建议是否和某个问题相关? 14 | description: 清晰并简洁地描述问题是什么,例如,当我...时,我总是感到困扰。 15 | validations: 16 | required: false 17 | - type: textarea 18 | id: desired-solution 19 | attributes: 20 | label: 你希望看到什么解决方案? 21 | description: 清晰并简洁地描述你希望发生的事情。 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: alternatives 26 | attributes: 27 | label: 你考虑过哪些替代方案? 28 | description: 清晰并简洁地描述你考虑过的任何替代解决方案或功能。 29 | validations: 30 | required: false 31 | - type: textarea 32 | id: additional-context 33 | attributes: 34 | label: 你有其他上下文或截图吗? 35 | description: 在此处添加有关功能请求的任何其他上下文或截图。 36 | validations: 37 | required: false 38 | - type: checkboxes 39 | attributes: 40 | label: 意向参与贡献 41 | options: 42 | - label: 我有意向参与具体功能的开发实现并将代码贡献回到上游社区 43 | required: false -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/tika/TikaContentTypeDetect.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.tika; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | 11 | /** 12 | * 基于 Tika 识别文件的 MIME 类型 13 | */ 14 | @Getter 15 | @Setter 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class TikaContentTypeDetect implements ContentTypeDetect { 19 | private TikaFactory tikaFactory; 20 | 21 | @Override 22 | public String detect(File file) throws IOException { 23 | return tikaFactory.getTika().detect(file); 24 | } 25 | 26 | @Override 27 | public String detect(byte[] bytes) { 28 | return tikaFactory.getTika().detect(bytes); 29 | } 30 | 31 | @Override 32 | public String detect(byte[] bytes, String filename) { 33 | return tikaFactory.getTika().detect(bytes, filename); 34 | } 35 | 36 | @Override 37 | public String detect(InputStream in, String filename) throws IOException { 38 | return tikaFactory.getTika().detect(in, filename); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/GetFileAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.get.GetFilePretreatment; 7 | import org.dromara.x.file.storage.core.get.RemoteFileInfo; 8 | import org.dromara.x.file.storage.core.platform.FileStorage; 9 | 10 | /** 11 | * 获取文件的切面调用链 12 | */ 13 | @Getter 14 | @Setter 15 | public class GetFileAspectChain { 16 | 17 | private GetFileAspectChainCallback callback; 18 | private Iterator aspectIterator; 19 | 20 | public GetFileAspectChain(Iterable aspects, GetFileAspectChainCallback callback) { 21 | this.aspectIterator = aspects.iterator(); 22 | this.callback = callback; 23 | } 24 | 25 | /** 26 | * 调用下一个切面 27 | */ 28 | public RemoteFileInfo next(GetFilePretreatment pre, FileStorage fileStorage) { 29 | if (aspectIterator.hasNext()) { // 还有下一个 30 | return aspectIterator.next().getFile(this, pre, fileStorage); 31 | } else { 32 | return callback.run(pre, fileStorage); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/ListFilesAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.get.ListFilesPretreatment; 7 | import org.dromara.x.file.storage.core.get.ListFilesResult; 8 | import org.dromara.x.file.storage.core.platform.FileStorage; 9 | 10 | /** 11 | * 列举文件的切面调用链 12 | */ 13 | @Getter 14 | @Setter 15 | public class ListFilesAspectChain { 16 | 17 | private ListFilesAspectChainCallback callback; 18 | private Iterator aspectIterator; 19 | 20 | public ListFilesAspectChain(Iterable aspects, ListFilesAspectChainCallback callback) { 21 | this.aspectIterator = aspects.iterator(); 22 | this.callback = callback; 23 | } 24 | 25 | /** 26 | * 调用下一个切面 27 | */ 28 | public ListFilesResult next(ListFilesPretreatment pre, FileStorage fileStorage) { 29 | if (aspectIterator.hasNext()) { // 还有下一个 30 | return aspectIterator.next().listFiles(this, pre, fileStorage); 31 | } else { 32 | return callback.run(pre, fileStorage); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/get/ListFilesResult.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.get; 2 | 3 | import java.util.List; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.experimental.Accessors; 7 | 8 | /** 9 | * 文件信息列举结果 10 | */ 11 | @Data 12 | @NoArgsConstructor 13 | @Accessors(chain = true) 14 | public class ListFilesResult { 15 | /** 16 | * 目录列表 17 | */ 18 | private List dirList; 19 | /** 20 | * 文件列表 21 | */ 22 | private List fileList; 23 | /** 24 | * 存储平台名称 25 | */ 26 | private String platform; 27 | /** 28 | * 基础存储路径 29 | */ 30 | private String basePath; 31 | /** 32 | * 路径,需要与上传时传入的路径保持一致 33 | */ 34 | private String path = ""; 35 | /** 36 | * 文件名前缀 37 | */ 38 | private String filenamePrefix = ""; 39 | /** 40 | * 本次列举的最大文件数量 41 | */ 42 | private Integer maxFiles; 43 | /** 44 | * 列表是否被截断,就是当前目录下还有其它文件超出最大文件数量未被列举 45 | */ 46 | private Boolean isTruncated; 47 | /** 48 | * 本次列举的起始位置 49 | */ 50 | private String marker; 51 | /** 52 | * 下次列举的起始位置 53 | */ 54 | private String nextMarker; 55 | } 56 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/DownloadAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.io.InputStream; 4 | import java.util.Iterator; 5 | import java.util.function.Consumer; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import org.dromara.x.file.storage.core.FileInfo; 9 | import org.dromara.x.file.storage.core.platform.FileStorage; 10 | 11 | /** 12 | * 下载的切面调用链 13 | */ 14 | @Getter 15 | @Setter 16 | public class DownloadAspectChain { 17 | 18 | private DownloadAspectChainCallback callback; 19 | private Iterator aspectIterator; 20 | 21 | public DownloadAspectChain(Iterable aspects, DownloadAspectChainCallback callback) { 22 | this.aspectIterator = aspects.iterator(); 23 | this.callback = callback; 24 | } 25 | 26 | /** 27 | * 调用下一个切面 28 | */ 29 | public void next(FileInfo fileInfo, FileStorage fileStorage, Consumer consumer) { 30 | if (aspectIterator.hasNext()) { // 还有下一个 31 | aspectIterator.next().downloadAround(this, fileInfo, fileStorage, consumer); 32 | } else { 33 | callback.run(fileInfo, fileStorage, consumer); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/IsSupportMultipartUploadAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.platform.FileStorage; 7 | import org.dromara.x.file.storage.core.upload.MultipartUploadSupportInfo; 8 | 9 | /** 10 | * 是否支持手动分片上传的切面调用链 11 | */ 12 | @Getter 13 | @Setter 14 | public class IsSupportMultipartUploadAspectChain { 15 | 16 | private IsSupportMultipartUploadChainCallback callback; 17 | private Iterator aspectIterator; 18 | 19 | public IsSupportMultipartUploadAspectChain( 20 | Iterable aspects, IsSupportMultipartUploadChainCallback callback) { 21 | this.aspectIterator = aspects.iterator(); 22 | this.callback = callback; 23 | } 24 | 25 | /** 26 | * 调用下一个切面 27 | */ 28 | public MultipartUploadSupportInfo next(FileStorage fileStorage) { 29 | if (aspectIterator.hasNext()) { // 还有下一个 30 | return aspectIterator.next().isSupportMultipartUpload(this, fileStorage); 31 | } else { 32 | return callback.run(fileStorage); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/ListPartsAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.platform.FileStorage; 7 | import org.dromara.x.file.storage.core.upload.FilePartInfoList; 8 | import org.dromara.x.file.storage.core.upload.ListPartsPretreatment; 9 | 10 | /** 11 | * 手动分片上传-列举已上传的分片的切面调用链 12 | */ 13 | @Getter 14 | @Setter 15 | public class ListPartsAspectChain { 16 | 17 | private ListPartsAspectChainCallback callback; 18 | private Iterator aspectIterator; 19 | 20 | public ListPartsAspectChain(Iterable aspects, ListPartsAspectChainCallback callback) { 21 | this.aspectIterator = aspects.iterator(); 22 | this.callback = callback; 23 | } 24 | 25 | /** 26 | * 调用下一个切面 27 | */ 28 | public FilePartInfoList next(ListPartsPretreatment pre, FileStorage fileStorage) { 29 | if (aspectIterator.hasNext()) { // 还有下一个 30 | return aspectIterator.next().listParts(this, pre, fileStorage); 31 | } else { 32 | return callback.run(pre, fileStorage); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/DeleteAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.FileInfo; 7 | import org.dromara.x.file.storage.core.platform.FileStorage; 8 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 9 | 10 | /** 11 | * 删除的切面调用链 12 | */ 13 | @Getter 14 | @Setter 15 | public class DeleteAspectChain { 16 | 17 | private DeleteAspectChainCallback callback; 18 | private Iterator aspectIterator; 19 | 20 | public DeleteAspectChain(Iterable aspects, DeleteAspectChainCallback callback) { 21 | this.aspectIterator = aspects.iterator(); 22 | this.callback = callback; 23 | } 24 | 25 | /** 26 | * 调用下一个切面 27 | */ 28 | public boolean next(FileInfo fileInfo, FileStorage fileStorage, FileRecorder fileRecorder) { 29 | if (aspectIterator.hasNext()) { // 还有下一个 30 | return aspectIterator.next().deleteAround(this, fileInfo, fileStorage, fileRecorder); 31 | } else { 32 | return callback.run(fileInfo, fileStorage, fileRecorder); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/DownloadThAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.io.InputStream; 4 | import java.util.Iterator; 5 | import java.util.function.Consumer; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import org.dromara.x.file.storage.core.FileInfo; 9 | import org.dromara.x.file.storage.core.platform.FileStorage; 10 | 11 | /** 12 | * 下载缩略图的切面调用链 13 | */ 14 | @Getter 15 | @Setter 16 | public class DownloadThAspectChain { 17 | 18 | private DownloadThAspectChainCallback callback; 19 | private Iterator aspectIterator; 20 | 21 | public DownloadThAspectChain(Iterable aspects, DownloadThAspectChainCallback callback) { 22 | this.aspectIterator = aspects.iterator(); 23 | this.callback = callback; 24 | } 25 | 26 | /** 27 | * 调用下一个切面 28 | */ 29 | public void next(FileInfo fileInfo, FileStorage fileStorage, Consumer consumer) { 30 | if (aspectIterator.hasNext()) { // 还有下一个 31 | aspectIterator.next().downloadThAround(this, fileInfo, fileStorage, consumer); 32 | } else { 33 | callback.run(fileInfo, fileStorage, consumer); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/recorder/DefaultFileRecorder.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.recorder; 2 | 3 | import org.dromara.x.file.storage.core.FileInfo; 4 | import org.dromara.x.file.storage.core.exception.FileStorageRuntimeException; 5 | import org.dromara.x.file.storage.core.upload.FilePartInfo; 6 | 7 | /** 8 | * 默认的文件记录者类,此类并不能真正保存、查询、删除记录,只是用来脱离数据库运行,保证文件上传功能可以正常使用 9 | */ 10 | public class DefaultFileRecorder implements FileRecorder { 11 | @Override 12 | public boolean save(FileInfo fileInfo) { 13 | return true; 14 | } 15 | 16 | public void update(FileInfo fileInfo) {} 17 | 18 | @Override 19 | public FileInfo getByUrl(String url) { 20 | throw new FileStorageRuntimeException( 21 | "尚未实现 FileRecorder 接口,暂时无法使用此功能,可以参考文档快速入门的其它操作章节,或者参考保存上传记录章节:https://x-file-storage.xuyanwu.cn/2.3.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"); 22 | } 23 | 24 | @Override 25 | public boolean delete(String url) { 26 | return true; 27 | } 28 | 29 | @Override 30 | public void saveFilePart(FilePartInfo filePartInfo) {} 31 | 32 | @Override 33 | public void deleteFilePartByUploadId(String uploadId) {} 34 | } 35 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-general-test/src/main/java/org/dromara/x/file/storage/test/mapper/xml/FilePartDetailMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | id, platform, upload_id, e_tag, part_number, part_size, hash_info, create_time 19 | 20 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/GenerateThPresignedUrlAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Date; 4 | import java.util.Iterator; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import org.dromara.x.file.storage.core.FileInfo; 8 | import org.dromara.x.file.storage.core.platform.FileStorage; 9 | 10 | /** 11 | * 对缩略图文件生成可以签名访问的 URL 的切面调用链 12 | */ 13 | @Getter 14 | @Setter 15 | public class GenerateThPresignedUrlAspectChain { 16 | 17 | private GenerateThPresignedUrlAspectChainCallback callback; 18 | private Iterator aspectIterator; 19 | 20 | public GenerateThPresignedUrlAspectChain( 21 | Iterable aspects, GenerateThPresignedUrlAspectChainCallback callback) { 22 | this.aspectIterator = aspects.iterator(); 23 | this.callback = callback; 24 | } 25 | 26 | /** 27 | * 调用下一个切面 28 | */ 29 | public String next(FileInfo fileInfo, Date expiration, FileStorage fileStorage) { 30 | if (aspectIterator.hasNext()) { // 还有下一个 31 | return aspectIterator.next().generateThPresignedUrlAround(this, fileInfo, expiration, fileStorage); 32 | } else { 33 | return callback.run(fileInfo, expiration, fileStorage); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/CopyAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.FileInfo; 7 | import org.dromara.x.file.storage.core.copy.CopyPretreatment; 8 | import org.dromara.x.file.storage.core.platform.FileStorage; 9 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 10 | 11 | /** 12 | * 复制的切面调用链 13 | */ 14 | @Getter 15 | @Setter 16 | public class CopyAspectChain { 17 | 18 | private CopyAspectChainCallback callback; 19 | private Iterator aspectIterator; 20 | 21 | public CopyAspectChain(Iterable aspects, CopyAspectChainCallback callback) { 22 | this.aspectIterator = aspects.iterator(); 23 | this.callback = callback; 24 | } 25 | 26 | /** 27 | * 调用下一个切面 28 | */ 29 | public FileInfo next( 30 | FileInfo srcFileInfo, CopyPretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder) { 31 | if (aspectIterator.hasNext()) { // 还有下一个 32 | return aspectIterator.next().copyAround(this, srcFileInfo, pre, fileStorage, fileRecorder); 33 | } else { 34 | return callback.run(srcFileInfo, pre, fileStorage, fileRecorder); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/MoveAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.FileInfo; 7 | import org.dromara.x.file.storage.core.move.MovePretreatment; 8 | import org.dromara.x.file.storage.core.platform.FileStorage; 9 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 10 | 11 | /** 12 | * 移动的切面调用链 13 | */ 14 | @Getter 15 | @Setter 16 | public class MoveAspectChain { 17 | 18 | private MoveAspectChainCallback callback; 19 | private Iterator aspectIterator; 20 | 21 | public MoveAspectChain(Iterable aspects, MoveAspectChainCallback callback) { 22 | this.aspectIterator = aspects.iterator(); 23 | this.callback = callback; 24 | } 25 | 26 | /** 27 | * 调用下一个切面 28 | */ 29 | public FileInfo next( 30 | FileInfo srcFileInfo, MovePretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder) { 31 | if (aspectIterator.hasNext()) { // 还有下一个 32 | return aspectIterator.next().moveAround(this, srcFileInfo, pre, fileStorage, fileRecorder); 33 | } else { 34 | return callback.run(srcFileInfo, pre, fileStorage, fileRecorder); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/UploadAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.FileInfo; 7 | import org.dromara.x.file.storage.core.UploadPretreatment; 8 | import org.dromara.x.file.storage.core.platform.FileStorage; 9 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 10 | 11 | /** 12 | * 上传的切面调用链 13 | */ 14 | @Getter 15 | @Setter 16 | public class UploadAspectChain { 17 | 18 | private UploadAspectChainCallback callback; 19 | private Iterator aspectIterator; 20 | 21 | public UploadAspectChain(Iterable aspects, UploadAspectChainCallback callback) { 22 | this.aspectIterator = aspects.iterator(); 23 | this.callback = callback; 24 | } 25 | 26 | /** 27 | * 调用下一个切面 28 | */ 29 | public FileInfo next( 30 | FileInfo fileInfo, UploadPretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder) { 31 | if (aspectIterator.hasNext()) { // 还有下一个 32 | return aspectIterator.next().uploadAround(this, fileInfo, pre, fileStorage, fileRecorder); 33 | } else { 34 | return callback.run(fileInfo, pre, fileStorage, fileRecorder); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/UploadPartAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.platform.FileStorage; 7 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 8 | import org.dromara.x.file.storage.core.upload.FilePartInfo; 9 | import org.dromara.x.file.storage.core.upload.UploadPartPretreatment; 10 | 11 | /** 12 | * 手动分片上传-上传分片的切面调用链 13 | */ 14 | @Getter 15 | @Setter 16 | public class UploadPartAspectChain { 17 | 18 | private UploadPartAspectChainCallback callback; 19 | private Iterator aspectIterator; 20 | 21 | public UploadPartAspectChain(Iterable aspects, UploadPartAspectChainCallback callback) { 22 | this.aspectIterator = aspects.iterator(); 23 | this.callback = callback; 24 | } 25 | 26 | /** 27 | * 调用下一个切面 28 | */ 29 | public FilePartInfo next(UploadPartPretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder) { 30 | if (aspectIterator.hasNext()) { // 还有下一个 31 | return aspectIterator.next().uploadPart(this, pre, fileStorage, fileRecorder); 32 | } else { 33 | return callback.run(pre, fileStorage, fileRecorder); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/file/ByteFileWrapperAdapter.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.file; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import org.dromara.x.file.storage.core.tika.ContentTypeDetect; 8 | 9 | /** 10 | * byte[] 文件包装适配器 11 | */ 12 | @Getter 13 | @Setter 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class ByteFileWrapperAdapter implements FileWrapperAdapter { 17 | private ContentTypeDetect contentTypeDetect; 18 | 19 | @Override 20 | public boolean isSupport(Object source) { 21 | return source instanceof byte[] || source instanceof ByteFileWrapper; 22 | } 23 | 24 | @Override 25 | public FileWrapper getFileWrapper(Object source, String name, String contentType, Long size) { 26 | if (source instanceof ByteFileWrapper) { 27 | return updateFileWrapper((ByteFileWrapper) source, name, contentType, size); 28 | } else { 29 | byte[] bytes = (byte[]) source; 30 | if (name == null) name = ""; 31 | if (contentType == null) contentType = contentTypeDetect.detect(bytes, name); 32 | if (size == null) size = (long) bytes.length; 33 | return new ByteFileWrapper(bytes, name, contentType, size); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /x-file-storage-spring/src/main/java/org/dromara/x/file/storage/spring/file/MultipartFileWrapperAdapter.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.spring.file; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.dromara.x.file.storage.core.file.FileWrapper; 6 | import org.dromara.x.file.storage.core.file.FileWrapperAdapter; 7 | import org.springframework.web.multipart.MultipartFile; 8 | 9 | /** 10 | * MultipartFile 文件包装适配器 11 | */ 12 | @Getter 13 | @Setter 14 | public class MultipartFileWrapperAdapter implements FileWrapperAdapter { 15 | 16 | @Override 17 | public boolean isSupport(Object source) { 18 | return source instanceof MultipartFile || source instanceof MultipartFileWrapper; 19 | } 20 | 21 | @Override 22 | public FileWrapper getFileWrapper(Object source, String name, String contentType, Long size) { 23 | if (source instanceof MultipartFileWrapper) { 24 | return updateFileWrapper((MultipartFileWrapper) source, name, contentType, size); 25 | } else { 26 | MultipartFile file = (MultipartFile) source; 27 | if (name == null) name = file.getOriginalFilename(); 28 | if (contentType == null) contentType = file.getContentType(); 29 | if (size == null) size = file.getSize(); 30 | return new MultipartFileWrapper(file, name, contentType, size); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/GeneratePresignedUrlAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.platform.FileStorage; 7 | import org.dromara.x.file.storage.core.presigned.GeneratePresignedUrlPretreatment; 8 | import org.dromara.x.file.storage.core.presigned.GeneratePresignedUrlResult; 9 | 10 | /** 11 | * 对文件生成可以签名访问的 URL 的切面调用链 12 | */ 13 | @Getter 14 | @Setter 15 | public class GeneratePresignedUrlAspectChain { 16 | 17 | private GeneratePresignedUrlAspectChainCallback callback; 18 | private Iterator aspectIterator; 19 | 20 | public GeneratePresignedUrlAspectChain( 21 | Iterable aspects, GeneratePresignedUrlAspectChainCallback callback) { 22 | this.aspectIterator = aspects.iterator(); 23 | this.callback = callback; 24 | } 25 | 26 | /** 27 | * 调用下一个切面 28 | */ 29 | public GeneratePresignedUrlResult next(GeneratePresignedUrlPretreatment pre, FileStorage fileStorage) { 30 | if (aspectIterator.hasNext()) { // 还有下一个 31 | return aspectIterator.next().generatePresignedUrlAround(this, pre, fileStorage); 32 | } else { 33 | return callback.run(pre, fileStorage); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/upload/FilePartInfo.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.upload; 2 | 3 | import java.util.Date; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.experimental.Accessors; 7 | import org.dromara.x.file.storage.core.FileInfo; 8 | import org.dromara.x.file.storage.core.hash.HashInfo; 9 | 10 | /** 11 | * 文件分片信息 12 | */ 13 | @Data 14 | @NoArgsConstructor 15 | @Accessors(chain = true) 16 | public class FilePartInfo { 17 | /** 18 | * 分片id,仅在数据库相关操作时使用 19 | */ 20 | private String id; 21 | /** 22 | * 存储平台 23 | */ 24 | private String platform; 25 | /** 26 | * 上传ID 27 | */ 28 | private String uploadId; 29 | /** 30 | * 分片 ETag(分片数据的MD5值) 31 | */ 32 | private String eTag; 33 | /** 34 | * 分片号。每一个上传的分片都有一个分片号,一般情况下取值范围是1~10000 35 | */ 36 | private Integer partNumber; 37 | /** 38 | * 分片大小,单位字节 39 | */ 40 | private Long partSize; 41 | /** 42 | * 哈希信息类,用来存储各种哈希值 43 | */ 44 | private HashInfo hashInfo; 45 | /** 46 | * 创建时间,仅在保存到数据库时使用 47 | */ 48 | private Date createTime; 49 | /** 50 | * 分片最后修改时间,仅在获取分片信息时使用 51 | */ 52 | private Date lastModified; 53 | 54 | public FilePartInfo(FileInfo fileInfo) { 55 | platform = fileInfo.getPlatform(); 56 | uploadId = fileInfo.getUploadId(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/file/LocalFileWrapperAdapter.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.file; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | import org.dromara.x.file.storage.core.tika.ContentTypeDetect; 10 | 11 | /** 12 | * 本地文件包装适配器 13 | */ 14 | @Getter 15 | @Setter 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class LocalFileWrapperAdapter implements FileWrapperAdapter { 19 | private ContentTypeDetect contentTypeDetect; 20 | 21 | @Override 22 | public boolean isSupport(Object source) { 23 | return source instanceof File || source instanceof LocalFileWrapper; 24 | } 25 | 26 | @Override 27 | public FileWrapper getFileWrapper(Object source, String name, String contentType, Long size) throws IOException { 28 | if (source instanceof LocalFileWrapper) { 29 | return updateFileWrapper((LocalFileWrapper) source, name, contentType, size); 30 | } else { 31 | File file = (File) source; 32 | if (name == null) name = file.getName(); 33 | if (contentType == null) contentType = contentTypeDetect.detect(file); 34 | if (size == null) size = file.length(); 35 | return new LocalFileWrapper(file, name, contentType, size); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/AbortMultipartUploadAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.FileInfo; 7 | import org.dromara.x.file.storage.core.platform.FileStorage; 8 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 9 | import org.dromara.x.file.storage.core.upload.AbortMultipartUploadPretreatment; 10 | 11 | /** 12 | * 手动分片上传-取消的切面调用链 13 | */ 14 | @Getter 15 | @Setter 16 | public class AbortMultipartUploadAspectChain { 17 | 18 | private AbortMultipartUploadAspectChainCallback callback; 19 | private Iterator aspectIterator; 20 | 21 | public AbortMultipartUploadAspectChain( 22 | Iterable aspects, AbortMultipartUploadAspectChainCallback callback) { 23 | this.aspectIterator = aspects.iterator(); 24 | this.callback = callback; 25 | } 26 | 27 | /** 28 | * 调用下一个切面 29 | */ 30 | public FileInfo next(AbortMultipartUploadPretreatment pre, FileStorage fileStorage, FileRecorder fileRecorder) { 31 | if (aspectIterator.hasNext()) { // 还有下一个 32 | return aspectIterator.next().abortMultipartUploadAround(this, pre, fileStorage, fileRecorder); 33 | } else { 34 | return callback.run(pre, fileStorage, fileRecorder); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/platform/UpyunUssFileStorageClientFactory.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.platform; 2 | 3 | import com.upyun.RestManager; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import org.dromara.x.file.storage.core.FileStorageProperties.UpyunUssConfig; 8 | 9 | /** 10 | * 又拍云 USS 存储平台的 Client 工厂 11 | */ 12 | @Getter 13 | @Setter 14 | @NoArgsConstructor 15 | public class UpyunUssFileStorageClientFactory implements FileStorageClientFactory { 16 | private String platform; 17 | private String username; 18 | private String password; 19 | private String bucketName; 20 | private volatile RestManager client; 21 | 22 | public UpyunUssFileStorageClientFactory(UpyunUssConfig config) { 23 | platform = config.getPlatform(); 24 | username = config.getUsername(); 25 | password = config.getPassword(); 26 | bucketName = config.getBucketName(); 27 | } 28 | 29 | @Override 30 | public RestManager getClient() { 31 | if (client == null) { 32 | synchronized (this) { 33 | if (client == null) { 34 | client = new RestManager(bucketName, username, password); 35 | } 36 | } 37 | } 38 | return client; 39 | } 40 | 41 | @Override 42 | public void close() { 43 | client = null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/hash/HashInfo.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.hash; 2 | 3 | import static org.dromara.x.file.storage.core.constant.Constant.Hash.MessageDigest.*; 4 | 5 | import cn.hutool.core.map.CaseInsensitiveLinkedMap; 6 | import java.util.Map; 7 | import lombok.NoArgsConstructor; 8 | 9 | /** 10 | * 哈希信息类,用来存储各种哈希值,例如 MD5、SHA1、SHA256等,详情{@link org.dromara.x.file.storage.core.constant.Constant.Hash} 11 | */ 12 | @NoArgsConstructor 13 | public class HashInfo extends CaseInsensitiveLinkedMap { 14 | 15 | /** 16 | * 构造方法 17 | */ 18 | public HashInfo(Map map) { 19 | super(map); 20 | } 21 | 22 | /** 23 | * 获取 MD2 24 | */ 25 | public String getMd2() { 26 | return get(MD2); 27 | } 28 | 29 | /** 30 | * 获取 MD5 31 | */ 32 | public String getMd5() { 33 | return get(MD5); 34 | } 35 | 36 | /** 37 | * 获取 SHA1 38 | */ 39 | public String getSha1() { 40 | return get(SHA1); 41 | } 42 | 43 | /** 44 | * 获取 SHA256 45 | */ 46 | public String getSha256() { 47 | return get(SHA256); 48 | } 49 | 50 | /** 51 | * 获取 SHA384 52 | */ 53 | public String getSha384() { 54 | return get(SHA384); 55 | } 56 | 57 | /** 58 | * 获取 SHA512 59 | */ 60 | public String getSha512() { 61 | return get(SHA512); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/platform/HuaweiObsFileStorageClientFactory.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.platform; 2 | 3 | import cn.hutool.core.io.IoUtil; 4 | import com.obs.services.ObsClient; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import org.dromara.x.file.storage.core.FileStorageProperties.HuaweiObsConfig; 9 | 10 | /** 11 | * 华为云 ObsClient 存储平台的 Client 工厂 12 | */ 13 | @Getter 14 | @Setter 15 | @NoArgsConstructor 16 | public class HuaweiObsFileStorageClientFactory implements FileStorageClientFactory { 17 | private String platform; 18 | private String accessKey; 19 | private String secretKey; 20 | private String endPoint; 21 | private volatile ObsClient client; 22 | 23 | public HuaweiObsFileStorageClientFactory(HuaweiObsConfig config) { 24 | platform = config.getPlatform(); 25 | accessKey = config.getAccessKey(); 26 | secretKey = config.getSecretKey(); 27 | endPoint = config.getEndPoint(); 28 | } 29 | 30 | @Override 31 | public ObsClient getClient() { 32 | if (client == null) { 33 | synchronized (this) { 34 | if (client == null) { 35 | client = new ObsClient(accessKey, secretKey, endPoint); 36 | } 37 | } 38 | } 39 | return client; 40 | } 41 | 42 | @Override 43 | public void close() { 44 | IoUtil.close(client); 45 | client = null; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/upload/MultipartUploadSupportInfo.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.upload; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.experimental.Accessors; 7 | 8 | /** 9 | * 手动分片上传支持信息 10 | */ 11 | @Data 12 | @Accessors(chain = true) 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class MultipartUploadSupportInfo { 16 | /** 17 | * 是否支持手动分片上传,正常情况下判断此参数就行了 18 | */ 19 | private Boolean isSupport; 20 | 21 | /** 22 | * 是否支持列举已上传的分片,又拍云 USS 不支持,建议将上传完成的分片信息通过 FileRecorder 接口保存到数据库, 23 | * 详情:https://x-file-storage.xuyanwu.cn/2.3.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 24 | */ 25 | private Boolean isSupportListParts; 26 | 27 | /** 28 | * 是否支持取消上传, 29 | * 又拍云 USS 不支持手动取消,未完成上传的文件信息及分片默认 24 小时后自动删除 30 | */ 31 | private Boolean isSupportAbort; 32 | 33 | /** 34 | * 手动分片上传-列举已上传的分片-每次获取的最大分片数,对象存储一般是 1000 35 | */ 36 | private Integer listPartsSupportMaxParts; 37 | 38 | /** 39 | * 不支持手动分片上传 40 | */ 41 | public static MultipartUploadSupportInfo notSupport() { 42 | return new MultipartUploadSupportInfo(false, false, false, null); 43 | } 44 | 45 | /** 46 | * 支持全部的手动分片上传功能 47 | */ 48 | public static MultipartUploadSupportInfo supportAll() { 49 | return new MultipartUploadSupportInfo(true, true, true, 1000); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /x-file-storage-spring/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.dromara.x-file-storage 6 | x-file-storage-parent 7 | ${revision} 8 | 9 | 10 | x-file-storage-spring 11 | X File Storage Spring 12 | 13 | 14 | 15 | org.dromara.x-file-storage 16 | x-file-storage-core 17 | 18 | 19 | org.projectlombok 20 | lombok 21 | provided 22 | true 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | provided 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-configuration-processor 32 | provided 33 | true 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/SameCopyAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.FileInfo; 7 | import org.dromara.x.file.storage.core.copy.CopyPretreatment; 8 | import org.dromara.x.file.storage.core.platform.FileStorage; 9 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 10 | 11 | /** 12 | * 同存储平台复制的切面调用链 13 | */ 14 | @Getter 15 | @Setter 16 | public class SameCopyAspectChain { 17 | 18 | private SameCopyAspectChainCallback callback; 19 | private Iterator aspectIterator; 20 | 21 | public SameCopyAspectChain(Iterable aspects, SameCopyAspectChainCallback callback) { 22 | this.aspectIterator = aspects.iterator(); 23 | this.callback = callback; 24 | } 25 | 26 | /** 27 | * 调用下一个切面 28 | */ 29 | public FileInfo next( 30 | FileInfo srcFileInfo, 31 | FileInfo destFileInfo, 32 | CopyPretreatment pre, 33 | FileStorage fileStorage, 34 | FileRecorder fileRecorder) { 35 | if (aspectIterator.hasNext()) { // 还有下一个 36 | return aspectIterator 37 | .next() 38 | .sameCopyAround(this, srcFileInfo, destFileInfo, pre, fileStorage, fileRecorder); 39 | } else { 40 | return callback.run(srcFileInfo, destFileInfo, pre, fileStorage, fileRecorder); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/SameMoveAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.FileInfo; 7 | import org.dromara.x.file.storage.core.move.MovePretreatment; 8 | import org.dromara.x.file.storage.core.platform.FileStorage; 9 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 10 | 11 | /** 12 | * 同存储平台移动的切面调用链 13 | */ 14 | @Getter 15 | @Setter 16 | public class SameMoveAspectChain { 17 | 18 | private SameMoveAspectChainCallback callback; 19 | private Iterator aspectIterator; 20 | 21 | public SameMoveAspectChain(Iterable aspects, SameMoveAspectChainCallback callback) { 22 | this.aspectIterator = aspects.iterator(); 23 | this.callback = callback; 24 | } 25 | 26 | /** 27 | * 调用下一个切面 28 | */ 29 | public FileInfo next( 30 | FileInfo srcFileInfo, 31 | FileInfo destFileInfo, 32 | MovePretreatment pre, 33 | FileStorage fileStorage, 34 | FileRecorder fileRecorder) { 35 | if (aspectIterator.hasNext()) { // 还有下一个 36 | return aspectIterator 37 | .next() 38 | .sameMoveAround(this, srcFileInfo, destFileInfo, pre, fileStorage, fileRecorder); 39 | } else { 40 | return callback.run(srcFileInfo, destFileInfo, pre, fileStorage, fileRecorder); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/upload/AbortMultipartUploadPretreatment.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.upload; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.experimental.Accessors; 6 | import org.dromara.x.file.storage.core.FileInfo; 7 | import org.dromara.x.file.storage.core.FileStorageService; 8 | 9 | /** 10 | * 手动分片上传-取消预处理器 11 | */ 12 | @Getter 13 | @Setter 14 | @Accessors(chain = true) 15 | public class AbortMultipartUploadPretreatment { 16 | /** 17 | * 文件存储服务类 18 | */ 19 | private FileStorageService fileStorageService; 20 | /** 21 | * 文件信息 22 | */ 23 | private FileInfo fileInfo; 24 | 25 | /** 26 | * 如果条件为 true 则:设置文件存储服务类 27 | * @param flag 条件 28 | * @param fileStorageService 文件存储服务类 29 | * @return 手动分片上传-取消预处理器 30 | */ 31 | public AbortMultipartUploadPretreatment setFileStorageService(boolean flag, FileStorageService fileStorageService) { 32 | if (flag) setFileStorageService(fileStorageService); 33 | return this; 34 | } 35 | 36 | /** 37 | * 如果条件为 true 则:设置文件信息 38 | * @param flag 条件 39 | * @param fileInfo 文件信息 40 | * @return 手动分片上传-取消预处理器 41 | */ 42 | public AbortMultipartUploadPretreatment setFileInfo(boolean flag, FileInfo fileInfo) { 43 | if (flag) setFileInfo(fileInfo); 44 | return this; 45 | } 46 | 47 | /** 48 | * 执行取消 49 | */ 50 | public FileInfo abort() { 51 | return new AbortMultipartUploadActuator(this).execute(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/platform/MinioFileStorageClientFactory.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.platform; 2 | 3 | import io.minio.MinioClient; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import org.dromara.x.file.storage.core.FileStorageProperties.MinioConfig; 8 | 9 | /** 10 | * MinIO 存储平台的 Client 工厂 11 | */ 12 | @Getter 13 | @Setter 14 | @NoArgsConstructor 15 | public class MinioFileStorageClientFactory implements FileStorageClientFactory { 16 | private String platform; 17 | private String accessKey; 18 | private String secretKey; 19 | private String endPoint; 20 | private volatile MinioClient client; 21 | 22 | public MinioFileStorageClientFactory(MinioConfig config) { 23 | platform = config.getPlatform(); 24 | accessKey = config.getAccessKey(); 25 | secretKey = config.getSecretKey(); 26 | endPoint = config.getEndPoint(); 27 | } 28 | 29 | @Override 30 | public MinioClient getClient() { 31 | if (client == null) { 32 | synchronized (this) { 33 | if (client == null) { 34 | client = new MinioClient.Builder() 35 | .credentials(accessKey, secretKey) 36 | .endpoint(endPoint) 37 | .build(); 38 | } 39 | } 40 | } 41 | return client; 42 | } 43 | 44 | @Override 45 | public void close() { 46 | client = null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /x-file-storage-solon/src/main/java/org/dromara/x/file/storage/solon/file/UploadedFileWrapperAdapter.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.solon.file; 2 | 3 | import java.io.IOException; 4 | import java.util.Optional; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import org.dromara.x.file.storage.core.file.FileWrapper; 8 | import org.dromara.x.file.storage.core.file.FileWrapperAdapter; 9 | import org.noear.solon.core.handle.UploadedFile; 10 | 11 | /** 12 | * Solon UploadedFile 文件包装适配器 13 | * 14 | * @author link2fun 15 | */ 16 | @Getter 17 | @Setter 18 | public class UploadedFileWrapperAdapter implements FileWrapperAdapter { 19 | 20 | @Override 21 | public boolean isSupport(final Object source) { 22 | return source instanceof UploadedFile || source instanceof UploadedFileWrapper; 23 | } 24 | 25 | @Override 26 | public FileWrapper getFileWrapper(final Object source, final String name, final String contentType, final Long size) 27 | throws IOException { 28 | if (source instanceof UploadedFileWrapper) { 29 | return updateFileWrapper((UploadedFileWrapper) source, name, contentType, size); 30 | } else { 31 | UploadedFile file = (UploadedFile) source; 32 | return new UploadedFileWrapper( 33 | file, 34 | Optional.ofNullable(name).orElseGet(file::getName), 35 | Optional.ofNullable(contentType).orElseGet(file::getContentType), 36 | Optional.ofNullable(size).orElseGet(file::getContentSize)); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/platform/AliyunOssFileStorageClientFactory.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.platform; 2 | 3 | import com.aliyun.oss.OSS; 4 | import com.aliyun.oss.OSSClientBuilder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import org.dromara.x.file.storage.core.FileStorageProperties.AliyunOssConfig; 9 | 10 | /** 11 | * 阿里云 OSS 存储平台的 Client 工厂 12 | */ 13 | @Getter 14 | @Setter 15 | @NoArgsConstructor 16 | public class AliyunOssFileStorageClientFactory implements FileStorageClientFactory { 17 | private String platform; 18 | private String accessKey; 19 | private String secretKey; 20 | private String endPoint; 21 | private volatile OSS client; 22 | 23 | public AliyunOssFileStorageClientFactory(AliyunOssConfig config) { 24 | platform = config.getPlatform(); 25 | accessKey = config.getAccessKey(); 26 | secretKey = config.getSecretKey(); 27 | endPoint = config.getEndPoint(); 28 | } 29 | 30 | @Override 31 | public OSS getClient() { 32 | if (client == null) { 33 | synchronized (this) { 34 | if (client == null) { 35 | client = new OSSClientBuilder().build(endPoint, accessKey, secretKey); 36 | } 37 | } 38 | } 39 | return client; 40 | } 41 | 42 | @Override 43 | public void close() { 44 | if (client != null) { 45 | client.shutdown(); 46 | client = null; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/InitiateMultipartUploadAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.FileInfo; 7 | import org.dromara.x.file.storage.core.platform.FileStorage; 8 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 9 | import org.dromara.x.file.storage.core.upload.InitiateMultipartUploadPretreatment; 10 | 11 | /** 12 | * 手动分片上传-初始化的切面调用链 13 | */ 14 | @Getter 15 | @Setter 16 | public class InitiateMultipartUploadAspectChain { 17 | 18 | private InitiateMultipartUploadAspectChainCallback callback; 19 | private Iterator aspectIterator; 20 | 21 | public InitiateMultipartUploadAspectChain( 22 | Iterable aspects, InitiateMultipartUploadAspectChainCallback callback) { 23 | this.aspectIterator = aspects.iterator(); 24 | this.callback = callback; 25 | } 26 | 27 | /** 28 | * 调用下一个切面 29 | */ 30 | public FileInfo next( 31 | FileInfo fileInfo, 32 | InitiateMultipartUploadPretreatment pre, 33 | FileStorage fileStorage, 34 | FileRecorder fileRecorder) { 35 | if (aspectIterator.hasNext()) { // 还有下一个 36 | return aspectIterator.next().initiateMultipartUploadAround(this, fileInfo, pre, fileStorage, fileRecorder); 37 | } else { 38 | return callback.run(fileInfo, pre, fileStorage, fileRecorder); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-fastdfs-test/docker/nginx.conf: -------------------------------------------------------------------------------- 1 | #user nobody; 2 | worker_processes 1; 3 | 4 | #error_log logs/error.log; 5 | #error_log logs/error.log notice; 6 | #error_log logs/error.log info; 7 | 8 | #pid logs/nginx.pid; 9 | 10 | 11 | events { 12 | worker_connections 1024; 13 | } 14 | 15 | 16 | http { 17 | include mime.types; 18 | default_type application/octet-stream; 19 | 20 | #access_log logs/access.log main; 21 | 22 | sendfile on; 23 | #tcp_nopush on; 24 | 25 | #keepalive_timeout 0; 26 | keepalive_timeout 65; 27 | 28 | #gzip on; 29 | 30 | server { 31 | listen 8088; 32 | server_name localhost; 33 | 34 | #charset koi8-r; 35 | 36 | #缩略图需要使用插件,需要单独构建nginx镜像,此处忽略 37 | #location /group([0-9])/M00/.*\.(gif|jpg|jpeg|png)$ { 38 | # root /fastdfs/storage/data; 39 | # image on; 40 | # image_output off; 41 | # image_jpeg_quality 75; 42 | # image_backend off; 43 | # image_backend_server http://baidu.com/xxx.png; 44 | # } 45 | 46 | # group1 47 | location /group1/M00 { 48 | # 文件存储目录 49 | root /fastdfs/storage/data; 50 | ngx_fastdfs_module; 51 | } 52 | 53 | #error_page 404 /404.html; 54 | 55 | # redirect server error pages to the static page /50x.html 56 | # 57 | error_page 500 502 503 504 /50x.html; 58 | location = /50x.html { 59 | root html; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/file/HttpServletRequestFileWrapper.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.file; 2 | 3 | import cn.hutool.core.io.IoUtil; 4 | import java.io.InputStream; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import org.dromara.x.file.storage.core.file.MultipartFormDataReader.MultipartFormData; 9 | 10 | /** 11 | * JavaxHttpServletRequest 文件包装类 12 | */ 13 | @Getter 14 | @Setter 15 | @NoArgsConstructor 16 | public class HttpServletRequestFileWrapper implements FileWrapper { 17 | private String name; 18 | private String contentType; 19 | private InputStream inputStream; 20 | private Long size; 21 | private MultipartFormData multipartFormData; 22 | 23 | public HttpServletRequestFileWrapper( 24 | InputStream inputStream, String name, String contentType, Long size, MultipartFormData multipartFormData) { 25 | this.name = name; 26 | this.contentType = contentType; 27 | this.inputStream = IoUtil.toMarkSupportStream(inputStream); 28 | this.size = size; 29 | this.multipartFormData = multipartFormData; 30 | } 31 | 32 | @Override 33 | public InputStream getInputStream() { 34 | return inputStream; 35 | } 36 | 37 | /** 38 | * 获取参数值 39 | */ 40 | public String getParameter(String name) { 41 | return multipartFormData.getParameter(name); 42 | } 43 | 44 | /** 45 | * 获取多个参数值 46 | */ 47 | public String[] getParameterValues(String name) { 48 | return multipartFormData.getParameterValues(name); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/file/InputStreamFileWrapperAdapter.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.file; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.dromara.x.file.storage.core.tika.ContentTypeDetect; 11 | 12 | /** 13 | * InputStream 文件包装适配器 14 | */ 15 | @Slf4j 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class InputStreamFileWrapperAdapter implements FileWrapperAdapter { 21 | private ContentTypeDetect contentTypeDetect; 22 | 23 | @Override 24 | public boolean isSupport(Object source) { 25 | return source instanceof InputStream || source instanceof InputStreamFileWrapper; 26 | } 27 | 28 | @Override 29 | public FileWrapper getFileWrapper(Object source, String name, String contentType, Long size) throws IOException { 30 | if (source instanceof InputStreamFileWrapper) { 31 | return updateFileWrapper((InputStreamFileWrapper) source, name, contentType, size); 32 | } else { 33 | InputStream inputStream = (InputStream) source; 34 | if (name == null) name = ""; 35 | InputStreamFileWrapper wrapper = new InputStreamFileWrapper(inputStream, name, contentType, size); 36 | if (contentType == null) { 37 | wrapper.getInputStreamMaskReset( 38 | in -> wrapper.setContentType(contentTypeDetect.detect(in, wrapper.getName()))); 39 | } 40 | return wrapper; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /x-file-storage-solon/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.dromara.x-file-storage 6 | x-file-storage-parent 7 | ${revision} 8 | 9 | 10 | x-file-storage-solon 11 | X File Storage Solon 12 | 13 | 14 | UTF-8 15 | 16 | 17 | 18 | 19 | org.dromara.x-file-storage 20 | x-file-storage-core 21 | 22 | 23 | org.projectlombok 24 | lombok 25 | provided 26 | true 27 | 28 | 29 | 30 | org.noear 31 | solon 32 | ${solon.version} 33 | provided 34 | 35 | 36 | 37 | org.noear 38 | solon.web.staticfiles 39 | ${solon.version} 40 | provided 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/aspect/CompleteMultipartUploadAspectChain.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.aspect; 2 | 3 | import java.util.Iterator; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.dromara.x.file.storage.core.FileInfo; 7 | import org.dromara.x.file.storage.core.platform.FileStorage; 8 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 9 | import org.dromara.x.file.storage.core.tika.ContentTypeDetect; 10 | import org.dromara.x.file.storage.core.upload.CompleteMultipartUploadPretreatment; 11 | 12 | /** 13 | * 手动分片上传-完成的切面调用链 14 | */ 15 | @Getter 16 | @Setter 17 | public class CompleteMultipartUploadAspectChain { 18 | 19 | private CompleteMultipartUploadAspectChainCallback callback; 20 | private Iterator aspectIterator; 21 | 22 | public CompleteMultipartUploadAspectChain( 23 | Iterable aspects, CompleteMultipartUploadAspectChainCallback callback) { 24 | this.aspectIterator = aspects.iterator(); 25 | this.callback = callback; 26 | } 27 | 28 | /** 29 | * 调用下一个切面 30 | */ 31 | public FileInfo next( 32 | CompleteMultipartUploadPretreatment pre, 33 | FileStorage fileStorage, 34 | FileRecorder fileRecorder, 35 | ContentTypeDetect contentTypeDetect) { 36 | if (aspectIterator.hasNext()) { // 还有下一个 37 | return aspectIterator 38 | .next() 39 | .completeMultipartUploadAround(this, pre, fileStorage, fileRecorder, contentTypeDetect); 40 | } else { 41 | return callback.run(pre, fileStorage, fileRecorder, contentTypeDetect); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/platform/VolcengineTosFileStorageClientFactory.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.platform; 2 | 3 | import com.volcengine.tos.TOSV2; 4 | import com.volcengine.tos.TOSV2ClientBuilder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import org.dromara.x.file.storage.core.FileStorageProperties.VolcengineTosConfig; 9 | 10 | /** 11 | * 火山引擎 TOS 存储平台的 Client 工厂 12 | */ 13 | @Getter 14 | @Setter 15 | @NoArgsConstructor 16 | public class VolcengineTosFileStorageClientFactory implements FileStorageClientFactory { 17 | private String platform; 18 | private String accessKey; 19 | private String secretKey; 20 | private String endPoint; 21 | private String region; 22 | private volatile TOSV2 client; 23 | 24 | public VolcengineTosFileStorageClientFactory(VolcengineTosConfig config) { 25 | platform = config.getPlatform(); 26 | accessKey = config.getAccessKey(); 27 | secretKey = config.getSecretKey(); 28 | endPoint = config.getEndPoint(); 29 | region = config.getRegion(); 30 | } 31 | 32 | @Override 33 | public TOSV2 getClient() { 34 | if (client == null) { 35 | synchronized (this) { 36 | if (client == null) { 37 | client = new TOSV2ClientBuilder().build(region, endPoint, accessKey, secretKey); 38 | } 39 | } 40 | } 41 | return client; 42 | } 43 | 44 | @Override 45 | public void close() { 46 | if (client != null) { 47 | try { 48 | client = null; 49 | } catch (Exception ignored) { 50 | // 忽略关闭异常 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/get/GetFileActuator.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.get; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import org.dromara.x.file.storage.core.FileStorageService; 6 | import org.dromara.x.file.storage.core.aspect.FileStorageAspect; 7 | import org.dromara.x.file.storage.core.aspect.GetFileAspectChain; 8 | import org.dromara.x.file.storage.core.exception.Check; 9 | import org.dromara.x.file.storage.core.platform.FileStorage; 10 | 11 | /** 12 | * 获取文件执行器 13 | */ 14 | public class GetFileActuator { 15 | private final FileStorageService fileStorageService; 16 | private final GetFilePretreatment pre; 17 | 18 | public GetFileActuator(GetFilePretreatment pre) { 19 | this.pre = pre; 20 | this.fileStorageService = pre.getFileStorageService(); 21 | } 22 | 23 | /** 24 | * 执行获取文件 25 | */ 26 | public RemoteFileInfo execute() { 27 | return execute(fileStorageService.getFileStorageVerify(pre.getPlatform()), fileStorageService.getAspectList()); 28 | } 29 | 30 | /** 31 | * 执行获取文件 32 | */ 33 | public RemoteFileInfo execute(FileStorage fileStorage, List aspectList) { 34 | Check.getFile(pre); 35 | return new GetFileAspectChain(aspectList, (_pre, _fileStorage) -> { 36 | RemoteFileInfo info = _fileStorage.getFile(_pre); 37 | if (info != null) { 38 | if (info.getMetadata() == null) info.setMetadata(new HashMap<>()); 39 | if (info.getUserMetadata() == null) info.setUserMetadata(new HashMap<>()); 40 | } 41 | return info; 42 | }) 43 | .next(pre, fileStorage); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/presigned/GeneratePresignedUrlActuator.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.presigned; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import org.dromara.x.file.storage.core.FileStorageService; 6 | import org.dromara.x.file.storage.core.aspect.FileStorageAspect; 7 | import org.dromara.x.file.storage.core.aspect.GeneratePresignedUrlAspectChain; 8 | import org.dromara.x.file.storage.core.exception.Check; 9 | import org.dromara.x.file.storage.core.platform.FileStorage; 10 | 11 | /** 12 | * 生成预签名 URL 执行器 13 | */ 14 | public class GeneratePresignedUrlActuator { 15 | private final FileStorageService fileStorageService; 16 | private final GeneratePresignedUrlPretreatment pre; 17 | 18 | public GeneratePresignedUrlActuator(GeneratePresignedUrlPretreatment pre) { 19 | this.pre = pre; 20 | this.fileStorageService = pre.getFileStorageService(); 21 | } 22 | 23 | /** 24 | * 执行生成预签名 URL 25 | */ 26 | public GeneratePresignedUrlResult execute() { 27 | return execute(fileStorageService.getFileStorageVerify(pre.getPlatform()), fileStorageService.getAspectList()); 28 | } 29 | 30 | /** 31 | * 执行生成预签名 URL 32 | */ 33 | public GeneratePresignedUrlResult execute(FileStorage fileStorage, List aspectList) { 34 | Check.generatePresignedUrl(pre); 35 | return new GeneratePresignedUrlAspectChain(aspectList, (_pre, _fileStorage) -> { 36 | GeneratePresignedUrlResult result = _fileStorage.generatePresignedUrl(_pre); 37 | if (result.getHeaders() == null) result.setHeaders(new HashMap<>()); 38 | return result; 39 | }) 40 | .next(pre, fileStorage); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-general-test/src/test/java/org/dromara/x/file/storage/test/DirectUseFileStorageTest.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.test; 2 | 3 | import java.io.File; 4 | import java.io.InputStream; 5 | import java.util.Collections; 6 | import org.dromara.x.file.storage.core.FileInfo; 7 | import org.dromara.x.file.storage.core.FileStorageProperties; 8 | import org.dromara.x.file.storage.core.FileStorageProperties.FtpConfig; 9 | import org.dromara.x.file.storage.core.FileStorageService; 10 | import org.dromara.x.file.storage.core.FileStorageServiceBuilder; 11 | import org.junit.jupiter.api.Test; 12 | 13 | public class DirectUseFileStorageTest { 14 | @Test 15 | public void upload() { 16 | 17 | // 配置文件定义存储平台 18 | FileStorageProperties properties = new FileStorageProperties(); 19 | properties.setDefaultPlatform("ftp-1"); 20 | FtpConfig ftp = new FtpConfig(); 21 | ftp.setPlatform("ftp-1"); 22 | ftp.setHost("192.168.3.100"); 23 | ftp.setPort(2121); 24 | ftp.setUser("root"); 25 | ftp.setPassword("123456"); 26 | ftp.setDomain("ftp://192.168.3.100:2121/"); 27 | ftp.setBasePath("ftp/"); 28 | ftp.setStoragePath("/"); 29 | properties.setFtp(Collections.singletonList(ftp)); 30 | 31 | // 创建,自定义存储平台、Client工厂、切面等功能都有对应的添加方法 32 | FileStorageService service = 33 | FileStorageServiceBuilder.create(properties).useDefault().build(); 34 | 35 | // 初始化完毕,开始上传吧 36 | FileInfo fileInfo = service.of(new File("D:\\Desktop\\a.png")).upload(); 37 | System.out.println(fileInfo); 38 | 39 | String filename = "image.jpg"; 40 | InputStream in = this.getClass().getClassLoader().getResourceAsStream(filename); 41 | FileInfo fileInfo2 = service.of(in).setOriginalFilename(filename).upload(); 42 | System.out.println(fileInfo2); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.gitee/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: 问题反馈 2 | description: 在使用时遇到一些问题 3 | title: "[问题反馈]: " 4 | labels: ["question"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 感谢对项目的支持与关注。在提出问题之前,请确保你已查看相关开发或使用文档: 10 | - [官方文档](https://x-file-storage.xuyanwu.cn/) 11 | - [常见问题](https://x-file-storage.xuyanwu.cn/#/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98) 12 | - type: checkboxes 13 | attributes: 14 | label: 这个问题是否已经存在? 15 | options: 16 | - label: 我已经搜索过现有的问题 (https://gitee.com/../../issues) 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: 相关依赖及版本号 21 | description: 请详细输入用到的相关依赖及版本号 22 | value: | 23 | X File Storage 版本号: 24 | SpringBoot 版本号: 25 | Solon 版本号: 26 | JVM 版本号: 27 | 其它依赖的名称及版本号: 28 | validations: 29 | required: true 30 | - type: textarea 31 | attributes: 32 | label: 配置文件 33 | description: 输入你的配置文件,IP、密钥等使用 xxx 代替 34 | value: | 35 | ```yaml 36 | # 在此输入配置文件 37 | 38 | ``` 39 | validations: 40 | required: true 41 | - type: textarea 42 | attributes: 43 | label: 复现代码 44 | description: 请详细告诉我们如何复现你遇到的问题,如涉及代码,可提供一个最小代码示例,并使用反引号```附上它 45 | placeholder: | 46 | 1. ... 47 | 2. ... 48 | 3. ... 49 | validations: 50 | required: true 51 | - type: textarea 52 | attributes: 53 | label: 预期结果 54 | description: 请告诉我们你预期会发生什么。 55 | validations: 56 | required: true 57 | - type: textarea 58 | attributes: 59 | label: 实际结果或详细的报错信息 60 | description: 请告诉我们实际发生了什么。 61 | validations: 62 | required: true 63 | - type: textarea 64 | attributes: 65 | label: 截图或视频 66 | description: 如果可以的话,上传任何关于 bug 的截图。 67 | value: | 68 | [在这里上传图片] -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/platform/WebDavFileStorageClientFactory.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.platform; 2 | 3 | import cn.hutool.core.util.URLUtil; 4 | import com.github.sardine.Sardine; 5 | import com.github.sardine.SardineFactory; 6 | import java.io.IOException; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import org.dromara.x.file.storage.core.FileStorageProperties.WebDavConfig; 11 | import org.dromara.x.file.storage.core.exception.FileStorageRuntimeException; 12 | 13 | /** 14 | * WebDAV 存储平台的 Client 工厂 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | public class WebDavFileStorageClientFactory implements FileStorageClientFactory { 20 | private String platform; 21 | private String server; 22 | private String user; 23 | private String password; 24 | private volatile Sardine client; 25 | 26 | public WebDavFileStorageClientFactory(WebDavConfig config) { 27 | platform = config.getPlatform(); 28 | server = config.getServer(); 29 | user = config.getUser(); 30 | password = config.getPassword(); 31 | } 32 | 33 | @Override 34 | public Sardine getClient() { 35 | if (client == null) { 36 | synchronized (this) { 37 | if (client == null) { 38 | client = SardineFactory.begin(user, password); 39 | client.enablePreemptiveAuthentication(URLUtil.url(server)); 40 | } 41 | } 42 | } 43 | return client; 44 | } 45 | 46 | @Override 47 | public void close() { 48 | if (client != null) { 49 | try { 50 | client.shutdown(); 51 | } catch (IOException e) { 52 | throw new FileStorageRuntimeException("关闭 WebDAV Client 失败!", e); 53 | } 54 | client = null; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/upload/UploadPartActuator.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.upload; 2 | 3 | import java.util.concurrent.CopyOnWriteArrayList; 4 | import org.dromara.x.file.storage.core.FileInfo; 5 | import org.dromara.x.file.storage.core.FileStorageService; 6 | import org.dromara.x.file.storage.core.aspect.FileStorageAspect; 7 | import org.dromara.x.file.storage.core.aspect.UploadPartAspectChain; 8 | import org.dromara.x.file.storage.core.exception.Check; 9 | import org.dromara.x.file.storage.core.platform.FileStorage; 10 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 11 | 12 | /** 13 | * 手动分片上传-上传分片执行器 14 | */ 15 | public class UploadPartActuator { 16 | private final FileStorageService fileStorageService; 17 | private final UploadPartPretreatment pre; 18 | 19 | public UploadPartActuator(UploadPartPretreatment pre) { 20 | this.pre = pre; 21 | this.fileStorageService = pre.getFileStorageService(); 22 | } 23 | 24 | /** 25 | * 执行上传 26 | */ 27 | public FilePartInfo execute() { 28 | FileInfo fileInfo = pre.getFileInfo(); 29 | Check.uploadPart(fileInfo); 30 | 31 | FileStorage fileStorage = fileStorageService.getFileStorageVerify(fileInfo.getPlatform()); 32 | CopyOnWriteArrayList aspectList = fileStorageService.getAspectList(); 33 | FileRecorder fileRecorder = fileStorageService.getFileRecorder(); 34 | 35 | return new UploadPartAspectChain(aspectList, (_pre, _fileStorage, _fileRecorder) -> { 36 | FilePartInfo filePartInfo = _fileStorage.uploadPart(_pre); 37 | filePartInfo.setHashInfo(_pre.getHashCalculatorManager().getHashInfo()); 38 | _fileRecorder.saveFilePart(filePartInfo); 39 | return filePartInfo; 40 | }) 41 | .next(pre, fileStorage, fileRecorder); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/ProgressInputStream.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core; 2 | 3 | import java.io.FilterInputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | 7 | /** 8 | * 带进度通知的 InputStream 包装类,将在后续版本中删除,请使用 InputStreamPlus 代替此类 9 | */ 10 | @Deprecated 11 | public class ProgressInputStream extends FilterInputStream { 12 | 13 | private boolean readFlag; 14 | private boolean finishFlag; 15 | private long progressSize; 16 | private final Long allSize; 17 | private final ProgressListener listener; 18 | 19 | public ProgressInputStream(InputStream in, ProgressListener listener, Long allSize) { 20 | super(in); 21 | this.listener = listener; 22 | this.allSize = allSize; 23 | } 24 | 25 | @Override 26 | public long skip(long n) throws IOException { 27 | long skip = super.skip(n); 28 | progress(skip); 29 | return skip; 30 | } 31 | 32 | @Override 33 | public int read() throws IOException { 34 | int b = super.read(); 35 | progress(b == -1 ? -1 : 1); 36 | return b; 37 | } 38 | 39 | @Override 40 | public int read(byte[] b, int off, int len) throws IOException { 41 | if (!this.readFlag) { 42 | this.readFlag = true; 43 | this.listener.start(); 44 | } 45 | int bytes = super.read(b, off, len); 46 | progress(bytes); 47 | return bytes; 48 | } 49 | 50 | @Override 51 | public boolean markSupported() { 52 | return false; 53 | } 54 | 55 | protected void progress(long size) { 56 | if (size > 0) { 57 | this.listener.progress(progressSize += size, allSize); 58 | } else if (size < 0) { 59 | if (!this.finishFlag) { 60 | this.finishFlag = true; 61 | this.listener.finish(); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/platform/TencentCosFileStorageClientFactory.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.platform; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.qcloud.cos.COSClient; 5 | import com.qcloud.cos.ClientConfig; 6 | import com.qcloud.cos.auth.BasicCOSCredentials; 7 | import com.qcloud.cos.region.Region; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.Setter; 11 | import org.dromara.x.file.storage.core.FileStorageProperties.TencentCosConfig; 12 | 13 | /** 14 | * 腾讯云 COS 存储平台的 Client 工厂 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | public class TencentCosFileStorageClientFactory implements FileStorageClientFactory { 20 | private String platform; 21 | private String secretId; 22 | private String secretKey; 23 | private String region; 24 | private volatile COSClient client; 25 | 26 | public TencentCosFileStorageClientFactory(TencentCosConfig config) { 27 | platform = config.getPlatform(); 28 | secretId = config.getSecretId(); 29 | secretKey = config.getSecretKey(); 30 | region = config.getRegion(); 31 | } 32 | 33 | @Override 34 | public COSClient getClient() { 35 | if (client == null) { 36 | synchronized (this) { 37 | if (client == null) { 38 | ClientConfig clientConfig = new ClientConfig(); 39 | if (StrUtil.isNotBlank(region)) { 40 | clientConfig.setRegion(new Region(region)); 41 | } 42 | client = new COSClient(new BasicCOSCredentials(secretId, secretKey), clientConfig); 43 | } 44 | } 45 | } 46 | return client; 47 | } 48 | 49 | @Override 50 | public void close() { 51 | if (client != null) { 52 | client.shutdown(); 53 | client = null; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/ProgressListener.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core; 2 | 3 | import java.util.function.LongSupplier; 4 | 5 | /** 6 | * 进度监听器 7 | */ 8 | public interface ProgressListener { 9 | 10 | /** 11 | * 开始 12 | */ 13 | void start(); 14 | 15 | /** 16 | * 进行中 17 | * 18 | * @param progressSize 已经进行的大小 19 | * @param allSize 总大小,来自 fileInfo.getSize(),未知大小的流可能会导致此参数为 null 20 | */ 21 | void progress(long progressSize, Long allSize); 22 | 23 | /** 24 | * 结束 25 | */ 26 | void finish(); 27 | 28 | /** 29 | * 快速触发开始 30 | */ 31 | static void quickStart(ProgressListener progressListener, Long size) { 32 | if (progressListener == null) return; 33 | progressListener.start(); 34 | progressListener.progress(0, size); 35 | } 36 | 37 | /** 38 | * 快速触发进行中 39 | */ 40 | static void quickProgress(ProgressListener progressListener, long progressSize, Long size) { 41 | if (progressListener == null) return; 42 | progressListener.progress(progressSize, size); 43 | } 44 | 45 | /** 46 | * 快速触发结束 47 | */ 48 | static void quickFinish(ProgressListener progressListener, Long size, LongSupplier progressSizeSupplier) { 49 | if (progressListener == null) return; 50 | progressListener.progress(progressSizeSupplier.getAsLong(), size); 51 | progressListener.finish(); 52 | } 53 | 54 | /** 55 | * 快速触发结束 56 | */ 57 | static void quickFinish(ProgressListener progressListener, Long size) { 58 | if (progressListener == null) return; 59 | progressListener.progress(size, size); 60 | progressListener.finish(); 61 | } 62 | 63 | /** 64 | * 快速触发结束 65 | */ 66 | static void quickFinish(ProgressListener progressListener) { 67 | if (progressListener == null) return; 68 | progressListener.finish(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-general-test/src/test/java/org/dromara/x/file/storage/test/HttpServletRequestFileTest.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.test; 2 | 3 | import cn.hutool.core.io.FileUtil; 4 | import cn.hutool.core.lang.Assert; 5 | import cn.hutool.http.HttpUtil; 6 | import java.io.File; 7 | import java.util.LinkedHashMap; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | 12 | /** 13 | * 对支持直接读取 HttpServletRequest 的流进行上传的功能进行测试 14 | */ 15 | @Slf4j 16 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) 17 | class HttpServletRequestFileTest { 18 | private final File file; 19 | private final File thfile; 20 | 21 | public HttpServletRequestFileTest() { 22 | file = new File(System.getProperty("java.io.tmpdir"), "image.jpg"); 23 | if (!file.exists()) { 24 | FileUtil.writeFromStream(this.getClass().getClassLoader().getResourceAsStream("image.jpg"), file); 25 | } 26 | thfile = new File(System.getProperty("java.io.tmpdir"), "image2.jpg"); 27 | if (!thfile.exists()) { 28 | FileUtil.writeFromStream(this.getClass().getClassLoader().getResourceAsStream("image2.jpg"), thfile); 29 | } 30 | } 31 | 32 | /** 33 | * 单独对文件上传进行测试 34 | */ 35 | @Test 36 | public void upload() { 37 | 38 | LinkedHashMap map = new LinkedHashMap<>(); 39 | map.put("aaa", "111"); 40 | map.put("bbb", "222"); 41 | map.put("ccc", ""); 42 | map.put("ddd", null); 43 | // map.put("_fileSize",file.length()); 44 | map.put("_hasTh", "true"); 45 | map.put("thfile", thfile); 46 | map.put("file", file); 47 | String res = HttpUtil.post("http://localhost:8030/upload-request", map); 48 | System.out.println("文件上传结果:" + res); 49 | Assert.isTrue(res.startsWith("{") && res.contains("url"), "文件上传失败!"); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /x-file-storage-solon/src/main/java/org/dromara/x/file/storage/solon/file/UploadedFileWrapper.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.solon.file; 2 | 3 | import cn.hutool.core.io.FileUtil; 4 | import cn.hutool.core.io.IoUtil; 5 | import java.io.BufferedInputStream; 6 | import java.io.File; 7 | import java.io.InputStream; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.Setter; 11 | import org.dromara.x.file.storage.core.exception.FileStorageRuntimeException; 12 | import org.dromara.x.file.storage.core.file.FileWrapper; 13 | import org.noear.solon.core.handle.UploadedFile; 14 | 15 | /** 16 | * Solon UploadedFile 文件包装类 17 | * 18 | * @author link2fun 19 | */ 20 | @Getter 21 | @Setter 22 | @NoArgsConstructor 23 | public class UploadedFileWrapper implements FileWrapper { 24 | 25 | private UploadedFile file; 26 | private String name; 27 | private String contentType; 28 | private InputStream inputStream; 29 | private Long size; 30 | 31 | public UploadedFileWrapper(UploadedFile file, String name, String contentType, Long size) { 32 | this.file = file; 33 | this.name = name; 34 | this.contentType = contentType; 35 | this.size = size; 36 | } 37 | 38 | @Override 39 | public InputStream getInputStream() { 40 | if (inputStream == null) { 41 | inputStream = new BufferedInputStream(file.getContent()); 42 | } 43 | return inputStream; 44 | } 45 | 46 | @Override 47 | public void transferTo(final File dest) { 48 | try { 49 | file.transferTo(dest); 50 | IoUtil.close(inputStream); 51 | } catch (Exception ignored) { 52 | try { 53 | FileUtil.writeFromStream(getInputStream(), dest); 54 | } catch (Exception e) { 55 | throw new FileStorageRuntimeException("文件移动失败", e); 56 | } 57 | } 58 | } 59 | 60 | @Override 61 | public boolean supportTransfer() { 62 | return true; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/upload/AbortMultipartUploadActuator.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.upload; 2 | 3 | import java.util.concurrent.CopyOnWriteArrayList; 4 | import org.dromara.x.file.storage.core.FileInfo; 5 | import org.dromara.x.file.storage.core.FileStorageService; 6 | import org.dromara.x.file.storage.core.aspect.AbortMultipartUploadAspectChain; 7 | import org.dromara.x.file.storage.core.aspect.FileStorageAspect; 8 | import org.dromara.x.file.storage.core.exception.Check; 9 | import org.dromara.x.file.storage.core.platform.FileStorage; 10 | import org.dromara.x.file.storage.core.recorder.FileRecorder; 11 | 12 | /** 13 | * 手动分片上传-取消执行器 14 | */ 15 | public class AbortMultipartUploadActuator { 16 | private final FileStorageService fileStorageService; 17 | private final AbortMultipartUploadPretreatment pre; 18 | 19 | public AbortMultipartUploadActuator(AbortMultipartUploadPretreatment pre) { 20 | this.pre = pre; 21 | this.fileStorageService = pre.getFileStorageService(); 22 | } 23 | 24 | /** 25 | * 执行取消 26 | */ 27 | public FileInfo execute() { 28 | FileInfo fileInfo = pre.getFileInfo(); 29 | Check.abortMultipartUpload(fileInfo); 30 | 31 | FileStorage fileStorage = fileStorageService.getFileStorageVerify(fileInfo.getPlatform()); 32 | CopyOnWriteArrayList aspectList = fileStorageService.getAspectList(); 33 | FileRecorder fileRecorder = fileStorageService.getFileRecorder(); 34 | 35 | return new AbortMultipartUploadAspectChain(aspectList, (_pre, _fileStorage, _fileRecorder) -> { 36 | FileInfo _fileInfo = _pre.getFileInfo(); 37 | _fileStorage.abortMultipartUpload(_pre); 38 | _fileRecorder.deleteFilePartByUploadId(_fileInfo.getUploadId()); 39 | _fileRecorder.delete(_fileInfo.getUrl()); 40 | return _fileInfo; 41 | }) 42 | .next(pre, fileStorage, fileRecorder); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/hash/HashCalculatorManager.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.hash; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import java.util.List; 5 | import java.util.concurrent.CopyOnWriteArrayList; 6 | import lombok.Getter; 7 | import org.dromara.x.file.storage.core.exception.FileStorageRuntimeException; 8 | 9 | /** 10 | * 哈希计算器管理器 11 | */ 12 | public class HashCalculatorManager implements HashCalculatorSetter { 13 | @Getter 14 | private final List hashCalculatorList = new CopyOnWriteArrayList<>(); 15 | 16 | private volatile HashInfo hashInfo; 17 | 18 | /** 19 | * 添加一个哈希计算器 20 | * @param hashCalculator 哈希计算器 21 | * @return 哈希计算器管理器 22 | */ 23 | public HashCalculatorManager setHashCalculator(HashCalculator hashCalculator) { 24 | hashCalculatorList.add(hashCalculator); 25 | return this; 26 | } 27 | 28 | /** 29 | * 增量计算哈希 30 | * @param bytes 字节数组 31 | * @return 哈希计算器管理器 32 | */ 33 | public HashCalculatorManager update(byte[] bytes) { 34 | if (hashInfo != null) { 35 | throw new FileStorageRuntimeException( 36 | StrUtil.format("当前 HashCalculatorManager 已调用 getHashInfo() 方法获取了哈希信息,无法再次进行增量计算")); 37 | } 38 | for (HashCalculator hashCalculator : hashCalculatorList) { 39 | hashCalculator.update(bytes); 40 | } 41 | return this; 42 | } 43 | 44 | /** 45 | * 获取哈希信息,注意:此方法一旦调用过后,将无法再次增量计算哈希 46 | * @return 哈希信息 47 | */ 48 | public HashInfo getHashInfo() { 49 | if (hashInfo == null) { 50 | synchronized (this) { 51 | if (hashInfo == null) { 52 | hashInfo = new HashInfo(); 53 | for (HashCalculator hashCalculator : hashCalculatorList) { 54 | hashInfo.put(hashCalculator.getName(), hashCalculator.getValue()); 55 | } 56 | } 57 | } 58 | } 59 | return hashInfo; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/platform/BaiduBosFileStorageClientFactory.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.platform; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.baidubce.Protocol; 5 | import com.baidubce.auth.DefaultBceCredentials; 6 | import com.baidubce.services.bos.BosClient; 7 | import com.baidubce.services.bos.BosClientConfiguration; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.Setter; 11 | import org.dromara.x.file.storage.core.FileStorageProperties.BaiduBosConfig; 12 | 13 | /** 14 | * 百度云 BOS 存储平台的 Client 工厂 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | public class BaiduBosFileStorageClientFactory implements FileStorageClientFactory { 20 | private String platform; 21 | private String accessKey; 22 | private String secretKey; 23 | private String endPoint; 24 | private volatile BosClient client; 25 | 26 | public BaiduBosFileStorageClientFactory(BaiduBosConfig config) { 27 | platform = config.getPlatform(); 28 | accessKey = config.getAccessKey(); 29 | secretKey = config.getSecretKey(); 30 | endPoint = config.getEndPoint(); 31 | } 32 | 33 | @Override 34 | public BosClient getClient() { 35 | if (client == null) { 36 | synchronized (this) { 37 | if (client == null) { 38 | BosClientConfiguration configuration = new BosClientConfiguration(); 39 | configuration.setProtocol(Protocol.HTTPS); 40 | configuration.setCredentials(new DefaultBceCredentials(accessKey, secretKey)); 41 | if (StrUtil.isNotBlank(endPoint)) { 42 | configuration.setEndpoint(endPoint); 43 | } 44 | client = new BosClient(configuration); 45 | } 46 | } 47 | } 48 | return client; 49 | } 50 | 51 | @Override 52 | public void close() { 53 | if (client != null) { 54 | client.shutdown(); 55 | client = null; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-general-test/src/main/java/org/dromara/x/file/storage/test/model/FilePartDetail.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.test.model; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import java.util.Date; 8 | import lombok.Data; 9 | 10 | /** 11 | * 文件分片信息表,仅在手动分片上传时使用 12 | */ 13 | @Data 14 | @TableName(value = "file_part_detail") 15 | public class FilePartDetail { 16 | /** 17 | * 分片id 18 | */ 19 | @TableId(value = "id", type = IdType.ASSIGN_ID) 20 | private String id; 21 | 22 | /** 23 | * 存储平台 24 | */ 25 | @TableField(value = "platform") 26 | private String platform; 27 | 28 | /** 29 | * 上传ID,仅在手动分片上传时使用 30 | */ 31 | @TableField(value = "upload_id") 32 | private String uploadId; 33 | 34 | /** 35 | * 分片 ETag 36 | */ 37 | @TableField(value = "e_tag") 38 | private String eTag; 39 | 40 | /** 41 | * 分片号。每一个上传的分片都有一个分片号,一般情况下取值范围是1~10000 42 | */ 43 | @TableField(value = "part_number") 44 | private Integer partNumber; 45 | 46 | /** 47 | * 文件大小,单位字节 48 | */ 49 | @TableField(value = "part_size") 50 | private Long partSize; 51 | 52 | /** 53 | * 哈希信息 54 | */ 55 | @TableField(value = "hash_info") 56 | private String hashInfo; 57 | 58 | /** 59 | * 创建时间 60 | */ 61 | @TableField(value = "create_time") 62 | private Date createTime; 63 | 64 | public static final String COL_ID = "id"; 65 | 66 | public static final String COL_PLATFORM = "platform"; 67 | 68 | public static final String COL_UPLOAD_ID = "upload_id"; 69 | 70 | public static final String COL_E_TAG = "e_tag"; 71 | 72 | public static final String COL_PART_NUMBER = "part_number"; 73 | 74 | public static final String COL_PART_SIZE = "part_size"; 75 | 76 | public static final String COL_HASH_INFO = "hash_info"; 77 | 78 | public static final String COL_CREATE_TIME = "create_time"; 79 | } 80 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/file/JavaxHttpServletRequestFileWrapperAdapter.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.file; 2 | 3 | import java.io.IOException; 4 | import java.nio.charset.Charset; 5 | import javax.servlet.http.HttpServletRequest; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.dromara.x.file.storage.core.file.MultipartFormDataReader.MultipartFormData; 10 | 11 | /** 12 | * 针对 javax 的 HttpServletRequest 文件包装适配器 13 | */ 14 | @Slf4j 15 | @Getter 16 | @Setter 17 | public class JavaxHttpServletRequestFileWrapperAdapter implements FileWrapperAdapter { 18 | 19 | @Override 20 | public boolean isSupport(Object source) { 21 | if (source instanceof HttpServletRequest) { 22 | HttpServletRequest request = (HttpServletRequest) source; 23 | String contentType = request.getContentType(); 24 | return contentType != null && contentType.toLowerCase().trim().startsWith("multipart/form-data"); 25 | } 26 | return source instanceof HttpServletRequestFileWrapper; 27 | } 28 | 29 | @Override 30 | public FileWrapper getFileWrapper(Object source, String name, String contentType, Long size) throws IOException { 31 | if (source instanceof HttpServletRequestFileWrapper) { 32 | return updateFileWrapper((HttpServletRequestFileWrapper) source, name, contentType, size); 33 | } else { 34 | HttpServletRequest request = (HttpServletRequest) source; 35 | Charset charset = Charset.forName(request.getCharacterEncoding()); 36 | MultipartFormData data = MultipartFormDataReader.read( 37 | request.getContentType(), request.getInputStream(), charset, request.getContentLengthLong()); 38 | 39 | if (name == null) name = data.getFileOriginalFilename(); 40 | if (contentType == null) contentType = data.getFileContentType(); 41 | if (size == null) size = data.getFileSize(); 42 | return new HttpServletRequestFileWrapper(data.getInputStream(), name, contentType, size, data); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/file/JakartaHttpServletRequestFileWrapperAdapter.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.file; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import java.io.IOException; 5 | import java.nio.charset.Charset; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.dromara.x.file.storage.core.file.MultipartFormDataReader.MultipartFormData; 10 | 11 | /** 12 | * 针对 jakarta 的 HttpServletRequest 文件包装适配器 13 | */ 14 | @Slf4j 15 | @Getter 16 | @Setter 17 | public class JakartaHttpServletRequestFileWrapperAdapter implements FileWrapperAdapter { 18 | 19 | @Override 20 | public boolean isSupport(Object source) { 21 | if (source instanceof HttpServletRequest) { 22 | HttpServletRequest request = (HttpServletRequest) source; 23 | String contentType = request.getContentType(); 24 | return contentType != null && contentType.toLowerCase().trim().startsWith("multipart/form-data"); 25 | } 26 | return source instanceof HttpServletRequestFileWrapper; 27 | } 28 | 29 | @Override 30 | public FileWrapper getFileWrapper(Object source, String name, String contentType, Long size) throws IOException { 31 | if (source instanceof HttpServletRequestFileWrapper) { 32 | return updateFileWrapper((HttpServletRequestFileWrapper) source, name, contentType, size); 33 | } else { 34 | HttpServletRequest request = (HttpServletRequest) source; 35 | Charset charset = Charset.forName(request.getCharacterEncoding()); 36 | MultipartFormData data = MultipartFormDataReader.read( 37 | request.getContentType(), request.getInputStream(), charset, request.getContentLengthLong()); 38 | 39 | if (name == null) name = data.getFileOriginalFilename(); 40 | if (contentType == null) contentType = data.getFileContentType(); 41 | if (size == null) size = data.getFileSize(); 42 | return new HttpServletRequestFileWrapper(data.getInputStream(), name, contentType, size, data); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/file/FileWrapper.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.file; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import org.dromara.x.file.storage.core.IOExceptionConsumer; 7 | import org.dromara.x.file.storage.core.IOExceptionFunction; 8 | import org.dromara.x.file.storage.core.exception.FileStorageRuntimeException; 9 | 10 | /** 11 | * 文件包装接口 12 | */ 13 | public interface FileWrapper { 14 | /** 15 | * 获取文件名称 16 | */ 17 | String getName(); 18 | 19 | /** 20 | * 设置文件名称 21 | */ 22 | void setName(String name); 23 | 24 | /** 25 | * 获取文件的 MIME 类型 26 | */ 27 | String getContentType(); 28 | 29 | /** 30 | * 设置文件的 MIME 类型 31 | */ 32 | void setContentType(String contentType); 33 | 34 | /** 35 | * 获取文件的 InputStream 36 | */ 37 | InputStream getInputStream() throws IOException; 38 | 39 | /** 40 | * 获取文件的 InputStream 并读取,会自动标记和重置流的位置 41 | */ 42 | default void getInputStreamMaskReset(IOExceptionConsumer consumer) throws IOException { 43 | getInputStreamMaskResetReturn(in -> { 44 | consumer.accept(in); 45 | return null; 46 | }); 47 | } 48 | 49 | /** 50 | * 获取文件的 InputStream 并读取,会自动标记和重置流的位置 51 | */ 52 | default R getInputStreamMaskResetReturn(IOExceptionFunction function) throws IOException { 53 | InputStream in = getInputStream(); 54 | in.mark(Integer.MAX_VALUE); 55 | try { 56 | return function.apply(in); 57 | } finally { 58 | in.reset(); 59 | } 60 | } 61 | 62 | /** 63 | * 获取文件大小 64 | */ 65 | Long getSize(); 66 | 67 | /** 68 | * 设置文件大小 69 | */ 70 | void setSize(Long size); 71 | 72 | /** 73 | * 移动文件 74 | */ 75 | default void transferTo(File dest) { 76 | throw new FileStorageRuntimeException("当前 FileWrapper 不支持 transferTo 方法"); 77 | } 78 | 79 | /** 80 | * 是否支持移动文件 81 | */ 82 | default boolean supportTransfer() { 83 | return false; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /x-file-storage-spring/src/main/java/org/dromara/x/file/storage/spring/file/MultipartFileWrapper.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.spring.file; 2 | 3 | import cn.hutool.core.io.FileUtil; 4 | import cn.hutool.core.io.IoUtil; 5 | import java.io.BufferedInputStream; 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import lombok.Setter; 12 | import org.dromara.x.file.storage.core.exception.FileStorageRuntimeException; 13 | import org.dromara.x.file.storage.core.file.FileWrapper; 14 | import org.springframework.web.multipart.MultipartFile; 15 | 16 | /** 17 | * MultipartFile 文件包装类 18 | */ 19 | @Getter 20 | @Setter 21 | @NoArgsConstructor 22 | public class MultipartFileWrapper implements FileWrapper { 23 | private MultipartFile file; 24 | private String name; 25 | private String contentType; 26 | private InputStream inputStream; 27 | private Long size; 28 | 29 | public MultipartFileWrapper(MultipartFile file, String name, String contentType, Long size) { 30 | this.file = file; 31 | this.name = name; 32 | this.contentType = contentType; 33 | this.size = size; 34 | } 35 | 36 | @Override 37 | public InputStream getInputStream() throws IOException { 38 | if (inputStream == null) { 39 | inputStream = new BufferedInputStream(file.getInputStream()); 40 | } 41 | return inputStream; 42 | } 43 | 44 | @Override 45 | public void transferTo(File dest) { 46 | // 在某些 SpringBoot 版本中,例如 2.4.6,此方法会调用失败, 47 | // 此时尝试手动将 InputStream 写入指定文件, 48 | // 根据文档来看 MultipartFile 最终都会由框架从临时目录中删除 49 | try { 50 | file.transferTo(dest); 51 | IoUtil.close(inputStream); 52 | } catch (Exception ignored) { 53 | try { 54 | FileUtil.writeFromStream(getInputStream(), dest); 55 | } catch (Exception e) { 56 | throw new FileStorageRuntimeException("文件移动失败", e); 57 | } 58 | } 59 | } 60 | 61 | @Override 62 | public boolean supportTransfer() { 63 | return true; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/hash/MessageDigestHashCalculator.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.hash; 2 | 3 | import cn.hutool.core.util.HexUtil; 4 | import java.security.MessageDigest; 5 | import org.dromara.x.file.storage.core.exception.FileStorageRuntimeException; 6 | 7 | /** 8 | * 摘要信息哈希计算器,支持计算 MD5、SHA1、SHA256等,详情{@link org.dromara.x.file.storage.core.constant.Constant.Hash.MessageDigest} 9 | */ 10 | public class MessageDigestHashCalculator implements HashCalculator { 11 | 12 | /** 13 | * 摘要信息对象 14 | */ 15 | private final MessageDigest messageDigest; 16 | /** 17 | * 哈希值 18 | */ 19 | private volatile String value; 20 | 21 | /** 22 | * 构造摘要信息哈希计算器 23 | * @param name 哈希名称,例如 MD5、SHA1、SHA256等,详情{@link org.dromara.x.file.storage.core.constant.Constant.Hash.MessageDigest} 24 | */ 25 | public MessageDigestHashCalculator(String name) { 26 | try { 27 | messageDigest = MessageDigest.getInstance(name); 28 | } catch (Exception e) { 29 | throw new FileStorageRuntimeException("创建 StandardHashCalculator 失败,暂不支持:" + name, e); 30 | } 31 | } 32 | 33 | /** 34 | * 构造摘要信息哈希计算器 35 | * @param messageDigest 消息摘要算法 36 | */ 37 | public MessageDigestHashCalculator(MessageDigest messageDigest) { 38 | this.messageDigest = messageDigest; 39 | } 40 | 41 | /** 42 | * 获取哈希名称,例如 MD5、SHA1、SHA256等,详情{@link org.dromara.x.file.storage.core.constant.Constant.Hash} 43 | */ 44 | @Override 45 | public String getName() { 46 | return messageDigest.getAlgorithm(); 47 | } 48 | 49 | /** 50 | * 获取哈希值,注意获取后将不能继续增量计算哈希 51 | */ 52 | @Override 53 | public String getValue() { 54 | if (value == null) { 55 | synchronized (this) { 56 | if (value == null) { 57 | value = HexUtil.encodeHexStr(messageDigest.digest()); 58 | } 59 | } 60 | } 61 | return value; 62 | } 63 | 64 | /** 65 | * 增量计算哈希 66 | */ 67 | @Override 68 | public void update(byte[] bytes) { 69 | messageDigest.update(bytes); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/upload/CompleteMultipartUploadPretreatment.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.upload; 2 | 3 | import java.util.List; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import lombok.experimental.Accessors; 7 | import org.dromara.x.file.storage.core.FileInfo; 8 | import org.dromara.x.file.storage.core.FileStorageService; 9 | import org.dromara.x.file.storage.core.ProgressListener; 10 | import org.dromara.x.file.storage.core.ProgressListenerSetter; 11 | 12 | /** 13 | * 手动分片上传-完成预处理器 14 | */ 15 | @Getter 16 | @Setter 17 | @Accessors(chain = true) 18 | public class CompleteMultipartUploadPretreatment 19 | implements ProgressListenerSetter { 20 | /** 21 | * 文件存储服务类 22 | */ 23 | private FileStorageService fileStorageService; 24 | /** 25 | * 文件信息 26 | */ 27 | private FileInfo fileInfo; 28 | /** 29 | * 文件分片信息,不传则自动使用全部已上传的分片 30 | */ 31 | private List partInfoList; 32 | /** 33 | * 完成进度监听 34 | */ 35 | private ProgressListener progressListener; 36 | 37 | /** 38 | * 如果条件为 true 则:设置文件存储服务类 39 | * @param flag 条件 40 | * @param fileStorageService 文件存储服务类 41 | * @return 手动分片上传-完成预处理器 42 | */ 43 | public CompleteMultipartUploadPretreatment setFileStorageService( 44 | boolean flag, FileStorageService fileStorageService) { 45 | if (flag) setFileStorageService(fileStorageService); 46 | return this; 47 | } 48 | 49 | /** 50 | * 如果条件为 true 则:设置文件信息 51 | * @param flag 条件 52 | * @param fileInfo 文件信息 53 | * @return 手动分片上传-完成预处理器 54 | */ 55 | public CompleteMultipartUploadPretreatment setFileInfo(boolean flag, FileInfo fileInfo) { 56 | if (flag) setFileInfo(fileInfo); 57 | return this; 58 | } 59 | 60 | /** 61 | * 如果条件为 true 则:设置文件分片信息,不传则自动使用全部已上传的分片 62 | * @param flag 条件 63 | * @param partInfoList 文件分片信息,不传则自动使用全部已上传的分片 64 | * @return 手动分片上传-完成预处理器 65 | */ 66 | public CompleteMultipartUploadPretreatment setFileInfo(boolean flag, List partInfoList) { 67 | if (flag) setPartInfoList(partInfoList); 68 | return this; 69 | } 70 | 71 | /** 72 | * 执行完成 73 | */ 74 | public FileInfo complete() { 75 | return new CompleteMultipartUploadActuator(this).execute(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/platform/AmazonS3FileStorageClientFactory.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.platform; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.amazonaws.auth.AWSStaticCredentialsProvider; 5 | import com.amazonaws.auth.BasicAWSCredentials; 6 | import com.amazonaws.client.builder.AwsClientBuilder; 7 | import com.amazonaws.services.s3.AmazonS3; 8 | import com.amazonaws.services.s3.AmazonS3ClientBuilder; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import lombok.Setter; 12 | import org.dromara.x.file.storage.core.FileStorageProperties.AmazonS3Config; 13 | 14 | /** 15 | * Amazon S3 存储平台的 Client 工厂 16 | */ 17 | @Getter 18 | @Setter 19 | @NoArgsConstructor 20 | public class AmazonS3FileStorageClientFactory implements FileStorageClientFactory { 21 | private String platform; 22 | private String accessKey; 23 | private String secretKey; 24 | private String region; 25 | private String endPoint; 26 | private volatile AmazonS3 client; 27 | 28 | public AmazonS3FileStorageClientFactory(AmazonS3Config config) { 29 | platform = config.getPlatform(); 30 | accessKey = config.getAccessKey(); 31 | secretKey = config.getSecretKey(); 32 | region = config.getRegion(); 33 | endPoint = config.getEndPoint(); 34 | } 35 | 36 | @Override 37 | public AmazonS3 getClient() { 38 | if (client == null) { 39 | synchronized (this) { 40 | if (client == null) { 41 | AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard() 42 | .withCredentials( 43 | new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey))); 44 | if (StrUtil.isNotBlank(endPoint)) { 45 | builder.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endPoint, region)); 46 | } else if (StrUtil.isNotBlank(region)) { 47 | builder.withRegion(region); 48 | } 49 | client = builder.build(); 50 | } 51 | } 52 | } 53 | return client; 54 | } 55 | 56 | @Override 57 | public void close() { 58 | if (client != null) { 59 | client.shutdown(); 60 | client = null; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-general-test/src/main/java/org/dromara/x/file/storage/test/service/FilePartDetailService.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.test.service; 2 | 3 | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 4 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import lombok.SneakyThrows; 8 | import org.dromara.x.file.storage.core.upload.FilePartInfo; 9 | import org.dromara.x.file.storage.test.mapper.FilePartDetailMapper; 10 | import org.dromara.x.file.storage.test.model.FilePartDetail; 11 | import org.springframework.stereotype.Service; 12 | 13 | /** 14 | * 用来将文件分片上传记录保存到数据库,这里使用了 MyBatis-Plus 和 Hutool 工具类 15 | * 目前仅手动分片分片上传时使用 16 | */ 17 | @Service 18 | public class FilePartDetailService extends ServiceImpl { 19 | 20 | private final ObjectMapper objectMapper = new ObjectMapper(); 21 | 22 | /** 23 | * 保存文件分片信息 24 | * @param info 文件分片信息 25 | */ 26 | @SneakyThrows 27 | public void saveFilePart(FilePartInfo info) { 28 | FilePartDetail detail = toFilePartDetail(info); 29 | if (save(detail)) { 30 | info.setId(detail.getId()); 31 | } 32 | } 33 | 34 | /** 35 | * 删除文件分片信息 36 | */ 37 | public void deleteFilePartByUploadId(String uploadId) { 38 | remove(new QueryWrapper().eq(FilePartDetail.COL_UPLOAD_ID, uploadId)); 39 | } 40 | 41 | /** 42 | * 将 FilePartInfo 转成 FilePartDetail 43 | * @param info 文件分片信息 44 | */ 45 | public FilePartDetail toFilePartDetail(FilePartInfo info) throws JsonProcessingException { 46 | FilePartDetail detail = new FilePartDetail(); 47 | detail.setPlatform(info.getPlatform()); 48 | detail.setUploadId(info.getUploadId()); 49 | detail.setETag(info.getETag()); 50 | detail.setPartNumber(info.getPartNumber()); 51 | detail.setPartSize(info.getPartSize()); 52 | detail.setHashInfo(valueToJson(info.getHashInfo())); 53 | detail.setCreateTime(info.getCreateTime()); 54 | return detail; 55 | } 56 | 57 | /** 58 | * 将指定值转换成 json 字符串 59 | */ 60 | public String valueToJson(Object value) throws JsonProcessingException { 61 | if (value == null) return null; 62 | return objectMapper.writeValueAsString(value); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /docs/在Solon中使用.md: -------------------------------------------------------------------------------- 1 | # 在 Solon 中使用 2 | 3 | > Solon 官网 https://solon.noear.org/ 4 | 5 | 从 `2.2.0` 版本开始原生支持在 `Solon` 中使用,之前的版本可以参考 [脱离 SpringBoot 单独使用](脱离SpringBoot单独使用) 6 | 7 | 先引入本项目, 注意这里是 `x-file-storage-solon`,而不是 `x-file-storage-core`,之后再参考 [快速入门](快速入门) 引入对应平台的依赖 8 | 9 | ```xml 10 | 11 | org.dromara.x-file-storage 12 | x-file-storage-solon 13 | 2.3.0 14 | 15 | ``` 16 | 17 | 如果使用本地存储, 请务必添加 `solon.web.staticfiles` 依赖 18 | ```xml 19 | 20 | org.noear 21 | solon.web.staticfiles 22 | 2.7.1 23 | 24 | ``` 25 | 26 | 27 | 默认情况下,Solon 会自动装配 `SolonFileStorageProperties` , 文件配置完全等同于使用 Spring 的情况, 可以在 `app.yml` 中按照 Spring 配置文件的格式进行配置。 28 | 如: 29 | ```yaml 30 | dromara: 31 | x-file-storage: #文件存储配置 32 | default-platform: minio-user #默认使用的存储平台 33 | thumbnail-suffix: ".min.jpg" #缩略图后缀,例如【.min.jpg】【.png】 34 | enable-multipart-file-wrapper: true #是否启用多文件包装器 35 | local-plus: 36 | - platform: local-plus-1 # 存储平台标识 37 | enable-storage: true #启用存储 38 | enable-access: true #启用访问(线上请使用 Nginx 配置,效率更高) 39 | domain: http://127.0.0.1:8080/file/ # 访问域名,例如:“http://127.0.0.1:8030/file/”,注意后面要和 path-patterns 保持一致,“/”结尾,本地存储建议使用相对路径,方便后期更换域名 40 | base-path: local-plus/ # 基础路径 41 | path-patterns: /file/ # 访问路径 42 | storage-path: D:/temp/ # 存储路径 43 | minio: 44 | - platform: minio-user # 存储平台标识 45 | enable-storage: true # 启用存储 46 | access-key: j9rMyECcmNH0lNBqPfOo 47 | secret-key: 0NYFJSl4D8msuxHirenthXA4lvju4c3QNdmQ29Ob 48 | end-point: http://127.0.0.1:9000 49 | bucket-name: user 50 | # domain: ?? # 访问域名,注意“/”结尾,例如:http://minio.abc.com/abc/ 51 | base-path: user/ # 基础路径 52 | - platform: minio-dept # 存储平台标识 53 | enable-storage: true # 启用存储 54 | access-key: j9rMyECcmNH0lNBqPfOo 55 | secret-key: 0NYFJSl4D8msuxHirenthXA4lvju4c3QNdmQ29Ob 56 | end-point: http://127.0.0.1:9000 57 | bucket-name: dept 58 | # domain: ?? # 访问域名,注意“/”结尾,例如:http://minio.abc.com/abc/ 59 | base-path: dept/ # 基础路径 60 | ``` 61 | 62 | 同时, 由于 Solon 和 Spring 环境不一致, 部分功能使用会有差异, 详情参见 [Solon 与 Spring Boot 的区别](https://solon.noear.org/article/compare-springboot) 63 | 64 | 在注入 `fileStorageService` 后, 可以使用 `fileStorageService` 进行文件操作, 例如上传、下载、删除等。完全等同 Spring 的使用方式。 65 | 66 | 67 | -------------------------------------------------------------------------------- /docs/Metadata.md: -------------------------------------------------------------------------------- 1 | # Metadata 元数据 2 | 3 | ## 使用 4 | 5 | 可以在上传时传入 Metadata 和 UserMetadata ,目前仅 华为云 OBS、阿里云 OSS、腾讯云 COS、百度云 BOS、七牛云 Kodo、又拍云 USS、MinIO、Amazon S3、Amazon S3 V2、GoogleCloud Storage、FastDFS、Azure Blob Storage、Mongo GridFS、火山引擎 TOS 平台支持 6 | 7 | ```java 8 | //判断是否支持 Metadata 9 | FileStorage storage = fileStorageService.getFileStorage(); 10 | boolean supportMetadata = fileStorageService.isSupportMetadata(storage); 11 | 12 | //上传并传入 Metadata 13 | FileInfo fileInfo = fileStorageService.of(file) 14 | .putMetadata(Constant.Metadata.CONTENT_DISPOSITION,"attachment;filename=DownloadFileName.jpg") 15 | .putMetadata("Test-Not-Support","123456")//测试不支持的元数据,此数据并不会生效 16 | .putUserMetadata("role","666") 17 | .putThMetadata(Constant.Metadata.CONTENT_DISPOSITION,"attachment;filename=DownloadThFileName.jpg") 18 | .putThUserMetadata("role","777") 19 | .thumbnail() 20 | .upload(); 21 | 22 | //获取 23 | RemoteFileInfo info = fileStorageService.getFile(fileInfo); 24 | Assert.notNull(info, "文件不存在"); 25 | //文件元数据 26 | MapProxy metadata = info.getKebabCaseInsensitiveMetadata(); 27 | //文件用户元数据 28 | MapProxy userMetadata = info.getKebabCaseInsensitiveUserMetadata(); 29 | ``` 30 | 31 | > [!WARNING|label:重要提示:] 32 | > 每个存储平台支持的 Metadata 有所不同,例如 七牛云 Kodo 和 又拍云 USS 就不支持 `Content-Disposition`,具体支持情况以每个存储平台的官方文档为准 33 | > 34 | > 在传入 UserMetadata 时,不用传入前缀,例如 `x-amz-meta-` `x-qn-meta-` `x-upyun-meta-`,SDK会自动处理 35 | > 36 | > 每个存储平台获取到的 Metadata 都不相同,有些是字符串类型,有些是其它类型的对象,这部分需要自行做好判断 37 | 38 | 39 | ## 处理异常 40 | 41 | 默认在不支持的存储平台传入 UserMetadata 会抛出异常,可以通过以下方式不抛出异常 42 | 43 | **第一种(全局)** 44 | ```yaml 45 | dromara: 46 | x-file-storage: 47 | upload-not-support-metadata-throw-exception: false # 上传时 48 | copy-not-support-metadata-throw-exception: false # 复制时 49 | move-not-support-metadata-throw-exception: false # 移动时 50 | ``` 51 | 52 | **第二种(仅当前)** 53 | ```java 54 | //上传时 55 | FileInfo fileInfo = fileStorageService.of(file) 56 | .setNotSupportMetadataThrowException(false) //在不支持 Metadata 的存储平台不抛出异常 57 | .putUserMetadata("role","666") 58 | .upload(); 59 | 60 | //复制时 61 | FileInfo fileInfo = fileStorageService.copy(fileInfo) 62 | .setNotSupportMetadataThrowException(false) //在不支持 Metadata 的存储平台不抛出异常 63 | .setPlatform("local-plus-1") 64 | .copy(); 65 | 66 | //移动时 67 | FileInfo fileInfo = fileStorageService.move(fileInfo) 68 | .setNotSupportMetadataThrowException(false) //在不支持 Metadata 的存储平台不抛出异常 69 | .setPlatform("local-plus-1") 70 | .move(); 71 | ``` 72 | 73 | 74 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/util/KebabCaseInsensitiveMap.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.util; 2 | 3 | import cn.hutool.core.map.FuncKeyMap; 4 | import cn.hutool.core.map.MapBuilder; 5 | import cn.hutool.core.text.NamingCase; 6 | import java.io.Serializable; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.function.Function; 10 | 11 | /** 12 | * 短横Key风格且不区分大小写的Map
13 | * 对KEY转换为短横,以下方式都获得的值相同,put进入的值也会被覆盖
14 | * get("ContentType")
15 | * get("Content_Type")
16 | * get("Content-Type")
17 | * get("contentType")
18 | * 19 | * @param 键类型 20 | * @param 值类型 21 | */ 22 | public class KebabCaseInsensitiveMap extends FuncKeyMap { 23 | private static final long serialVersionUID = 4043263744224569870L; 24 | 25 | /** 26 | * 构造 27 | */ 28 | public KebabCaseInsensitiveMap() { 29 | this(DEFAULT_INITIAL_CAPACITY); 30 | } 31 | 32 | /** 33 | * 构造 34 | * 35 | * @param initialCapacity 初始大小 36 | */ 37 | public KebabCaseInsensitiveMap(int initialCapacity) { 38 | this(initialCapacity, DEFAULT_LOAD_FACTOR); 39 | } 40 | 41 | /** 42 | * 构造 43 | * 44 | * @param m Map 45 | */ 46 | public KebabCaseInsensitiveMap(Map m) { 47 | this(DEFAULT_LOAD_FACTOR, m); 48 | } 49 | 50 | /** 51 | * 构造 52 | * 53 | * @param loadFactor 加载因子 54 | * @param m 初始Map,数据会被默认拷贝到一个新的HashMap中 55 | */ 56 | public KebabCaseInsensitiveMap(float loadFactor, Map m) { 57 | this(m.size(), loadFactor); 58 | this.putAll(m); 59 | } 60 | 61 | /** 62 | * 构造 63 | * 64 | * @param initialCapacity 初始大小 65 | * @param loadFactor 加载因子 66 | */ 67 | public KebabCaseInsensitiveMap(int initialCapacity, float loadFactor) { 68 | this(MapBuilder.create(new HashMap<>(initialCapacity, loadFactor))); 69 | } 70 | 71 | /** 72 | * 构造
73 | * 注意此构造将传入的Map作为被包装的Map,针对任何修改,传入的Map都会被同样修改。 74 | * 75 | * @param emptyMapBuilder Map构造器,必须构造空的Map 76 | */ 77 | KebabCaseInsensitiveMap(MapBuilder emptyMapBuilder) { 78 | super(emptyMapBuilder.build(), (Function & Serializable) (key) -> { 79 | if (key instanceof CharSequence) { 80 | key = NamingCase.toKebabCase(NamingCase.toCamelCase(key.toString())) 81 | .toLowerCase(); 82 | } 83 | return Tools.cast(key); 84 | }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-general-test/src/test/java/org/dromara/x/file/storage/test/FileStorageServiceBigFileTest.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.test; 2 | 3 | import cn.hutool.core.io.FileUtil; 4 | import cn.hutool.core.lang.Assert; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.net.URL; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.dromara.x.file.storage.core.FileInfo; 10 | import org.dromara.x.file.storage.core.FileStorageService; 11 | import org.dromara.x.file.storage.core.ProgressListener; 12 | import org.junit.jupiter.api.Test; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | 16 | @Slf4j 17 | @SpringBootTest 18 | class FileStorageServiceBigFileTest { 19 | 20 | @Autowired 21 | private FileStorageService fileStorageService; 22 | 23 | /** 24 | * 测试大文件上传 25 | */ 26 | @Test 27 | public void uploadBigFile() throws IOException { 28 | String url = "https://app.xuyanwu.cn/BadApple/video/Bad%20Apple.mp4"; 29 | 30 | File file = new File(System.getProperty("java.io.tmpdir"), "Bad Apple.mp4"); 31 | if (!file.exists()) { 32 | log.info("测试大文件不存在,正在下载中"); 33 | FileUtil.writeFromStream(new URL(url).openStream(), file); 34 | log.info("测试大文件下载完成"); 35 | } 36 | 37 | FileInfo fileInfo = fileStorageService 38 | .of(file) 39 | .setPath("test/") 40 | .setProgressListener(new ProgressListener() { 41 | @Override 42 | public void start() { 43 | System.out.println("上传开始"); 44 | } 45 | 46 | @Override 47 | public void progress(long progressSize, Long allSize) { 48 | if (allSize == null) { 49 | System.out.println("已上传 " + progressSize + " 总大小未知"); 50 | } else { 51 | System.out.println("已上传 " + progressSize + " 总大小" + allSize + " " 52 | + (progressSize * 10000 / allSize * 0.01) + "%"); 53 | } 54 | } 55 | 56 | @Override 57 | public void finish() { 58 | System.out.println("上传结束"); 59 | } 60 | }) 61 | .upload(); 62 | Assert.notNull(fileInfo, "大文件上传失败!"); 63 | log.info("大文件上传成功:{}", fileInfo.toString()); 64 | 65 | fileStorageService.delete(fileInfo); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /docs/识别文件的MIME类型.md: -------------------------------------------------------------------------------- 1 | # 识别文件的 MIME 类型 2 | 3 | 默认是基于 `Tika` 来识别文件的 MIME 类型的,流程如下 4 | 5 | ```plantuml 6 | @startuml 7 | FileStorageService -> FileWrapperAdapter :申请包装文件 8 | FileWrapperAdapter -> ContentTypeDetect :识别文件 MIME 类型 9 | ContentTypeDetect -> TikaFactory : 获取 Tika 实例 10 | ContentTypeDetect <- TikaFactory : 返回 Tika 实例 11 | FileWrapperAdapter <- ContentTypeDetect :返回文件 MIME 类型 12 | FileStorageService <- FileWrapperAdapter :返回包装后的文件 13 | @enduml 14 | ``` 15 | 16 | ## 自定义 Tika 17 | 18 | 如果默认的 `TikaFactory` 所创建的 `Tika` 对象满足不了你的需求,可通过以下方式自定义 19 | 20 | 这里用默认的 `DefaultTikaFactory` 来演示 21 | 22 | **第一步:** 23 | 24 | 创建 `DefaultTikaFactory` 实现 `TikaFactory` 接口 25 | 26 | ```java 27 | public class DefaultTikaFactory implements TikaFactory { 28 | private volatile Tika tika; 29 | 30 | @Override 31 | public Tika getTika() { 32 | if (tika == null) { 33 | synchronized (this) { 34 | if (tika == null) { 35 | tika = new Tika(); 36 | } 37 | } 38 | } 39 | return tika; 40 | } 41 | } 42 | ``` 43 | 44 | **第二步:** 45 | 46 | 在 `SpringBoot` 中初始化 `DefaultTikaFactory` 即可 47 | 48 | ```java 49 | @Bean 50 | public TikaFactory tikaFactory() { 51 | return new DefaultTikaFactory(); 52 | } 53 | ``` 54 | 55 | 56 | ## 自定义 ContentType 识别 57 | 58 | 要是压根就不想用 `Tika` ,可以采用下面的方式自定义 `ContentTypeDetect` 59 | 60 | 这里用默认的 `TikaContentTypeDetect` 来演示 61 | 62 | **第一步:** 63 | 64 | 创建 `TikaContentTypeDetect` 实现 `ContentTypeDetect` 接口 65 | 66 | ```java 67 | @Getter 68 | @Setter 69 | @NoArgsConstructor 70 | @AllArgsConstructor 71 | public class TikaContentTypeDetect implements ContentTypeDetect { 72 | private TikaFactory tikaFactory; 73 | 74 | @Override 75 | public String detect(File file) throws IOException { 76 | return tikaFactory.getTika().detect(file); 77 | } 78 | 79 | @Override 80 | public String detect(byte[] bytes) { 81 | return tikaFactory.getTika().detect(bytes); 82 | } 83 | 84 | @Override 85 | public String detect(byte[] bytes,String filename) { 86 | return tikaFactory.getTika().detect(bytes,filename); 87 | } 88 | 89 | @Override 90 | public String detect(InputStream in,String filename) throws IOException { 91 | return tikaFactory.getTika().detect(in,filename); 92 | } 93 | } 94 | ``` 95 | 96 | **第二步:** 97 | 98 | 在 `SpringBoot` 中初始化 `TikaContentTypeDetect` 即可 99 | 100 | ```java 101 | @Bean 102 | public ContentTypeDetect contentTypeDetect(TikaFactory tikaFactory) { 103 | return new TikaContentTypeDetect(tikaFactory); 104 | } 105 | ``` 106 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-fastdfs-test/src/test/java/org/dromara/x/file/storage/fastdfs/test/FastDfsFileStorageTests.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.fastdfs.test; 2 | 3 | import cn.hutool.core.io.FileUtil; 4 | import cn.hutool.core.lang.Console; 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.IOException; 8 | import javax.annotation.Resource; 9 | import org.dromara.x.file.storage.core.FileInfo; 10 | import org.dromara.x.file.storage.core.FileStorageService; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.mock.web.MockMultipartFile; 15 | import org.springframework.test.context.junit.jupiter.SpringExtension; 16 | import org.springframework.web.multipart.MultipartFile; 17 | 18 | /** 19 | * There is no description. 20 | * 21 | * @author XS 22 | * @version 1.0 23 | * @date 2023/10/23 10:35 24 | */ 25 | @ExtendWith(SpringExtension.class) 26 | @SpringBootTest 27 | class FastDfsFileStorageTests { 28 | 29 | /** 30 | * File name 31 | */ 32 | private static final String FILE_NAME = "M00/00/00/rByFDmVu22GAGIUXAAAANH8O2pA060.txt"; 33 | 34 | @Resource 35 | private FileStorageService fileStorageService; 36 | 37 | @Test 38 | void upload() { 39 | File file = FileUtil.file("fastdfs.txt"); 40 | 41 | try { 42 | FileInputStream fileInputStream = new FileInputStream(file); 43 | MultipartFile multipartFile = 44 | new MockMultipartFile("uploadFile", file.getName(), "text/plain", fileInputStream); 45 | FileInfo upload = fileStorageService.of(multipartFile).upload(); 46 | Console.log(upload); 47 | } catch (IOException e) { 48 | e.printStackTrace(); 49 | } 50 | } 51 | 52 | @Test 53 | void download() { 54 | File tempFile = FileUtil.createTempFile(); 55 | Console.log(tempFile); 56 | fileStorageService 57 | .download(new FileInfo().setPlatform("fastdfs-1").setFilename(FILE_NAME)) 58 | .file(tempFile); 59 | } 60 | 61 | @Test 62 | void exists() { 63 | boolean exists = fileStorageService.exists( 64 | new FileInfo().setPlatform("fastdfs-1").setFilename(FILE_NAME)); 65 | Console.log("exists: " + exists); 66 | } 67 | 68 | @Test 69 | void delete() { 70 | boolean deleted = fileStorageService.delete( 71 | new FileInfo().setPlatform("fastdfs-1").setFilename(FILE_NAME)); 72 | Console.log("deleted: " + deleted); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-general-test/src/test/java/org/dromara/x/file/storage/test/FileStorageServicePoolTest.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.test; // package org.dromara.x.file.core.test; 2 | // 3 | // import cn.hutool.core.lang.Assert; 4 | // import cn.hutool.extra.ssh.Sftp; 5 | // import org.dromara.x.file.storage.core.FileInfo; 6 | // import org.dromara.x.file.storage.core.FileStorageService; 7 | // import org.dromara.x.file.storage.core.platform.SftpFileStorage; 8 | // import org.dromara.x.file.storage.core.platform.SftpFileStorageClientFactory; 9 | // import lombok.extern.slf4j.Slf4j; 10 | // import org.junit.jupiter.api.Test; 11 | // import org.springframework.beans.factory.annotation.Autowired; 12 | // import org.springframework.boot.test.context.SpringBootTest; 13 | // 14 | // import java.io.InputStream; 15 | // import java.util.Arrays; 16 | // import java.util.List; 17 | // import java.util.stream.Collectors; 18 | // 19 | // 20 | // @Slf4j 21 | // @SpringBootTest 22 | // class FileStorageServicePoolTest { 23 | // 24 | // @Autowired 25 | // private FileStorageService fileStorageService; 26 | // 27 | // /** 28 | // * 测试存储平台的对象池 29 | // */ 30 | // @Test 31 | // public void pool() throws InterruptedException { 32 | // 33 | // SftpFileStorage sf = fileStorageService.getFileStorage(); 34 | // SftpFileStorageClientFactory factory = (SftpFileStorageClientFactory) sf.getClientFactory(); 35 | // 36 | // List sftpList = Arrays.stream(new Integer[10]) 37 | // .parallel() 38 | // .map(v -> factory.getClient()) 39 | // .collect(Collectors.toList()); 40 | // 41 | // sftpList.forEach(factory::returnClient); 42 | // 43 | // log.info("开始尝试第一次验证"); 44 | // upload(); 45 | // log.info("第一次验证成功"); 46 | // 47 | // log.info("等待 3 分钟后检查再次进行尝试"); 48 | // Thread.sleep(60 * 1000); 49 | // log.info("等待 2 分钟后检查再次进行尝试"); 50 | // Thread.sleep(60 * 1000); 51 | // log.info("等待 1 分钟后检查再次进行尝试"); 52 | // Thread.sleep(60 * 1000); 53 | // 54 | // log.info("开始尝试第二次验证"); 55 | // upload(); 56 | // log.info("第二次验证成功"); 57 | // 58 | // } 59 | // 60 | // public void upload() { 61 | // String filename = "image.jpg"; 62 | // InputStream in = this.getClass().getClassLoader().getResourceAsStream(filename); 63 | // FileInfo fileInfo = fileStorageService.of(in).setOriginalFilename(filename).upload(); 64 | // Assert.notNull(fileInfo,"文件上传失败!"); 65 | // 66 | // log.info("尝试删除已存在的文件:{}",fileInfo); 67 | // boolean delete = fileStorageService.delete(fileInfo.getUrl()); 68 | // Assert.isTrue(delete,"文件删除失败!" + fileInfo.getUrl()); 69 | // log.info("文件删除成功:{}",fileInfo); 70 | // } 71 | // 72 | // } 73 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-general-test/src/main/java/org/dromara/x/file/storage/test/mapper/xml/FileDetailMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | id, url, `size`, filename, original_filename, base_path, `path`, ext, content_type, 36 | platform, th_url, th_filename, th_size, th_content_type, object_id, object_type, 37 | metadata, user_metadata, th_metadata, th_user_metadata, attr, hash_info, upload_id, 38 | upload_status, create_time 39 | 40 | -------------------------------------------------------------------------------- /docs/文件适配器.md: -------------------------------------------------------------------------------- 1 | # 文件适配器 2 | 3 | 目前已支持 File、MultipartFile、UploadedFile、byte[]、InputStream、URL、URI、String,支持自定义文件适配器 4 | 5 | 这里用 byte[] 文件适配器举例 6 | 7 | **第一步:** 8 | 9 | 先在配置文件里禁用自带的 byte[] 文件适配器,防止和这里自定义的 byte[] 文件适配器冲突 10 | 11 | ```yaml 12 | dromara: 13 | x-file-storage: 14 | enable-byte-file-wrapper: false 15 | ``` 16 | 17 | 18 | **第二步:** 19 | 20 | 创建 `ByteFileWrapper` 类并实现 `FileWrapper` 接口 21 | 22 | ```java 23 | @Getter 24 | @Setter 25 | @NoArgsConstructor 26 | public class ByteFileWrapper implements FileWrapper { 27 | private byte[] bytes; 28 | private String name; 29 | private String contentType; 30 | private InputStream inputStream; 31 | private Long size; 32 | 33 | public ByteFileWrapper(byte[] bytes,String name,String contentType,Long size) { 34 | this.bytes = bytes; 35 | this.name = name; 36 | this.contentType = contentType; 37 | this.size = size; 38 | } 39 | 40 | @Override 41 | public InputStream getInputStream() { 42 | if (inputStream == null) { 43 | inputStream = new ByteArrayInputStream(bytes); 44 | } 45 | return inputStream; 46 | } 47 | } 48 | ``` 49 | 50 | **第三步:** 51 | 52 | 创建 `ByteFileWrapperAdapter` 类并实现 `FileWrapperAdapter` 接口,这是个适配器,用来检查上传的文件是否支持,并对支持的文件进行包装 53 | 54 | ```java 55 | @Getter 56 | @Setter 57 | @NoArgsConstructor 58 | @AllArgsConstructor 59 | public class ByteFileWrapperAdapter implements FileWrapperAdapter { 60 | private ContentTypeDetect contentTypeDetect; 61 | 62 | /** 63 | * 是否支持此资源文件 64 | */ 65 | @Override 66 | public boolean isSupport(Object source) { 67 | return source instanceof byte[] || source instanceof ByteFileWrapper; 68 | } 69 | 70 | /** 71 | * 对资源文件进行包装 72 | */ 73 | @Override 74 | public FileWrapper getFileWrapper(Object source,String name,String contentType,Long size) { 75 | if (source instanceof ByteFileWrapper) { 76 | return updateFileWrapper((ByteFileWrapper) source,name,contentType,size); 77 | } else { 78 | byte[] bytes = (byte[]) source; 79 | if (name == null) name = ""; 80 | if (contentType == null) contentType = contentTypeDetect.detect(bytes,name); 81 | if (size == null) size = (long) bytes.length; 82 | return new ByteFileWrapper(bytes,name,contentType,size); 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | **第四步:** 89 | 90 | 在 `SpringBoot` 中初始化 `ByteFileWrapperAdapter` ,这样就完成了对 byte[] 类型的文件的支持 91 | 92 | ```java 93 | /** 94 | * byte[] 文件包装适配器 95 | */ 96 | @Bean 97 | public ByteFileWrapperAdapter byteFileWrapperAdapter(ContentTypeDetect contentTypeDetect) { 98 | return new ByteFileWrapperAdapter(contentTypeDetect); 99 | } 100 | ``` 101 | -------------------------------------------------------------------------------- /x-file-storage-solon/src/main/java/org/dromara/x/file/storage/solon/DromaraXFileStoragePluginImpl.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.solon; 2 | 3 | import java.util.List; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.noear.solon.core.AppContext; 6 | import org.noear.solon.core.Plugin; 7 | import org.noear.solon.web.staticfiles.StaticMappings; 8 | import org.noear.solon.web.staticfiles.repository.FileStaticRepository; 9 | 10 | /** 11 | * 启动时 注册本地存储的访问地址 12 | * @author link2fun 13 | */ 14 | @Slf4j 15 | public class DromaraXFileStoragePluginImpl implements Plugin { 16 | /** 17 | * 启动(保留,为兼容性过度) 18 | * 19 | * @param context 应用上下文 20 | */ 21 | @Override 22 | public void start(final AppContext context) { 23 | 24 | // 配置包扫描, 扫描当前插件包下的所有类 25 | context.beanScan(FileStorageAutoConfiguration.class); 26 | 27 | // 通过插件 配置本地存储的访问地址 28 | context.getBeanAsync(SolonFileStorageProperties.class, cfg -> { 29 | 30 | // local 配置 31 | //noinspection deprecation 32 | final List localList = cfg.getLocal(); 33 | //noinspection deprecation 34 | for (final SolonFileStorageProperties.SolonLocalConfig localConfig : localList) { 35 | 36 | if (!localConfig.getEnableStorage() || !localConfig.getEnableAccess()) { 37 | // 没有启用存储或 不允许访问, 则不配置映射 38 | continue; 39 | } 40 | 41 | // 添加本地绝对目录(例:/img/logo.jpg 映射地址为:/data/sss/app/img/logo.jpg) 42 | StaticMappings.add(localConfig.getPathPatterns(), new FileStaticRepository(localConfig.getBasePath())); 43 | log.info( 44 | "[x-file-storage] [local] 添加本地存储映射: {} -> {}", 45 | localConfig.getPathPatterns(), 46 | localConfig.getBasePath()); 47 | } 48 | 49 | // localPlus 配置 50 | final List localPlusList = cfg.getLocalPlus(); 51 | for (final SolonFileStorageProperties.SolonLocalPlusConfig localPlusConfig : localPlusList) { 52 | 53 | if (!localPlusConfig.getEnableStorage() || !localPlusConfig.getEnableAccess()) { 54 | // 没有启用存储或 不允许访问, 则不配置映射 55 | continue; 56 | } 57 | 58 | // 添加本地绝对目录(例:/img/logo.jpg 映射地址为:/data/sss/app/img/logo.jpg) 59 | StaticMappings.add( 60 | localPlusConfig.getPathPatterns(), new FileStaticRepository(localPlusConfig.getStoragePath())); 61 | log.info( 62 | "[x-file-storage] [localPlus] 添加本地存储映射: {} -> {}", 63 | localPlusConfig.getPathPatterns(), 64 | localPlusConfig.getStoragePath()); 65 | } 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/platform/GoogleCloudStorageFileStorageClientFactory.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.platform; 2 | 3 | import cn.hutool.core.util.URLUtil; 4 | import com.google.auth.oauth2.ServiceAccountCredentials; 5 | import com.google.cloud.storage.Storage; 6 | import com.google.cloud.storage.StorageOptions; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import lombok.Getter; 12 | import lombok.NoArgsConstructor; 13 | import lombok.Setter; 14 | import org.dromara.x.file.storage.core.FileStorageProperties.GoogleCloudStorageConfig; 15 | import org.dromara.x.file.storage.core.exception.FileStorageRuntimeException; 16 | 17 | /** 18 | * GoogleCloud Storage 存储平台的 Client 工厂 19 | */ 20 | @Getter 21 | @Setter 22 | @NoArgsConstructor 23 | public class GoogleCloudStorageFileStorageClientFactory implements FileStorageClientFactory { 24 | private String platform; 25 | private String projectId; 26 | private String credentialsPath; 27 | private volatile Storage client; 28 | 29 | public GoogleCloudStorageFileStorageClientFactory(GoogleCloudStorageConfig config) { 30 | platform = config.getPlatform(); 31 | projectId = config.getProjectId(); 32 | credentialsPath = config.getCredentialsPath(); 33 | } 34 | 35 | @Override 36 | public Storage getClient() { 37 | if (client == null) { 38 | synchronized (this) { 39 | if (client == null) { 40 | ServiceAccountCredentials credentialsFromStream; 41 | try (InputStream in = URLUtil.url(credentialsPath).openStream()) { 42 | credentialsFromStream = ServiceAccountCredentials.fromStream(in); 43 | } catch (IOException e) { 44 | throw new FileStorageRuntimeException( 45 | "GoogleCloud Storage Platform 授权 key 文件获取失败!credentialsPath:" + credentialsPath); 46 | } 47 | List scopes = Collections.singletonList("https://www.googleapis.com/auth/cloud-platform"); 48 | ServiceAccountCredentials credentials = 49 | credentialsFromStream.toBuilder().setScopes(scopes).build(); 50 | StorageOptions storageOptions = StorageOptions.newBuilder() 51 | .setProjectId(projectId) 52 | .setCredentials(credentials) 53 | .build(); 54 | client = storageOptions.getService(); 55 | } 56 | } 57 | } 58 | return client; 59 | } 60 | 61 | @Override 62 | public void close() { 63 | if (client != null) { 64 | try { 65 | client.close(); 66 | } catch (Exception e) { 67 | throw new FileStorageRuntimeException("关闭 GoogleCloud Storage Client 失败!", e); 68 | } 69 | client = null; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/get/GetFilePretreatment.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.get; 2 | 3 | import java.util.List; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import lombok.experimental.Accessors; 7 | import org.dromara.x.file.storage.core.FileStorageService; 8 | import org.dromara.x.file.storage.core.aspect.FileStorageAspect; 9 | import org.dromara.x.file.storage.core.platform.FileStorage; 10 | 11 | /** 12 | * 获取文件预处理器 13 | */ 14 | @Getter 15 | @Setter 16 | @Accessors(chain = true) 17 | public class GetFilePretreatment { 18 | /** 19 | * 文件存储服务类 20 | */ 21 | private FileStorageService fileStorageService; 22 | /** 23 | * 存储平台名称 24 | */ 25 | private String platform; 26 | /** 27 | * 路径,需要与上传时传入的路径保持一致 28 | */ 29 | private String path = ""; 30 | /** 31 | * 文件名 32 | */ 33 | private String filename = ""; 34 | /** 35 | * 文件访问地址,仅用于兼容 FastDFS 的 URL 模式 36 | */ 37 | private String url = ""; 38 | 39 | /** 40 | * 如果条件为 true 则:设置文件存储服务类 41 | * @param flag 条件 42 | * @param fileStorageService 文件存储服务类 43 | * @return 获取文件预处理器 44 | */ 45 | public GetFilePretreatment setFileStorageService(boolean flag, FileStorageService fileStorageService) { 46 | if (flag) setFileStorageService(fileStorageService); 47 | return this; 48 | } 49 | 50 | /** 51 | * 设置存储平台名称(如果条件为 true) 52 | * @param flag 条件 53 | * @param platform 存储平台名称 54 | * @return 获取文件预处理器 55 | */ 56 | public GetFilePretreatment setPlatform(boolean flag, String platform) { 57 | if (flag) setPlatform(platform); 58 | return this; 59 | } 60 | 61 | /** 62 | * 设置路径,需要与上传时传入的路径保持一致(如果条件为 true) 63 | * @param flag 条件 64 | * @param path 文件访问地址 65 | * @return 获取文件预处理器 66 | */ 67 | public GetFilePretreatment setPath(boolean flag, String path) { 68 | if (flag) setPath(path); 69 | return this; 70 | } 71 | 72 | /** 73 | * 设置文件名(如果条件为 true) 74 | * @param flag 条件 75 | * @param filename 文件名 76 | * @return 获取文件预处理器 77 | */ 78 | public GetFilePretreatment setFilename(boolean flag, String filename) { 79 | if (flag) setFilename(filename); 80 | return this; 81 | } 82 | 83 | /** 84 | * 设置文件访问地址(如果条件为 true) 85 | * @param flag 条件 86 | * @param url 文件访问地址 87 | * @return 获取文件预处理器 88 | */ 89 | public GetFilePretreatment setUrl(boolean flag, String url) { 90 | if (flag) setUrl(url); 91 | return this; 92 | } 93 | 94 | /** 95 | * 执行获取文件 96 | */ 97 | public RemoteFileInfo getFile() { 98 | return new GetFileActuator(this).execute(); 99 | } 100 | 101 | /** 102 | * 执行获取文件,此方法仅限内部使用 103 | */ 104 | public RemoteFileInfo getFile(FileStorage fileStorage, List aspectList) { 105 | return new GetFileActuator(this).execute(fileStorage, aspectList); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /x-file-storage-tests/x-file-storage-general-test/src/main/resources/db/schema-mysql.sql: -------------------------------------------------------------------------------- 1 | /** 2 | 这里存放着测试用到的表结构 3 | */ 4 | 5 | -- ---------------------------- 6 | -- Table structure for file_detail 7 | -- ---------------------------- 8 | DROP TABLE IF EXISTS `file_detail`; 9 | CREATE TABLE `file_detail` 10 | ( 11 | `id` varchar(32) NOT NULL COMMENT '文件id', 12 | `url` varchar(512) NOT NULL COMMENT '文件访问地址', 13 | `size` bigint(20) DEFAULT NULL COMMENT '文件大小,单位字节', 14 | `filename` varchar(256) DEFAULT NULL COMMENT '文件名称', 15 | `original_filename` varchar(256) DEFAULT NULL COMMENT '原始文件名', 16 | `base_path` varchar(256) DEFAULT NULL COMMENT '基础存储路径', 17 | `path` varchar(256) DEFAULT NULL COMMENT '存储路径', 18 | `ext` varchar(32) DEFAULT NULL COMMENT '文件扩展名', 19 | `content_type` varchar(128) DEFAULT NULL COMMENT 'MIME类型', 20 | `platform` varchar(32) DEFAULT NULL COMMENT '存储平台', 21 | `th_url` varchar(512) DEFAULT NULL COMMENT '缩略图访问路径', 22 | `th_filename` varchar(256) DEFAULT NULL COMMENT '缩略图名称', 23 | `th_size` bigint(20) DEFAULT NULL COMMENT '缩略图大小,单位字节', 24 | `th_content_type` varchar(128) DEFAULT NULL COMMENT '缩略图MIME类型', 25 | `object_id` varchar(32) DEFAULT NULL COMMENT '文件所属对象id', 26 | `object_type` varchar(32) DEFAULT NULL COMMENT '文件所属对象类型,例如用户头像,评价图片', 27 | `metadata` text COMMENT '文件元数据', 28 | `user_metadata` text COMMENT '文件用户元数据', 29 | `th_metadata` text COMMENT '缩略图元数据', 30 | `th_user_metadata` text COMMENT '缩略图用户元数据', 31 | `attr` text COMMENT '附加属性', 32 | `file_acl` varchar(32) DEFAULT NULL COMMENT '文件ACL', 33 | `th_file_acl` varchar(32) DEFAULT NULL COMMENT '缩略图文件ACL', 34 | `hash_info` text COMMENT '哈希信息', 35 | `upload_id` varchar(128) DEFAULT NULL COMMENT '上传ID,仅在手动分片上传时使用', 36 | `upload_status` int(11) DEFAULT NULL COMMENT '上传状态,仅在手动分片上传时使用,1:初始化完成,2:上传完成', 37 | `create_time` datetime DEFAULT NULL COMMENT '创建时间', 38 | PRIMARY KEY (`id`) USING BTREE 39 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8 ROW_FORMAT = DYNAMIC COMMENT ='文件记录表'; 40 | 41 | -- ---------------------------- 42 | -- Table structure for file_part_detail 43 | -- ---------------------------- 44 | DROP TABLE IF EXISTS `file_part_detail`; 45 | CREATE TABLE `file_part_detail` 46 | ( 47 | `id` varchar(32) NOT NULL COMMENT '分片id', 48 | `platform` varchar(32) DEFAULT NULL COMMENT '存储平台', 49 | `upload_id` varchar(128) DEFAULT NULL COMMENT '上传ID,仅在手动分片上传时使用', 50 | `e_tag` varchar(255) DEFAULT NULL COMMENT '分片 ETag', 51 | `part_number` int(11) DEFAULT NULL COMMENT '分片号。每一个上传的分片都有一个分片号,一般情况下取值范围是1~10000', 52 | `part_size` bigint(20) DEFAULT NULL COMMENT '文件大小,单位字节', 53 | `hash_info` text CHARACTER SET utf8 COMMENT '哈希信息', 54 | `create_time` datetime DEFAULT NULL COMMENT '创建时间', 55 | PRIMARY KEY (`id`) 56 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='文件分片信息表,仅在手动分片上传时使用'; 57 | -------------------------------------------------------------------------------- /x-file-storage-core/src/main/java/org/dromara/x/file/storage/core/platform/AzureBlobStorageFileStorageClientFactory.java: -------------------------------------------------------------------------------- 1 | package org.dromara.x.file.storage.core.platform; 2 | 3 | import com.azure.storage.blob.BlobServiceClient; 4 | import com.azure.storage.blob.BlobServiceClientBuilder; 5 | import com.azure.storage.file.datalake.DataLakeServiceClient; 6 | import com.azure.storage.file.datalake.DataLakeServiceClientBuilder; 7 | import lombok.Data; 8 | import lombok.Getter; 9 | import lombok.Setter; 10 | import org.dromara.x.file.storage.core.FileStorageProperties.AzureBlobStorageConfig; 11 | import org.dromara.x.file.storage.core.platform.AzureBlobStorageFileStorageClientFactory.AzureBlobStorageClient; 12 | 13 | @Data 14 | public class AzureBlobStorageFileStorageClientFactory implements FileStorageClientFactory { 15 | 16 | private AzureBlobStorageConfig config; 17 | private volatile AzureBlobStorageClient client; 18 | 19 | public AzureBlobStorageFileStorageClientFactory(AzureBlobStorageConfig config) { 20 | this.config = config; 21 | } 22 | 23 | @Override 24 | public String getPlatform() { 25 | return config.getPlatform(); 26 | } 27 | 28 | @Override 29 | public AzureBlobStorageClient getClient() { 30 | if (client == null) { 31 | synchronized (this) { 32 | if (client == null) { 33 | client = new AzureBlobStorageClient(config); 34 | } 35 | } 36 | } 37 | return client; 38 | } 39 | 40 | @Override 41 | public void close() { 42 | client = null; 43 | } 44 | 45 | @Getter 46 | @Setter 47 | public static final class AzureBlobStorageClient { 48 | private AzureBlobStorageConfig config; 49 | private volatile BlobServiceClient blobServiceClient; 50 | private volatile DataLakeServiceClient dataLakeServiceClient; 51 | 52 | public AzureBlobStorageClient(AzureBlobStorageConfig config) { 53 | this.config = config; 54 | } 55 | 56 | public BlobServiceClient getBlobServiceClient() { 57 | if (blobServiceClient == null) { 58 | synchronized (this) { 59 | if (blobServiceClient == null) { 60 | blobServiceClient = new BlobServiceClientBuilder() 61 | .endpoint(config.getEndPoint()) 62 | .connectionString(config.getConnectionString()) 63 | .buildClient(); 64 | } 65 | } 66 | } 67 | return blobServiceClient; 68 | } 69 | 70 | public DataLakeServiceClient getDataLakeServiceClient() { 71 | if (dataLakeServiceClient == null) { 72 | synchronized (this) { 73 | if (dataLakeServiceClient == null) { 74 | dataLakeServiceClient = new DataLakeServiceClientBuilder() 75 | .endpoint(config.getEndPoint()) 76 | .connectionString(config.getConnectionString()) 77 | .buildClient(); 78 | } 79 | } 80 | } 81 | return dataLakeServiceClient; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /x-file-storage-tests/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.dromara.x-file-storage 7 | x-file-storage-parent 8 | ${revision} 9 | 10 | 11 | x-file-storage-tests 12 | pom 13 | X File Storage Tests 14 | x-file-storage 的测试和演示模块 15 | 16 | 17 | x-file-storage-general-test 18 | x-file-storage-fastdfs-test 19 | 20 | 21 | 22 | false 23 | 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-parent 30 | ${spring-boot.version} 31 | pom 32 | import 33 | 34 | 35 | 36 | 37 | 38 | 39 | org.dromara.x-file-storage 40 | x-file-storage-spring 41 | 42 | 43 | 44 | cn.hutool 45 | hutool-core 46 | 47 | 48 | cn.hutool 49 | hutool-http 50 | 51 | 52 | org.projectlombok 53 | lombok 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter-web 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-configuration-processor 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-starter-test 66 | test 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | org.springframework.boot 75 | spring-boot-maven-plugin 76 | ${spring-boot.version} 77 | 78 | 79 | 80 | 81 | 82 | org.springframework.boot 83 | spring-boot-maven-plugin 84 | ${spring-boot.version} 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | X File Storage 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 26 | 27 | 28 |
29 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | --------------------------------------------------------------------------------