import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; import copy from "copy-to-clipboard"; import debounce from "lodash/debounce"; import { CheckIcon, ChevronsUpDown, Copy, DatabaseZap, RefreshCw, RotateCcw, } from "lucide-react"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import { DrawerLogs } from "@/components/shared/drawer-logs"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, } from "@/components/ui/command"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import type { ServiceType } from "../../application/advanced/show-resources"; import { type LogLine, parseLogs } from "../../docker/logs/utils"; type DatabaseType = | Exclude | "web-server"; interface Props { id: string; databaseType?: DatabaseType; serverId?: string | null; backupType?: "database" | "compose"; } const RestoreBackupSchema = z .object({ destinationId: z.string().min(1, { message: "Destination is required", }), backupFile: z.string().min(1, { message: "Backup file is required", }), databaseName: z.string().min(1, { message: "Database name is required", }), databaseType: z .enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"]) .optional(), backupType: z.enum(["database", "compose"]).default("database"), metadata: z .object({ postgres: z .object({ databaseUser: z.string(), }) .optional(), mariadb: z .object({ databaseUser: z.string(), databasePassword: z.string(), }) .optional(), mongo: z .object({ databaseUser: z.string(), databasePassword: z.string(), }) .optional(), mysql: z .object({ databaseRootPassword: z.string(), }) .optional(), serviceName: z.string().optional(), }) .optional(), }) .superRefine((data, ctx) => { if (data.backupType === "compose" && !data.databaseType) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Database type is required for compose backups", path: ["databaseType"], }); } if (data.backupType === "compose" && !data.metadata?.serviceName) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Service name is required for compose backups", path: ["metadata", "serviceName"], }); } if (data.backupType === "compose" && data.databaseType) { if (data.databaseType === "postgres") { if (!data.metadata?.postgres?.databaseUser) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Database user is required for PostgreSQL", path: ["metadata", "postgres", "databaseUser"], }); } } else if (data.databaseType === "mariadb") { if (!data.metadata?.mariadb?.databaseUser) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Database user is required for MariaDB", path: ["metadata", "mariadb", "databaseUser"], }); } if (!data.metadata?.mariadb?.databasePassword) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Database password is required for MariaDB", path: ["metadata", "mariadb", "databasePassword"], }); } } else if (data.databaseType === "mongo") { if (!data.metadata?.mongo?.databaseUser) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Database user is required for MongoDB", path: ["metadata", "mongo", "databaseUser"], }); } if (!data.metadata?.mongo?.databasePassword) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Database password is required for MongoDB", path: ["metadata", "mongo", "databasePassword"], }); } } else if (data.databaseType === "mysql") { if (!data.metadata?.mysql?.databaseRootPassword) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Root password is required for MySQL", path: ["metadata", "mysql", "databaseRootPassword"], }); } } } }); export const formatBytes = (bytes: number): string => { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`; }; export const RestoreBackup = ({ id, databaseType, serverId, backupType = "database", }: Props) => { const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); const { data: destinations = [] } = api.destination.all.useQuery(); const form = useForm({ defaultValues: { destinationId: "", backupFile: "", databaseName: databaseType === "web-server" ? "dokploy" : databaseType === "libsql" ? "iku.db" : "", databaseType: backupType === "compose" ? ("postgres" as DatabaseType) : databaseType, backupType: backupType, metadata: {}, }, resolver: zodResolver(RestoreBackupSchema), }); const destinationId = form.watch("destinationId"); const currentDatabaseType = form.watch("databaseType"); const metadata = form.watch("metadata"); const debouncedSetSearch = debounce((value: string) => { setDebouncedSearchTerm(value); }, 350); const handleSearchChange = (value: string) => { setSearch(value); debouncedSetSearch(value); }; const { data: files = [], isPending } = api.backup.listBackupFiles.useQuery( { destinationId: destinationId, search: debouncedSearchTerm, serverId: serverId ?? "", }, { enabled: isOpen && !!destinationId, }, ); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [filteredLogs, setFilteredLogs] = useState([]); const [isDeploying, setIsDeploying] = useState(false); api.backup.restoreBackupWithLogs.useSubscription( { databaseId: id, databaseType: currentDatabaseType as DatabaseType, databaseName: form.watch("databaseName"), backupFile: form.watch("backupFile"), destinationId: form.watch("destinationId"), backupType: backupType, metadata: metadata, }, { enabled: isDeploying, onData(log) { if (!isDrawerOpen) { setIsDrawerOpen(true); } if (log === "Restore completed successfully!") { setIsDeploying(false); } const parsedLogs = parseLogs(log); setFilteredLogs((prev) => [...prev, ...parsedLogs]); }, onError(error) { console.error("Restore logs error:", error); setIsDeploying(false); }, }, ); const onSubmit = async (data: z.infer) => { if (backupType === "compose" && !data.databaseType) { toast.error("Please select a database type"); return; } console.log({ data }); setIsDeploying(true); }; const [cacheType, setCacheType] = useState<"fetch" | "cache">("cache"); const { data: services = [], isLoading: isLoadingServices, refetch: refetchServices, } = api.compose.loadServices.useQuery( { composeId: id, type: cacheType, }, { retry: false, refetchOnWindowFocus: false, enabled: backupType === "compose", }, ); return ( Restore Backup Select a destination and search for backup files
( Destination No destinations found. {destinations.map((destination) => ( { form.setValue( "destinationId", destination.destinationId, ); }} > {destination.name} ))} )} /> ( Search Backup Files {field.value && ( {field.value} { e.stopPropagation(); e.preventDefault(); copy(field.value); toast.success("Backup file copied to clipboard"); }} /> )} {isPending ? (
Loading backup files...
) : files.length === 0 && search ? (
No backup files found for "{search}"
) : files.length === 0 ? (
No backup files available
) : ( {files?.map((file) => ( { form.setValue("backupFile", file.Path); if (file.IsDir) { setSearch(`${file.Path}/`); setDebouncedSearchTerm(`${file.Path}/`); } else { setSearch(file.Path); setDebouncedSearchTerm(file.Path); } }} >
{file.Path}
Size: {formatBytes(file.Size)} {file.IsDir && ( Directory )} {file.Hashes?.MD5 && ( MD5: {file.Hashes.MD5} )}
))}
)}
)} /> ( Database Name )} /> {backupType === "compose" && ( <> ( Database Type )} /> ( Service Name

Fetch: Will clone the repository and load the services

Cache: If you previously deployed this compose, it will read the services from the last deployment/fetch from the repository

)} /> {currentDatabaseType === "postgres" && ( ( Database User )} /> )} {currentDatabaseType === "mariadb" && ( <> ( Database User )} /> ( Database Password )} /> )} {currentDatabaseType === "mongo" && ( <> ( Database User )} /> ( Database Password )} /> )} {currentDatabaseType === "mysql" && ( ( Root Password )} /> )} )} { setIsDrawerOpen(false); setFilteredLogs([]); setIsDeploying(false); // refetch(); }} filteredLogs={filteredLogs} />
); };