From 3c5a005165d288920608f69d9e9c634381c51cff Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 16 Mar 2025 18:53:20 -0600 Subject: [PATCH 1/3] feat(backup): implement restore backup functionality - Added a new component `RestoreBackup` for restoring database backups. - Integrated the restore functionality with a form to select destination, backup file, and database name. - Implemented API endpoints for listing backup files and restoring backups with logs. - Enhanced the `ShowBackups` component to include the `RestoreBackup` option alongside existing backup features. --- .../database/backups/restore-backup.tsx | 367 ++++++++++++++++++ .../database/backups/show-backups.tsx | 40 +- .../dashboard/docker/logs/docker-logs-id.tsx | 2 - apps/dokploy/server/api/routers/backup.ts | 170 ++++++-- packages/server/src/utils/restore/index.ts | 4 + packages/server/src/utils/restore/mariadb.ts | 56 +++ packages/server/src/utils/restore/mongo.ts | 64 +++ packages/server/src/utils/restore/mysql.ts | 54 +++ packages/server/src/utils/restore/postgres.ts | 60 +++ 9 files changed, 783 insertions(+), 34 deletions(-) create mode 100644 apps/dokploy/components/dashboard/database/backups/restore-backup.tsx create mode 100644 packages/server/src/utils/restore/index.ts create mode 100644 packages/server/src/utils/restore/mariadb.ts create mode 100644 packages/server/src/utils/restore/mongo.ts create mode 100644 packages/server/src/utils/restore/mysql.ts create mode 100644 packages/server/src/utils/restore/postgres.ts diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx new file mode 100644 index 00000000..c761fc70 --- /dev/null +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -0,0 +1,367 @@ +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 { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { cn } from "@/lib/utils"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import type { ServiceType } from "../../application/advanced/show-resources"; +import { debounce } from "lodash"; +import { Input } from "@/components/ui/input"; +import { type LogLine, parseLogs } from "../../docker/logs/utils"; +import { DrawerLogs } from "@/components/shared/drawer-logs"; +import { Badge } from "@/components/ui/badge"; +import copy from "copy-to-clipboard"; +import { toast } from "sonner"; + +interface Props { + databaseId: string; + databaseType: Exclude; +} + +const RestoreBackupSchema = z.object({ + destinationId: z + .string({ + required_error: "Please select a destination", + }) + .min(1, { + message: "Destination is required", + }), + backupFile: z + .string({ + required_error: "Please select a backup file", + }) + .min(1, { + message: "Backup file is required", + }), + databaseName: z + .string({ + required_error: "Please enter a database name", + }) + .min(1, { + message: "Database name is required", + }), +}); + +type RestoreBackup = z.infer; + +export const RestoreBackup = ({ databaseId, databaseType }: Props) => { + const [isOpen, setIsOpen] = useState(false); + const [search, setSearch] = useState(""); + + const { data: destinations = [] } = api.destination.all.useQuery(); + + const form = useForm({ + defaultValues: { + destinationId: "", + backupFile: "", + databaseName: "", + }, + resolver: zodResolver(RestoreBackupSchema), + }); + + const destionationId = form.watch("destinationId"); + + const debouncedSetSearch = debounce((value: string) => { + setSearch(value); + }, 300); + + const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery( + { + destinationId: destionationId, + search, + }, + { + enabled: isOpen && !!destionationId, + }, + ); + + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [filteredLogs, setFilteredLogs] = useState([]); + const [isDeploying, setIsDeploying] = useState(false); + + // const { mutateAsync: restore, isLoading: isRestoring } = + // api.backup.restoreBackup.useMutation(); + + api.backup.restoreBackupWithLogs.useSubscription( + { + databaseId, + databaseType, + databaseName: form.watch("databaseName"), + backupFile: form.watch("backupFile"), + destinationId: form.watch("destinationId"), + }, + { + 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: RestoreBackup) => { + setIsDeploying(true); + }; + + 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"); + }} + /> + + )} + + + + + + + + + + + {isLoading ? ( +
+ 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); + }} + > + {file} + + + ))} + + + )} +
+
+
+ +
+ )} + /> + ( + + Database Name + + + + + + )} + /> + + + + + + + { + setIsDrawerOpen(false); + setFilteredLogs([]); + setIsDeploying(false); + // refetch(); + }} + filteredLogs={filteredLogs} + /> +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index b4606583..52a5748f 100644 --- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -20,6 +20,7 @@ import { toast } from "sonner"; import type { ServiceType } from "../../application/advanced/show-resources"; import { AddBackup } from "./add-backup"; import { UpdateBackup } from "./update-backup"; +import { RestoreBackup } from "./restore-backup"; import { useState } from "react"; interface Props { @@ -27,7 +28,9 @@ interface Props { type: Exclude; } export const ShowBackups = ({ id, type }: Props) => { - const [activeManualBackup, setActiveManualBackup] = useState(); + const [activeManualBackup, setActiveManualBackup] = useState< + string | undefined + >(); const queryMap = { postgres: () => api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), @@ -69,7 +72,10 @@ export const ShowBackups = ({ id, type }: Props) => { {postgres && postgres?.backups?.length > 0 && ( - +
+ + +
)} @@ -96,11 +102,14 @@ export const ShowBackups = ({ id, type }: Props) => { No backups configured - +
+ + +
) : (
@@ -142,7 +151,7 @@ export const ShowBackups = ({ id, type }: Props) => {
Keep Latest - {backup.keepLatestCount || 'All'} + {backup.keepLatestCount || "All"}
@@ -153,7 +162,10 @@ export const ShowBackups = ({ id, type }: Props) => {