给 Boker 的媒体文件加一道安全清理:先找对目录,再谈删除
为 Boker 增加媒体孤儿文件清理时,dry-run 发现相对上传目录在网站和独立命令中指向不同位置。本文记录清理规则、安全校验、验证结果与尚未完成的上线步骤。
博客图片上传后,Boker 会在数据库保存媒体记录,并把文件写进上传目录。如果文件留下了、记录却没有保存成功,时间久了就会积累无人使用的文件。我为这类文件增加清理命令时,第一次只读检查就发现一个更实际的问题:同一份相对路径配置,在网站进程和命令行里指向了两个目录。
这篇记录清理规则、安全措施和这次排查的结果。清理代码仍在功能分支;生产目录迁移和定时任务尚未执行。
为什么不能直接删除“数据库里没有的文件”
上传文件与写入数据库不是同一个原子操作。一个刚写好的文件可能还没来得及生成媒体记录;如果清理任务此时扫到它,立刻删除就会干扰正常上传。反过来,数据库里的媒体记录也可能指向丢失的文件,需要报告出来供人工处理,不能把这种情况当成清理成功。
这次采用 Media.key 作为保留名单,只检查上传目录顶层的普通文件。文件名存在于数据库中就保留;没有记录但修改时间仍在宽限期内也保留。默认宽限期是 24 小时,超过后才列为候选。数据库有记录而磁盘缺文件时,命令只报告 missingFiles,不会修改数据库。
一个相对路径,两个实际目录
原有配置写着 UPLOAD_DIR=./var/uploads。Next.js standalone 启动时会切换工作目录,因此网站实际使用的是 .next/standalone/var/uploads;从项目根目录启动独立脚本,./var/uploads 却落在项目根目录。第一次 dry-run 扫描了后者:那里有 21 个旧文件,而数据库里的 4 个媒体 key 都显示为“缺失”。这次检查没有删除文件,却暴露了清理任务可能扫错位置的问题。
我改用网站实际上传目录的绝对路径重跑只读检查。结果是数据库 4 条记录、磁盘 4 个文件、4 个有效文件、0 个删除候选,也没有缺失文件。随后,清理命令改为强制要求绝对 UPLOAD_DIR,并拒绝根目录、常见上层目录和符号链接目录。部署时还需要把网站与清理任务统一指向同一个持久目录;当前服务器尚未完成这一步。
清理命令怎样限制误删
命令先校验目录,再从 PostgreSQL 读取媒体 key。扫描时只处理顶层普通文件,跳过子目录和符号链接。--dry-run 输出候选和汇总,不执行删除;默认实际运行模式适合在核对无误后交给定时任务。
删除前,代码再次核对目录是否仍是打开时的目录,并检查候选文件的类型、设备号、inode、修改时间和大小。文件或目录在扫描后发生变化就跳过或中止,避免继续按旧判断处理新目标。PostgreSQL 会话级 advisory lock 防止两个清理进程同时执行。逐文件结果以 JSON 记录,删除失败会进入汇总并使进程返回非零状态。
这些检查不能把并发文件系统操作变成事务。首次执行实际删除前,仍须备份上传目录,检查 dry-run 的每个候选,并确认应用与命令使用同一个绝对路径。
验证到哪一步
功能分支通过了 76 个单元测试、12 个 PostgreSQL 集成测试和 TypeScript 类型检查。生产构建、ESLint 与 systemd 定时表达式也在变基前检查过;变基后没有重跑构建。当前服务只执行了只读 dry-run,确认实际上传目录中 4 个媒体文件与数据库记录一一对应,0 个候选、0 个删除。
代码尚未合并或部署,生产目录迁移、媒体读取复核、实际删除及定时任务运行仍待验证。上线时应先完成备份与目录迁移,再在目标目录重跑 dry-run;候选符合预期后才能启用定时器。