← back to Dw Image Shrink

purge-candidates.js

56 lines

// Build a GATED purge manifest of disposable image bytes, sized in GB via CDN
// HEAD. Categories (least-regret first):
//   A archived_img  — images on ARCHIVED products (hidden from storefront)
//   B gif_archived  — animated GIFs (fat spin-gifs) on archived products
//   C gif_active    — animated GIFs on ACTIVE products (product decision: deleting
//                     removes the "spin" viewer — FLAGGED, not recommended)
// READ-ONLY (HEAD only). Writes purge-manifest.jsonl (candidates) + purge-summary.md.
// NOTHING is deleted here — deletion is a separate Steve-gated step.
import fs from 'fs';
import https from 'https';
const DIR='/Users/macstudio3/Projects/dw-image-shrink';
const rows=fs.readFileSync(DIR+'/inventory.jsonl','utf8').split('\n').filter(Boolean).map(l=>JSON.parse(l));
const isGif=s=>/\.gif(\?|$)/i.test(s||'');
function head(url){return new Promise(res=>{try{const u=new URL(url);const rq=https.request({method:'HEAD',host:u.host,path:u.pathname+u.search},r=>{r.resume();res(+(r.headers['content-length']||0));});rq.on('error',()=>res(0));rq.setTimeout(15000,()=>{rq.destroy();res(0);});rq.end();}catch(e){res(0);}});}
async function pool(items,fn,conc=24){let i=0,out=new Array(items.length);await Promise.all(Array.from({length:conc},async()=>{while(i<items.length){const k=i++;out[k]=await fn(items[k]);}}));return out;}
const sample=(a,n)=>{if(a.length<=n)return a.slice();const b=a.slice();for(let i=b.length-1;i>0;i--){const j=(i*2654435761>>>0)%(i+1);[b[i],b[j]]=[b[j],b[i]];}return b.slice(0,n);};
const GB=b=>(b/1073741824);

(async()=>{
  const archived = rows.filter(r=>r.status==='archived');
  const archImg = archived.filter(r=>!isGif(r.src));
  const gifArch = archived.filter(r=>isGif(r.src));
  const gifActive = rows.filter(r=>r.status==='active' && isGif(r.src));

  // A: archived non-gif — HEAD sample -> extrapolate
  const sA=sample(archImg,800); const bA=(await pool(sA,r=>head(r.src))).filter(x=>x>0);
  const meanA=bA.reduce((a,b)=>a+b,0)/(bA.length||1); const totA=meanA*archImg.length;
  // B: archived gifs — HEAD ALL (fat, few)
  const bB=(await pool(gifArch,r=>head(r.src))).filter(x=>x>0); const totB=bB.reduce((a,b)=>a+b,0);
  // C: active gifs — HEAD ALL (flag only)
  const bC=(await pool(gifActive,r=>head(r.src))).filter(x=>x>0); const totC=bC.reduce((a,b)=>a+b,0);

  // write manifest (A + B are the recommended-deletable set; C flagged)
  const man=fs.createWriteStream(DIR+'/purge-manifest.jsonl');
  for(const r of archImg) man.write(JSON.stringify({cat:'A_archived_img',product_id:r.product_id,image_id:r.image_id,src:r.src})+'\n');
  for(const r of gifArch) man.write(JSON.stringify({cat:'B_gif_archived',product_id:r.product_id,image_id:r.image_id,src:r.src})+'\n');
  for(const r of gifActive) man.write(JSON.stringify({cat:'C_gif_active_FLAG',product_id:r.product_id,image_id:r.image_id,src:r.src})+'\n');
  man.end();

  const md=`# Purge Candidates (GATED — nothing deleted yet)
_Sized via CDN HEAD. Deletion frees storage only after Shopify async-GC._

| cat | what | # images | est. size | recommend |
|---|---|--:|--:|---|
| A | images on ARCHIVED products | ${archImg.length.toLocaleString()} | ~${GB(totA).toFixed(1)} GB | DELETE (hidden from storefront) |
| B | spin-GIFs on ARCHIVED products | ${gifArch.length.toLocaleString()} | ${GB(totB).toFixed(2)} GB (exact) | DELETE |
| C | spin-GIFs on ACTIVE products | ${gifActive.length.toLocaleString()} | ${GB(totC).toFixed(2)} GB (exact) | FLAG — removes the on-page "spin" viewer; your call |
| — | **A+B recommended reclaim** | | **~${GB(totA+totB).toFixed(1)} GB** | |

Manifest: purge-manifest.jsonl (one row per candidate image; delete only the categories you approve).
NOT measured here (need your manual check / separate step): the **Files section** (Settings → Files) and **videos** — potentially the biggest non-image consumers.
`;
  fs.writeFileSync(DIR+'/purge-summary.md',md);
  console.log(md);
})();