120 lines
4.8 KiB
Python
120 lines
4.8 KiB
Python
#!/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 实际执行)")
|