← back to Wallco Ai
Add sweep-orphan-rooms.py: remove unreferenced room PNGs (dry-run default, --delete/--verify), reclaimed ~1.7GB on prod
6ef7cefcd2bca2b7dfa81115b22594387f1c4e5f · 2026-05-29 08:43:51 -0700 · Steve
Files touched
A scripts/sweep-orphan-rooms.py
Diff
commit 6ef7cefcd2bca2b7dfa81115b22594387f1c4e5f
Author: Steve <steve@designerwallcoverings.com>
Date: Fri May 29 08:43:51 2026 -0700
Add sweep-orphan-rooms.py: remove unreferenced room PNGs (dry-run default, --delete/--verify), reclaimed ~1.7GB on prod
---
scripts/sweep-orphan-rooms.py | 96 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 96 insertions(+)
diff --git a/scripts/sweep-orphan-rooms.py b/scripts/sweep-orphan-rooms.py
new file mode 100644
index 0000000..fe459af
--- /dev/null
+++ b/scripts/sweep-orphan-rooms.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+"""Sweep orphan room-mockup PNGs from data/rooms.
+
+An orphan is any data/rooms/design_<id>_<room>[_hero].png whose (id, room) pair
+is NOT referenced by that design's room_mockups in designs.json (or whose id is
+absent from the snapshot entirely). The server only ever serves rooms listed in
+room_mockups, so orphans are unreachable dead weight on disk.
+
+_hero variants are tied to their base room: design_<id>_<room>_hero.png is kept
+iff (id, room) is referenced — so a referenced hero is never swept.
+
+Default is a DRY RUN (reports scope, writes the orphan list, deletes nothing).
+Pass --delete to actually remove. Pass --verify to re-audit broken refs after.
+
+Usage:
+ sweep-orphan-rooms.py # dry run
+ sweep-orphan-rooms.py --delete --verify
+"""
+import json, os, re, glob, argparse
+from collections import defaultdict
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # served project dir
+ROOMS_DIR = os.path.join(ROOT, 'data', 'rooms')
+DESIGNS = os.path.join(ROOT, 'data', 'designs.json')
+PAT = re.compile(r'design_(\d+)_([a-z_]+?)(_hero)?\.png$')
+
+
+def rooms_of(x):
+ rm = x.get('room_mockups') or []
+ if isinstance(rm, dict):
+ return set(rm.keys())
+ if isinstance(rm, list):
+ return set(r for r in rm if isinstance(r, str))
+ return set()
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--delete', action='store_true', help='actually remove orphans (default: dry run)')
+ ap.add_argument('--verify', action='store_true', help='re-audit broken refs after')
+ ap.add_argument('--list-out', default='/tmp/orphan_rooms.txt')
+ a = ap.parse_args()
+
+ d = json.load(open(DESIGNS))
+ ids_in_snap = set(x['id'] for x in d)
+ ref = {x['id']: rooms_of(x) for x in d}
+
+ files = glob.glob(ROOMS_DIR + '/design_*.png')
+ orphans = []
+ reason = defaultdict(int)
+ total_sz = orphan_sz = 0
+ kept = 0
+ for f in files:
+ sz = os.path.getsize(f); total_sz += sz
+ m = PAT.search(os.path.basename(f))
+ if not m:
+ orphans.append(f); orphan_sz += sz; reason['unparseable'] += 1; continue
+ did = int(m.group(1)); room = m.group(2)
+ if did not in ids_in_snap:
+ orphans.append(f); orphan_sz += sz; reason['id_not_in_snapshot'] += 1
+ elif room not in ref.get(did, set()):
+ orphans.append(f); orphan_sz += sz; reason['room_not_in_mockups'] += 1
+ else:
+ kept += 1
+
+ print('total room PNGs: %d (%.1f MB)' % (len(files), total_sz / 1e6))
+ print('KEEP (referenced): %d' % kept)
+ print('ORPHANS: %d (%.1f MB reclaimable)' % (len(orphans), orphan_sz / 1e6))
+ print('orphan reasons:', dict(reason))
+ open(a.list_out, 'w').write('\n'.join(orphans))
+ print('orphan list -> %s' % a.list_out)
+
+ if a.delete:
+ n = 0
+ for f in orphans:
+ try:
+ os.remove(f); n += 1
+ except FileNotFoundError:
+ pass
+ print('[deleted] %d files' % n)
+ else:
+ print('[dry run] nothing deleted — pass --delete to act')
+
+ if a.verify:
+ broken = defaultdict(int)
+ for x in d:
+ if not x.get('is_published'):
+ continue
+ for r in rooms_of(x):
+ if not os.path.exists(ROOMS_DIR + '/design_%d_%s.png' % (x['id'], r)):
+ broken[r] += 1
+ print('post-sweep broken refs (published):', dict(broken) or 'NONE — 0 broken')
+
+
+if __name__ == '__main__':
+ main()
← 9b7f1eb feat(designs-grid): Contact sheet button on admin bulk-selec
·
back to Wallco Ai
·
fix(cactus-curator): batch bulk-delete + drop removed SKUs f 3778fde →