diff --git a/Dockerfile b/Dockerfile index 152ef97..477e1e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,8 @@ WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 +RUN apk add --no-cache mysql-client openssh-client + RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs diff --git a/app/api/logbuch/[id]/route.ts b/app/api/logbuch/[id]/route.ts index a84805d..2edf820 100644 --- a/app/api/logbuch/[id]/route.ts +++ b/app/api/logbuch/[id]/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { query, getPool } from '@/lib/db'; import { getSession } from '@/lib/session'; +import { triggerBackup } from '@/lib/backup'; import type { SelectedObjekt } from '@/types/logbuch'; export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { @@ -68,6 +69,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ ); } + triggerBackup(); return NextResponse.json({ ok: true }); } catch (error) { console.error('PUT /api/logbuch/[id]:', error); diff --git a/app/api/logbuch/route.ts b/app/api/logbuch/route.ts index ae1fca2..6332cb4 100644 --- a/app/api/logbuch/route.ts +++ b/app/api/logbuch/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { query, getPool } from '@/lib/db'; import { getSession } from '@/lib/session'; +import { triggerBackup } from '@/lib/backup'; import type { SelectedObjekt } from '@/types/logbuch'; const LIST_SQL = @@ -137,6 +138,7 @@ export async function POST(request: NextRequest) { ); } + triggerBackup(); return NextResponse.json({ id: logbuchId }, { status: 201 }); } catch (error) { console.error('POST /api/logbuch:', error); diff --git a/lib/backup.ts b/lib/backup.ts new file mode 100644 index 0000000..2356df4 --- /dev/null +++ b/lib/backup.ts @@ -0,0 +1,76 @@ +import { spawn } from 'child_process'; + +export function triggerBackup(): void { + runBackup().catch((e) => console.error('[backup] Fehler:', e)); +} + +async function runBackup(): Promise { + 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 keyPath = process.env.BACKUP_SSH_KEY_PATH || '/app/.ssh/id_rsa'; + + 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((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('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('close', (code) => { + if (code !== 0) console.error('[backup] Cleanup fehlgeschlagen (exit ' + code + ')'); + resolve(); + }); + }); + + console.log(`[backup] ${filename} → ${sshHost}:${remotePath}`); +}