# retoor from devplacepy.cli._shared import _audit_cli RULE_WIDTH = 72 def _rule(char: str = "=") -> str: return char * RULE_WIDTH def _human(n) -> str: n = float(n) for unit in ("B", "KB", "MB", "GB", "TB"): if n < 1024: return f"{n:.1f}{unit}" n /= 1024 return f"{n:.1f}PB" def _dir_stats(path) -> tuple[int, int]: import os count = 0 total = 0 for root, _dirs, files in os.walk(str(path)): for name in files: count += 1 try: total += os.path.getsize(os.path.join(root, name)) except OSError: continue return count, total def _section(title: str) -> None: print() print(_rule()) print(title) print(_rule()) def _build_areas(): from pathlib import Path from devplacepy import config from devplacepy.attachments import ( ATTACHMENTS_DIR, purge_soft_deleted_attachments, sweep_orphan_attachment_blobs, ) from devplacepy.project_files import ( PROJECT_FILES_DIR, purge_soft_deleted_project_files, sweep_orphan_project_file_blobs, ) from devplacepy.services.containers import store as container_store return [ { "label": "Attachments", "path": ATTACHMENTS_DIR, "checks": [ ("soft-deleted attachments", purge_soft_deleted_attachments), ( "orphan attachment blobs (zero DB reference)", sweep_orphan_attachment_blobs, ), ], }, { "label": "Project files", "path": PROJECT_FILES_DIR, "checks": [ ("soft-deleted project files", purge_soft_deleted_project_files), ( "orphan project-file blobs (zero DB reference)", sweep_orphan_project_file_blobs, ), ], }, { "label": "Container workspaces", "path": Path(config.CONTAINER_WORKSPACES_DIR), "checks": [ ( "orphaned workspace directories (no live instance)", container_store.gc_workspaces, ), ], }, ] def cmd_system_prune(args): import time dry_run = bool(args.dry_run) areas = _build_areas() print("System prune - safe but aggressive disk-space cleanup") print( "Removes only content with zero live reference: soft-deleted attachment/" "\nproject-file blobs, blob files with no matching database row at all" "\n(orphans, e.g. left by an interrupted or racing sync), and orphaned" "\ncontainer workspace directories. Never touches live content." ) if dry_run: print("DRY RUN: this pass only reports; nothing will be deleted.") started = time.monotonic() _section("Where the data is, and what can be reclaimed (estimate)") estimate_items = 0 estimate_bytes = 0 for area in areas: count, size = _dir_stats(area["path"]) print(f"\n{area['label']}") print(f" location: {area['path']}") print(f" currently on disk: {count} file(s), {_human(size)}") for check_label, fn in area["checks"]: c, f = fn(dry_run=True) estimate_items += c estimate_bytes += f print(f" expected to reclaim - {check_label}: {c} item(s), {_human(f)}") print() print(_rule("-")) print( f"Estimated total: {estimate_items} item(s), {_human(estimate_bytes)} reclaimable" ) print(_rule("-")) if dry_run: print() print("DRY RUN complete: nothing was deleted. Re-run without --dry-run to apply.") return _section("Executing") actual_items = 0 actual_bytes = 0 for area in areas: print(f"\n{area['label']} - {area['path']}") for check_label, fn in area["checks"]: c, f = fn(dry_run=False) actual_items += c actual_bytes += f print(f" removed - {check_label}: {c} item(s), {_human(f)}") _section("After") for area in areas: count, size = _dir_stats(area["path"]) print(f" {area['label']} ({area['path']}): {count} file(s), {_human(size)}") elapsed = time.monotonic() - started print() print(_rule()) print( f"Removed {actual_items} item(s) total, freed {_human(actual_bytes)} " f"in {elapsed:.1f}s" ) if estimate_bytes != actual_bytes or estimate_items != actual_items: print( f"(Estimate was {estimate_items} item(s), {_human(estimate_bytes)} - " "state changed between the estimate and execution passes, e.g. the " "live server wrote or soft-deleted something in between.)" ) print(_rule()) _audit_cli( "cli.system.prune", f"CLI system prune removed {actual_items} item(s), freed {_human(actual_bytes)}", metadata={ "items": actual_items, "bytes_freed": actual_bytes, "estimated_items": estimate_items, "estimated_bytes": estimate_bytes, "elapsed_seconds": round(elapsed, 3), }, ) def register_system(subparsers): system = subparsers.add_parser("system", help="Cross-cutting system maintenance") system_sub = system.add_subparsers(title="action", dest="action") prune = system_sub.add_parser( "prune", help=( "Safe but aggressive disk-space cleanup: purges soft-deleted " "attachment/project-file blobs and any blob with zero database " "reference at all, plus orphaned container workspace directories. " "Reports location, current size, and estimated reclaim per area " "before acting, then confirms what was actually freed." ), ) prune.add_argument( "--dry-run", action="store_true", help="Report location, size, and estimated reclaim without deleting anything", ) prune.set_defaults(func=cmd_system_prune)