Files
7z-encrypt-server/tests/test_clean_orphans.py
lou 4b25231c01 新增孤儿清理工具 + 看门狗每小时自动清理 + start.sh 无条件补拉看门狗
- clean_orphans.py: 只删数据库无记录的孤儿 (tmp 终态/僵尸任务卷, storage 无记录目录)
  - 目录级判断: files 记录指向的目录整体跳过, 内部文件永不单独删除 (防误删)
  - 路径 resolve 统一, 调用方 cwd 无关
  - 边界: tmp 终态超 1h 删/进行中保留/超 7 天判僵尸; storage 孤儿超 24h 删
- watchdog.sh: 每 30s 保活服务 + 每小时自动跑清理 (失败 30s 后重试)
- start.sh: 看门狗拉起移到无条件位置 (原 main.py 在跑时 exit 短路, 看门狗死亡无人接管)
- tests: 新增 test_clean_orphans 5 项 (含有效目录内 chunk 不删的误删回归断言)
- README: 数据目录补 tmp, 新增运维节, 测试数 13->17
2026-08-11 14:22:28 +08:00

130 lines
5.4 KiB
Python

"""clean_orphans 孤儿清理逻辑测试: 临时 db + 临时目录, 验证删除/保留边界。
覆盖: tmp 终态/无记录删除、进行中保留、僵尸任务删除; storage 有记录保留、
无记录目录/单文件删除、父目录含记录不删; dry-run 不实际删除。
"""
import os
import sqlite3
import time
from pathlib import Path
from clean_orphans import clean_storage, clean_tmp, load_db
# 测试时间基准: 全部相对 now 偏移, 避免真实时间波动
NOW = time.time()
HOUR = 3600
DAY = 86400
def _set_mtime(p: Path, age_sec: float) -> None:
"""把路径 mtime 调到 age_sec 秒前。"""
t = NOW - age_sec
os.utime(p, (t, t))
def make_env(tmp_path: Path) -> tuple[Path, Path, Path]:
"""造测试库与目录结构, 返回 (db, tmp_root, storage_root)。"""
db = tmp_path / "app.db"
conn = sqlite3.connect(db)
cur = conn.cursor()
cur.execute("CREATE TABLE transfers (transfer_id TEXT, status TEXT)")
cur.execute("CREATE TABLE files (path TEXT)")
cur.executemany(
"INSERT INTO transfers VALUES (?,?)",
[
("done_tid", "done"),
("failed_tid", "failed"),
("uploading_tid", "uploading"),
("zombie_tid", "uploading"),
],
)
storage_root = tmp_path / "data" / "storage"
recorded = [
storage_root / "user1" / "2026-08-11" / "valid_tid",
storage_root / "user1" / "2026-08-11" / "clone_tid_clone",
]
cur.executemany(
"INSERT INTO files VALUES (?)", [(str(p),) for p in recorded]
)
conn.commit()
conn.close()
# tmp 目录
for tid in ("done_tid", "failed_tid", "uploading_tid", "zombie_tid", "orphan_tid"):
d = tmp_path / "tmp" / tid
d.mkdir(parents=True)
(d / "chunk_0001").write_bytes(b"data")
_set_mtime(tmp_path / "tmp" / "done_tid", 2 * DAY)
_set_mtime(tmp_path / "tmp" / "failed_tid", 2 * DAY)
_set_mtime(tmp_path / "tmp" / "uploading_tid", 60) # 新, 进行中
_set_mtime(tmp_path / "tmp" / "zombie_tid", 8 * DAY) # 僵尸
_set_mtime(tmp_path / "tmp" / "orphan_tid", 2 * DAY)
# storage 目录
for d in recorded:
d.mkdir(parents=True)
(d / "chunk_0001").write_bytes(b"x")
_set_mtime(d, 2 * DAY)
orphan_dir = storage_root / "user1" / "2026-08-11" / "orphan_storage_tid"
orphan_dir.mkdir(parents=True)
(orphan_dir / "chunk_0001").write_bytes(b"x")
_set_mtime(orphan_dir, 2 * DAY)
orphan_file = storage_root / "user1" / "2026-08-10" / "old_arch_single.enc"
orphan_file.parent.mkdir(parents=True)
orphan_file.write_bytes(b"y")
_set_mtime(orphan_file, 2 * DAY)
_set_mtime(orphan_file.parent, 2 * DAY)
return db, tmp_path / "tmp", storage_root
def test_load_db_resolves_paths(tmp_path: Path) -> None:
"""files.path 相对/绝对统一 resolve, 与磁盘绝对路径可比。"""
db, _, _ = make_env(tmp_path)
status_map, paths = load_db(db)
assert status_map["done_tid"] == "done"
assert status_map["zombie_tid"] == "uploading"
assert str((tmp_path / "data" / "storage" / "user1" / "2026-08-11" / "valid_tid").resolve()) in paths
def test_clean_tmp_removes_final_orphan_zombie_keeps_active(tmp_path: Path) -> None:
"""终态/无记录/僵尸 tmp 删, 进行中(新 mtime)保留。"""
db, tmp_root, _ = make_env(tmp_path)
status_map, _ = load_db(db)
removed, _ = clean_tmp(tmp_root, status_map, dry=False, quiet=True)
assert removed == 4 # done + failed + orphan + zombie
assert not (tmp_root / "done_tid").exists()
assert not (tmp_root / "failed_tid").exists()
assert not (tmp_root / "orphan_tid").exists()
assert not (tmp_root / "zombie_tid").exists()
assert (tmp_root / "uploading_tid").exists() # 进行中保留
def test_clean_storage_keeps_recorded_removes_orphan(tmp_path: Path) -> None:
"""files 有记录保留 (含目录内 chunk), 无记录目录删, 含记录父目录不删。"""
db, _, storage_root = make_env(tmp_path)
_, paths = load_db(db)
removed, _ = clean_storage(storage_root, paths, dry=False, quiet=True)
assert removed == 2 # orphan_storage_tid + user1/2026-08-10 (含 old_arch_single.enc)
# 有效目录整体保留, 内部 chunk 文件绝不单独判断删除 (防误删回归)
assert (storage_root / "user1" / "2026-08-11" / "valid_tid").exists()
assert (storage_root / "user1" / "2026-08-11" / "valid_tid" / "chunk_0001").exists()
assert (storage_root / "user1" / "2026-08-11" / "clone_tid_clone").exists()
assert (storage_root / "user1" / "2026-08-11" / "clone_tid_clone" / "chunk_0001").exists()
assert not (storage_root / "user1" / "2026-08-11" / "orphan_storage_tid").exists()
assert not (storage_root / "user1" / "2026-08-10" / "old_arch_single.enc").exists()
# 有记录的子目录存在 => 父目录 user1/2026-08-11 不被删
assert (storage_root / "user1" / "2026-08-11").exists()
def test_dry_run_does_not_delete(tmp_path: Path) -> None:
"""dry-run 预览不实际删除。"""
db, tmp_root, storage_root = make_env(tmp_path)
status_map, paths = load_db(db)
t_removed, _ = clean_tmp(tmp_root, status_map, dry=True, quiet=True)
s_removed, _ = clean_storage(storage_root, paths, dry=True, quiet=True)
assert t_removed == 0 and s_removed == 0
assert (tmp_root / "done_tid").exists()
assert (storage_root / "user1" / "2026-08-11" / "orphan_storage_tid").exists()