10b52d268e
Ohne 'error'-Handler auf den Child-Prozessen führt spawn ENOENT zu uncaughtException statt zu einem gefangenen Promise-Reject. Außerdem wird '~' im BACKUP_SSH_KEY_PATH jetzt manuell zu $HOME expandiert, da spawn keine Shell-Expansion macht. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
import { spawn } from 'child_process';
|
|
|
|
export function triggerBackup(): void {
|
|
runBackup().catch((e) => console.error('[backup] Fehler:', e));
|
|
}
|
|
|
|
async function runBackup(): Promise<void> {
|
|
const sshUrl = process.env.BACKUP_SSH_URL;
|
|
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 rawKeyPath = process.env.BACKUP_SSH_KEY_PATH || '/app/.ssh/id_rsa';
|
|
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 sshOpts = [
|
|
'-i', keyPath,
|
|
'-o', 'StrictHostKeyChecking=no',
|
|
'-o', 'BatchMode=yes',
|
|
];
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
const dump = spawn('mysqldump', [
|
|
`-h${dbHost}`, `-P${dbPort}`, `-u${dbUser}`, `-p${dbPass}`,
|
|
`--ignore-table=${dbName}.beos`,
|
|
dbName,
|
|
]);
|
|
const gzip = spawn('gzip');
|
|
const ssh = spawn('ssh', [...sshOpts, sshHost, `cat > ${remotePath}/${filename}`]);
|
|
|
|
dump.stdout.pipe(gzip.stdin);
|
|
gzip.stdout.pipe(ssh.stdin);
|
|
|
|
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<void>((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();
|
|
});
|
|
});
|
|
|
|
console.log(`[backup] ${filename} → ${sshHost}:${remotePath}`);
|
|
}
|