back to scripts

safe-recycle.py

python 79 lines secrets redacted

Fail-safe deletion — moves files to a @Recycle area with a JSON audit trail instead of ever hard-deleting.

Note Live script from my home-lab server. Tokens, IDs, phone numbers and other secrets have been replaced with placeholders like <WHATSAPP_GROUP_ID> — everything else is the real, running code.
#!/usr/bin/env python3
import datetime
import json
import os
import shutil
import sys
import uuid

ROOTS = ("/mnt/nas", "/mnt/jelly")
REPORT_DIR = "/home/lanky/reports"
LOG_PATH = os.path.join(REPORT_DIR, "media-recycle-audit.jsonl")


def resolve_source(value: str) -> tuple[str, str]:
    path = os.path.realpath(value)
    for root in ROOTS:
        root_real = os.path.realpath(root)
        if os.path.commonpath([path, root_real]) != root_real:
            continue
        relative = os.path.relpath(path, root_real)
        if relative == "." or relative.startswith("@Recycle" + os.sep):
            raise ValueError(f"refusing recycle source: {value}")
        return path, root_real
    raise ValueError(f"path is outside approved NAS roots: {value}")


def recycle(source_value: str, reason: str) -> dict:
    source, root = resolve_source(source_value)
    if not os.path.lexists(source):
        raise FileNotFoundError(source)

    stamp = datetime.datetime.now().astimezone()
    relative = os.path.relpath(source, root)
    destination = os.path.join(
        root,
        "@Recycle",
        "CodexReview",
        stamp.strftime("%Y-%m-%d"),
        relative,
    )
    if os.path.lexists(destination):
        base, extension = os.path.splitext(destination)
        destination = f"{base}.{stamp.strftime('%H%M%S')}.{uuid.uuid4().hex[:8]}{extension}"

    os.makedirs(os.path.dirname(destination), exist_ok=True)
    shutil.move(source, destination)
    record = {
        "timestamp": stamp.isoformat(),
        "source": source,
        "destination": destination,
        "reason": reason,
        "action": "moved_to_recycle",
    }
    os.makedirs(REPORT_DIR, exist_ok=True)
    with open(LOG_PATH, "a", encoding="utf-8") as log:
        log.write(json.dumps(record, ensure_ascii=True) + "\n")
    return record


def main() -> int:
    if len(sys.argv) < 3:
        print("Usage: safe-recycle.py <reason> <path> [path ...]", file=sys.stderr)
        return 2

    reason = sys.argv[1]
    failures = 0
    for value in sys.argv[2:]:
        try:
            record = recycle(value, reason)
            print(f"RECYCLED: {record['source']}")
            print(f"REVIEW AT: {record['destination']}")
        except Exception as error:
            failures += 1
            print(f"ERROR: {value}: {error}", file=sys.stderr)
    return 1 if failures else 0


if __name__ == "__main__":
    raise SystemExit(main())

back to scripts