私有文件存储
当前状态:文件 API 与定时清理已通过本地真实 Workers 验收,工作台已接入主预览。 两个干净生成站已有文件与隔离验收;本轮补齐 320 / 390 像素、浅深主题、键盘预览 / 取消 / 重试以及永久删除的实际浏览器流程。云端 R2 绑定、定时触发与生产验收尚未完成。
配置与数据
packages/core/website.ts 的 storage 控制产品策略,apps/api/wrangler.jsonc 的 STORAGE 绑定提供私有 R2 桶。它们分别表示“允许怎样使用”和“实际存在哪里”。这里没有公开桶地址,也不接受客户端传入对象路径或用户 ID。
| 配置 | 默认值 | 作用 |
|---|---|---|
enabled | true | 关闭后文件 API 拒绝访问;不自动删除现有数据 |
maxFileBytes | 5 MiB | 单文件上限;可配置 1 字节至 16 MiB |
maxFiles | 25 | 每账号保留文件数量;可配置 1 至 1000 |
maxTotalBytes | 50 MiB | 每账号累计字节上限;不能小于单文件上限 |
retentionDays | 7 | 上传时确定有效期;可配置 1 至 365 天 |
uploadsPerMinute | 10 | 上传与删除共用的每账号每分钟写入次数 |
readsPerMinute | 120 | 列表、详情与下载共用的每账号每分钟读取次数 |
这些是模板的产品限制,不是 Cloudflare 免费额度。降低上限不会删除已有文件;修改保留天数只影响新上传文件。过期后立即拒绝读取,物理删除由后续清理批次完成。
增量迁移 0002_private_files.sql 增加三张表:user_file 保存所有权、摘要、状态与有效期;file_quota 保存预留数量和字节;operation_limit 保存账号操作窗口。既有迁移保持不变。普通预览需要先核实自己的本地数据库目标,再按迁移教程应用迁移;代码更新不会自动迁移预览或共享数据库。
0003_file_cleanup.sql 增加 file_maintenance 扫描游标 / 租约和 file_cleanup_run 批次记录,文件模块运行需要两项增量迁移。
使用文件工作台
登录产品后打开 /files,主预览地址为 http://localhost:4410/files。侧栏的 Files 是个人文件入口,切换团队不会改变文件归属。文件能力关闭时隐藏导航,直接打开页面仍明确提示不可用;私有路由不会进入公开站点的搜索索引。
- 点击 Choose a file 选择文件;页面显示名称和实际大小。空文件、超出配置上限的文件不能提交,服务端仍会重新验证内容和大小。
- 点击 Upload file 上传。发送进度来自实际传输字节;100% 表示传输完成,页面继续等待存储确认,只有服务端返回
ready才显示成功。 - 在 Your library 查看有效期、状态与个人配额。View 打开会话授权预览,Download 下载原始字节;图片使用临时对象 URL,关闭预览后释放。文本和 JSON 按纯文本显示,最多预览 64 KiB,下载保留完整内容。
- Delete 打开永久删除确认框,可用 Keep file 或 Escape 取消。删除失败显示原因并保留记录,只有服务端确认删除后才显示存储释放。
- Refresh 更新当前状态;超过 20 条时使用 Load more files 翻页。处于保存或删除中的记录会自动轮询,失败和到期文件禁止查看 / 下载。
键盘可用回车打开预览或删除确认,Escape 关闭预览 / 取消未执行的删除,焦点回到原操作按钮。删除请求未结束时不能取消确认;失败后焦点回到重试按钮,成功后回到选择文件。列表刷新较慢时,等待状态结束后也恢复焦点。任务详情使用同一个私有预览组件,关闭后回到对应附件的预览按钮。
断网或超时可能发生在服务端已保存之后。工作台不会自动重传文件;先刷新列表确认,再决定是否重试,避免重复占用配额。登录过期交给已有认证恢复页面处理,不继续显示旧账号预览。配额包括尚未完成确认或删除的记录,页面也会解释这部分占用。
浏览器原件下载已核对 SHA-256,伪图片拒绝、超限提示、长文本限制与取消确认已实测。两个新生成站已有上传 / 查看 / 下载、实际 API 删除、定时清理和隔离验收;本轮另在隔离测试账号完成手机上传、长文件名、原件下载、实际图片解码、永久删除与任务占用拒绝。任务取消后在原失败弹窗用键盘重试成功;结束核对实际数据库、配额及 R2 对象全部归零。四种尺寸 / 主题组合均无横向溢出,确认弹窗在视口内;这是本地浏览器证据,不能替代真机、云端资源与生产验收。
上传与下载接口
| 接口 | 用途 |
|---|---|
POST /api/files | 直接发送文件二进制;不是表单上传 |
GET /api/files/:id/content | 会话授权读取;加 ?download=1 强制下载 |
tRPC files.list | 每页 20 条,使用返回的 nextCursor 翻页;同时返回配额与策略 |
tRPC files.get | 当前账号的文件元数据 |
tRPC files.remove | 删除当前账号文件并释放配额 |
上传请求使用当前账号 cookie,Origin 必须等于 APP_ORIGIN;X-File-Name 填写经过 encodeURIComponent 编码的文件名,Content-Type 填写真实类型。服务端按实际读入字节执行大小限制,再检查内容。当前支持 PNG、JPEG、UTF-8 文本与 JSON,不支持 PDF、SVG 或其他格式。
PNG / JPEG 检查签名与必要结构,不代表完整图片解码或恶意内容扫描。文本拒绝非法 UTF-8 与不允许的控制字符;声明为 JSON 的输入必须能解析。摘要基于原始字节计算,下载不会改变内容。名字会移除路径和控制字符,不作为对象路径使用。
每次请求都重新核实账号及文件所有权。匿名会话不可使用文件能力;切换组织不会把个人文件变成团队文件。其他账号访问文件返回不存在,列表只包含自己的记录。JSON 默认作为附件;下载响应使用私有且不可缓存的策略,以及禁止内容嗅探和限制执行的响应头。
状态与失败处理
上传先在数据库事务中预留数量和字节,再写入 R2,最后标记 ready。uploading 只表示正在写入;deleting 表示删除尚未完成;failed 保存故障代码。下载只接受未过期的 ready 文件。
数据库与 R2 无法共用一个事务。上传失败会尝试删除对象,只有确认对象删除后才释放配额;补偿失败保留记录与占用,等待后续清理重试。删除失败也保留占用,重复删除不会重复减额度。读取发现对象不存在时记录故障并只释放一次配额。不要把失败记录当成已经物理清理。
定时清理与故障排查
实际 worker.ts 的 scheduled 入口调用 lib/files/cleanup.ts,使用新鲜数据库连接。当前 dev 配置为每 15 分钟一次;云端绑定和调度仍待独立部署验收。本地 Miniflare 不自动执行这个计划,验收命令显式调用真正的定时入口,普通本地预览暂不自动清理。
每批最多处理 20 条到期、失败、删除中或超过一小时未完成的上传记录,再扫描当前站点前缀下最多 100 个对象。游标保存在数据库,下一批继续翻页;扫描完重新开始。不属于本站前缀的对象不会被删除。无元数据的对象保留至少一小时后再删除,包括账号级联删除或上传进程中断后的遗留对象。
批次使用两分钟租约避免常规重叠;过期租约可重新领取。旧执行器不能覆盖新租约的游标,重复删除不会重复释放配额。超时上传清理后,迟到上传不能重新标记为可下载;没有完成补偿的迟到对象由后续扫描回收。
file_cleanup_run 保存开始 / 完成时间、文件删除数、扫描数、孤立对象删除数、故障数和稳定故障代码,保留 30 天;不保存内容和凭据。未完成且租约已到期的记录表示执行可能中断,下一批重新核对。文件本身保留 DELETE_FAILED 等原因;失败记录更新时间用于后续重试,避免只反复处理第一批文件。扫描页删除失败时保留该页游标,重新扫描。
清理失败会记录批次并使定时调用报告失败。检查批次记录、文件状态及仅含记录 ID 的日志,再核对 R2 与数据库;恢复服务后下一次调用继续处理。管理员查看界面尚待 I9。云端清理延迟与调用成本仍需实测,不承诺到期瞬间完成物理删除。
| 错误代码 | 排查方向 |
|---|---|
UNAUTHORIZED | 登录状态失效,或当前是匿名账号 |
DISABLED / NOT_CONFIGURED | 检查配置开关和实际 STORAGE 绑定 |
BAD_FILE / TOO_LARGE | 检查内容、声明类型、文件名和大小 |
QUOTA_EXCEEDED | 检查数量、字节,以及未完成补偿的记录 |
RATE_LIMITED | 等待操作窗口结束;HTTP 文件路由返回 Retry-After |
NOT_FOUND / FILE_NOT_READY | 检查账号、文件 ID 和处理状态 |
EXPIRED / MISSING_OBJECT | 文件已到期或实际对象不存在 |
STORAGE_UNAVAILABLE | 检查本地资源服务、R2 与元数据写入;重试删除或保留故障记录 |
本地验收
bun run files:validate命令要求开发环境与 loopback PostgreSQL,并需要该本地数据库用户具备创建和删除测试数据库的权限。它创建独立临时数据库,应用真实迁移,启动实际 Worker bundle 与隔离 R2,通过两个账号验证四种文件类型、下载字节、摘要、安全响应头、权限拒绝、到期拒绝和删除;两次触发定时清理,检查实际删除、配额只释放一次及批次记录;结束后销毁测试资源。它不迁移配置中的数据库,不调用真实邮件、OAuth 或支付服务。
需要复验浏览器流程时,先构建工作台,再运行可选模式:
bun app:build
bun run files:validate -- --browser原文件验收完成后,命令继续保持自己的回环测试站运行,终端输出具体 /files 地址和该次临时目录内的 browser.local.json 路径。私有记录包含合成账号、上传样本、所选一次性数据库和停止标记;不要分享记录或复制到新站。用记录里的测试账号登录,只上传该目录的合成样本,不操作主预览文件。手机测试需在实际浏览器设置尺寸并使用页面主题按钮,不能用静态截图代替操作。
完成上传、预览、下载及删除后,确认库内文件与配额归零,再在该次记录的 stop 路径创建文件以结束。命令会核对数据库、配额和实际 R2 对象均为空,随后关闭自己的服务、删除临时库和目录。若还有文件则报告验收失败,清理不算浏览器删除成功。此模式默认阻断外部请求,不与 --turnstile 同跑;任务占用用例需在这份一次性库中安排未派发的测试任务,不能改正常预览的任务状态。
apps/api/lib/files/storage.test.ts 与 cleanup.test.ts 另用实际迁移和本地 R2 验证并发数量 / 字节配额、补偿失败、删除重试、缺失对象、操作限流、迟到上传、翻页与租约边界。孤立对象测试仅调整测试返回的对象时间来检查一小时宽限,实际对象由本地 R2 写入和删除。测试范围仍为本地模拟资源;生产权限、收费和配额需要独立云端验收。