Files
homelabstack/stacks/romhacks/orchestrator/clean_extracted.py
T

96 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Remove JDownloader auto-extraction cruft from the romhacks library.
For each library/<game>/ folder, list every archive's contents (bsdtar) and
delete only the loose files whose path exactly matches an archive entry — i.e.
files that are reproducible by re-extracting an archive that is STILL PRESENT.
Archives themselves, art, guides/spreadsheets, and anything NOT contained in an
archive are always kept. If a folder has no archive, nothing is deleted (the
loose files are the only copy).
Also audits for possible data loss: flags any folder that ends up with neither a
source archive nor a rom file.
CLEAN_DRY=true (default) previews only.
"""
import os
import pathlib
import subprocess
import sys
LIBRARY = pathlib.Path(os.environ.get("LIBRARY_DIR", "/library"))
DRY = os.environ.get("CLEAN_DRY", "true").lower() != "false"
ARCHIVE_EXTS = {".zip", ".rar", ".7z"}
ROM_EXTS = {".gba", ".gb", ".gbc", ".nds", ".3ds", ".cia", ".nsp", ".xci"}
def listing(path):
try:
out = subprocess.run(["bsdtar", "-tf", str(path)], capture_output=True,
text=True, timeout=300)
return [l for l in out.stdout.splitlines() if l.strip()]
except Exception:
return []
def main():
total_files = 0
total_bytes = 0
flagged = []
for folder in sorted(p for p in LIBRARY.iterdir() if p.is_dir()):
archives = [p for p in folder.rglob("*")
if p.is_file() and p.suffix.lower() in ARCHIVE_EXTS]
# Resolve the set of paths each archive would extract to (relative to the
# archive's own directory) — these are the reproducible files.
extracted = set()
for a in archives:
for entry in listing(a):
extracted.add((a.parent / entry).resolve())
del_files = [p for p in folder.rglob("*")
if p.is_file() and p.suffix.lower() not in ARCHIVE_EXTS
and p.resolve() in extracted]
nbytes = sum(p.stat().st_size for p in del_files)
# Audit: will the folder still hold a recoverable rom (an archive, or a
# loose rom we are NOT deleting)?
keeps = bool(archives) or any(
p.suffix.lower() in ROM_EXTS and p not in del_files
for p in folder.rglob("*") if p.is_file())
if not keeps:
flagged.append(folder.name)
if del_files:
verb = "would delete" if DRY else "deleting"
print(f"[clean] {folder.name}: {verb} {len(del_files)} file(s) "
f"{nbytes/1048576:.0f}MB (keeping {len(archives)} archive(s))")
total_files += len(del_files)
total_bytes += nbytes
if not DRY:
for p in del_files:
try:
p.unlink()
except Exception as e:
print(f" unlink fail {p}: {e}", file=sys.stderr)
if not DRY:
for d in sorted((p for p in folder.rglob("*") if p.is_dir()),
key=lambda x: len(x.parts), reverse=True):
try:
d.rmdir()
except OSError:
pass
print(f"\n[clean] {'WOULD reclaim' if DRY else 'reclaimed'} "
f"{total_bytes/1073741824:.2f} GB across {total_files} files; DRY={DRY}")
if flagged:
print(f"[clean] WARNING — {len(flagged)} folder(s) with NO archive and "
f"NO rom (possible loss / check these):")
for f in flagged:
print(" ", f)
else:
print("[clean] audit OK — every folder retains an archive or rom.")
if __name__ == "__main__":
main()