import { mkdir, readdir, stat, unlink } from "node:fs/promises"; import { randomBytes } from "node:crypto"; import { resolve } from "node:path"; import { db, dbPath } from "./db.mjs"; const projectRoot = resolve(import.meta.dirname, ".."); const backupRoot = resolve(projectRoot, "data", "backups"); const relativeBackupRoot = "data/backups"; function backupEntry(name, fileStat) { return { name, relativePath: `${relativeBackupRoot}/${name}`, bytes: Number(fileStat.size || 0), createdAt: fileStat.birthtime?.toISOString?.() || fileStat.mtime?.toISOString?.() || null, modifiedAt: fileStat.mtime?.toISOString?.() || null }; } export async function listDatabaseBackups() { let names = []; try { names = await readdir(backupRoot); } catch (error) { if (error.code !== "ENOENT") throw error; } const entries = await Promise.all(names.filter((name) => /^platform-[A-Z0-9-]+\.sqlite$/i.test(name)).map(async (name) => { try { return backupEntry(name, await stat(resolve(backupRoot, name))); } catch (error) { if (error.code === "ENOENT") return null; throw error; } })); return entries.filter(Boolean).sort((left, right) => String(right.modifiedAt || "").localeCompare(String(left.modifiedAt || ""))); } export async function backupSummary() { const backups = await listDatabaseBackups(); return { root: relativeBackupRoot, source: dbPath, count: backups.length, latest: backups[0] || null, backups }; } export async function createDatabaseBackup() { await mkdir(backupRoot, { recursive: true }); const stamp = new Date().toISOString().replace(/[^0-9TZ]/g, "-"); const name = `platform-${stamp}-${randomBytes(4).toString("hex")}.sqlite`; const target = resolve(backupRoot, name); const escapedTarget = target.replaceAll("'", "''"); try { db.exec(`VACUUM INTO '${escapedTarget}'`); const entry = backupEntry(name, await stat(target)); return { backup: entry, ...(await backupSummary()) }; } catch (error) { await unlink(target).catch(() => {}); throw error; } }