对象存储与媒体完整性
附件通过 StorageProvider 保存,开发环境使用 MinIO 或本地适配器。数据库保存对象键、MIME、大小、SHA-256、所有者和隐私等级;业务模块不直接依赖云厂商 SDK。发布前可执行 S3_ENDPOINT=http://localhost:9000 pnpm storage:smoke 验证项目适配器的上传、下载和删除。
R32 本地目录边界(AUD-042)
LocalStorageProvider 固定启动配置,逐层拒绝软链接与 Windows junction,包括指向根内的链接;叶文件必须是单链接普通文件。新对象采用独占创建并刷盘,不覆盖既有目标;读取使用核对过身份的文件句柄;清理不递归,缺失可重试,遗留链接保持冻结。对象 key 和已有合法对象不变。Linux 检查根目录和内部目录归服务身份所有且其他身份不可写,使用 /proc/self/fd 目录句柄锚定后续 IO;需要可用的 proc 文件系统。Windows 检查路径、目录及打开文件身份,在检测到替换后拒绝,但 Node 没有跨平台的 openat 保证:根目录及其父目录必须由服务账号独占写权限,不能把此实现视为隔离同一服务身份或可改写目录 ACL 的宿主机进程;未经保护的共享目录应使用 VM/S3,不能只靠一次 realpath 检查。测试覆盖 Windows junction 与受控替换,Linux 实际编译代码验证打开目录后的 put/get/delete 替换;均只用临时目录。HTTP 测试助手为每个应用新建独立临时对象目录并核对路径后清理,不再混用默认 runtime/files。
R32 持久对象补偿(AUD-043)
对象上传前先把恢复记录写入独立 PostgreSQL 连接池,记录 owner、对象键、摘要和存储位置指纹;登记失败不上传。0076 只给已有 storage_cleanup_job 增加位置与下次重试时间,不依赖可能回滚的 stored_file。上传确认后,正式文件与任务绑定在同一业务事务提交;清理持有行锁并核对全部正式引用,绑定与删除不能同时成功。上传结果未知保留五分钟窗口,生产适配器的 PUT 支持 30 秒取消;窗口是恢复策略,不能宣称数据库与外部对象具有分布式原子性。后台每 30 秒消费最多 10 条,失败按上限一小时退避并保留原因;维护模式阻止后台外部删除。STORAGE_CLEANUP_WORKER_ENABLED 默认 true,可显式暂停。未知历史位置或配置变更均保留对象并报告失败,不能推断旧桶/目录,也不能自动找回修复前从未登记的孤儿对象。
真实 PostgreSQL 16 的 11 项用例通过,涵盖事务与删除同时失败、引用保护、位置变化、并发绑定/清理、未知上传、连接池耗尽和服务实例重建;对象 Provider 使用内存替身,未执行 S3 或强杀进程验收。两个独立 PostgreSQL 16 容器实测 0000–0076 后列数均为 5093,指纹一致,已更新备份列合同;不代表整套备份恢复流程重新验收。
R32 不可变对象的冷热切换
本地热、冷对象使用独立目录,默认冷目录为热目录后附 -cold,可由 LOCAL_COLD_STORAGE_PATH 配置到独立归档磁盘;默认两个目录可能仍在同一磁盘,不能据此宣称硬件成本降低。S3 冷层请求 STANDARD_IA,读取实际类别确认,兼容存储不支持时失败。
迁移先生成新对象键并登记上传意图,完整复制后重读校验大小与 SHA-256。随后在同一短事务切换 stored_file 引用、记录迁移与策略、安排旧键回收。文件编号和内容摘要保留,旧对象延迟至少五分钟并核对全部引用后才删除,回收失败继续重试。
冷热目录分别绑定位置指纹;改变冷目录不会让旧任务误删另一个目录。SECRET/LEGACY_ONLY 禁止迁入冷层。0080 允许同一资产版本保留多次迁移历史。
这是已编码待测试能力,尚未执行真实迁移或备份恢复;运维备份需同时涵盖热目录、冷目录和数据库。
普通附件上传幂等(2026-10-01)
POST /api/v1/files/upload 接收可选 Idempotency-Key。完全不带键时兼容旧客户端,每次调用创建新文件;显式空白、控制字符或长度超过 160 的键在对象副作用前返回 400。有效键去除首尾空格后按认证 owner 隔离,沿用公共幂等层的 24 小时有效期;同 owner 的键不能跨操作复用。
有键上传对完整原始字节、原始名称、声明 MIME、大小及固定 PRIVATE 隐私计算不可逆摘要。同键不同输入返回 409 IDEMPOTENCY_SCOPE_CONFLICT,不再上传;并发相同输入由进程内单飞及 PostgreSQL owner/key 唯一约束仲裁。每次请求先验证 file:write,回放无需 file:read。回执只持久化文件编号和摘要,按 owner 读取公开元数据,绝不返回 owner、对象键或 Provider 位置。
对象仍先建立独立持久意图;文件元数据和幂等回执共同提交。事务失败后清理 staged 对象,删除失败进入既有恢复队列;数据库已提交但响应丢失时正式引用保护对象,重试取回同一文件。独立 PostgreSQL 16 与内存对象替身覆盖 HTTP 重放、冲突、权限、跨实例竞争、提交失败及响应丢失;真实 S3、浏览器和真机验收另行记录。
已提交附件下载完整性(2026-10-01)
普通文件和系统反馈附件下载使用 StorageService.getVerified:先执行 file:read 与 owner 元数据查询,再核对 Provider 类别、本次字节大小及 SHA-256。反馈记录和附件绑定仍先按 owner 检查。不存在与跨 owner 统一 404;Provider 或完整性不符在设置附件头、发送字节前失败,经既有过滤器返回脱敏 INTERNAL_ERROR。普通下载成功响应使用 private, no-store。
上传回执及 owner+key/24 小时 TTL 不变;每次下载重新核验。stored_file 未保存位置指纹,本次不声明同类别目录或桶的位置绑定。内部原始 get 保留给 Media/Knowledge 的专用校验与 MED-005 损坏证据,不改变现有隐私策略。新增真实 Controller/Storage 与隔离 Nest/pg-mem HTTP 回归,Provider 为内存替身;真实 PostgreSQL、S3、浏览器及设备验收另行记录。
2026-10-25 getTier须验证真实对象边界
LocalStorageProvider.getTier不能仅根据key里的cold段判断。现在层级探测先验证真实目录/文件身份、no-follow及单链接普通文件,和get共用同一受控文件边界,避免缺失或链接对象进入同层apply的成功路径。冷热物理根仍不重叠,文件ID/hash/大小/原创建时间与旧键回收事务合同保持。
本轮Windows32真实FS和nativePG17.11五项物理HTTP/事务证据通过;S3 STANDARD_IA与HeadObject供应商行为、Linux和300秒cleanup真实到期/重启未验。成功登记OBJECT_REPLACED回收意图不等于清理已执行,不启动worker凑验收。