import { getCurrentUser } from "@/lib/auth"; import { NextRequest, NextResponse } from "next/server"; import { writeFile, copyFile, unlink } from "fs/promises"; import { existsSync } from "fs"; import path from "path"; import { execSync } from "child_process"; /** * POST /api/settings/import-db * Upload a SQLite database file to replace the current one. * Creates a backup of the existing DB before replacing. * Validates the uploaded file is a valid SQLite database with the expected tables. */ export async function POST(request: NextRequest) { try { const user = await getCurrentUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const formData = await request.formData(); const file = formData.get("database") as File | null; if (!file) { return NextResponse.json( { error: "No database file provided" }, { status: 400 } ); } // Basic size check (SQLite DBs for this app should be under 100MB) if (file.size > 100 * 1024 * 1024) { return NextResponse.json( { error: "File too large (max 100MB)" }, { status: 400 } ); } // Read the uploaded file into a buffer const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); // Check SQLite magic bytes (first 16 bytes should start with "SQLite format 3\0") const magic = buffer.slice(0, 16).toString("ascii"); if (!magic.startsWith("SQLite format 3")) { return NextResponse.json( { error: "Invalid file — not a SQLite database" }, { status: 400 } ); } // Determine the database file path from DATABASE_URL const dbUrl = process.env.DATABASE_URL || "file:./data/app.db"; let dbPath: string; if (dbUrl.startsWith("file:")) { dbPath = dbUrl.replace("file:", ""); // Handle relative paths if (!path.isAbsolute(dbPath)) { dbPath = path.resolve(process.cwd(), "prisma", dbPath.replace("./", "")); } } else { dbPath = path.resolve(process.cwd(), "prisma", "data", "app.db"); } // Write uploaded file to a temp location for validation const tempPath = dbPath + ".upload-temp"; await writeFile(tempPath, buffer); // Validate the uploaded DB has the expected tables try { const tables = execSync( `sqlite3 "${tempPath}" "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"`, { encoding: "utf-8", timeout: 10000 } ).trim(); const tableList = tables.split("\n").map((t) => t.trim()); const requiredTables = ["User", "Exercise", "Workout", "SetLog"]; const missingTables = requiredTables.filter( (t) => !tableList.includes(t) ); if (missingTables.length > 0) { await unlink(tempPath); return NextResponse.json( { error: `Invalid database — missing tables: ${missingTables.join(", ")}. This doesn't look like a Workout Planner database.`, }, { status: 400 } ); } // Run integrity check const integrity = execSync( `sqlite3 "${tempPath}" "PRAGMA integrity_check;"`, { encoding: "utf-8", timeout: 10000 } ).trim(); if (integrity !== "ok") { await unlink(tempPath); return NextResponse.json( { error: "Database integrity check failed — file may be corrupted" }, { status: 400 } ); } } catch (err) { // Clean up temp file if (existsSync(tempPath)) await unlink(tempPath); return NextResponse.json( { error: "Could not validate the uploaded database file" }, { status: 400 } ); } // Get some stats from the uploaded DB for the response let stats = { users: 0, exercises: 0, workouts: 0 }; try { const userCount = execSync( `sqlite3 "${tempPath}" "SELECT COUNT(*) FROM User;"`, { encoding: "utf-8" } ).trim(); const exerciseCount = execSync( `sqlite3 "${tempPath}" "SELECT COUNT(*) FROM Exercise;"`, { encoding: "utf-8" } ).trim(); const workoutCount = execSync( `sqlite3 "${tempPath}" "SELECT COUNT(*) FROM Workout;"`, { encoding: "utf-8" } ).trim(); stats = { users: parseInt(userCount) || 0, exercises: parseInt(exerciseCount) || 0, workouts: parseInt(workoutCount) || 0, }; } catch { // Stats are optional, continue anyway } // Back up the current database const backupPath = dbPath + ".backup-" + Date.now(); if (existsSync(dbPath)) { await copyFile(dbPath, backupPath); } // Replace the current database with the uploaded one await copyFile(tempPath, dbPath); // Also remove WAL/SHM files if they exist (SQLite journal files) for (const ext of ["-wal", "-shm", "-journal"]) { const journalPath = dbPath + ext; if (existsSync(journalPath)) { await unlink(journalPath); } } // Clean up temp file await unlink(tempPath); return NextResponse.json({ success: true, message: "Database imported successfully. Please refresh the page.", stats, backup: path.basename(backupPath), }); } catch (error) { console.error("Database import error:", error); return NextResponse.json( { error: error instanceof Error ? error.message : "An error occurred during import", }, { status: 500 } ); } }