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:
1 parent
02d238b043
commit
2edf577faf
34 files changed
+1742
-80
No files matched your search
@@ -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,
|
||||
};
|
||||
Reference in new issue
Block a user