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>
This commit is contained in:
co-authored by
Cursor
parent
02d238b043
commit
2edf577faf
@@ -0,0 +1,135 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
const pg = require('pg');
|
||||
const {
|
||||
assertProdDatabase,
|
||||
assertDevDatabase,
|
||||
loadDevPgConfig,
|
||||
loadProdPgConfig,
|
||||
rootDir,
|
||||
} = require('./db-env');
|
||||
|
||||
const { Client } = pg;
|
||||
|
||||
const backupDir = path.join(rootDir, 'db', 'DataBackup');
|
||||
const fromProd = process.argv.includes('--prod');
|
||||
const config = fromProd ? loadProdPgConfig() : loadDevPgConfig();
|
||||
|
||||
if (fromProd) {
|
||||
assertProdDatabase(config.database);
|
||||
} else {
|
||||
assertDevDatabase(config.database);
|
||||
if (config.database.endsWith('_prod')) {
|
||||
console.error('Refusing backup of production database without --prod. Use: npm run db:backup:prod');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function sqlLiteral(value) {
|
||||
if (value === null || value === undefined) return 'NULL';
|
||||
if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE';
|
||||
if (value instanceof Date) return `'${value.toISOString()}'`;
|
||||
if (typeof value === 'number' || typeof value === 'bigint') return String(value);
|
||||
if (typeof value === 'object') {
|
||||
return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
|
||||
}
|
||||
return `'${String(value).replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
function timestampSlug(date = new Date()) {
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return [
|
||||
date.getFullYear(),
|
||||
pad(date.getMonth() + 1),
|
||||
pad(date.getDate()),
|
||||
'_',
|
||||
pad(date.getHours()),
|
||||
pad(date.getMinutes()),
|
||||
pad(date.getSeconds()),
|
||||
].join('');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
|
||||
const client = new Client(config);
|
||||
await client.connect();
|
||||
|
||||
const dbNameRes = await client.query('SELECT current_database() AS name');
|
||||
const dbName = dbNameRes.rows[0].name;
|
||||
|
||||
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 lines = [
|
||||
'-- Gallery PostgreSQL data backup',
|
||||
`-- Generated: ${new Date().toISOString()}`,
|
||||
`-- Database: ${dbName}`,
|
||||
'-- Format: INSERT statements (data only)',
|
||||
'',
|
||||
];
|
||||
|
||||
let totalRows = 0;
|
||||
|
||||
for (const { table_name: tableName } of tablesRes.rows) {
|
||||
const colsRes = await client.query(
|
||||
`SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = $1
|
||||
ORDER BY ordinal_position`,
|
||||
[tableName],
|
||||
);
|
||||
const columns = colsRes.rows.map((row) => row.column_name);
|
||||
const dataRes = await client.query(`SELECT * FROM "${tableName}"`);
|
||||
const rowCount = dataRes.rowCount ?? 0;
|
||||
totalRows += rowCount;
|
||||
|
||||
lines.push(`-- TABLE ${tableName} (${rowCount} rows)`);
|
||||
|
||||
if (rowCount === 0) {
|
||||
lines.push('');
|
||||
continue;
|
||||
}
|
||||
|
||||
const quotedCols = columns.map((c) => `"${c}"`).join(', ');
|
||||
for (const row of dataRes.rows) {
|
||||
const values = columns.map((col) => sqlLiteral(row[col])).join(', ');
|
||||
lines.push(`INSERT INTO "${tableName}" (${quotedCols}) VALUES (${values});`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
await client.end();
|
||||
|
||||
const slug = timestampSlug();
|
||||
const txtName = `${dbName}_data_${slug}.txt`;
|
||||
const zipName = `${dbName}_data_${slug}.zip`;
|
||||
const txtPath = path.join(backupDir, txtName);
|
||||
const zipPath = path.join(backupDir, zipName);
|
||||
|
||||
fs.writeFileSync(txtPath, lines.join('\n'), 'utf8');
|
||||
|
||||
execFileSync(
|
||||
'powershell',
|
||||
[
|
||||
'-NoProfile',
|
||||
'-Command',
|
||||
`Compress-Archive -LiteralPath '${txtPath.replace(/'/g, "''")}' -DestinationPath '${zipPath.replace(/'/g, "''")}' -Force`,
|
||||
],
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
|
||||
console.log(`Backup written: ${txtPath}`);
|
||||
console.log(`Archive written: ${zipPath}`);
|
||||
console.log(`Tables: ${tablesRes.rowCount}, rows: ${totalRows}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const readline = require('readline');
|
||||
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
|
||||
const PROD_DB_SUFFIX = '_prod';
|
||||
const DEV_DB_NAME = 'gallery_dev';
|
||||
const PROD_DB_NAME = 'gallery_prod';
|
||||
const LEGACY_DB_NAMES = ['Gallery', 'gallery'];
|
||||
|
||||
function loadEnvFile(filePath) {
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
for (const line of content.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq === -1) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let value = trimmed.slice(eq + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"'))
|
||||
|| (value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
if (process.env[key] === undefined) process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function pgConfigFromEnv() {
|
||||
const host = process.env.DB_HOST || '192.168.10.122';
|
||||
const port = Number(process.env.DB_PORT || 5432);
|
||||
const user = process.env.DB_USER;
|
||||
const password = process.env.DB_PASSWORD;
|
||||
const database = process.env.DB_NAME;
|
||||
if (!user || !password || !database) {
|
||||
throw new Error('DB_USER, DB_PASSWORD, and DB_NAME are required');
|
||||
}
|
||||
return { host, port, user, password, database };
|
||||
}
|
||||
|
||||
function assertProdDatabase(dbName) {
|
||||
if (!dbName.endsWith(PROD_DB_SUFFIX) && dbName !== PROD_DB_NAME) {
|
||||
throw new Error(
|
||||
`Refusing prod operation on database "${dbName}". `
|
||||
+ `Target must be "${PROD_DB_NAME}" or end with "${PROD_DB_SUFFIX}".`,
|
||||
);
|
||||
}
|
||||
return dbName;
|
||||
}
|
||||
|
||||
function assertDevDatabase(dbName) {
|
||||
if (dbName === PROD_DB_NAME || dbName.endsWith(PROD_DB_SUFFIX)) {
|
||||
throw new Error(
|
||||
`Refusing dev operation on production database "${dbName}". `
|
||||
+ `Use "${DEV_DB_NAME}" for development.`,
|
||||
);
|
||||
}
|
||||
return dbName;
|
||||
}
|
||||
|
||||
function loadDevPgConfig() {
|
||||
loadEnvFile(path.join(rootDir, '.env'));
|
||||
const config = pgConfigFromEnv();
|
||||
assertDevDatabase(config.database);
|
||||
return config;
|
||||
}
|
||||
|
||||
function loadProdPgConfig() {
|
||||
loadEnvFile(path.join(rootDir, 'infra', 'docker', '.env.prod'));
|
||||
const config = pgConfigFromEnv();
|
||||
assertProdDatabase(config.database);
|
||||
return config;
|
||||
}
|
||||
|
||||
async function confirmProdAction(message) {
|
||||
if (process.env.CONFIRM_PROD === '1') return;
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
const answer = await new Promise((resolve) => {
|
||||
rl.question(`${message}\nType "yes" to continue: `, resolve);
|
||||
});
|
||||
rl.close();
|
||||
if (answer.trim().toLowerCase() !== 'yes') {
|
||||
throw new Error('Aborted.');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PROD_DB_SUFFIX,
|
||||
DEV_DB_NAME,
|
||||
PROD_DB_NAME,
|
||||
LEGACY_DB_NAMES,
|
||||
rootDir,
|
||||
loadEnvFile,
|
||||
pgConfigFromEnv,
|
||||
assertProdDatabase,
|
||||
assertDevDatabase,
|
||||
loadDevPgConfig,
|
||||
loadProdPgConfig,
|
||||
confirmProdAction,
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Start public dev stack: Vite on :5173, API on :3451 (or PORT from .env).
|
||||
* Keenetic: devgallery.mysuperlab.netcraze.pro → 192.168.10.70:5173
|
||||
*/
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||||
|
||||
const apiPort = process.env.DEV_API_PORT || process.env.PORT || '3451';
|
||||
const vitePort = process.env.DEV_WEB_PORT || '5173';
|
||||
|
||||
const env = { ...process.env, PORT: apiPort };
|
||||
|
||||
function run(label, command, args, cwd) {
|
||||
const child = spawn(command, args, {
|
||||
cwd,
|
||||
env,
|
||||
shell: true,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
child.on('exit', (code) => {
|
||||
if (code && code !== 0) {
|
||||
console.error(`${label} exited with code ${code}`);
|
||||
}
|
||||
});
|
||||
return child;
|
||||
}
|
||||
|
||||
console.log(`Gallery dev:web — UI http://0.0.0.0:${vitePort} API http://127.0.0.1:${apiPort}`);
|
||||
console.log(`Set PUBLIC_URL=http://devgallery.mysuperlab.netcraze.pro in .env for Keenetic`);
|
||||
|
||||
const api = run('API', 'npx', ['nodemon', 'server/index.js'], path.join(__dirname, '..'));
|
||||
const client = run('Vite', 'npm', ['run', 'dev', '--prefix', 'client', '--', '--port', vitePort, '--host'], path.join(__dirname, '..'));
|
||||
|
||||
function shutdown() {
|
||||
api.kill('SIGTERM');
|
||||
client.kill('SIGTERM');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* One-time split of legacy Gallery → gallery_prod + gallery_dev on TrueNAS PostgreSQL.
|
||||
*
|
||||
* Usage:
|
||||
* PGPASSWORD=... PGHOST=192.168.10.122 PGUSER=postgres npm run db:split-databases
|
||||
*
|
||||
* Options:
|
||||
* --copy pg_dump legacy → gallery_prod instead of rename
|
||||
* --skip-dev Do not create gallery_dev
|
||||
*/
|
||||
const { execFileSync } = require('child_process');
|
||||
const pg = require('pg');
|
||||
const {
|
||||
DEV_DB_NAME,
|
||||
LEGACY_DB_NAMES,
|
||||
PROD_DB_NAME,
|
||||
} = require('./db-env');
|
||||
|
||||
const { Client } = pg;
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const useCopy = args.has('--copy');
|
||||
const skipDev = args.has('--skip-dev');
|
||||
|
||||
const host = process.env.PGHOST ?? process.env.DB_HOST ?? '192.168.10.122';
|
||||
const port = Number(process.env.PGPORT ?? process.env.DB_PORT ?? 5432);
|
||||
const user = process.env.PGUSER ?? process.env.DB_USER ?? 'postgres';
|
||||
const password = process.env.PGPASSWORD ?? process.env.DB_PASSWORD;
|
||||
|
||||
if (!password) {
|
||||
console.error('PGPASSWORD (or DB_PASSWORD) is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function quoteIdent(ident) {
|
||||
return `"${ident.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
async function databaseExists(client, name) {
|
||||
const res = await client.query(
|
||||
'SELECT 1 FROM pg_database WHERE datname = $1',
|
||||
[name],
|
||||
);
|
||||
return res.rowCount > 0;
|
||||
}
|
||||
|
||||
async function findLegacyName(client) {
|
||||
for (const name of LEGACY_DB_NAMES) {
|
||||
if (await databaseExists(client, name)) return name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function createDatabaseIfMissing(admin, name) {
|
||||
if (await databaseExists(admin, name)) {
|
||||
console.log(`Database "${name}" already exists`);
|
||||
return false;
|
||||
}
|
||||
await admin.query(`CREATE DATABASE ${quoteIdent(name)}`);
|
||||
console.log(`Created database "${name}"`);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function terminateConnections(admin, dbName) {
|
||||
await admin.query(
|
||||
`SELECT pg_terminate_backend(pid)
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = $1 AND pid <> pg_backend_pid()`,
|
||||
[dbName],
|
||||
);
|
||||
}
|
||||
|
||||
async function cloneDevFromProd(admin) {
|
||||
if (await databaseExists(admin, DEV_DB_NAME)) {
|
||||
console.log(`"${DEV_DB_NAME}" already exists — skipping dev clone`);
|
||||
return;
|
||||
}
|
||||
|
||||
await terminateConnections(admin, PROD_DB_NAME);
|
||||
console.log(`Creating "${DEV_DB_NAME}" from TEMPLATE "${PROD_DB_NAME}"...`);
|
||||
await admin.query(
|
||||
`CREATE DATABASE ${quoteIdent(DEV_DB_NAME)} WITH TEMPLATE ${quoteIdent(PROD_DB_NAME)} OWNER ${quoteIdent(user)}`,
|
||||
);
|
||||
console.log(`Created "${DEV_DB_NAME}" as a full copy of "${PROD_DB_NAME}"`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const admin = new Client({ host, port, user, password, database: 'postgres' });
|
||||
await admin.connect();
|
||||
|
||||
const legacyName = await findLegacyName(admin);
|
||||
const hasProd = await databaseExists(admin, PROD_DB_NAME);
|
||||
|
||||
if (hasProd) {
|
||||
console.log(`"${PROD_DB_NAME}" already exists — skipping prod migration`);
|
||||
} else if (legacyName) {
|
||||
if (useCopy) {
|
||||
await createDatabaseIfMissing(admin, PROD_DB_NAME);
|
||||
console.log(`Copying ${legacyName} → ${PROD_DB_NAME} via pg_dump...`);
|
||||
const dump = execFileSync(
|
||||
'pg_dump',
|
||||
['-h', host, '-p', String(port), '-U', user, '-d', legacyName, '--no-owner', '--no-acl'],
|
||||
{ env: { ...process.env, PGPASSWORD: password }, encoding: 'buffer' },
|
||||
);
|
||||
execFileSync(
|
||||
'psql',
|
||||
['-h', host, '-p', String(port), '-U', user, '-d', PROD_DB_NAME, '-v', 'ON_ERROR_STOP=1'],
|
||||
{ env: { ...process.env, PGPASSWORD: password }, input: dump, stdio: ['pipe', 'inherit', 'inherit'] },
|
||||
);
|
||||
console.log(`Copied ${legacyName} → ${PROD_DB_NAME}`);
|
||||
} else {
|
||||
await terminateConnections(admin, legacyName);
|
||||
await admin.query(`ALTER DATABASE ${quoteIdent(legacyName)} RENAME TO ${quoteIdent(PROD_DB_NAME)}`);
|
||||
console.log(`Renamed ${legacyName} → ${PROD_DB_NAME}`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`No legacy database (${LEGACY_DB_NAMES.join(' / ')}) or "${PROD_DB_NAME}" found`);
|
||||
await createDatabaseIfMissing(admin, PROD_DB_NAME);
|
||||
console.log('Run npm run migrate against gallery_prod to apply schema');
|
||||
}
|
||||
|
||||
if (!skipDev) {
|
||||
await cloneDevFromProd(admin);
|
||||
console.log(`Next on dev PC: set .env DB_NAME=${DEV_DB_NAME}, then npm run migrate`);
|
||||
}
|
||||
|
||||
await admin.end();
|
||||
console.log('Done. TrueNAS compose.truenas.yaml should use DB_NAME=gallery_prod.');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
if (error.code === '42501') {
|
||||
console.error(
|
||||
'Permission denied — the gallery user cannot rename databases.\n'
|
||||
+ 'Use pgAdmin on the dev PC (recommended):\n'
|
||||
+ ' Open db/split-dev-prod-pgadmin.sql → connect as postgres → run each STEP (F5)\n'
|
||||
+ 'Or: PGUSER=postgres PGPASSWORD=... npm run db:split-databases\n'
|
||||
+ 'Or: psql -h 192.168.10.122 -U postgres -d postgres -f db/split-dev-prod.sql',
|
||||
);
|
||||
}
|
||||
console.error(error.message || error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Clone gallery_prod → gallery_dev (PostgreSQL TEMPLATE).
|
||||
* Requires superuser or CREATEDB on the Postgres host.
|
||||
*/
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
const pg = require('pg');
|
||||
const {
|
||||
DEV_DB_NAME,
|
||||
PROD_DB_NAME,
|
||||
loadProdPgConfig,
|
||||
rootDir,
|
||||
} = require('./db-env');
|
||||
|
||||
const { Client } = pg;
|
||||
|
||||
function quoteIdent(ident) {
|
||||
return `"${ident.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const prodConfig = loadProdPgConfig();
|
||||
const { host, port, user, password } = prodConfig;
|
||||
|
||||
const admin = new Client({ host, port, user, password, database: 'postgres' });
|
||||
await admin.connect();
|
||||
|
||||
console.log(`Terminating connections to "${DEV_DB_NAME}"...`);
|
||||
await admin.query(
|
||||
`SELECT pg_terminate_backend(pid)
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = $1 AND pid <> pg_backend_pid()`,
|
||||
[DEV_DB_NAME],
|
||||
);
|
||||
|
||||
const exists = await admin.query(
|
||||
'SELECT 1 FROM pg_database WHERE datname = $1',
|
||||
[DEV_DB_NAME],
|
||||
);
|
||||
|
||||
if (exists.rowCount > 0) {
|
||||
console.log(`Dropping "${DEV_DB_NAME}"...`);
|
||||
await admin.query(`DROP DATABASE ${quoteIdent(DEV_DB_NAME)}`);
|
||||
}
|
||||
|
||||
console.log(`Terminating connections to "${PROD_DB_NAME}"...`);
|
||||
await admin.query(
|
||||
`SELECT pg_terminate_backend(pid)
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = $1 AND pid <> pg_backend_pid()`,
|
||||
[PROD_DB_NAME],
|
||||
);
|
||||
|
||||
console.log(`Cloning "${PROD_DB_NAME}" → "${DEV_DB_NAME}" (TEMPLATE)...`);
|
||||
await admin.query(
|
||||
`CREATE DATABASE ${quoteIdent(DEV_DB_NAME)} WITH TEMPLATE ${quoteIdent(PROD_DB_NAME)} OWNER ${quoteIdent(user)}`,
|
||||
);
|
||||
|
||||
await admin.end();
|
||||
|
||||
const verify = new Client({ host, port, user, password, database: DEV_DB_NAME });
|
||||
await verify.connect();
|
||||
const tables = await verify.query(
|
||||
`SELECT count(*)::int AS n FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'`,
|
||||
);
|
||||
const paintings = await verify.query('SELECT count(*)::int AS n FROM paintings');
|
||||
await verify.end();
|
||||
|
||||
console.log(`Done. "${DEV_DB_NAME}" is a copy of "${PROD_DB_NAME}" (${tables.rows[0].n} tables, ${paintings.rows[0].n} paintings).`);
|
||||
console.log('Syncing image files from production...');
|
||||
execFileSync('powershell', ['-NoProfile', '-File', path.join(rootDir, 'infra', 'scripts', 'sync-images-from-prod.ps1'), '-SkipConfirm'], {
|
||||
cwd: rootDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user