文件存储(上传下载)
项目支持将文件上传到三类存储器:
- 兼容 S3 协议的对象存储:支持 MinIO、RustFS、腾讯云 COS、七牛云 Kodo、华为云 OBS、亚马逊 S3 等等。
- 磁盘存储:本地、FTP 服务器、SFTP 服务器。
- 数据库存储:MySQL、Oracle、PostgreSQL、SQL Server 等等。
技术选型?
- 优先,✔ 推荐方案 1。如果无法使用云服务,可以自己搭建一个 MinIO 或 RustFS 服务。MinIO 参见 《芋道 Spring Boot 对象存储 MinIO 入门 》 (opens new window) 文章及本文的「5.1 MinIO 对象存储」小节,RustFS 参见本文的「5.2 RustFS 对象存储」小节。
- 其次,推荐方案 3。数据库的主从机制可以实现高可用,备份也方便,少量小文件问题不大。
- 最后,× 不推荐方案 2。主要是实现高可用比较困难,无法实现故障转移。
# 1. 快速入门
本小节,我们来添加个文件配置,并使用它上传下载文件。
# 1.1 新增配置
① 打开 [基础设施 -> 文件管理 -> 文件配置] 菜单,进入文件配置的界面。

② 点击 [新增] 按钮,选择存储器为【S3 对象存储器】,并填写七牛云的配置。如下图:

- 节点地址:s3.cn-south-1.qiniucs.com
- 存储 bucket:ruoyi-vue-pro
- accessKey:3TvrJ70gl2Gt6IBe7_IZT1F6i_k0iMuRtyEv4EyS
- accessSecret:wd0tbVBYlp0S-ihA8Qg2hPLncoP83wyrIq24OZuY
- 自定义域名:http://test.yudao.iocoder.cn
- 是否 Path Style:
false禁用。如果使用 MinIO、RustFS,建议设置为true启用,看看本文的“MinIO 做了 Nginx 反向代理(提供了独立域名),需要怎么配置?”部分 - 是否公开:
true公开(公开桶)。如果配置false私有(私有桶),看看本文的「7. 私有桶支持」小节 - 区域:一般来说,只有 AWS S3 存储器,才需要配置区域,其他云存储服务,可以不填(会自动识别)。如果使用 MinIO、RustFS,需与服务端的区域一致(默认
us-east-1)。
友善的眼神!
上述七牛云的配置,是艿艿为了大家方便体验,请勿在测试或生产环境体验。
也就是说,测试或生产环境下,请换成自己的七牛、阿里云、腾讯云等等的配置!!!
疑问:MinIO 做了 Nginx 反向代理(提供了独立域名),需要怎么配置?
可见 https://t.zsxq.com/wKmMW (opens new window) 帖子,有 2 种解决方案。
比较推荐的方式,是把“是否 Path Style”设置为 true 启用,原因是 https://www.inlighting.org/archives/aws-s3-virtual-hosted-style-vs-path-style (opens new window) 文档。
③ 添加完后,点击该配置所在行的 [测试] 按钮,测试配置是否正确。

④ 测试通过后,点击该配置所在行的 [主配置] 按钮,设置它为默认的配置,后续使用它进行文件的上传。

# 1.2 上传文件
① 点击 [基础设施 -> 文件管理 -> 文件列表] 菜单,进入文件列表的界面。

② 点击 [上传文件] 按钮,选择要上传的文件。

③ 上传完成后,如果想要删除,可点击该文件所在行的 [删除] 按钮。

# 2. 文件上传
项目提供了 2 种文件上传的方式,分别适合前端、后端使用。
# 2.1 方式一:前端上传
FileController (opens new window) 提供了 /admin-api/infra/file/upload RESTful API,用于前端直接上传文件。
// FileController.java
@PostMapping("/upload")
@Operation(summary = "上传文件")
public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception {
MultipartFile file = uploadReqVO.getFile();
String path = uploadReqVO.getPath();
return success(fileService.createFile(file.getOriginalFilename(), path,
IoUtil.readBytes(file.getInputStream())));
}
前端上传文件的代码如何实现,可见:
- 文件列表,文件上传
index.vue(opens new window) - 个人中心,头像修改
userAvatar.vue(opens new window)
# 2.2 方式二:后端上传
yudao-module-infra 的 FileApi (opens new window) 提供了 #createFile(...) 方法,用于后端需要上传文件的逻辑。
// FileApi.java
/**
* 保存文件,并返回文件的访问路径
*
* @param path 文件路径
* @param content 文件内容
* @return 文件路径
*/
String createFile(String path, byte[] content);
例如说,个人中心修改头像时,需要进行头像的上传。如下图所示:
图片纠错:最新版本不区分 yudao-module-member-api 和 yudao-module-member-biz 子模块,代码直接合并到 yudao-module-member 模块的 src 目录下,更适合单体项目
图片纠错:最新版本不区分 yudao-module-system-api 和 yudao-module-system-biz 子模块,代码直接合并到 yudao-module-system 模块的 src 目录下,更适合单体项目

