diff --git a/lib/backup.ts b/lib/backup.ts index 3df4c9d..5ad6890 100644 --- a/lib/backup.ts +++ b/lib/backup.ts @@ -77,57 +77,60 @@ async function runBackup(): Promise { await dumpToFile(localPath); console.log(`[backup] Dump geschrieben: ${localPath}`); - if (!sshUrl) return; + try { + if (!sshUrl) return; - 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 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') - : rawKeyPath; + const rawKeyPath = process.env.BACKUP_SSH_KEY_PATH || ''; + const keyPath = rawKeyPath.startsWith('~') + ? rawKeyPath.replace('~', process.env.HOME || '/root') + : rawKeyPath; - const sshOpts = [ - ...(keyPath ? ['-i', keyPath] : []), - '-o', 'StrictHostKeyChecking=no', - '-o', 'BatchMode=yes', - '-o', 'ConnectTimeout=15', - ]; + const sshOpts = [ + ...(keyPath ? ['-i', keyPath] : []), + '-o', 'StrictHostKeyChecking=no', + '-o', 'BatchMode=yes', + '-o', 'ConnectTimeout=15', + ]; - // Zielverzeichnis auf Remote anlegen falls nicht vorhanden - await new Promise((resolve, reject) => { - const ssh = spawn('ssh', [...sshOpts, sshHost, `mkdir -p ${remotePath}`]); - ssh.on('error', reject); - ssh.on('close', (code) => code === 0 ? resolve() : reject(new Error(`mkdir -p exit ${code}`))); - }); - - 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) => - code === 0 ? resolve() : reject(new Error(`scp exit ${code}${scpErr ? ': ' + scpErr.trim() : ''}`)) - ); - }); - - unlinkSync(localPath); - 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(); + // Zielverzeichnis auf Remote anlegen falls nicht vorhanden + await new Promise((resolve, reject) => { + const ssh = spawn('ssh', [...sshOpts, sshHost, `mkdir -p ${remotePath}`]); + ssh.on('error', reject); + ssh.on('close', (code) => code === 0 ? resolve() : reject(new Error(`mkdir -p exit ${code}`))); }); - }); + + 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) => + code === 0 ? resolve() : 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(); + }); + }); + } finally { + try { unlinkSync(localPath); } catch { /* bereits gelöscht oder nie angelegt */ } + } }