diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c0c8926
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,35 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# IDEA artifacts and output dirs
+*.iml
+*.ipr
+*.iws
+.idea
+out
+test-output
+atlassian-ide-plugin.xml
+.gradletasknamecache
+classes/
+target/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
diff --git a/README b/README
deleted file mode 100644
index e69de29..0000000
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fdd5466
--- /dev/null
+++ b/README.md
@@ -0,0 +1,83 @@
+> 特别说明:源码、JDK、数据库、Redis等安装或存放路径禁止包含中文、空格、特殊字符等
+
+## 一 项目结构
+
+```text
+yunzhupaas-file-core-starter
+ ├── aspect- 切面层
+ ├── exception- 自定义异常
+ ├── platform - 存储平台实现层
+ └── recorder- 记录器
+```
+
+## 二 环境要求
+
+| 类目 | 版本或建议 |
+|-------|---------|
+| 硬件 | 开发电脑建议使用I3及以上CPU,16G及以上内存 |
+| 操作系统 | Windows 10/11,MacOS |
+| JDK | 默认使用JDK 21,兼容JDK 8/11、JDK17(需调整部分代码),推荐使用 `OpenJDK`,如 `Liberica JDK`、`Eclipse Temurin`、`Alibaba Dragonwell`、`BiSheng` 等发行版; |
+| Maven | 依赖管理工具,推荐使用 `3.6.3` 及以上版本 |
+| IDE | 代码集成开发环境,推荐使用 `IDEA2024` 及以上版本,兼容 `Eclipse`、 `Spring Tool Suite` 等IDE工具 |
+
+## 三 关联项目
+> 为以下项目提供基础依赖
+
+| 项目 | 分支 | 说明 |
+| --- | --- | --- |
+| yunzhupaas-common | v5.2.x-stable | Java基础依赖项目源码 |
+| yunzhupaas-java-boot | v5.2.x-stable | Java单体后端项目源码 |
+| yunzhupaas-java-cloud | v5.2.x-stable | Java微服务后端项目源码 |
+
+## 四 使用方式
+
+### 4.1 前置条件
+
+#### 4.1.1 本地安装yunzhupaas-common-core
+
+IDEA中打开 `yunzhupaas-common` 项目, 双击右侧 `Maven` 中 `yunzhupaas-common` > `yunzhupaas-boot-common` > `yunzhupaas-common-core` > `Lifecycle` > `install`,将 `yunzhupaas-common-core` 包安装至本地
+
+#### 4.1.2 本地安装dependencies
+
+IDEA中打开 `yunzhupaas-common` 项目,双击右侧 `Maven` 中 `yunzhupaas-common` > `yunzhupaas-dependencies` > `Lifecycle` > `install`,将 `yunzhupaas-dependencies` 包安装至本地
+
+### 4.2 本地安装
+
+在IDEA中,双击右侧 `Maven` 中`yunzhupaas-file-core-starter` > `Lifecycle` > `install`,将`yunzhupaas-file-core-starter`包安装至本地
+
+### 4.3 私服发布
+> 若无Maven私服,忽略本节内容
+
+#### 4.3.1 配置Maven
+
+打开Maven安装目录中的 `conf/setttings.xml` ,
+
+在 ``节点增加 `` ,如下所示:
+
+```xml
+
+
+ maven-releases
+ admin(账号,结合私服配置设置)
+ 123456(密码,结合私服配置设置)
+
+```
+#### 4.3.2 配置项目
+
+> 注意:pom.xml里 `` 和 setting.xml 配置里 `` 对应。
+
+IDEA打开 `yunzhupaas-common` 项目, 修改 `yunzhupaas-dependencies/pom.xml` 文件中私服配置
+
+```xml
+
+
+ maven-releases
+ maven-releases
+ http://nexus.yunzhupaas.com/repository/maven-releases/
+
+
+```
+
+#### 4.3.3 发布到私服
+
+在IDEA中,双击右侧 `Maven` 中 `yunzhupaas-file-core-starter` > `Lifecycle` > `deploy` 发布至私服。
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..0d9a3b2
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,172 @@
+
+
+
+
+ yunzhupaas-dependencies
+ com.yunzhupaas
+ 5.2.0-RELEASE
+
+
+ 4.0.0
+
+ com.yunzhupaas
+
+ yunzhupaas-file-core-starter
+ 5.2.0-RELEASE
+
+
+
+
+
+
+
+
+ com.github.lookfirst
+ sardine
+ provided
+ true
+
+
+
+
+ com.jcraft
+ jsch
+ provided
+ true
+
+
+
+
+ commons-net
+ commons-net
+ provided
+ true
+
+
+
+
+ cn.hutool
+ hutool-extra
+ ${hutool.version}
+ provided
+ true
+
+
+
+
+ com.amazonaws
+ aws-java-sdk-s3
+ provided
+ true
+
+
+
+
+
+ io.minio
+ minio
+ provided
+ true
+
+
+
+
+ com.upyun
+ java-sdk
+ provided
+ true
+
+
+
+
+ com.baidubce
+ bce-java-sdk
+ provided
+ true
+
+
+ org.springframework
+ spring-core
+
+
+ jdk.tools
+ jdk.tools
+
+
+
+
+
+
+ com.qcloud
+ cos_api
+ provided
+ true
+
+
+
+
+ com.qiniu
+ qiniu-java-sdk
+ provided
+ true
+
+
+
+
+ com.aliyun.oss
+ aliyun-sdk-oss
+ provided
+ true
+
+
+
+
+ com.huaweicloud
+ esdk-obs-java
+ provided
+ true
+
+
+
+
+ cn.hutool
+ hutool-core
+ ${hutool.version}
+
+
+
+
+ net.coobird
+ thumbnailator
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ provided
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+ com.yunzhupaas
+ yunzhupaas-common-core
+ ${project.version}
+ provided
+
+
+
+
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/Downloader.java b/src/main/java/cn/xuyanwu/spring/file/storage/Downloader.java
new file mode 100644
index 0000000..39e15b5
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/Downloader.java
@@ -0,0 +1,139 @@
+package cn.xuyanwu.spring.file.storage;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.xuyanwu.spring.file.storage.aspect.DownloadAspectChain;
+import cn.xuyanwu.spring.file.storage.aspect.DownloadThAspectChain;
+import cn.xuyanwu.spring.file.storage.aspect.FileStorageAspect;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import cn.xuyanwu.spring.file.storage.platform.FileStorage;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * 下载器
+ */
+public class Downloader {
+ /**
+ * 下载目标:文件
+ */
+ public static final int TARGET_FILE = 1;
+ /**
+ * 下载目标:缩略图文件
+ */
+ public static final int TARGET_TH_FILE = 2;
+
+ private final FileStorage fileStorage;
+ private final List aspectList;
+ private final FileInfo fileInfo;
+ private final Integer target;
+ private ProgressListener progressListener;
+
+ /**
+ * 构造下载器
+ *
+ * @param target 下载目标:{@link Downloader#TARGET_FILE}下载文件,{@link Downloader#TARGET_TH_FILE}下载缩略图文件
+ */
+ public Downloader(FileInfo fileInfo,List aspectList,FileStorage fileStorage,Integer target) {
+ this.fileStorage = fileStorage;
+ this.aspectList = aspectList;
+ this.fileInfo = fileInfo;
+ this.target = target;
+ }
+
+ /**
+ * 设置下载进度监听器
+ * @param progressListener 提供一个参数,表示已传输字节数
+ */
+ public Downloader setProgressMonitor(Consumer progressListener) {
+ return setProgressMonitor((progressSize,allSize) -> progressListener.accept(progressSize));
+ }
+
+ /**
+ * 设置下载进度监听器
+ * @param progressListener 提供两个参数,第一个是 progressSize已传输字节数,第二个是 allSize总字节数
+ */
+ public Downloader setProgressMonitor(BiConsumer progressListener) {
+ return setProgressMonitor(new ProgressListener() {
+ @Override
+ public void start() {
+ }
+
+ @Override
+ public void progress(long progressSize,long allSize) {
+ progressListener.accept(progressSize,allSize);
+ }
+
+ @Override
+ public void finish() {
+ }
+ });
+ }
+
+ /**
+ * 设置下载进度监听器
+ */
+ public Downloader setProgressMonitor(ProgressListener progressListener) {
+ this.progressListener = progressListener;
+ return this;
+ }
+
+ /**
+ * 获取 InputStream ,在此方法结束后会自动关闭 InputStream
+ */
+ public void inputStream(Consumer consumer) {
+ if (target == TARGET_FILE) { //下载文件
+ new DownloadAspectChain(aspectList,(_fileInfo,_fileStorage,_consumer) ->
+ _fileStorage.download(_fileInfo,_consumer)
+ ).next(fileInfo,fileStorage,in ->
+ consumer.accept(progressListener == null ? in : new ProgressInputStream(in,progressListener,fileInfo.getSize()))
+ );
+ } else if (target == TARGET_TH_FILE) { //下载缩略图文件
+ new DownloadThAspectChain(aspectList,(_fileInfo,_fileStorage,_consumer) ->
+ _fileStorage.downloadTh(_fileInfo,_consumer)
+ ).next(fileInfo,fileStorage,in ->
+ consumer.accept(progressListener == null ? in : new ProgressInputStream(in,progressListener,fileInfo.getThSize()))
+ );
+ } else {
+ throw new FileStorageRuntimeException("没找到对应的下载目标,请设置 target 参数!");
+ }
+ }
+
+ /**
+ * 下载 byte 数组
+ */
+ public byte[] bytes() {
+ byte[][] bytes = new byte[1][];
+ inputStream(in -> bytes[0] = IoUtil.readBytes(in));
+ return bytes[0];
+ }
+
+ /**
+ * 下载到指定文件
+ */
+ public void file(File file) {
+ inputStream(in -> FileUtil.writeFromStream(in,file));
+ }
+
+ /**
+ * 下载到指定文件
+ */
+ public void file(String filename) {
+ inputStream(in -> FileUtil.writeFromStream(in,filename));
+ }
+
+ /**
+ * 下载到指定输出流
+ */
+ public void outputStream(OutputStream out) {
+ inputStream(in -> IoUtil.copy(in,out));
+ }
+
+
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/EnableFileStorage.java b/src/main/java/cn/xuyanwu/spring/file/storage/EnableFileStorage.java
new file mode 100644
index 0000000..9a3b255
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/EnableFileStorage.java
@@ -0,0 +1,16 @@
+package cn.xuyanwu.spring.file.storage;
+
+import org.springframework.context.annotation.Import;
+
+import java.lang.annotation.*;
+
+/**
+ * 启用文件存储,会自动根据配置文件进行加载
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+@Documented
+@Import({FileStorageAutoConfiguration.class,FileStorageProperties.class})
+public @interface EnableFileStorage {
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/FileInfo.java b/src/main/java/cn/xuyanwu/spring/file/storage/FileInfo.java
new file mode 100644
index 0000000..d1b2352
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/FileInfo.java
@@ -0,0 +1,105 @@
+package cn.xuyanwu.spring.file.storage;
+
+
+import cn.hutool.core.lang.Dict;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+public class FileInfo implements Serializable {
+
+ /**
+ * 文件id
+ */
+ private String id;
+
+ /**
+ * 文件访问地址
+ */
+ private String url;
+
+ /**
+ * 文件大小,单位字节
+ */
+ private Long size;
+
+ /**
+ * 文件名称
+ */
+ private String filename;
+
+ /**
+ * 原始文件名
+ */
+ private String originalFilename;
+
+ /**
+ * 基础存储路径
+ */
+ private String basePath;
+
+ /**
+ * 存储路径
+ */
+ private String path;
+
+ /**
+ * 文件扩展名
+ */
+ private String ext;
+
+ /**
+ * MIME 类型
+ */
+ private String contentType;
+
+ /**
+ * 存储平台
+ */
+ private String platform;
+
+ /**
+ * 缩略图访问路径
+ */
+ private String thUrl;
+
+ /**
+ * 缩略图名称
+ */
+ private String thFilename;
+
+ /**
+ * 缩略图大小,单位字节
+ */
+ private Long thSize;
+
+ /**
+ * 缩略图 MIME 类型
+ */
+ private String thContentType;
+
+ /**
+ * 文件所属对象id
+ */
+ private String objectId;
+
+ /**
+ * 文件所属对象类型,例如用户头像,评价图片
+ */
+ private String objectType;
+
+ /**
+ * 附加属性字典
+ */
+ private Dict attr;
+
+ /**
+ * 创建时间
+ */
+ private Date createTime;
+
+ private static final long serialVersionUID = 1L;
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/FileStorageAutoConfiguration.java b/src/main/java/cn/xuyanwu/spring/file/storage/FileStorageAutoConfiguration.java
new file mode 100644
index 0000000..bfb3506
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/FileStorageAutoConfiguration.java
@@ -0,0 +1,413 @@
+package cn.xuyanwu.spring.file.storage;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.xuyanwu.spring.file.storage.aspect.FileStorageAspect;
+import cn.xuyanwu.spring.file.storage.platform.*;
+import cn.xuyanwu.spring.file.storage.recorder.DefaultFileRecorder;
+import cn.xuyanwu.spring.file.storage.recorder.FileRecorder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Configuration
+@ConditionalOnMissingBean(FileStorageService.class)
+public class FileStorageAutoConfiguration implements WebMvcConfigurer {
+
+ @Autowired
+ private FileStorageProperties properties;
+ @Autowired
+ private ApplicationContext applicationContext;
+
+
+ /**
+ * 配置本地存储的访问地址
+ */
+ @Override
+ public void addResourceHandlers(ResourceHandlerRegistry registry) {
+ for (FileStorageProperties.Local local : properties.getLocal()) {
+ if (local.getEnableAccess()) {
+ registry.addResourceHandler(local.getPathPatterns()).addResourceLocations("file:" + local.getBasePath());
+ }
+ }
+ for (FileStorageProperties.LocalPlus local : properties.getLocalPlus()) {
+ if (local.getEnableAccess()) {
+ registry.addResourceHandler(local.getPathPatterns()).addResourceLocations("file:" + local.getStoragePath());
+ }
+ }
+ }
+
+ /**
+ * 本地存储 Bean
+ */
+ @Bean
+ public List localFileStorageList() {
+ return properties.getLocal().stream().map(local -> {
+ if (!local.getEnableStorage()) return null;
+ log.info("加载存储平台:{}",local.getPlatform());
+ LocalFileStorage localFileStorage = new LocalFileStorage();
+ localFileStorage.setPlatform(local.getPlatform());
+ localFileStorage.setBasePath(local.getBasePath());
+ localFileStorage.setDomain(local.getDomain());
+ return localFileStorage;
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ /**
+ * 本地存储升级版 Bean
+ */
+ @Bean
+ public List localPlusFileStorageList() {
+ return properties.getLocalPlus().stream().map(local -> {
+ if (!local.getEnableStorage()) return null;
+ log.info("加载存储平台:{}",local.getPlatform());
+ LocalPlusFileStorage localFileStorage = new LocalPlusFileStorage();
+ localFileStorage.setPlatform(local.getPlatform());
+ localFileStorage.setBasePath(local.getBasePath());
+ localFileStorage.setDomain(local.getDomain());
+ localFileStorage.setStoragePath(local.getStoragePath());
+ return localFileStorage;
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ /**
+ * 华为云 OBS 存储 Bean
+ */
+ @Bean
+ @ConditionalOnClass(name = "com.obs.services.ObsClient")
+ public List huaweiObsFileStorageList() {
+ return properties.getHuaweiObs().stream().map(obs -> {
+ if (!obs.getEnableStorage()) return null;
+ log.info("加载存储平台:{}",obs.getPlatform());
+ HuaweiObsFileStorage storage = new HuaweiObsFileStorage();
+ storage.setPlatform(obs.getPlatform());
+ storage.setAccessKey(obs.getAccessKey());
+ storage.setSecretKey(obs.getSecretKey());
+ storage.setEndPoint(obs.getEndPoint());
+ storage.setBucketName(obs.getBucketName());
+ storage.setDomain(obs.getDomain());
+ storage.setBasePath(obs.getBasePath());
+ return storage;
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ /**
+ * 阿里云 OSS 存储 Bean
+ */
+ @Bean
+ @ConditionalOnClass(name = "com.aliyun.oss.OSS")
+ public List aliyunOssFileStorageList() {
+ return properties.getAliyunOss().stream().map(oss -> {
+ if (!oss.getEnableStorage()) return null;
+ log.info("加载存储平台:{}",oss.getPlatform());
+ AliyunOssFileStorage storage = new AliyunOssFileStorage();
+ storage.setPlatform(oss.getPlatform());
+ storage.setAccessKey(oss.getAccessKey());
+ storage.setSecretKey(oss.getSecretKey());
+ storage.setEndPoint(oss.getEndPoint());
+ storage.setBucketName(oss.getBucketName());
+ storage.setDomain(oss.getDomain());
+ storage.setBasePath(oss.getBasePath());
+ return storage;
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ /**
+ * 七牛云 Kodo 存储 Bean
+ */
+ @Bean
+ @ConditionalOnClass(name = "com.qiniu.storage.UploadManager")
+ public List qiniuKodoFileStorageList() {
+ return properties.getQiniuKodo().stream().map(kodo -> {
+ if (!kodo.getEnableStorage()) return null;
+ log.info("加载存储平台:{}",kodo.getPlatform());
+ QiniuKodoFileStorage storage = new QiniuKodoFileStorage();
+ storage.setPlatform(kodo.getPlatform());
+ storage.setAccessKey(kodo.getAccessKey());
+ storage.setSecretKey(kodo.getSecretKey());
+ storage.setBucketName(kodo.getBucketName());
+ storage.setDomain(kodo.getDomain());
+ storage.setBasePath(kodo.getBasePath());
+ return storage;
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ /**
+ * 腾讯云 COS 存储 Bean
+ */
+ @Bean
+ @ConditionalOnClass(name = "com.qcloud.cos.COSClient")
+ public List tencentCosFileStorageList() {
+ return properties.getTencentCos().stream().map(cos -> {
+ if (!cos.getEnableStorage()) return null;
+ log.info("加载存储平台:{}",cos.getPlatform());
+ TencentCosFileStorage storage = new TencentCosFileStorage();
+ storage.setPlatform(cos.getPlatform());
+ storage.setSecretId(cos.getSecretId());
+ storage.setSecretKey(cos.getSecretKey());
+ storage.setRegion(cos.getRegion());
+ storage.setBucketName(cos.getBucketName());
+ storage.setDomain(cos.getDomain());
+ storage.setBasePath(cos.getBasePath());
+ return storage;
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ /**
+ * 百度云 BOS 存储 Bean
+ */
+ @Bean
+ @ConditionalOnClass(name = "com.baidubce.services.bos.BosClient")
+ public List baiduBosFileStorageList() {
+ return properties.getBaiduBos().stream().map(bos -> {
+ if (!bos.getEnableStorage()) return null;
+ log.info("加载存储平台:{}",bos.getPlatform());
+ BaiduBosFileStorage storage = new BaiduBosFileStorage();
+ storage.setPlatform(bos.getPlatform());
+ storage.setAccessKey(bos.getAccessKey());
+ storage.setSecretKey(bos.getSecretKey());
+ storage.setEndPoint(bos.getEndPoint());
+ storage.setBucketName(bos.getBucketName());
+ storage.setDomain(bos.getDomain());
+ storage.setBasePath(bos.getBasePath());
+ return storage;
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ /**
+ * 又拍云 USS 存储 Bean
+ */
+ @Bean
+ @ConditionalOnClass(name = "com.upyun.RestManager")
+ public List upyunUssFileStorageList() {
+ return properties.getUpyunUSS().stream().map(uss -> {
+ if (!uss.getEnableStorage()) return null;
+ log.info("加载存储平台:{}",uss.getPlatform());
+ UpyunUssFileStorage storage = new UpyunUssFileStorage();
+ storage.setPlatform(uss.getPlatform());
+ storage.setUsername(uss.getUsername());
+ storage.setPassword(uss.getPassword());
+ storage.setBucketName(uss.getBucketName());
+ storage.setDomain(uss.getDomain());
+ storage.setBasePath(uss.getBasePath());
+ return storage;
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ /**
+ * MinIO 存储 Bean
+ */
+ @Bean
+ @ConditionalOnClass(name = "io.minio.MinioClient")
+ public List minioFileStorageList() {
+ return properties.getMinio().stream().map(minio -> {
+ if (!minio.getEnableStorage()) return null;
+ log.info("加载存储平台:{}",minio.getPlatform());
+ MinIOFileStorage storage = new MinIOFileStorage();
+ storage.setPlatform(minio.getPlatform());
+ storage.setAccessKey(minio.getAccessKey());
+ storage.setSecretKey(minio.getSecretKey());
+ storage.setEndPoint(minio.getEndPoint());
+ storage.setBucketName(minio.getBucketName());
+ storage.setDomain(minio.getDomain());
+ storage.setBasePath(minio.getBasePath());
+ return storage;
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ /**
+ * AWS 存储 Bean
+ */
+ @Bean
+ @ConditionalOnClass(name = "com.amazonaws.services.s3.AmazonS3")
+ public List amazonS3FileStorageList() {
+ return properties.getAwsS3().stream().map(s3 -> {
+ if (!s3.getEnableStorage()) return null;
+ log.info("加载存储平台:{}",s3.getPlatform());
+ AwsS3FileStorage storage = new AwsS3FileStorage();
+ storage.setPlatform(s3.getPlatform());
+ storage.setAccessKey(s3.getAccessKey());
+ storage.setSecretKey(s3.getSecretKey());
+ storage.setRegion(s3.getRegion());
+ storage.setEndPoint(s3.getEndPoint());
+ storage.setBucketName(s3.getBucketName());
+ storage.setDomain(s3.getDomain());
+ storage.setBasePath(s3.getBasePath());
+ return storage;
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ /**
+ * FTP 存储 Bean
+ */
+ @Bean
+ @ConditionalOnClass(name = {"org.apache.commons.net.ftp.FTPClient","cn.hutool.extra.ftp.Ftp"})
+ public List ftpFileStorageList() {
+ return properties.getFtp().stream().map(ftp -> {
+ if (!ftp.getEnableStorage()) return null;
+ log.info("加载存储平台:{}",ftp.getPlatform());
+ FtpFileStorage storage = new FtpFileStorage();
+ storage.setPlatform(ftp.getPlatform());
+ storage.setHost(ftp.getHost());
+ storage.setPort(ftp.getPort());
+ storage.setUser(ftp.getUser());
+ storage.setPassword(ftp.getPassword());
+ storage.setCharset(ftp.getCharset());
+ storage.setConnectionTimeout(ftp.getConnectionTimeout());
+ storage.setSoTimeout(ftp.getSoTimeout());
+ storage.setServerLanguageCode(ftp.getServerLanguageCode());
+ storage.setSystemKey(ftp.getSystemKey());
+ storage.setIsActive(ftp.getIsActive());
+ storage.setDomain(ftp.getDomain());
+ storage.setBasePath(ftp.getBasePath());
+ storage.setStoragePath(ftp.getStoragePath());
+ return storage;
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ /**
+ * SFTP 存储 Bean
+ */
+ @Bean
+ @ConditionalOnClass(name = {"com.jcraft.jsch.ChannelSftp","cn.hutool.extra.ftp.Ftp"})
+ public List sftpFileStorageList() {
+ return properties.getSftp().stream().map(sftp -> {
+ if (!sftp.getEnableStorage()) return null;
+ log.info("加载存储平台:{}",sftp.getPlatform());
+ SftpFileStorage storage = new SftpFileStorage();
+ storage.setPlatform(sftp.getPlatform());
+ storage.setHost(sftp.getHost());
+ storage.setPort(sftp.getPort());
+ storage.setUser(sftp.getUser());
+ storage.setPassword(sftp.getPassword());
+ storage.setPrivateKeyPath(sftp.getPrivateKeyPath());
+ storage.setCharset(sftp.getCharset());
+ storage.setConnectionTimeout(sftp.getConnectionTimeout());
+ storage.setDomain(sftp.getDomain());
+ storage.setBasePath(sftp.getBasePath());
+ storage.setStoragePath(sftp.getStoragePath());
+ return storage;
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ /**
+ * WebDAV 存储 Bean
+ */
+ @Bean
+ @ConditionalOnClass(name = "com.github.sardine.Sardine")
+ public List webDavFileStorageList() {
+ return properties.getWebDav().stream().map(sftp -> {
+ if (!sftp.getEnableStorage()) return null;
+ log.info("加载存储平台:{}",sftp.getPlatform());
+ WebDavFileStorage storage = new WebDavFileStorage();
+ storage.setPlatform(sftp.getPlatform());
+ storage.setServer(sftp.getServer());
+ storage.setUser(sftp.getUser());
+ storage.setPassword(sftp.getPassword());
+ storage.setDomain(sftp.getDomain());
+ storage.setBasePath(sftp.getBasePath());
+ storage.setStoragePath(sftp.getStoragePath());
+ return storage;
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ /**
+ * 当没有找到 FileRecorder 时使用默认的 FileRecorder
+ */
+ @Bean
+ @ConditionalOnMissingBean(FileRecorder.class)
+ public FileRecorder fileRecorder() {
+ log.warn("没有找到 FileRecorder 的实现类,文件上传之外的部分功能无法正常使用,必须实现该接口才能使用完整功能!");
+ return new DefaultFileRecorder();
+ }
+
+ /**
+ * 文件存储服务
+ */
+ @Bean
+ public FileStorageService fileStorageService(FileRecorder fileRecorder,
+ List> fileStorageLists,
+ List aspectList) {
+ this.initDetect();
+ FileStorageService service = new FileStorageService();
+ service.setFileStorageList(new CopyOnWriteArrayList<>());
+ fileStorageLists.forEach(service.getFileStorageList()::addAll);
+ service.setFileRecorder(fileRecorder);
+ service.setProperties(properties);
+ service.setAspectList(new CopyOnWriteArrayList<>(aspectList));
+ return service;
+ }
+
+ /**
+ * 对 FileStorageService 注入自己的代理对象,不然会导致针对 FileStorageService 的代理方法不生效
+ */
+ @EventListener(ContextRefreshedEvent.class)
+ public void onContextRefreshedEvent() {
+ FileStorageService service = applicationContext.getBean(FileStorageService.class);
+ service.setSelf(service);
+ }
+
+ public void initDetect() {
+ String template = "检测到{}配置,但是没有找到对应的依赖库,所以无法加载此存储平台!配置参考地址:https://spring-file-storage.xuyanwu.cn/#/%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8";
+ if (CollUtil.isNotEmpty(properties.getHuaweiObs()) && doesNotExistClass("com.obs.services.ObsClient")) {
+ log.warn(template,"华为云 OBS ");
+ }
+ if (CollUtil.isNotEmpty(properties.getAliyunOss()) && doesNotExistClass("com.aliyun.oss.OSS")) {
+ log.warn(template,"阿里云 OSS ");
+ }
+ if (CollUtil.isNotEmpty(properties.getQiniuKodo()) && doesNotExistClass("com.qiniu.storage.UploadManager")) {
+ log.warn(template,"七牛云 Kodo ");
+ }
+ if (CollUtil.isNotEmpty(properties.getTencentCos()) && doesNotExistClass("com.qcloud.cos.COSClient")) {
+ log.warn(template,"腾讯云 COS ");
+ }
+ if (CollUtil.isNotEmpty(properties.getBaiduBos()) && doesNotExistClass("com.baidubce.services.bos.BosClient")) {
+ log.warn(template,"百度云 BOS ");
+ }
+ if (CollUtil.isNotEmpty(properties.getUpyunUSS()) && doesNotExistClass("com.upyun.RestManager")) {
+ log.warn(template,"又拍云 USS ");
+ }
+ if (CollUtil.isNotEmpty(properties.getMinio()) && doesNotExistClass("io.minio.MinioClient")) {
+ log.warn(template," MinIO ");
+ }
+ if (CollUtil.isNotEmpty(properties.getAwsS3()) && doesNotExistClass("com.amazonaws.services.s3.AmazonS3")) {
+ log.warn(template," AmazonS3 ");
+ }
+ if (CollUtil.isNotEmpty(properties.getFtp()) && (doesNotExistClass("org.apache.commons.net.ftp.FTPClient") || doesNotExistClass("cn.hutool.extra.ftp.Ftp"))) {
+ log.warn(template," FTP ");
+ }
+ if (CollUtil.isNotEmpty(properties.getFtp()) && (doesNotExistClass("com.jcraft.jsch.ChannelSftp") || doesNotExistClass("cn.hutool.extra.ftp.Ftp"))) {
+ log.warn(template," SFTP ");
+ }
+ if (CollUtil.isNotEmpty(properties.getAwsS3()) && doesNotExistClass("com.github.sardine.Sardine")) {
+ log.warn(template," WebDAV ");
+ }
+ }
+
+ /**
+ * 判断是否没有引入指定 Class
+ */
+ public static boolean doesNotExistClass(String name) {
+ try {
+ Class.forName(name);
+ return false;
+ } catch (ClassNotFoundException e) {
+ return true;
+ }
+ }
+
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/FileStorageProperties.java b/src/main/java/cn/xuyanwu/spring/file/storage/FileStorageProperties.java
new file mode 100644
index 0000000..ea8fe12
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/FileStorageProperties.java
@@ -0,0 +1,527 @@
+package cn.xuyanwu.spring.file.storage;
+
+import lombok.Data;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@Component
+@ConditionalOnMissingBean(FileStorageProperties.class)
+@ConfigurationProperties(prefix = "config.file-storage")
+public class FileStorageProperties {
+
+ /**
+ * 默认存储平台
+ */
+ private String defaultPlatform = "local";
+ /**
+ * 缩略图后缀,例如【.min.jpg】【.png】
+ */
+ private String thumbnailSuffix = ".min.jpg";
+ /**
+ * 本地存储
+ */
+ private List local = new ArrayList<>();
+ /**
+ * 本地存储
+ */
+ private List localPlus = new ArrayList<>();
+ /**
+ * 华为云 OBS
+ */
+ private List huaweiObs = new ArrayList<>();
+ /**
+ * 阿里云 OSS
+ */
+ private List aliyunOss = new ArrayList<>();
+ /**
+ * 七牛云 Kodo
+ */
+ private List qiniuKodo = new ArrayList<>();
+ /**
+ * 腾讯云 COS
+ */
+ private List tencentCos = new ArrayList<>();
+ /**
+ * 百度云 BOS
+ */
+ private List baiduBos = new ArrayList<>();
+ /**
+ * 又拍云 USS
+ */
+ private List upyunUSS = new ArrayList<>();
+ /**
+ * MinIO USS
+ */
+ private List minio = new ArrayList<>();
+
+ /**
+ * AWS S3
+ */
+ private List awsS3 = new ArrayList<>();
+
+ /**
+ * FTP
+ */
+ private List ftp = new ArrayList<>();
+
+ /**
+ * FTP
+ */
+ private List sftp = new ArrayList<>();
+
+ /**
+ * WebDAV
+ */
+ private List WebDav = new ArrayList<>();
+
+ /**
+ * 本地存储
+ */
+ @Data
+ public static class Local {
+ /**
+ * 本地存储路径
+ */
+ private String basePath = "";
+ /**
+ * 本地存储访问路径
+ */
+ private String[] pathPatterns = new String[0];
+ /**
+ * 启用本地存储
+ */
+ private Boolean enableStorage = false;
+ /**
+ * 启用本地访问
+ */
+ private Boolean enableAccess = false;
+ /**
+ * 存储平台
+ */
+ private String platform = "local";
+ /**
+ * 访问域名
+ */
+ private String domain = "";
+ }
+
+ /**
+ * 本地存储升级版
+ */
+ @Data
+ public static class LocalPlus {
+ /**
+ * 基础路径
+ */
+ private String basePath = "";
+ /**
+ * 存储路径,上传的文件都会存储在这个路径下面,默认“/”,注意“/”结尾
+ */
+ private String storagePath = "/";
+ /**
+ * 本地存储访问路径
+ */
+ private String[] pathPatterns = new String[0];
+ /**
+ * 启用本地存储
+ */
+ private Boolean enableStorage = false;
+ /**
+ * 启用本地访问
+ */
+ private Boolean enableAccess = false;
+ /**
+ * 存储平台
+ */
+ private String platform = "local";
+ /**
+ * 访问域名
+ */
+ private String domain = "";
+ }
+
+ /**
+ * 华为云 OBS
+ */
+ @Data
+ public static class HuaweiObs {
+ private String accessKey;
+ private String secretKey;
+ private String endPoint;
+ private String bucketName;
+ /**
+ * 访问域名
+ */
+ private String domain = "";
+ /**
+ * 启用存储
+ */
+ private Boolean enableStorage = false;
+ /**
+ * 存储平台
+ */
+ private String platform = "";
+ /**
+ * 基础路径
+ */
+ private String basePath = "";
+ }
+
+ /**
+ * 阿里云 OSS
+ */
+ @Data
+ public static class AliyunOss {
+ private String accessKey;
+ private String secretKey;
+ private String endPoint;
+ private String bucketName;
+ /**
+ * 访问域名
+ */
+ private String domain = "";
+ /**
+ * 启用存储
+ */
+ private Boolean enableStorage = false;
+ /**
+ * 存储平台
+ */
+ private String platform = "";
+ /**
+ * 基础路径
+ */
+ private String basePath = "";
+ }
+
+ /**
+ * 七牛云 Kodo
+ */
+ @Data
+ public static class QiniuKodo {
+ private String accessKey;
+ private String secretKey;
+ private String bucketName;
+ /**
+ * 访问域名
+ */
+ private String domain = "";
+ /**
+ * 启用存储
+ */
+ private Boolean enableStorage = false;
+ /**
+ * 存储平台
+ */
+ private String platform = "";
+ /**
+ * 基础路径
+ */
+ private String basePath = "";
+ }
+
+ /**
+ * 腾讯云 COS
+ */
+ @Data
+ public static class TencentCos {
+ private String secretId;
+ private String secretKey;
+ private String region;
+ private String bucketName;
+ /**
+ * 访问域名
+ */
+ private String domain = "";
+ /**
+ * 启用存储
+ */
+ private Boolean enableStorage = false;
+ /**
+ * 存储平台
+ */
+ private String platform = "";
+ /**
+ * 基础路径
+ */
+ private String basePath = "";
+ }
+
+ /**
+ * 百度云 BOS
+ */
+ @Data
+ public static class BaiduBos {
+ private String accessKey;
+ private String secretKey;
+ private String endPoint;
+ private String bucketName;
+ /**
+ * 访问域名
+ */
+ private String domain = "";
+ /**
+ * 启用存储
+ */
+ private Boolean enableStorage = false;
+ /**
+ * 存储平台
+ */
+ private String platform = "";
+ /**
+ * 基础路径
+ */
+ private String basePath = "";
+ }
+
+ /**
+ * 又拍云 USS
+ */
+ @Data
+ public static class UpyunUSS {
+ private String username;
+ private String password;
+ private String bucketName;
+ /**
+ * 访问域名
+ */
+ private String domain = "";
+ /**
+ * 启用存储
+ */
+ private Boolean enableStorage = false;
+ /**
+ * 存储平台
+ */
+ private String platform = "";
+ /**
+ * 基础路径
+ */
+ private String basePath = "";
+ }
+
+ /**
+ * MinIO
+ */
+ @Data
+ public static class MinIO {
+ private String accessKey;
+ private String secretKey;
+ private String endPoint;
+ private String bucketName;
+ /**
+ * 访问域名
+ */
+ private String domain = "";
+ /**
+ * 启用存储
+ */
+ private Boolean enableStorage = false;
+ /**
+ * 存储平台
+ */
+ private String platform = "";
+ /**
+ * 基础路径
+ */
+ private String basePath = "";
+ }
+
+ /**
+ * AWS S3
+ */
+ @Data
+ public static class AwsS3 {
+ private String accessKey;
+ private String secretKey;
+ private String region;
+ private String endPoint;
+ private String bucketName;
+ /**
+ * 访问域名
+ */
+ private String domain = "";
+ /**
+ * 启用存储
+ */
+ private Boolean enableStorage = false;
+ /**
+ * 存储平台
+ */
+ private String platform = "";
+ /**
+ * 基础路径
+ */
+ private String basePath = "";
+ }
+
+ /**
+ * FTP
+ */
+ @Data
+ public static class FTP {
+ /**
+ * 主机
+ */
+ private String host;
+ /**
+ * 端口,默认21
+ */
+ private int port = 21;
+ /**
+ * 用户名,默认 anonymous(匿名)
+ */
+ private String user = "anonymous";
+ /**
+ * 密码,默认空
+ */
+ private String password = "";
+ /**
+ * 编码,默认UTF-8
+ */
+ private Charset charset = StandardCharsets.UTF_8;
+ /**
+ * 连接超时时长,单位毫秒,默认10秒 {@link org.apache.commons.net.SocketClient#setConnectTimeout(int)}
+ */
+ private long connectionTimeout = 10 * 1000;
+ /**
+ * Socket连接超时时长,单位毫秒,默认10秒 {@link org.apache.commons.net.SocketClient#setSoTimeout(int)}
+ */
+ private long soTimeout = 10 * 1000;
+ /**
+ * 设置服务器语言,默认空,{@link org.apache.commons.net.ftp.FTPClientConfig#setServerLanguageCode(String)}
+ */
+ private String serverLanguageCode;
+ /**
+ * 服务器标识,默认空,{@link org.apache.commons.net.ftp.FTPClientConfig#FTPClientConfig(String)}
+ * 例如:org.apache.commons.net.ftp.FTPClientConfig.SYST_NT
+ */
+ private String systemKey;
+ /**
+ * 是否主动模式,默认被动模式
+ */
+ private Boolean isActive = false;
+ /**
+ * 访问域名
+ */
+ private String domain = "";
+ /**
+ * 启用存储
+ */
+ private Boolean enableStorage = false;
+ /**
+ * 存储平台
+ */
+ private String platform = "";
+ /**
+ * 基础路径
+ */
+ private String basePath = "";
+ /**
+ * 存储路径,上传的文件都会存储在这个路径下面,默认“/”,注意“/”结尾
+ */
+ private String storagePath = "/";
+ }
+
+ /**
+ * SFTP
+ */
+ @Data
+ public static class SFTP {
+ /**
+ * 主机
+ */
+ private String host;
+ /**
+ * 端口,默认22
+ */
+ private int port = 22;
+ /**
+ * 用户名
+ */
+ private String user;
+ /**
+ * 密码
+ */
+ private String password;
+ /**
+ * 私钥路径
+ */
+ private String privateKeyPath;
+ /**
+ * 编码,默认UTF-8
+ */
+ private Charset charset = StandardCharsets.UTF_8;
+ /**
+ * 连接超时时长,单位毫秒,默认10秒
+ */
+ private long connectionTimeout = 10 * 1000;
+ /**
+ * 访问域名
+ */
+ private String domain = "";
+ /**
+ * 启用存储
+ */
+ private Boolean enableStorage = false;
+ /**
+ * 存储平台
+ */
+ private String platform = "";
+ /**
+ * 基础路径
+ */
+ private String basePath = "";
+ /**
+ * 存储路径,上传的文件都会存储在这个路径下面,默认“/”,注意“/”结尾
+ */
+ private String storagePath = "/";
+ }
+
+ /**
+ * WebDAV
+ */
+ @Data
+ public static class WebDAV {
+ /**
+ * 服务器地址,注意“/”结尾,例如:http://192.168.1.105:8405/
+ */
+ private String server;
+ /**
+ * 用户名
+ */
+ private String user;
+ /**
+ * 密码
+ */
+ private String password;
+ /**
+ * 访问域名
+ */
+ private String domain = "";
+ /**
+ * 启用存储
+ */
+ private Boolean enableStorage = false;
+ /**
+ * 存储平台
+ */
+ private String platform = "";
+ /**
+ * 基础路径
+ */
+ private String basePath = "";
+ /**
+ * 存储路径,上传的文件都会存储在这个路径下面,默认“/”,注意“/”结尾
+ */
+ private String storagePath = "/";
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/FileStorageService.java b/src/main/java/cn/xuyanwu/spring/file/storage/FileStorageService.java
new file mode 100644
index 0000000..ba74bd0
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/FileStorageService.java
@@ -0,0 +1,349 @@
+package cn.xuyanwu.spring.file.storage;
+
+import cn.hutool.core.io.file.FileNameUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.ReUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.URLUtil;
+import cn.xuyanwu.spring.file.storage.aspect.DeleteAspectChain;
+import cn.xuyanwu.spring.file.storage.aspect.ExistsAspectChain;
+import cn.xuyanwu.spring.file.storage.aspect.FileStorageAspect;
+import cn.xuyanwu.spring.file.storage.aspect.UploadAspectChain;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import cn.xuyanwu.spring.file.storage.platform.FileStorage;
+import cn.xuyanwu.spring.file.storage.recorder.FileRecorder;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Date;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Predicate;
+
+
+/**
+ * 用来处理文件存储,对接多个平台
+ */
+@Slf4j
+@Getter
+@Setter
+public class FileStorageService implements DisposableBean {
+
+ private FileStorageService self;
+ private FileRecorder fileRecorder;
+ private CopyOnWriteArrayList fileStorageList;
+ private FileStorageProperties properties;
+ private CopyOnWriteArrayList aspectList;
+
+
+ /**
+ * 获取默认的存储平台
+ */
+ public FileStorage getFileStorage() {
+ return getFileStorage(properties.getDefaultPlatform());
+ }
+
+ /**
+ * 获取对应的存储平台
+ */
+ public FileStorage getFileStorage(String platform) {
+ for (FileStorage fileStorage : fileStorageList) {
+ if (fileStorage.getPlatform().equals(platform)) {
+ return fileStorage;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 获取对应的存储平台,如果存储平台不存在则抛出异常
+ */
+ public FileStorage getFileStorageVerify(FileInfo fileInfo) {
+ FileStorage fileStorage = getFileStorage(fileInfo.getPlatform());
+ if (fileStorage == null) throw new FileStorageRuntimeException("没有找到对应的存储平台!");
+ return fileStorage;
+ }
+
+ /**
+ * 上传文件,成功返回文件信息,失败返回 null
+ */
+ public FileInfo upload(UploadPretreatment pre) {
+ MultipartFile file = pre.getFileWrapper();
+ if (file == null) throw new FileStorageRuntimeException("文件不允许为 null !");
+ if (pre.getPlatform() == null) throw new FileStorageRuntimeException("platform 不允许为 null !");
+
+ FileInfo fileInfo = new FileInfo();
+ fileInfo.setCreateTime(new Date());
+ fileInfo.setSize(file.getSize());
+ fileInfo.setOriginalFilename(file.getOriginalFilename());
+ fileInfo.setExt(FileNameUtil.getSuffix(file.getOriginalFilename()));
+ fileInfo.setObjectId(pre.getObjectId());
+ fileInfo.setObjectType(pre.getObjectType());
+ fileInfo.setPath(pre.getPath());
+ fileInfo.setPlatform(pre.getPlatform());
+ fileInfo.setAttr(pre.getAttr());
+ if (StrUtil.isNotBlank(pre.getSaveFilename())) {
+ fileInfo.setFilename(pre.getSaveFilename());
+ } else {
+ fileInfo.setFilename(IdUtil.objectId() + (StrUtil.isEmpty(fileInfo.getExt()) ? StrUtil.EMPTY : "." + fileInfo.getExt()));
+ }
+ if (pre.getContentType() != null) {
+ fileInfo.setContentType(pre.getContentType());
+ } else if (pre.getFileWrapper().getContentType() != null) {
+ fileInfo.setContentType(pre.getFileWrapper().getContentType());
+ } else {
+ String contentType = URLConnection.guessContentTypeFromName(fileInfo.getFilename());
+ fileInfo.setContentType(contentType != null ? contentType : "application/octet-stream");
+ }
+
+ byte[] thumbnailBytes = pre.getThumbnailBytes();
+ if (thumbnailBytes != null) {
+ fileInfo.setThSize((long) thumbnailBytes.length);
+ if (StrUtil.isNotBlank(pre.getSaveThFilename())) {
+ fileInfo.setThFilename(pre.getSaveThFilename() + pre.getThumbnailSuffix());
+ } else {
+ fileInfo.setThFilename(fileInfo.getFilename() + pre.getThumbnailSuffix());
+ }
+ String contentType = URLConnection.guessContentTypeFromName(fileInfo.getThFilename());
+ fileInfo.setThContentType(contentType != null ? contentType : "application/octet-stream");
+ }
+
+ FileStorage fileStorage = getFileStorage(pre.getPlatform());
+ if (fileStorage == null) throw new FileStorageRuntimeException("没有找到对应的存储平台!");
+
+ //处理切面
+ return new UploadAspectChain(aspectList,(_fileInfo,_pre,_fileStorage,_fileRecorder) -> {
+ //真正开始保存
+ if (_fileStorage.save(_fileInfo,_pre)) {
+ if (_fileRecorder.record(_fileInfo)) {
+ return _fileInfo;
+ }
+ }
+ return null;
+ }).next(fileInfo,pre,fileStorage,fileRecorder);
+ }
+
+ /**
+ * 根据 url 获取 FileInfo
+ */
+ public FileInfo getFileInfoByUrl(String url) {
+ return fileRecorder.getByUrl(url);
+ }
+
+ /**
+ * 根据 url 删除文件
+ */
+ public boolean delete(String url) {
+ return delete(getFileInfoByUrl(url));
+ }
+
+ /**
+ * 根据 url 删除文件
+ */
+ public boolean delete(String url,Predicate predicate) {
+ return delete(getFileInfoByUrl(url),predicate);
+ }
+
+ /**
+ * 根据条件
+ */
+ public boolean delete(FileInfo fileInfo) {
+ return delete(fileInfo,null);
+ }
+
+ /**
+ * 根据条件删除文件
+ */
+ public boolean delete(FileInfo fileInfo,Predicate predicate) {
+ if (fileInfo == null) return true;
+ if (predicate != null && !predicate.test(fileInfo)) return false;
+ FileStorage fileStorage = getFileStorage(fileInfo.getPlatform());
+ if (fileStorage == null) throw new FileStorageRuntimeException("没有找到对应的存储平台!");
+
+ return new DeleteAspectChain(aspectList,(_fileInfo,_fileStorage,_fileRecorder) -> {
+ if (_fileStorage.delete(_fileInfo)) { //删除文件
+ return _fileRecorder.delete(_fileInfo.getUrl()); //删除文件记录
+ }
+ return false;
+ }).next(fileInfo,fileStorage,fileRecorder);
+ }
+
+ /**
+ * 文件是否存在
+ */
+ public boolean exists(String url) {
+ return exists(getFileInfoByUrl(url));
+ }
+
+ /**
+ * 文件是否存在
+ */
+ public boolean exists(FileInfo fileInfo) {
+ if (fileInfo == null) return false;
+ return new ExistsAspectChain(aspectList,(_fileInfo,_fileStorage) ->
+ _fileStorage.exists(_fileInfo)
+ ).next(fileInfo,getFileStorageVerify(fileInfo));
+ }
+
+
+ /**
+ * 获取文件下载器
+ */
+ public Downloader download(FileInfo fileInfo) {
+ return new Downloader(fileInfo,aspectList,getFileStorageVerify(fileInfo),Downloader.TARGET_FILE);
+ }
+
+ /**
+ * 获取文件下载器
+ */
+ public Downloader download(String url) {
+ return download(getFileInfoByUrl(url));
+ }
+
+ /**
+ * 获取缩略图文件下载器
+ */
+ public Downloader downloadTh(FileInfo fileInfo) {
+ return new Downloader(fileInfo,aspectList,getFileStorageVerify(fileInfo),Downloader.TARGET_TH_FILE);
+ }
+
+ /**
+ * 获取缩略图文件下载器
+ */
+ public Downloader downloadTh(String url) {
+ return downloadTh(getFileInfoByUrl(url));
+ }
+
+
+ /**
+ * 创建上传预处理器
+ */
+ public UploadPretreatment of() {
+ UploadPretreatment pre = new UploadPretreatment();
+ pre.setFileStorageService(self);
+ pre.setPlatform(properties.getDefaultPlatform());
+ pre.setThumbnailSuffix(properties.getThumbnailSuffix());
+ return pre;
+ }
+
+ /**
+ * 根据 MultipartFile 创建上传预处理器
+ */
+ public UploadPretreatment of(MultipartFile file) {
+ UploadPretreatment pre = of();
+ pre.setFileWrapper(new MultipartFileWrapper(file));
+ return pre;
+ }
+
+ /**
+ * 根据 byte[] 创建上传预处理器,name 为空字符串
+ */
+ public UploadPretreatment of(byte[] bytes) {
+ UploadPretreatment pre = of();
+ pre.setFileWrapper(new MultipartFileWrapper(new MockMultipartFile("",bytes)));
+ return pre;
+ }
+
+ /**
+ * 根据 InputStream 创建上传预处理器,originalFilename 为空字符串
+ */
+ public UploadPretreatment of(InputStream in) {
+ try {
+ UploadPretreatment pre = of();
+ pre.setFileWrapper(new MultipartFileWrapper(new MockMultipartFile("",in)));
+ return pre;
+ } catch (Exception e) {
+ throw new FileStorageRuntimeException("根据 InputStream 创建上传预处理器失败!",e);
+ }
+ }
+
+ /**
+ * 根据 File 创建上传预处理器,originalFilename 为 file 的 name
+ */
+ public UploadPretreatment of(File file) {
+ try {
+ UploadPretreatment pre = of();
+ pre.setFileWrapper(new MultipartFileWrapper(new MockMultipartFile(file.getName(),file.getName(),URLConnection.guessContentTypeFromName(file.getName()),Files.newInputStream(file.toPath()))));
+ return pre;
+ } catch (Exception e) {
+ throw new FileStorageRuntimeException("根据 File 创建上传预处理器失败!",e);
+ }
+ }
+
+ /**
+ * 根据 URL 创建上传预处理器,originalFilename 将尝试自动识别,识别不到则为空字符串
+ */
+ public UploadPretreatment of(URL url) {
+ try {
+ UploadPretreatment pre = of();
+
+ URLConnection conn = url.openConnection();
+
+ //尝试获取文件名
+ String name = "";
+ String disposition = conn.getHeaderField("Content-Disposition");
+ if (StrUtil.isNotBlank(disposition)) {
+ name = ReUtil.get("filename=\"(.*?)\"",disposition,1);
+ if (StrUtil.isBlank(name)) {
+ name = StrUtil.subAfter(disposition,"filename=",true);
+ }
+ }
+ if (StrUtil.isBlank(name)) {
+ final String path = url.getPath();
+ name = StrUtil.subSuf(path,path.lastIndexOf('/') + 1);
+ if (StrUtil.isNotBlank(name)) {
+ name = URLUtil.decode(name,StandardCharsets.UTF_8);
+ }
+ }
+
+ pre.setFileWrapper(new MultipartFileWrapper(new MockMultipartFile(url.toString(),name,conn.getContentType(),conn.getInputStream())));
+ return pre;
+ } catch (Exception e) {
+ throw new FileStorageRuntimeException("根据 URL 创建上传预处理器失败!",e);
+ }
+ }
+
+ /**
+ * 根据 URI 创建上传预处理器,originalFilename 将尝试自动识别,识别不到则为空字符串
+ */
+ public UploadPretreatment of(URI uri) {
+ try {
+ return of(uri.toURL());
+ } catch (Exception e) {
+ throw new FileStorageRuntimeException("根据 URI 创建上传预处理器失败!",e);
+ }
+ }
+
+ /**
+ * 根据 url 字符串创建上传预处理器,兼容Spring的ClassPath路径、文件路径、HTTP路径等,originalFilename 将尝试自动识别,识别不到则为空字符串
+ */
+ public UploadPretreatment of(String url) {
+ try {
+ return of(URLUtil.url(url));
+ } catch (Exception e) {
+ throw new FileStorageRuntimeException("根据 url:" + url + " 创建上传预处理器失败!",e);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ for (FileStorage fileStorage : fileStorageList) {
+ try {
+ fileStorage.close();
+ log.error("销毁存储平台 {} 成功",fileStorage.getPlatform());
+ } catch (Exception e) {
+ log.error("销毁存储平台 {} 失败,{}",fileStorage.getPlatform(),e.getMessage(),e);
+ }
+ }
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/MockMultipartFile.java b/src/main/java/cn/xuyanwu/spring/file/storage/MockMultipartFile.java
new file mode 100644
index 0000000..7af15ff
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/MockMultipartFile.java
@@ -0,0 +1,81 @@
+package cn.xuyanwu.spring.file.storage;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IoUtil;
+import lombok.Getter;
+import org.springframework.lang.Nullable;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InputStream;
+
+/**
+ * 一个模拟 MultipartFile 的类
+ */
+@Getter
+public class MockMultipartFile implements MultipartFile {
+
+ /**
+ * 文件名
+ */
+ private final String name;
+
+ /**
+ * 原始文件名
+ */
+ private final String originalFilename;
+
+ /**
+ * 内容类型
+ */
+ @Nullable
+ private final String contentType;
+
+ /**
+ * 文件内容
+ */
+ private final byte[] bytes;
+
+
+ public MockMultipartFile(String name,InputStream in) {
+ this(name,"",null,IoUtil.readBytes(in));
+ }
+
+ public MockMultipartFile(String name,@Nullable byte[] bytes) {
+ this(name,"",null,bytes);
+ }
+
+ public MockMultipartFile(String name,@Nullable String originalFilename,@Nullable String contentType,InputStream in) {
+ this(name,originalFilename,contentType,IoUtil.readBytes(in));
+ }
+
+ public MockMultipartFile(@Nullable String name,@Nullable String originalFilename,@Nullable String contentType,@Nullable byte[] bytes) {
+ this.name = (name != null ? name : "");
+ this.originalFilename = (originalFilename != null ? originalFilename : "");
+ this.contentType = contentType;
+ this.bytes = (bytes != null ? bytes : new byte[0]);
+ }
+
+
+ @Override
+ public boolean isEmpty() {
+ return (this.bytes.length == 0);
+ }
+
+ @Override
+ public long getSize() {
+ return this.bytes.length;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new ByteArrayInputStream(this.bytes);
+ }
+
+ @Override
+ public void transferTo(File dest) {
+ FileUtil.writeBytes(bytes,dest);
+ }
+
+}
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/MultipartFileWrapper.java b/src/main/java/cn/xuyanwu/spring/file/storage/MultipartFileWrapper.java
new file mode 100644
index 0000000..0bb54ca
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/MultipartFileWrapper.java
@@ -0,0 +1,84 @@
+package cn.xuyanwu.spring.file.storage;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.core.io.Resource;
+import org.springframework.lang.Nullable;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+
+/**
+ * MultipartFile 的包装类
+ */
+public class MultipartFileWrapper implements MultipartFile {
+
+ @Setter
+ private String name;
+ @Setter
+ private String originalFilename;
+ @Setter
+ private String contentType;
+ @Setter
+ @Getter
+ private MultipartFile multipartFile;
+
+ public MultipartFileWrapper(MultipartFile multipartFile) {
+ this.multipartFile = multipartFile;
+ }
+
+ @Override
+ public String getName() {
+ return name != null ? name : multipartFile.getName();
+ }
+
+ @Override
+ public String getOriginalFilename() {
+ return originalFilename != null ? originalFilename : multipartFile.getOriginalFilename();
+ }
+
+ @Override
+ @Nullable
+ public String getContentType() {
+ return contentType != null ? contentType : multipartFile.getContentType();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return multipartFile.isEmpty();
+ }
+
+ @Override
+ public long getSize() {
+ return multipartFile.getSize();
+ }
+
+ @Override
+ public byte[] getBytes() throws IOException {
+ return multipartFile.getBytes();
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return multipartFile.getInputStream();
+ }
+
+
+ @Override
+ public Resource getResource() {
+ return multipartFile.getResource();
+ }
+
+ @Override
+ public void transferTo(File dest) throws IOException, IllegalStateException {
+ multipartFile.transferTo(dest);
+ }
+
+ @Override
+ public void transferTo(Path dest) throws IOException, IllegalStateException {
+ multipartFile.transferTo(dest);
+ }
+}
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/PathUtil.java b/src/main/java/cn/xuyanwu/spring/file/storage/PathUtil.java
new file mode 100644
index 0000000..058592b
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/PathUtil.java
@@ -0,0 +1,38 @@
+package cn.xuyanwu.spring.file.storage;
+
+public class PathUtil {
+
+
+ /**
+ * 获取父路径
+ */
+ public static String getParent(String path) {
+ if (path.endsWith("/") || path.endsWith("\\")) {
+ path = path.substring(0,path.length() - 1);
+ }
+ int endIndex = Math.max(path.lastIndexOf("/"),path.lastIndexOf("\\"));
+ return endIndex > -1 ? path.substring(0,endIndex) : null;
+ }
+
+ /**
+ * 合并路径
+ */
+ public static String join(String... paths) {
+ StringBuilder sb = new StringBuilder();
+ for (String path : paths) {
+ String left = sb.toString();
+ boolean leftHas = left.endsWith("/") || left.endsWith("\\");
+ boolean rightHas = path.endsWith("/") || path.endsWith("\\");
+
+ if (leftHas && rightHas) {
+ sb.append(path.substring(1));
+ } else if (!left.isEmpty() && !leftHas && !rightHas) {
+ sb.append("/").append(path);
+ } else {
+ sb.append(path);
+ }
+ }
+ return sb.toString();
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/ProgressInputStream.java b/src/main/java/cn/xuyanwu/spring/file/storage/ProgressInputStream.java
new file mode 100644
index 0000000..204645b
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/ProgressInputStream.java
@@ -0,0 +1,62 @@
+package cn.xuyanwu.spring.file.storage;
+
+import java.io.*;
+
+/**
+ * 带进度通知的 InputStream 包装类
+ */
+public class ProgressInputStream extends FilterInputStream {
+
+ private boolean readFlag;
+ private long progressSize;
+ private final long allSize;
+ private final ProgressListener listener;
+
+ public ProgressInputStream(InputStream in,ProgressListener listener,long allSize) {
+ super(in);
+ this.listener = listener;
+ this.allSize = allSize;
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ long skip = super.skip(n);
+ progress(skip);
+ return skip;
+ }
+
+
+ @Override
+ public int read() throws IOException {
+ int b = super.read();
+ progress(b == -1 ? -1 : 1);
+ return b;
+ }
+
+ @Override
+ public int read(byte[] b,int off,int len) throws IOException {
+ if (!this.readFlag) {
+ this.readFlag = true;
+ this.listener.start();
+ }
+ int bytes = super.read(b,off,len);
+ progress(bytes);
+ return bytes;
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ protected void progress(long size) {
+ if (size > 0) {
+ this.listener.progress(progressSize += size,allSize);
+ } else if (size < 0) {
+ this.listener.finish();
+ }
+ }
+
+
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/ProgressListener.java b/src/main/java/cn/xuyanwu/spring/file/storage/ProgressListener.java
new file mode 100644
index 0000000..0697926
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/ProgressListener.java
@@ -0,0 +1,26 @@
+package cn.xuyanwu.spring.file.storage;
+
+/**
+ * 进度监听器
+ */
+public interface ProgressListener {
+
+ /**
+ * 开始
+ */
+ void start();
+
+ /**
+ * 进行中
+ *
+ * @param progressSize 已经进行的大小
+ * @param allSize 总大小,来自 fileInfo.getSize()
+ */
+ void progress(long progressSize,long allSize);
+
+ /**
+ * 结束
+ */
+ void finish();
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/UploadPretreatment.java b/src/main/java/cn/xuyanwu/spring/file/storage/UploadPretreatment.java
new file mode 100644
index 0000000..1b83789
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/UploadPretreatment.java
@@ -0,0 +1,251 @@
+package cn.xuyanwu.spring.file.storage;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.io.file.FileNameUtil;
+import cn.hutool.core.lang.Dict;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import net.coobird.thumbnailator.Thumbnails;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+import java.util.function.Consumer;
+
+/**
+ * 文件上传预处理对象
+ */
+@Getter
+@Setter
+@Accessors(chain = true)
+public class UploadPretreatment {
+ private FileStorageService fileStorageService;
+ /**
+ * 要上传到的平台
+ */
+ private String platform;
+ /**
+ * 要上传的文件包装类
+ */
+ private MultipartFileWrapper fileWrapper;
+ /**
+ * 要上传文件的缩略图
+ */
+ private byte[] thumbnailBytes;
+ /**
+ * 缩略图后缀,不是扩展名但包含扩展名,例如【.min.jpg】【.png】。
+ * 只能在缩略图生成前进行修改后缀中的扩展名部分。
+ * 例如当前是【.min.jpg】那么扩展名就是【jpg】,当缩略图未生成的情况下可以随意修改(扩展名必须是 thumbnailator 支持的图片格式),
+ * 一旦缩略图生成后,扩展名之外的部分可以随意改变 ,扩展名部分不能改变,除非你在 {@link UploadPretreatment#thumbnail} 方法中修改了输出格式。
+ */
+ private String thumbnailSuffix;
+ /**
+ * 文件所属对象id
+ */
+ private String objectId;
+ /**
+ * 文件所属对象类型
+ */
+ private String objectType;
+ /**
+ * 文件存储路径
+ */
+ private String path = "";
+
+ /**
+ * 保存文件名,如果不设置则自动生成
+ */
+ private String saveFilename;
+
+ /**
+ * 缩略图的保存文件名,注意此文件名不含后缀,后缀用 {@link UploadPretreatment#thumbnailSuffix} 属性控制
+ */
+ private String saveThFilename;
+
+ /**
+ * MIME 类型,如果不设置则在上传文件根据 {@link MultipartFileWrapper#getContentType()} 和文件名自动识别
+ */
+ private String contentType;
+
+ /**
+ * 缩略图 MIME 类型,如果不设置则在上传文件根据缩略图文件名自动识别
+ */
+ private String thContentType;
+
+ /**
+ * 附加属性字典
+ */
+ private Dict attr;
+
+ /**
+ * 设置文件所属对象id
+ *
+ * @param objectId 如果不是 String 类型会自动调用 toString() 方法
+ */
+ public UploadPretreatment setObjectId(Object objectId) {
+ this.objectId = objectId == null ? null : objectId.toString();
+ return this;
+ }
+
+ /**
+ * 获取文件名
+ */
+ public String getName() {
+ return fileWrapper.getName();
+ }
+
+ /**
+ * 设置文件名
+ */
+ public UploadPretreatment setName(String name) {
+ fileWrapper.setName(name);
+ return this;
+ }
+
+ /**
+ * 获取原始文件名
+ */
+ public String getOriginalFilename() {
+ return fileWrapper.getOriginalFilename();
+ }
+
+ /**
+ * 设置原始文件名
+ */
+ public UploadPretreatment setOriginalFilename(String originalFilename) {
+ fileWrapper.setOriginalFilename(originalFilename);
+ return this;
+ }
+
+ /**
+ * 获取附加属性字典
+ */
+ public Dict getAttr() {
+ if (attr == null) attr = new Dict();
+ return attr;
+ }
+
+ /**
+ * 设置附加属性
+ */
+ public UploadPretreatment putAttr(String key,Object value) {
+ getAttr().put(key,value);
+ return this;
+ }
+
+
+ /**
+ * 进行图片处理,可以进行裁剪、旋转、缩放、水印等操作
+ */
+ public UploadPretreatment image(Consumer> consumer) {
+ try (InputStream in = fileWrapper.getInputStream()) {
+ Thumbnails.Builder extends InputStream> builder = Thumbnails.of(in);
+ consumer.accept(builder);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ builder.toOutputStream(out);
+ MultipartFile mf = fileWrapper.getMultipartFile();
+ fileWrapper.setMultipartFile(new MockMultipartFile(mf.getName(),mf.getOriginalFilename(),null,out.toByteArray()));
+ return this;
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("图片处理失败!",e);
+ }
+ }
+
+ /**
+ * 缩放到指定大小
+ */
+ public UploadPretreatment image(int width,int height) {
+ return image(th -> th.size(width,height));
+ }
+
+ /**
+ * 缩放到 200*200 大小
+ */
+ public UploadPretreatment image() {
+ return image(th -> th.size(200,200));
+ }
+
+ /**
+ * 清空缩略图
+ */
+ public UploadPretreatment clearThumbnail() {
+ thumbnailBytes = null;
+ return this;
+ }
+
+
+ /**
+ * 生成缩略图并进行图片处理,如果缩略图已存在则使用已有的缩略图进行处理,
+ * 可以进行裁剪、旋转、缩放、水印等操作,默认输出图片格式通过 thumbnailSuffix 获取
+ */
+ public UploadPretreatment thumbnail(Consumer> consumer) {
+ try {
+ return thumbnail(consumer,thumbnailBytes != null ? new ByteArrayInputStream(thumbnailBytes) : fileWrapper.getInputStream());
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("生成缩略图失败!",e);
+ }
+ }
+
+ /**
+ * 通过指定 MultipartFile 生成缩略图并进行图片处理,
+ * 可以进行裁剪、旋转、缩放、水印等操作,默认输出图片格式通过 thumbnailSuffix 获取,
+ */
+ public UploadPretreatment thumbnail(Consumer> consumer,MultipartFile file) {
+ try {
+ return thumbnail(consumer,file.getInputStream());
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("生成缩略图失败!",e);
+ }
+ }
+
+ /**
+ * 通过指定 InputStream 生成缩略图并进行图片处理,
+ * 可以进行裁剪、旋转、缩放、水印等操作,默认输出图片格式通过 thumbnailSuffix 获取,
+ * 操作完成后会自动关闭 InputStream
+ */
+ public UploadPretreatment thumbnail(Consumer> consumer,InputStream in) {
+ try {
+ Thumbnails.Builder extends InputStream> builder = Thumbnails.of(in);
+ builder.outputFormat(FileNameUtil.extName(thumbnailSuffix));
+ consumer.accept(builder);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ builder.toOutputStream(out);
+ thumbnailBytes = out.toByteArray();
+ return this;
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("生成缩略图失败!",e);
+ } finally {
+ IoUtil.close(in);
+ }
+ }
+
+ /**
+ * 生成缩略图并缩放到指定大小,默认输出图片格式通过 thumbnailSuffix 获取
+ */
+ public UploadPretreatment thumbnail(int width,int height) {
+ return thumbnail(th -> th.size(width,height));
+ }
+
+ /**
+ * 生成缩略图并缩放到 200*200 大小,默认输出图片格式通过 thumbnailSuffix 获取
+ */
+ public UploadPretreatment thumbnail() {
+ return thumbnail(200,200);
+ }
+
+ /**
+ * 上传文件,成功返回文件信息,失败返回null
+ */
+ public FileInfo upload() {
+ UploadPretreatment thumbnail = null;
+ try {
+ thumbnail = thumbnail(120, 120);
+ } catch (Exception e) {
+ thumbnail = this;
+ thumbnail.setThumbnailBytes(null);
+ }
+ return fileStorageService.upload(thumbnail);
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DeleteAspectChain.java b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DeleteAspectChain.java
new file mode 100644
index 0000000..77e1d8d
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DeleteAspectChain.java
@@ -0,0 +1,37 @@
+package cn.xuyanwu.spring.file.storage.aspect;
+
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.platform.FileStorage;
+import cn.xuyanwu.spring.file.storage.recorder.FileRecorder;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Iterator;
+
+/**
+ * 删除的切面调用链
+ */
+@Getter
+@Setter
+public class DeleteAspectChain {
+
+ private DeleteAspectChainCallback callback;
+ private Iterator aspectIterator;
+
+ public DeleteAspectChain(Iterable aspects,DeleteAspectChainCallback callback) {
+ this.aspectIterator = aspects.iterator();
+ this.callback = callback;
+ }
+
+ /**
+ * 调用下一个切面
+ */
+ public boolean next(FileInfo fileInfo,FileStorage fileStorage,FileRecorder fileRecorder) {
+ if (aspectIterator.hasNext()) {//还有下一个
+ return aspectIterator.next().deleteAround(this,fileInfo,fileStorage,fileRecorder);
+ } else {
+ return callback.run(fileInfo,fileStorage,fileRecorder);
+ }
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DeleteAspectChainCallback.java b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DeleteAspectChainCallback.java
new file mode 100644
index 0000000..4786e04
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DeleteAspectChainCallback.java
@@ -0,0 +1,13 @@
+package cn.xuyanwu.spring.file.storage.aspect;
+
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.platform.FileStorage;
+import cn.xuyanwu.spring.file.storage.recorder.FileRecorder;
+
+/**
+ * 删除切面调用链结束回调
+ */
+public interface DeleteAspectChainCallback {
+ boolean run(FileInfo fileInfo,FileStorage fileStorage,FileRecorder fileRecorder);
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DownloadAspectChain.java b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DownloadAspectChain.java
new file mode 100644
index 0000000..9fe7fca
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DownloadAspectChain.java
@@ -0,0 +1,38 @@
+package cn.xuyanwu.spring.file.storage.aspect;
+
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.platform.FileStorage;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.function.Consumer;
+
+/**
+ * 下载的切面调用链
+ */
+@Getter
+@Setter
+public class DownloadAspectChain {
+
+ private DownloadAspectChainCallback callback;
+ private Iterator aspectIterator;
+
+ public DownloadAspectChain(Iterable aspects,DownloadAspectChainCallback callback) {
+ this.aspectIterator = aspects.iterator();
+ this.callback = callback;
+ }
+
+ /**
+ * 调用下一个切面
+ */
+ public void next(FileInfo fileInfo,FileStorage fileStorage,Consumer consumer) {
+ if (aspectIterator.hasNext()) {//还有下一个
+ aspectIterator.next().downloadAround(this,fileInfo,fileStorage,consumer);
+ } else {
+ callback.run(fileInfo,fileStorage,consumer);
+ }
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DownloadAspectChainCallback.java b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DownloadAspectChainCallback.java
new file mode 100644
index 0000000..b1493d7
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DownloadAspectChainCallback.java
@@ -0,0 +1,15 @@
+package cn.xuyanwu.spring.file.storage.aspect;
+
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.platform.FileStorage;
+
+import java.io.InputStream;
+import java.util.function.Consumer;
+
+/**
+ * 下载切面调用链结束回调
+ */
+public interface DownloadAspectChainCallback {
+ void run(FileInfo fileInfo,FileStorage fileStorage,Consumer consumer);
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DownloadThAspectChain.java b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DownloadThAspectChain.java
new file mode 100644
index 0000000..6fc746f
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DownloadThAspectChain.java
@@ -0,0 +1,38 @@
+package cn.xuyanwu.spring.file.storage.aspect;
+
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.platform.FileStorage;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.function.Consumer;
+
+/**
+ * 下载缩略图的切面调用链
+ */
+@Getter
+@Setter
+public class DownloadThAspectChain {
+
+ private DownloadThAspectChainCallback callback;
+ private Iterator aspectIterator;
+
+ public DownloadThAspectChain(Iterable aspects,DownloadThAspectChainCallback callback) {
+ this.aspectIterator = aspects.iterator();
+ this.callback = callback;
+ }
+
+ /**
+ * 调用下一个切面
+ */
+ public void next(FileInfo fileInfo,FileStorage fileStorage,Consumer consumer) {
+ if (aspectIterator.hasNext()) {//还有下一个
+ aspectIterator.next().downloadThAround(this,fileInfo,fileStorage,consumer);
+ } else {
+ callback.run(fileInfo,fileStorage,consumer);
+ }
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DownloadThAspectChainCallback.java b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DownloadThAspectChainCallback.java
new file mode 100644
index 0000000..91049d8
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/DownloadThAspectChainCallback.java
@@ -0,0 +1,15 @@
+package cn.xuyanwu.spring.file.storage.aspect;
+
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.platform.FileStorage;
+
+import java.io.InputStream;
+import java.util.function.Consumer;
+
+/**
+ * 下载缩略图切面调用链结束回调
+ */
+public interface DownloadThAspectChainCallback {
+ void run(FileInfo fileInfo,FileStorage fileStorage,Consumer consumer);
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/aspect/ExistsAspectChain.java b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/ExistsAspectChain.java
new file mode 100644
index 0000000..e6e4269
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/ExistsAspectChain.java
@@ -0,0 +1,36 @@
+package cn.xuyanwu.spring.file.storage.aspect;
+
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.platform.FileStorage;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Iterator;
+
+/**
+ * 文件是否存在的切面调用链
+ */
+@Getter
+@Setter
+public class ExistsAspectChain {
+
+ private ExistsAspectChainCallback callback;
+ private Iterator aspectIterator;
+
+ public ExistsAspectChain(Iterable aspects,ExistsAspectChainCallback callback) {
+ this.aspectIterator = aspects.iterator();
+ this.callback = callback;
+ }
+
+ /**
+ * 调用下一个切面
+ */
+ public boolean next(FileInfo fileInfo,FileStorage fileStorage) {
+ if (aspectIterator.hasNext()) {//还有下一个
+ return aspectIterator.next().existsAround(this,fileInfo,fileStorage);
+ } else {
+ return callback.run(fileInfo,fileStorage);
+ }
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/aspect/ExistsAspectChainCallback.java b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/ExistsAspectChainCallback.java
new file mode 100644
index 0000000..65c5b32
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/ExistsAspectChainCallback.java
@@ -0,0 +1,12 @@
+package cn.xuyanwu.spring.file.storage.aspect;
+
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.platform.FileStorage;
+
+/**
+ * 文件是否存在切面调用链结束回调
+ */
+public interface ExistsAspectChainCallback {
+ boolean run(FileInfo fileInfo,FileStorage fileStorage);
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/aspect/FileStorageAspect.java b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/FileStorageAspect.java
new file mode 100644
index 0000000..941b9fc
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/FileStorageAspect.java
@@ -0,0 +1,53 @@
+package cn.xuyanwu.spring.file.storage.aspect;
+
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.platform.FileStorage;
+import cn.xuyanwu.spring.file.storage.recorder.FileRecorder;
+
+import java.io.InputStream;
+import java.util.function.Consumer;
+
+/**
+ * 文件服务切面接口,用来干预文件上传,删除等
+ */
+public interface FileStorageAspect {
+
+
+ /**
+ * 上传,成功返回文件信息,失败返回 null
+ */
+ default FileInfo uploadAround(UploadAspectChain chain,FileInfo fileInfo,UploadPretreatment pre,FileStorage fileStorage,FileRecorder fileRecorder) {
+ return chain.next(fileInfo,pre,fileStorage,fileRecorder);
+ }
+
+
+ /**
+ * 删除文件,成功返回 true
+ */
+ default boolean deleteAround(DeleteAspectChain chain,FileInfo fileInfo,FileStorage fileStorage,FileRecorder fileRecorder) {
+ return chain.next(fileInfo,fileStorage,fileRecorder);
+ }
+
+ /**
+ * 文件是否存在,成功返回文件内容
+ */
+ default boolean existsAround(ExistsAspectChain chain,FileInfo fileInfo,FileStorage fileStorage) {
+ return chain.next(fileInfo,fileStorage);
+ }
+
+ /**
+ * 下载文件,成功返回文件内容
+ */
+ default void downloadAround(DownloadAspectChain chain,FileInfo fileInfo,FileStorage fileStorage,Consumer consumer) {
+ chain.next(fileInfo,fileStorage,consumer);
+ }
+
+ /**
+ * 下载缩略图文件,成功返回文件内容
+ */
+ default void downloadThAround(DownloadThAspectChain chain,FileInfo fileInfo,FileStorage fileStorage,Consumer consumer) {
+ chain.next(fileInfo,fileStorage,consumer);
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/aspect/UploadAspectChain.java b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/UploadAspectChain.java
new file mode 100644
index 0000000..d66b885
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/UploadAspectChain.java
@@ -0,0 +1,38 @@
+package cn.xuyanwu.spring.file.storage.aspect;
+
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.platform.FileStorage;
+import cn.xuyanwu.spring.file.storage.recorder.FileRecorder;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Iterator;
+
+/**
+ * 上传的切面调用链
+ */
+@Getter
+@Setter
+public class UploadAspectChain {
+
+ private UploadAspectChainCallback callback;
+ private Iterator aspectIterator;
+
+ public UploadAspectChain(Iterable aspects,UploadAspectChainCallback callback) {
+ this.aspectIterator = aspects.iterator();
+ this.callback = callback;
+ }
+
+ /**
+ * 调用下一个切面
+ */
+ public FileInfo next(FileInfo fileInfo,UploadPretreatment pre,FileStorage fileStorage,FileRecorder fileRecorder) {
+ if (aspectIterator.hasNext()) {//还有下一个
+ return aspectIterator.next().uploadAround(this,fileInfo,pre,fileStorage,fileRecorder);
+ } else {
+ return callback.run(fileInfo,pre,fileStorage,fileRecorder);
+ }
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/aspect/UploadAspectChainCallback.java b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/UploadAspectChainCallback.java
new file mode 100644
index 0000000..a8fa9b7
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/aspect/UploadAspectChainCallback.java
@@ -0,0 +1,14 @@
+package cn.xuyanwu.spring.file.storage.aspect;
+
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.platform.FileStorage;
+import cn.xuyanwu.spring.file.storage.recorder.FileRecorder;
+
+/**
+ * 上传切面调用链结束回调
+ */
+public interface UploadAspectChainCallback {
+ FileInfo run(FileInfo fileInfo,UploadPretreatment pre,FileStorage fileStorage,FileRecorder fileRecorder);
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/exception/FileStorageRuntimeException.java b/src/main/java/cn/xuyanwu/spring/file/storage/exception/FileStorageRuntimeException.java
new file mode 100644
index 0000000..bd8d2d1
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/exception/FileStorageRuntimeException.java
@@ -0,0 +1,26 @@
+package cn.xuyanwu.spring.file.storage.exception;
+
+/**
+ * FileStorage 运行时异常
+ */
+public class FileStorageRuntimeException extends RuntimeException {
+ public FileStorageRuntimeException() {
+ }
+
+ public FileStorageRuntimeException(String message) {
+ super(message);
+ }
+
+ public FileStorageRuntimeException(String message,Throwable cause) {
+ super(message,cause);
+ }
+
+ public FileStorageRuntimeException(Throwable cause) {
+ super(cause);
+ }
+
+ public FileStorageRuntimeException(String message,Throwable cause,boolean enableSuppression,boolean writableStackTrace) {
+ super(message,cause,enableSuppression,writableStackTrace);
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/platform/AliyunOssFileStorage.java b/src/main/java/cn/xuyanwu/spring/file/storage/platform/AliyunOssFileStorage.java
new file mode 100644
index 0000000..d4da685
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/platform/AliyunOssFileStorage.java
@@ -0,0 +1,189 @@
+package cn.xuyanwu.spring.file.storage.platform;
+
+import cn.hutool.core.util.StrUtil;
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import com.aliyun.oss.model.*;
+import com.yunzhupaas.model.FileListVO;
+import com.yunzhupaas.model.FileModel;
+import com.yunzhupaas.util.DateUtil;
+import com.yunzhupaas.util.FileUtil;
+import com.yunzhupaas.util.JsonUtil;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * 阿里云 OSS 存储
+ */
+@Getter
+@Setter
+public class AliyunOssFileStorage implements FileStorage {
+
+ /* 存储平台 */
+ private String platform;
+ private String accessKey;
+ private String secretKey;
+ private String endPoint;
+ private String bucketName;
+ private String domain;
+ private String basePath;
+ private OSS client;
+
+ /**
+ * 单例模式运行,不需要每次使用完再销毁了
+ */
+ public OSS getClient() {
+ if (client == null) {
+ client = new OSSClientBuilder().build(endPoint, accessKey, secretKey);
+ }
+ return client;
+ }
+
+ /**
+ * 仅在移除这个存储平台时调用
+ */
+ @Override
+ public void close() {
+ if (client != null) {
+ client.shutdown();
+ client = null;
+ }
+ }
+
+ @Override
+ public boolean save(FileInfo fileInfo, UploadPretreatment pre) {
+ String newFileKey = basePath + fileInfo.getPath() + fileInfo.getFilename();
+ fileInfo.setBasePath(basePath);
+ fileInfo.setUrl(domain + bucketName + "/" + newFileKey);
+
+ OSS client = getClient();
+ try (InputStream in = pre.getFileWrapper().getInputStream()) {
+ ObjectMetadata metadata = new ObjectMetadata();
+ metadata.setContentLength(fileInfo.getSize());
+ metadata.setContentType(fileInfo.getContentType());
+ client.putObject(bucketName, newFileKey, in, metadata);
+
+ byte[] thumbnailBytes = pre.getThumbnailBytes();
+ if (thumbnailBytes != null) { // 上传缩略图
+ String newThFileKey = basePath + fileInfo.getPath() + fileInfo.getThFilename();
+ fileInfo.setThUrl(domain + newThFileKey);
+ ObjectMetadata thMetadata = new ObjectMetadata();
+ thMetadata.setContentLength(thumbnailBytes.length);
+ thMetadata.setContentType(fileInfo.getThContentType());
+ client.putObject(bucketName, newThFileKey, new ByteArrayInputStream(thumbnailBytes), thMetadata);
+ }
+
+ return true;
+ } catch (IOException e) {
+ client.deleteObject(bucketName, newFileKey);
+ throw new FileStorageRuntimeException(
+ "文件上传失败!platform:" + platform + ",filename:" + fileInfo.getOriginalFilename(), e);
+ }
+ }
+
+ @Override
+ public boolean delete(FileInfo fileInfo) {
+ OSS client = getClient();
+ if (fileInfo.getThFilename() != null) { // 删除缩略图
+ client.deleteObject(bucketName, fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename());
+ }
+ client.deleteObject(bucketName, fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ return true;
+ }
+
+ @Override
+ public boolean exists(FileInfo fileInfo) {
+ return getClient().doesObjectExist(bucketName,
+ fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ }
+
+ @Override
+ public void download(FileInfo fileInfo, Consumer consumer) {
+ OSSObject object = getClient().getObject(bucketName,
+ fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ try (InputStream in = object.getObjectContent()) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("文件下载失败!platform:" + fileInfo, e);
+ }
+
+ }
+
+ @Override
+ public void downloadTh(FileInfo fileInfo, Consumer consumer) {
+ if (StrUtil.isBlank(fileInfo.getThFilename())) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败,文件不存在!fileInfo:" + fileInfo);
+ }
+ OSSObject object = getClient().getObject(bucketName,
+ fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename());
+ try (InputStream in = object.getObjectContent()) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败!fileInfo:" + fileInfo, e);
+ }
+ }
+
+ @Override
+ public List getFileList(String folderName) {
+ List list = new ArrayList<>();
+ try {
+ ObjectListing objectListing = null;
+ ListObjectsRequest listObjectsRequest = new ListObjectsRequest(bucketName);
+ listObjectsRequest.setPrefix(this.getBasePath() + folderName);
+ objectListing = getClient().listObjects(listObjectsRequest);
+ List sums = objectListing.getObjectSummaries();
+ for (OSSObjectSummary result : sums) {
+ list.add(result);
+ }
+ } catch (Exception e) {
+ throw new FileStorageRuntimeException("文件获取失败!platform:" + platform, e);
+ }
+ return list;
+ }
+
+ @Override
+ public List conversionList(String folderName) {
+ List fileList = getFileList(folderName);
+ List listVOS = new ArrayList<>(fileList.size());
+ if (fileList.size() > 0 && fileList.get(0) instanceof FileModel) {
+ return JsonUtil.getJsonToList(fileList, FileListVO.class);
+ }
+ for (int i = 0; i < fileList.size(); i++) {
+ FileListVO fileListVO = new FileListVO();
+ fileListVO.setFileId(i + "");
+ // 阿里云
+ OSSObjectSummary summary = (OSSObjectSummary) fileList.get(i);
+ String objectName = summary.getKey().replace(this.getBasePath() + folderName + "/", "");
+ fileListVO.setFileName(objectName);
+ fileListVO.setFileType(FileUtil.getFileType(objectName));
+ fileListVO.setFileSize(FileUtil.getSize(String.valueOf(summary.getSize())));
+ fileListVO.setFileTime(DateUtil.dateFormat(summary.getLastModified()));
+ listVOS.add(fileListVO);
+ }
+ return listVOS;
+ }
+
+ @Override
+ public void downLocal(String folderName, String filePath, String objectName) {
+ // 判断存储桶是否存在
+ try {
+ getClient().getObject(new GetObjectRequest(bucketName, this.getBasePath() + folderName + objectName),
+ new File(filePath + objectName));
+ } catch (Exception e) {
+ throw new FileStorageRuntimeException(
+ "文件下载失败!platform:" + platform + ",下载路径:" + filePath + ",文件夹名称:" + folderName + ",文件名:" + objectName,
+ e);
+ }
+ }
+}
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/platform/AwsS3FileStorage.java b/src/main/java/cn/xuyanwu/spring/file/storage/platform/AwsS3FileStorage.java
new file mode 100644
index 0000000..41aea15
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/platform/AwsS3FileStorage.java
@@ -0,0 +1,137 @@
+package cn.xuyanwu.spring.file.storage.platform;
+
+import cn.hutool.core.util.StrUtil;
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.client.builder.AwsClientBuilder;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3ClientBuilder;
+import com.amazonaws.services.s3.model.ObjectMetadata;
+import com.amazonaws.services.s3.model.S3Object;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.function.Consumer;
+
+/**
+ * AWS S3 存储
+ */
+@Getter
+@Setter
+public class AwsS3FileStorage implements FileStorage {
+
+ /* 存储平台 */
+ private String platform;
+ private String accessKey;
+ private String secretKey;
+ private String region;
+ private String endPoint;
+ private String bucketName;
+ private String domain;
+ private String basePath;
+ private AmazonS3 client;
+
+ /**
+ * 单例模式运行,不需要每次使用完再销毁了
+ */
+ public AmazonS3 getClient() {
+ if (client == null) {
+ AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard()
+ .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey,secretKey)));
+ if (StrUtil.isNotBlank(endPoint)) {
+ builder.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endPoint,region));
+ } else if (StrUtil.isNotBlank(region)) {
+ builder.withRegion(region);
+ }
+ client = builder.build();
+ }
+ return client;
+ }
+
+ /**
+ * 仅在移除这个存储平台时调用
+ */
+ @Override
+ public void close() {
+ if (client != null) {
+ client.shutdown();
+ client = null;
+ }
+ }
+
+ @Override
+ public boolean save(FileInfo fileInfo,UploadPretreatment pre) {
+ String newFileKey = basePath + fileInfo.getPath() + fileInfo.getFilename();
+ fileInfo.setBasePath(basePath);
+ fileInfo.setUrl(domain + newFileKey);
+
+ AmazonS3 client = getClient();
+ try (InputStream in = pre.getFileWrapper().getInputStream()) {
+ ObjectMetadata metadata = new ObjectMetadata();
+ metadata.setContentLength(fileInfo.getSize());
+ metadata.setContentType(fileInfo.getContentType());
+ client.putObject(bucketName,newFileKey,in,metadata);
+
+ byte[] thumbnailBytes = pre.getThumbnailBytes();
+ if (thumbnailBytes != null) { //上传缩略图
+ String newThFileKey = basePath + fileInfo.getPath() + fileInfo.getThFilename();
+ fileInfo.setThUrl(domain + newThFileKey);
+ ObjectMetadata thMetadata = new ObjectMetadata();
+ thMetadata.setContentLength(thumbnailBytes.length);
+ thMetadata.setContentType(fileInfo.getThContentType());
+ client.putObject(bucketName,newThFileKey,new ByteArrayInputStream(thumbnailBytes),thMetadata);
+ }
+
+ return true;
+ } catch (IOException e) {
+ client.deleteObject(bucketName,newFileKey);
+ throw new FileStorageRuntimeException("文件上传失败!platform:" + platform + ",filename:" + fileInfo.getOriginalFilename(),e);
+ }
+ }
+
+ @Override
+ public boolean delete(FileInfo fileInfo) {
+ AmazonS3 client = getClient();
+ if (fileInfo.getThFilename() != null) { //删除缩略图
+ client.deleteObject(bucketName,fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename());
+ }
+ client.deleteObject(bucketName,fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ return true;
+ }
+
+
+ @Override
+ public boolean exists(FileInfo fileInfo) {
+ return getClient().doesObjectExist(bucketName,fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ }
+
+ @Override
+ public void download(FileInfo fileInfo,Consumer consumer) {
+ S3Object object = getClient().getObject(bucketName,fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ try (InputStream in = object.getObjectContent()) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("文件下载失败!platform:" + fileInfo,e);
+ }
+ }
+
+ @Override
+ public void downloadTh(FileInfo fileInfo,Consumer consumer) {
+ if (StrUtil.isBlank(fileInfo.getThFilename())) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败,文件不存在!fileInfo:" + fileInfo);
+ }
+ S3Object object = getClient().getObject(bucketName,fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename());
+ try (InputStream in = object.getObjectContent()) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败!fileInfo:" + fileInfo,e);
+ }
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/platform/BaiduBosFileStorage.java b/src/main/java/cn/xuyanwu/spring/file/storage/platform/BaiduBosFileStorage.java
new file mode 100644
index 0000000..604ed50
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/platform/BaiduBosFileStorage.java
@@ -0,0 +1,132 @@
+package cn.xuyanwu.spring.file.storage.platform;
+
+import cn.hutool.core.util.StrUtil;
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import com.baidubce.Protocol;
+import com.baidubce.auth.DefaultBceCredentials;
+import com.baidubce.services.bos.BosClient;
+import com.baidubce.services.bos.BosClientConfiguration;
+import com.baidubce.services.bos.model.BosObject;
+import com.baidubce.services.bos.model.ObjectMetadata;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.function.Consumer;
+
+/**
+ * 百度云 BOS 存储
+ */
+@Getter
+@Setter
+public class BaiduBosFileStorage implements FileStorage {
+
+ /* 存储平台 */
+ private String platform;
+ private String accessKey;
+ private String secretKey;
+ private String endPoint;
+ private String bucketName;
+ private String domain;
+ private String basePath;
+ private BosClient client;
+
+ /**
+ * 单例模式运行,不需要每次使用完再销毁了
+ */
+ public BosClient getClient() {
+ if (client == null) {
+ BosClientConfiguration config = new BosClientConfiguration();
+ config.setCredentials(new DefaultBceCredentials(accessKey,secretKey));
+ config.setEndpoint(endPoint);
+ config.setProtocol(Protocol.HTTPS);
+ client = new BosClient(config);
+ }
+ return client;
+ }
+
+ /**
+ * 仅在移除这个存储平台时调用
+ */
+ @Override
+ public void close() {
+ if (client != null) {
+ client.shutdown();
+ client = null;
+ }
+ }
+
+ @Override
+ public boolean save(FileInfo fileInfo,UploadPretreatment pre) {
+ String newFileKey = basePath + fileInfo.getPath() + fileInfo.getFilename();
+ fileInfo.setBasePath(basePath);
+ fileInfo.setUrl(domain + newFileKey);
+
+ BosClient client = getClient();
+ try (InputStream in = pre.getFileWrapper().getInputStream()) {
+ ObjectMetadata metadata = new ObjectMetadata();
+ metadata.setContentLength(fileInfo.getSize());
+ metadata.setContentType(fileInfo.getContentType());
+ client.putObject(bucketName,newFileKey,in,metadata);
+
+ byte[] thumbnailBytes = pre.getThumbnailBytes();
+ if (thumbnailBytes != null) { //上传缩略图
+ String newThFileKey = basePath + fileInfo.getPath() + fileInfo.getThFilename();
+ fileInfo.setThUrl(domain + newThFileKey);
+ ObjectMetadata thMetadata = new ObjectMetadata();
+ thMetadata.setContentLength(thumbnailBytes.length);
+ thMetadata.setContentType(fileInfo.getThContentType());
+ client.putObject(bucketName,newThFileKey,new ByteArrayInputStream(thumbnailBytes),thMetadata);
+ }
+
+ return true;
+ } catch (IOException e) {
+ client.deleteObject(bucketName,newFileKey);
+ throw new FileStorageRuntimeException("文件上传失败!platform:" + platform + ",filename:" + fileInfo.getOriginalFilename(),e);
+ }
+ }
+
+ @Override
+ public boolean delete(FileInfo fileInfo) {
+ BosClient client = getClient();
+ if (fileInfo.getThFilename() != null) { //删除缩略图
+ client.deleteObject(bucketName,fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename());
+ }
+ client.deleteObject(bucketName,fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ return true;
+ }
+
+
+ @Override
+ public boolean exists(FileInfo fileInfo) {
+ return getClient().doesObjectExist(bucketName,fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ }
+
+ @Override
+ public void download(FileInfo fileInfo,Consumer consumer) {
+ BosObject object = getClient().getObject(bucketName,fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ try (InputStream in = object.getObjectContent()) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("文件下载失败!platform:" + fileInfo,e);
+ }
+ }
+
+ @Override
+ public void downloadTh(FileInfo fileInfo,Consumer consumer) {
+ if (StrUtil.isBlank(fileInfo.getThFilename())) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败,文件不存在!fileInfo:" + fileInfo);
+ }
+ BosObject object = getClient().getObject(bucketName,fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename());
+ try (InputStream in = object.getObjectContent()) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败!fileInfo:" + fileInfo,e);
+ }
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/platform/FileStorage.java b/src/main/java/cn/xuyanwu/spring/file/storage/platform/FileStorage.java
new file mode 100644
index 0000000..edfe635
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/platform/FileStorage.java
@@ -0,0 +1,112 @@
+package cn.xuyanwu.spring.file.storage.platform;
+
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.FileStorageProperties;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import com.yunzhupaas.model.FileListVO;
+import com.yunzhupaas.util.context.SpringContext;
+
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * 文件存储接口,对应各个平台
+ */
+public interface FileStorage extends AutoCloseable {
+
+ /**
+ * 获取平台
+ */
+ String getPlatform();
+
+ /**
+ * 获取地址
+ */
+ String getBasePath();
+
+ /**
+ * 获取地址
+ */
+ String getDomain();
+
+ /**
+ * 获取命名空间
+ */
+ default String getBucketName() {
+ return "";
+ }
+
+ /**
+ * 设置平台
+ */
+ void setPlatform(String platform);
+
+ /**
+ * 保存文件
+ */
+ boolean save(FileInfo fileInfo, UploadPretreatment pre);
+
+ /**
+ * 删除文件
+ */
+ boolean delete(FileInfo fileInfo);
+
+ /**
+ * 文件是否存在
+ */
+ boolean exists(FileInfo fileInfo);
+
+ /**
+ * 下载文件
+ */
+ void download(FileInfo fileInfo, Consumer consumer);
+
+ /**
+ * 下载缩略图文件
+ */
+ void downloadTh(FileInfo fileInfo, Consumer consumer);
+
+ /**
+ * 释放相关资源
+ */
+ void close();
+
+ /**
+ * 获取本地储存路径
+ */
+ default String getLocalPath() {
+ FileStorageProperties fileStorageProperties = SpringContext.getBean(FileStorageProperties.class);
+ if (fileStorageProperties.getLocalPlus().size() < 1) {
+ return null;
+ }
+ String storagePath = fileStorageProperties.getLocalPlus().get(0).getBasePath();
+ return storagePath;
+ }
+
+ /**
+ * 获取文件列表
+ */
+ default List getFileList(String folderName) {
+ return Collections.EMPTY_LIST;
+ }
+
+ /**
+ * 返回值统一泛型
+ */
+ default List conversionList(String folderName) {
+ return Collections.EMPTY_LIST;
+ }
+
+ /**
+ * 下载到本地
+ *
+ * @param folderName 文件夹名
+ * @param filePath 下载到本地文件路径
+ * @param objectName 文件名
+ */
+ default void downLocal(String folderName, String filePath, String objectName) {
+ }
+
+}
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/platform/FtpFileStorage.java b/src/main/java/cn/xuyanwu/spring/file/storage/platform/FtpFileStorage.java
new file mode 100644
index 0000000..8ca5131
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/platform/FtpFileStorage.java
@@ -0,0 +1,165 @@
+package cn.xuyanwu.spring.file.storage.platform;
+
+import cn.hutool.core.io.IORuntimeException;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.ftp.Ftp;
+import cn.hutool.extra.ftp.FtpConfig;
+import cn.hutool.extra.ftp.FtpMode;
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.function.Consumer;
+
+/**
+ * FTP 存储
+ */
+@Getter
+@Setter
+public class FtpFileStorage implements FileStorage {
+
+ /* 主机 */
+ private String host;
+ /* 端口,默认21 */
+ private int port;
+ /* 用户名,默认 anonymous(匿名) */
+ private String user;
+ /* 密码,默认空 */
+ private String password;
+ /* 编码,默认UTF-8 */
+ private Charset charset;
+ /* 连接超时时长,单位毫秒,默认10秒 {@link org.apache.commons.net.SocketClient#setConnectTimeout(int)} */
+ private long connectionTimeout;
+ /* Socket连接超时时长,单位毫秒,默认10秒 {@link org.apache.commons.net.SocketClient#setSoTimeout(int)} */
+ private long soTimeout;
+ /* 设置服务器语言,默认空,{@link org.apache.commons.net.ftp.FTPClientConfig#setServerLanguageCode(String)} */
+ private String serverLanguageCode;
+ /**
+ * 服务器标识,默认空,{@link org.apache.commons.net.ftp.FTPClientConfig#FTPClientConfig(String)}
+ * 例如:org.apache.commons.net.ftp.FTPClientConfig.SYST_NT
+ */
+ private String systemKey;
+ /* 是否主动模式,默认被动模式 */
+ private Boolean isActive = false;
+ /* 存储平台 */
+ private String platform;
+ private String domain;
+ private String basePath;
+ private String storagePath;
+
+ /**
+ * 不支持单例模式运行,每次使用完了需要销毁
+ */
+ public Ftp getClient() {
+ FtpConfig config = FtpConfig.create().setHost(host).setPort(port).setUser(user).setPassword(password).setCharset(charset)
+ .setConnectionTimeout(connectionTimeout).setSoTimeout(soTimeout).setServerLanguageCode(serverLanguageCode)
+ .setSystemKey(systemKey);
+ return new Ftp(config,isActive ? FtpMode.Active : FtpMode.Passive);
+ }
+
+
+ @Override
+ public void close() {
+ }
+
+ /**
+ * 获取远程绝对路径
+ */
+ public String getAbsolutePath(String path) {
+ return storagePath + path;
+ }
+
+ @Override
+ public boolean save(FileInfo fileInfo,UploadPretreatment pre) {
+ String newFileKey = basePath + fileInfo.getPath() + fileInfo.getFilename();
+ fileInfo.setBasePath(basePath);
+ fileInfo.setUrl(domain + newFileKey);
+
+ Ftp client = getClient();
+ try (InputStream in = pre.getFileWrapper().getInputStream()) {
+ client.upload(getAbsolutePath(basePath + fileInfo.getPath()),fileInfo.getFilename(),in);
+
+ byte[] thumbnailBytes = pre.getThumbnailBytes();
+ if (thumbnailBytes != null) { //上传缩略图
+ String newThFileKey = basePath + fileInfo.getPath() + fileInfo.getThFilename();
+ fileInfo.setThUrl(domain + newThFileKey);
+ client.upload(getAbsolutePath(basePath + fileInfo.getPath()),fileInfo.getThFilename(),new ByteArrayInputStream(thumbnailBytes));
+ }
+
+ return true;
+ } catch (IOException | IORuntimeException e) {
+ try {
+ client.delFile(getAbsolutePath(newFileKey));
+ } catch (IORuntimeException ignored) {
+ }
+ throw new FileStorageRuntimeException("文件上传失败!platform:" + platform + ",filename:" + fileInfo.getOriginalFilename(),e);
+ } finally {
+ IoUtil.close(client);
+ }
+ }
+
+ @Override
+ public boolean delete(FileInfo fileInfo) {
+ try (Ftp client = getClient()) {
+ if (fileInfo.getThFilename() != null) { //删除缩略图
+ client.delFile(getAbsolutePath(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename()));
+ }
+ client.delFile(getAbsolutePath(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename()));
+ return true;
+ } catch (IOException | IORuntimeException e) {
+ throw new FileStorageRuntimeException("文件删除失败!fileInfo:" + fileInfo,e);
+ }
+ }
+
+
+ @Override
+ public boolean exists(FileInfo fileInfo) {
+ try (Ftp client = getClient()) {
+ return client.existFile(getAbsolutePath(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename()));
+ } catch (IOException | IORuntimeException e) {
+ throw new FileStorageRuntimeException("查询文件是否存在失败!fileInfo:" + fileInfo,e);
+ }
+ }
+
+ @Override
+ public void download(FileInfo fileInfo,Consumer consumer) {
+ try (Ftp client = getClient()) {
+ client.cd(getAbsolutePath(fileInfo.getBasePath() + fileInfo.getPath()));
+ try (InputStream in = client.getClient().retrieveFileStream(fileInfo.getFilename())) {
+ if (in == null) {
+ throw new FileStorageRuntimeException("文件下载失败,文件不存在!platform:" + fileInfo);
+ }
+ consumer.accept(in);
+ }
+ } catch (IOException | IORuntimeException e) {
+ throw new FileStorageRuntimeException("文件下载失败!platform:" + fileInfo,e);
+ }
+ }
+
+ @Override
+ public void downloadTh(FileInfo fileInfo,Consumer consumer) {
+ if (StrUtil.isBlank(fileInfo.getThFilename())) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败,文件不存在!fileInfo:" + fileInfo);
+ }
+
+ try (Ftp client = getClient()) {
+ client.cd(getAbsolutePath(fileInfo.getBasePath() + fileInfo.getPath()));
+ try (InputStream in = client.getClient().retrieveFileStream(fileInfo.getThFilename())) {
+ if (in == null) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败,文件不存在!platform:" + fileInfo);
+ }
+ consumer.accept(in);
+ }
+ } catch (IOException | IORuntimeException e) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败!fileInfo:" + fileInfo,e);
+ }
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/platform/HuaweiObsFileStorage.java b/src/main/java/cn/xuyanwu/spring/file/storage/platform/HuaweiObsFileStorage.java
new file mode 100644
index 0000000..50e575c
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/platform/HuaweiObsFileStorage.java
@@ -0,0 +1,184 @@
+package cn.xuyanwu.spring.file.storage.platform;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import com.obs.services.ObsClient;
+import com.obs.services.model.ListObjectsRequest;
+import com.obs.services.model.ObjectListing;
+import com.obs.services.model.ObjectMetadata;
+import com.obs.services.model.ObsObject;
+import com.yunzhupaas.model.FileListVO;
+import com.yunzhupaas.model.FileModel;
+import com.yunzhupaas.util.DateUtil;
+import com.yunzhupaas.util.FileUtil;
+import com.yunzhupaas.util.JsonUtil;
+import com.yunzhupaas.util.StringUtil;
+import lombok.Cleanup;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * 华为云 OBS 存储
+ */
+@Getter
+@Setter
+public class HuaweiObsFileStorage implements FileStorage {
+
+ /* 存储平台 */
+ private String platform;
+ private String accessKey;
+ private String secretKey;
+ private String endPoint;
+ private String bucketName;
+ private String domain;
+ private String basePath;
+ private ObsClient client;
+
+ /**
+ * 单例模式运行,不需要每次使用完再销毁了
+ */
+ public ObsClient getClient() {
+ if (client == null) {
+ client = new ObsClient(accessKey, secretKey, endPoint);
+ }
+ return client;
+ }
+
+ /**
+ * 仅在移除这个存储平台时调用
+ */
+ @Override
+ public void close() {
+ IoUtil.close(client);
+ }
+
+ @Override
+ public boolean save(FileInfo fileInfo, UploadPretreatment pre) {
+ String newFileKey = basePath + pre.getPath() + fileInfo.getFilename();
+ fileInfo.setBasePath(basePath);
+ fileInfo.setUrl(domain + newFileKey);
+
+ ObsClient client = getClient();
+ try (InputStream in = pre.getFileWrapper().getInputStream()) {
+ ObjectMetadata metadata = new ObjectMetadata();
+ metadata.setContentLength(fileInfo.getSize());
+ metadata.setContentType(fileInfo.getContentType());
+ client.putObject(bucketName, newFileKey, in, metadata);
+
+ byte[] thumbnailBytes = pre.getThumbnailBytes();
+ if (thumbnailBytes != null) { // 上传缩略图
+ String newThFileKey = basePath + fileInfo.getPath() + fileInfo.getThFilename();
+ fileInfo.setThUrl(domain + newThFileKey);
+ ObjectMetadata thMetadata = new ObjectMetadata();
+ thMetadata.setContentLength((long) thumbnailBytes.length);
+ thMetadata.setContentType(fileInfo.getThContentType());
+ client.putObject(bucketName, newThFileKey, new ByteArrayInputStream(thumbnailBytes), thMetadata);
+ }
+
+ return true;
+ } catch (IOException e) {
+ client.deleteObject(bucketName, newFileKey);
+ throw new FileStorageRuntimeException(
+ "文件上传失败!platform:" + platform + ",filename:" + fileInfo.getOriginalFilename(), e);
+ }
+ }
+
+ @Override
+ public boolean delete(FileInfo fileInfo) {
+ ObsClient client = getClient();
+ if (fileInfo.getThFilename() != null) { // 删除缩略图
+ client.deleteObject(bucketName, fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename());
+ }
+ client.deleteObject(bucketName, fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ return true;
+ }
+
+ @Override
+ public boolean exists(FileInfo fileInfo) {
+ return getClient().doesObjectExist(bucketName,
+ fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ }
+
+ @Override
+ public void download(FileInfo fileInfo, Consumer consumer) {
+ ObsObject object = getClient().getObject(bucketName,
+ fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ try (InputStream in = object.getObjectContent()) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("文件下载失败!platform:" + fileInfo, e);
+ }
+ }
+
+ @Override
+ public void downloadTh(FileInfo fileInfo, Consumer consumer) {
+ if (StrUtil.isBlank(fileInfo.getThFilename())) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败,文件不存在!fileInfo:" + fileInfo);
+ }
+ ObsObject object = getClient().getObject(bucketName,
+ fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename());
+ try (InputStream in = object.getObjectContent()) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败!fileInfo:" + fileInfo, e);
+ }
+ }
+
+ @Override
+ public void downLocal(String folderName, String filePath, String objectName) {
+ try {
+ ObsObject obsObject = getClient().getObject(bucketName, this.getBasePath() + folderName + objectName + "/");
+ @Cleanup
+ InputStream stream = obsObject.getObjectContent();
+ FileUtil.write(stream, filePath, objectName);
+ } catch (Exception e) {
+ throw new FileStorageRuntimeException("文件获取失败!platform:" + platform, e);
+ }
+ }
+
+ @Override
+ public List getFileList(String folderName) {
+ ListObjectsRequest listObjectsRequest = new ListObjectsRequest(bucketName);
+ listObjectsRequest.setPrefix(this.getBasePath() + folderName);
+ ObjectListing objectListing = getClient().listObjects(listObjectsRequest);
+ return objectListing.getObjects() != null ? objectListing.getObjects() : Collections.EMPTY_LIST;
+ }
+
+ @Override
+ public List conversionList(String folderName) {
+ List fileList = getFileList(folderName);
+ List listVOS = new ArrayList<>(fileList.size());
+ if (fileList.size() > 0 && fileList.get(0) instanceof FileModel) {
+ return JsonUtil.getJsonToList(fileList, FileListVO.class);
+ }
+ for (int i = 0; i < fileList.size(); i++) {
+ FileListVO fileListVO = new FileListVO();
+ fileListVO.setFileId(i + "");
+ ObsObject obsObject = (ObsObject) fileList.get(i);
+ String objectName = obsObject.getObjectKey();
+ if (StringUtil.isEmpty(objectName)
+ // || objectName.split("/").length <= 1 || objectName.split("/").length > 2
+ ) {
+ continue;
+ }
+ fileListVO.setFileName(objectName);
+ fileListVO.setFileType(FileUtil.getFileType(objectName));
+ fileListVO.setFileSize(FileUtil.getSize(String.valueOf(obsObject.getMetadata().getContentLength())));
+ fileListVO.setFileTime(DateUtil.dateFormat(obsObject.getMetadata().getLastModified()));
+ listVOS.add(fileListVO);
+ }
+ return listVOS;
+ }
+
+}
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/platform/LocalFileStorage.java b/src/main/java/cn/xuyanwu/spring/file/storage/platform/LocalFileStorage.java
new file mode 100644
index 0000000..388482a
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/platform/LocalFileStorage.java
@@ -0,0 +1,92 @@
+package cn.xuyanwu.spring.file.storage.platform;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.function.Consumer;
+
+/**
+ * 本地文件存储
+ */
+@Getter
+@Setter
+public class LocalFileStorage implements FileStorage {
+
+ /* 本地存储路径*/
+ private String basePath;
+ /* 存储平台 */
+ private String platform;
+ /* 访问域名 */
+ private String domain;
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public boolean save(FileInfo fileInfo,UploadPretreatment pre) {
+ String path = fileInfo.getPath();
+
+ File newFile = FileUtil.touch(basePath + path,fileInfo.getFilename());
+ fileInfo.setBasePath(basePath);
+ fileInfo.setUrl(domain + path + fileInfo.getFilename());
+
+ try {
+ pre.getFileWrapper().transferTo(newFile);
+
+ byte[] thumbnailBytes = pre.getThumbnailBytes();
+ if (thumbnailBytes != null) { //上传缩略图
+ fileInfo.setThUrl(domain + path + fileInfo.getThFilename());
+ FileUtil.writeBytes(thumbnailBytes,basePath + path + fileInfo.getThFilename());
+ }
+ return true;
+ } catch (IOException e) {
+ FileUtil.del(newFile);
+ throw new FileStorageRuntimeException("文件上传失败!platform:" + platform + ",filename:" + fileInfo.getOriginalFilename(),e);
+ }
+ }
+
+ @Override
+ public boolean delete(FileInfo fileInfo) {
+ if (fileInfo.getThFilename() != null) { //删除缩略图
+ FileUtil.del(new File(fileInfo.getBasePath() + fileInfo.getPath(),fileInfo.getThFilename()));
+ }
+ return FileUtil.del(new File(fileInfo.getBasePath() + fileInfo.getPath(),fileInfo.getFilename()));
+ }
+
+
+ @Override
+ public boolean exists(FileInfo fileInfo) {
+ return new File(fileInfo.getBasePath() + fileInfo.getPath(),fileInfo.getFilename()).exists();
+ }
+
+ @Override
+ public void download(FileInfo fileInfo,Consumer consumer) {
+ try (InputStream in = FileUtil.getInputStream(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename())) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("文件下载失败!platform:" + fileInfo,e);
+ }
+ }
+
+ @Override
+ public void downloadTh(FileInfo fileInfo,Consumer consumer) {
+ if (StrUtil.isBlank(fileInfo.getThFilename())) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败,文件不存在!fileInfo:" + fileInfo);
+ }
+ try (InputStream in = FileUtil.getInputStream(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename())) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败!fileInfo:" + fileInfo,e);
+ }
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/platform/LocalPlusFileStorage.java b/src/main/java/cn/xuyanwu/spring/file/storage/platform/LocalPlusFileStorage.java
new file mode 100644
index 0000000..db494fd
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/platform/LocalPlusFileStorage.java
@@ -0,0 +1,138 @@
+package cn.xuyanwu.spring.file.storage.platform;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import com.yunzhupaas.model.FileListVO;
+import com.yunzhupaas.model.FileModel;
+import com.yunzhupaas.util.JsonUtil;
+import com.yunzhupaas.util.XSSEscape;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * 本地文件存储升级版
+ */
+@Getter
+@Setter
+public class LocalPlusFileStorage implements FileStorage {
+
+ /* 基础路径 */
+ private String basePath;
+ /* 本地存储路径*/
+ private String storagePath;
+ /* 存储平台 */
+ private String platform;
+ /* 访问域名 */
+ private String domain;
+
+ @Override
+ public void close() {
+ }
+
+ /**
+ * 获取本地绝对路径
+ */
+ public String getAbsolutePath(String path) {
+ return storagePath + path;
+ }
+
+ @Override
+ public boolean save(FileInfo fileInfo,UploadPretreatment pre) {
+
+ String newFileKey = basePath + fileInfo.getPath() + fileInfo.getFilename();
+ fileInfo.setBasePath(basePath);
+ fileInfo.setUrl(domain + newFileKey);
+
+ try {
+ File newFile = FileUtil.touch(getAbsolutePath(newFileKey));
+ pre.getFileWrapper().transferTo(newFile);
+
+ byte[] thumbnailBytes = pre.getThumbnailBytes();
+ if (thumbnailBytes != null) { //上传缩略图
+ String newThFileKey = basePath + fileInfo.getPath() + fileInfo.getThFilename();
+ fileInfo.setThUrl(domain + newThFileKey);
+ FileUtil.writeBytes(thumbnailBytes,getAbsolutePath(newThFileKey));
+ }
+ return true;
+ } catch (IOException e) {
+ FileUtil.del(getAbsolutePath(newFileKey));
+ throw new FileStorageRuntimeException("文件上传失败!platform:" + platform + ",filename:" + fileInfo.getOriginalFilename(),e);
+ }
+ }
+
+ @Override
+ public boolean delete(FileInfo fileInfo) {
+ if (fileInfo.getThFilename() != null) { //删除缩略图
+ FileUtil.del(getAbsolutePath(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename()));
+ }
+ return FileUtil.del(getAbsolutePath(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename()));
+ }
+
+
+ @Override
+ public boolean exists(FileInfo fileInfo) {
+ return new File(getAbsolutePath(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename())).exists();
+ }
+
+ @Override
+ public void download(FileInfo fileInfo,Consumer consumer) {
+ try (InputStream in = FileUtil.getInputStream(getAbsolutePath(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename()))) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("文件下载失败!platform:" + fileInfo,e);
+ }
+ }
+
+ @Override
+ public void downloadTh(FileInfo fileInfo,Consumer consumer) {
+ if (StrUtil.isBlank(fileInfo.getThFilename())) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败,文件不存在!fileInfo:" + fileInfo);
+ }
+ try (InputStream in = FileUtil.getInputStream(getAbsolutePath(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename()))) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败!fileInfo:" + fileInfo,e);
+ }
+ }
+
+ @Override
+ public List getFileList(String folderName) {
+ List data = new ArrayList<>();
+ File filePath = new File(XSSEscape.escapePath(getLocalPath() + folderName));
+ List files = com.yunzhupaas.util.FileUtil.getFile(filePath);
+ if (files != null) {
+ for (int i = 0; i < files.size(); i++) {
+ File item = files.get(i);
+ FileModel fileModel = new FileModel();
+ fileModel.setFileId(i + "");
+ fileModel.setFileName(folderName + item.getName());
+ fileModel.setFileType(com.yunzhupaas.util.FileUtil.getFileType(item));
+ fileModel.setFileSize(com.yunzhupaas.util.FileUtil.getSize(String.valueOf(item.length())));
+ fileModel.setFileTime(com.yunzhupaas.util.FileUtil.getCreateTime(filePath + item.getName()));
+ data.add(fileModel);
+ }
+ }
+ return data;
+ }
+
+ @Override
+ public List conversionList(String folderName) {
+ List fileList = getFileList(folderName);
+ List listVOS = new ArrayList<>(fileList.size());
+ if (fileList.size() > 0 && fileList.get(0) instanceof FileModel) {
+ return JsonUtil.getJsonToList(fileList, FileListVO.class);
+ }
+ return new ArrayList<>();
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/platform/MinIOFileStorage.java b/src/main/java/cn/xuyanwu/spring/file/storage/platform/MinIOFileStorage.java
new file mode 100644
index 0000000..bf57869
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/platform/MinIOFileStorage.java
@@ -0,0 +1,215 @@
+package cn.xuyanwu.spring.file.storage.platform;
+
+import cn.hutool.core.util.StrUtil;
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import io.minio.*;
+import io.minio.errors.*;
+import io.minio.messages.Item;
+import com.yunzhupaas.model.FileListVO;
+import com.yunzhupaas.model.FileModel;
+import com.yunzhupaas.util.DateUtil;
+import com.yunzhupaas.util.FileUtil;
+import com.yunzhupaas.util.JsonUtil;
+import lombok.Cleanup;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * MinIO 存储
+ */
+@Getter
+@Setter
+public class MinIOFileStorage implements FileStorage {
+
+ /* 存储平台 */
+ private String platform;
+ private String accessKey;
+ private String secretKey;
+ private String endPoint;
+ private String bucketName;
+ private String domain;
+ private String basePath;
+ private MinioClient client;
+
+ /**
+ * 单例模式运行,不需要每次使用完再销毁了
+ */
+ public MinioClient getClient() {
+ if (client == null) {
+ client = new MinioClient.Builder().credentials(accessKey, secretKey).endpoint(endPoint).build();
+ }
+ return client;
+ }
+
+ /**
+ * 仅在移除这个存储平台时调用
+ */
+ @Override
+ public void close() {
+ client = null;
+ }
+
+ @Override
+ public boolean save(FileInfo fileInfo, UploadPretreatment pre) {
+ String newFileKey = basePath + fileInfo.getPath() + fileInfo.getFilename();
+ fileInfo.setBasePath(basePath);
+ fileInfo.setUrl(domain + bucketName + "/" + newFileKey);
+
+ MinioClient client = getClient();
+ try (InputStream in = pre.getFileWrapper().getInputStream()) {
+ client.putObject(PutObjectArgs.builder().bucket(bucketName).object(newFileKey)
+ .stream(in, pre.getFileWrapper().getSize(), -1)
+ .contentType(fileInfo.getContentType()).build());
+
+ byte[] thumbnailBytes = pre.getThumbnailBytes();
+ if (thumbnailBytes != null) { // 上传缩略图
+ String newThFileKey = basePath + fileInfo.getPath() + fileInfo.getThFilename();
+ fileInfo.setThUrl(domain + newThFileKey);
+ client.putObject(PutObjectArgs.builder().bucket(bucketName).object(newThFileKey)
+ .stream(new ByteArrayInputStream(thumbnailBytes), thumbnailBytes.length, -1)
+ .contentType(fileInfo.getThContentType()).build());
+ }
+
+ return true;
+ } catch (ErrorResponseException | InsufficientDataException | InternalException | ServerException
+ | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException
+ | XmlParserException e) {
+ try {
+ client.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(newFileKey).build());
+ } catch (Exception ignored) {
+ }
+ throw new FileStorageRuntimeException(
+ "文件上传失败!platform:" + platform + ",filename:" + fileInfo.getOriginalFilename(), e);
+ }
+ }
+
+ @Override
+ public boolean delete(FileInfo fileInfo) {
+ MinioClient client = getClient();
+ try {
+ if (fileInfo.getThFilename() != null) { // 删除缩略图
+ client.removeObject(RemoveObjectArgs.builder().bucket(bucketName)
+ .object(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename()).build());
+ }
+ client.removeObject(RemoveObjectArgs.builder().bucket(bucketName)
+ .object(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename()).build());
+ return true;
+ } catch (ErrorResponseException | InsufficientDataException | InternalException | ServerException
+ | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException
+ | XmlParserException e) {
+ throw new FileStorageRuntimeException("文件删除失败!fileInfo:" + fileInfo, e);
+ }
+ }
+
+ @Override
+ public boolean exists(FileInfo fileInfo) {
+ MinioClient client = getClient();
+ try {
+ StatObjectResponse stat = client.statObject(StatObjectArgs.builder().bucket(bucketName)
+ .object(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename()).build());
+ return stat != null && stat.lastModified() != null;
+ } catch (ErrorResponseException e) {
+ String code = e.errorResponse().code();
+ if ("NoSuchKey".equals(code)) {
+ return false;
+ }
+ throw new FileStorageRuntimeException("查询文件是否存在失败!", e);
+ } catch (InsufficientDataException | InternalException | ServerException | InvalidKeyException
+ | InvalidResponseException | IOException | NoSuchAlgorithmException | XmlParserException e) {
+ throw new FileStorageRuntimeException("查询文件是否存在失败!", e);
+ }
+ }
+
+ @Override
+ public void download(FileInfo fileInfo, Consumer consumer) {
+ MinioClient client = getClient();
+ try (InputStream in = client.getObject(GetObjectArgs.builder().bucket(bucketName)
+ .object(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename()).build())) {
+ consumer.accept(in);
+ } catch (ErrorResponseException | InsufficientDataException | InternalException | ServerException
+ | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException
+ | XmlParserException e) {
+ throw new FileStorageRuntimeException("文件下载失败!platform:" + fileInfo, e);
+ }
+ }
+
+ @Override
+ public void downloadTh(FileInfo fileInfo, Consumer consumer) {
+ if (StrUtil.isBlank(fileInfo.getThFilename())) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败,文件不存在!fileInfo:" + fileInfo);
+ }
+ MinioClient client = getClient();
+ try (InputStream in = client.getObject(GetObjectArgs.builder().bucket(bucketName)
+ .object(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename()).build())) {
+ consumer.accept(in);
+ } catch (ErrorResponseException | InsufficientDataException | InternalException | ServerException
+ | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException
+ | XmlParserException e) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败!fileInfo:" + fileInfo, e);
+ }
+
+ }
+
+ @Override
+ public List getFileList(String folderName) {
+ List- list = new ArrayList<>();
+ try {
+ Iterable> results = null;
+ results = getClient().listObjects(
+ ListObjectsArgs.builder().bucket(bucketName).prefix(this.getBasePath() + folderName).recursive(true)
+ .build());
+ for (Result
- result : results) {
+ Item item = result.get();
+ list.add(item);
+ }
+ } catch (Exception e) {
+ throw new FileStorageRuntimeException("文件获取失败!platform:" + platform, e);
+ }
+ return list;
+ }
+
+ @Override
+ public List conversionList(String folderName) {
+ List fileList = getFileList(folderName);
+ List listVOS = new ArrayList<>(fileList.size());
+ if (fileList.size() > 0 && fileList.get(0) instanceof FileModel) {
+ return JsonUtil.getJsonToList(fileList, FileListVO.class);
+ }
+ for (int i = 0; i < fileList.size(); i++) {
+ FileListVO fileListVO = new FileListVO();
+ fileListVO.setFileId(i + "");
+ Item item = (Item) fileList.get(i);
+ String objectName = item.objectName();
+ fileListVO.setFileName(objectName);
+ fileListVO.setFileType(FileUtil.getFileType(objectName));
+ fileListVO.setFileSize(FileUtil.getSize(String.valueOf(item.size())));
+ fileListVO.setFileTime(DateUtil.getZonedDateTimeToString(item.lastModified()));
+ listVOS.add(fileListVO);
+ }
+ return listVOS;
+ }
+
+ @Override
+ public void downLocal(String folderName, String filePath, String objectName) {
+ try {
+ @Cleanup
+ InputStream stream = getClient().getObject(
+ GetObjectArgs.builder().bucket(bucketName).object(this.getBasePath() + folderName + objectName)
+ .build());
+ FileUtil.write(stream, filePath, objectName);
+ } catch (Exception e) {
+ throw new FileStorageRuntimeException("文件获取失败!platform:" + platform, e);
+ }
+ }
+}
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/platform/QiniuKodoFileStorage.java b/src/main/java/cn/xuyanwu/spring/file/storage/platform/QiniuKodoFileStorage.java
new file mode 100644
index 0000000..bd36c3a
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/platform/QiniuKodoFileStorage.java
@@ -0,0 +1,254 @@
+package cn.xuyanwu.spring.file.storage.platform;
+
+import cn.hutool.core.util.StrUtil;
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import com.qiniu.common.QiniuException;
+import com.qiniu.storage.BucketManager;
+import com.qiniu.storage.Configuration;
+import com.qiniu.storage.Region;
+import com.qiniu.storage.UploadManager;
+import com.qiniu.util.Auth;
+import com.yunzhupaas.model.FileListVO;
+import com.yunzhupaas.model.FileModel;
+import com.yunzhupaas.util.DateUtil;
+import com.yunzhupaas.util.FileUtil;
+import com.yunzhupaas.util.JsonUtil;
+import lombok.Cleanup;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * 七牛云 Kodo 存储
+ */
+@Getter
+@Setter
+public class QiniuKodoFileStorage implements FileStorage {
+
+ /* 存储平台 */
+ private String platform;
+ private String accessKey;
+ private String secretKey;
+ private String bucketName;
+ private String domain;
+ private String basePath;
+ private Region region;
+ private QiniuKodoClient client;
+
+ /**
+ * 单例模式运行,不需要每次使用完再销毁了
+ */
+ public QiniuKodoClient getClient() {
+ if (client == null) {
+ client = new QiniuKodoClient(accessKey, secretKey);
+ }
+ return client;
+ }
+
+ /**
+ * 仅在移除这个存储平台时调用
+ */
+ @Override
+ public void close() {
+ client = null;
+ }
+
+ @Override
+ public boolean save(FileInfo fileInfo, UploadPretreatment pre) {
+ String newFileKey = basePath + fileInfo.getPath() + fileInfo.getFilename();
+ fileInfo.setBasePath(basePath);
+ fileInfo.setUrl(domain + bucketName + "/" + newFileKey);
+
+ try (InputStream in = pre.getFileWrapper().getInputStream()) {
+ QiniuKodoClient client = getClient();
+ UploadManager uploadManager = client.getUploadManager();
+ String token = client.getAuth().uploadToken(bucketName);
+ uploadManager.put(in, newFileKey, token, null, fileInfo.getContentType());
+
+ byte[] thumbnailBytes = pre.getThumbnailBytes();
+ if (thumbnailBytes != null) { // 上传缩略图
+ String newThFileKey = basePath + fileInfo.getPath() + fileInfo.getThFilename();
+ fileInfo.setThUrl(domain + newThFileKey);
+ uploadManager.put(new ByteArrayInputStream(thumbnailBytes), newThFileKey, token, null,
+ fileInfo.getThContentType());
+ }
+
+ return true;
+ } catch (IOException e) {
+ try {
+ client.getBucketManager().delete(bucketName, newFileKey);
+ } catch (QiniuException ignored) {
+ }
+ throw new FileStorageRuntimeException(
+ "文件上传失败!platform:" + platform + ",filename:" + fileInfo.getOriginalFilename(), e);
+ }
+ }
+
+ @Override
+ public boolean delete(FileInfo fileInfo) {
+ BucketManager manager = getClient().getBucketManager();
+ try {
+ if (fileInfo.getThFilename() != null) { // 删除缩略图
+ delete(manager, fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename());
+ }
+ delete(manager, fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ } catch (QiniuException e) {
+ throw new FileStorageRuntimeException("删除文件失败!" + e.code() + "," + e.response.toString(), e);
+ }
+ return true;
+ }
+
+ public void delete(BucketManager manager, String filename) throws QiniuException {
+ try {
+ manager.delete(bucketName, filename);
+ } catch (QiniuException e) {
+ if (!(e.response != null && e.response.statusCode == 612)) {
+ throw e;
+ }
+ }
+ }
+
+ @Override
+ public boolean exists(FileInfo fileInfo) {
+ BucketManager manager = getClient().getBucketManager();
+ try {
+ com.qiniu.storage.model.FileInfo stat = manager.stat(bucketName,
+ fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ if (stat != null && stat.md5 != null)
+ return true;
+ } catch (QiniuException e) {
+ throw new FileStorageRuntimeException("查询文件是否存在失败!" + e.code() + "," + e.response.toString(), e);
+ }
+ return false;
+ }
+
+ @Override
+ public void download(FileInfo fileInfo, Consumer consumer) {
+ String url = getClient().getAuth().privateDownloadUrl(this.getDomain() +
+ this.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ try (InputStream in = new URL(url).openStream()) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("文件下载失败!platform:" + fileInfo, e);
+ }
+ }
+
+ @Override
+ public void downloadTh(FileInfo fileInfo, Consumer consumer) {
+ if (StrUtil.isBlank(fileInfo.getThUrl())) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败,文件不存在!fileInfo:" + fileInfo);
+ }
+ String url = getClient().getAuth().privateDownloadUrl(this.getDomain() +
+ this.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ try (InputStream in = new URL(url).openStream()) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败!fileInfo:" + fileInfo, e);
+ }
+ }
+
+ @Getter
+ @Setter
+ public static class QiniuKodoClient {
+ private String accessKey;
+ private String secretKey;
+ private Auth auth;
+ private BucketManager bucketManager;
+ private UploadManager uploadManager;
+
+ public QiniuKodoClient(String accessKey, String secretKey) {
+ this.accessKey = accessKey;
+ this.secretKey = secretKey;
+ }
+
+ public Auth getAuth() {
+ if (auth == null) {
+ auth = Auth.create(accessKey, secretKey);
+ }
+ return auth;
+ }
+
+ public BucketManager getBucketManager() {
+ if (bucketManager == null) {
+ bucketManager = new BucketManager(getAuth(), new Configuration(Region.autoRegion()));
+ }
+ return bucketManager;
+ }
+
+ public UploadManager getUploadManager() {
+ if (uploadManager == null) {
+ uploadManager = new UploadManager(new Configuration(Region.autoRegion()));
+ }
+ return uploadManager;
+ }
+ }
+
+ @Override
+ public List getFileList(String folderName) {
+ // 判断存储桶是否存在
+ List list = new ArrayList<>();
+ try {
+ BucketManager.FileListIterator fileListIterator = getClient().getBucketManager()
+ .createFileListIterator(this.getBasePath() + bucketName, folderName, 1000, "");
+ while (fileListIterator.hasNext()) {
+ // 处理获取的file list结果
+ com.qiniu.storage.model.FileInfo[] items = fileListIterator.next();
+ for (com.qiniu.storage.model.FileInfo item : items) {
+ list.add(item);
+ }
+ }
+ } catch (Exception e) {
+ throw new FileStorageRuntimeException("文件获取失败!platform:" + platform, e);
+ }
+ return list;
+ }
+
+ @Override
+ public List conversionList(String folderName) {
+ List fileList = getFileList(folderName);
+ List listVOS = new ArrayList<>(fileList.size());
+ if (fileList.size() > 0 && fileList.get(0) instanceof FileModel) {
+ return JsonUtil.getJsonToList(fileList, FileListVO.class);
+ }
+ for (int i = 0; i < fileList.size(); i++) {
+ FileListVO fileListVO = new FileListVO();
+ fileListVO.setFileId(i + "");
+ // 七牛
+ com.qiniu.storage.model.FileInfo fileInfo = (com.qiniu.storage.model.FileInfo) fileList.get(i);
+ String objectName = fileInfo.key.replace(this.getBasePath() + folderName + "/", "");
+ fileListVO.setFileName(objectName);
+ fileListVO.setFileType(FileUtil.getFileType(objectName));
+ fileListVO.setFileSize(FileUtil.getSize(String.valueOf(fileInfo.fsize)));
+ fileListVO.setFileTime(DateUtil.daFormat(fileInfo.putTime));
+ listVOS.add(fileListVO);
+ }
+ return listVOS;
+ }
+
+ @Override
+ public void downLocal(String folderName, String filePath, String objectName) {
+ try {
+ String encodedFileName = URLEncoder.encode(this.getBasePath() + folderName + objectName, "utf-8")
+ .replace("+", "%20");
+ String finalUrl = String.format("%s/%s", domain, encodedFileName);
+ String downloadUrl = getClient().getAuth().privateDownloadUrl(finalUrl);
+ @Cleanup
+ InputStream inputStream = new URL(downloadUrl).openStream();
+ FileUtil.write(inputStream, filePath, objectName);
+ } catch (Exception e) {
+ throw new FileStorageRuntimeException(
+ "文件下载失败!platform:" + platform + ",下载路径:" + filePath + ",文件夹名称:" + folderName + ",文件名:" + objectName,
+ e);
+ }
+ }
+}
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/platform/SftpFileStorage.java b/src/main/java/cn/xuyanwu/spring/file/storage/platform/SftpFileStorage.java
new file mode 100644
index 0000000..33ee320
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/platform/SftpFileStorage.java
@@ -0,0 +1,179 @@
+package cn.xuyanwu.spring.file.storage.platform;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.URLUtil;
+import cn.hutool.extra.ssh.JschRuntimeException;
+import cn.hutool.extra.ssh.JschUtil;
+import cn.hutool.extra.ssh.Sftp;
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.Session;
+import com.jcraft.jsch.SftpException;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Consumer;
+
+import static com.jcraft.jsch.ChannelSftp.SSH_FX_NO_SUCH_FILE;
+
+/**
+ * SFTP 存储
+ */
+@Getter
+@Setter
+public class SftpFileStorage implements FileStorage {
+
+ /* 主机 */
+ private String host;
+ /* 端口,默认22 */
+ private int port;
+ /* 用户名 */
+ private String user;
+ /* 密码,默认空 */
+ private String password;
+ /* 私钥路径,默认空 */
+ private String privateKeyPath;
+ /* 编码,默认UTF-8 */
+ private Charset charset;
+ /* 连接超时时长,单位毫秒,默认10秒 */
+ private long connectionTimeout;
+ /* 存储平台 */
+ private String platform;
+ private String domain;
+ private String basePath;
+ private String storagePath;
+
+ /**
+ * 不支持单例模式运行,每次使用完了需要销毁
+ */
+ public Sftp getClient() {
+ Session session = null;
+ try {
+ if (StrUtil.isNotBlank(privateKeyPath)) {
+ //使用秘钥连接,这里手动读取 byte 进行构造用于兼容Spring的ClassPath路径、文件路径、HTTP路径等
+ byte[] passphrase = StrUtil.isBlank(password) ? null : password.getBytes(StandardCharsets.UTF_8);
+ JSch jsch = new JSch();
+ byte[] privateKey = IoUtil.readBytes(URLUtil.url(privateKeyPath).openStream());
+ jsch.addIdentity(privateKeyPath,privateKey,null,passphrase);
+ session = JschUtil.createSession(jsch,host,port,user);
+ session.connect((int) connectionTimeout);
+ } else {
+ session = JschUtil.openSession(host,port,user,password,(int) connectionTimeout);
+ }
+ return new Sftp(session,charset,connectionTimeout);
+ } catch (Exception e) {
+ JschUtil.close(session);
+ throw new FileStorageRuntimeException("SFTP连接失败!platform:" + platform,e);
+ }
+ }
+
+
+ @Override
+ public void close() {
+ }
+
+ /**
+ * 获取远程绝对路径
+ */
+ public String getAbsolutePath(String path) {
+ return storagePath + path;
+ }
+
+ @Override
+ public boolean save(FileInfo fileInfo,UploadPretreatment pre) {
+ String newFileKey = basePath + fileInfo.getPath() + fileInfo.getFilename();
+ fileInfo.setBasePath(basePath);
+ fileInfo.setUrl(domain + newFileKey);
+
+ Sftp client = getClient();
+ try (InputStream in = pre.getFileWrapper().getInputStream()) {
+ String path = getAbsolutePath(basePath + fileInfo.getPath());
+ if (!client.exist(path)) {
+ client.mkDirs(path);
+ }
+ client.upload(path,fileInfo.getFilename(),in);
+
+ byte[] thumbnailBytes = pre.getThumbnailBytes();
+ if (thumbnailBytes != null) { //上传缩略图
+ String newThFileKey = basePath + fileInfo.getPath() + fileInfo.getThFilename();
+ fileInfo.setThUrl(domain + newThFileKey);
+ client.upload(path,fileInfo.getThFilename(),new ByteArrayInputStream(thumbnailBytes));
+ }
+
+ return true;
+ } catch (IOException | JschRuntimeException e) {
+ try {
+ client.delFile(getAbsolutePath(newFileKey));
+ } catch (JschRuntimeException ignored) {
+ }
+ throw new FileStorageRuntimeException("文件上传失败!platform:" + platform + ",filename:" + fileInfo.getOriginalFilename(),e);
+ } finally {
+ IoUtil.close(client);
+ }
+ }
+
+ @Override
+ public boolean delete(FileInfo fileInfo) {
+ try (Sftp client = getClient()) {
+ if (fileInfo.getThFilename() != null) { //删除缩略图
+ delFile(client,getAbsolutePath(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename()));
+ }
+ delFile(client,getAbsolutePath(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename()));
+ return true;
+ } catch (JschRuntimeException e) {
+ throw new FileStorageRuntimeException("文件删除失败!fileInfo:" + fileInfo,e);
+ }
+ }
+
+ public void delFile(Sftp client,String filename) {
+ try {
+ client.delFile(filename);
+ } catch (JschRuntimeException e) {
+ if (!(e.getCause() instanceof SftpException && ((SftpException) e.getCause()).id == SSH_FX_NO_SUCH_FILE)) {
+ throw e;
+ }
+ }
+ }
+
+
+ @Override
+ public boolean exists(FileInfo fileInfo) {
+ try (Sftp client = getClient()) {
+ return client.exist(getAbsolutePath(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename()));
+ } catch (JschRuntimeException e) {
+ throw new FileStorageRuntimeException("查询文件是否存在失败!fileInfo:" + fileInfo,e);
+ }
+ }
+
+ @Override
+ public void download(FileInfo fileInfo,Consumer consumer) {
+ try (Sftp client = getClient();
+ InputStream in = client.getClient().get(getAbsolutePath(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename()))) {
+ consumer.accept(in);
+ } catch (IOException | JschRuntimeException | SftpException e) {
+ throw new FileStorageRuntimeException("文件下载失败!platform:" + fileInfo,e);
+ }
+ }
+
+ @Override
+ public void downloadTh(FileInfo fileInfo,Consumer consumer) {
+ if (StrUtil.isBlank(fileInfo.getThFilename())) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败,文件不存在!fileInfo:" + fileInfo);
+ }
+
+ try (Sftp client = getClient(); InputStream in = client.getClient().get(getAbsolutePath(fileInfo.getBasePath() + fileInfo.getPath()) + fileInfo.getThFilename())) {
+ consumer.accept(in);
+ } catch (IOException | JschRuntimeException | SftpException e) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败!fileInfo:" + fileInfo,e);
+ }
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/platform/TencentCosFileStorage.java b/src/main/java/cn/xuyanwu/spring/file/storage/platform/TencentCosFileStorage.java
new file mode 100644
index 0000000..8bedcb2
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/platform/TencentCosFileStorage.java
@@ -0,0 +1,205 @@
+package cn.xuyanwu.spring.file.storage.platform;
+
+import cn.hutool.core.util.StrUtil;
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import com.qcloud.cos.COSClient;
+import com.qcloud.cos.ClientConfig;
+import com.qcloud.cos.auth.BasicCOSCredentials;
+import com.qcloud.cos.auth.COSCredentials;
+import com.qcloud.cos.http.HttpProtocol;
+import com.qcloud.cos.model.*;
+import com.qcloud.cos.region.Region;
+import com.yunzhupaas.model.FileListVO;
+import com.yunzhupaas.model.FileModel;
+import com.yunzhupaas.util.DateUtil;
+import com.yunzhupaas.util.FileUtil;
+import com.yunzhupaas.util.JsonUtil;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * 腾讯云 COS 存储
+ */
+@Getter
+@Setter
+public class TencentCosFileStorage implements FileStorage {
+
+ /* 存储平台 */
+ private String platform;
+ private String secretId;
+ private String secretKey;
+ private String region;
+ private String bucketName;
+ private String domain;
+ private String basePath;
+ private COSClient client;
+
+ /**
+ * 单例模式运行,不需要每次使用完再销毁了
+ */
+ public COSClient getClient() {
+ if (client == null) {
+ COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
+ ClientConfig clientConfig = new ClientConfig(new Region(region));
+ clientConfig.setHttpProtocol(HttpProtocol.https);
+ client = new COSClient(cred, clientConfig);
+ }
+ return client;
+ }
+
+ /**
+ * 仅在移除这个存储平台时调用
+ */
+ @Override
+ public void close() {
+ if (client != null) {
+ client.shutdown();
+ client = null;
+ }
+ }
+
+ @Override
+ public boolean save(FileInfo fileInfo, UploadPretreatment pre) {
+ String newFileKey = basePath + fileInfo.getPath() + fileInfo.getFilename();
+ fileInfo.setBasePath(basePath);
+ fileInfo.setUrl(domain + bucketName + "/" + newFileKey);
+
+ COSClient client = getClient();
+ try (InputStream in = pre.getFileWrapper().getInputStream()) {
+ ObjectMetadata metadata = new ObjectMetadata();
+ metadata.setContentLength(fileInfo.getSize());
+ metadata.setContentType(fileInfo.getContentType());
+ client.putObject(bucketName, newFileKey, in, metadata);
+
+ byte[] thumbnailBytes = pre.getThumbnailBytes();
+ if (thumbnailBytes != null) { // 上传缩略图
+ String newThFileKey = basePath + fileInfo.getPath() + fileInfo.getThFilename();
+ fileInfo.setThUrl(domain + newThFileKey);
+ ObjectMetadata thMetadata = new ObjectMetadata();
+ thMetadata.setContentLength(thumbnailBytes.length);
+ thMetadata.setContentType(fileInfo.getThContentType());
+ client.putObject(bucketName, newThFileKey, new ByteArrayInputStream(thumbnailBytes), thMetadata);
+ }
+
+ return true;
+ } catch (IOException e) {
+ client.deleteObject(bucketName, newFileKey);
+ throw new FileStorageRuntimeException(
+ "文件上传失败!platform:" + platform + ",filename:" + fileInfo.getOriginalFilename(), e);
+ }
+ }
+
+ @Override
+ public boolean delete(FileInfo fileInfo) {
+ COSClient client = getClient();
+ if (fileInfo.getThFilename() != null) { // 删除缩略图
+ client.deleteObject(bucketName, fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename());
+ }
+ client.deleteObject(bucketName, fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ return true;
+ }
+
+ @Override
+ public boolean exists(FileInfo fileInfo) {
+ return getClient().doesObjectExist(bucketName,
+ fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ }
+
+ @Override
+ public void download(FileInfo fileInfo, Consumer consumer) {
+ COSObject object = getClient().getObject(bucketName,
+ fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ try (InputStream in = object.getObjectContent()) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("文件下载失败!platform:" + fileInfo, e);
+ }
+ }
+
+ @Override
+ public void downloadTh(FileInfo fileInfo, Consumer consumer) {
+ if (StrUtil.isBlank(fileInfo.getThFilename())) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败,文件不存在!fileInfo:" + fileInfo);
+ }
+ COSObject object = getClient().getObject(bucketName,
+ fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename());
+ try (InputStream in = object.getObjectContent()) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败!fileInfo:" + fileInfo, e);
+ }
+ }
+
+ @Override
+ public List getFileList(String folderName) {
+ List list = new ArrayList<>();
+ ListObjectsRequest listObjectsRequest = new ListObjectsRequest();
+ listObjectsRequest.setBucketName(bucketName);
+ listObjectsRequest.setPrefix(this.getBasePath() + folderName);
+ // deliter表示分隔符, 设置为/表示列出当前目录下的object, 设置为空表示列出所有的object
+ listObjectsRequest.setDelimiter("/");
+ listObjectsRequest.setMaxKeys(1000);
+ ObjectListing objectListing = null;
+ do {
+ try {
+ objectListing = getClient().listObjects(listObjectsRequest);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ // common prefix表示表示被delimiter截断的路径, 如delimter设置为/, common prefix则表示所有子目录的路径
+ // List commonPrefixs = objectListing.getCommonPrefixes();
+ // object summary表示所有列出的object列表
+ List cosObjectSummaries = objectListing.getObjectSummaries();
+ for (COSObjectSummary cosObjectSummary : cosObjectSummaries) {
+ list.add(cosObjectSummary);
+ }
+ String nextMarker = objectListing.getNextMarker();
+ listObjectsRequest.setMarker(nextMarker);
+ } while (objectListing.isTruncated());
+ return list;
+ }
+
+ @Override
+ public List conversionList(String folderName) {
+ List fileList = getFileList(folderName);
+ List listVOS = new ArrayList<>(fileList.size());
+ if (fileList.size() > 0 && fileList.get(0) instanceof FileModel) {
+ return JsonUtil.getJsonToList(fileList, FileListVO.class);
+ }
+ for (int i = 0; i < fileList.size(); i++) {
+ FileListVO fileListVO = new FileListVO();
+ fileListVO.setFileId(i + "");// 腾讯
+ COSObjectSummary cosObjectSummary = (COSObjectSummary) fileList.get(i);
+ String objectName = cosObjectSummary.getKey().replace(this.getBasePath() + folderName + "/", "");
+ fileListVO.setFileName(objectName);
+ fileListVO.setFileType(FileUtil.getFileType(objectName));
+ fileListVO.setFileSize(FileUtil.getSize(String.valueOf(cosObjectSummary.getSize())));
+ fileListVO.setFileTime(DateUtil.daFormat(cosObjectSummary.getLastModified()));
+ listVOS.add(fileListVO);
+ }
+ return listVOS;
+ }
+
+ @Override
+ public void downLocal(String folderName, String filePath, String objectName) {
+ try {
+ GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName,
+ this.getBasePath() + folderName + objectName);
+ getClient().getObject(getObjectRequest, new File(filePath + objectName));
+ } catch (Exception e) {
+ throw new FileStorageRuntimeException(
+ "文件下载失败!platform:" + platform + ",下载路径:" + filePath + ",文件夹名称:" + folderName + ",文件名:" + objectName,
+ e);
+ }
+ }
+}
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/platform/UpyunUssFileStorage.java b/src/main/java/cn/xuyanwu/spring/file/storage/platform/UpyunUssFileStorage.java
new file mode 100644
index 0000000..ab6fdf1
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/platform/UpyunUssFileStorage.java
@@ -0,0 +1,153 @@
+package cn.xuyanwu.spring.file.storage.platform;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import com.upyun.RestManager;
+import com.upyun.UpException;
+import lombok.Getter;
+import lombok.Setter;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.function.Consumer;
+
+/**
+ * 又拍云 USS 存储
+ */
+@Getter
+@Setter
+public class UpyunUssFileStorage implements FileStorage {
+
+ /* 存储平台 */
+ private String platform;
+ private String username;
+ private String password;
+ private String bucketName;
+ private String domain;
+ private String basePath;
+ private RestManager client;
+
+ public RestManager getClient() {
+ if (client == null) {
+ client = new RestManager(bucketName,username,password);
+ }
+ return client;
+ }
+
+ /**
+ * 仅在移除这个存储平台时调用
+ */
+ @Override
+ public void close() {
+ client = null;
+ }
+
+ @Override
+ public boolean save(FileInfo fileInfo,UploadPretreatment pre) {
+
+
+ String newFileKey = basePath + fileInfo.getPath() + fileInfo.getFilename();
+ fileInfo.setBasePath(basePath);
+ fileInfo.setUrl(domain + newFileKey);
+
+ RestManager manager = getClient();
+ try (InputStream in = pre.getFileWrapper().getInputStream()) {
+ HashMap params = new HashMap<>();
+ params.put(RestManager.PARAMS.CONTENT_TYPE.getValue(),fileInfo.getContentType());
+ try (Response result = manager.writeFile(newFileKey,in,params)) {
+ if (!result.isSuccessful()) {
+ throw new UpException(result.toString());
+ }
+ }
+
+ byte[] thumbnailBytes = pre.getThumbnailBytes();
+ if (thumbnailBytes != null) { //上传缩略图
+ String newThFileKey = basePath + fileInfo.getPath() + fileInfo.getThFilename();
+ fileInfo.setThUrl(domain + newThFileKey);
+ HashMap thParams = new HashMap<>();
+ thParams.put(RestManager.PARAMS.CONTENT_TYPE.getValue(),fileInfo.getThContentType());
+ Response thResult = manager.writeFile(newThFileKey,new ByteArrayInputStream(thumbnailBytes),thParams);
+ if (!thResult.isSuccessful()) {
+ throw new UpException(thResult.toString());
+ }
+ }
+
+ return true;
+ } catch (IOException | UpException e) {
+ try {
+ manager.deleteFile(newFileKey,null).close();
+ } catch (IOException | UpException ignored) {
+ }
+ throw new FileStorageRuntimeException("文件上传失败!platform:" + platform + ",filename:" + fileInfo.getOriginalFilename(),e);
+ }
+ }
+
+ @Override
+ public boolean delete(FileInfo fileInfo) {
+ RestManager manager = getClient();
+ String file = fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename();
+ String thFile = fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename();
+
+ try (Response ignored = fileInfo.getThFilename() != null ? manager.deleteFile(thFile,null) : null;
+ Response ignored2 = manager.deleteFile(file,null)) {
+ return true;
+ } catch (IOException | UpException e) {
+ throw new FileStorageRuntimeException("文件删除失败!fileInfo:" + fileInfo,e);
+ }
+ }
+
+ @Override
+ public boolean exists(FileInfo fileInfo) {
+ try (Response response = getClient().getFileInfo(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename())) {
+ return StrUtil.isNotBlank(response.header("x-upyun-file-size"));
+ } catch (IOException | UpException e) {
+ throw new FileStorageRuntimeException("判断文件是否存在失败!fileInfo:" + fileInfo,e);
+ }
+ }
+
+ @Override
+ public void download(FileInfo fileInfo,Consumer consumer) {
+ try (Response response = getClient().readFile(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename());
+ ResponseBody body = response.body();
+ InputStream in = body == null ? null : body.byteStream()) {
+ if (body == null) {
+ throw new FileStorageRuntimeException("文件下载失败,结果为 null !fileInfo:" + fileInfo);
+ }
+ if (!response.isSuccessful()) {
+ throw new UpException(IoUtil.read(in,StandardCharsets.UTF_8));
+ }
+ consumer.accept(in);
+ } catch (IOException | UpException e) {
+ throw new FileStorageRuntimeException("文件下载失败!fileInfo:" + fileInfo,e);
+ }
+ }
+
+ @Override
+ public void downloadTh(FileInfo fileInfo,Consumer consumer) {
+ if (StrUtil.isBlank(fileInfo.getThFilename())) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败,文件不存在!fileInfo:" + fileInfo);
+ }
+ try (Response response = getClient().readFile(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename());
+ ResponseBody body = response.body();
+ InputStream in = body == null ? null : body.byteStream()) {
+ if (body == null) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败,结果为 null !fileInfo:" + fileInfo);
+ }
+ if (!response.isSuccessful()) {
+ throw new UpException(IoUtil.read(in,StandardCharsets.UTF_8));
+ }
+ consumer.accept(in);
+ } catch (IOException | UpException e) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败!fileInfo:" + fileInfo,e);
+ }
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/platform/WebDavFileStorage.java b/src/main/java/cn/xuyanwu/spring/file/storage/platform/WebDavFileStorage.java
new file mode 100644
index 0000000..5a70a84
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/platform/WebDavFileStorage.java
@@ -0,0 +1,161 @@
+package cn.xuyanwu.spring.file.storage.platform;
+
+import cn.hutool.core.io.IORuntimeException;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.xuyanwu.spring.file.storage.FileInfo;
+import cn.xuyanwu.spring.file.storage.PathUtil;
+import cn.xuyanwu.spring.file.storage.UploadPretreatment;
+import cn.xuyanwu.spring.file.storage.exception.FileStorageRuntimeException;
+import com.github.sardine.Sardine;
+import com.github.sardine.SardineFactory;
+import com.github.sardine.impl.SardineException;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.SneakyThrows;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.function.Consumer;
+
+/**
+ * WebDav 存储
+ */
+@Getter
+@Setter
+public class WebDavFileStorage implements FileStorage {
+
+ private String server;
+ private String user;
+ private String password;
+ private String platform;
+ private String domain;
+ private String basePath;
+ private String storagePath;
+ private Sardine client;
+
+ /**
+ * 不支持单例模式运行,每次使用完了需要销毁
+ */
+ public Sardine getClient() {
+ if (client == null) {
+ client = SardineFactory.begin(user,password);
+ }
+ return client;
+ }
+
+ /**
+ * 仅在移除这个存储平台时调用
+ */
+ @SneakyThrows
+ @Override
+ public void close() {
+ if (client != null) {
+ client.shutdown();
+ client = null;
+ }
+ }
+
+ /**
+ * 获取远程绝对路径
+ */
+ public String getUrl(String path) {
+ return PathUtil.join(server,storagePath + path);
+ }
+
+ /**
+ * 递归创建目录
+ */
+ public void createDirectory(Sardine client,String path) throws IOException {
+ if (!client.exists(path)) {
+ createDirectory(client,PathUtil.join(PathUtil.getParent(path),"/"));
+ client.createDirectory(path);
+ }
+ }
+
+ @Override
+ public boolean save(FileInfo fileInfo,UploadPretreatment pre) {
+ String path = basePath + fileInfo.getPath();
+ String newFileKey = path + fileInfo.getFilename();
+ fileInfo.setBasePath(basePath);
+ fileInfo.setUrl(domain + newFileKey);
+
+ Sardine client = getClient();
+ try (InputStream in = pre.getFileWrapper().getInputStream()) {
+ byte[] bytes = IoUtil.readBytes(in);
+ createDirectory(client,getUrl(path));
+ client.put(getUrl(newFileKey),bytes);
+
+ byte[] thumbnailBytes = pre.getThumbnailBytes();
+ if (thumbnailBytes != null) { //上传缩略图
+ String newThFileKey = path + fileInfo.getThFilename();
+ fileInfo.setThUrl(domain + newThFileKey);
+ client.put(getUrl(newThFileKey),thumbnailBytes);
+ }
+
+ return true;
+ } catch (IOException | IORuntimeException e) {
+ try {
+ client.delete(getUrl(newFileKey));
+ } catch (IOException ignored) {
+ }
+ throw new FileStorageRuntimeException("文件上传失败!platform:" + platform + ",filename:" + fileInfo.getOriginalFilename(),e);
+ }
+ }
+
+ @Override
+ public boolean delete(FileInfo fileInfo) {
+ Sardine client = getClient();
+ try {
+ if (fileInfo.getThFilename() != null) { //删除缩略图
+ try {
+ client.delete(getUrl(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename()));
+ } catch (SardineException e) {
+ if (e.getStatusCode() != 404) throw e;
+ }
+ }
+ try {
+ client.delete(getUrl(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename()));
+ } catch (SardineException e) {
+ if (e.getStatusCode() != 404) throw e;
+ }
+ return true;
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("文件删除失败!fileInfo:" + fileInfo,e);
+ }
+ }
+
+
+ @Override
+ public boolean exists(FileInfo fileInfo) {
+ try {
+ return getClient().exists(getUrl(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename()));
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("查询文件是否存在失败!fileInfo:" + fileInfo,e);
+ }
+ }
+
+ @Override
+ public void download(FileInfo fileInfo,Consumer consumer) {
+ try (InputStream in = getClient().get(getUrl(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename()))) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("文件下载失败!platform:" + fileInfo,e);
+ }
+ }
+
+ @Override
+ public void downloadTh(FileInfo fileInfo,Consumer consumer) {
+ if (StrUtil.isBlank(fileInfo.getThFilename())) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败,文件不存在!fileInfo:" + fileInfo);
+ }
+
+ try (InputStream in = getClient().get(getUrl(fileInfo.getBasePath() + fileInfo.getPath()) + fileInfo.getThFilename())) {
+ consumer.accept(in);
+ } catch (IOException e) {
+ throw new FileStorageRuntimeException("缩略图文件下载失败!fileInfo:" + fileInfo,e);
+ }
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/recorder/DefaultFileRecorder.java b/src/main/java/cn/xuyanwu/spring/file/storage/recorder/DefaultFileRecorder.java
new file mode 100644
index 0000000..732fb54
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/recorder/DefaultFileRecorder.java
@@ -0,0 +1,24 @@
+package cn.xuyanwu.spring.file.storage.recorder;
+
+import cn.xuyanwu.spring.file.storage.FileInfo;
+
+/**
+ * 默认的文件记录者类,此类并不能真正保存、查询、删除记录,只是用来脱离数据库运行,保证文件上传功能可以正常使用
+ */
+public class DefaultFileRecorder implements FileRecorder {
+ @Override
+ public boolean record(FileInfo fileInfo) {
+ return true;
+ }
+
+ @Override
+ public FileInfo getByUrl(String url) {
+ return null;
+ }
+
+ @Override
+ public boolean delete(String url) {
+ return true;
+ }
+}
+
diff --git a/src/main/java/cn/xuyanwu/spring/file/storage/recorder/FileRecorder.java b/src/main/java/cn/xuyanwu/spring/file/storage/recorder/FileRecorder.java
new file mode 100644
index 0000000..7d02799
--- /dev/null
+++ b/src/main/java/cn/xuyanwu/spring/file/storage/recorder/FileRecorder.java
@@ -0,0 +1,25 @@
+package cn.xuyanwu.spring.file.storage.recorder;
+
+import cn.xuyanwu.spring.file.storage.FileInfo;
+
+/**
+ * 文件记录记录者接口
+ */
+public interface FileRecorder {
+
+ /**
+ * 保存文件记录
+ */
+ boolean record(FileInfo fileInfo);
+
+ /**
+ * 根据 url 获取文件记录
+ */
+ FileInfo getByUrl(String url);
+
+ /**
+ * 根据 url 删除文件记录
+ */
+ boolean delete(String url);
+}
+