import { spawn } from 'child_process'; import { createWriteStream, mkdirSync } from 'fs'; import { join } from 'path'; export function triggerBackup(): void { setImmediate(() => runBackup().catch((e) => console.error('[backup] Fehler:', e))); } async function runBackup(): Promise { const sshUrl = process.env.BACKUP_SSH_URL || ''; const localDir = process.env.BACKUP_LOCAL_DIR || '/tmp/logbuch-backup'; const rawKeyPath = process.env.BACKUP_SSH_KEY_PATH || ''; const keyPath = rawKeyPath.startsWith('~') ? rawKeyPath.replace('~', process.env.HOME || '/root') : rawKeyPath; const ts = new Date().toISOString().replace('T', '_').replace(/:/g, '-').slice(0, 19); const filename = `sternwarte_${ts}.sql.gz`; const dbHost = process.env.DB_HOST || 'db'; const dbPort = process.env.DB_PORT || '3306'; const dbUser = process.env.DB_USER || ''; const dbPass = process.env.DB_PASS || ''; const dbName = process.env.DB_NAME || 'sternwarte'; const dumpBin = process.env.BACKUP_DUMP_CMD || 'mariadb-dump'; mkdirSync(localDir, { recursive: true }); const localPath = join(localDir, filename); // Schritt 1: Dump lokal schreiben await new Promise((resolve, reject) => { const dump = spawn(dumpBin, [ `-h${dbHost}`, `-P${dbPort}`, `-u${dbUser}`, '--skip-ssl', `--ignore-table=${dbName}.beos`, dbName, ], { env: { ...process.env, MYSQL_PWD: dbPass } }); const gzip = spawn('gzip'); const file = createWriteStream(localPath); dump.stdout.pipe(gzip.stdin); gzip.stdout.pipe(file); let dumpErr = ''; dump.stderr.on('data', (d: Buffer) => { dumpErr += d.toString(); }); dump.on('error', reject); gzip.on('error', reject); file.on('error', reject); file.on('close', () => { if (dumpErr.trim()) console.log('[backup] dump stderr:', dumpErr.trim()); resolve(); }); }); console.log(`[backup] Dump geschrieben: ${localPath}`); // Schritt 2: Per SCP auf externen Server übertragen (optional) if (sshUrl) { const match = sshUrl.match(/^([^:]+):(.+)$/); if (!match) { console.error('[backup] BACKUP_SSH_URL muss das Format user@host:/pfad haben'); } else { const [, sshHost, remotePath] = match; const sshOpts = [ ...(keyPath ? ['-i', keyPath] : []), '-o', 'StrictHostKeyChecking=no', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=15', ]; await new Promise((resolve, reject) => { const scp = spawn('scp', [...sshOpts, localPath, `${sshHost}:${remotePath}/${filename}`]); let scpErr = ''; scp.stderr.on('data', (d: Buffer) => { scpErr += d.toString(); }); scp.on('error', reject); scp.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`scp exit ${code}${scpErr ? ': ' + scpErr.trim() : ''}`)); } }); }); console.log(`[backup] ${filename} → ${sshHost}:${remotePath}`); // Backups älter als 30 Tage auf Remote löschen await new Promise((resolve) => { const ssh = spawn('ssh', [ ...sshOpts, sshHost, `find ${remotePath} -name 'sternwarte_*.sql.gz' -mtime +30 -delete`, ]); ssh.on('error', (e) => { console.error('[backup] Cleanup spawn-Fehler:', e.message); resolve(); }); ssh.on('close', (code) => { if (code !== 0) console.error('[backup] Cleanup fehlgeschlagen (exit ' + code + ')'); resolve(); }); }); } } }