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; errors: Array<{ table: string; message: string }>; snapshotPath: string | null; error?: string; }; export async function importDatabaseTables( tables: Record, ): Promise { const counts: Record = {}; 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; 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; 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 { 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(); } }