#!/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); });