Files
devplacepy/devplacepy/cli/system.py
T
retoorandClaude Sonnet 5 68a7c3b002 Give the system-prune CLI command a two-phase plan/execute report
Restructures the disk-cleanup command to show location, current on-disk
size, and an estimated reclaim per area (attachments, project files,
container workspaces) before touching anything - --dry-run stops there.
A real run re-executes each check, reports actual items/bytes freed,
an "After" size per area, and a final total with elapsed time, flagging
any drift between the estimate and execution passes (e.g. the live
server wrote something in between).

store.gc_workspaces() now reports bytes freed alongside the removed
count (previously count-only), so the containers gc-workspaces CLI
action picks up the same reporting for free.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5tFWkm3UbcstcbPKFE5gP
2026-09-08 05:07:31 +02:00

203 lines
6.1 KiB
Python

# retoor <retoor@molodetz.nl>
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)