#!/usr/bin/env python3 """Dry-run-first cleanup for old unreferenced derived/cache artifacts.""" from __future__ import annotations import argparse import json from pathlib import Path from audit_data_operations import build_report from release_backup_guard import require_confirmation, verify_current_backup from app.core.config import get_settings from app.db.session import SessionLocal CONFIRMATION = "DELETE_STORAGE_ARTIFACTS" def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--storage-root", type=Path) parser.add_argument("--minimum-age-days", type=int, default=7) parser.add_argument("--max-delete", type=int, default=25) parser.add_argument("--apply", action="store_true") parser.add_argument("--confirm") parser.add_argument("--backup-dir", type=Path) parser.add_argument("--backup-max-age-hours", type=float, default=24.0) return parser.parse_args() def main() -> int: args = parse_args() if args.max_delete < 0: raise SystemExit("--max-delete must be greater than or equal to zero") storage_root = (args.storage_root or Path(get_settings().storage_root)).resolve() with SessionLocal() as db: report, candidates = build_report( storage_root, db, minimum_age_days=args.minimum_age_days, max_candidate_records=max(args.max_delete, 500), ) blocked_reason = None backup = None deleted: list[str] = [] if args.apply: require_confirmation(args.confirm, CONFIRMATION) if args.backup_dir is None: raise RuntimeError("--backup-dir is required with --apply") backup = verify_current_backup( args.backup_dir, max_age_hours=args.backup_max_age_hours, ) if len(candidates) > args.max_delete: blocked_reason = ( f"candidate_count {len(candidates)} exceeds --max-delete {args.max_delete}; " "review the dry run and raise the explicit limit" ) else: for candidate in candidates: candidate.path.unlink() deleted.append(candidate.relative_path) payload = { "schema_version": 1, "mode": "apply" if args.apply else "dry-run", "storage_root": str(storage_root), "minimum_age_days": args.minimum_age_days, "max_delete": args.max_delete, "candidate_count": len(candidates), "candidate_bytes": sum(item.size_bytes for item in candidates), "candidates": [item.relative_path for item in candidates], "deleted_count": len(deleted), "deleted": deleted, "blocked_reason": blocked_reason, "protected_prefixes": report["cleanup"]["protected_prefixes"], "backup": ( { "release_id": backup.release_id, "created_at": backup.created_at.isoformat(), "age_hours": round(backup.age_hours, 3), "git_commit": backup.git_commit, } if backup else None ), } print(json.dumps(payload, indent=2, sort_keys=True)) return 1 if blocked_reason else 0 if __name__ == "__main__": raise SystemExit(main())