/ scripts / restore-json-from-backup.js
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.');