restore-json-from-backup.js
1 #!/usr/bin/env node 2 /** 3 * restore-json-from-backup.js 4 * 5 * Extracts score_json and contacts_json from a backup DB to the filesystem: 6 * data/scores/{site_id}.json 7 * data/contacts/{site_id}.json 8 * 9 * Does NOT overwrite existing files — only fills gaps. 10 * 11 * Usage: 12 * node scripts/restore-json-from-backup.js /path/to/backup.db 13 * node scripts/restore-json-from-backup.js /path/to/backup.db --dry-run 14 * node scripts/restore-json-from-backup.js /path/to/backup.db --scores-only 15 * node scripts/restore-json-from-backup.js /path/to/backup.db --contacts-only 16 */ 17 18 import Database from 'better-sqlite3'; 19 import { existsSync, mkdirSync, writeFileSync } from 'fs'; 20 import { join, resolve, dirname } from 'path'; 21 import { fileURLToPath } from 'url'; 22 23 const __dirname = dirname(fileURLToPath(import.meta.url)); 24 const PROJECT_ROOT = resolve(__dirname, '..'); 25 26 const backupPath = process.argv[2]; 27 if (!backupPath || backupPath.startsWith('--')) { 28 console.error('Usage: node scripts/restore-json-from-backup.js /path/to/backup.db [--dry-run] [--scores-only] [--contacts-only]'); 29 process.exit(1); 30 } 31 32 if (!existsSync(backupPath)) { 33 console.error(`Backup not found: ${backupPath}`); 34 process.exit(1); 35 } 36 37 const DRY_RUN = process.argv.includes('--dry-run'); 38 const SCORES_ONLY = process.argv.includes('--scores-only'); 39 const CONTACTS_ONLY = process.argv.includes('--contacts-only'); 40 41 const SCORES_DIR = join(PROJECT_ROOT, 'data/scores'); 42 const CONTACTS_DIR = join(PROJECT_ROOT, 'data/contacts'); 43 const BATCH_SIZE = 500; 44 45 function extract(db, column, outDir, label) { 46 console.log(`\n=== Extracting ${label} ===`); 47 if (!DRY_RUN) mkdirSync(outDir, { recursive: true }); 48 49 const total = db.prepare( 50 `SELECT COUNT(*) as c FROM sites WHERE ${column} IS NOT NULL AND LENGTH(${column}) > 20` 51 ).get().c; 52 console.log(`Rows with ${label}: ${total.toLocaleString()}`); 53 if (total === 0) return { written: 0, skipped: 0, errors: 0 }; 54 55 let written = 0, skipped = 0, errors = 0, offset = 0; 56 57 while (true) { 58 const rows = db.prepare( 59 `SELECT id, ${column} as json_data FROM sites 60 WHERE ${column} IS NOT NULL AND LENGTH(${column}) > 20 61 LIMIT ? OFFSET ?` 62 ).all(BATCH_SIZE, offset); 63 64 if (rows.length === 0) break; 65 offset += BATCH_SIZE; 66 67 for (const row of rows) { 68 try { 69 const filePath = join(outDir, `${row.id}.json`); 70 if (existsSync(filePath)) { 71 skipped++; 72 continue; 73 } 74 if (!DRY_RUN) writeFileSync(filePath, row.json_data, 'utf8'); 75 written++; 76 } catch (err) { 77 console.error(` Error site ${row.id}: ${err.message}`); 78 errors++; 79 } 80 } 81 82 const progress = written + skipped + errors; 83 if (progress % 5000 < BATCH_SIZE || rows.length < BATCH_SIZE) { 84 console.log(` ${progress.toLocaleString()}/${total.toLocaleString()} (written=${written}, skipped=${skipped}, errors=${errors})`); 85 } 86 } 87 88 console.log(`${label} done: written=${written}, skipped=${skipped}, errors=${errors}`); 89 return { written, skipped, errors }; 90 } 91 92 console.log(`=== Restore JSON from backup ===`); 93 console.log(`Backup: ${backupPath}`); 94 console.log(`Mode: ${DRY_RUN ? 'DRY RUN' : 'LIVE'}`); 95 96 const db = new Database(backupPath, { readonly: true }); 97 db.pragma('busy_timeout = 30000'); 98 99 try { 100 if (!CONTACTS_ONLY) extract(db, 'score_json', SCORES_DIR, 'score_json'); 101 if (!SCORES_ONLY) extract(db, 'contacts_json', CONTACTS_DIR, 'contacts_json'); 102 } finally { 103 db.close(); 104 } 105 106 console.log('\nDone.');