Files
Art-gallery/scripts/restore-db-data.js
T
Danila KhodjaefandCursor 2edf577faf Add dev/prod environments with TrueNAS Docker production deploy.
Split PostgreSQL into gallery_dev and gallery_prod, add Docker/Gitea deploy tooling,
SMB image sync, pgAdmin split script, dev:web on Keenetic :5173, and operator docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 15:15:19 +03:00

105 lines
2.9 KiB
JavaScript

/**
* Restore a data-only backup (INSERT dumps from npm run db:backup) into a database.
*
* Dev: node scripts/restore-db-data.js --file db/DataBackup/gallery_dev_data_....txt
* Prod: npm run db:restore:prod -- --file db/DataBackup/gallery_dev_data_....txt
*/
const fs = require('fs');
const path = require('path');
const pg = require('pg');
const {
assertProdDatabase,
assertDevDatabase,
confirmProdAction,
loadDevPgConfig,
loadProdPgConfig,
} = require('./db-env');
const { Client } = pg;
function parseArgs(argv) {
const fileIdx = argv.indexOf('--file');
if (fileIdx === -1 || !argv[fileIdx + 1]) {
throw new Error('--file <path> is required');
}
return {
filePath: path.resolve(argv[fileIdx + 1]),
prod: argv.includes('--prod'),
};
}
function extractInsertStatements(content) {
return content
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.startsWith('INSERT INTO '));
}
async function truncatePublicTables(client) {
const tablesRes = await client.query(
`SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
ORDER BY table_name`,
);
const names = tablesRes.rows.map((r) => `"${r.table_name}"`).join(', ');
if (!names) return;
await client.query(`TRUNCATE TABLE ${names} RESTART IDENTITY CASCADE`);
}
async function main() {
const { filePath, prod } = parseArgs(process.argv.slice(2));
if (!fs.existsSync(filePath)) {
throw new Error(`Backup file not found: ${filePath}`);
}
const config = prod ? loadProdPgConfig() : loadDevPgConfig();
const dbName = config.database;
const inserts = extractInsertStatements(fs.readFileSync(filePath, 'utf8'));
if (inserts.length === 0) {
throw new Error('No INSERT statements found in backup file');
}
const prompt = prod
? `RESTORE ${inserts.length} rows into PRODUCTION "${dbName}" from:\n ${filePath}\nThis TRUNCATES all public tables first.`
: `Restore ${inserts.length} rows into "${dbName}" from:\n ${filePath}\nThis TRUNCATES all public tables first.`;
if (prod) {
assertProdDatabase(dbName);
} else {
assertDevDatabase(dbName);
}
await confirmProdAction(prompt);
const client = new Client(config);
await client.connect();
console.log(`Truncating public tables in "${dbName}"...`);
await truncatePublicTables(client);
console.log(`Restoring ${inserts.length} INSERT statements...`);
await client.query('SET session_replication_role = replica');
let restored = 0;
try {
for (const statement of inserts) {
await client.query(statement);
restored += 1;
if (restored % 500 === 0) {
console.log(` ${restored}/${inserts.length}`);
}
}
} finally {
await client.query('SET session_replication_role = DEFAULT');
}
await client.end();
console.log(`Restore complete: ${restored} statements into "${dbName}".`);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});