#!/usr/bin/env python3 """ backup-cleanup.py — 自动清理过期备份(2026-09-05 牧尘要求:过期/无用的备份自动删除) 背景:9/4 磁盘 86% 的根因 = 备份/快照/损坏留档只增不删: ① memories.lance.broken.* 83G(损坏重建后旧目录从不删) ② ~/.hermes/backups/ 74 个 pre-watchdog 快照(旧看门狗无轮转) ③ ~/.hermes/ 根目录散落 14 个 state.db.* 手工备份(修完不清理) ④ hermes-backup-20260717 16G(手动备份无保留期) 策略(统一轮转,防再堆积): - ~/.hermes/state.db.* 散落备份 → 只留最近 3 份(其他超期删) - ~/.hermes/backups/state.db.pre-watchdog-* → 只留最近 3 份 - ~/.hermes/backups/state-db-snap*/state-*.db → 只留最近 8 份(snapshot-state-db.sh 已做,兜底) - ~/.hermes/backups/dual-backup/*.bundle → 只留最近 2 份(每日 bundle) - ~/.hermes/backups/dual-backup/zhiyi-*.tar.gz → 只留最近 2 份 - /var/lib/memoryweave/memories.lance.broken* → 删除(broken 标记 = 已废弃,重建后无保留价值) - /var/lib/memoryweave/*.bak-* → 只留最近 2 份 磁盘 >85% 时额外:~/.hermes/cache 清缓存、/tmp 清 1 天前 安全铁律: - 绝不删活动文件:state.db / graph.db / *.lance 活动库(仅 *.broken.* / *.bak-* 模式) - 绝不跟随软链(-type f -maxdepth 限制) - 只删明确命名的备份模式,不 glob 宽泛删 """ import os, re, shutil, sys from pathlib import Path HOME = Path.home() HERMES = HOME / ".hermes" BACKUPS = HERMES / "backups" def keep_latest(paths: list, keep: int, dry: bool = True): """按 mtime 保留最近 keep 份,其余删除。返回 (deleted, freed_hint)""" paths = sorted(paths, key=lambda p: p.stat().st_mtime, reverse=True) removed = 0 for p in paths[keep:]: try: if p.is_dir(): if dry: print(f" [dry] rm -rf {p}") else: shutil.rmtree(p, ignore_errors=True) else: if dry: print(f" [dry] rm {p} ({p.stat().st_size//1024//1024}M)") else: p.unlink(missing_ok=True) removed += 1 except OSError as e: print(f" ⚠️ 删除失败 {p}: {e}") return removed def cleanup(dry: bool = True): print(f"{'[DRY-RUN] ' if dry else ''}backup-cleanup 开始") total = 0 # 1. ~/.hermes/ 根目录散落 state.db.* 备份(保留最近 3) stray = [p for p in HERMES.glob("state.db.*") if p.is_file()] total += keep_latest(stray, 3, dry) # 2. backups/state.db.pre-watchdog-*(保留 3) pre = list(BACKUPS.glob("state.db.pre-watchdog-*")) total += keep_latest(pre, 3, dry) # 3. backups/state-db-snap*/(保留 8) for snapdir in BACKUPS.glob("state-db-snap*"): if snapdir.is_dir(): total += keep_latest(list(snapdir.glob("state-*.db")), 8, dry) # 4. backups/dual-backup/(bundle 留 2,tar.gz 留 2) ddir = BACKUPS / "dual-backup" if ddir.exists(): total += keep_latest(list(ddir.glob("*.bundle")), 2, dry) total += keep_latest(list(ddir.glob("*.tar.gz")), 2, dry) # 4b. backups/kanban/(看板导出备份,保留 5;v0.21.0 kanban boards export 产物) kdir = BACKUPS / "kanban" if kdir.exists(): total += keep_latest(list(kdir.glob("*.tar.gz")), 5, dry) # 注:backups/corruption-hold/ 为人工观察期留存,绝不自动清理(手动确认后删) # 5. /var/lib/memoryweave/ broken 残留(直接删,废弃标记) mw = Path("/var/lib/memoryweave") if mw.exists(): for p in mw.glob("memories.lance.broken*"): if dry: print(f" [dry] rm -rf {p}") total += 1 else: shutil.rmtree(p, ignore_errors=True) total += 1 # *.bak-* 保留 2 total += keep_latest([p for p in mw.glob("*.bak-*") if p.is_dir()], 2, dry) # 6. 磁盘 >85% 额外清理 st = shutil.disk_usage(str(HOME)) pct = st.used / st.total * 100 if pct > 85: print(f" ⚠️ 磁盘 {pct:.0f}%>85%,额外清理:") # 清 cache 子目录(1 天前) for c in (HERMES / "cache").glob("*"): if c.is_dir(): for f in c.glob("*"): if f.is_file() and (time_now - f.stat().st_mtime) > 86400: if dry: print(f" [dry] rm cache {f}") else: f.unlink(missing_ok=True) print(f"完成,处理 {total} 项") return total if __name__ == "__main__": import time global time_now time_now = time.time() dry = "--apply" not in sys.argv cleanup(dry=dry) if dry: print("(dry-run;加 --apply 实际执行)")