refactor: backup schreibt Dump zuerst lokal, dann scp
Statt direkter pipe dump→gzip→ssh wird der Dump jetzt in BACKUP_LOCAL_DIR (default /tmp/logbuch-backup) geschrieben und danach per scp übertragen. So ist der Dump jederzeit im Container einsehbar; SSH bleibt optional. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+44
-29
@@ -1,19 +1,15 @@
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
|
import { createWriteStream, mkdirSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
export function triggerBackup(): void {
|
export function triggerBackup(): void {
|
||||||
setImmediate(() => runBackup().catch((e) => console.error('[backup] Fehler:', e)));
|
setImmediate(() => runBackup().catch((e) => console.error('[backup] Fehler:', e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runBackup(): Promise<void> {
|
async function runBackup(): Promise<void> {
|
||||||
const sshUrl = process.env.BACKUP_SSH_URL;
|
const sshUrl = process.env.BACKUP_SSH_URL || '';
|
||||||
if (!sshUrl) return;
|
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 rawKeyPath = process.env.BACKUP_SSH_KEY_PATH || '';
|
||||||
const keyPath = rawKeyPath.startsWith('~')
|
const keyPath = rawKeyPath.startsWith('~')
|
||||||
? rawKeyPath.replace('~', process.env.HOME || '/root')
|
? rawKeyPath.replace('~', process.env.HOME || '/root')
|
||||||
@@ -27,16 +23,12 @@ async function runBackup(): Promise<void> {
|
|||||||
const dbUser = process.env.DB_USER || '';
|
const dbUser = process.env.DB_USER || '';
|
||||||
const dbPass = process.env.DB_PASS || '';
|
const dbPass = process.env.DB_PASS || '';
|
||||||
const dbName = process.env.DB_NAME || 'sternwarte';
|
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';
|
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<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const dump = spawn(dumpBin, [
|
const dump = spawn(dumpBin, [
|
||||||
`-h${dbHost}`, `-P${dbPort}`, `-u${dbUser}`,
|
`-h${dbHost}`, `-P${dbPort}`, `-u${dbUser}`,
|
||||||
@@ -44,34 +36,57 @@ async function runBackup(): Promise<void> {
|
|||||||
`--ignore-table=${dbName}.beos`,
|
`--ignore-table=${dbName}.beos`,
|
||||||
dbName,
|
dbName,
|
||||||
], { env: { ...process.env, MYSQL_PWD: dbPass } });
|
], { env: { ...process.env, MYSQL_PWD: dbPass } });
|
||||||
|
|
||||||
const gzip = spawn('gzip');
|
const gzip = spawn('gzip');
|
||||||
const ssh = spawn('ssh', [...sshOpts, sshHost, `cat > ${remotePath}/${filename}`]);
|
const file = createWriteStream(localPath);
|
||||||
|
|
||||||
dump.stdout.pipe(gzip.stdin);
|
dump.stdout.pipe(gzip.stdin);
|
||||||
gzip.stdout.pipe(ssh.stdin);
|
gzip.stdout.pipe(file);
|
||||||
|
|
||||||
let dumpErr = '';
|
let dumpErr = '';
|
||||||
let sshErr = '';
|
|
||||||
dump.stderr.on('data', (d: Buffer) => { dumpErr += d.toString(); });
|
dump.stderr.on('data', (d: Buffer) => { dumpErr += d.toString(); });
|
||||||
ssh.stderr.on('data', (d: Buffer) => { sshErr += d.toString(); });
|
|
||||||
|
|
||||||
dump.on('error', reject);
|
dump.on('error', reject);
|
||||||
gzip.on('error', reject);
|
gzip.on('error', reject);
|
||||||
ssh.on('error', reject);
|
file.on('error', reject);
|
||||||
|
file.on('close', () => {
|
||||||
|
if (dumpErr.trim()) console.log('[backup] dump stderr:', dumpErr.trim());
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
dump.on('close', (code) => { if (code !== 0) gzip.stdin.end(); });
|
console.log(`[backup] Dump geschrieben: ${localPath}`);
|
||||||
gzip.on('close', () => ssh.stdin.end());
|
|
||||||
|
|
||||||
ssh.on('close', (code) => {
|
// 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<void>((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) {
|
if (code === 0) {
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(`ssh exit ${code}${sshErr ? ': ' + sshErr.trim() : ''}${dumpErr ? ' | dump: ' + dumpErr.trim() : ''}`));
|
reject(new Error(`scp exit ${code}${scpErr ? ': ' + scpErr.trim() : ''}`));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Backups älter als 30 Tage löschen
|
console.log(`[backup] ${filename} → ${sshHost}:${remotePath}`);
|
||||||
|
|
||||||
|
// Backups älter als 30 Tage auf Remote löschen
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
const ssh = spawn('ssh', [
|
const ssh = spawn('ssh', [
|
||||||
...sshOpts, sshHost,
|
...sshOpts, sshHost,
|
||||||
@@ -83,6 +98,6 @@ async function runBackup(): Promise<void> {
|
|||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
console.log(`[backup] ${filename} → ${sshHost}:${remotePath}`);
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user