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:
42
src/lib/server/backup/export.ts
Normal file
42
src/lib/server/backup/export.ts
Normal 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`;
|
||||||
|
}
|
||||||
18
src/routes/api/admin/backup/+server.ts
Normal file
18
src/routes/api/admin/backup/+server.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { createBackupStream, backupFilename } from '$lib/server/backup/export';
|
||||||
|
import { Readable } from 'node:stream';
|
||||||
|
|
||||||
|
const DB_PATH = process.env.DATABASE_PATH ?? './data/kochwas.db';
|
||||||
|
const IMAGE_DIR = process.env.IMAGE_DIR ?? './data/images';
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async () => {
|
||||||
|
const archive = createBackupStream({ dbPath: DB_PATH, imagesDir: IMAGE_DIR });
|
||||||
|
const filename = backupFilename();
|
||||||
|
return new Response(Readable.toWeb(archive) as ReadableStream, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/zip',
|
||||||
|
'content-disposition': `attachment; filename="${filename}"`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user