site logo

Marico's space

ACAI — 第40章:文件与对象存储架构

AI技术与应用 2026-09-09 17:35:16 9

前几章把数据库、认证鉴权、API 基础设施讲清楚了,这章聊聊文件存储这个子系统。

AI(人工智能)平台要处理的用户数据种类不少:

图片
视频
音频
文档
PDF 文件
文本文件
生成式媒体
处理后的输出
临时处理产物

这类二进制数据直接塞进 PostgreSQL 是不行的。

正确的做法是分离:

元数据 ↓
PostgreSQL 大文件二进制数据 ↓
对象存储

最终架构是这样:

 ACAI │ ┌──────┴──────┐ │ │ PostgreSQL 对象存储 │ │ 元数据 二进制数据

分离之后,扩展性、性能、生命周期管理、安全性都能上一个台阶。

40.2 数据库 vs 对象存储

PostgreSQL 适合存结构化信息:

fileId
ownerId
projectId
fileName
mimeType
size
status
storageKey
createdAt

对象存储适合存:

photo.jpg
video.mp4
audio.wav
document.pdf
generated-image.png

数据库在这里扮演"权威元数据层"的角色。

40.3 文件架构

基本流程是:

用户 │ ▼
上传请求 │ ▼
身份认证 │ ▼
权限校验 │ ▼
上传策略 │ ▼
对象存储 │ ▼
元数据记录 │ ▼
处理管道

大文件场景下,可以用直传或者分块上传模式:

客户端 │ ▼
API 请求上传授权 │ ▼
服务端校验请求 │ ▼
临时上传授权 │ ▼
对象存储 │ ▼
上传完成 │ ▼
服务端记录/更新元数据

客户端绝对不能拿到无限制的存储凭证。

40.4 存储 Key 设计

文件应该有内部存储 key,而不是直接依赖用户上传时的原始文件名。

比如:

users/{userId}/projects/{projectId}/files/{fileId}/original

生成的资产可以用:

users/{userId}/projects/{projectId}/generations/{generationId}/output

具体格式可以灵活调整,但 key 应该满足:

  • 唯一
  • 只在必要的地方可预测
  • 和用户提供的文件名解耦
  • 不含敏感信息
  • 支持生命周期策略

40.5 原始文件名 vs 存储 Key

假设用户上传了:

我的度假照片.jpg

应用存的是:

name = "我的度假照片.jpg"

但内部用的 key 是:

users/<用户>/projects/<项目>/files/<文件>/original

这样分离之后,用户文件名就不会成为存储对象的主要标识了。

40.6 文件元数据

之前提到的 File 模型可以扩展成这样:

model File { id String @id @default(uuid()) name String mimeType String sizeBytes BigInt status FileStatus @default(UPLOADING) ownerId String owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) projectId String? project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull) storageKey String? checksum String? metadata Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([ownerId]) @@index([projectId]) @@index([status])
}

可能需要存的元数据包括:

宽度
高度
时长
页数
编码格式
处理状态

只存真正有用的元数据,别什么都往里塞。

40.7 文件生命周期

文件应该有明确的状态流转:

UPLOADING ↓
UPLOADED ↓
PROCESSING ↓
READY

失败路径:

PROCESSING ↓
FAILED

删除:

READY ↓
DELETED

这样设计,应用能区分两种情况:

文件存在但还在处理中

和:

文件处理失败了

40.8 上传校验

安全的上传系统至少要校验这些:

文件大小
声明的 MIME 类型
实际文件特征
扩展名
应用支持的格式
所有权
目标项目
上传授权

客户端提供的文件名或者 MIME 类型绝对不能直接当权威数据用。

40.9 文件大小限制

不同资源类型可以有不同的限制:

头像 ↓
小文件限制 文档 ↓
中等文件限制 视频 ↓
大文件限制

具体数值应该根据产品需求、基础设施容量、压测结果来定。

限制必须由服务端强制执行,不能只靠前端。

40.10 MIME 类型校验

浏览器可能上报:

image/jpeg

但服务端不能盲目信任这个声明。

在合适的情况下,处理管道应该检查文件的实际特征。

原则是:

声明的元数据 +
内容检测 +
应用策略

40.11 扩展名处理

用户可控的文件名里可能包含奇怪的或者误导性的扩展名。

应用不能直接用不可信的文件名来构造敏感的存储行为。

正确做法:

原始文件名 ↓
存为元数据 ↓
内部对象 key

存储 key 必须由应用生成。

40.12 文件所有权

每个私有文件都必须有明确的归属边界。

概念上:

文件 │ ├── ownerId └── projectId

访问前:

当前用户 ↓
项目/文件 ↓
所有权或成员身份 ↓
权限判断

只有权限校验通过之后,应用才提供访问。

40.13 私有存储

用户的私有文件通常不应该通过匿名公开 URL 访问。

推荐模式:

用户 │ ▼
认证 API │ ▼
权限校验 │ ▼
临时授权访问 │ ▼
对象存储

这样设计,拿到一个 URL 也不会自动获得永久访问权。

40.14 临时访问

对于私有对象,如果存储平台支持,应用可以下发短命的授权凭证。

