# spring-file-storage
**Repository Path**: stm/spring-file-storage
## Basic Information
- **Project Name**: spring-file-storage
- **Description**: 在SpringBoot中通过简单的方式将文件存储到本地、阿里云OSS、华为云OBS、七牛云Kodo、腾讯云COS、百度云 BOS、又拍云USS、MinIO。后续即将支持 亚马逊S3、谷歌云存储、FTP、SFTP、WebDAV、Samba、NFS
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 764
- **Created**: 2021-05-26
- **Last Updated**: 2023-10-07
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
X Spring File Storage
### 简介
在SpringBoot中通过简单的方式将文件存储到本地、阿里云OSS、华为云OBS、七牛云Kodo、腾讯云COS、百度云 BOS、又拍云USS、MinIO
后续即将支持 亚马逊S3、谷歌云存储、FTP、SFTP、WebDAV、Samba、NFS
`spring-file-storage` 模块是本体。
`spring-file-storage-test` 模块是测试+使用演示,不需要的情况下可以直接删除。
GitHub:https://github.com/1171736840/spring-file-storage
Gitee:https://gitee.com/XYW1171736840/spring-file-storage
如果你觉得这个项目不错,可以在右上角点个 Star 或捐赠请作者吃包辣条~,在此表示感谢^_^。
点击以下链接,将页面拉到最下方点击“捐赠”即可。
[Gitee上捐赠](https://gitee.com/XYW1171736840/spring-file-storage)
### 使用说明
#### 配置
`pom.xml`引入依赖
```xml
cn.xuyanwu
spring-file-storage
0.2.1
com.huaweicloud
esdk-obs-java
3.20.6.1
com.aliyun.oss
aliyun-sdk-oss
3.6.0
com.qiniu
qiniu-java-sdk
7.4.0
com.qcloud
cos_api
5.6.38
com.baidubce
bce-java-sdk
0.10.162
com.upyun
java-sdk
4.2.2
io.minio
minio
7.0.2
```
`application.yml`配置文件中添加以下相关配置(不使用的平台可以不配置)
```yaml
spring:
file-storage: #文件存储配置
default-platform: local-1 #默认使用的存储平台
thumbnail-suffix: ".min.jpg" #缩略图后缀,例如【.min.jpg】【.png】
local: # 本地存储,不使用的情况下可以不写
- platform: local-1 # 存储平台标识
enable-storage: true #启用存储
enable-access: true #启用访问(线上请使用 Nginx 配置,效率更高)
domain: "" # 访问域名,例如:“http://127.0.0.1:8030/test/file/”,注意后面要和 path-patterns 保持一致,“/”结尾,本地存储建议使用相对路径,方便后期更换域名
base-path: D:/Temp/test/ # 存储地址
path-patterns: /test/file/** # 访问路径,开启 enable-access 后,通过此路径可以访问到上传的文件
huawei-obs: # 华为云 OBS ,不使用的情况下可以不写
- platform: huawei-obs-1 # 存储平台标识
enable-storage: false # 启用存储
access-key: ??
secret-key: ??
end-point: ??
bucket-name: ??
domain: ?? # 访问域名,注意“/”结尾,例如:http://abc.obs.com/
base-path: hy/ # 基础路径
aliyun-oss: # 阿里云 OSS ,不使用的情况下可以不写
- platform: aliyun-oss-1 # 存储平台标识
enable-storage: false # 启用存储
access-key: ??
secret-key: ??
end-point: ??
bucket-name: ??
domain: ?? # 访问域名,注意“/”结尾,例如:https://abc.oss-cn-shanghai.aliyuncs.com/
base-path: hy/ # 基础路径
qiniu-kodo: # 七牛云 kodo ,不使用的情况下可以不写
- platform: qiniu-kodo-1 # 存储平台标识
enable-storage: false # 启用存储
access-key: ??
secret-key: ??
bucket-name: ??
domain: ?? # 访问域名,注意“/”结尾,例如:http://abc.hn-bkt.clouddn.com/
base-path: base/ # 基础路径
tencent-cos: # 腾讯云 COS
- platform: tencent-cos-1 # 存储平台标识
enable-storage: true # 启用存储
secret-id: ??
secret-key: ??
region: ?? #存仓库所在地域
bucket-name: ??
domain: ?? # 访问域名,注意“/”结尾,例如:https://abc.cos.ap-nanjing.myqcloud.com/
base-path: hy/ # 基础路径
baidu-bos: # 百度云 BOS
- platform: baidu-bos-1 # 存储平台标识
enable-storage: true # 启用存储
access-key: ??
secret-key: ??
end-point: ?? # 例如 abc.fsh.bcebos.com
bucket-name: ??
domain: ?? # 访问域名,注意“/”结尾,例如:https://abc.fsh.bcebos.com/abc/
base-path: hy/ # 基础路径
upyun-uss: # 又拍云 USS
- platform: upyun-uss-1 # 存储平台标识
enable-storage: true # 启用存储
username: ??
password: ??
bucket-name: ??
domain: ?? # 访问域名,注意“/”结尾,例如:http://abc.test.upcdn.net/
base-path: hy/ # 基础路径
minio: # MinIO
- platform: minio-1 # 存储平台标识
enable-storage: true # 启用存储
access-key: ??
secret-key: ??
end-point: ??
bucket-name: ??
domain: ?? # 访问域名,注意“/”结尾,例如:http://minio.abc.com/abc/
base-path: hy/ # 基础路径
```
注意配置每个平台前面都有个`-`号,通过以下方式可以配置多个
```yaml
local:
- platform: local-1 # 存储平台标识
enable-storage: true
enable-access: true
domain: ""
base-path: D:/Temp/test/
path-patterns: /test/file/**
- platform: local-2 # 存储平台标识,注意这里不能重复
enable-storage: true
enable-access: true
domain: ""
base-path: D:/Temp/test2/
path-patterns: /test2/file/**
```
#### 编码
在启动类上加上`@EnableFileStorage`注解
```java
@EnableFileStorage
@SpringBootApplication
public class SpringFileStorageTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringFileStorageTestApplication.class, args);
}
}
```
#### 开始使用
```java
@RestController
public class FileDetailController {
@Autowired
private FileStorageService fileStorageService;//注入实列
/**
* 上传文件,成功返回文件 url
*/
@PostMapping("/upload")
public String upload(MultipartFile file) {
FileInfo fileInfo = fileStorageService.of(file)
.setPath("upload/") //保存到相对路径下,为了方便管理,不需要可以不写
.setObjectId("0") //关联对象id,为了方便管理,不需要可以不写
.setObjectType("0") //关联对象类型,为了方便管理,不需要可以不写
.upload(); //将文件上传到对应地方
return fileInfo == null ? "上传失败!" : fileInfo.getUrl();
}
/**
* 上传图片,成功返回文件信息
* 图片处理使用的是 https://github.com/coobird/thumbnailator
*/
@PostMapping("/upload-image")
public FileInfo uploadImage(MultipartFile file) {
return fileStorageService.of(file)
.image(img -> img.size(1000,1000)) //将图片大小调整到 1000*1000
.thumbnail(th -> th.size(200,200)) //再生成一张 200*200 的缩略图
.upload();
}
/**
* 上传文件到指定存储平台,成功返回文件信息
*/
@PostMapping("/upload-platform")
public FileInfo uploadPlatform(MultipartFile file) {
return fileStorageService.of(file)
.setPlatform("aliyun-oss-1") //使用指定的存储平台
.upload();
}
}
```
如果还想使用除了保存文件之前的其它功能,例如删除文件,还需要实现 `FileRecorder` 这个接口,把文件信息保存到数据库中
点击查看详情
```java
/**
* 用来将文件上传记录保存到数据库,这里使用了 MyBatis-Plus 和 Hutool 工具类
*/
@Service
public class FileDetailService extends ServiceImpl implements FileRecorder {
/**
* 保存文件信息到数据库
*/
@Override
public boolean record(FileInfo info) {
FileDetail detail = BeanUtil.copyProperties(info,FileDetail.class);
boolean b = save(detail);
if (b) {
info.setId(detail.getId());
}
return b;
}
/**
* 根据 url 查询文件信息
*/
@Override
public FileInfo getByUrl(String url) {
return BeanUtil.copyProperties(getOne(new QueryWrapper().eq(FileDetail.COL_URL,url)),FileInfo.class);
}
/**
* 根据 url 删除文件信息
*/
@Override
public boolean delete(String url) {
return remove(new QueryWrapper().eq(FileDetail.COL_URL,url));
}
}
```
数据库表结构推荐如下,你也可以根据自己喜好在这里自己扩展
```sql
-- 这里使用的是 mysql
CREATE TABLE `file_detail`
(
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '文件id',
`url` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '文件访问地址',
`size` bigint(20) NULL DEFAULT NULL COMMENT '文件大小,单位字节',
`filename` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件名称',
`original_filename` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '原始文件名',
`base_path` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '基础存储路径',
`path` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '存储路径',
`ext` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件扩展名',
`platform` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '存储平台',
`th_url` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '缩略图访问路径',
`th_filename` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '缩略图名称',
`th_size` bigint(20) NULL DEFAULT NULL COMMENT '缩略图大小,单位字节',
`object_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件所属对象id',
`object_type` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件所属对象类型,例如用户头像,评价图片',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB
AUTO_INCREMENT = 1
CHARACTER SET = utf8
COLLATE = utf8_general_ci COMMENT = '文件记录表'
ROW_FORMAT = Dynamic;
```
#### 自定义存储平台
点击查看详情
想要自定义存储平台就要实现 `FileStorage` 这个接口,并进行实例化,注意返回的 bean 是个 list
这里拿 LocalFileStorage 举例
```java
/**
* 实现 FileStorage 接口,这里使用了 Lombok 和 Hutool 工具类
*/
@Getter
@Setter
public class LocalFileStorage implements FileStorage {
/* 本地存储路径*/
private String basePath;
/* 存储平台 */
private String platform;
/* 访问域名 */
private String domain;
/**
* 保存文件
*/
@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(fileInfo.getUrl() + pre.getThumbnailSuffix());
FileUtil.writeBytes(thumbnailBytes,newFile.getPath() + pre.getThumbnailSuffix());
}
return true;
} catch (IOException e) {
FileUtil.del(newFile);
throw new FileStorageRuntimeException("文件上传失败!platform:" + platform + ",filename:" + fileInfo.getOriginalFilename(),e);
}
}
/**
* 删除文件
*/
@Override
public boolean delete(FileInfo fileInfo) {
if (fileInfo.getThFilename() != null) { //删除缩略图
FileUtil.del(new File(fileInfo.getBasePath() + fileInfo.getPath(),fileInfo.getThFilename()));
}
return FileUtil.del(new File(fileInfo.getBasePath() + fileInfo.getPath(),fileInfo.getFilename()));
}
/**
* 文件是否存在
*/
@Override
public boolean exists(FileInfo fileInfo) {
return new File(fileInfo.getBasePath() + fileInfo.getPath(),fileInfo.getFilename()).exists();
}
/**
* 下载文件
*/
@Override
public void download(FileInfo fileInfo,Consumer consumer) {
try (InputStream in = FileUtil.getInputStream(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getFilename())) {
consumer.accept(in);
} catch (IOException e) {
throw new FileStorageRuntimeException("文件下载失败!platform:" + fileInfo,e);
}
}
/**
* 下载缩略图文件
*/
@Override
public void downloadTh(FileInfo fileInfo,Consumer consumer) {
if (StrUtil.isBlank(fileInfo.getThFilename())) {
throw new FileStorageRuntimeException("缩略图文件下载失败,文件不存在!fileInfo:" + fileInfo);
}
try (InputStream in = FileUtil.getInputStream(fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getThFilename())) {
consumer.accept(in);
} catch (IOException e) {
throw new FileStorageRuntimeException("缩略图文件下载失败!fileInfo:" + fileInfo,e);
}
}
}
/**
* 初始化
*/
@Configuration
public class LocalFileStorageAutoConfiguration {
/**
* 这里拿本地存储做个演示,注意返回的是个List
*/
@Bean
public List localFileStorageList() {
ArrayList list = new ArrayList<>();
LocalFileStorage localFileStorage = new LocalFileStorage();
localFileStorage.setPlatform("my-local-1");//平台名称
localFileStorage.setBasePath("");
localFileStorage.setDomain("");
list.add(localFileStorage);
return list;
}
}
```
#### 自定义上传和删除等切面
点击查看详情
只需要实现`FileStorageAspect`接口即可对文件上传和删除等进行干预。
不需要的方法可以不用实现,此接口里的方法全部都有默认实现
```java
/**
* 使用切面打印文件上传和删除的日志
*/
@Slf4j
@Component
public class LogFileStorageAspect implements FileStorageAspect {
/**
* 上传,成功返回文件信息,失败返回 null
*/
@Override
public FileInfo uploadAround(UploadAspectChain chain,FileInfo fileInfo,UploadPretreatment pre,FileStorage fileStorage,FileRecorder fileRecorder) {
log.info("上传文件 before -> {}",fileInfo);
fileInfo = chain.next(fileInfo,pre,fileStorage,fileRecorder);
log.info("上传文件 after -> {}",fileInfo);
return fileInfo;
}
/**
* 删除文件,成功返回 true
*/
@Override
public boolean deleteAround(DeleteAspectChain chain,FileInfo fileInfo,FileStorage fileStorage,FileRecorder fileRecorder) {
log.info("删除文件 before -> {}",fileInfo);
boolean res = chain.next(fileInfo,fileStorage,fileRecorder);
log.info("删除文件 after -> {}",res);
return res;
}
/**
* 文件是否存在
*/
@Override
public boolean existsAround(ExistsAspectChain chain,FileInfo fileInfo,FileStorage fileStorage) {
log.info("文件是否存在 before -> {}",fileInfo);
boolean res = chain.next(fileInfo,fileStorage);
log.info("文件是否存在 after -> {}",res);
return res;
}
/**
* 下载文件
*/
@Override
public void downloadAround(DownloadAspectChain chain,FileInfo fileInfo,FileStorage fileStorage,Consumer consumer) {
log.info("下载文件 before -> {}",fileInfo);
chain.next(fileInfo,fileStorage,consumer);
log.info("下载文件 after -> {}",fileInfo);
}
/**
* 下载缩略图文件
*/
@Override
public void downloadThAround(DownloadThAspectChain chain,FileInfo fileInfo,FileStorage fileStorage,Consumer consumer) {
log.info("下载缩略图文件 before -> {}",fileInfo);
chain.next(fileInfo,fileStorage,consumer);
log.info("下载缩略图文件 after -> {}",fileInfo);
}
}
```