← back to Dw Image Shrink
files-audit.js
54 lines
// Enumerate the Shopify Files section (Settings → Files) with SIZES — the UI
// can't sort by size, but the GraphQL files connection exposes fileSize. Uses the
// THEME token (has read_themes → files access; the products token doesn't).
// Sorts by size desc, sums totals by type, writes files-inventory.jsonl +
// files-biggest.md (top consumers with IDs + links). READ-ONLY.
import fs from 'fs';
import https from 'https';
const SHOP='designer-laboratory-sandbox.myshopify.com';
const TOK=fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8').split('\n').find(l=>l.startsWith('SHOPIFY_THEME_TOKEN=')).split('=').slice(1).join('=').replace(/["' ]/g,'');
const DIR='/Users/macstudio3/Projects/dw-image-shrink';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
function gql(query){return new Promise((res,rej)=>{const body=JSON.stringify({query});const req=https.request({method:'POST',host:SHOP,path:'/admin/api/2024-10/graphql.json',headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json','Content-Length':Buffer.byteLength(body)}},r=>{let d='';r.on('data',c=>d+=c);r.on('end',()=>{try{res(JSON.parse(d))}catch(e){res({errors:[{message:d.slice(0,100)}]})}});});req.on('error',rej);req.write(body);req.end();});}
const Q=(cursor)=>`{ files(first: 250${cursor?`, after: "${cursor}"`:''}, sortKey: CREATED_AT, reverse: true) {
pageInfo { hasNextPage endCursor }
edges { node { __typename id alt createdAt
... on MediaImage { image { url } originalSource { fileSize } }
... on GenericFile { url originalFileSize mimeType }
... on Video { filename originalSource { fileSize } }
} } } }`;
const out=fs.createWriteStream(DIR+'/files-inventory.jsonl');
let cursor=null, page=0, total=0, bytes=0;
const byType={}; const all=[];
while(true){
const r=await gql(Q(cursor));
if(r.errors){ console.error('ERR',JSON.stringify(r.errors).slice(0,160)); break; }
const conn=r.data.files;
for(const e of conn.edges){
const n=e.node; const t=n.__typename;
const size = n.__typename==='GenericFile' ? (n.originalFileSize||0) : (n.originalSource&&n.originalSource.fileSize)||0;
const url = n.__typename==='GenericFile'? n.url : (n.image&&n.image.url)|| n.filename || '';
const rec={id:n.id,type:t,size:+size,mime:n.mimeType||'',url,created:n.createdAt};
out.write(JSON.stringify(rec)+'\n'); all.push(rec);
total++; bytes+=+size; byType[t]=byType[t]||{n:0,b:0}; byType[t].n++; byType[t].b+=+size;
}
page++;
if(page%10===0) console.log(`page ${page} | files ${total} | ${(bytes/1073741824).toFixed(1)}GB so far`);
if(!conn.pageInfo.hasNextPage) break;
cursor=conn.pageInfo.endCursor;
const cost=r.extensions&&r.extensions.cost&&r.extensions.cost.throttleStatus;
if(cost && cost.currentlyAvailable<400) await sleep(800); else await sleep(150);
}
out.end();
const GB=b=>(b/1073741824).toFixed(2);
all.sort((a,b)=>b.size-a.size);
let md=`# Shopify Files Section — Size Audit\n\n**${total.toLocaleString()} files, total ${GB(bytes)} GB**\n\n## By type\n`;
for(const [t,v] of Object.entries(byType).sort((a,b)=>b[1].b-a[1].b)) md+=`- ${t}: ${v.n.toLocaleString()} files, ${GB(v.b)} GB\n`;
md+=`\n## Top 40 biggest files (delete these first)\n| size | type | id | url |\n|--:|---|---|---|\n`;
for(const r of all.slice(0,40)) md+=`| ${(r.size/1048576).toFixed(1)} MB | ${r.type} | ${r.id.split('/').pop()} | ${(r.url||'').slice(0,60)} |\n`;
fs.writeFileSync(DIR+'/files-biggest.md',md);
console.log('\n'+md.split('## Top')[0]);
console.log('→ files-biggest.md (top 40) + files-inventory.jsonl (all)');