196 lines
5.3 KiB
TypeScript
196 lines
5.3 KiB
TypeScript
import "server-only";
|
|
import path from "node:path";
|
|
import { rawDb } from "@/lib/db/client";
|
|
import { clearAppSettingsCache } from "@/lib/db/appSettings";
|
|
|
|
const DB_PATH = path.join(process.cwd(), "data", "library.db");
|
|
|
|
const WIPE_ORDER = [
|
|
"actress_categories_map",
|
|
"collection_images",
|
|
"image_tags",
|
|
"image_genres",
|
|
"image_actresses",
|
|
"tags",
|
|
"tag_categories",
|
|
"genres",
|
|
"actresses",
|
|
"actress_categories",
|
|
"collections",
|
|
"series",
|
|
"labels",
|
|
"studios",
|
|
"images",
|
|
"app_settings",
|
|
];
|
|
|
|
const INSERT_ORDER = [
|
|
"studios",
|
|
"labels",
|
|
"series",
|
|
"actresses",
|
|
"genres",
|
|
"tag_categories",
|
|
"tags",
|
|
"actress_categories",
|
|
"images",
|
|
"collections",
|
|
"image_actresses",
|
|
"image_genres",
|
|
"image_tags",
|
|
"collection_images",
|
|
"actress_categories_map",
|
|
"app_settings",
|
|
];
|
|
|
|
function escIdent(s: string): string {
|
|
return `"${s.replace(/"/g, '""')}"`;
|
|
}
|
|
|
|
function tableColumns(schema: "main" | "restore", table: string): string[] {
|
|
try {
|
|
const rows = rawDb.prepare(`PRAGMA ${schema}.table_info(${escIdent(table)})`).all() as Array<{ name: string }>;
|
|
return rows.map((r) => r.name);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export type ImportDbResult = {
|
|
ok: boolean;
|
|
counts: Record<string, number>;
|
|
errors: Array<{ table: string; message: string }>;
|
|
snapshotPath: string | null;
|
|
error?: string;
|
|
};
|
|
|
|
export async function importDatabaseTables(
|
|
tables: Record<string, unknown[]>,
|
|
): Promise<ImportDbResult> {
|
|
const counts: Record<string, number> = {};
|
|
const errors: Array<{ table: string; message: string }> = [];
|
|
|
|
let snapshotPath: string | null = null;
|
|
try {
|
|
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
snapshotPath = `${DB_PATH}.${ts}.bak`;
|
|
await rawDb.backup(snapshotPath);
|
|
} catch (e) {
|
|
return {
|
|
ok: false,
|
|
counts,
|
|
errors,
|
|
snapshotPath: null,
|
|
error: `Failed to snapshot DB before import: ${(e as Error).message}`,
|
|
};
|
|
}
|
|
|
|
rawDb.pragma("foreign_keys = OFF");
|
|
try {
|
|
const tx = rawDb.transaction(() => {
|
|
for (const t of WIPE_ORDER) {
|
|
try {
|
|
rawDb.prepare(`DELETE FROM ${escIdent(t)}`).run();
|
|
rawDb.prepare(`DELETE FROM sqlite_sequence WHERE name = ?`).run(t);
|
|
} catch (e) {
|
|
const msg = (e as Error).message ?? "";
|
|
if (!/no such table/i.test(msg)) {
|
|
throw new Error(`Wipe failed on ${t}: ${msg}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const t of INSERT_ORDER) {
|
|
const rows = tables[t];
|
|
if (!Array.isArray(rows) || rows.length === 0) {
|
|
counts[t] = 0;
|
|
continue;
|
|
}
|
|
const sample = rows[0] as Record<string, unknown>;
|
|
const cols = Object.keys(sample);
|
|
if (cols.length === 0) {
|
|
counts[t] = 0;
|
|
continue;
|
|
}
|
|
const colList = cols.map(escIdent).join(",");
|
|
const placeholders = cols.map(() => "?").join(",");
|
|
const stmt = rawDb.prepare(
|
|
`INSERT INTO ${escIdent(t)} (${colList}) VALUES (${placeholders})`,
|
|
);
|
|
let inserted = 0;
|
|
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
const r = rows[rowIndex];
|
|
if (!r || typeof r !== "object") continue;
|
|
const row = r as Record<string, unknown>;
|
|
const values = cols.map((c) => {
|
|
const v = row[c];
|
|
if (v === undefined) return null;
|
|
if (typeof v === "boolean") return v ? 1 : 0;
|
|
return v as null | string | number | bigint | Buffer;
|
|
});
|
|
try {
|
|
stmt.run(...values);
|
|
inserted++;
|
|
} catch (e) {
|
|
const message = `Insert failed on ${t} row ${rowIndex + 1}: ${(e as Error).message}`;
|
|
errors.push({ table: t, message });
|
|
throw new Error(message);
|
|
}
|
|
}
|
|
counts[t] = inserted;
|
|
}
|
|
});
|
|
tx();
|
|
} catch (e) {
|
|
rawDb.pragma("foreign_keys = ON");
|
|
return {
|
|
ok: false,
|
|
counts,
|
|
errors,
|
|
snapshotPath,
|
|
error: (e as Error).message,
|
|
};
|
|
}
|
|
rawDb.pragma("foreign_keys = ON");
|
|
clearAppSettingsCache();
|
|
|
|
return { ok: true, counts, errors, snapshotPath };
|
|
}
|
|
|
|
export async function restoreDatabaseSnapshot(snapshotPath: string): Promise<void> {
|
|
rawDb.pragma("foreign_keys = OFF");
|
|
try {
|
|
rawDb.prepare("ATTACH DATABASE ? AS restore").run(snapshotPath);
|
|
const tx = rawDb.transaction(() => {
|
|
for (const t of WIPE_ORDER) {
|
|
try {
|
|
rawDb.prepare(`DELETE FROM ${escIdent(t)}`).run();
|
|
rawDb.prepare(`DELETE FROM sqlite_sequence WHERE name = ?`).run(t);
|
|
} catch (e) {
|
|
const msg = (e as Error).message ?? "";
|
|
if (!/no such table/i.test(msg)) throw e;
|
|
}
|
|
}
|
|
|
|
for (const t of INSERT_ORDER) {
|
|
const mainCols = tableColumns("main", t);
|
|
const restoreCols = new Set(tableColumns("restore", t));
|
|
const cols = mainCols.filter((c) => restoreCols.has(c));
|
|
if (cols.length === 0) continue;
|
|
const colList = cols.map(escIdent).join(",");
|
|
rawDb.prepare(`
|
|
INSERT INTO ${escIdent(t)} (${colList})
|
|
SELECT ${colList} FROM restore.${escIdent(t)}
|
|
`).run();
|
|
}
|
|
});
|
|
tx();
|
|
} finally {
|
|
try {
|
|
rawDb.prepare("DETACH DATABASE restore").run();
|
|
} catch {}
|
|
rawDb.pragma("foreign_keys = ON");
|
|
clearAppSettingsCache();
|
|
}
|
|
}
|