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>
144 lines
4.7 KiB
JavaScript
144 lines
4.7 KiB
JavaScript
/**
|
|
* 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);
|
|
});
|