文件与对象存储
# 文件与对象存储
# 目的
让读者在租户内完成受权限约束的文件上传、授权下载和删除,并正确开通 S3 或单机 local 存储。成功上传后列表出现 ready 记录;删除后短期内不可再下载,worker 异步清理对象字节。
# 前置条件
schema v16 已迁移;目标租户套餐含 infra.files,账号具备 infra:file:* 权限。API 与 worker 必须使用相同的 APP_STORAGE_* 配置。disabled 时仅元数据与预约删除,上传/下载返回 503001。
文件管理提供租户内上传、授权下载、元数据查询和异步删除。文件字节保存在私有 S3 兼容桶,或保存在操作者预建的本地目录。应用只保存元数据与清理状态;不提供公共对象地址、预签名 URL 或直接绕过应用权限的下载。
# 开通与配置
- 显式迁移到 schema v16,再启动匹配版本的 API、worker 和前端,参见 数据库迁移。
- 平台管理员在可编辑套餐中加入
infra.files,再分配给目标租户。迁移不自动扩大已有套餐,保留的legacy套餐不能编辑。 - 租户管理员按需分配
infra:file:read/create/download/delete。保留的tenant_admin角色有这些权限,仍受套餐上限约束。 - 配置 API 与 worker 使用相同的对象存储,再重新获取导航打开「文件管理」。平台身份不能直接代入租户操作文件。
默认 storage.driver: disabled,只允许元数据查询和预约删除,上传、下载返回 503001;worker 不执行对象清理。禁用不会改用内存或本地目录。启用时通过 统一配置 注入,s3 与 local 二选一:
| 环境变量 | 用途 |
|---|---|
APP_STORAGE_DRIVER=s3 | 启用通用 S3 客户端 |
APP_STORAGE_ENDPOINT | 明确的 S3 兼容服务地址;生产必须 HTTPS |
APP_STORAGE_REGION | 签名 region,默认 us-east-1,按对象服务要求设置 |
APP_STORAGE_BUCKET | 操作者预建的私有桶 |
APP_STORAGE_ACCESS_KEY / APP_STORAGE_SECRET_KEY | 运行时凭证,不能写入 Git、租户参数或日志 |
APP_STORAGE_PATH_STYLE=true | 本地服务通常使用 path-style;也可显式设 false 使用虚拟主机寻址 |
APP_STORAGE_DRIVER=local | 把对象字节写到本机目录。不读取 S3 endpoint 或凭证 |
APP_STORAGE_ROOT | 预建的绝对路径。拒绝相对路径、文件系统根和符号链接 |
使用固定版本官方 AWS SDK 的 S3 协议客户端,通过自定义 endpoint 连接兼容服务,不加载云账号发现或 AWS 专属服务。当前真实联调对象是 RustFS 1.0.3;其他 S3 实现仍需按自己的 endpoint、region、权限和 TLS 验证,不把协议兼容理解为全部厂商认证。
桶必须保持私有,且从未开启过版本化。Enabled 和 Suspended 都拒绝:普通删除在版本桶中可能只生成删除标记,不能据此声称字节已清理。启动和每次清理都会检查该前提;操作者负责限制桶策略和配置变更。运行账号至少需要桶检查/版本状态读取,以及对象 Put/Get/Delete 权限;应用不创建桶、不修改策略,不需要创建公共资源的权限。
local 适合单机上的 API 与 worker 共用同一目录。目录必须事先创建,应用不创建根目录。根目录本身不能是符号链接;路径中间已有的系统链接(例如 macOS 的 /var)仍可使用。其他用户不能有读、写或执行权限(例如 0700)。同一受信服务组可以用 0770:子目录保留该组的进入和删除权限,并继承根目录的用户组;对象文件仍是 0600,同组成员不能读内容,但可以删除。这个组可以替换根目录里的名字,不设置 sticky,否则另一个服务用户无法清理对象。API 与 worker 可以是同一用户,也可以是该组里的两个用户,并且必须配置同一个 APP_STORAGE_ROOT。
上传在改名前后同步文件和父目录。成功返回后才允许把元数据发布为可下载;同步失败不会发布。键路径中指向目录外的符号链接会导致该次写入失败,不会把字节写到根外。读取失败只返回固定错误,不带根路径或对象键。多副本若不共享这一目录,不能用 local 代替对象存储。进程崩溃可能留下 .part- 临时文件,这些文件不能通过下载接口读出,可由操作者在确认没有进行中的上传后删除。
单机 local 最小配置(须先 mkdir 并 chmod 0700 绝对目录;与 S3 二选一):
export APP_STORAGE_DRIVER=local
export APP_STORAGE_ROOT=/var/fago/object-store
# API 与 worker 启动前使用相同变量;不要同时设置 S3 endpoint/凭证。
本地可使用 deploy/compose/storage.yaml 单独启动 RustFS。先在当前 shell 设置独立随机的 FAGO_STORAGE_ACCESS_KEY 和 FAGO_STORAGE_SECRET_KEY,然后执行:
docker compose -f deploy/compose/storage.yaml up -d
S3 endpoint 为 http://127.0.0.1:19000,控制台为 http://127.0.0.1:19001。在控制台用本次凭证预建私有且未版本化的桶,再把配置注入 API 和 worker。该单节点配置只用于本地联调;命名卷保留数据,停止容器不会删除卷。
# 上传、下载和删除
页面先选择文件,再点击「上传文件」,选择动作本身不发送。单文件原始内容最多 5 MiB,仅接受 PNG、JPEG、PDF、纯文本,后缀必须分别对应 .png、.jpg/.jpeg、.pdf、.txt。服务端检查实际 MIME,不只相信选择器;拒绝路径、控制字符、空内容和伪造类型。每租户最多 1000 条未完成删除的记录。
上传先预约 uploading,在数据库事务外写对象,再复核当前会话、租户、权限、期限和版本后发布为 ready。上传中撤权、退出、删除或预约超时都不能迟到发布。浏览器切换身份或离开页面时,未发送的文件不会借新会话上传。
只有 ready 能下载。服务端在对象读取前后复核授权及版本,校验长度和 SHA256,并成功提交下载审计后才返回字节。下载走统一 JSON/JWE 出口,浏览器生成临时 Blob 下载;不会内联执行文件内容。每实例限制四个在途上传和四个在途下载;超过额度返回 503001,页面不会自动重传。
删除携带当前版本,成功后立即撤销下载,状态为 cleanup_pending。独立 worker 默认每 3 秒扫描一批,失败约 30 秒后重试;成功后状态为 deleted 并从列表隐藏。清理租约与随机 token 阻止旧执行者覆盖新状态;删除标记仍长期保留、每小时再次清理,覆盖远端迟到 Put 的情况。不要手工删除这些恢复元数据。
文件权限属于租户级资源,未实现按部门或每文件 ACL。不可变文档版本是另一套目录:确认本人已上传的文件,保留留存、legal hold 和精确引用,不把文件列表本身变成版本库。当前提供可选扫描器接入端口,扫描失败会拒绝上传;默认没有真实恶意代码扫描后端,类型校验不能代替恶意代码检测。
# 接口和失败处理
根路径为 /api/admin/v1/infra/files,不接受客户端租户选择器或对象键。上传正文为 JSON,content 为标准 base64(开发/测试明文;生产走 JWE):
POST /api/admin/v1/infra/files
Origin: http://127.0.0.1:5174
X-App-Request: admin
Authorization: Bearer <access>
Content-Type: application/json
{
"name": "readme.txt",
"mime": "text/plain",
"content": "SGVsbG8="
}
成功示例:
{
"code": 0,
"data": {
"id": "1",
"status": "ready",
"version": 1
},
"message": "ok",
"requestId": "req_example"
}
存储关闭或依赖故障时返回 503001:
{
"code": 503001,
"message": "依赖服务暂时不可用",
"requestId": "req_example"
}
| 操作 | 接口 |
|---|---|
| 列表 | GET /,使用 pageNo/pageSize/keyword |
| 上传 | POST /,JSON 字段 name、mime、标准 base64 content |
| 下载 | GET /{id}/content |
| 删除 | DELETE /{id}?version=N,JSON 正文 {} |
所有请求使用现有身份、权限、Origin 与统一加密协议;生产的查询参数也放在加密上下文中。只有精确的文件上传路由扩大正文限额,普通 JSON 接口保持原限额。
RSA 与国密客户端对 POST /api/admin/v1/infra/files 允许最多 8 MiB JSON 正文,绑定上下文另外保留 4 KiB;原始文件仍限 5 MiB。其他方法或相邻路径不会获得该额度,普通业务正文仍限 16 KiB。超额在发送前提示「请求内容过大」,不会自动拆分或重发。
反向代理还需容纳 base64 与 JWE 编码膨胀。仓库 nginx 模板为精确文件路径设置 16 MiB 线报文上限,其他 API 保持 1 MiB 代理上限,后端继续执行更严格的业务额度与权限检查。部署时保留模板中的公共代理头和超时设置,并替换为实际 upstream:
location = /api/admin/v1/infra/files {
client_max_body_size 16m;
proxy_pass http://api:8081;
}
如上传返回 nginx 的 413 而没有业务 JSON/JWE,检查当前生效的代理配置。安装本机 nginx 后,可在仓库根目录独立验证模板;测试不连接业务服务,不需要上传真实文件,成功输出 OK。缺少 nginx 时会明确跳过,不能算验证通过:
python3 scripts/test-proxy-upload.py
# 验证精确文件路径可通过 10/16 MiB,超额及相邻路径返回 413。
参数/类型不合法返回 400002,权限不足为 403001,跨租户或不可下载文件为 404001,旧版本为 409001。存储、数据库、扫描或审计故障为 503001。版本冲突先刷新;上传超时先查看列表,不能盲目重发,因为远端或数据库可能已经提交。存储关闭或 worker 停止时,待清理元数据保留,恢复相同配置后继续处理。
业务元数据和审计同事务;审计只包含 ID、状态、版本、大小与类型,不含文件名、内容、对象键、上传 token 或凭证。备份恢复需同时规划数据库和私有对象数据,单独恢复元数据不等于恢复文件字节。双库及真实对象测试边界见 M5 实施记录。