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);
|
||||
});
|
||||
Reference in New Issue
Block a user