diff --git a/apps/dokploy/components/dashboard/database/backups/add-backup.tsx b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx similarity index 71% rename from apps/dokploy/components/dashboard/database/backups/add-backup.tsx rename to apps/dokploy/components/dashboard/database/backups/handle-backup.tsx index 1b3828e6..2d8a6b6d 100644 --- a/apps/dokploy/components/dashboard/database/backups/add-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx @@ -49,7 +49,7 @@ import { import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { DatabaseZap, PlusIcon, RefreshCw } from "lucide-react"; +import { DatabaseZap, PenBoxIcon, PlusIcon, RefreshCw } from "lucide-react"; import { CheckIcon, ChevronsUpDown } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -58,75 +58,140 @@ import { z } from "zod"; type CacheType = "cache" | "fetch"; -const getMetadataSchema = ( - backupType: "database" | "compose", - databaseType: DatabaseType, -) => { - if (backupType !== "compose") return z.object({}).optional(); +type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server"; - const schemas = { - postgres: z.object({ - databaseUser: z.string().min(1, "Database user is required"), - }), - mariadb: z.object({ - databaseUser: z.string().min(1, "Database user is required"), - databasePassword: z.string().min(1, "Database password is required"), - }), - mongo: z.object({ - databaseUser: z.string().min(1, "Database user is required"), - databasePassword: z.string().min(1, "Database password is required"), - }), - mysql: z.object({ - databaseRootPassword: z.string().min(1, "Root password is required"), - }), - "web-server": z.object({}), - }; - - return z.object({ - [databaseType]: schemas[databaseType], +const Schema = z + .object({ + destinationId: z.string().min(1, "Destination required"), + schedule: z.string().min(1, "Schedule (Cron) required"), + prefix: z.string().min(1, "Prefix required"), + enabled: z.boolean(), + database: z.string().min(1, "Database required"), + keepLatestCount: z.coerce.number().optional(), + serviceName: z.string().nullable(), + databaseType: z + .enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]) + .optional(), + backupType: z.enum(["database", "compose"]), + 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(), + }) + .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.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"], + }); + } + } + } }); -}; - -const Schema = z.object({ - destinationId: z.string().min(1, "Destination required"), - schedule: z.string().min(1, "Schedule (Cron) required"), - prefix: z.string().min(1, "Prefix required"), - enabled: z.boolean(), - database: z.string().min(1, "Database required"), - keepLatestCount: z.coerce.number().optional(), - serviceName: z.string().nullable(), -}); - -type Schema = z.infer; interface Props { - id: string; + id?: string; + backupId?: string; databaseType?: DatabaseType; refetch: () => void; backupType: "database" | "compose"; } -type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server"; - -export const AddBackup = ({ +export const HandleBackup = ({ id, + backupId, databaseType = "postgres", refetch, backupType = "database", }: Props) => { + const [isOpen, setIsOpen] = useState(false); + const { data, isLoading } = api.destination.all.useQuery(); + const { data: backup } = api.backup.one.useQuery( + { + backupId: backupId ?? "", + }, + { + enabled: !!backupId, + }, + ); const [cacheType, setCacheType] = useState("cache"); - const [selectedDatabaseType, setSelectedDatabaseType] = - useState(databaseType as DatabaseType); - const { mutateAsync: createBackup, isLoading: isCreatingPostgresBackup } = - api.backup.create.useMutation(); + backupId + ? api.backup.update.useMutation() + : api.backup.create.useMutation(); - const schema = Schema.extend({ - metadata: getMetadataSchema(backupType, selectedDatabaseType), - }); - - const form = useForm>({ + const form = useForm>({ defaultValues: { database: databaseType === "web-server" ? "dokploy" : "", destinationId: "", @@ -135,11 +200,15 @@ export const AddBackup = ({ schedule: "", keepLatestCount: undefined, serviceName: null, + databaseType: backupType === "compose" ? undefined : databaseType, + backupType: backupType, metadata: {}, }, - resolver: zodResolver(schema), + resolver: zodResolver(Schema), }); + console.log(backup); + const { data: services, isFetching: isLoadingServices, @@ -147,38 +216,33 @@ export const AddBackup = ({ refetch: refetchServices, } = api.compose.loadServices.useQuery( { - composeId: id, + composeId: backup?.composeId ?? id ?? "", type: cacheType, }, { retry: false, refetchOnWindowFocus: false, - enabled: backupType === "compose", + enabled: backupType === "compose" && !!backup?.composeId && !!id, }, ); useEffect(() => { form.reset({ - database: databaseType === "web-server" ? "dokploy" : "", - destinationId: "", - enabled: true, - prefix: "/", - schedule: "", - keepLatestCount: undefined, - serviceName: null, - metadata: {}, + database: + (backup?.database ?? databaseType === "web-server") ? "dokploy" : "", + destinationId: backup?.destinationId ?? "", + enabled: backup?.enabled ?? true, + prefix: backup?.prefix ?? "/", + schedule: backup?.schedule ?? "", + keepLatestCount: backup?.keepLatestCount ?? undefined, + serviceName: backup?.serviceName ?? null, + databaseType: backup?.databaseType ?? databaseType, + backupType: backup?.backupType ?? backupType, + metadata: backup?.metadata ?? {}, }); - }, [form, form.reset, form.formState.isSubmitSuccessful, databaseType]); - - const onSubmit = async (data: Schema) => { - if (backupType === "compose" && !data.serviceName) { - form.setError("serviceName", { - type: "manual", - message: "Service name is required for compose backups", - }); - return; - } + }, [form, form.reset, backupId, backup]); + const onSubmit = async (data: z.infer) => { const getDatabaseId = backupType === "compose" ? { @@ -212,33 +276,50 @@ export const AddBackup = ({ schedule: data.schedule, enabled: data.enabled, database: data.database, - keepLatestCount: data.keepLatestCount, - databaseType: - backupType === "compose" ? selectedDatabaseType : databaseType, + keepLatestCount: data.keepLatestCount ?? null, + databaseType: data.databaseType || databaseType, serviceName: data.serviceName, ...getDatabaseId, + backupId: backupId ?? "", backupType, + metadata: data.metadata, }) .then(async () => { - toast.success("Backup Created"); + toast.success(`Backup ${backupId ? "Updated" : "Created"}`); refetch(); + setIsOpen(false); }) .catch(() => { - toast.error("Error creating a backup"); + toast.error(`Error ${backupId ? "updating" : "creating"} a backup`); }); }; + return ( - + - + {backupId ? ( + + ) : ( + + )} - Create a backup - Add a new backup + + {backupId ? "Update Backup" : "Create Backup"} + + + {backupId ? "Update a backup" : "Add a new backup"} +
@@ -254,25 +335,33 @@ export const AddBackup = ({ )} {backupType === "compose" && ( - - Database Type - - + ( + + Database Type + + + + )} + /> )} {backupType === "compose" && ( <> - {selectedDatabaseType === "postgres" && ( + {form.watch("databaseType") === "postgres" && ( )} - {selectedDatabaseType === "mariadb" && ( + {form.watch("databaseType") === "mariadb" && ( <> )} - {selectedDatabaseType === "mongo" && ( + {form.watch("databaseType") === "mongo" && ( <> )} - {selectedDatabaseType === "mysql" && ( + {form.watch("databaseType") === "mysql" && ( - Create + {backupId ? "Update" : "Create"} diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index 062e7be7..0a6517f9 100644 --- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -19,10 +19,10 @@ import Link from "next/link"; import { useState } from "react"; import { toast } from "sonner"; import type { ServiceType } from "../../application/advanced/show-resources"; -import { AddBackup } from "./add-backup"; import { RestoreBackup } from "./restore-backup"; -import { UpdateBackup } from "./update-backup"; import { AlertBlock } from "@/components/shared/alert-block"; +import { HandleBackup } from "./handle-backup"; +import { cn } from "@/lib/utils"; interface Props { id: string; @@ -100,7 +100,7 @@ export const ShowBackups = ({ {postgres && postgres?.backups?.length > 0 && (
{databaseType !== "web-server" && ( -
- (
-
+
{backup.backupType === "compose" && ( <>
@@ -265,7 +270,8 @@ export const ShowBackups = ({ - diff --git a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx b/apps/dokploy/components/dashboard/database/backups/update-backup.tsx deleted file mode 100644 index 8a12c535..00000000 --- a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx +++ /dev/null @@ -1,681 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -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, - FormDescription, - 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 { Switch } from "@/components/ui/switch"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { - CheckIcon, - ChevronsUpDown, - DatabaseZap, - PenBoxIcon, - RefreshCw, -} from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -type CacheType = "cache" | "fetch"; - -const getMetadataSchema = ( - backupType: "database" | "compose", - databaseType: DatabaseType, -) => { - if (backupType !== "compose") return z.object({}).optional(); - - const schemas = { - postgres: z.object({ - databaseUser: z.string().min(1, "Database user is required"), - }), - mariadb: z.object({ - databaseUser: z.string().min(1, "Database user is required"), - databasePassword: z.string().min(1, "Database password is required"), - }), - mongo: z.object({ - databaseUser: z.string().min(1, "Database user is required"), - databasePassword: z.string().min(1, "Database password is required"), - }), - mysql: z.object({ - databaseRootPassword: z.string().min(1, "Root password is required"), - }), - "web-server": z.object({}), - }; - - return z.object({ - [databaseType]: schemas[databaseType as keyof typeof schemas], - }); -}; - -const Schema = z.object({ - destinationId: z.string().min(1, "Destination required"), - schedule: z.string().min(1, "Schedule (Cron) required"), - prefix: z.string().min(1, "Prefix required"), - enabled: z.boolean(), - database: z.string().min(1, "Database required"), - keepLatestCount: z.coerce.number().optional(), - serviceName: z.string().nullable(), - metadata: z.object({}).optional(), -}); - -interface Props { - backupId: string; - refetch: () => void; -} - -type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server"; - -export const UpdateBackup = ({ backupId, refetch }: Props) => { - const [isOpen, setIsOpen] = useState(false); - const [cacheType, setCacheType] = useState("cache"); - - const { data, isLoading } = api.destination.all.useQuery(); - const { data: backup } = api.backup.one.useQuery( - { - backupId, - }, - { - enabled: !!backupId, - }, - ); - const [selectedDatabaseType, setSelectedDatabaseType] = - useState( - (backup?.databaseType as DatabaseType) || "postgres", - ); - const { - data: services, - isFetching: isLoadingServices, - error: errorServices, - refetch: refetchServices, - } = api.compose.loadServices.useQuery( - { - composeId: backup?.composeId || "", - type: cacheType, - }, - { - retry: false, - refetchOnWindowFocus: false, - enabled: - isOpen && backup?.backupType === "compose" && !!backup?.composeId, - }, - ); - - const { mutateAsync, isLoading: isLoadingUpdate } = - api.backup.update.useMutation(); - - const schema = Schema.extend({ - metadata: getMetadataSchema( - backup?.backupType || "database", - selectedDatabaseType, - ), - }); - - const form = useForm>({ - defaultValues: { - database: "", - destinationId: "", - enabled: true, - prefix: "/", - schedule: "", - keepLatestCount: undefined, - serviceName: null, - metadata: {}, - }, - resolver: zodResolver(schema), - }); - - useEffect(() => { - if (backup) { - form.reset({ - database: backup.database, - destinationId: backup.destinationId, - enabled: backup.enabled || false, - prefix: backup.prefix, - schedule: backup.schedule, - serviceName: backup.serviceName || null, - keepLatestCount: backup.keepLatestCount - ? Number(backup.keepLatestCount) - : undefined, - metadata: backup.metadata || {}, - }); - } - }, [form, form.reset, backup]); - - useEffect(() => { - if (backup?.backupType === "compose") { - const currentValues = form.getValues(); - form.reset( - { - ...currentValues, - }, - { keepDefaultValues: true }, - ); - } - }, [selectedDatabaseType, backup?.backupType, form]); - - const onSubmit = async (data: z.infer) => { - if (backup?.backupType === "compose" && !data.serviceName) { - form.setError("serviceName", { - type: "manual", - message: "Service name is required for compose backups", - }); - return; - } - - await mutateAsync({ - backupId, - destinationId: data.destinationId, - prefix: data.prefix, - schedule: data.schedule, - enabled: data.enabled, - database: data.database, - serviceName: data.serviceName, - keepLatestCount: data.keepLatestCount as number | null, - metadata: data.metadata || {}, - databaseType: - backup?.backupType === "compose" - ? selectedDatabaseType - : (backup?.databaseType as DatabaseType), - }) - .then(async () => { - toast.success("Backup Updated"); - refetch(); - setIsOpen(false); - }) - .catch(() => { - toast.error("Error updating the Backup"); - }); - }; - - return ( - - - - - - - Update Backup - Update the backup - - -
- -
- {errorServices && ( - - {errorServices?.message} - - )} - {backup?.backupType === "compose" && ( - - Database Type - - - )} - ( - - Destination - - - - - - - - - - {isLoading && ( - - Loading Destinations.... - - )} - No destinations found. - - - {data?.map((destination) => ( - { - form.setValue( - "destinationId", - destination.destinationId, - ); - }} - > - {destination.name} - - - ))} - - - - - - - - - )} - /> - {backup?.backupType === "compose" && ( -
- ( - - 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 -

-
-
-
-
- - -
- )} - /> -
- )} - { - return ( - - Database - - - - - - ); - }} - /> - { - return ( - - Schedule (Cron) - - - - - - ); - }} - /> - { - return ( - - Prefix Destination - - - - - Use if you want to back up in a specific path of your - destination/bucket - - - - - ); - }} - /> - { - return ( - - Keep the latest - - - - - Optional. If provided, only keeps the latest N backups - in the cloud. - - - - ); - }} - /> - ( - -
- Enabled - - Enable or disable the backup - -
- - - -
- )} - /> - {backup?.backupType === "compose" && ( - <> - {selectedDatabaseType === "postgres" && ( - ( - - Database User - - - - - - )} - /> - )} - - {selectedDatabaseType === "mariadb" && ( - <> - ( - - Database User - - - - - - )} - /> - ( - - Database Password - - - - - - )} - /> - - )} - - {selectedDatabaseType === "mongo" && ( - <> - ( - - Database User - - - - - - )} - /> - ( - - Database Password - - - - - - )} - /> - - )} - - {selectedDatabaseType === "mysql" && ( - ( - - Root Password - - - - - - )} - /> - )} - - )} -
- - - -
- -
-
- ); -}; diff --git a/packages/server/src/utils/restore/compose.ts b/packages/server/src/utils/restore/compose.ts index f9deb52b..1b7f9fd9 100644 --- a/packages/server/src/utils/restore/compose.ts +++ b/packages/server/src/utils/restore/compose.ts @@ -14,7 +14,6 @@ export const restoreComposeBackup = async ( emit: (log: string) => void, ) => { try { - console.log({ metadata }); const { serverId } = compose; const rcloneFlags = getS3Credentials(destination); @@ -78,8 +77,6 @@ export const restoreComposeBackup = async ( emit(stderr); } else { const { stdout, stderr } = await execAsync(restoreCommand); - console.log("stdout", stdout); - console.log("stderr", stderr); emit(stdout); emit(stderr); } diff --git a/packages/server/src/utils/restore/postgres.ts b/packages/server/src/utils/restore/postgres.ts index 5ab7c74e..d6eae283 100644 --- a/packages/server/src/utils/restore/postgres.ts +++ b/packages/server/src/utils/restore/postgres.ts @@ -40,8 +40,6 @@ rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | docker exec -i ${ emit(stderr); } else { const { stdout, stderr } = await execAsync(command); - console.log("stdout", stdout); - console.log("stderr", stderr); emit(stdout); emit(stderr); }