初始代码

This commit is contained in:
wangmingwei
2026-04-21 17:32:56 +08:00
parent da4c1c21c0
commit 40dd964969
44 changed files with 5081 additions and 0 deletions

35
.gitignore vendored Normal file
View 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*

0
README
View File

83
README.md Normal file
View File

@@ -0,0 +1,83 @@
> 特别说明源码、JDK、数据库、Redis等安装或存放路径禁止包含中文、空格、特殊字符等
## 一 项目结构
```text
yunzhupaas-file-core-starter
├── aspect- 切面层
├── exception- 自定义异常
├── platform - 存储平台实现层
└── recorder- 记录器
```
## 二 环境要求
| 类目 | 版本或建议 |
|-------|---------|
| 硬件 | 开发电脑建议使用I3及以上CPU16G及以上内存 |
| 操作系统 | Windows 10/11MacOS |
| 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
View 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>

View 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));
}
}

View File

@@ -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 {
}

View 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;
}

View File

@@ -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;
}
}
}

View File

@@ -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 = "/";
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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) {
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<>();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}