fix: Dump via mysql2 statt mariadb-dump
MariaDB-Client kennt caching_sha2_password (MySQL-8-Default) nicht. Der mysql2-Node.js-Treiber implementiert das Plugin nativ und verbindet sich problemlos. Der Dump schreibt CREATE TABLE + INSERT-Batches (200 Zeilen) direkt via gzip in die lokale Datei. Keine externen Binaries mehr für den Dump-Schritt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+80
-67
@@ -1,77 +1,96 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { createWriteStream, mkdirSync } from 'fs';
|
||||
import { createGzip } from 'zlib';
|
||||
import { join } from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import { getPool } from './db';
|
||||
|
||||
export function triggerBackup(): void {
|
||||
setImmediate(() => runBackup().catch((e) => console.error('[backup] Fehler:', e)));
|
||||
}
|
||||
|
||||
async function dumpToFile(filePath: string): Promise<void> {
|
||||
const dbName = process.env.DB_NAME || 'sternwarte';
|
||||
const pool = getPool();
|
||||
|
||||
const gzip = createGzip();
|
||||
const file = createWriteStream(filePath);
|
||||
gzip.pipe(file);
|
||||
|
||||
const write = (s: string) => new Promise<void>((res, rej) =>
|
||||
gzip.write(s, (e) => e ? rej(e) : res())
|
||||
);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
await write(`-- Führungsbuch Backup ${now}\n-- Datenbank: ${dbName} (ohne Tabelle beos)\n\nSET FOREIGN_KEY_CHECKS=0;\n\n`);
|
||||
|
||||
const [tableRows] = await pool.query('SHOW TABLES') as [Record<string, string>[], unknown];
|
||||
const tables = tableRows
|
||||
.map((r) => Object.values(r)[0])
|
||||
.filter((t) => t !== 'beos');
|
||||
|
||||
for (const table of tables) {
|
||||
const [[createRow]] = await pool.query(`SHOW CREATE TABLE \`${table}\``) as [Record<string, string>[], unknown];
|
||||
const createSql = Object.values(createRow)[1];
|
||||
|
||||
await write(`DROP TABLE IF EXISTS \`${table}\`;\n`);
|
||||
await write(`${createSql};\n\n`);
|
||||
|
||||
const [rows] = await pool.query(`SELECT * FROM \`${table}\``) as [Record<string, unknown>[], unknown];
|
||||
if (rows.length > 0) {
|
||||
const cols = Object.keys(rows[0]).map((c) => `\`${c}\``).join(', ');
|
||||
const batchSize = 200;
|
||||
for (let i = 0; i < rows.length; i += batchSize) {
|
||||
const batch = rows.slice(i, i + batchSize);
|
||||
const values = batch.map((row) =>
|
||||
'(' + Object.values(row).map((v) => {
|
||||
if (v === null) return 'NULL';
|
||||
if (typeof v === 'number') return String(v);
|
||||
if (v instanceof Date) return `'${v.toISOString().slice(0, 19).replace('T', ' ')}'`;
|
||||
return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
||||
}).join(', ') + ')'
|
||||
).join(',\n ');
|
||||
await write(`INSERT INTO \`${table}\` (${cols}) VALUES\n ${values};\n`);
|
||||
}
|
||||
await write('\n');
|
||||
}
|
||||
}
|
||||
|
||||
await write('SET FOREIGN_KEY_CHECKS=1;\n');
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
gzip.end();
|
||||
file.on('close', resolve);
|
||||
file.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function runBackup(): Promise<void> {
|
||||
const sshUrl = process.env.BACKUP_SSH_URL || '';
|
||||
const localDir = process.env.BACKUP_LOCAL_DIR || '/tmp/logbuch-backup';
|
||||
|
||||
const ts = new Date().toISOString().replace('T', '_').replace(/:/g, '-').slice(0, 19);
|
||||
const filename = `sternwarte_${ts}.sql.gz`;
|
||||
|
||||
mkdirSync(localDir, { recursive: true });
|
||||
const localPath = join(localDir, filename);
|
||||
|
||||
await dumpToFile(localPath);
|
||||
console.log(`[backup] Dump geschrieben: ${localPath}`);
|
||||
|
||||
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 || '';
|
||||
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<void>((resolve, reject) => {
|
||||
const dump = spawn(dumpBin, [
|
||||
`-h${dbHost}`, `-P${dbPort}`, `-u${dbUser}`,
|
||||
'--skip-ssl',
|
||||
'--default-auth=mysql_native_password',
|
||||
`--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 = '';
|
||||
let dumpCode: number | null = null;
|
||||
dump.stderr.on('data', (d: Buffer) => { dumpErr += d.toString(); });
|
||||
dump.on('error', reject);
|
||||
gzip.on('error', reject);
|
||||
file.on('error', reject);
|
||||
dump.on('close', (code) => {
|
||||
dumpCode = code;
|
||||
if (code !== 0) gzip.stdin.end();
|
||||
});
|
||||
file.on('close', () => {
|
||||
if (dumpCode !== 0) {
|
||||
reject(new Error(`${dumpBin} exit ${dumpCode}: ${dumpErr.trim()}`));
|
||||
} else {
|
||||
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',
|
||||
@@ -84,13 +103,9 @@ async function runBackup(): Promise<void> {
|
||||
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() : ''}`));
|
||||
}
|
||||
});
|
||||
scp.on('close', (code) =>
|
||||
code === 0 ? resolve() : reject(new Error(`scp exit ${code}${scpErr ? ': ' + scpErr.trim() : ''}`))
|
||||
);
|
||||
});
|
||||
|
||||
console.log(`[backup] ${filename} → ${sshHost}:${remotePath}`);
|
||||
@@ -108,5 +123,3 @@ async function runBackup(): Promise<void> {
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user