feat(backup): add ZIP export endpoint (DB + images)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 15:35:19 +02:00
parent 3207166fe8
commit 2ca1bbaf07
2 changed files with 60 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
import archiver from 'archiver';
import { createReadStream, existsSync, readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { Readable } from 'node:stream';
type Options = {
dbPath: string;
imagesDir: string;
};
export function createBackupStream({ dbPath, imagesDir }: Options): Readable {
const archive = archiver('zip', { zlib: { level: 6 } });
// DB file — include .db plus WAL if present (best-effort)
if (existsSync(dbPath)) {
archive.file(dbPath, { name: 'kochwas.db' });
const wal = `${dbPath}-wal`;
if (existsSync(wal)) archive.file(wal, { name: 'kochwas.db-wal' });
const shm = `${dbPath}-shm`;
if (existsSync(shm)) archive.file(shm, { name: 'kochwas.db-shm' });
}
// Images
if (existsSync(imagesDir) && statSync(imagesDir).isDirectory()) {
for (const name of readdirSync(imagesDir)) {
const full = join(imagesDir, name);
if (statSync(full).isFile()) {
archive.file(full, { name: `images/${name}` });
}
}
}
void archive.finalize();
return archive;
}
export function backupFilename(): string {
const now = new Date();
const pad = (n: number) => String(n).padStart(2, '0');
const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}`;
return `kochwas-backup_${ts}.zip`;
}