4b25231c01
- 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
130 lines
5.4 KiB
Python
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()
|