注意,需要使用到后端上传的 Maven 模块,需要引入 yudao-module-infra-api 依赖。例如说 yudao-module-system 模块的 pom.xml 文件,引用如下:
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-infra</artifactId>
<version>${revision}</version>
</dependency>
# 3. 文件下载
文件上传成功后,返回的是完整的 URL 访问路径,例如说 http://test.yudao.iocoder.cn/822aebded6e6414e912534c6091771a4.jpg (opens new window) 。
不同的文件存储器,返回的 URL 路径的规则是不同的:
① 当存储器是【S3 对象存储】时,支持 HTTP 访问,所以直接使用 S3 对象存储返回的 URL 路径即可。
② 当存储器是【数据库】【本地磁盘】等时,它们只支持存储,所以需要 FileController (opens new window) 提供的 /admin-api/infra/file/{configId}/get/{path} RESTful API,读取文件内容后返回。
// FileController.java
@GetMapping("/{configId}/get/**")
@PermitAll
@Operation(summary = "下载文件")
@Parameter(name = "configId", description = "配置编号", required = true)
public void getFileContent(HttpServletRequest request,
HttpServletResponse response,
@PathVariable("configId") Long configId) throws Exception {
// 获取请求的路径
String path = StrUtil.subAfter(request.getRequestURI(), "/get/", false);
if (StrUtil.isEmpty(path)) {
throw new IllegalArgumentException("结尾的 path 路径必须传递");
}
// 读取内容
byte[] content = fileService.getFileContent(configId, path);
if (content == null) {
log.warn("[getFileContent][configId({}) path({}) 文件不存在]", configId, path);
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
ServletUtils.writeAttachment(response, path, content);
}
# 4. 文件客户端
在 yudao-module-infra 模块中,它的 framework/file (opens new window) 包下,定义了 FileClient (opens new window) 接口,抽象了文件客户端的方法。代码如下所示:
public interface FileClient {
/**
* 获得客户端编号
*
* @return 客户端编号
*/
Long getId();
/**
* 上传文件
*
* @param content 文件流
* @param path 相对路径
* @return 完整路径,即 HTTP 访问地址
*/
String upload(byte[] content, String path);
/**
* 删除文件
*
* @param path 相对路径
*/
void delete(String path);
/**
* 获得文件的内容
*
* @param path 相对路径
* @return 文件的内容
*/
byte[] getContent(String path);
}
FileClient 有 5 个实现类,使用不同存储器进行文件的上传与下载。UML 类图如所示:

文件上传的调用的 UML 时序图如下所示:

# 5. S3 对象存储的配置
做的不错的云存储服务,都是兼容 S3 协议的。如何获取对应的 S3 配置,艿艿整理到了 S3FileClientConfig (opens new window) 配置类。
有一点要注意,如果配置为公开访问,云存储服务的 Bucket 需要设置为公共读,不然 URL 无法访问到文件;如果配置为私有访问,Bucket 保持私有即可,看看本文的「7. 私有桶支持」小节。
并且,最好使用自定义域名,方便迁移到不同的云存储服务。
# 5.1 MinIO 对象存储
MinIO (opens new window) 是兼容 S3 协议的对象存储,直接使用现有的【S3 对象存储器】接入即可,无需新增存储器类型。它的部署,可见 《芋道 Spring Boot 对象存储 MinIO 入门 》 (opens new window) 文章。
① 在 MinIO 控制台,提前创建一个 Bucket,例如说 ruoyi-vue-pro。
② 在 [基础设施 -> 文件管理 -> 文件配置] 菜单,新增一个【S3 对象存储器】的配置:
| 配置项 | 示例 | 说明 |
|---|---|---|
| 节点地址 | http://127.0.0.1:9000 | S3 API 端口,不是 9001 等控制台端口 |
| 存储 bucket | ruoyi-vue-pro | 控制台提前创建的 Bucket |
| accessKey、accessSecret | <your-access-key>、<your-secret-key> | 自己的访问密钥 |
| 自定义域名 | http://127.0.0.1:9000/ruoyi-vue-pro | Path Style 时需带上 /bucket,用于生成文件的访问地址、预签名地址。示例适用于浏览器与 MinIO 在同一台机器;容器、远程部署时,需填写浏览器可访问的域名或 IP |
| 是否 Path Style | true | 建议启用,看看本文的“MinIO 做了 Nginx 反向代理(提供了独立域名),需要怎么配置?”部分 |
| 是否公开 | false | false 私有访问,返回预签名 URL,看看本文的「7. 私有桶支持」小节;true 公开访问,需将 Bucket 设置为匿名只读(例如说 mc anonymous set download),不要开放匿名写入 |
| 区域 | us-east-1 | 需与服务端的区域一致,默认 us-east-1 |
# 5.2 RustFS 对象存储
RustFS (opens new window) 是兼容 S3 协议的开源对象存储,可以作为自建 MinIO 之外的另一种选择。它同样直接使用现有的【S3 对象存储器】接入,无需修改后端依赖,也无需修改前端的存储器类型。下面以 RustFS 1.0.3 (opens new window) 版本为例。
① 使用 Docker 启动 RustFS,参见 《RustFS Docker 安装》 (opens new window) 文档:
docker volume create rustfs-data
docker run -d \
--name rustfs \
--restart unless-stopped \
-p 9000:9000 \
-p 9001:9001 \
-v rustfs-data:/data \
-e RUSTFS_ACCESS_KEY="<your-access-key>" \
-e RUSTFS_SECRET_KEY="<your-secret-key>" \
-e RUSTFS_ADDRESS=":9000" \
-e RUSTFS_CONSOLE_ADDRESS=":9001" \
-e RUSTFS_CONSOLE_ENABLE=true \
rustfs/rustfs:1.0.3 \
/data
9000端口:S3 API,后端的“节点地址”使用它;9001端口:Web 控制台。RUSTFS_ACCESS_KEY、RUSTFS_SECRET_KEY:替换成自己的访问密钥,不要使用默认值。- 如果本机已有 MinIO 等服务占用
9000、9001端口,或已存在同名的容器、数据卷,请调整宿主机端口、容器名、数据卷名,避免覆盖已有的服务与数据。
② 访问 http://127.0.0.1:9001 (opens new window) 控制台,使用上述访问密钥登录,创建一个 Bucket,例如说 ruoyi-vue-pro。
③ 在 [基础设施 -> 文件管理 -> 文件配置] 菜单,新增一个【S3 对象存储器】的配置:
| 配置项 | 示例 | 说明 |
|---|---|---|
| 节点地址 | http://127.0.0.1:9000 | S3 API 端口,不是 9001 控制台端口 |
| 存储 bucket | ruoyi-vue-pro | 控制台创建的 Bucket |
| accessKey、accessSecret | <your-access-key>、<your-secret-key> | 启动时设置的访问密钥 |
| 自定义域名 | http://127.0.0.1:9000/ruoyi-vue-pro | Path Style 时需带上 /bucket,用于生成文件的访问地址、预签名地址。示例适用于浏览器与 RustFS 在同一台机器;容器、远程部署时,需填写浏览器可访问的域名或 IP |
| 是否 Path Style | true | RustFS 默认使用 Path Style;虚拟主机风格需服务端额外配置 RUSTFS_SERVER_DOMAINS |
| 是否公开 | false | false 私有访问,返回预签名 URL,看看本文的「7. 私有桶支持」小节;true 公开访问,见下方 ④ |
| 区域 | us-east-1 | 需与服务端的 RUSTFS_REGION 一致,默认 us-east-1 |
项目的 MySQL 初始化脚本已提供“RustFS 存储(示例)”配置,需先按上述步骤部署 RustFS、创建 Bucket,并替换成自己的地址和密钥,点击 [测试] 按钮确认通过后,才设置为主配置。
④ 如果需要公开访问(“是否公开”为 true),需在 RustFS 控制台该 Bucket 的 [存储桶设置] 中,将策略设置为【公有】并保存(匿名可读取文件,匿名上传会被拒绝)。注意,“是否公开”只决定返回的 URL 是否签名,不会自动修改 Bucket 的权限。
⑤ 后端上传(前端 => 后端 => RustFS)无需配置跨域;前端直传(看看本文的「6. 前端直传 S3 存储」小节)需要配置跨域。在 ① 的启动命令中,增加 RUSTFS_CORS_ALLOWED_ORIGINS 环境变量,填写前端的真实访问地址(协议 + 域名 + 端口),多个使用逗号分隔,不建议使用 *。例如说,yudao-ui-admin-vue3 本地默认使用 80 端口:
-e RUSTFS_CORS_ALLOWED_ORIGINS="http://localhost" \
环境变量修改后,需要重新创建容器才会生效(数据保存在 rustfs-data 数据卷中)。也可以通过 S3 工具为 Bucket 配置 CORS 规则,允许前端来源的 PUT、GET 方法及 Content-Type 请求头。更多可见 《RustFS CORS 配置》 (opens new window) 文档。
⑥ 如果从 MinIO 迁移,需要使用 S3 工具将文件复制到 RustFS,并保持 Bucket 与文件路径不变;不能直接复用 MinIO 的数据目录。
更多 RustFS 的 S3 协议说明,可见 《RustFS S3 协议》 (opens new window) 文档。
# 6. 前端直传 S3 存储【推荐】
友情提示:目前仅 yudao-ui-admin-vue3 前端项目支持,Vue3 + Element Plus 版本
前面小节的文件上传,都是 前端 => 后端 => S3 存储器 的方式。这种方式,有一个问题,就是文件的流量会经过后端,如果后端的服务器带宽不够,就会影响文件的上传速度。例如说:上传文件有 10MB,后端服务器带宽只有 1MB,那么上传文件就需要 10 秒。如果多个人上传文件,就会导致后端服务器的带宽被占满。
因此,更加推荐采用 前端 => S3 存储器 的方式,即前端直传 S3 存储器。这样,文件的流量不会经过后端,上传速度会更快。例如说:上传文件有 10MB,用户的带宽有 100MB,那么上传文件就需要 0.1 秒。
下面,我们以七牛云的配置为例,演示如何在前端直传 S3 存储器。当然,其它阿里云、腾讯云、华为云等等都是类似的。
# 6.1 新增 S3 配置
在 [基础设施 -> 文件管理 -> 文件配置] 菜单,新增一个 S3 对象存储器的配置,填写七牛云的配置,并设置它为默认的配置。结果如下图所示:

实际上,这个步骤和「2.1 新增步骤」是一样的哈!
# 6.2 配置 S3 跨域
友情提示:这个步骤,是为了解决前端直传 S3 存储器的跨域问题。
- 七牛云的跨域配置:参见 《设置跨域资源共享 》 (opens new window)
- 阿里云的跨域配置:参见 《阿里云 OSS 设置跨域访问》 (opens new window)
- 腾讯云的跨域配置:参见 《设置跨域访问》 (opens new window)
- RustFS 的跨域配置:参见本文的「5.2 RustFS 对象存储」小节
如下是七牛云的跨域配置截图:

# 6.3 配置前端直传
修改 yudao-ui-admin-vue3 前端项目的配置文件的 VITE_UPLOAD_TYPE 为 client 前端直传模式。例如说,你是本地环境,则修改 .env.local 文件,如下图所示:

# 6.4 测试上传文件
点击 [基础设施 -> 文件管理 -> 文件列表] 菜单,测试上传文件。结果如下图所示:

具体的代码实现:
- 前端:
src/components/UploadFile/src/useUpload.ts文件 - 后端:FileController 的
/presigned-urlRESTful API
# 7. 私有桶支持
① 如果 S3 对象存储器的 Bucket 是私有的,那么需要使用预签名 URL 的方式,才能访问到文件。
需要使用 FileApi 的 #presignGetUrl(url, expirationSeconds) 方法,获取预签名 URL,并进行返回!!!
② 默认情况下,[基础设施 -> 文件管理 -> 文件列表] 菜单的文件列表,无法查看私有桶的文件。
如果需要,可在 FileController 的 #getFilePage(...) 方法中,进行预签名 URL 的处理。
③ 数据新增或修改时,提交的 URL 地址是预签名的 Query 参数,可以后端通过 HttpUtils 的 #removeUrlQuery(url) 方法进行移除。
因为,预签名 URL 的 Query 参数是有过期时间的,过期后就无法访问了,并且往往比较长,超过了数据库的字段长度。
# 666. 社区贡献的文件相关
- 文件存储支持相对路径:#1334 (opens new window)、#778 (opens new window)
- 支持 Hbase 文件存储格式 (opens new window)
- 《Pull Request:增加图片自动压缩》 (opens new window)
- 《Pull Request:实现流式文件上传与下载,支持大文件上传》 (opens new window)
- 《Pull Request:upload first file item from clipboard》 (opens new window)
- 《Pull Request:增加 minio 的专属客户端、使用 minio 客户端实现断点续传的功能》 (opens new window)
- S3 文件客户端支持基础路径,可作为腾讯云 COS 桶下的自定义虚拟根路径:#1502 (opens new window)、#855 (opens new window)