/** * Read-only MinIO inventory for the legacy → new-CRM migration (Phase 2 sizing). * * Lists every bucket the creds can see, then for the document buckets * (`client-portal`, `signatures`) groups objects by top-level prefix with * counts + sizes + samples — so we can see exactly where the EOIs, berth * PDFs, receipts and business-card images live before backfilling them. * * Secret-free: reads creds from env. Run with: * MINIO_ACCESS_KEY=... MINIO_SECRET_KEY=... \ * pnpm tsx scripts/migration/probe-minio.ts * * Strictly read-only (listBuckets + listObjectsV2). No writes. */ import { Client } from 'minio'; const endPoint = process.env.MINIO_ENDPOINT || 's3.portnimara.com'; const accessKey = process.env.MINIO_ACCESS_KEY; const secretKey = process.env.MINIO_SECRET_KEY; if (!accessKey || !secretKey) { console.error('Set MINIO_ACCESS_KEY and MINIO_SECRET_KEY'); process.exit(1); } const client = new Client({ endPoint, port: 443, useSSL: true, accessKey, secretKey }); interface PrefixStat { count: number; bytes: number; samples: string[]; } async function inventory(bucket: string) { const byPrefix = new Map(); let total = 0; let totalBytes = 0; await new Promise((resolve, reject) => { const stream = client.listObjectsV2(bucket, '', true); stream.on('data', (o) => { if (!o.name) return; total++; totalBytes += o.size || 0; const top = o.name.includes('/') ? o.name.split('/')[0] + '/' : '(root)'; const e = byPrefix.get(top) || { count: 0, bytes: 0, samples: [] }; e.count++; e.bytes += o.size || 0; if (e.samples.length < 4) e.samples.push(`${o.name} (${o.size}b)`); byPrefix.set(top, e); }); stream.on('end', () => resolve()); stream.on('error', reject); }); return { bucket, total, totalBytes, byPrefix }; } const mb = (b: number) => (b / 1e6).toFixed(1); async function main() { console.log(`MinIO @ ${endPoint}\n`); let buckets: string[] = []; try { const list = await client.listBuckets(); buckets = list.map((b) => b.name); console.log('=== all buckets visible to these creds ==='); for (const b of list) console.log(` ${b.name}`); } catch (err) { console.log(`listBuckets failed: ${(err as Error).message}`); } const targets = (process.env.MINIO_BUCKETS || 'client-portal,signatures') .split(',') .map((s) => s.trim()); for (const bucket of targets) { if (buckets.length && !buckets.includes(bucket)) { console.log(`\n=== bucket: ${bucket} — NOT VISIBLE to these creds ===`); continue; } try { const inv = await inventory(bucket); console.log( `\n=== bucket: ${inv.bucket} — ${inv.total} objects, ${mb(inv.totalBytes)} MB ===`, ); const rows = [...inv.byPrefix.entries()].sort((a, z) => z[1].count - a[1].count); for (const [prefix, e] of rows) { console.log( ` ${prefix.padEnd(30)} ${String(e.count).padStart(5)} obj ${mb(e.bytes).padStart(8)} MB`, ); for (const s of e.samples) console.log(` e.g. ${s}`); } } catch (err) { console.log(`\n=== bucket: ${bucket} — ERROR: ${(err as Error).message} ===`); } } } main().catch((err) => { console.error('probe-minio failed:', err); process.exit(1); });