64 lines
2.1 KiB
JavaScript
64 lines
2.1 KiB
JavaScript
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;
|
|
}
|
|
}
|