← back to Wallco Ai
purge-orphan-pngs: reclaim gen-time orphan PNGs (no DB row) — 1693 files freed
eedbd244c1621a442dd7e08763a037ff0179b647 · 2026-06-09 13:32:28 -0700 · Steve
Files touched
A scripts/purge-orphan-pngs.js
Diff
commit eedbd244c1621a442dd7e08763a037ff0179b647
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Jun 9 13:32:28 2026 -0700
purge-orphan-pngs: reclaim gen-time orphan PNGs (no DB row) — 1693 files freed
---
scripts/purge-orphan-pngs.js | 53 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 53 insertions(+)
diff --git a/scripts/purge-orphan-pngs.js b/scripts/purge-orphan-pngs.js
new file mode 100644
index 0000000..83206f0
--- /dev/null
+++ b/scripts/purge-orphan-pngs.js
@@ -0,0 +1,53 @@
+#!/usr/bin/env node
+// Purge orphan generation PNGs — files in data/generated with NO matching DB row
+// (timeout-killed mid-pipeline or gate-rejected). Keeps every file referenced by an
+// all_designs.local_path. Excludes files <30min old (in-flight gens). Sanity-guards
+// against a bad DB query deleting the world. Run with --apply to actually delete.
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const GEN = path.join(process.env.HOME, 'Projects/wallco-ai/data/generated');
+const PSQL = '/opt/homebrew/opt/postgresql@14/bin/psql';
+const APPLY = process.argv.includes('--apply');
+const NOW = Date.now();
+const MIN_AGE_MS = 30 * 60 * 1000; // skip files touched in the last 30 min
+
+// 1) in-use filename set from DB (basename of every local_path)
+const rows = execSync(
+ `${PSQL} -U stevestudio2 -d dw_unified -tA -c "SELECT DISTINCT regexp_replace(local_path,'^.*/','') FROM all_designs WHERE local_path IS NOT NULL AND local_path <> '';"`,
+ { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
+const inUse = new Set(rows.split('\n').map(s => s.trim()).filter(Boolean));
+console.log(`DB references ${inUse.size} distinct filenames`);
+
+// 2) walk data/generated
+const all = fs.readdirSync(GEN);
+const baseRe = /^\d{10,}_\d+\.png$/; // the generator's <ms>_<seed>.png
+const origRe = /^\d{10,}_\d+\.original\.png$/; // its pre-processing sibling
+let kept = 0, orphanBytes = 0, orphanCount = 0, skippedRecent = 0;
+const toDelete = [];
+
+for (const f of all) {
+ const isBase = baseRe.test(f), isOrig = origRe.test(f);
+ if (!isBase && !isOrig) { kept++; continue; } // not a gen PNG — leave it
+ // the row-key for an .original is its base .png name
+ const key = isOrig ? f.replace('.original.png', '.png') : f;
+ if (inUse.has(key)) { kept++; continue; } // has a DB row → KEEP
+ const fp = path.join(GEN, f);
+ let st; try { st = fs.statSync(fp); } catch { continue; }
+ if (NOW - st.mtimeMs < MIN_AGE_MS) { skippedRecent++; continue; } // in-flight → KEEP
+ toDelete.push(fp); orphanBytes += st.size; orphanCount++;
+}
+
+const totalGenLike = kept + orphanCount + skippedRecent;
+console.log(`scanned: ${all.length} files | gen-PNGs kept(have row): ${kept} | recent-skip: ${skippedRecent} | ORPHANS: ${orphanCount} (${(orphanBytes/1e9).toFixed(2)} GB)`);
+
+// 3) sanity guards — refuse to run if the result looks insane
+if (inUse.size < 1000) { console.error('ABORT: DB returned <1000 filenames — query likely broke. No deletion.'); process.exit(1); }
+if (orphanCount > all.length*0.95){ console.error('ABORT: >95% would be deleted — refusing.'); process.exit(1); }
+
+if (!APPLY) { console.log('\nDRY RUN — pass --apply to delete the above orphans.'); process.exit(0); }
+
+let freed = 0, del = 0;
+for (const fp of toDelete) { try { freed += fs.statSync(fp).size; fs.unlinkSync(fp); del++; } catch {} }
+console.log(`\nDELETED ${del} orphan files, reclaimed ${(freed/1e9).toFixed(2)} GB.`);
← dbe4464 generator: log gate rejections to data/logs/gate-rejections.
·
back to Wallco Ai
·
settlement vision: free local ollama fallback (gemma3:12b) w 4e65df7 →