import { useState, useEffect, useCallback, useRef } from "react"; import { Download, FolderOpen, HardDrive } from "lucide-react"; import { save } from "@tauri-apps/plugin-dialog"; import { Button } from "../ui/Button"; import { BackupProgress } from "./BackupProgress"; import { useBackupStore } from "../../stores/backupStore"; import { useNotificationStore } from "../../stores/notificationStore"; import { useConnectionStore } from "../../stores/connectionStore"; import { detectPgTools, pgDump, getSchemas, detectMysqlTools, mysqlDump, sqliteDump, } from "../../lib/commands"; import type { PgToolStatus, MySqlToolStatus, BackupJob } from "../../lib/types"; interface BackupPageProps { connectionId: string; } type BackupFormat = "plain" | "custom" | "tar" | "directory"; 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_dump 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 mysqldump 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 BackupPage({ connectionId }: BackupPageProps) { 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 [format, setFormat] = useState("custom"); const [filePath, setFilePath] = useState(""); const [schema, setSchema] = useState(""); const [noOwner, setNoOwner] = useState(true); const [singleTransaction, setSingleTransaction] = useState(true); const [noData, setNoData] = useState(false); const [routines, setRoutines] = useState(true); const [triggers, setTriggers] = useState(true); const [events, setEvents] = useState(false); const [pgToolStatus, setPgToolStatus] = useState(null); const [mysqlToolStatus, setMysqlToolStatus] = 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"; // Track our job ID so we only react to jobs we started const pendingJobRef = useRef(null); // React to job completion/failure via store events useEffect(() => { if (!pendingJobRef.current || !activeJob) return; if (activeJob.id !== pendingJobRef.current) return; if (activeJob.status === "completed") { notify("Backup completed successfully", "success"); pendingJobRef.current = null; } else if (activeJob.status === "failed") { notify( `Backup failed: ${activeJob.error_message || "Unknown error"}`, "error", ); pendingJobRef.current = null; } }, [activeJob, notify]); useEffect(() => { setCheckingTools(true); setPgToolStatus(null); setMysqlToolStatus(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) { detectMysqlTools() .then((status) => setMysqlToolStatus(status)) .catch(() => setMysqlToolStatus({ mysqldumpFound: false, mysqlFound: false, mysqldumpVersion: null, mysqlVersion: null, mysqldumpSource: null, mysqlSource: null, }), ) .finally(() => setCheckingTools(false)); getSchemas(connectionId) .then((schemas) => setAvailableSchemas(schemas)) .catch(() => setAvailableSchemas([])); } else { setCheckingTools(false); } }, [connectionId, isPg, isMysql]); const handlePickFile = useCallback(async () => { let defaultPath = "backup"; let extensions: string[] = []; if (isPg) { const pgExtensions: Record = { plain: ["sql"], custom: ["dump", "custom"], tar: ["tar"], directory: [], }; extensions = pgExtensions[format]; defaultPath = `backup.${ format === "custom" ? "dump" : format === "plain" ? "sql" : "tar" }`; } else if (isMysql) { extensions = ["sql"]; defaultPath = "backup.sql"; } else { extensions = ["db", "sqlite", "sql"]; defaultPath = "backup.db"; } const picked = await save({ defaultPath, filters: [{ name: "Backup", extensions }], }); if (picked) setFilePath(picked); }, [format, isPg, isMysql, isSqlite]); const runWithProgress = useCallback( async (type: BackupJob["type"], action: () => Promise) => { const jobId = `${type}-${Date.now()}`; startJob(jobId, type); pendingJobRef.current = jobId; try { // Command returns the job ID immediately — completion // comes via Tauri events handled by the backupStore await action(); } catch (e) { // If the command itself fails (e.g. connection not found), // the event won't fire — handle here const msg = e instanceof Error ? e.message : String(e); useBackupStore.getState().failJob(jobId, msg); } }, [startJob], ); const handleStartBackup = 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("dump", () => { if (isPg) { return pgDump(connectionId, { format, filePath, schema: schema || undefined, tables: undefined, noOwner, }); } if (isMysql) { return mysqlDump(connectionId, { database: database!, filePath, singleTransaction, noData, routines, triggers, events, }); } return sqliteDump(connectionId, { filePath }); }); }, [ filePath, database, isPg, isMysql, isSqlite, format, schema, noOwner, singleTransaction, noData, routines, triggers, events, connectionId, notify, runWithProgress, ]); const toolsMissing = isPg ? pgToolStatus && !pgToolStatus.pg_dump_found : isMysql ? mysqlToolStatus && !mysqlToolStatus.mysqldumpFound : false; const toolsBundled = isPg ? pgToolStatus?.pg_dump_source === "bundled" : isMysql ? mysqlToolStatus?.mysqldumpSource === "bundled" : false; const checkingMessage = isPg ? "Checking for pg_dump..." : isMysql ? "Checking for mysqldump..." : null; const headerDescription = isPg ? "Create a database backup via pg_dump" : isMysql ? "Create a database backup via mysqldump" : "Create a database backup"; return (
{/* Toolbar header */}
Backup {headerDescription}
{/* Content */}
{/* Tool check */} {checkingTools && checkingMessage && (

{checkingMessage}

)} {toolsMissing && !toolsBundled && (

{isPg ? "pg_dump not found" : "mysqldump not found"}

The {isPg ? "PostgreSQL" : "MySQL"} client tools are required for backup/restore operations. Install them using:

                                {getPlatformInstructions(
                                    isPg
                                        ? PG_INSTALL_INSTRUCTIONS
                                        : MYSQL_INSTALL_INSTRUCTIONS,
                                )}
                            
)} {!checkingTools && !toolsMissing && ( <> {/* Configuration card */}
{/* Format */} {isPg ? (
) : isMysql ? (
Plain SQL
) : null} {/* Output file */}
setFilePath(e.target.value) } placeholder="/path/to/backup.dump" className="flex-1 px-4 py-2 text-sm text-text placeholder-text-muted/50 border-b border-border focus:border-accent focus:outline-none transition-colors" />
{/* Schema (optional) */} {(isPg || isMysql) && (
)} {/* PostgreSQL: no-owner toggle */} {isPg && ( )} {/* MySQL: option toggles */} {isMysql && (
)}
{/* Progress */} {activeJob && (
)} {/* Actions */}
)}
); }