清理脚本最贵的动作不是删,是删完之后才发现删错了。所以我先做了一次「只填不删」的演练:把 266 个待清理文件逐行填进 manifest,算清每一行的指纹,最后一个字节都没动。
结论先放这儿:按文件名判「有没有幸存原件」会误判,266 行里有 30 行是错的;换成 SHA256 双指纹重判之后,265 行从「有副本可删」变成「唯一副本,不能删」。
一、这次演练在清理什么
本地工作区有个长期通道:临时件、探针产物、一次性脚本不直接删,统一 park 进几处 _trash 目录,规则是 mtime + 30 天到期才允许清。
演练的输入是一次全量扫描:266 个文件 / 41,841,745 字节,到期 = 0,全部条目距到期还有 26~30 天。也就是说,此刻按规则一个都不该删。演练要验证的不是「敢不敢删」,而是这件事:模板在真数据上填不填得进去、算不算得对。
产出是 4 份 manifest,明细就是扫描结果的 266 行全量:
| 文件 | 根目录 | 行数 | sum_bytes | HIGH | 结论 |
|---|---|---|---|---|---|
| manifest-01 | 主工作区临时区 | 244 | 25,895,601 | 243 | 最大的一根 |
| manifest-02 | 主工作区根 | 19 | 34,601 | 19 | 全部高风险 |
| manifest-03 | 副临时区 | 1 | 1,343 | 1 | 单条 |
| manifest-04 | 用户目录下的工具区 | 2 | 15,910,200 | 2 | 两份大日志归档 |
| 合计 | 4 根 | 266 | 41,841,745 | 265 | 与扫描基线逐字一致 |
拆成 4 份不是啰嗦。模板有一条硬规则:一个 manifest 只对应一个 trash_root,跨根必须拆开。四个根的文件数从 1 到 244,硬塞进一份会让 sum_bytes 和 post_purge_total_bytes_max 失去意义——你没法再回答「清掉这一根之后,总量上限是多少」。
二、判据第一版:按文件名检索,30 行命中
第一版判据很朴素,也很像人干活的直觉:
在
_trash之外搜同名文件。搜得到 → 说明原件还在 → 这是副本 → LOW,可以删。搜不到 → 唯一副本 → HIGH,不能删。
跑完 266 行,30 行命中了同名候选。
如果到这里收工,结论会是「30 行可以放心删」。差一点就删了。
三、反例:同名,但差了一个数量级
出问题的是日志归档那一根。有一行 desktop.log.3.gz-20261113-143722,trash 里那份是 10,666,992 字节,sha256 前 16 位 9FDA4DE44232236C。
按文件名去搜,确实搜到了一个同名件 desktop.log.3.gz,但那个件只有 967,468 字节,sha256-16 是 D432F617F87AD98E。
同一个名字,一个 10.6 MB,一个 0.97 MB,指纹完全不同。
原因不复杂:那台机器上的日志轮转脚本升级过一次版本,旧归档被截断后重新生成,同名文件被新内容覆盖了。文件名保住了,内容没了。
这就是「按名字判唯一副本」的致命处——它把「存在」误当成了「同一份内容」。恢复场景下这两件事的差别就是全部差别:你以为能找回来的东西,其实早被另一次写入顶掉了。真删了,10,666,992 字节的那段日志不会再出现,因为它的来源已经被覆盖。
四、判据第二版:三条,全部机算
把判据升级成 v2-sha256,写死三条:
- 同名候选必须 SHA256 一致,才认定为幸存原件(LOW,可删);同名但内容不一致的,记录双指纹后判内容唯一副本(HIGH,不可删)。
- 可重建的生成物——日志、构建产物、中间件临时件——无论有没有同名候选,一律 HIGH。
- HIGH / LOW 每一行都要给出 trash 件与同名候选的双指纹,让复核的人能自己验,而不是相信一句结论。
按这三条全量重判,结果翻转:HIGH = 265,LOW = 1。那 30 行「有同名候选」的,全部改判为唯一副本。
明细行的形态是这样的(两份日志归档,就是前面那个反例):
|
|
五、为什么「可重建」必须一律 HIGH
第 2 条一开始我觉得多余:日志嘛,本来就是生成物,删了再跑不就有了?
后来想清楚了一个区分:可重建 ≠ 已重建。
判据该问的不是「这类文件理论上能不能重新生成」,而是「我现在、立刻,能不能重跑出同样的内容」。这两个问题在真实环境里经常不同答案:
- 生成它的那个任务早就结束了,重跑要重建上下文、重建输入;
- 日志来自已升版的轮转脚本,旧版本的行为已经复现不出来(第三节就是这么丢的);
- 中间件的临时件依赖当时的进程状态,重启一次就是另一份数据。
所以对生成物,我给的是最保守的一档:不看有没有同名候选,直接 HIGH。这一条让 265 行里的绝大多数落到「不可删」——听起来像是判据失灵了,其实这正是它该有的样子:真正能安全删的比例本来就低,判据的价值是把它算准,不是把它算多。
六、唯一的 LOW 长什么样
265 : 1,那个 1 值得单独看一眼,因为它能说明 LOW 该满足什么条件。
patrol-baseline-6seg.txt,0 字节。幸存原件在归档目录里,也是 0 字节。空文件的 SHA256 是平凡相同的——它们确实是同一份内容(都是空的),恢复无损。
这就是 LOW 的正当性:不是「名字对上了」,而是内容指纹对上了,且丢了也能无损恢复。差一个条件都不成立。
七、零删除怎么证明
「我没删」这句话不能靠自觉,得有前后快照。
演练前后各做一次只读统计(遍历四个根的全部文件数与字节),逐根比对:
| 根目录 | 演练前 | 演练后 | 差异 |
|---|---|---|---|
| 主工作区临时区 | 259 文件 / 25,945,326 B | 259 文件 / 25,945,326 B | 0 |
| 主工作区根 | 19 文件 / 34,601 B | 19 文件 / 34,601 B | 0 |
| 副临时区 | 1 文件 / 1,343 B | 1 文件 / 1,343 B | 0 |
| 工具区 | 2 文件 / 15,910,200 B | 2 文件 / 15,910,200 B | 0 |
| 合计 | 281 文件 / 41,891,470 B | 281 文件 / 41,891,470 B | 0 / 0 |
程序判定 TRASH_UNCHANGED=True,删除调用次数 = 0。
注意这里的 281 和基线 266 不是一个数,差 15 个文件 / 49,725 字节,全在临时区(259 vs 244)。原因是 16:00:20 那次扫描之后,各轮定时任务还在按通道往里 park 新件。处理口径是:manifest 只收录扫描清单里的 266 行(逐行回验都还在盘上、字节和 mtime 未变),扫描之后新进来的留给下一轮周扫。至于「清完之后的合计上限」,用的是 41,841,745 这个更低起点算出来的,实际起点更高,所以上限只会更紧,不会放宽。
顺带记一笔:266 行全部有可读的 LastWriteTime,UndatedFile = 0,没有一个文件走「无日期例外保留」这条特殊路径——少一类例外,就少一处将来会被忽略的角落。
八、真到执行那天会发生什么
演练里顺手把「执行日」推演了一遍,时点取首个合法执行日 2026-12-08(日期锁):
- 到期可删的:4 行 / 25,452,544 字节。四份探针产物
p0~p3.bin,mtime 在 2026-11-08 20:48:09 到 20:48:28 之间,30 天期满正好落在 12-08。 - 保留的:262 行,距到期 1~30 天,滚进后续周扫重新圈定。
- 回滚:没有。
_trash里的内容没有任何外部备份,删掉就是不可回滚。所以 manifest 头部的rollback_plan只能如实写「不可回滚,故须人工复核」——执行前逐行核对 6 列明细与 sha256_16,大体积条目单独过目。 - 审批闸门:4 份 manifest 的
approved_at/approval_ref都留空。按自检规则 fail-closed,即使到期也拒绝执行。approval_gate: REQUIRED恒定,超时自动放行这种事不适用于不可逆操作。
这四条合起来才是「安全」的全貌:清单准、到期算对、回滚不可能、所以必须有人签。
九、四十分钟买的是什么
机器侧从落第一份只读快照到判定跑完,大约五分钟(报告里 generated_at 实测 19:10:17)。把读模板、写生成器、266 行逐行回验磁盘实况、再把报告写出来全算上,整场演练占掉大约四十分钟。
四十分钟换来的是三件事:
一、把 30 行错误判据挡在删除请求之前。 这 30 行里只要有几行是真唯一副本,损失就是不可逆的——不是磁盘空间的损失,是那段日志、那份脚本、那个批次的产物再也不在的损失。
二、让「删」这个动作有了一份可复核的依据。 四项机算校验全过:sum_bytes 与明细列回读求和 4/4 一致、row_count 与解析行数 4/4 一致、post_purge_total_bytes_max 4/4 成立、scan_baseline_* 与扫描器输出逐字相同;266 行的磁盘实况(存在性 / 字节 / mtime)与扫描表比对漂移 = 0。
三、暴露了一个规格级问题。 第一版判据是写进模板的,也就是说它不是一次操作失误,是一个会反复复发的设计缺陷。演练把它挑出来、升级成双指纹判据,此后每一份 manifest 都带着这个修正跑。
算指纹的成本是线性的、可预算的:266 个文件、41,841,745 字节,多花四十分钟,多算 266 个 SHA256。删错的成本是不可逆的、无法预算的。
这笔账怎么算都不亏。所以这次演练的 RESULT 行是这个:
|
|
266 个文件看得清清楚楚,一个都没删。这就是我想要的结果。