import { useState, useEffect, useCallback, useRef } from "react"; import { FolderOpen, Upload } from "lucide-react"; import { open } from "@tauri-apps/plugin-dialog"; import { CopyButton } from "../ui/CopyButton"; import { BackupProgress } from "./BackupProgress"; import { FormRow, FormSectionHeader, inputClass, controlClass, } from "./objects/formRow"; import { useBackupStore } from "../../stores/backupStore"; import { useNotificationStore } from "../../stores/notificationStore"; import { useConnectionStore } from "../../stores/connectionStore"; import { detectPgTools, pgRestore, getSchemas, mysqlRestore, sqliteRestore, } from "../../lib/commands"; import type { PgToolStatus, BackupJob } from "../../lib/types"; interface RestorePageProps { connectionId: string; } const PG_INSTALL_INSTRUCTIONS: Record = { darwin: "brew install libpq", linux: "sudo apt install postgresql-client # Debian/Ubuntu\nsudo dnf install postgresql # Fedora\nsudo pacman -S postgresql # Arch", win32: "Download PostgreSQL installer from https://www.postgresql.org/download/windows/ and ensure pg_restore is in your PATH.", }; const MYSQL_INSTALL_INSTRUCTIONS: Record = { darwin: "brew install mysql-client", linux: "sudo apt install mysql-client # Debian/Ubuntu\nsudo dnf install mysql # Fedora\nsudo pacman -S mariadb # Arch", win32: "Download MySQL installer from https://dev.mysql.com/downloads/installer/ and ensure mysql is in your PATH.", }; function getPlatformInstructions(map: Record): string { const platform = typeof navigator !== "undefined" ? navigator.platform.toLowerCase() : ""; if (platform.includes("mac") || platform.includes("darwin")) return map.darwin; if (platform.includes("linux")) return map.linux; if (platform.includes("win")) return map.win32; return map.linux; } export function RestorePage({ connectionId }: RestorePageProps) { const connection = useConnectionStore((s) => s.connections.find((c) => c.id === connectionId), ); const dbType = connection?.db_type ?? "postgresql"; const database = connection?.database ?? null; const isPg = dbType === "postgresql"; const isMysql = dbType === "mysql"; const isSqlite = dbType === "sqlite"; const [filePath, setFilePath] = useState(""); const [format, setFormat] = useState("custom"); const [clean, setClean] = useState(true); const [schema, setSchema] = useState(""); const [confirmed, setConfirmed] = useState(false); const [pgToolStatus, setPgToolStatus] = useState(null); const [checkingTools, setCheckingTools] = useState(true); const [availableSchemas, setAvailableSchemas] = useState([]); const activeJobId = useBackupStore((s) => s.activeJobId); const jobs = useBackupStore((s) => s.jobs); const startJob = useBackupStore((s) => s.startJob); const notify = useNotificationStore((s) => s.notify); const activeJob = jobs.find((j) => j.id === activeJobId); const isRunning = activeJob?.status === "running"; const pendingJobRef = useRef(null); useEffect(() => { if (!pendingJobRef.current || !activeJob) return; if (activeJob.id !== pendingJobRef.current) return; if (activeJob.status === "completed") { notify("Restore completed successfully", "success"); pendingJobRef.current = null; } else if (activeJob.status === "failed") { notify( `Restore failed: ${activeJob.error_message || "Unknown error"}`, "error", ); pendingJobRef.current = null; } }, [activeJob, notify]); useEffect(() => { setCheckingTools(true); setConfirmed(false); setPgToolStatus(null); setAvailableSchemas([]); if (isPg) { detectPgTools() .then((status) => setPgToolStatus(status)) .catch(() => setPgToolStatus({ pg_dump_found: false, pg_restore_found: false, pg_dump_version: null, pg_restore_version: null, pg_dump_source: null, pg_restore_source: null, }), ) .finally(() => setCheckingTools(false)); getSchemas(connectionId) .then((schemas) => setAvailableSchemas(schemas)) .catch(() => setAvailableSchemas([])); } else if (isMysql) { // MySQL restore runs in-process over the sqlx driver connection, so // there is no client to probe for or wait on here (Backup/Sync // still probe via detectMysqlTools for mysqldump). setCheckingTools(false); getSchemas(connectionId) .then((schemas) => setAvailableSchemas(schemas)) .catch(() => setAvailableSchemas([])); } else { setCheckingTools(false); } }, [connectionId, isPg, isMysql]); const handlePickFile = useCallback(async () => { const extensions = isPg ? ["dump", "sql", "tar", "custom", "gz"] : isMysql ? ["sql"] : ["db", "sqlite", "sql"]; const picked = await open({ multiple: false, filters: [ { name: "Backup Files", extensions, }, ], }); if (picked && typeof picked === "string") setFilePath(picked); }, [isPg, isMysql, isSqlite]); const runWithProgress = useCallback( async (type: BackupJob["type"], action: () => Promise) => { const jobId = `${type}-${Date.now()}`; startJob(jobId, type); pendingJobRef.current = jobId; try { await action(); } catch (e) { const msg = e instanceof Error ? e.message : String(e); useBackupStore.getState().failJob(jobId, msg); } }, [startJob], ); const handleStartRestore = useCallback(async () => { if (!filePath) { notify("Please select a file path", "error"); return; } if (isMysql && !database) { notify("MySQL connection has no database selected", "error"); return; } await runWithProgress("restore", () => { if (isPg) { return pgRestore(connectionId, { format, filePath, clean, schema: schema || undefined, }); } if (isMysql) { return mysqlRestore(connectionId, { database: database!, filePath, clean, }); } return sqliteRestore(connectionId, { filePath, clean }); }); }, [ filePath, database, isPg, isMysql, isSqlite, format, clean, schema, connectionId, notify, runWithProgress, ]); // pg_restore is still required for PostgreSQL. MySQL restore runs in-process // via the sqlx driver, so a missing mariadb/mysql client must not gate it — // that client could never authenticate to MySQL 8 anyway (#41 follow-up). const toolsMissing = isPg ? pgToolStatus && !pgToolStatus.pg_restore_found : false; const toolsBundled = isPg ? pgToolStatus?.pg_restore_source === "bundled" : false; const canStart = filePath && confirmed && !isRunning; const installInstructions = getPlatformInstructions( isPg ? PG_INSTALL_INSTRUCTIONS : MYSQL_INSTALL_INSTRUCTIONS, ); const checkingMessage = isPg ? "Checking for pg_restore..." : isMysql ? "Checking for mysql..." : null; const headerDescription = isPg ? "Restore a database from a backup file" : isMysql ? "Restore a database from a SQL dump" : "Restore a database from a backup file"; const cleanDisabled = isPg && format === "plain"; const restoreToolResolvedName = pgToolStatus?.pg_restore_resolved_name ?? "pg_restore"; return (
{/* Toolbar header */}
Restore {headerDescription}
{!checkingTools && !toolsMissing && ( )}
{/* Content */}
{/* Tool check */} {checkingTools && checkingMessage && (
{checkingMessage}
)} {toolsMissing && !toolsBundled && (

{isPg ? "pg_restore not found" : "mysql client not found"}

Gridline ships its own {isPg ? "PostgreSQL" : "MySQL"}{" "} client tools. This build could not locate or run them, so a manual install is needed:

{pgToolStatus?.pg_restore_bundled_available && (

A bundled copy is present but could not run — reinstall Gridline, or install the tools below.

)}
Install
                                {installInstructions}
                            
)} {!checkingTools && !toolsMissing && ( <> {/* Restore tool */} {isPg && pgToolStatus?.pg_restore_found && ( Using{" "} {pgToolStatus.pg_restore_source === "bundled" ? "bundled" : "system"}{" "} {restoreToolResolvedName} )} {isMysql && ( Restores run in-process over the driver connection — no MySQL client installation required )} {/* Format (PostgreSQL only) */} {isPg && ( )} {/* Backup file */} setFilePath(e.target.value)} placeholder="/path/to/backup.dump" className={inputClass} /> {/* Schema (optional) */} {(isPg || isMysql) && ( )} {/* Clean toggle */} {isPg && format === "plain" && (
Plain SQL restores run via psql and don't support DROP-before-CREATE. Use Custom Archive for clean restores.
)} {/* Destructive confirmation */}
{isMysql && (

MySQL DDL commits as it runs and can't be rolled back — a failed restore may leave this database partially applied.

)}
{/* Progress */} {activeJob && (
)} )}
); }