chore(romhacks): add extraction-cruft cleanup script (keeps archives, audits for loss)
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user