概念上:

请求 ↓
身份认证 ↓
权限校验 ↓
生成临时访问凭证 ↓
客户端获取对象

有效期要控制在实际使用场景所需的范围内。

40.15 上传流程

一个可靠的上传序列:

1. 用户选择文件 ↓
2. 客户端请求上传授权 ↓
3. 服务端认证用户 ↓
4. 服务端校验项目所有权 ↓
5. 服务端校验请求的文件策略 ↓
6. 服务端创建文件元数据 ↓
7. 下发上传授权 ↓
8. 客户端上传对象 ↓
9. 记录上传完成 ↓
10. 创建处理任务

这样形成一条可追溯的生命周期。

40.16 处理管道

上传之后:

对象存储 ↓
处理队列 ↓
Worker ↓
校验 ↓
提取 ↓
转换 ↓
结果 ↓
数据库

文档的处理流程:

PDF ↓
文本提取 ↓
规范化 ↓
分块 ↓
向量化 ↓
向量索引

图片的处理流程:

图片 ↓
元数据提取 ↓
校验 ↓
可选处理 ↓
生成/处理后的资产

40.17 恶意文件和不安全文件处理

文件处理基础设施应该假设所有上传文件都是不可信的。

处理环境要和特权基础设施尽可能隔离。

核心原则:

不可信文件 ↓
受控处理环境 ↓
最小权限 ↓
不必要的网络访问 ↓
校验后的输出

具体安全控制取决于部署架构。

40.18 处理隔离

文件处理 Worker 不应该默认拥有这些权限:

生产库凭证
认证密钥
支付凭证
管理 API
其他用户的文件

Worker 只能拿到任务所需的权限。

这叫最小权限原则。

40.19 临时文件

处理过程中可能需要本地临时存储。

比如:

对象存储 ↓
Worker 临时目录 ↓
处理 ↓
输出存储 ↓
清理临时数据

临时文件不能无限期留存。

生命周期策略要定义清楚:

创建时间
存储位置
访问权限
清理时间

40.20 生成的资产

AI 生成的输出应该作为一等公民来对待。

比如:

生成记录 │ ├── 请求元数据 ├── 模型信息 ├── 状态 └── 输出文件

一次生成可能产出:

图片
视频
音频
文档

输出应该和生成记录有明确的关联关系。

40.21 文件版本控制

编辑系统可能需要版本管理。

比如:

原始版本 ↓
编辑版本 1 ↓
编辑版本 2 ↓
编辑版本 3

可以用这样的模型:

FileVersion ├── id ├── fileId ├── versionNumber ├── storageKey ├── createdAt └── metadata

这对创意类应用特别有用,用户期望有撤销、版本历史、恢复功能。

40.22 衍生资产

一个源文件可能生成多个衍生版本。

比如:

原始图片 │ ├── 缩略图 ├── 预览图 ├── 优化版本 └── 编辑版本

数据库要维护源资产和衍生资产的关联关系。

这样应用才不会丢失对输出来源的追踪。

40.23 内容哈希

校验和可以帮助检测意外损坏或者识别相同内容。

概念上:

文件 ↓
哈希函数 ↓
校验和

校验和可以存在元数据里。

但校验和不能被当成万能的安全机制来用。

它的具体作用要定义清楚。

40.24 存储配额

用户可以有存储上限。

概念上:

用户 │ ├── 已用存储 ├── 存储上限 └── 可用存储

接受上传之前:

请求大小 ↓
当前用量 ↓
配额检查 ↓
允许 / 拒绝

配额计算必须在服务端执行。

40.25 项目级配额

同样原则可以应用到项目级别。

账户配额 │ ├── 项目 A ├── 项目 B └── 项目 C

项目可以有自己独立的资源限制。

这在协作或组织环境下很有用。

40.26 文件删除

删除文件应该是受控的工作流。

删除请求 ↓
身份认证 ↓
权限校验 ↓
标记已删除 ↓
清理存储 ↓
审计事件

对于重要数据,延迟删除策略可能更合适:

ACTIVE ↓
MARKED_FOR_DELETION ↓
保留期 ↓
永久删除

这样可以提供恢复窗口。

40.27 孤儿对象检测

分布式系统可能产生数据不一致。

比如:

数据库记录存在
但对象缺失

或者:

对象存在
但数据库记录未完成

对账流程可以定期检测这类情况。

概念上:

数据库 ↕
对象存储 ↕
对账 Worker

可能的状态:

CONSISTENT
MISSING_OBJECT
MISSING_METADATA
PROCESSING_STUCK

40.28 存储生命周期策略

对象存储应该在合适的地方用生命周期规则。

比如临时上传:

临时上传 ↓
超过保留期后删除

处理产物:

临时产物 ↓
自动清理

旧版本:

不活跃版本 ↓
长期保留或删除

生命周期策略能减少不必要的存储成本和运维负担。

40.29 下载授权

下载端点不能直接接受:

/api/files/{id}/download

然后返回对象。

正确做法:

请求 ↓
身份认证 ↓
查找文件 ↓
检查所有者/成员身份 ↓
检查权限 ↓
生成授权访问 ↓
返回

