← back to Dw Image Shrink
restore-imageless.js
42 lines
// Auto-restore the products left imageless by the canary. Their ORIGINAL images
// still resolve on Shopify's CDN (orig_src → 200), so we re-POST them by src.
// While AT the cap this 413s harmlessly; it retries (smallest-first) so the
// instant Shopify GC frees space, the originals are restored. Marks the ledger
// row 'restored' on success. node restore-imageless.js [--loop] [--interval 120]
import fs from 'fs';
import https from 'https';
import pg from 'pg';
const SHOP='designer-laboratory-sandbox.myshopify.com';
const ATOK=fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8').split('\n').find(l=>l.startsWith('SHOPIFY_ADMIN_TOKEN=')).split('=').slice(1).join('=').replace(/["' ]/g,'');
const LOOP=process.argv.includes('--loop');
const arg=(k,d)=>{const a=process.argv.find(x=>x.startsWith('--'+k+'='));return a?a.split('=').slice(1).join('='):d;};
const INTERVAL=(+arg('interval','120'))*1000;
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
function raw(m,p,b){return new Promise((res,rej)=>{const data=b?JSON.stringify(b):null;const req=https.request({method:m,host:SHOP,path:p,headers:{'X-Shopify-Access-Token':ATOK,'Content-Type':'application/json',...(data?{'Content-Length':Buffer.byteLength(data)}:{})}},r=>{let d='';r.on('data',c=>d+=c);r.on('end',()=>res({status:r.statusCode,json:(()=>{try{return JSON.parse(d)}catch(e){return d}})()}));});req.on('error',rej);if(data)req.write(data);req.end();});}
function headLen(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.end();}catch(e){res(0);}});}
const db=new pg.Client({host:'/tmp',database:'dw_unified'}); await db.connect();
async function pass(){
const {rows}=await db.query("SELECT * FROM img_shrink_ledger WHERE state='imageless_alert' ORDER BY id");
if(!rows.length){ console.log('no imageless rows remaining'); return 0; }
// sort smallest-first by CDN size so partial free restores the most
for(const r of rows){ r._len=await headLen(r.orig_src); }
rows.sort((a,b)=>a._len-b._len);
let restored=0;
for(const r of rows){
const alt=r.orig_alt||null;
const up=await raw('POST',`/admin/api/2024-10/products/${r.product_id}/images.json`,{image:{src:r.orig_src,position:r.orig_position||1,...(alt?{alt}:{})}});
if(up.status<300 && up.json.image){
await db.query("UPDATE img_shrink_ledger SET state='restored', new_image_id=$2, new_src=$3, updated_at=now() WHERE id=$1",[r.id,up.json.image.id,up.json.image.src]);
console.log(`RESTORED product ${r.product_id} (${(r._len/1048576).toFixed(1)}MB)`);
restored++;
} else {
console.log(`still blocked product ${r.product_id}: ${up.status} ${JSON.stringify(up.json).slice(0,60)}`);
}
}
return restored;
}
if(LOOP){ while(true){ const {rows:[c]}=await db.query("SELECT count(*) n FROM img_shrink_ledger WHERE state='imageless_alert'"); if(+c.n===0){console.log('all restored — exiting loop');break;} await pass(); await sleep(INTERVAL); } }
else { await pass(); }
await db.end();