u
This commit is contained in:
154
scripts/s3-backup/backup.mjs
Normal file
154
scripts/s3-backup/backup.mjs
Normal file
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import https from 'node:https';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
import {
|
||||
ACCESS_KEY,
|
||||
BUCKET,
|
||||
DATA_DIR,
|
||||
HOST,
|
||||
PORT,
|
||||
SECRET_KEY,
|
||||
s3BaseUrl,
|
||||
} from './config.mjs';
|
||||
import { ensureCerts, installCaTrust } from './ensure-certs.mjs';
|
||||
import { startServer } from './server.mjs';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
dotenv.config({ path: path.join(root, '.env') });
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const databases = args.has('--all')
|
||||
? ['eazybusiness', 'Mandant_3']
|
||||
: [process.env.MSSQL_DATABASE || 'eazybusiness'];
|
||||
|
||||
const serverOnly = args.has('--server-only');
|
||||
const skipTrust = args.has('--skip-trust');
|
||||
|
||||
function sqlcmd(query) {
|
||||
const server = process.env.MSSQL_SERVER || 'localhost';
|
||||
const port = process.env.MSSQL_PORT || '1433';
|
||||
const user = process.env.MSSQL_USER || 'sa';
|
||||
const password = process.env.MSSQL_PASSWORD || '';
|
||||
const result = spawnSync(
|
||||
'sqlcmd',
|
||||
['-W', '-C', '-S', `${server},${port}`, '-U', user, '-P', password, '-Q', query],
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
const output = `${result.stdout || ''}${result.stderr || ''}`;
|
||||
if (result.status !== 0 || /^\s*Msg \d+,/m.test(output)) {
|
||||
throw new Error(output.trim() || 'sqlcmd failed');
|
||||
}
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
function timestamp() {
|
||||
return new Date().toISOString().replace(/[:.]/g, '-');
|
||||
}
|
||||
|
||||
async function waitForServer() {
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const ok = await new Promise((resolve) => {
|
||||
const req = https.request(
|
||||
{ host: HOST, port: PORT, path: '/', method: 'GET', rejectUnauthorized: false },
|
||||
(res) => {
|
||||
res.resume();
|
||||
resolve(res.statusCode === 403 || res.statusCode === 200);
|
||||
}
|
||||
);
|
||||
req.on('error', () => resolve(false));
|
||||
req.end();
|
||||
});
|
||||
if (ok) return;
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
}
|
||||
throw new Error(`S3 endpoint did not start on https://${HOST}:${PORT}`);
|
||||
}
|
||||
|
||||
function ensureCredential() {
|
||||
const cred = s3BaseUrl();
|
||||
sqlcmd(`
|
||||
IF EXISTS (SELECT 1 FROM sys.credentials WHERE name = N'${cred}')
|
||||
DROP CREDENTIAL [${cred}];
|
||||
CREATE CREDENTIAL [${cred}]
|
||||
WITH IDENTITY = 'S3 Access Key',
|
||||
SECRET = '${ACCESS_KEY}:${SECRET_KEY}';
|
||||
`);
|
||||
}
|
||||
|
||||
function backupDatabase(db) {
|
||||
const file = `${db}-${timestamp()}.bak`;
|
||||
const url = `${s3BaseUrl()}/${file}`;
|
||||
console.log(`Backing up ${db} -> ${url}`);
|
||||
sqlcmd(`
|
||||
BACKUP DATABASE [${db}]
|
||||
TO URL = '${url}'
|
||||
WITH FORMAT, COMPRESSION, STATS = 10;
|
||||
`);
|
||||
const onDisk = path.join(DATA_DIR, file);
|
||||
if (!fs.existsSync(onDisk)) {
|
||||
throw new Error(`Backup finished but file missing on disk: ${onDisk}`);
|
||||
}
|
||||
const mb = (fs.statSync(onDisk).size / (1024 * 1024)).toFixed(1);
|
||||
console.log(`Saved ${onDisk} (${mb} MB)`);
|
||||
return onDisk;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
ensureCerts(HOST);
|
||||
if (!skipTrust) {
|
||||
const trusted = installCaTrust();
|
||||
if (!trusted) {
|
||||
console.warn('Could not install CA into MSSQL container. Run:');
|
||||
console.warn(` docker cp scripts/s3-backup/certs/ca.pem mssql:/tmp/jtlsrv-s3-ca.pem`);
|
||||
console.warn(' docker exec mssql bash -lc "cp /tmp/jtlsrv-s3-ca.pem /usr/local/share/ca-certificates/jtlsrv-s3.crt && update-ca-certificates"');
|
||||
} else {
|
||||
console.log('Installed S3 CA into MSSQL container');
|
||||
}
|
||||
}
|
||||
|
||||
await startServer();
|
||||
await waitForServer();
|
||||
|
||||
if (serverOnly) {
|
||||
console.log('Server running (--server-only). Ctrl+C to stop.');
|
||||
return;
|
||||
}
|
||||
|
||||
ensureCredential();
|
||||
const saved = [];
|
||||
for (const db of databases) {
|
||||
saved.push(backupDatabase(db));
|
||||
}
|
||||
|
||||
console.log('\nBackups on disk:');
|
||||
for (const file of saved) {
|
||||
console.log(` ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.has('--help')) {
|
||||
console.log(`Usage: node scripts/s3-backup/backup.mjs [options]
|
||||
|
||||
Starts a local S3-compatible HTTPS endpoint and backs up MSSQL to disk.
|
||||
|
||||
Options:
|
||||
--all Backup eazybusiness and Mandant_3
|
||||
--server-only Start endpoint only, no backup
|
||||
--skip-trust Skip installing CA cert into system trust store
|
||||
|
||||
Reads MSSQL_* from .env in repo root.
|
||||
Backups land in scripts/s3-backup/data/${BUCKET}/
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err.message || err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user