PostgreSQL保存事实,Redis保存TTL临时run与原子作用域限流,本地POSIX文件系统保存payload。管理员选择的来源路径只读;批准、启动、恢复只消费受管文件,源文件移走不影响已导入内容。私有ROM/BIOS和路径不提交Git。
受管存储的共同根目录为 RETROM_DATA_DIR/managed/,数据库的 storage_key 相对此根记录:
| 内容 | 相对受管根的路径 |
|---|---|
| 游戏内容及媒体 | games/<game-id>/<file-uuid> |
| KRKR 内容改写产物独占的离线原档 | games/<game-id>/origins/<game-file-id>/original |
| 存档 payload 及截图 | saves/<save-id>/<file-uuid> |
| BIOS/运行依赖 | bios/<bios-file-id>/<file-uuid> |
| Run 临时资源(mkxp 子集 ZIP) | runs/<run-id>/<file-uuid> |
| 尚未发布的临时文件、导入暂存 | temporary/<uuid> |
例如存档实际位于 RETROM_DATA_DIR/managed/saves/,不在 RETROM_DATA_DIR/saves/。账号链接密钥另存于 RETROM_DATA_DIR/account-link.key。浏览器上传暂存于 RETROM_DATA_DIR/uploads/<upload-id>/(files/ 为完成的来源树,parts/ 为接收中的文件),它是临时来源而非受管内容,扫描结束或会话失效后删除,可不纳入备份。备份应包含整个数据根和对应的 PostgreSQL 备份,步骤及停写要求见Docker 部署指南。
准备先写 temporary/ 独占文件,计算 SHA/大小、fsync,再原子 rename 并 fsync 文件/父目录。通用存储允许0字节项目文件;ROM/save/BIOS领域在适用边界要求非空。DB事务内不进行复制、解包或Node运行准备。
普通受管文件及每个逻辑成员上限为 8 GiB;项目最多 50,000 成员,正文总量最多 64 GiB。受管项目 ZIP 支持 ZIP64,物理上限另加每个成员两份最长路径、头部和 ZIP64 元数据的预算,不能套用 8 GiB 或恰好 64 GiB 的限制。目录/归档读取同时限制实际成员数量和字节数;拒绝危险、重复路径、符号链接和加密归档成员。
runtime 声明 ZIP_STORE 布局的项目在导入/替换的内容准备阶段写为一个 STORE ZIP;当前包含 RPG Maker 七种版本、Siglus、TyranoScript、洞窟物语、GameMaker、KRKR、ONS 和 ScummVM。原生 ZIP、单 ROM 及范围外文件集合使用 FILES 布局。输入仍接受目录、ZIP、7z;ASAR/EXE 等项目转换由 runtime 解释,不新增 gzip/RAR 导入能力。浏览器上传只是可选来源,不是打包依赖。
KRKR 准备过程先在 temporary 下保全原始输入,再解包、归一化并构建运行包:写入的同一遍计算整包大小和摘要,随后逐成员回读校验,再 fsync/rename,然后短事务发布物理包记录、成员索引及原档描述。完成、失败及取消清理本次暂存;事务结果不明先按分配 ID 核实。STORE 不压缩正文,活动受管运行内容保留一份包;只有 KRKR 改写内部文件时另有独占的离线原档;只读来源原包不由系统删除。空间检查包含已留存原档、实际展开量、新包及目录原档归档的新增空间,并保留 1 GiB 余量;不按压缩输入大小乘二估算。
本期仅在 KRKR 规范化实际改写内容时留档。比较规范化前后逻辑路径、大小、SHA,忽略存储路径和记录 ID;仅重打包不触发留档,其他平台导入流程不增加原档复制。需要留档时,单文件输入(ZIP、7z、EXE 等)原字节留存;目录、多文件或有归档外配套输入时,用标准 STORE ZIP/ZIP64 保存本款游戏选择的原始文件及必要配套文件,保留来源相对路径。外层归档作为成员时不重新压缩或用转换结果代替其原字节,不收录整批来源中的其他游戏、封面或元数据。原档流式复制并计算 SHA/大小,原始外层归档使用项目包级上限,普通单逻辑文件的 8 GiB 限制不变。每步写入均计入实际可用空间,原档发布失败则本次导入/替换失败。
原档只用于维护者从服务器离线提取,不展示、不提供下载/重转换/切换 API;审核和正式运行始终读取当前主文件及逻辑成员。内部描述在 game_file_tab.ext_info.conversion_info.origin_info,storage_key 相对 managed 根,logical_key 是复制后的建议文件名,size_bytes 与 sha256 用于复核。维护者查询目标活动文件的 ext_info,复制 original 到分析目录后按 logical_key 命名并核对大小/SHA。备份必须同时包含数据库和 managed 下的原档。KRKR 未改写内容、其他平台及原生 ROM 不额外复制;历史记录及首次导入失败且未入库的来源不自动永久留档。
通过 metadata.pegasus.txt 等挂载来源导入时,来源中的元数据、原档、补丁、封面和其他文件始终保留。平台只回收自己在受管根内创建的导入副本与归一化中间文件;清理不接受来源绝对路径。替换已入库内容仍先校验并发布新包、确认事务,再由既有退休清理按 24 小时宽限和当前引用复查回收旧受管内容。提交结果无法核实时保留已发布的新文件,不能把核对失败当作未提交后删除。
大项目的 normalize-content 请求最多执行60分钟,单个扫描候选准备最多90分钟;后台内容替换从接收起最多90分钟,包含有界等待、来源暂存、归一化、构包校验和提交。提交HTTP结束不取消已接收替换;显式取消与服务停止传入后台上下文,在发布前生效。进入短事务发布边界时canCancel=false,取消请求返回当前进度,不能将已发布内容报告为cancelled;COMMIT结果不明按本次分配文件ID核实,不能盲删。HTTP请求仍为10分钟,运行临时资源拼装仍为2分钟。替换只保存Redis暂态,不增加持久业务表,原子发布及退休文件清理规则保持一致。
runtime 可返回有界的归一化摘要,Host 在输出文件大小与摘要核对后写入结构化服务日志。摘要包含转换器标识、版本、来源内容摘要、格式与校验/修正数量及最多八类告警,不进入 Game 配置、内容身份或业务表。异常修正和省略的物理条目还可附带最多 2,048 条结构化明细,整个摘要编码不超过 4 MiB;正常条目不逐项记录。每条必填稳定告警码和归档/成员逻辑相对路径(各最多 4,096 字节),可选过滤前后 SHA-256、大小(各不超过 8 GiB)、32 位无符号 seed/校验值,不包含宿主绝对路径。过滤前 SHA-256 对应解压后尚未过滤的资源流,不等同于整个原物理归档的摘要。详细临时审计与归一化输出一起回收,服务日志保留异常明细;该阶段日志不表示数据库提交或游戏运行已经成功。
运行资源冻结具体 key、包记录、成员偏移、长度和 SHA。浏览器继续使用普通单文件 URL/Range,服务端返回逻辑成员的长度、强 ETag 和 MIME,不暴露物理偏移。Linux 保留可用的 sendfile 路径;每次请求使用独立描述符,有界 Read/Seek 及 ServeContent 的受限复制防止跨成员读取。Tyrano/ScummVM 等宿主工具也通过同一有界 locator 读取。
mkxp 所需 ZIP 的成员集合和路径完全一致时复用物理包;子集或路径不同则在 runs/<run-id>/ 生成 Run 临时 ZIP。成员正文在复制时已核对 SHA,临时 ZIP 只校验目录与偏移,不逐成员回读、不 fsync。关闭时释放,每次读取 Run 续期,过期或进程中断残留按与 Run 相同的两小时空闲宽限有界回收;temporary/ 下的导入暂存及其他未发布文件仍按 24 小时宽限回收,长时间导入不会被提前清理。不会每次启动都新增永久游戏 ZIP。
宿主工具需要真实目录树时(如 ScummVM 检测),独立受管文件以硬链接暂存,不复制字节;只有项目包成员需要复制,复制前按所需字节加 1 GiB 余量检查可用空间。暂存树成员的回收时间取成员与顶层暂存目录 mtime 的较新者,避免新硬链接继承旧源文件时间而被提前回收;不修改源 inode 时间。
文件替换写新路径,再短事务活动切换;退休文件24小时宽限。单维护worker每分钟处理≤50个deleted Game、每类≤100退休文件、≤50个仍有私人资源的deleted User,以及≤1000个文件系统遍历步骤。扫描器保留目录偏移,空ReadDir批次和并发消失目录安全处理,不全量walk/sort。终态scan进度超过24小时分批删除≤200。
删除Game先收回Game及Save owner字节,再短事务清关系和从属行、标记purged。移除失败保留deleted和从属路径,下轮重试;退休单文件先复查当前active引用,引用存在保留字节。孤立扫描保护所有 active/deleted 文件记录所拥有的 payload、媒体及存档截图;deleted 文件只走按数据库 updated_at_ms 计算宽限的有界退休清理,不能因物理 mtime 很旧而绕过宽限、失败重试或每批数量限制。没有数据库归属的残留才按物理时间宽限回收;purged 行不继续占有字节。清理失败记录错误,不建立持久工作流事件或恢复计划。
原档按规范路径中的文件主键及 Game ID 判断归属,active/deleted 拥有者均受孤立清理保护;错配 Game、无主或 purged 拥有者按现有孤立宽限回收。文件退休时先移除主内容及该记录独占的附件目录,两者都成功后才标记 purged;失败保留 deleted 和元数据重试。删除整款游戏回收整个 Game owner,包含全部原档;不会跟随原档目录中的跨拥有者符号链接。外部替换的新记录拥有新原档,旧原档随旧记录退休。
数据库迁移没有业务 seed;001 为完整基线,后续 migration 就地追加、不清空数据,标题字母序在启动时按版本回填排序键。新版本通过统一内容目录读取 FILES 和 ZIP_STORE 两种合法布局,既有散文件无需在启动时重打包。破坏性开发 reset 必须停止 exact PFB 并归档 data 与 postgres,保留 Provider/缓存/ID/URL。
升级后,管理员可在相同数据库、受管根及配套 runtime/Provider 环境执行:
retrom content pack-existing --dry-run --all
retrom content pack-existing --apply --game-id <UUID>
retrom content pack-existing --apply --all --limit 20默认预览;--all 和 --game-id 二选一,每次默认最多处理 20 个项目,已完成或范围外的游戏不占限额。工具串行读取当前受管散文件并核验大小/SHA,不需要上传来源,不重新识别入口或生成配置。新包准备完成后锁定 Game 并复查版本/文件快照,原子切换文件布局、递增版本,保留 Game ID、状态、来源、配置、contentHash 及私人关系。失败保留原活动内容;已完成项目重跑跳过,提交结果不明先核实,空间不足停止后续转换。
旧文件继续按 24 小时宽限退休。只改变布局时若旧记录已有原档,将其校验并复制到新记录自己的附件目录,不复用旧路径;多个独立原档无法合并为单一拥有者时拒绝该次打包。已是项目包时重跑保留现有原档。批量迁移需累计预留所有尚未回收旧文件、旧原档、新包及新原档同时存在的空间,dry-run 的 AdditionalBytes 包含需要复制的原档。正式转换前备份同一时间点的数据库和受管内容;仅回滚应用镜像不会撤销新的存储布局。此命令不在启动或数据库 migration 中自动执行。