diff --git a/lib/backup.ts b/lib/backup.ts index db51aca..104bcc6 100644 --- a/lib/backup.ts +++ b/lib/backup.ts @@ -1,19 +1,15 @@ 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; - if (!sshUrl) return; + const sshUrl = process.env.BACKUP_SSH_URL || ''; + const localDir = process.env.BACKUP_LOCAL_DIR || '/tmp/logbuch-backup'; - const match = sshUrl.match(/^([^:]+):(.+)$/); - if (!match) { - console.error('[backup] BACKUP_SSH_URL muss das Format user@host:/pfad haben'); - return; - } - const [, sshHost, remotePath] = match; const rawKeyPath = process.env.BACKUP_SSH_KEY_PATH || ''; const keyPath = rawKeyPath.startsWith('~') ? rawKeyPath.replace('~', process.env.HOME || '/root') @@ -27,16 +23,12 @@ async function runBackup(): Promise { const dbUser = process.env.DB_USER || ''; const dbPass = process.env.DB_PASS || ''; const dbName = process.env.DB_NAME || 'sternwarte'; - - const sshOpts = [ - ...(keyPath ? ['-i', keyPath] : []), - '-o', 'StrictHostKeyChecking=no', - '-o', 'BatchMode=yes', - '-o', 'ConnectTimeout=15', - ]; - 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}`, @@ -44,45 +36,68 @@ async function runBackup(): Promise { `--ignore-table=${dbName}.beos`, dbName, ], { env: { ...process.env, MYSQL_PWD: dbPass } }); + const gzip = spawn('gzip'); - const ssh = spawn('ssh', [...sshOpts, sshHost, `cat > ${remotePath}/${filename}`]); + const file = createWriteStream(localPath); dump.stdout.pipe(gzip.stdin); - gzip.stdout.pipe(ssh.stdin); + gzip.stdout.pipe(file); let dumpErr = ''; - let sshErr = ''; dump.stderr.on('data', (d: Buffer) => { dumpErr += d.toString(); }); - ssh.stderr.on('data', (d: Buffer) => { sshErr += d.toString(); }); - dump.on('error', reject); gzip.on('error', reject); - ssh.on('error', reject); - - dump.on('close', (code) => { if (code !== 0) gzip.stdin.end(); }); - gzip.on('close', () => ssh.stdin.end()); - - ssh.on('close', (code) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`ssh exit ${code}${sshErr ? ': ' + sshErr.trim() : ''}${dumpErr ? ' | dump: ' + dumpErr.trim() : ''}`)); - } - }); - }); - - // Backups älter als 30 Tage 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 + ')'); + file.on('error', reject); + file.on('close', () => { + if (dumpErr.trim()) console.log('[backup] dump stderr:', dumpErr.trim()); resolve(); }); }); - console.log(`[backup] ${filename} → ${sshHost}:${remotePath}`); + 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(); + }); + }); + } + } }