这保持了和第 38 章相同的授权原则。

40.30 文件 API

一种可能的 API 结构:

POST /api/files
GET /api/files
GET /api/files/{id}
DELETE /api/files/{id}
POST /api/files/{id}/complete
POST /api/files/{id}/process

大文件上传场景,API 可以把:

创建上传
完成上传
处理文件

和普通文件元数据操作分开。

40.31 文件 API 安全

每个私有文件端点都要考虑:

身份认证
授权
所有权
项目成员身份
文件状态
配额
限流
输入校验
审计日志

这点特别重要,因为文件 ID 通常容易被客户端获取。

知道一个 ID 不应该自动获得访问权。

40.32 AI 文档管道

当文档被 AI 使用时,文件架构尤其重要。

安全管道是:

上传 ↓
校验 ↓
存储 ↓
文本提取 ↓
分块 ↓
向量化 ↓
向量存储 ↓
授权检索 ↓
AI 上下文

检索阶段必须遵守和原始文档相同的所有权和权限边界。

40.33 文档授权

假设:

项目 A ├── 文档 1 └── 文档 2 项目 B └── 文档 3

如果用户只能访问项目 A,AI 检索查询绝对不能意外返回文档 3。

所以:

语义相关性 +
授权过滤器

两个条件必须同时满足。

相关的文档不一定是已授权的文档。

40.34 检索安全原则

正确的模型是:

候选文档 ↓
授权过滤器 ↓
符合条件的文档 ↓
相关性排序 ↓
AI 上下文

另一种实现可以在排序之前先应用授权。

关键要求是:未授权文档绝对不能进入模型上下文。

40.35 媒体处理

视频和音频可以异步处理。

比如:

上传 ↓
元数据提取 ↓
队列 ↓
Worker ↓
转码 / 分析 ↓
输出

前端可以显示:

上传中
处理中
已完成
失败

而不是让用户等待一个漫长的 HTTP 请求。

40.36 进度追踪

处理任务可以暴露:

0%
25%
50%
75%
100%

但进度只有在能有效估算时才应该上报。

不应该为了界面看起来活跃而虚假精确。

40.37 存储与隐私

文件可能包含高度敏感信息。

所以:

访问控制
加密
保留
删除
审计

应该一起考虑。

系统应该只收集和保留功能所需的数据。

40.38 备份策略

对象存储和数据库备份应该分开考虑。

PostgreSQL 备份 +
对象存储保护

只有数据库备份没有对应对象可能不完整。

同样,只有对象没有元数据也可能难以解析。

恢复计划要定义清楚两层如何一起恢复。

40.39 灾难恢复

恢复工作流可能是:

事件 ↓
基础设施恢复 ↓
数据库恢复 ↓
对象存储验证 ↓
元数据/对象对账 ↓
应用恢复 ↓
完整性验证

恢复流程要测试,不能只是写文档。

40.40 文件安全检查清单

[ ] 文件被视为不可信输入
[ ] 文件所有权被记录
[ ] 项目访问被校验
[ ] 大小限制被执行
[ ] 文件类型被校验
[ ] 用户文件名不作为对象标识
[ ] 存储 key 在服务端生成
[ ] 私有对象不能匿名访问
[ ] 临时访问有时限
[ ] 处理 Worker 使用最小权限
[ ] 临时文件被清理
[ ] 删除对象遵循定义的生命周期
[ ] 能检测孤儿对象
[ ] 存储配额被执行
[ ] 文件操作可审计
[ ] 数据库和存储恢复协同

40.41 完整存储架构

完整设计:

 用户 │ ▼ 身份认证 │ ▼ 权限校验 │ ▼ 文件 API │ ┌───────────┴───────────┐ │ │ 文件元数据 上传策略 │ │ ▼ ▼ PostgreSQL 对象存储 │ │ └───────────┬───────────┘ │ ▼ 处理队列 │ ▼ Worker │ ┌────────────┴────────────┐ │ │ 文档管道 媒体管道 │ │ ▼ ▼ 文本/向量 衍生资产 │ │ └────────────┬────────────┘ │ ▼ AI 服务

40.42 架构意义

文件系统不只是个存放上传文件的地方。

它是安全和数据治理子系统。

它决定了:

谁拥有数据?
谁能访问?
保留多久?
如何处理?
存在哪里?
删除后怎样?
能恢复吗?
AI 系统能检索吗?
处理 Worker 能访问吗?

这些问题必须在平台处理大量用户数据之前回答清楚。

40.43 结语

第 40 章建立了对象存储架构。

系统现在有清晰的分离:

结构化元数据 ↓
PostgreSQL 大文件二进制对象 ↓
对象存储

以及受控的生命周期:

上传 ↓
校验 ↓
存储 ↓
处理 ↓
授权 ↓
检索 ↓
保留 ↓
删除

最重要的原则是:

文件被存储了不等于它就可以被访问。

每次访问都必须通过身份、授权、所有权和策略边界。

这个架构为下一个主要子系统做好了准备:文档摄入、文本提取、分块、向量化、向量存储、检索和 RAG(检索增强生成)。

第 40 章完