feat: bootstrap commercial AI drama platform
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user