初始代码
This commit is contained in:
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
@@ -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*
|
||||||
83
README.md
Normal file
83
README.md
Normal file
@@ -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` ,
|
||||||
|
|
||||||
|
在 `<servers></servers>`节点增加 `<server></server>` ,如下所示:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- 发布版 -->
|
||||||
|
<server>
|
||||||
|
<id>maven-releases</id>
|
||||||
|
<username>admin(账号,结合私服配置设置)</username>
|
||||||
|
<password>123456(密码,结合私服配置设置)</password>
|
||||||
|
</server>
|
||||||
|
```
|
||||||
|
#### 4.3.2 配置项目
|
||||||
|
|
||||||
|
> 注意:pom.xml里 `<id>` 和 setting.xml 配置里 `<id>` 对应。
|
||||||
|
|
||||||
|
IDEA打开 `yunzhupaas-common` 项目, 修改 `yunzhupaas-dependencies/pom.xml` 文件中私服配置
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<distributionManagement>
|
||||||
|
<repository>
|
||||||
|
<id>maven-releases</id>
|
||||||
|
<name>maven-releases</name>
|
||||||
|
<url>http://nexus.yunzhupaas.com/repository/maven-releases/</url>
|
||||||
|
</repository>
|
||||||
|
</distributionManagement>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.3.3 发布到私服
|
||||||
|
|
||||||
|
在IDEA中,双击右侧 `Maven` 中 `yunzhupaas-file-core-starter` > `Lifecycle` > `deploy` 发布至私服。
|
||||||
172
pom.xml
Normal file
172
pom.xml
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<artifactId>yunzhupaas-dependencies</artifactId>
|
||||||
|
<groupId>com.yunzhupaas</groupId>
|
||||||
|
<version>5.2.0-RELEASE</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.yunzhupaas</groupId>
|
||||||
|
|
||||||
|
<artifactId>yunzhupaas-file-core-starter</artifactId>
|
||||||
|
<version>5.2.0-RELEASE</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
|
||||||
|
<!-- WebDAV -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.lookfirst</groupId>
|
||||||
|
<artifactId>sardine</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- SFTP -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.jcraft</groupId>
|
||||||
|
<artifactId>jsch</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- FTP -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-net</groupId>
|
||||||
|
<artifactId>commons-net</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!--糊涂工具类扩展-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-extra</artifactId>
|
||||||
|
<version>${hutool.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- AWS S3 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.amazonaws</groupId>
|
||||||
|
<artifactId>aws-java-sdk-s3</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- MinIO -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.minio</groupId>
|
||||||
|
<artifactId>minio</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 又拍云 USS -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.upyun</groupId>
|
||||||
|
<artifactId>java-sdk</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 百度云 BOS -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baidubce</groupId>
|
||||||
|
<artifactId>bce-java-sdk</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-core</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>jdk.tools</groupId>
|
||||||
|
<artifactId>jdk.tools</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 腾讯云 COS -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.qcloud</groupId>
|
||||||
|
<artifactId>cos_api</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 七牛云 Kodo -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.qiniu</groupId>
|
||||||
|
<artifactId>qiniu-java-sdk</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 阿里云 OSS -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.aliyun.oss</groupId>
|
||||||
|
<artifactId>aliyun-sdk-oss</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 华为云 OBS -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.huaweicloud</groupId>
|
||||||
|
<artifactId>esdk-obs-java</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!--糊涂工具类核心-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-core</artifactId>
|
||||||
|
<version>${hutool.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 图片处理 https://github.com/coobird/thumbnailator -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.coobird</groupId>
|
||||||
|
<artifactId>thumbnailator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.yunzhupaas</groupId>
|
||||||
|
<artifactId>yunzhupaas-common-core</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
|
||||||
|
</project>
|
||||||
139
src/main/java/cn/xuyanwu/spring/file/storage/Downloader.java
Normal file
139
src/main/java/cn/xuyanwu/spring/file/storage/Downloader.java
Normal file
@@ -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<FileStorageAspect> 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<FileStorageAspect> aspectList,FileStorage fileStorage,Integer target) {
|
||||||
|
this.fileStorage = fileStorage;
|
||||||
|
this.aspectList = aspectList;
|
||||||
|
this.fileInfo = fileInfo;
|
||||||
|
this.target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置下载进度监听器
|
||||||
|
* @param progressListener 提供一个参数,表示已传输字节数
|
||||||
|
*/
|
||||||
|
public Downloader setProgressMonitor(Consumer<Long> progressListener) {
|
||||||
|
return setProgressMonitor((progressSize,allSize) -> progressListener.accept(progressSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置下载进度监听器
|
||||||
|
* @param progressListener 提供两个参数,第一个是 progressSize已传输字节数,第二个是 allSize总字节数
|
||||||
|
*/
|
||||||
|
public Downloader setProgressMonitor(BiConsumer<Long,Long> 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<InputStream> 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
|
|
||||||
105
src/main/java/cn/xuyanwu/spring/file/storage/FileInfo.java
Normal file
105
src/main/java/cn/xuyanwu/spring/file/storage/FileInfo.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<LocalFileStorage> 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<LocalPlusFileStorage> 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<HuaweiObsFileStorage> 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<AliyunOssFileStorage> 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<QiniuKodoFileStorage> 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<TencentCosFileStorage> 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<BaiduBosFileStorage> 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<UpyunUssFileStorage> 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<MinIOFileStorage> 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<AwsS3FileStorage> 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<FtpFileStorage> 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<SftpFileStorage> 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<WebDavFileStorage> 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<List<? extends FileStorage>> fileStorageLists,
|
||||||
|
List<FileStorageAspect> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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> local = new ArrayList<>();
|
||||||
|
/**
|
||||||
|
* 本地存储
|
||||||
|
*/
|
||||||
|
private List<LocalPlus> localPlus = new ArrayList<>();
|
||||||
|
/**
|
||||||
|
* 华为云 OBS
|
||||||
|
*/
|
||||||
|
private List<HuaweiObs> huaweiObs = new ArrayList<>();
|
||||||
|
/**
|
||||||
|
* 阿里云 OSS
|
||||||
|
*/
|
||||||
|
private List<AliyunOss> aliyunOss = new ArrayList<>();
|
||||||
|
/**
|
||||||
|
* 七牛云 Kodo
|
||||||
|
*/
|
||||||
|
private List<QiniuKodo> qiniuKodo = new ArrayList<>();
|
||||||
|
/**
|
||||||
|
* 腾讯云 COS
|
||||||
|
*/
|
||||||
|
private List<TencentCos> tencentCos = new ArrayList<>();
|
||||||
|
/**
|
||||||
|
* 百度云 BOS
|
||||||
|
*/
|
||||||
|
private List<BaiduBos> baiduBos = new ArrayList<>();
|
||||||
|
/**
|
||||||
|
* 又拍云 USS
|
||||||
|
*/
|
||||||
|
private List<UpyunUSS> upyunUSS = new ArrayList<>();
|
||||||
|
/**
|
||||||
|
* MinIO USS
|
||||||
|
*/
|
||||||
|
private List<MinIO> minio = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AWS S3
|
||||||
|
*/
|
||||||
|
private List<AwsS3> awsS3 = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FTP
|
||||||
|
*/
|
||||||
|
private List<FTP> ftp = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FTP
|
||||||
|
*/
|
||||||
|
private List<SFTP> sftp = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebDAV
|
||||||
|
*/
|
||||||
|
private List<WebDAV> 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 = "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<FileStorage> fileStorageList;
|
||||||
|
private FileStorageProperties properties;
|
||||||
|
private CopyOnWriteArrayList<FileStorageAspect> 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<FileInfo> predicate) {
|
||||||
|
return delete(getFileInfoByUrl(url),predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据条件
|
||||||
|
*/
|
||||||
|
public boolean delete(FileInfo fileInfo) {
|
||||||
|
return delete(fileInfo,null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据条件删除文件
|
||||||
|
*/
|
||||||
|
public boolean delete(FileInfo fileInfo,Predicate<FileInfo> predicate) {
|
||||||
|
if (fileInfo == null) return true;
|
||||||
|
if (predicate != null && !predicate.test(fileInfo)) return false;
|
||||||
|
FileStorage fileStorage = 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/main/java/cn/xuyanwu/spring/file/storage/PathUtil.java
Normal file
38
src/main/java/cn/xuyanwu/spring/file/storage/PathUtil.java
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<Thumbnails.Builder<? extends InputStream>> consumer) {
|
||||||
|
try (InputStream in = fileWrapper.getInputStream()) {
|
||||||
|
Thumbnails.Builder<? extends InputStream> builder = Thumbnails.of(in);
|
||||||
|
consumer.accept(builder);
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
builder.toOutputStream(out);
|
||||||
|
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<Thumbnails.Builder<? extends InputStream>> 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<Thumbnails.Builder<? extends InputStream>> consumer,MultipartFile file) {
|
||||||
|
try {
|
||||||
|
return thumbnail(consumer,file.getInputStream());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FileStorageRuntimeException("生成缩略图失败!",e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过指定 InputStream 生成缩略图并进行图片处理,
|
||||||
|
* 可以进行裁剪、旋转、缩放、水印等操作,默认输出图片格式通过 thumbnailSuffix 获取,
|
||||||
|
* 操作完成后会自动关闭 InputStream
|
||||||
|
*/
|
||||||
|
public UploadPretreatment thumbnail(Consumer<Thumbnails.Builder<? extends InputStream>> consumer,InputStream in) {
|
||||||
|
try {
|
||||||
|
Thumbnails.Builder<? extends InputStream> builder = Thumbnails.of(in);
|
||||||
|
builder.outputFormat(FileNameUtil.extName(thumbnailSuffix));
|
||||||
|
consumer.accept(builder);
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
builder.toOutputStream(out);
|
||||||
|
thumbnailBytes = out.toByteArray();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<FileStorageAspect> aspectIterator;
|
||||||
|
|
||||||
|
public DeleteAspectChain(Iterable<FileStorageAspect> aspects,DeleteAspectChainCallback callback) {
|
||||||
|
this.aspectIterator = aspects.iterator();
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用下一个切面
|
||||||
|
*/
|
||||||
|
public boolean next(FileInfo fileInfo,FileStorage fileStorage,FileRecorder fileRecorder) {
|
||||||
|
if (aspectIterator.hasNext()) {//还有下一个
|
||||||
|
return aspectIterator.next().deleteAround(this,fileInfo,fileStorage,fileRecorder);
|
||||||
|
} else {
|
||||||
|
return callback.run(fileInfo,fileStorage,fileRecorder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<FileStorageAspect> aspectIterator;
|
||||||
|
|
||||||
|
public DownloadAspectChain(Iterable<FileStorageAspect> aspects,DownloadAspectChainCallback callback) {
|
||||||
|
this.aspectIterator = aspects.iterator();
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用下一个切面
|
||||||
|
*/
|
||||||
|
public void next(FileInfo fileInfo,FileStorage fileStorage,Consumer<InputStream> consumer) {
|
||||||
|
if (aspectIterator.hasNext()) {//还有下一个
|
||||||
|
aspectIterator.next().downloadAround(this,fileInfo,fileStorage,consumer);
|
||||||
|
} else {
|
||||||
|
callback.run(fileInfo,fileStorage,consumer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<InputStream> consumer);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<FileStorageAspect> aspectIterator;
|
||||||
|
|
||||||
|
public DownloadThAspectChain(Iterable<FileStorageAspect> aspects,DownloadThAspectChainCallback callback) {
|
||||||
|
this.aspectIterator = aspects.iterator();
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用下一个切面
|
||||||
|
*/
|
||||||
|
public void next(FileInfo fileInfo,FileStorage fileStorage,Consumer<InputStream> consumer) {
|
||||||
|
if (aspectIterator.hasNext()) {//还有下一个
|
||||||
|
aspectIterator.next().downloadThAround(this,fileInfo,fileStorage,consumer);
|
||||||
|
} else {
|
||||||
|
callback.run(fileInfo,fileStorage,consumer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<InputStream> consumer);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<FileStorageAspect> aspectIterator;
|
||||||
|
|
||||||
|
public ExistsAspectChain(Iterable<FileStorageAspect> aspects,ExistsAspectChainCallback callback) {
|
||||||
|
this.aspectIterator = aspects.iterator();
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用下一个切面
|
||||||
|
*/
|
||||||
|
public boolean next(FileInfo fileInfo,FileStorage fileStorage) {
|
||||||
|
if (aspectIterator.hasNext()) {//还有下一个
|
||||||
|
return aspectIterator.next().existsAround(this,fileInfo,fileStorage);
|
||||||
|
} else {
|
||||||
|
return callback.run(fileInfo,fileStorage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<InputStream> consumer) {
|
||||||
|
chain.next(fileInfo,fileStorage,consumer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载缩略图文件,成功返回文件内容
|
||||||
|
*/
|
||||||
|
default void downloadThAround(DownloadThAspectChain chain,FileInfo fileInfo,FileStorage fileStorage,Consumer<InputStream> consumer) {
|
||||||
|
chain.next(fileInfo,fileStorage,consumer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<FileStorageAspect> aspectIterator;
|
||||||
|
|
||||||
|
public UploadAspectChain(Iterable<FileStorageAspect> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<InputStream> 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<InputStream> 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<OSSObjectSummary> list = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
ObjectListing objectListing = null;
|
||||||
|
ListObjectsRequest listObjectsRequest = new ListObjectsRequest(bucketName);
|
||||||
|
listObjectsRequest.setPrefix(this.getBasePath() + folderName);
|
||||||
|
objectListing = getClient().listObjects(listObjectsRequest);
|
||||||
|
List<OSSObjectSummary> sums = objectListing.getObjectSummaries();
|
||||||
|
for (OSSObjectSummary result : sums) {
|
||||||
|
list.add(result);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FileStorageRuntimeException("文件获取失败!platform:" + platform, e);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<FileListVO> conversionList(String folderName) {
|
||||||
|
List fileList = getFileList(folderName);
|
||||||
|
List<FileListVO> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<InputStream> 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<InputStream> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<InputStream> 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<InputStream> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<InputStream> consumer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载缩略图文件
|
||||||
|
*/
|
||||||
|
void downloadTh(FileInfo fileInfo, Consumer<InputStream> 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<FileListVO> conversionList(String folderName) {
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载到本地
|
||||||
|
*
|
||||||
|
* @param folderName 文件夹名
|
||||||
|
* @param filePath 下载到本地文件路径
|
||||||
|
* @param objectName 文件名
|
||||||
|
*/
|
||||||
|
default void downLocal(String folderName, String filePath, String objectName) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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<InputStream> 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<InputStream> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<InputStream> 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<InputStream> 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<FileListVO> conversionList(String folderName) {
|
||||||
|
List fileList = getFileList(folderName);
|
||||||
|
List<FileListVO> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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<InputStream> 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<InputStream> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<InputStream> 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<InputStream> 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<FileModel> data = new ArrayList<>();
|
||||||
|
File filePath = new File(XSSEscape.escapePath(getLocalPath() + folderName));
|
||||||
|
List<File> 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<FileListVO> conversionList(String folderName) {
|
||||||
|
List fileList = getFileList(folderName);
|
||||||
|
List<FileListVO> listVOS = new ArrayList<>(fileList.size());
|
||||||
|
if (fileList.size() > 0 && fileList.get(0) instanceof FileModel) {
|
||||||
|
return JsonUtil.getJsonToList(fileList, FileListVO.class);
|
||||||
|
}
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<InputStream> 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<InputStream> 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<Item> list = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
Iterable<Result<Item>> results = null;
|
||||||
|
results = getClient().listObjects(
|
||||||
|
ListObjectsArgs.builder().bucket(bucketName).prefix(this.getBasePath() + folderName).recursive(true)
|
||||||
|
.build());
|
||||||
|
for (Result<Item> result : results) {
|
||||||
|
Item item = result.get();
|
||||||
|
list.add(item);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FileStorageRuntimeException("文件获取失败!platform:" + platform, e);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<FileListVO> conversionList(String folderName) {
|
||||||
|
List fileList = getFileList(folderName);
|
||||||
|
List<FileListVO> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<InputStream> 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<InputStream> 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<com.qiniu.storage.model.FileInfo> 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<FileListVO> conversionList(String folderName) {
|
||||||
|
List fileList = getFileList(folderName);
|
||||||
|
List<FileListVO> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<InputStream> 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<InputStream> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<InputStream> 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<InputStream> 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<COSObjectSummary> 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<String> commonPrefixs = objectListing.getCommonPrefixes();
|
||||||
|
// object summary表示所有列出的object列表
|
||||||
|
List<COSObjectSummary> 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<FileListVO> conversionList(String folderName) {
|
||||||
|
List fileList = getFileList(folderName);
|
||||||
|
List<FileListVO> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String,String> 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<String,String> 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<InputStream> 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<InputStream> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<InputStream> 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<InputStream> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user