Initial commit
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user