From 2ee40f28ed06ff7cd3e3453a19770a6926c5a8f8 Mon Sep 17 00:00:00 2001 From: vishalkadam47 Date: Sat, 31 May 2025 17:01:47 +0530 Subject: [PATCH] feat: Implement cloud storage provider backups system with OAuth support --- .../cloud-storage/add-cloud-backup.tsx | 222 + .../cloud-storage/restore-cloud-backup.tsx | 738 ++ .../cloud-storage/show-cloud-backups.tsx | 285 + .../cloud-storage/update-cloud-backup.tsx | 183 + .../database/backups/show-backups.tsx | 10 +- .../cloud-storage-destinations.tsx | 562 ++ .../settings/destination/constants.ts | 38 +- .../destination/handle-destinations.tsx | 458 +- .../destination/show-destinations.tsx | 195 +- apps/dokploy/components/layouts/side.tsx | 2 +- apps/dokploy/drizzle/0093_pale_guardsmen.sql | 33 + apps/dokploy/drizzle/meta/0093_snapshot.json | 5919 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 7 + apps/dokploy/server/api/root.ts | 4 + .../api/routers/cloud-storage-backup.ts | 626 ++ .../api/routers/cloud-storage-destination.ts | 642 ++ .../src/db/schema/cloud-storage-backup.ts | 40 + .../db/schema/cloud-storage-destination.ts | 125 + packages/server/src/db/schema/index.ts | 2 + .../src/services/cloud-storage-backup.ts | 104 + .../src/services/cloud-storage-destination.ts | 185 + .../server/src/utils/backups/cloud-storage.ts | 496 ++ 22 files changed, 10598 insertions(+), 278 deletions(-) create mode 100644 apps/dokploy/components/dashboard/database/backups/cloud-storage/add-cloud-backup.tsx create mode 100644 apps/dokploy/components/dashboard/database/backups/cloud-storage/restore-cloud-backup.tsx create mode 100644 apps/dokploy/components/dashboard/database/backups/cloud-storage/show-cloud-backups.tsx create mode 100644 apps/dokploy/components/dashboard/database/backups/cloud-storage/update-cloud-backup.tsx create mode 100644 apps/dokploy/components/dashboard/settings/destination/cloud-storage-destinations.tsx create mode 100644 apps/dokploy/drizzle/0093_pale_guardsmen.sql create mode 100644 apps/dokploy/drizzle/meta/0093_snapshot.json create mode 100644 apps/dokploy/server/api/routers/cloud-storage-backup.ts create mode 100644 apps/dokploy/server/api/routers/cloud-storage-destination.ts create mode 100644 packages/server/src/db/schema/cloud-storage-backup.ts create mode 100644 packages/server/src/db/schema/cloud-storage-destination.ts create mode 100644 packages/server/src/services/cloud-storage-backup.ts create mode 100644 packages/server/src/services/cloud-storage-destination.ts create mode 100644 packages/server/src/utils/backups/cloud-storage.ts diff --git a/apps/dokploy/components/dashboard/database/backups/cloud-storage/add-cloud-backup.tsx b/apps/dokploy/components/dashboard/database/backups/cloud-storage/add-cloud-backup.tsx new file mode 100644 index 00000000..ebdfc30c --- /dev/null +++ b/apps/dokploy/components/dashboard/database/backups/cloud-storage/add-cloud-backup.tsx @@ -0,0 +1,222 @@ +import { Button } from "@/components/ui/button"; +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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { PlusIcon } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +const AddCloudBackupSchema = 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(), +}); + +type AddCloudBackup = z.infer; + +interface Props { + databaseId: string; + databaseType: "postgres" | "mariadb" | "mysql" | "mongo" | "web-server"; + refetch: () => void; +} + +export const AddCloudBackup = ({ + databaseId, + databaseType, + refetch, +}: Props) => { + const { data: cloudDestinations, isLoading: isLoadingDestinations } = + api.cloudStorageDestination.all.useQuery(); + + const { mutateAsync: createBackup, isLoading: isCreating } = + api.cloudStorageBackup.create.useMutation(); + + const form = useForm({ + defaultValues: { + database: "", + destinationId: "", + enabled: true, + prefix: "/", + schedule: "", + keepLatestCount: undefined, + }, + resolver: zodResolver(AddCloudBackupSchema), + }); + + useEffect(() => { + form.reset({ + database: databaseType === "web-server" ? "dokploy" : "", + destinationId: "", + enabled: true, + prefix: "/", + schedule: "", + keepLatestCount: undefined, + }); + }, [form, form.reset, form.formState.isSubmitSuccessful]); + + const onSubmit = async (data: AddCloudBackup) => { + try { + await createBackup({ + ...data, + databaseType, + cloudStorageDestinationId: data.destinationId, + postgresId: databaseType === "postgres" ? databaseId : undefined, + mysqlId: databaseType === "mysql" ? databaseId : undefined, + mariadbId: databaseType === "mariadb" ? databaseId : undefined, + mongoId: databaseType === "mongo" ? databaseId : undefined, + }); + + toast.success("Cloud backup created successfully"); + refetch(); + } catch (error) { + toast.error("Failed to create cloud backup"); + console.error(error); + } + }; + + return ( + + + + + + + Add Cloud Backup + + Configure a new backup to your cloud storage destination. + + +
+ + ( + + Destination + + + + )} + /> + + ( + + Schedule (Cron) + + + + + + )} + /> + + ( + + Prefix + + + + + + )} + /> + + ( + + Database + + + + + + )} + /> + + ( + + Keep Latest Count + + + + + + )} + /> + + + + + + +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/database/backups/cloud-storage/restore-cloud-backup.tsx b/apps/dokploy/components/dashboard/database/backups/cloud-storage/restore-cloud-backup.tsx new file mode 100644 index 00000000..d3a8f0cd --- /dev/null +++ b/apps/dokploy/components/dashboard/database/backups/cloud-storage/restore-cloud-backup.tsx @@ -0,0 +1,738 @@ +import { + type LogLine, + parseLogs, +} from "@/components/dashboard/docker/logs/utils"; +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 { cn } from "@/lib/utils"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { debounce } from "lodash"; +import { CheckIcon, ChevronsUpDown, RotateCcw } from "lucide-react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +interface BackupFile { + path: string; + name: string; + size: number; + isDir: boolean; + hashes?: { + MD5: string; + }; + lastModified: string; +} + +type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server"; + +interface Props { + databaseId: string; + databaseType: DatabaseType; +} + +const restoreBackupSchema = z + .object({ + backupId: z.string().min(1, "Backup is required"), + backupFile: z.string().min(1, "Backup file is required"), + databaseName: z.string().min(1, "Database name is required"), + 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.metadata?.postgres && !data.metadata.postgres.databaseUser) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Database user is required for PostgreSQL", + path: ["metadata", "postgres", "databaseUser"], + }); + } + if (data.metadata?.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"], + }); + } + } + if (data.metadata?.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"], + }); + } + } + if (data.metadata?.mysql && !data.metadata.mysql.databaseRootPassword) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Root password is required for MySQL", + path: ["metadata", "mysql", "databaseRootPassword"], + }); + } + }); + +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 RestoreCloudBackup = ({ databaseId, databaseType }: Props) => { + const [open, setOpen] = useState(false); + const [search, setSearch] = useState(""); + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); + const [selectedBackup, setSelectedBackup] = useState(null); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [filteredLogs, setFilteredLogs] = useState([]); + const [isDeploying, setIsDeploying] = useState(false); + const [isFilePopoverOpen, setIsFilePopoverOpen] = useState(false); + + const { data: cloudBackups } = api.cloudStorageBackup.list.useQuery(); + + const { data: backupFiles, isLoading: isLoadingFiles } = + api.cloudStorageBackup.listBackupFiles.useQuery( + { + destinationId: selectedBackup?.cloudStorageDestinationId || "", + prefix: selectedBackup?.prefix || "", + searchTerm: debouncedSearchTerm, + }, + { + enabled: !!selectedBackup?.cloudStorageDestinationId, + onSuccess: (data) => { + console.log("Backup files loaded:", data); + }, + onError: (error) => { + console.error("Error loading backup files:", error); + }, + }, + ); + + const { mutateAsync: restoreBackup } = + api.cloudStorageBackup.restore.useMutation(); + + const form = useForm>({ + resolver: zodResolver(restoreBackupSchema), + defaultValues: { + backupId: "", + backupFile: "", + databaseName: databaseType === "web-server" ? "dokploy" : "", + metadata: {}, + }, + }); + + const debouncedSetSearch = debounce((value: string) => { + setDebouncedSearchTerm(value); + }, 350); + + const handleSearchChange = (value: string) => { + setSearch(value); + debouncedSetSearch(value); + }; + + const handleBackupSelect = (backup: any) => { + console.log("Selected backup:", backup); + form.setValue("backupId", backup.id); + setSelectedBackup(backup); + setSearch(""); + setDebouncedSearchTerm(""); + }; + + api.cloudStorageBackup.restoreBackupWithLogs.useSubscription( + { + databaseId, + databaseType, + databaseName: form.watch("databaseName"), + backupFile: form.watch("backupFile"), + destinationId: selectedBackup?.cloudStorageDestinationId || "", + metadata: form.watch("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 filteredBackups = cloudBackups?.filter((backup) => { + const matchesType = (() => { + switch (databaseType) { + case "postgres": + return backup.postgresId === databaseId; + case "mysql": + return backup.mysqlId === databaseId; + case "mariadb": + return backup.mariadbId === databaseId; + case "mongo": + return backup.mongoId === databaseId; + case "web-server": + return backup.databaseType === "web-server"; + default: + return false; + } + })(); + + const matchesSearch = search + ? backup.database?.toLowerCase().includes(search.toLowerCase()) || + backup.cloudStorageDestination?.name + ?.toLowerCase() + .includes(search.toLowerCase()) + : true; + + return matchesType && matchesSearch; + }); + + const onSubmit = async (values: z.infer) => { + if (!selectedBackup) { + toast.error("Please select a valid backup"); + return; + } + setIsDeploying(true); + try { + const remoteName = (() => { + switch (selectedBackup.cloudStorageDestination?.provider) { + case "drive": + return "dokploy-drive"; + case "dropbox": + return "dokploy-dropbox"; + case "box": + return "dokploy-box"; + default: + return selectedBackup.cloudStorageDestination?.provider; + } + })(); + + const prefix = selectedBackup.prefix.startsWith("/") + ? selectedBackup.prefix.slice(1) + : selectedBackup.prefix; + + await restoreBackup({ + destinationId: selectedBackup.cloudStorageDestinationId, + backupFile: `${remoteName}:${prefix}/${values.backupFile}`, + databaseType, + databaseName: values.databaseName, + metadata: values.metadata, + }); + toast.success("Backup restore initiated successfully"); + setOpen(false); + } catch (error) { + console.error("Restore error:", error); + toast.error( + error instanceof Error ? error.message : "Failed to restore backup", + ); + } finally { + setIsDeploying(false); + } + }; + + return ( + { + setOpen(newOpen); + if (!newOpen) { + // Reset form and state when dialog is closed + form.reset(); + setSelectedBackup(null); + setSearch(""); + setDebouncedSearchTerm(""); + setIsFilePopoverOpen(false); + setFilteredLogs([]); + setIsDeploying(false); + } + }} + > + + + + + + + + Restore Cloud Backup + + + Select a backup to restore from your cloud storage destination. + + +
+ + ( + + Backup + + + + + + + + + + No backups found. + + + {filteredBackups?.map((backup) => ( + handleBackupSelect(backup)} + > + + {backup.cloudStorageDestination?.name} -{" "} + {backup.database} + + ))} + + + + + + + + )} + /> + + ( + + + Search Backup Files + + + + + + + + + + + {isLoadingFiles ? ( +
+
+
+ + Loading backup files... + +
+
+ ) : backupFiles?.length === 0 && search ? ( +
+ + No backup files found for "{search}" + + +
+ ) : backupFiles?.length === 0 ? ( +
+ + No backup files available + + + Make sure you have backup files in the selected + destination + +
+ ) : ( + + + {backupFiles + ?.sort( + (a: BackupFile, b: BackupFile) => + new Date(b.lastModified).getTime() - + new Date(a.lastModified).getTime(), + ) + .map((file: BackupFile) => ( + { + form.setValue("backupFile", file.path); + setSearch(""); + setDebouncedSearchTerm(""); + setIsFilePopoverOpen(false); + }} + className="flex flex-col items-start gap-1 p-2" + > +
+ + {file.name} + +
+ + {new Date( + file.lastModified, + ).toLocaleDateString()} + + + {new Date( + file.lastModified, + ).toLocaleTimeString()} + +
+
+
+ + + Size: + + {formatBytes(file.size)} + + {file.isDir && ( + + + Type: + + Directory + + )} + {file.hashes?.MD5 && ( + + + MD5: + + + {file.hashes.MD5} + + + )} +
+
+ ))} +
+
+ )} + + + + + + )} + /> + + ( + + Database Name + + + + + + )} + /> + + {databaseType === "postgres" && ( + ( + + Database User + + + + + + )} + /> + )} + + {databaseType === "mariadb" && ( + <> + ( + + Database User + + + + + + )} + /> + ( + + Database Password + + + + + + )} + /> + + )} + + {databaseType === "mongo" && ( + <> + ( + + Database User + + + + + + )} + /> + ( + + Database Password + + + + + + )} + /> + + )} + + {databaseType === "mysql" && ( + ( + + Root Password + + + + + + )} + /> + )} + + + + + + + + { + setIsDrawerOpen(false); + setFilteredLogs([]); + setIsDeploying(false); + }} + filteredLogs={filteredLogs} + /> + +
+ ); +}; diff --git a/apps/dokploy/components/dashboard/database/backups/cloud-storage/show-cloud-backups.tsx b/apps/dokploy/components/dashboard/database/backups/cloud-storage/show-cloud-backups.tsx new file mode 100644 index 00000000..52410cac --- /dev/null +++ b/apps/dokploy/components/dashboard/database/backups/cloud-storage/show-cloud-backups.tsx @@ -0,0 +1,285 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { api } from "@/utils/api"; +import { Database, Play, RefreshCw, Trash2 } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { AddCloudBackup } from "./add-cloud-backup"; +import { RestoreCloudBackup } from "./restore-cloud-backup"; +import { UpdateCloudBackup } from "./update-cloud-backup"; + +interface Props { + databaseId: string; + databaseType: "postgres" | "mariadb" | "mysql" | "mongo" | "web-server"; +} + +export const ShowCloudBackups = ({ databaseId, databaseType }: Props) => { + const [activeManualBackup, setActiveManualBackup] = useState< + string | undefined + >(); + const [reconnectingBackupId, setReconnectingBackupId] = useState< + string | undefined + >(); + + const { + data: cloudBackups, + isLoading: isLoadingBackups, + refetch, + } = api.cloudStorageBackup.list.useQuery(); + + const { mutateAsync: manualBackup, isLoading: isManualBackup } = + api.cloudStorageBackup.manualBackup.useMutation(); + + const { mutateAsync: deleteBackup, isLoading: isRemoving } = + api.cloudStorageBackup.remove.useMutation(); + + const { mutateAsync: reconnectMutation } = + api.cloudStorageDestination.reconnect.useMutation(); + + const filteredBackups = cloudBackups?.filter((backup) => { + switch (databaseType) { + case "postgres": + return backup.postgresId === databaseId; + case "mysql": + return backup.mysqlId === databaseId; + case "mariadb": + return backup.mariadbId === databaseId; + case "mongo": + return backup.mongoId === databaseId; + case "web-server": + return backup.databaseType === "web-server"; + default: + return false; + } + }); + + const handleReconnect = async (destinationId: string, backupId: string) => { + try { + setReconnectingBackupId(backupId); + const result = await reconnectMutation({ destinationId }); + if (result.silent === false) { + toast.info( + "Please complete the authentication in the opened browser window.", + ); + } + toast.success("Reconnected successfully! Please try your backup again."); + await refetch(); + } catch (_err) { + toast.error("Reconnect failed or was cancelled. Please try again."); + } finally { + setReconnectingBackupId(undefined); + } + }; + return ( + + +
+ + + Cloud Backups + + + Manage your cloud storage backups and restore data when needed. + +
+ + {filteredBackups && filteredBackups.length > 0 && ( +
+ + +
+ )} +
+ + {isLoadingBackups ? ( +
+
+
+ ) : filteredBackups?.length === 0 ? ( +
+ + + No cloud backups configured + +
+ + +
+
+ ) : ( +
+ {filteredBackups?.map((backup) => ( +
+
+
+ Destination + + {backup.cloudStorageDestination?.name || "Unknown"} + +
+
+ Database + + {backup.database} + +
+
+ Scheduled + + {backup.schedule} + +
+
+ Prefix Storage + + {backup.prefix} + +
+
+ Enabled + + {backup.enabled ? "Yes" : "No"} + +
+
+
+ + + + + + Run Manual Backup + + + {/* Refresh Token for OAuth providers */} + {backup.cloudStorageDestination?.provider && + ["drive", "dropbox", "box"].includes( + backup.cloudStorageDestination.provider, + ) && ( + + + + + Refresh Token + + )} + + + + + + + + Delete Backup + + +
+
+ ))} +
+ )} + + + ); +}; diff --git a/apps/dokploy/components/dashboard/database/backups/cloud-storage/update-cloud-backup.tsx b/apps/dokploy/components/dashboard/database/backups/cloud-storage/update-cloud-backup.tsx new file mode 100644 index 00000000..031ae42b --- /dev/null +++ b/apps/dokploy/components/dashboard/database/backups/cloud-storage/update-cloud-backup.tsx @@ -0,0 +1,183 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + 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 { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Pencil } from "lucide-react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +const updateBackupSchema = z.object({ + schedule: z.string().min(1, "Schedule is required"), + prefix: z.string().min(1, "Prefix is required"), + enabled: z.boolean(), + keepLatestCount: z.string().optional(), +}); + +interface Props { + backupId: string; + refetch: () => void; +} + +export const UpdateCloudBackup = ({ backupId, refetch }: Props) => { + const [open, setOpen] = useState(false); + + const { data: backup, isLoading: isLoadingBackup } = + api.cloudStorageBackup.get.useQuery({ backupId }); + + const { mutateAsync: updateBackup, isLoading: isUpdating } = + api.cloudStorageBackup.update.useMutation(); + + const form = useForm>({ + resolver: zodResolver(updateBackupSchema), + defaultValues: { + schedule: backup?.schedule || "", + prefix: backup?.prefix || "", + enabled: backup?.enabled || false, + keepLatestCount: backup?.keepLatestCount?.toString() || "", + }, + }); + + const onSubmit = async (values: z.infer) => { + await updateBackup({ + backupId, + schedule: values.schedule, + prefix: values.prefix, + enabled: values.enabled, + keepLatestCount: values.keepLatestCount + ? Number.parseInt(values.keepLatestCount) + : undefined, + }) + .then(() => { + toast.success("Backup updated successfully"); + refetch(); + setOpen(false); + }) + .catch(() => { + toast.error("Error updating backup"); + }); + }; + + return ( + + + + + + + Update Cloud Backup + + Modify the settings for your cloud storage backup. + + +
+ + ( + + Schedule (Cron Expression) + + + + + + )} + /> + ( + + Storage Prefix + + + + + + )} + /> + ( + +
+ Enabled +
+ + + +
+ )} + /> + ( + + Keep Latest Count (Optional) + + + + + + )} + /> +
+ + +
+ + +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index 28ee68a9..7d8b112b 100644 --- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -34,6 +34,7 @@ import { useState } from "react"; import { toast } from "sonner"; import type { ServiceType } from "../../application/advanced/show-resources"; import { ShowDeploymentsModal } from "../../application/deployments/show-deployments-modal"; +import { ShowCloudBackups } from "./cloud-storage/show-cloud-backups"; import { HandleBackup } from "./handle-backup"; import { RestoreBackup } from "./restore-backup"; @@ -102,10 +103,10 @@ export const ShowBackups = ({
- Backups + S3 Backups - Add backups to your database to save the data to a different + Add S3 backups to your database to save the data to a different provider.
@@ -151,7 +152,7 @@ export const ShowBackups = ({
- No backups configured + No S3 backups configured
)} + {databaseType && ( + + )} ); }; diff --git a/apps/dokploy/components/dashboard/settings/destination/cloud-storage-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/cloud-storage-destinations.tsx new file mode 100644 index 00000000..7a91a650 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/destination/cloud-storage-destinations.tsx @@ -0,0 +1,562 @@ +import { Button } from "@/components/ui/button"; +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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; +import { + type CloudStorageDestinationFormValues, + cloudStorageDestinationSchema, +} from "@dokploy/server/db/schema/cloud-storage-destination"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Check, + CheckCircle2, + Copy, + Eye, + EyeOff, + Loader2, + XCircle, +} from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { CLOUD_STORAGE_PROVIDERS } from "./constants"; + +export const CLOUD_STORAGE_PROVIDER_TYPES = CLOUD_STORAGE_PROVIDERS.map( + (p) => p.key as string, +); + +export function isCloudStorageProvider(provider: string): boolean { + return CLOUD_STORAGE_PROVIDER_TYPES.includes(provider); +} + +interface Props { + destinationId?: string; + inTabContent?: boolean; + commonName?: string; +} + +type CloudStorageProviderType = "drive" | "dropbox" | "box" | "ftp" | "sftp"; + +export const CloudStorageDestinations = ({ + destinationId, + inTabContent = false, + commonName, +}: Props) => { + const [open, setOpen] = useState(false); + const utils = api.useUtils(); + const [connectionTested, setConnectionTested] = useState(false); + + const { mutateAsync: createMutation, isLoading: isCreating } = + api.cloudStorageDestination.create.useMutation(); + const { mutateAsync: updateMutation, isLoading: isUpdating } = + api.cloudStorageDestination.update.useMutation(); + const { mutateAsync: testConnection, isLoading: isLoadingConnection } = + api.cloudStorageDestination.testConnection.useMutation(); + + const isLoading = isCreating || isUpdating; + + const { data: destinations } = api.cloudStorageDestination.all.useQuery(); + const destination = destinationId + ? destinations?.find((d) => d.id === destinationId) + : undefined; + + const form = useForm({ + defaultValues: { + providerType: undefined, + host: "", + username: "", + password: "", + port: "", + token: "", + }, + resolver: zodResolver(cloudStorageDestinationSchema), + }); + + const providerType = form.watch("providerType") as CloudStorageProviderType; + + useEffect(() => { + setConnectionTested(false); + form.setValue("token", ""); + }, [providerType, form]); + + useEffect(() => { + if (!open) { + form.reset(); + setConnectionTested(false); + } + }, [open, form]); + + useEffect(() => { + if (destination) { + form.reset({ + providerType: destination.provider as any, + host: destination.host || "", + username: destination.username || "", + password: destination.password || "", + port: destination.port || "", + token: destination.config || "", + }); + } else { + form.reset({ + providerType: undefined, + host: "", + username: "", + password: "", + port: "", + token: "", + }); + } + }, [form, destination]); + + // Handle form submission + const onSubmit = async (data: CloudStorageDestinationFormValues) => { + try { + if (!connectionTested && !destinationId) { + toast.error("Please test the connection first"); + return; + } + + const credentials = { + ...(data.providerType === "ftp" || data.providerType === "sftp" + ? { + host: data.host, + username: data.username, + password: data.password, + port: data.port, + } + : {}), + ...(["drive", "dropbox", "box"].includes(data.providerType) + ? { + token: data.token, + } + : {}), + }; + + if (destinationId) { + await updateMutation({ + destinationId, + name: commonName || data.providerType, + provider: data.providerType, + config: JSON.stringify(credentials), + }); + toast.success("Destination updated"); + } else { + await createMutation({ + name: commonName || data.providerType, + provider: data.providerType, + config: JSON.stringify(credentials), + }); + toast.success("Destination created"); + } + + await utils.cloudStorageDestination.all.invalidate(); + form.reset(); + setConnectionTested(false); + setOpen(false); + } catch (err: any) { + toast.error("Error saving destination", { + description: err.message, + }); + } + }; + + // Handle test connection + const handleTestConnection = async () => { + const data = form.getValues(); + + try { + // For FTP/SFTP, ensure password is base64 encoded + if ( + (data.providerType === "ftp" || data.providerType === "sftp") && + data.password + ) { + try { + const cleanPassword = data.password.trim(); + + if (!cleanPassword || cleanPassword.includes(" ")) { + throw new Error("Invalid password format"); + } + + form.setValue("password", cleanPassword); + } catch (_e) { + toast.error("Invalid password format", { + description: + "Please make sure you copied the entire output from 'rclone obscure' command without any extra spaces.", + }); + return; + } + } + + const result = await testConnection({ + provider: data.providerType, + credentials: { + host: data.host, + username: data.username, + password: data.password, + port: data.port, + token: data.token, + }, + }); + + // Store the token if it exists + if (result.token) { + form.setValue("token", result.token); + const providerName = (() => { + switch (data.providerType as CloudStorageProviderType) { + case "drive": + return "Google Drive"; + case "dropbox": + return "Dropbox"; + case "box": + return "Box"; + case "ftp": + return "FTP"; + case "sftp": + return "SFTP"; + default: + return data.providerType; + } + })(); + toast.success(`${providerName} authentication successful`, { + description: "Your account has been connected successfully.", + }); + } + + setConnectionTested(true); + if (!result.token) { + toast.success("Connection test successful"); + } + } catch (err: any) { + setConnectionTested(false); + form.setValue("token", ""); + toast.error("Connection test failed", { + description: "Connection failed, please try again.", + }); + } + }; + + // Render provider-specific fields + const renderProviderFields = () => { + if (!providerType) return null; + + switch (providerType) { + case "ftp": + case "sftp": + return ( + <> + ( + + Host + + + + + + )} + /> + ( + + Username + + + + + + )} + /> + ( + + Password + + + + +
+

To get the base64 encoded password:

+
    +
  1. + Open terminal and run:{" "} + + rclone obscure your_password + +
  2. +
  3. Copy the output and paste it here
  4. +
+

+ Note: Do not enter your plain password. It must be base64 + encoded using rclone obscure. +

+
+
+ )} + /> + ( + + Port (Optional) + + + + + + )} + /> + + ); + + case "drive": + case "dropbox": + case "box": + return ( + { + const [copied, setCopied] = useState(false); + const [showToken, setShowToken] = useState(false); + + const handleCopy = async (text: string) => { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const providerName = (() => { + switch (providerType as CloudStorageProviderType) { + case "drive": + return "Google Drive"; + case "dropbox": + return "Dropbox"; + case "box": + return "Box"; + case "ftp": + return "FTP"; + case "sftp": + return "SFTP"; + default: + return providerType; + } + })(); + + return ( + + {providerName} + +
+
+ {value ? ( + <> + + + Authentication Successful + + + ) : ( + <> + + + Not Authenticated + + + )} +
+ + {value && ( +
+
+
+ OAuth Token: +
+
+ + +
+
+ {showToken && ( +
+ + {JSON.stringify(JSON.parse(value), null, 2)} + +
+ )} +
+ )} + +
+ {value + ? "Token obtained successfully. You can now create the destination." + : `Click 'Test Connection' to start the OAuth flow with ${providerName}`} +
+
+
+ +
+ ); + }} + /> + ); + } + }; + + // Render the form content + const formContent = ( + <> +
+ + ( + + Provider Type + + + + )} + /> + + {renderProviderFields()} + + + + + + + + + + ); + + if (inTabContent) { + return formContent; + } + + return ( + + + + + + + + {destinationId ? "Update" : "Add"} Cloud Storage Destination + + + Configure cloud storage providers like Google Drive, FTP, or SFTP. + + + {formContent} + + + ); +}; diff --git a/apps/dokploy/components/dashboard/settings/destination/constants.ts b/apps/dokploy/components/dashboard/settings/destination/constants.ts index f43e47d1..293dd9fb 100644 --- a/apps/dokploy/components/dashboard/settings/destination/constants.ts +++ b/apps/dokploy/components/dashboard/settings/destination/constants.ts @@ -1,7 +1,15 @@ -export const S3_PROVIDERS: Array<{ +export interface CloudStorageProvider { key: string; name: string; -}> = [ + oauthRequired?: boolean; +} + +export interface S3Provider { + key: string; + name: string; +} + +export const S3_PROVIDERS: S3Provider[] = [ { key: "AWS", name: "Amazon Web Services (AWS) S3", @@ -131,3 +139,29 @@ export const S3_PROVIDERS: Array<{ name: "Any other S3 compatible provider", }, ]; + +export const CLOUD_STORAGE_PROVIDERS: CloudStorageProvider[] = [ + { + key: "drive", + name: "Google Drive", + oauthRequired: true, + }, + { + key: "dropbox", + name: "Dropbox", + oauthRequired: true, + }, + { + key: "box", + name: "Box", + oauthRequired: true, + }, + { + key: "ftp", + name: "FTP", + }, + { + key: "sftp", + name: "SFTP", + }, +] as const; diff --git a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx index aedf8744..ee4e2b8c 100644 --- a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx @@ -27,6 +27,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -35,6 +36,7 @@ import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { CloudStorageDestinations } from "./cloud-storage-destinations"; import { S3_PROVIDERS } from "./constants"; const addDestination = z.object({ @@ -52,13 +54,20 @@ type AddDestination = z.infer; interface Props { destinationId?: string; + cloudDestinationId?: string; + type?: "s3" | "cloud"; } -export const HandleDestinations = ({ destinationId }: Props) => { +export const HandleDestinations = ({ + destinationId, + cloudDestinationId, + type, +}: Props) => { const [open, setOpen] = useState(false); const utils = api.useUtils(); const { data: servers } = api.server.withSSHKey.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); + const [activeTab, setActiveTab] = useState(type || "s3"); const { mutateAsync, isError, error, isLoading } = destinationId ? api.destination.update.useMutation() @@ -119,13 +128,15 @@ export const HandleDestinations = ({ destinationId }: Props) => { destinationId: destinationId || "", }) .then(async () => { - toast.success(`Destination ${destinationId ? "Updated" : "Created"}`); + toast.success( + `S3 Destination ${destinationId ? "Updated" : "Created"}`, + ); await utils.destination.all.invalidate(); setOpen(false); }) .catch(() => { toast.error( - `Error ${destinationId ? "Updating" : "Creating"} the Destination`, + `Error ${destinationId ? "Updating" : "Creating"} the S3 Destination`, ); }); }; @@ -189,7 +200,7 @@ export const HandleDestinations = ({ destinationId }: Props) => { return ( - {destinationId ? ( + {destinationId || cloudDestinationId ? ( -
- ) : ( - - )} - - - + + {isCloud ? ( +
+ + Select a server to test the destination. If you don't + have a server choose the default one. + + ( + + Server (Optional) + + + + + + + )} + /> + +
+ ) : ( + + )} + + +
+ + + + + {/* Cloud Storage Tab Content */} + + + + ); diff --git a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx index 014596ce..9edcbaab 100644 --- a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx @@ -10,24 +10,74 @@ import { import { api } from "@/utils/api"; import { Database, FolderUp, Loader2, Trash2 } from "lucide-react"; import { toast } from "sonner"; +import { isCloudStorageProvider } from "./cloud-storage-destinations"; import { HandleDestinations } from "./handle-destinations"; export const ShowDestinations = () => { - const { data, isLoading, refetch } = api.destination.all.useQuery(); - const { mutateAsync, isLoading: isRemoving } = + const { + data: s3Destinations, + isLoading: isLoadingS3, + refetch: refetchS3, + } = api.destination.all.useQuery(); + const { + data: cloudDestinations, + isLoading: isLoadingCloud, + refetch: refetchCloud, + } = api.cloudStorageDestination.all.useQuery(); + + const destinations = [ + ...(s3Destinations || []).map((s3) => ({ + ...s3, + isCloudStorage: false, + })), + ...(cloudDestinations || []).map((cloud) => ({ + destinationId: cloud.id, + name: cloud.name, + provider: cloud.provider, + createdAt: cloud.createdAt, + isCloudStorage: true, + })), + ]; + + const isLoading = isLoadingS3 || isLoadingCloud; + const { mutateAsync: removeS3Destination, isLoading: isRemovingS3 } = api.destination.remove.useMutation(); + const { mutateAsync: removeCloudDestination, isLoading: isRemovingCloud } = + api.cloudStorageDestination.delete.useMutation(); + + const handleDelete = async ( + destinationId: string, + isCloudStorage: boolean, + ) => { + try { + if (isCloudStorage) { + await removeCloudDestination({ destinationId }); + } else { + await removeS3Destination({ destinationId }); + } + toast.success("Destination deleted successfully"); + await refetchS3(); + await refetchCloud(); + } catch (_err: any) { + toast.error("Cannot delete destination", { + description: + "You must delete all backups associated with this destination before deleting the destination.", + }); + } + }; + return (
- +
- + - S3 Destinations + Backup Destinations - Add your providers like AWS S3, Cloudflare R2, Wasabi, - DigitalOcean Spaces etc. + Configure storage providers for your backups including + S3-compatible services, Google Drive, Dropbox, Box and more. @@ -38,71 +88,90 @@ export const ShowDestinations = () => {
) : ( <> - {data?.length === 0 ? ( -
+ {destinations?.length === 0 ? ( +
To create a backup it is required to set at least 1 - provider. + storage provider. - +
+ +
) : ( -
-
- {data?.map((destination, index) => ( -
-
-
- - {index + 1}. {destination.name} - - - Created at:{" "} - {new Date( - destination.createdAt, - ).toLocaleDateString()} - -
-
- - { - await mutateAsync({ - destinationId: destination.destinationId, - }) - .then(() => { - toast.success( - "Destination deleted successfully", - ); - refetch(); - }) - .catch(() => { - toast.error("Error deleting destination"); - }); - }} - > - - + + +
-
- ))} + ); + })}
diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index 8d180967..272b81ae 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -330,7 +330,7 @@ const MENU: Menu = { }, { isSingle: true, - title: "S3 Destinations", + title: "Backup Destinations", url: "/dashboard/settings/destinations", icon: Database, // Only enabled for admins diff --git a/apps/dokploy/drizzle/0093_pale_guardsmen.sql b/apps/dokploy/drizzle/0093_pale_guardsmen.sql new file mode 100644 index 00000000..10639ecc --- /dev/null +++ b/apps/dokploy/drizzle/0093_pale_guardsmen.sql @@ -0,0 +1,33 @@ +CREATE TABLE "cloud_storage_destination" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "provider" text NOT NULL, + "username" text, + "password" text, + "host" text, + "port" text, + "config" text, + "organization_id" text NOT NULL, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "cloud_storage_backup" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "cloud_storage_destination_id" uuid NOT NULL, + "schedule" text NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "database_type" text NOT NULL, + "prefix" text, + "database" text, + "postgres_id" text, + "mysql_id" text, + "mariadb_id" text, + "mongo_id" text, + "keep_latest_count" integer, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "cloud_storage_backup" ADD CONSTRAINT "cloud_storage_backup_cloud_storage_destination_id_cloud_storage_destination_id_fk" FOREIGN KEY ("cloud_storage_destination_id") REFERENCES "public"."cloud_storage_destination"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/0093_snapshot.json b/apps/dokploy/drizzle/meta/0093_snapshot.json new file mode 100644 index 00000000..8c6babba --- /dev/null +++ b/apps/dokploy/drizzle/meta/0093_snapshot.json @@ -0,0 +1,5919 @@ +{ + "id": "9916621c-4225-4fbf-9f73-7161b35f67f0", + "prevId": "76eb8fe3-21c0-4544-962c-1ae18e8e6730", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.application": { + "name": "application", + "schema": "", + "columns": { + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewEnv": { + "name": "previewEnv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewBuildArgs": { + "name": "previewBuildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewWildcard": { + "name": "previewWildcard", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewPort": { + "name": "previewPort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "previewHttps": { + "name": "previewHttps", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previewPath": { + "name": "previewPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "previewCustomCertResolver": { + "name": "previewCustomCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLimit": { + "name": "previewLimit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "isPreviewDeploymentsActive": { + "name": "isPreviewDeploymentsActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "buildArgs": { + "name": "buildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "cleanCache": { + "name": "cleanCache", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildPath": { + "name": "buildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBuildPath": { + "name": "gitlabBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBuildPath": { + "name": "giteaBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBuildPath": { + "name": "bitbucketBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBuildPath": { + "name": "customGitBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerfile": { + "name": "dockerfile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerContextPath": { + "name": "dockerContextPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerBuildStage": { + "name": "dockerBuildStage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropBuildPath": { + "name": "dropBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "buildType": { + "name": "buildType", + "type": "buildType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'nixpacks'" + }, + "herokuVersion": { + "name": "herokuVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'24'" + }, + "publishDirectory": { + "name": "publishDirectory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isStaticSpa": { + "name": "isStaticSpa", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "application_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "application_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "application", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_registryId_registry_registryId_fk": { + "name": "application_registryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "registryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_projectId_project_projectId_fk": { + "name": "application_projectId_project_projectId_fk", + "tableFrom": "application", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_githubId_github_githubId_fk": { + "name": "application_githubId_github_githubId_fk", + "tableFrom": "application", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_gitlabId_gitlab_gitlabId_fk": { + "name": "application_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "application", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_giteaId_gitea_giteaId_fk": { + "name": "application_giteaId_gitea_giteaId_fk", + "tableFrom": "application", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "application_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "application", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_serverId_server_serverId_fk": { + "name": "application_serverId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "application_appName_unique": { + "name": "application_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.postgres": { + "name": "postgres", + "schema": "", + "columns": { + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "postgres_projectId_project_projectId_fk": { + "name": "postgres_projectId_project_projectId_fk", + "tableFrom": "postgres", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "postgres_serverId_server_serverId_fk": { + "name": "postgres_serverId_server_serverId_fk", + "tableFrom": "postgres", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postgres_appName_unique": { + "name": "postgres_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_temp": { + "name": "user_temp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "isRegistered": { + "name": "isRegistered", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expirationDate": { + "name": "expirationDate", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "serverIp": { + "name": "serverIp", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "letsEncryptEmail": { + "name": "letsEncryptEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sshPrivateKey": { + "name": "sshPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "logCleanupCron": { + "name": "logCleanupCron", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "enablePaidFeatures": { + "name": "enablePaidFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "allowImpersonation": { + "name": "allowImpersonation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Dokploy\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"retentionDays\":2,\"cronJob\":\"\",\"urlCallback\":\"\",\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + }, + "cleanupCacheApplications": { + "name": "cleanupCacheApplications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnPreviews": { + "name": "cleanupCacheOnPreviews", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnCompose": { + "name": "cleanupCacheOnCompose", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serversQuantity": { + "name": "serversQuantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_temp_email_unique": { + "name": "user_temp_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "project_organizationId_organization_id_fk": { + "name": "project_organizationId_organization_id_fk", + "tableFrom": "project", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domainType": { + "name": "domainType", + "type": "domainType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'application'" + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customCertResolver": { + "name": "customCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + } + }, + "indexes": {}, + "foreignKeys": { + "domain_composeId_compose_composeId_fk": { + "name": "domain_composeId_compose_composeId_fk", + "tableFrom": "domain", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_applicationId_application_applicationId_fk": { + "name": "domain_applicationId_application_applicationId_fk", + "tableFrom": "domain", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "domain", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mariadb": { + "name": "mariadb", + "schema": "", + "columns": { + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mariadb_projectId_project_projectId_fk": { + "name": "mariadb_projectId_project_projectId_fk", + "tableFrom": "mariadb", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mariadb_serverId_server_serverId_fk": { + "name": "mariadb_serverId_server_serverId_fk", + "tableFrom": "mariadb", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mariadb_appName_unique": { + "name": "mariadb_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mongo": { + "name": "mongo", + "schema": "", + "columns": { + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replicaSets": { + "name": "replicaSets", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mongo_projectId_project_projectId_fk": { + "name": "mongo_projectId_project_projectId_fk", + "tableFrom": "mongo", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mongo_serverId_server_serverId_fk": { + "name": "mongo_serverId_server_serverId_fk", + "tableFrom": "mongo", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mongo_appName_unique": { + "name": "mongo_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mysql": { + "name": "mysql", + "schema": "", + "columns": { + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mysql_projectId_project_projectId_fk": { + "name": "mysql_projectId_project_projectId_fk", + "tableFrom": "mysql", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mysql_serverId_server_serverId_fk": { + "name": "mysql_serverId_server_serverId_fk", + "tableFrom": "mysql", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mysql_appName_unique": { + "name": "mysql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup": { + "name": "backup", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "backupType": { + "name": "backupType", + "type": "backupType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'database'" + }, + "databaseType": { + "name": "databaseType", + "type": "databaseType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_destinationId_destination_destinationId_fk": { + "name": "backup_destinationId_destination_destinationId_fk", + "tableFrom": "backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_composeId_compose_composeId_fk": { + "name": "backup_composeId_compose_composeId_fk", + "tableFrom": "backup", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_postgresId_postgres_postgresId_fk": { + "name": "backup_postgresId_postgres_postgresId_fk", + "tableFrom": "backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mariadbId_mariadb_mariadbId_fk": { + "name": "backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mysqlId_mysql_mysqlId_fk": { + "name": "backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mongoId_mongo_mongoId_fk": { + "name": "backup_mongoId_mongo_mongoId_fk", + "tableFrom": "backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_userId_user_temp_id_fk": { + "name": "backup_userId_user_temp_id_fk", + "tableFrom": "backup", + "tableTo": "user_temp", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "backup_appName_unique": { + "name": "backup_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.destination": { + "name": "destination", + "schema": "", + "columns": { + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessKey": { + "name": "accessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secretAccessKey": { + "name": "secretAccessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "destination_organizationId_organization_id_fk": { + "name": "destination_organizationId_organization_id_fk", + "tableFrom": "destination", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deploymentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'running'" + }, + "logPath": { + "name": "logPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isPreviewDeployment": { + "name": "isPreviewDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startedAt": { + "name": "startedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finishedAt": { + "name": "finishedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_applicationId_application_applicationId_fk": { + "name": "deployment_applicationId_application_applicationId_fk", + "tableFrom": "deployment", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_composeId_compose_composeId_fk": { + "name": "deployment_composeId_compose_composeId_fk", + "tableFrom": "deployment", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_serverId_server_serverId_fk": { + "name": "deployment_serverId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "deployment", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_scheduleId_schedule_scheduleId_fk": { + "name": "deployment_scheduleId_schedule_scheduleId_fk", + "tableFrom": "deployment", + "tableTo": "schedule", + "columnsFrom": [ + "scheduleId" + ], + "columnsTo": [ + "scheduleId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_backupId_backup_backupId_fk": { + "name": "deployment_backupId_backup_backupId_fk", + "tableFrom": "deployment", + "tableTo": "backup", + "columnsFrom": [ + "backupId" + ], + "columnsTo": [ + "backupId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mount": { + "name": "mount", + "schema": "", + "columns": { + "mountId": { + "name": "mountId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "mountType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hostPath": { + "name": "hostPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "mountPath": { + "name": "mountPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mount_applicationId_application_applicationId_fk": { + "name": "mount_applicationId_application_applicationId_fk", + "tableFrom": "mount", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_postgresId_postgres_postgresId_fk": { + "name": "mount_postgresId_postgres_postgresId_fk", + "tableFrom": "mount", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mariadbId_mariadb_mariadbId_fk": { + "name": "mount_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "mount", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mongoId_mongo_mongoId_fk": { + "name": "mount_mongoId_mongo_mongoId_fk", + "tableFrom": "mount", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mysqlId_mysql_mysqlId_fk": { + "name": "mount_mysqlId_mysql_mysqlId_fk", + "tableFrom": "mount", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_redisId_redis_redisId_fk": { + "name": "mount_redisId_redis_redisId_fk", + "tableFrom": "mount", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_composeId_compose_composeId_fk": { + "name": "mount_composeId_compose_composeId_fk", + "tableFrom": "mount", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.certificate": { + "name": "certificate", + "schema": "", + "columns": { + "certificateId": { + "name": "certificateId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificateData": { + "name": "certificateData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificatePath": { + "name": "certificatePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autoRenew": { + "name": "autoRenew", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "certificate_organizationId_organization_id_fk": { + "name": "certificate_organizationId_organization_id_fk", + "tableFrom": "certificate", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "certificate_serverId_server_serverId_fk": { + "name": "certificate_serverId_server_serverId_fk", + "tableFrom": "certificate", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "certificate_certificatePath_unique": { + "name": "certificate_certificatePath_unique", + "nullsNotDistinct": false, + "columns": [ + "certificatePath" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_temp": { + "name": "session_temp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_temp_user_id_user_temp_id_fk": { + "name": "session_temp_user_id_user_temp_id_fk", + "tableFrom": "session_temp", + "tableTo": "user_temp", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_temp_token_unique": { + "name": "session_temp_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redirect": { + "name": "redirect", + "schema": "", + "columns": { + "redirectId": { + "name": "redirectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "regex": { + "name": "regex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permanent": { + "name": "permanent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "redirect_applicationId_application_applicationId_fk": { + "name": "redirect_applicationId_application_applicationId_fk", + "tableFrom": "redirect", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security": { + "name": "security", + "schema": "", + "columns": { + "securityId": { + "name": "securityId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "security_applicationId_application_applicationId_fk": { + "name": "security_applicationId_application_applicationId_fk", + "tableFrom": "security", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_username_applicationId_unique": { + "name": "security_username_applicationId_unique", + "nullsNotDistinct": false, + "columns": [ + "username", + "applicationId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.port": { + "name": "port", + "schema": "", + "columns": { + "portId": { + "name": "portId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publishedPort": { + "name": "publishedPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "targetPort": { + "name": "targetPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "protocolType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "port_applicationId_application_applicationId_fk": { + "name": "port_applicationId_application_applicationId_fk", + "tableFrom": "port", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redis": { + "name": "redis", + "schema": "", + "columns": { + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "redis_projectId_project_projectId_fk": { + "name": "redis_projectId_project_projectId_fk", + "tableFrom": "redis", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "redis_serverId_server_serverId_fk": { + "name": "redis_serverId_server_serverId_fk", + "tableFrom": "redis", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "redis_appName_unique": { + "name": "redis_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.compose": { + "name": "compose", + "schema": "", + "columns": { + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeFile": { + "name": "composeFile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceTypeCompose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "composeType": { + "name": "composeType", + "type": "composeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'docker-compose'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "composePath": { + "name": "composePath", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'./docker-compose.yml'" + }, + "suffix": { + "name": "suffix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "randomize": { + "name": "randomize", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeployment": { + "name": "isolatedDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "composeStatus": { + "name": "composeStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "compose", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_projectId_project_projectId_fk": { + "name": "compose_projectId_project_projectId_fk", + "tableFrom": "compose", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "compose_githubId_github_githubId_fk": { + "name": "compose_githubId_github_githubId_fk", + "tableFrom": "compose", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_gitlabId_gitlab_gitlabId_fk": { + "name": "compose_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "compose", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "compose_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "compose", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_giteaId_gitea_giteaId_fk": { + "name": "compose_giteaId_gitea_giteaId_fk", + "tableFrom": "compose", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_serverId_server_serverId_fk": { + "name": "compose_serverId_server_serverId_fk", + "tableFrom": "compose", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registry": { + "name": "registry", + "schema": "", + "columns": { + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registryName": { + "name": "registryName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imagePrefix": { + "name": "imagePrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "selfHosted": { + "name": "selfHosted", + "type": "RegistryType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "registry_organizationId_organization_id_fk": { + "name": "registry_organizationId_organization_id_fk", + "tableFrom": "registry", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord": { + "name": "discord", + "schema": "", + "columns": { + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email": { + "name": "email", + "schema": "", + "columns": { + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "smtpServer": { + "name": "smtpServer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "smtpPort": { + "name": "smtpPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gotify": { + "name": "gotify", + "schema": "", + "columns": { + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appToken": { + "name": "appToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "notificationId": { + "name": "notificationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appDeploy": { + "name": "appDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appBuildError": { + "name": "appBuildError", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "databaseBackup": { + "name": "databaseBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployRestart": { + "name": "dokployRestart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerCleanup": { + "name": "dockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "serverThreshold": { + "name": "serverThreshold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notificationType": { + "name": "notificationType", + "type": "notificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "notification_slackId_slack_slackId_fk": { + "name": "notification_slackId_slack_slackId_fk", + "tableFrom": "notification", + "tableTo": "slack", + "columnsFrom": [ + "slackId" + ], + "columnsTo": [ + "slackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_telegramId_telegram_telegramId_fk": { + "name": "notification_telegramId_telegram_telegramId_fk", + "tableFrom": "notification", + "tableTo": "telegram", + "columnsFrom": [ + "telegramId" + ], + "columnsTo": [ + "telegramId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_discordId_discord_discordId_fk": { + "name": "notification_discordId_discord_discordId_fk", + "tableFrom": "notification", + "tableTo": "discord", + "columnsFrom": [ + "discordId" + ], + "columnsTo": [ + "discordId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_emailId_email_emailId_fk": { + "name": "notification_emailId_email_emailId_fk", + "tableFrom": "notification", + "tableTo": "email", + "columnsFrom": [ + "emailId" + ], + "columnsTo": [ + "emailId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_gotifyId_gotify_gotifyId_fk": { + "name": "notification_gotifyId_gotify_gotifyId_fk", + "tableFrom": "notification", + "tableTo": "gotify", + "columnsFrom": [ + "gotifyId" + ], + "columnsTo": [ + "gotifyId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_organizationId_organization_id_fk": { + "name": "notification_organizationId_organization_id_fk", + "tableFrom": "notification", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack": { + "name": "slack", + "schema": "", + "columns": { + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telegram": { + "name": "telegram", + "schema": "", + "columns": { + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "botToken": { + "name": "botToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "messageThreadId": { + "name": "messageThreadId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh-key": { + "name": "ssh-key", + "schema": "", + "columns": { + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ssh-key_organizationId_organization_id_fk": { + "name": "ssh-key_organizationId_organization_id_fk", + "tableFrom": "ssh-key", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_provider": { + "name": "git_provider", + "schema": "", + "columns": { + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "gitProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "git_provider_organizationId_organization_id_fk": { + "name": "git_provider_organizationId_organization_id_fk", + "tableFrom": "git_provider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bitbucket": { + "name": "bitbucket", + "schema": "", + "columns": { + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bitbucketUsername": { + "name": "bitbucketUsername", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "appPassword": { + "name": "appPassword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketWorkspaceName": { + "name": "bitbucketWorkspaceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bitbucket_gitProviderId_git_provider_gitProviderId_fk": { + "name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "bitbucket", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github": { + "name": "github", + "schema": "", + "columns": { + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "githubAppName": { + "name": "githubAppName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubAppId": { + "name": "githubAppId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "githubClientId": { + "name": "githubClientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubClientSecret": { + "name": "githubClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubInstallationId": { + "name": "githubInstallationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubPrivateKey": { + "name": "githubPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubWebhookSecret": { + "name": "githubWebhookSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "github_gitProviderId_git_provider_gitProviderId_fk": { + "name": "github_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "github", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitlab": { + "name": "gitlab", + "schema": "", + "columns": { + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "gitlabUrl": { + "name": "gitlabUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitlab.com'" + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "gitlab_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitlab_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitlab", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitea": { + "name": "gitea", + "schema": "", + "columns": { + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "giteaUrl": { + "name": "giteaUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitea.com'" + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'repo,repo:status,read:user,read:org'" + }, + "last_authenticated_at": { + "name": "last_authenticated_at", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "gitea_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitea_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitea", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server": { + "name": "server", + "schema": "", + "columns": { + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'root'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverStatus": { + "name": "serverStatus", + "type": "serverStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Remote\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"urlCallback\":\"\",\"cronJob\":\"\",\"retentionDays\":2,\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "server_organizationId_organization_id_fk": { + "name": "server_organizationId_organization_id_fk", + "tableFrom": "server", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "server_sshKeyId_ssh-key_sshKeyId_fk": { + "name": "server_sshKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "server", + "tableTo": "ssh-key", + "columnsFrom": [ + "sshKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preview_deployments": { + "name": "preview_deployments", + "schema": "", + "columns": { + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestId": { + "name": "pullRequestId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestNumber": { + "name": "pullRequestNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestURL": { + "name": "pullRequestURL", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestTitle": { + "name": "pullRequestTitle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestCommentId": { + "name": "pullRequestCommentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previewStatus": { + "name": "previewStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "preview_deployments_applicationId_application_applicationId_fk": { + "name": "preview_deployments_applicationId_application_applicationId_fk", + "tableFrom": "preview_deployments", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "preview_deployments_domainId_domain_domainId_fk": { + "name": "preview_deployments_domainId_domain_domainId_fk", + "tableFrom": "preview_deployments", + "tableTo": "domain", + "columnsFrom": [ + "domainId" + ], + "columnsTo": [ + "domainId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "preview_deployments_appName_unique": { + "name": "preview_deployments_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai": { + "name": "ai", + "schema": "", + "columns": { + "aiId": { + "name": "aiId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiUrl": { + "name": "apiUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isEnabled": { + "name": "isEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ai_organizationId_organization_id_fk": { + "name": "ai_organizationId_organization_id_fk", + "tableFrom": "ai", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is2FAEnabled": { + "name": "is2FAEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "resetPasswordToken": { + "name": "resetPasswordToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resetPasswordExpiresAt": { + "name": "resetPasswordExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationToken": { + "name": "confirmationToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationExpiresAt": { + "name": "confirmationExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_temp_id_fk": { + "name": "account_user_id_user_temp_id_fk", + "tableFrom": "account", + "tableTo": "user_temp", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apikey_user_id_user_temp_id_fk": { + "name": "apikey_user_id_user_temp_id_fk", + "tableFrom": "apikey", + "tableTo": "user_temp", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_temp_id_fk": { + "name": "invitation_inviter_id_user_temp_id_fk", + "tableFrom": "invitation", + "tableTo": "user_temp", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canCreateProjects": { + "name": "canCreateProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToSSHKeys": { + "name": "canAccessToSSHKeys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateServices": { + "name": "canCreateServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteProjects": { + "name": "canDeleteProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteServices": { + "name": "canDeleteServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToDocker": { + "name": "canAccessToDocker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToAPI": { + "name": "canAccessToAPI", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToGitProviders": { + "name": "canAccessToGitProviders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToTraefikFiles": { + "name": "canAccessToTraefikFiles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_temp_id_fk": { + "name": "member_user_id_user_temp_id_fk", + "tableFrom": "member", + "tableTo": "user_temp", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_owner_id_user_temp_id_fk": { + "name": "organization_owner_id_user_temp_id_fk", + "tableFrom": "organization", + "tableTo": "user_temp", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_temp_id_fk": { + "name": "two_factor_user_id_user_temp_id_fk", + "tableFrom": "two_factor", + "tableTo": "user_temp", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shellType": { + "name": "shellType", + "type": "shellType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'bash'" + }, + "scheduleType": { + "name": "scheduleType", + "type": "scheduleType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "script": { + "name": "script", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_applicationId_application_applicationId_fk": { + "name": "schedule_applicationId_application_applicationId_fk", + "tableFrom": "schedule", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_composeId_compose_composeId_fk": { + "name": "schedule_composeId_compose_composeId_fk", + "tableFrom": "schedule", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_serverId_server_serverId_fk": { + "name": "schedule_serverId_server_serverId_fk", + "tableFrom": "schedule", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_userId_user_temp_id_fk": { + "name": "schedule_userId_user_temp_id_fk", + "tableFrom": "schedule", + "tableTo": "user_temp", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_storage_destination": { + "name": "cloud_storage_destination", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_storage_backup": { + "name": "cloud_storage_backup", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cloud_storage_destination_id": { + "name": "cloud_storage_destination_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "database_type": { + "name": "database_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgres_id": { + "name": "postgres_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysql_id": { + "name": "mysql_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadb_id": { + "name": "mariadb_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongo_id": { + "name": "mongo_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "keep_latest_count": { + "name": "keep_latest_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cloud_storage_backup_cloud_storage_destination_id_cloud_storage_destination_id_fk": { + "name": "cloud_storage_backup_cloud_storage_destination_id_cloud_storage_destination_id_fk", + "tableFrom": "cloud_storage_backup", + "tableTo": "cloud_storage_destination", + "columnsFrom": [ + "cloud_storage_destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.buildType": { + "name": "buildType", + "schema": "public", + "values": [ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static", + "railpack" + ] + }, + "public.sourceType": { + "name": "sourceType", + "schema": "public", + "values": [ + "docker", + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "drop" + ] + }, + "public.domainType": { + "name": "domainType", + "schema": "public", + "values": [ + "compose", + "application", + "preview" + ] + }, + "public.backupType": { + "name": "backupType", + "schema": "public", + "values": [ + "database", + "compose" + ] + }, + "public.databaseType": { + "name": "databaseType", + "schema": "public", + "values": [ + "postgres", + "mariadb", + "mysql", + "mongo", + "web-server" + ] + }, + "public.deploymentStatus": { + "name": "deploymentStatus", + "schema": "public", + "values": [ + "running", + "done", + "error" + ] + }, + "public.mountType": { + "name": "mountType", + "schema": "public", + "values": [ + "bind", + "volume", + "file" + ] + }, + "public.serviceType": { + "name": "serviceType", + "schema": "public", + "values": [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "compose" + ] + }, + "public.protocolType": { + "name": "protocolType", + "schema": "public", + "values": [ + "tcp", + "udp" + ] + }, + "public.applicationStatus": { + "name": "applicationStatus", + "schema": "public", + "values": [ + "idle", + "running", + "done", + "error" + ] + }, + "public.certificateType": { + "name": "certificateType", + "schema": "public", + "values": [ + "letsencrypt", + "none", + "custom" + ] + }, + "public.triggerType": { + "name": "triggerType", + "schema": "public", + "values": [ + "push", + "tag" + ] + }, + "public.composeType": { + "name": "composeType", + "schema": "public", + "values": [ + "docker-compose", + "stack" + ] + }, + "public.sourceTypeCompose": { + "name": "sourceTypeCompose", + "schema": "public", + "values": [ + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "raw" + ] + }, + "public.RegistryType": { + "name": "RegistryType", + "schema": "public", + "values": [ + "selfHosted", + "cloud" + ] + }, + "public.notificationType": { + "name": "notificationType", + "schema": "public", + "values": [ + "slack", + "telegram", + "discord", + "email", + "gotify" + ] + }, + "public.gitProviderType": { + "name": "gitProviderType", + "schema": "public", + "values": [ + "github", + "gitlab", + "bitbucket", + "gitea" + ] + }, + "public.serverStatus": { + "name": "serverStatus", + "schema": "public", + "values": [ + "active", + "inactive" + ] + }, + "public.scheduleType": { + "name": "scheduleType", + "schema": "public", + "values": [ + "application", + "compose", + "server", + "dokploy-server" + ] + }, + "public.shellType": { + "name": "shellType", + "schema": "public", + "values": [ + "bash", + "sh" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index 1d04d3c6..6d7f2e9b 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -652,6 +652,13 @@ "when": 1747713229160, "tag": "0092_stiff_the_watchers", "breakpoints": true + }, + { + "idx": 93, + "version": "7", + "when": 1748419547200, + "tag": "0093_pale_guardsmen", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dokploy/server/api/root.ts b/apps/dokploy/server/api/root.ts index 68cba6bd..d86ce9ce 100644 --- a/apps/dokploy/server/api/root.ts +++ b/apps/dokploy/server/api/root.ts @@ -5,6 +5,8 @@ import { applicationRouter } from "./routers/application"; import { backupRouter } from "./routers/backup"; import { bitbucketRouter } from "./routers/bitbucket"; import { certificateRouter } from "./routers/certificate"; +import { cloudStorageBackupRouter } from "./routers/cloud-storage-backup"; +import { cloudStorageDestinationRouter } from "./routers/cloud-storage-destination"; import { clusterRouter } from "./routers/cluster"; import { composeRouter } from "./routers/compose"; import { deploymentRouter } from "./routers/deployment"; @@ -57,6 +59,7 @@ export const appRouter = createTRPCRouter({ domain: domainRouter, destination: destinationRouter, backup: backupRouter, + cloudStorageBackup: cloudStorageBackupRouter, deployment: deploymentRouter, previewDeployment: previewDeploymentRouter, mounts: mountRouter, @@ -80,6 +83,7 @@ export const appRouter = createTRPCRouter({ ai: aiRouter, organization: organizationRouter, schedule: scheduleRouter, + cloudStorageDestination: cloudStorageDestinationRouter, }); // export type definition of API diff --git a/apps/dokploy/server/api/routers/cloud-storage-backup.ts b/apps/dokploy/server/api/routers/cloud-storage-backup.ts new file mode 100644 index 00000000..40e91e55 --- /dev/null +++ b/apps/dokploy/server/api/routers/cloud-storage-backup.ts @@ -0,0 +1,626 @@ +import { writeFileSync } from "node:fs"; +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { db } from "@/server/db"; +import { cloudStorageBackup } from "@dokploy/server/db/schema/cloud-storage-backup"; +import { cloudStorageDestination } from "@dokploy/server/db/schema/cloud-storage-destination"; +import { generateProviderConfig } from "@dokploy/server/db/schema/cloud-storage-destination"; +import { cloudStorageBackupService } from "@dokploy/server/services/cloud-storage-backup"; +import { + type CloudStorageProvider, + getRcloneConfigPath, + normalizeCloudPath, +} from "@dokploy/server/utils/backups/cloud-storage"; +import { + executeCloudStorageBackup, + executeCloudStorageRestore, +} from "@dokploy/server/utils/backups/cloud-storage"; +import { + execAsync, + execAsyncRemote, +} from "@dokploy/server/utils/process/execAsync"; +import { TRPCError } from "@trpc/server"; +import { observable } from "@trpc/server/observable"; +import { eq } from "drizzle-orm"; +import { z } from "zod"; + +const listBackupFilesInput = z.object({ + destinationId: z.string(), + prefix: z.string().optional(), + searchTerm: z.string().optional(), + serverId: z.string().optional(), +}); + +const createBackupInput = z.object({ + schedule: z.string(), + enabled: z.boolean(), + databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]), + cloudStorageDestinationId: z.string(), + prefix: z.string().optional(), + database: z.string().optional(), + postgresId: z.string().optional(), + mysqlId: z.string().optional(), + mariadbId: z.string().optional(), + mongoId: z.string().optional(), +}); + +const updateBackupInput = z.object({ + backupId: z.string(), + schedule: z.string().optional(), + enabled: z.boolean().optional(), + databaseType: z + .enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]) + .optional(), + cloudStorageDestinationId: z.string().optional(), + prefix: z.string().optional(), + database: z.string().optional(), + postgresId: z.string().optional(), + mysqlId: z.string().optional(), + mariadbId: z.string().optional(), + mongoId: z.string().optional(), + keepLatestCount: z.number().optional(), +}); + +const manualBackupInput = z.object({ + backupId: z.string(), +}); + +// Helper to perform an operation with silent token refresh for OAuth providers +async function withSilentTokenRefresh(params: { + ctx: any; + destination: any; + operationFn: (destination: any) => Promise; + updateConfig?: boolean; +}): Promise { + const { ctx, destination, operationFn, updateConfig = true } = params; + const provider = destination.provider; + if (["drive", "dropbox", "box"].includes(provider)) { + let credentials: Record = {}; + try { + credentials = destination.config ? JSON.parse(destination.config) : {}; + } catch {} + const configPath = getRcloneConfigPath( + ctx.session.activeOrganizationId, + destination.id, + ); + + // Use existing token if available + if (credentials.token) { + try { + const configContent = generateProviderConfig(provider, credentials); + writeFileSync(configPath, configContent); + return await operationFn(destination); + } catch (err: any) { + if ( + err.stderr && + (err.stderr.includes("token expired") || + err.stderr.includes("invalid_grant") || + err.stderr.includes("unauthorized") || + err.stderr.includes("401") || + err.stderr.includes("403")) + ) { + console.log("Token expired or invalid, attempting refresh"); + credentials.token = undefined; + } else { + throw err; + } + } + } + + console.log("Getting new token through OAuth flow"); + const { + testCloudStorageConnection, + } = require("./cloud-storage-destination"); + const result = await testCloudStorageConnection({ + provider, + credentials, + destinationId: destination.id, + ctx, + }); + + if (result.token) { + credentials.token = result.token; + if (updateConfig) { + await db + .update(cloudStorageDestination) + .set({ config: JSON.stringify(credentials), updatedAt: new Date() }) + .where(eq(cloudStorageDestination.id, destination.id)); + } + const configContent = generateProviderConfig(provider, credentials); + writeFileSync(configPath, configContent); + return await operationFn({ + ...destination, + config: JSON.stringify(credentials), + }); + } + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Failed to get new token. Please reconnect this destination.", + }); + } + return await operationFn(destination); +} + +export const cloudStorageBackupRouter = createTRPCRouter({ + listBackupFiles: protectedProcedure + .input(listBackupFilesInput) + .query(async ({ input, ctx }) => { + const { destinationId, prefix, searchTerm, serverId } = input; + + const [destination] = await db + .select() + .from(cloudStorageDestination) + .where(eq(cloudStorageDestination.id, destinationId)) + .limit(1); + + if (!destination) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Cloud storage destination not found", + }); + } + + if (destination.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this destination", + }); + } + + if (!destination.provider) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Destination is not a valid cloud storage provider", + }); + } + + const provider = destination.provider as CloudStorageProvider; + + // Use withSilentTokenRefresh to handle OAuth token refresh + return await withSilentTokenRefresh({ + ctx, + destination, + operationFn: async (dest) => { + const configPath = getRcloneConfigPath( + ctx.session.activeOrganizationId, + dest.id, + ); + + let credentials: Record; + try { + credentials = dest.config ? JSON.parse(dest.config) : {}; + } catch (_e) { + credentials = {}; + } + + const configContent = generateProviderConfig(provider, credentials); + writeFileSync(configPath, configContent); + + const normalizedPrefix = + prefix === "/" ? "" : normalizeCloudPath(prefix || ""); + + const rcloneRemote = provider; + + const command = `rclone lsjson --config=${configPath} "${rcloneRemote}:${normalizedPrefix}"`; + console.log("Executing rclone command:", command); + + try { + const { stdout } = serverId + ? await execAsyncRemote(serverId, command) + : await execAsync(command); + + console.log("Rclone output:", stdout); + + const files = JSON.parse(stdout) as Array<{ + Path: string; + Name: string; + Size: number; + ModTime: string; + IsDir: boolean; + Hashes: { MD5: string }; + }>; + + console.log("Parsed files:", files); + + const backupFiles = files + .filter((file) => + file.Name.endsWith(".zip") || + file.Name.endsWith(".sql.gz") + ) + .map((file) => ({ + path: file.Path, + name: file.Name, + size: file.Size, + lastModified: new Date(file.ModTime), + isDir: file.IsDir, + hashes: file.Hashes, + })); + + console.log("Filtered backup files:", backupFiles); + + if (searchTerm) { + const filteredFiles = backupFiles.filter((file) => + file.name.toLowerCase().includes(searchTerm.toLowerCase()), + ); + console.log("Search filtered files:", filteredFiles); + return filteredFiles; + } + + return backupFiles; + } catch (error: any) { + if (error.stderr?.includes("directory not found")) { + console.log("Directory not found, returning empty array"); + return []; + } + console.error("Error listing cloud storage backup files:", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to list cloud storage backup files", + cause: error, + }); + } + }, + }); + }), + + create: protectedProcedure + .input(createBackupInput) + .mutation(async ({ input, ctx }) => { + const { cloudStorageDestinationId, ...data } = input; + + const [destination] = await db + .select() + .from(cloudStorageDestination) + .where(eq(cloudStorageDestination.id, cloudStorageDestinationId)) + .limit(1); + + if (!destination) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Cloud storage destination not found", + }); + } + + if (destination.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this destination", + }); + } + + const backup = await cloudStorageBackupService.createBackup({ + ...data, + cloudStorageDestinationId, + organizationId: ctx.session.activeOrganizationId, + }); + + return backup; + }), + + update: protectedProcedure + .input(updateBackupInput) + .mutation(async ({ input, ctx }) => { + const { backupId, cloudStorageDestinationId, ...data } = input; + + if (cloudStorageDestinationId) { + const [destination] = await db + .select() + .from(cloudStorageDestination) + .where(eq(cloudStorageDestination.id, cloudStorageDestinationId)) + .limit(1); + + if (!destination) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Cloud storage destination not found", + }); + } + + if (destination.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this destination", + }); + } + } + + return await cloudStorageBackupService.updateBackup(backupId, { + ...data, + cloudStorageDestinationId, + }); + }), + + remove: protectedProcedure + .input(z.object({ backupId: z.string() })) + .mutation(async ({ input }) => { + return await cloudStorageBackupService.removeBackup(input.backupId); + }), + + list: protectedProcedure.query(async () => { + const backups = await db.query.cloudStorageBackup.findMany({ + with: { + cloudStorageDestination: true, + }, + }); + return backups; + }), + + get: protectedProcedure + .input(z.object({ backupId: z.string() })) + .query(async ({ input }) => { + const backup = await db.query.cloudStorageBackup.findFirst({ + where: eq(cloudStorageBackup.id, input.backupId), + with: { + cloudStorageDestination: true, + }, + }); + + if (!backup) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Backup not found", + }); + } + + return backup; + }), + + manualBackup: protectedProcedure + .input(manualBackupInput) + .mutation(async ({ input, ctx }) => { + const { backupId } = input; + const backup = await cloudStorageBackupService.findBackupById(backupId); + if (backup.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this backup", + }); + } + const [destination] = await db + .select() + .from(cloudStorageDestination) + .where(eq(cloudStorageDestination.id, backup.cloudStorageDestinationId)) + .limit(1); + if (!destination) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Cloud storage destination not found", + }); + } + if (destination.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this destination", + }); + } + const doBackup = async (dest: any) => { + switch (backup.databaseType) { + case "web-server": + return await executeCloudStorageBackup( + { ...backup, webServer: { path: backup.database || "" } }, + dest, + ); + case "postgres": + return await executeCloudStorageBackup( + { + ...backup, + postgres: { + host: "localhost", + port: 5432, + user: "postgres", + password: "", + }, + }, + dest, + ); + case "mysql": + return await executeCloudStorageBackup( + { + ...backup, + mysql: { + host: "localhost", + port: 3306, + user: "root", + password: "", + }, + }, + dest, + ); + case "mariadb": + return await executeCloudStorageBackup( + { + ...backup, + mariadb: { + host: "localhost", + port: 3306, + user: "root", + password: "", + }, + }, + dest, + ); + case "mongo": + return await executeCloudStorageBackup( + { + ...backup, + mongo: { + host: "localhost", + port: 27017, + user: "", + password: "", + }, + }, + dest, + ); + default: + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Unsupported database type for manual backup", + }); + } + }; + try { + await withSilentTokenRefresh({ + ctx, + destination, + operationFn: doBackup, + }); + return { success: true }; + } catch (error) { + console.error("Manual backup error:", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error + ? error.message + : "Failed to execute manual backup", + cause: error, + }); + } + }), + + restore: protectedProcedure + .input( + z.object({ + destinationId: z.string(), + backupFile: z.string(), + databaseType: z + .enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]) + .optional(), + databaseName: z.string().optional(), + metadata: z.any().optional(), + }), + ) + .mutation(async ({ input, ctx }) => { + const destination = await db.query.cloudStorageDestination.findFirst({ + where: eq(cloudStorageDestination.id, input.destinationId), + }); + if (!destination) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Destination not found", + }); + } + const backup = await db.query.cloudStorageBackup.findFirst({ + where: eq( + cloudStorageBackup.cloudStorageDestinationId, + input.destinationId, + ), + }); + if (!backup) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "No backup found for this destination", + }); + } + + const configPath = getRcloneConfigPath( + ctx.session.activeOrganizationId, + destination.id, + ); + + let credentials: Record; + try { + credentials = destination.config ? JSON.parse(destination.config) : {}; + } catch (_e) { + credentials = {}; + } + + const configContent = generateProviderConfig( + destination.provider as CloudStorageProvider, + credentials, + ); + writeFileSync(configPath, configContent); + + const doRestore = async (dest: any) => { + const backupWithConfig = { + ...backup, + webServer: + input.databaseType === "web-server" + ? { path: input.databaseName || "/var/www/html" } + : undefined, + postgres: + input.databaseType === "postgres" + ? { + host: "localhost", + port: 5432, + user: input.metadata?.postgres?.databaseUser || "postgres", + password: "", + } + : undefined, + mysql: + input.databaseType === "mysql" + ? { + host: "localhost", + port: 3306, + user: "root", + password: input.metadata?.mysql?.databaseRootPassword || "", + } + : undefined, + mariadb: + input.databaseType === "mariadb" + ? { + host: "localhost", + port: 3306, + user: input.metadata?.mariadb?.databaseUser || "root", + password: input.metadata?.mariadb?.databasePassword || "", + } + : undefined, + mongo: + input.databaseType === "mongo" + ? { + host: "localhost", + port: 27017, + user: input.metadata?.mongo?.databaseUser || "", + password: input.metadata?.mongo?.databasePassword || "", + } + : undefined, + }; + return await executeCloudStorageRestore( + backupWithConfig, + dest, + input.backupFile, + () => {}, + ); + }; + try { + await withSilentTokenRefresh({ + ctx, + destination, + operationFn: doRestore, + }); + return { success: true }; + } catch (error) { + console.error("Restore error:", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error ? error.message : "Failed to restore backup", + cause: error, + }); + } + }), + + restoreBackupWithLogs: protectedProcedure + .input( + z.object({ + databaseId: z.string(), + databaseType: z.enum([ + "postgres", + "mariadb", + "mysql", + "mongo", + "web-server", + ]), + databaseName: z.string(), + backupFile: z.string(), + destinationId: z.string(), + metadata: z.any().optional(), + }), + ) + .subscription(async () => { + return observable((_emit) => { + // Implementation will be added + return () => {}; + }); + }), +}); diff --git a/apps/dokploy/server/api/routers/cloud-storage-destination.ts b/apps/dokploy/server/api/routers/cloud-storage-destination.ts new file mode 100644 index 00000000..1445088e --- /dev/null +++ b/apps/dokploy/server/api/routers/cloud-storage-destination.ts @@ -0,0 +1,642 @@ +import { spawn } from "node:child_process"; +import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import { join } from "node:path"; +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { db } from "@/server/db"; +import { cloudStorageBackup } from "@dokploy/server/db/schema/cloud-storage-backup"; +import { + cloudStorageDestination, + generateProviderConfig, + storeCredentials, +} from "@dokploy/server/db/schema/cloud-storage-destination"; +import { + CloudStorageProviderEnum, + apiCreateCloudStorageDestination, + apiDeleteCloudStorageDestination, + apiUpdateCloudStorageDestination, +} from "@dokploy/server/db/schema/cloud-storage-destination"; +import { execAsync } from "@dokploy/server/utils/process/execAsync"; +import { TRPCError } from "@trpc/server"; +import { observable } from "@trpc/server/observable"; +import { and, desc, eq } from "drizzle-orm"; +import { z } from "zod"; + +// Function to kill any existing rclone processes +async function killExistingRcloneProcesses() { + try { + const { stdout } = await execAsync("pgrep -f rclone"); + const pids = stdout.trim().split("\n").filter(Boolean); + + for (const pid of pids) { + try { + await execAsync(`kill -9 ${pid}`); + } catch (error) { + console.error(`Failed to kill process ${pid}:`, error); + } + } + } catch (_error) { + console.log("No existing rclone processes found"); + } +} + +// Function to create rclone config path +function getRcloneConfigPath( + organizationId: string, + destinationId: string, +): string { + const configDir = join( + os.homedir(), + ".dokploy", + "rclone", + "cloud", + organizationId, + ); + + if (!existsSync(configDir)) { + mkdirSync(configDir, { recursive: true }); + } + + return join(configDir, `${destinationId}.conf`); +} + +// Helper function to run rclone authorize command +async function runRcloneAuthorize(providerType: string): Promise { + try { + const tempConfigPath = join(os.tmpdir(), `rclone-auth-${Date.now()}.conf`); + const configContent = `[${providerType}] +no_browser = true`; + writeFileSync(tempConfigPath, configContent); + + try { + const { stdout } = await execAsync( + `rclone --config ${tempConfigPath} authorize "${providerType}"`, + ); + console.log("Rclone output:", stdout); + + const tokenMatch = stdout.match(/\{[\s\S]*?\}/); + if (tokenMatch) { + return tokenMatch[0]; + } + throw new Error("Could not find token in rclone output"); + } finally { + if (existsSync(tempConfigPath)) { + unlinkSync(tempConfigPath); + } + } + } catch (error) { + console.error("Failed to run rclone authorize:", error); + throw error; + } +} + +// Add this helper function near the top of the file +async function testCloudStorageConnection({ + provider, + credentials, +}: { provider: string; credentials: any }) { + if (["drive", "dropbox", "box"].includes(provider)) { + if (!credentials.token) { + const providerType = + provider === "drive" + ? "drive" + : provider === "dropbox" + ? "dropbox" + : "box"; + try { + console.log("Starting OAuth flow for provider:", providerType); + await killExistingRcloneProcesses(); + let lastError: Error | undefined; + for (let attempt = 0; attempt < 3; attempt++) { + try { + const token = await runRcloneAuthorize(providerType); + console.log("Got token from OAuth flow"); + try { + const parsedToken = JSON.parse(token); + if (!parsedToken.access_token) { + throw new Error("Invalid token format: missing access_token"); + } + credentials.token = token; + console.log("Token validated successfully"); + break; + } catch (parseError) { + console.error("Token parse error:", parseError); + console.error("Token string:", token); + throw new Error( + "Failed to parse the OAuth token. Please try authenticating again.", + ); + } + } catch (error: unknown) { + lastError = + error instanceof Error ? error : new Error(String(error)); + console.error(`Attempt ${attempt + 1} failed:`, error); + if ( + error instanceof Error && + error.message.includes("address already in use") + ) { + await killExistingRcloneProcesses(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + continue; + } + throw error; + } + } + if (lastError) { + throw lastError; + } + } catch (authError) { + console.error("Authentication error:", authError); + throw new Error( + authError instanceof Error + ? authError.message + : "Authentication failed", + ); + } + } + } + // Create temporary config file + const configPath = join(os.tmpdir(), `test-${Date.now()}.conf`); + try { + const configContent = generateProviderConfig(provider, credentials); + writeFileSync(configPath, configContent); + await execAsync(`rclone lsd --config=${configPath} "${provider}:"`); + return { + success: true, + token: ["drive", "dropbox", "box"].includes(provider) + ? credentials.token + : undefined, + }; + } finally { + if (existsSync(configPath)) { + unlinkSync(configPath); + } + } +} + +export const cloudStorageDestinationRouter = createTRPCRouter({ + all: protectedProcedure.query(async ({ ctx }) => { + try { + const destinations = await db + .select() + .from(cloudStorageDestination) + .where( + eq( + cloudStorageDestination.organizationId, + ctx.session.activeOrganizationId, + ), + ) + .orderBy(desc(cloudStorageDestination.createdAt)); + + return destinations; + } catch (error) { + console.error("Error fetching destinations:", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Error fetching cloud storage destinations", + }); + } + }), + + create: protectedProcedure + .input(apiCreateCloudStorageDestination) + .mutation(async ({ input, ctx }) => { + try { + const { provider, name, config } = input; + + const credentials = JSON.parse(config); + + const storedConfig = storeCredentials(provider, credentials); + + const [newDestination] = await db + .insert(cloudStorageDestination) + .values({ + name, + provider, + organizationId: ctx.session.activeOrganizationId, + config: storedConfig, + }) + .returning(); + + if (!newDestination) { + throw new Error("Failed to create cloud storage destination"); + } + + const configPath = getRcloneConfigPath( + ctx.session.activeOrganizationId, + newDestination.id, + ); + + const configContent = generateProviderConfig(provider, credentials); + writeFileSync(configPath, configContent); + + return newDestination; + } catch (error) { + console.error("Create error:", error); + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error + ? error.message + : "Error creating destination", + }); + } + }), + + update: protectedProcedure + .input(apiUpdateCloudStorageDestination) + .mutation(async ({ input, ctx }) => { + try { + const { destinationId, provider, name, config } = input; + + let credentials: any = {}; + try { + credentials = JSON.parse(config); + } catch {} + + const [updatedDestination] = await db + .update(cloudStorageDestination) + .set({ + name, + provider, + config, + host: credentials.host || null, + port: credentials.port || null, + username: credentials.username || null, + password: credentials.password || null, + updatedAt: new Date(), + }) + .where( + and( + eq(cloudStorageDestination.id, destinationId), + eq( + cloudStorageDestination.organizationId, + ctx.session.activeOrganizationId, + ), + ), + ) + .returning(); + + if (!updatedDestination) { + throw new Error("Cloud storage destination not found"); + } + + const configPath = getRcloneConfigPath( + ctx.session.activeOrganizationId, + updatedDestination.id, + ); + + const configContent = generateProviderConfig( + updatedDestination.provider, + credentials, + ); + writeFileSync(configPath, configContent); + + return updatedDestination; + } catch (error) { + console.error("Update error:", error); + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error + ? error.message + : "Error updating destination", + }); + } + }), + + // Delete a cloud storage destination + delete: protectedProcedure + .input(apiDeleteCloudStorageDestination) + .mutation(async ({ input, ctx }) => { + try { + const associatedBackups = await db + .select() + .from(cloudStorageBackup) + .where( + and( + eq( + cloudStorageBackup.cloudStorageDestinationId, + input.destinationId, + ), + eq( + cloudStorageBackup.organizationId, + ctx.session.activeOrganizationId, + ), + ), + ); + + if (associatedBackups.length > 0) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + "Cannot delete destination with associated backups. Please delete the backups first.", + }); + } + + const deletedDestination = await db + .delete(cloudStorageDestination) + .where( + and( + eq(cloudStorageDestination.id, input.destinationId), + eq( + cloudStorageDestination.organizationId, + ctx.session.activeOrganizationId, + ), + ), + ) + .returning(); + + if (!deletedDestination.length) { + throw new Error("Cloud storage destination not found"); + } + + const configPath = getRcloneConfigPath( + ctx.session.activeOrganizationId, + input.destinationId, + ); + + if (existsSync(configPath)) { + unlinkSync(configPath); + } + + return deletedDestination[0]; + } catch (error) { + console.error("Delete error:", error); + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error + ? error.message + : "Error deleting destination", + }); + } + }), + + getStorageMetrics: protectedProcedure + .input(z.object({ destinationId: z.string() })) + .query(async ({ input, ctx }) => { + try { + const destinations = await db + .select() + .from(cloudStorageDestination) + .where( + and( + eq(cloudStorageDestination.id, input.destinationId), + eq( + cloudStorageDestination.organizationId, + ctx.session.activeOrganizationId, + ), + ), + ) + .limit(1); + + const destination = destinations[0]; + if (!destination) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Cloud storage destination not found", + }); + } + + const configPath = getRcloneConfigPath( + ctx.session.activeOrganizationId, + input.destinationId, + ); + + const result = await execAsync( + `rclone about --config=${configPath} "${destination.provider}:" --json`, + ); + + return JSON.parse(result.stdout); + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to get storage metrics", + cause: error, + }); + } + }), + + authorizeOAuth: protectedProcedure + .input( + z.object({ + provider: CloudStorageProviderEnum, + }), + ) + .mutation(async ({ input }) => { + try { + if (input.provider !== "drive") { + throw new Error( + "OAuth authorization is only supported for Google Drive", + ); + } + + const result = await execAsync('rclone authorize "drive" | cat'); + + const tokenMatch = result.stdout.match(/\{.*\}/); + if (!tokenMatch) { + throw new Error("Failed to extract OAuth token from rclone output"); + } + + const token = tokenMatch[0]; + + try { + const parsedToken = JSON.parse(token); + if (!parsedToken.access_token || !parsedToken.refresh_token) { + throw new Error("Invalid token format"); + } + } catch { + throw new Error("Invalid token JSON"); + } + + return { token }; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error + ? error.message + : "Failed to authorize with provider", + }); + } + }), + + testConnection: protectedProcedure + .input( + z.object({ + provider: CloudStorageProviderEnum, + credentials: z.record(z.any()), + destinationId: z.string().optional(), + }), + ) + .mutation(async ({ input }) => { + try { + return await testCloudStorageConnection({ + provider: input.provider, + credentials: input.credentials, + }); + } catch (error) { + console.error("Test connection error:", error); + const errorMessage = + error instanceof Error ? error.message : "Connection test failed"; + throw new TRPCError({ + code: "BAD_REQUEST", + message: errorMessage, + }); + } + }), + + getTransferStatus: protectedProcedure + .input(z.object({ destinationId: z.string() })) + .subscription(async ({ input, ctx }) => { + const destinations = await db + .select() + .from(cloudStorageDestination) + .where( + and( + eq(cloudStorageDestination.id, input.destinationId), + eq( + cloudStorageDestination.organizationId, + ctx.session.activeOrganizationId, + ), + ), + ) + .limit(1); + + const destination = destinations[0]; + if (!destination) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Cloud storage destination not found", + }); + } + + const configPath = getRcloneConfigPath( + ctx.session.activeOrganizationId, + input.destinationId, + ); + + return observable((emit) => { + const process = spawn("rclone", [ + "rc", + "core/stats", + `--config=${configPath}`, + ]); + + process.stdout.on("data", (data: Buffer) => { + emit.next(data.toString()); + }); + + process.stderr.on("data", (data: Buffer) => { + console.error(`rclone error: ${data.toString()}`); + }); + + process.on("error", (error) => { + console.error("Failed to start rclone process:", error); + emit.error(error); + }); + + return () => { + process.kill(); + }; + }); + }), + + reconnect: protectedProcedure + .input(z.object({ destinationId: z.string() })) + .mutation(async ({ input, ctx }) => { + const destination = await db + .select() + .from(cloudStorageDestination) + .where( + and( + eq(cloudStorageDestination.id, input.destinationId), + eq( + cloudStorageDestination.organizationId, + ctx.session.activeOrganizationId, + ), + ), + ); + const dest = destination[0]; + if (!dest) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Destination not found", + }); + } + + let credentials: any = {}; + try { + credentials = dest.config ? JSON.parse(dest.config) : {}; + } catch {} + const provider = dest.provider; + if (["drive", "dropbox", "box"].includes(provider)) { + const configPath = getRcloneConfigPath( + ctx.session.activeOrganizationId, + dest.id, + ); + + // If there is a token, try silent refresh first + if (credentials.token) { + try { + const configContent = generateProviderConfig(provider, credentials); + writeFileSync(configPath, configContent); + await execAsync(`rclone lsd --config=${configPath} "${provider}:"`); + return { success: true, silent: true }; + } catch (err: any) { + if ( + err.stderr && + (err.stderr.includes("token expired") || + err.stderr.includes("invalid_grant") || + err.stderr.includes("unauthorized") || + err.stderr.includes("401") || + err.stderr.includes("403")) + ) { + console.log("Token expired or invalid, starting OAuth flow"); + credentials.token = undefined; + } else { + console.error("Reconnect error:", err); + throw new TRPCError({ + code: "BAD_REQUEST", + message: err.message || "Reconnect failed", + }); + } + } + } + + // If there is no token, start the OAuth flow + console.log("Starting full OAuth flow"); + const result = await testCloudStorageConnection({ + provider, + credentials, + }); + + if (result.token) { + credentials.token = result.token; + await db + .update(cloudStorageDestination) + .set({ config: JSON.stringify(credentials), updatedAt: new Date() }) + .where( + and( + eq(cloudStorageDestination.id, input.destinationId), + eq( + cloudStorageDestination.organizationId, + ctx.session.activeOrganizationId, + ), + ), + ); + const configContent = generateProviderConfig(provider, credentials); + writeFileSync(configPath, configContent); + return { success: true, silent: false }; + } + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Failed to get new token", + }); + } + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Reconnect is only supported for OAuth providers.", + }); + }), +}); diff --git a/packages/server/src/db/schema/cloud-storage-backup.ts b/packages/server/src/db/schema/cloud-storage-backup.ts new file mode 100644 index 00000000..b0a7dfdf --- /dev/null +++ b/packages/server/src/db/schema/cloud-storage-backup.ts @@ -0,0 +1,40 @@ +import { relations } from "drizzle-orm"; +import { + boolean, + integer, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; +import { cloudStorageDestination } from "./cloud-storage-destination"; + +export const cloudStorageBackup = pgTable("cloud_storage_backup", { + id: text("id").primaryKey(), + organizationId: text("organization_id").notNull(), + cloudStorageDestinationId: uuid("cloud_storage_destination_id") + .notNull() + .references(() => cloudStorageDestination.id), + schedule: text("schedule").notNull(), + enabled: boolean("enabled").notNull().default(true), + databaseType: text("database_type").notNull(), + prefix: text("prefix"), + database: text("database"), + postgresId: text("postgres_id"), + mysqlId: text("mysql_id"), + mariadbId: text("mariadb_id"), + mongoId: text("mongo_id"), + keepLatestCount: integer("keep_latest_count"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +export const cloudStorageBackupRelations = relations( + cloudStorageBackup, + ({ one }) => ({ + cloudStorageDestination: one(cloudStorageDestination, { + fields: [cloudStorageBackup.cloudStorageDestinationId], + references: [cloudStorageDestination.id], + }), + }), +); diff --git a/packages/server/src/db/schema/cloud-storage-destination.ts b/packages/server/src/db/schema/cloud-storage-destination.ts new file mode 100644 index 00000000..ad6b438b --- /dev/null +++ b/packages/server/src/db/schema/cloud-storage-destination.ts @@ -0,0 +1,125 @@ +import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { createInsertSchema, createSelectSchema } from "drizzle-zod"; +import { z } from "zod"; + +// Define cloud storage provider types +export const CloudStorageProviderEnum = z.enum([ + "drive", // Google Drive + "dropbox", // Dropbox + "box", // Box + "ftp", // FTP + "sftp", // SFTP +]); + +export type CloudStorageProvider = z.infer; + +// All providers use direct credentials +export function isCredentialProvider(provider: string): boolean { + return ["ftp", "sftp"].includes(provider); +} + +export function isOAuthProvider(provider: string): boolean { + return ["drive", "dropbox", "box"].includes(provider); +} + +// Define the cloud storage destination table +export const cloudStorageDestination = pgTable("cloud_storage_destination", { + id: uuid("id").primaryKey().defaultRandom(), + name: text("name").notNull(), + provider: text("provider").notNull(), + // Direct credential fields for FTP/SFTP + username: text("username"), // For FTP/SFTP + password: text("password"), // For FTP/SFTP + host: text("host"), // For FTP/SFTP + port: text("port"), // For FTP/SFTP + config: text("config"), // For OAuth token (Drive, Dropbox, Box) + organizationId: text("organization_id").notNull(), + createdAt: timestamp("created_at").defaultNow(), + updatedAt: timestamp("updated_at").defaultNow(), +}); + +// Create Zod schemas for validation +export const insertCloudStorageDestinationSchema = createInsertSchema( + cloudStorageDestination, +); +export const selectCloudStorageDestinationSchema = createSelectSchema( + cloudStorageDestination, +); + +// API schemas +export const apiCreateCloudStorageDestination = z.object({ + name: z.string().min(1, "Name is required"), + provider: CloudStorageProviderEnum, + config: z.string(), +}); + +export const apiUpdateCloudStorageDestination = + apiCreateCloudStorageDestination.extend({ + destinationId: z.string(), + }); + +export const apiFindOneCloudStorageDestination = z.object({ + destinationId: z.string(), +}); + +export const apiDeleteCloudStorageDestination = z.object({ + destinationId: z.string(), +}); + +// Form validation schema +export const cloudStorageDestinationSchema = z.object({ + providerType: CloudStorageProviderEnum, + host: z.string().optional(), + username: z.string().optional(), + password: z.string().optional(), + port: z.string().optional(), + token: z.string().optional(), +}); + +export type CloudStorageDestinationFormValues = z.infer< + typeof cloudStorageDestinationSchema +>; + +// Store raw credentials +export function storeCredentials( + provider: string, + credentials?: Record, +): string { + // For OAuth, just store the token + if (provider === "drive") { + return credentials?.token || ""; + } + + // For other providers, store credentials as is + return JSON.stringify(credentials || {}); +} + +// Generate rclone config +export function generateProviderConfig( + provider: string, + credentials?: Record, +): string { + switch (provider) { + case "ftp": + case "sftp": + return `[${provider}] +type = ${provider} +host = ${credentials?.host || ""} +user = ${credentials?.username || ""} +pass = ${credentials?.password || ""} +${credentials?.port ? `port = ${credentials?.port}` : ""}`; + + case "drive": + case "dropbox": + case "box": + if (!credentials?.token) { + throw new Error("OAuth token is required"); + } + return `[${provider}] +type = ${provider} +${provider === "drive" ? "scope = drive.file\n" : ""}token = ${credentials.token}`; + + default: + throw new Error(`Unsupported provider: ${provider}`); + } +} diff --git a/packages/server/src/db/schema/index.ts b/packages/server/src/db/schema/index.ts index e5c346cf..39404905 100644 --- a/packages/server/src/db/schema/index.ts +++ b/packages/server/src/db/schema/index.ts @@ -32,3 +32,5 @@ export * from "./preview-deployments"; export * from "./ai"; export * from "./account"; export * from "./schedule"; +export * from "./cloud-storage-destination"; +export * from "./cloud-storage-backup"; diff --git a/packages/server/src/services/cloud-storage-backup.ts b/packages/server/src/services/cloud-storage-backup.ts new file mode 100644 index 00000000..eb1619f6 --- /dev/null +++ b/packages/server/src/services/cloud-storage-backup.ts @@ -0,0 +1,104 @@ +import { randomUUID } from "node:crypto"; +import { db } from "@dokploy/server/db"; +import { cloudStorageBackup } from "@dokploy/server/db/schema/cloud-storage-backup"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; + +export type CloudStorageBackup = typeof cloudStorageBackup.$inferSelect; + +export const cloudStorageBackupService = { + createBackup: async (data: { + organizationId: string; + schedule: string; + enabled: boolean; + databaseType: string; + cloudStorageDestinationId: string; + prefix?: string; + database?: string; + postgresId?: string; + mysqlId?: string; + mariadbId?: string; + mongoId?: string; + }) => { + console.log("Creating backup with data:", data); + + const backupData = { + ...data, + id: randomUUID(), + }; + + console.log("Backup data to insert:", backupData); + + try { + const [backup] = await db + .insert(cloudStorageBackup) + .values(backupData) + .returning(); + + console.log("Successfully created backup:", backup); + return backup; + } catch (error) { + console.error("Error creating backup:", error); + throw error; + } + }, + + updateBackup: async ( + id: string, + data: { + schedule?: string; + enabled?: boolean; + databaseType?: string; + cloudStorageDestinationId?: string; + prefix?: string; + database?: string; + postgresId?: string; + mysqlId?: string; + mariadbId?: string; + mongoId?: string; + }, + ) => { + const [backup] = await db + .update(cloudStorageBackup) + .set(data) + .where(eq(cloudStorageBackup.id, id)) + .returning(); + + return backup; + }, + + removeBackup: async (id: string) => { + const [backup] = await db + .delete(cloudStorageBackup) + .where(eq(cloudStorageBackup.id, id)) + .returning(); + + return backup; + }, + + findBackupById: async (id: string) => { + const backup = await db.query.cloudStorageBackup.findFirst({ + where: eq(cloudStorageBackup.id, id), + with: { + cloudStorageDestination: true, + }, + }); + if (!backup) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Backup not found", + }); + } + return backup; + }, + + listBackups: async (organizationId: string) => { + console.log("Listing backups for organization:", organizationId); + const backups = await db + .select() + .from(cloudStorageBackup) + .where(eq(cloudStorageBackup.organizationId, organizationId)); + console.log("Found backups:", backups); + return backups; + }, +}; diff --git a/packages/server/src/services/cloud-storage-destination.ts b/packages/server/src/services/cloud-storage-destination.ts new file mode 100644 index 00000000..6561d94c --- /dev/null +++ b/packages/server/src/services/cloud-storage-destination.ts @@ -0,0 +1,185 @@ +import { eq } from "drizzle-orm"; +import { db } from "../db"; +import { + type CloudStorageProvider, + cloudStorageDestination, + isCredentialProvider, +} from "../db/schema/cloud-storage-destination"; + +interface Credential { + user: string; + pass: string; + host: string; +} + +/** + * Service for handling cloud storage destinations + */ +export class CloudStorageDestinationService { + /** + * Create a new cloud storage destination + */ + async create({ + name, + provider, + config, + organizationId, + }: { + name: string; + provider: CloudStorageProvider; + config: string; + organizationId: string; + }) { + try { + return await db + .insert(cloudStorageDestination) + .values({ + name, + provider, + config, + organizationId, + }) + .returning(); + } catch (error) { + console.error("Error creating cloud storage destination", { + error, + name, + provider, + }); + throw error; + } + } + + /** + * Update an existing cloud storage destination + */ + async update({ + destinationId, + name, + provider, + config, + }: { + destinationId: string; + name: string; + provider: CloudStorageProvider; + config: string; + }) { + try { + return await db + .update(cloudStorageDestination) + .set({ + name, + provider, + config, + }) + .where(eq(cloudStorageDestination.id, destinationId)) + .returning(); + } catch (error) { + console.error("Error updating cloud storage destination", { + error, + destinationId, + }); + throw error; + } + } + + /** + * Delete a cloud storage destination + */ + async delete(destinationId: string) { + try { + await db + .delete(cloudStorageDestination) + .where(eq(cloudStorageDestination.id, destinationId)); + + return { success: true }; + } catch (error) { + console.error("Error deleting cloud storage destination", { + error, + destinationId, + }); + throw error; + } + } + + /** + * Test connection to a cloud storage destination + */ + async testConnection({ + provider, + customConfig, + }: { + provider: CloudStorageProvider; + customConfig: string; + }) { + try { + // For credential-based providers + if (isCredentialProvider(provider)) { + if (provider === "drive") { + try { + const config = JSON.parse(customConfig); + if (!config.clientId || !config.clientSecret) { + throw new Error( + "Invalid OAuth configuration: missing client credentials", + ); + } + return { + success: true, + message: "Google Drive OAuth configuration is valid", + }; + } catch (_error) { + throw new Error("Invalid OAuth configuration format"); + } + } + + const credentials = this.parseCredentials(customConfig); + if (!credentials) { + throw new Error( + `Invalid ${provider.toUpperCase()} configuration: missing credentials`, + ); + } + + return { + success: true, + message: `${provider.toUpperCase()} configuration is valid`, + }; + } + + return { success: true, message: "Configuration validated" }; + } catch (error) { + console.error("Error testing cloud storage connection", { + error, + provider, + }); + throw error; + } + } + + /** + * Parse credentials from configuration string for FTP/SFTP + */ + private parseCredentials(configString: string): Credential | null { + try { + const lines = configString.split("\n"); + const hostLine = lines.find((line) => line.startsWith("host=")); + const userLine = lines.find((line) => line.startsWith("user=")); + const passLine = lines.find((line) => line.startsWith("pass=")); + + if (!hostLine || !userLine || !passLine) { + return null; + } + return { + host: hostLine.replace("host=", "").trim(), + user: userLine.replace("user=", "").trim(), + pass: passLine.replace("pass=", "").trim(), + }; + } catch (error) { + console.error("Error parsing credentials from config string", { error }); + return null; + } + } +} + +// Export a singleton instance +export const cloudStorageDestinationService = + new CloudStorageDestinationService(); diff --git a/packages/server/src/utils/backups/cloud-storage.ts b/packages/server/src/utils/backups/cloud-storage.ts new file mode 100644 index 00000000..d89d9a5f --- /dev/null +++ b/packages/server/src/utils/backups/cloud-storage.ts @@ -0,0 +1,496 @@ +import { existsSync, mkdirSync } from "node:fs"; +import { mkdir, writeFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { paths } from "@dokploy/server/constants"; +import type { + cloudStorageBackup, + cloudStorageDestination, +} from "@dokploy/server/db/schema"; +import type { InferSelectModel } from "drizzle-orm"; +import { execAsync } from "../process/execAsync"; + +type CloudStorageDestination = InferSelectModel; +type CloudStorageBackup = InferSelectModel & { + postgres?: { host: string; port: number; user: string; password: string }; + mysql?: { host: string; port: number; user: string; password: string }; + mariadb?: { host: string; port: number; user: string; password: string }; + mongo?: { host: string; port: number; user: string; password: string }; + webServer?: { path: string }; +}; +export type CloudStorageProvider = "drive" | "dropbox" | "box" | "ftp" | "sftp"; + +export const normalizeCloudPath = (path: string): string => { + const normalized = path.replace(/\/+/g, "/").trim(); + const cleaned = normalized.replace(/^\/+|\/+$/g, ""); + return cleaned ? `${cleaned}/` : ""; +}; + +export const constructCloudStoragePath = ( + provider: CloudStorageProvider, + prefix: string | null, + fileName: string, +): string => { + const normalizedPrefix = normalizeCloudPath(prefix || ""); + + switch (provider) { + case "drive": + case "dropbox": + case "box": + case "ftp": + return `${provider}:${normalizedPrefix}${fileName}`; + case "sftp": { + const sftpPath = normalizedPrefix.replace(/^\/+/, ""); + return `${provider}:${sftpPath}${fileName}`; + } + default: + throw new Error(`Unsupported provider: ${provider}`); + } +}; + +export const getCloudStorageCredentials = async ( + destination: CloudStorageDestination, +): Promise => { + const { provider, config } = destination; + const credentials: string[] = []; + + try { + switch (provider) { + case "drive": + case "dropbox": + case "box": { + const configDir = join(homedir(), ".config", "rclone"); + await mkdir(configDir, { recursive: true }); + + const configFile = join(configDir, "rclone.conf"); + let configContent = ""; + + const parsedOAuthConfig = JSON.parse(config || "{}"); + + switch (provider) { + case "drive": + if (!parsedOAuthConfig.token) { + throw new Error("Google Drive OAuth token is missing"); + } + configContent = `[drive] +type = drive +token = ${parsedOAuthConfig.token} +`; + break; + case "dropbox": + if (!parsedOAuthConfig.token) { + throw new Error("Dropbox OAuth token is missing"); + } + configContent = `[dropbox] +type = dropbox +token = ${parsedOAuthConfig.token} +`; + break; + case "box": + if (!parsedOAuthConfig.token) { + throw new Error("Box OAuth token is missing"); + } + configContent = `[box] +type = box +token = ${parsedOAuthConfig.token} +`; + break; + } + + await writeFile(configFile, configContent, "utf8"); + credentials.push(`--config=${configFile}`); + break; + } + + case "ftp": + case "sftp": { + const ftpConfigDir = join(homedir(), ".config", "rclone"); + await mkdir(ftpConfigDir, { recursive: true }); + + const ftpConfigFile = join(ftpConfigDir, "rclone.conf"); + let ftpConfigContent = ""; + + const parsedFtpConfig = JSON.parse(config || "{}"); + const { host, username, password, port } = parsedFtpConfig; + + if (!username || !password || !host) { + throw new Error(`${provider.toUpperCase()} credentials are missing`); + } + + ftpConfigContent = `[${provider}] +type = ${provider} +host = ${host} +user = ${username} +pass = ${password} +${port ? `port = ${port}` : ""} +mkdir = true +dir_perms = 0755 +file_perms = 0644 +concurrency = 4 +transfers = 4 +checkers = 4 +low_level_retries = 10 +retries = 3 +retries_sleep = 10s +${ + provider === "ftp" + ? ` +allow_writeable_chroot = true +passive = true +encoding = Slash,Del,Ctl,RightSpace,Dot` + : "" +}`; + + await writeFile(ftpConfigFile, ftpConfigContent, "utf8"); + credentials.push(`--config=${ftpConfigFile}`); + break; + } + + default: + throw new Error(`Unsupported cloud storage provider: ${provider}`); + } + } catch (error) { + console.error("Error parsing cloud storage config:", error); + throw new Error("Invalid cloud storage configuration"); + } + + return credentials; +}; + +export const isCloudStorage = (provider?: string): boolean => { + return ["drive", "dropbox", "box", "ftp", "sftp"].includes(provider || ""); +}; + +export const executeCloudStorageBackup = async ( + backup: CloudStorageBackup, + destination: CloudStorageDestination, +): Promise => { + try { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const filename = `${backup.database || "backup"}-${timestamp}.sql.gz`; + const tempDir = `/tmp/dokploy-backup-${Date.now()}`; + + await execAsync(`rm -rf ${tempDir}`); + await execAsync(`mkdir -p ${tempDir}`); + + try { + let backupCommand = ""; + switch (backup.databaseType) { + case "postgres": + if (!backup.postgres) + throw new Error("PostgreSQL configuration missing"); + backupCommand = `PGPASSWORD="${backup.postgres.password}" pg_dump -h ${backup.postgres.host} -p ${backup.postgres.port} -U ${backup.postgres.user} -d ${backup.database} | gzip | rclone rcat ${(await getCloudStorageCredentials(destination)).join(" ")} "${constructCloudStoragePath(destination.provider as CloudStorageProvider, backup.prefix || "", filename)}"`; + break; + case "mysql": + if (!backup.mysql) throw new Error("MySQL configuration missing"); + backupCommand = `mysqldump -h ${backup.mysql.host} -P ${backup.mysql.port} -u ${backup.mysql.user} -p${backup.mysql.password} ${backup.database} | gzip | rclone rcat ${(await getCloudStorageCredentials(destination)).join(" ")} "${constructCloudStoragePath(destination.provider as CloudStorageProvider, backup.prefix || "", filename)}"`; + break; + case "mariadb": + if (!backup.mariadb) throw new Error("MariaDB configuration missing"); + backupCommand = `mysqldump -h ${backup.mariadb.host} -P ${backup.mariadb.port} -u ${backup.mariadb.user} -p${backup.mariadb.password} ${backup.database} | gzip | rclone rcat ${(await getCloudStorageCredentials(destination)).join(" ")} "${constructCloudStoragePath(destination.provider as CloudStorageProvider, backup.prefix || "", filename)}"`; + break; + case "mongo": + if (!backup.mongo) throw new Error("MongoDB configuration missing"); + backupCommand = `mongodump --host ${backup.mongo.host} --port ${backup.mongo.port} --username ${backup.mongo.user} --password ${backup.mongo.password} --db ${backup.database} --archive --gzip | rclone rcat ${(await getCloudStorageCredentials(destination)).join(" ")} "${constructCloudStoragePath(destination.provider as CloudStorageProvider, backup.prefix || "", filename)}"`; + break; + case "web-server": { + const { BASE_PATH } = paths(); + const webServerFilename = `webserver-backup-${timestamp}.zip`; + + try { + const { stdout: dirExists } = await execAsync( + `test -d "${BASE_PATH}" && echo "exists" || echo "not exists"`, + ); + if (dirExists.trim() === "not exists") { + throw new Error(`Directory ${BASE_PATH} does not exist`); + } + + await execAsync(`mkdir -p ${tempDir}/filesystem`); + await execAsync( + `rsync -a --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`, + ); + + const { stdout: containerId } = await execAsync( + `docker ps --filter "name=dokploy-postgres" --filter "status=running" -q | head -n 1`, + ); + + if (containerId) { + const postgresContainerId = containerId.trim(); + await execAsync( + `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy -f /tmp/database.sql`, + ); + await execAsync( + `docker cp ${postgresContainerId}:/tmp/database.sql ${tempDir}/database.sql`, + ); + await execAsync( + `docker exec ${postgresContainerId} rm -f /tmp/database.sql`, + ); + } + + await execAsync( + `cd ${tempDir} && zip -r ${webServerFilename} *.sql filesystem/ > /dev/null 2>&1`, + ); + + const backupPath = constructCloudStoragePath( + destination.provider as CloudStorageProvider, + backup.prefix || "", + webServerFilename, + ); + + const credentials = await getCloudStorageCredentials(destination); + const rcloneCommand = `rclone copyto ${tempDir}/${webServerFilename} "${backupPath}" ${credentials.join(" ")}`; + + await execAsync(rcloneCommand); + console.log(`Cloud storage backup completed: ${backupPath}`); + return; + } finally { + await execAsync(`rm -rf ${tempDir}`); + } + } + default: + throw new Error(`Unsupported database type: ${backup.databaseType}`); + } + + if (backupCommand) { + await execAsync(backupCommand); + console.log(`Cloud storage backup completed: ${filename}`); + } + + if (backup.keepLatestCount) { + await keepLatestNCloudStorageBackups( + backup, + destination, + backup.keepLatestCount, + ); + } + } finally { + await execAsync(`rm -rf ${tempDir}`); + } + } catch (error) { + console.error("Error during cloud storage backup:", error); + throw error; + } +}; + +export const executeCloudStorageRestore = async ( + backup: CloudStorageBackup, + destination: CloudStorageDestination, + backupPath: string, + emit: (log: string) => void, +): Promise => { + try { + const tempDir = `/tmp/dokploy-restore-${Date.now()}`; + await execAsync(`mkdir -p ${tempDir}`); + + try { + const configPath = getRcloneConfigPath( + destination.organizationId, + destination.id, + ); + const rcloneCommand = `rclone copy ${backupPath} ${tempDir} --config=${configPath}`; + await execAsync(rcloneCommand); + + let restoreCommand = ""; + switch (backup.databaseType) { + case "postgres": + if (!backup.postgres) + throw new Error("PostgreSQL configuration missing"); + restoreCommand = `psql -h ${backup.postgres.host} -p ${backup.postgres.port} -U ${backup.postgres.user} -d ${backup.database} < ${tempDir}/${backupPath.split("/").pop()}`; + break; + case "mysql": + if (!backup.mysql) throw new Error("MySQL configuration missing"); + restoreCommand = `mysql -h ${backup.mysql.host} -P ${backup.mysql.port} -u ${backup.mysql.user} -p${backup.mysql.password} ${backup.database} < ${tempDir}/${backupPath.split("/").pop()}`; + break; + case "mariadb": + if (!backup.mariadb) throw new Error("MariaDB configuration missing"); + restoreCommand = `mysql -h ${backup.mariadb.host} -P ${backup.mariadb.port} -u ${backup.mariadb.user} -p${backup.mariadb.password} ${backup.database} < ${tempDir}/${backupPath.split("/").pop()}`; + break; + case "mongo": + if (!backup.mongo) throw new Error("MongoDB configuration missing"); + restoreCommand = `mongorestore --host ${backup.mongo.host} --port ${backup.mongo.port} --username ${backup.mongo.user} --password ${backup.mongo.password} --db ${backup.database} ${tempDir}/${backup.database}`; + break; + case "web-server": { + if (!backup.webServer) + throw new Error("Web server configuration missing"); + await execAsync(`mkdir -p ${backup.webServer.path}`); + + emit("Extracting backup..."); + await execAsync( + `cd ${tempDir} && unzip ${backupPath.split("/").pop()} > /dev/null 2>&1`, + ); + + emit("Restoring filesystem..."); + emit( + `Copying from ${tempDir}/filesystem/* to ${backup.webServer.path}/`, + ); + + emit("Cleaning target directory..."); + await execAsync(`rm -rf "${backup.webServer.path}/"*`); + + emit("Setting up target directory..."); + await execAsync(`mkdir -p "${backup.webServer.path}"`); + + emit("Copying files..."); + await execAsync( + `cp -rp "${tempDir}/filesystem/"* "${backup.webServer.path}/"`, + ); + + emit("Starting database restore..."); + + const { stdout: hasSqlFile } = await execAsync( + `ls ${tempDir}/database.sql || true`, + ); + if (hasSqlFile.includes("database.sql")) { + const { stdout: postgresContainer } = await execAsync( + `docker ps --filter "name=dokploy-postgres" --filter "status=running" -q | head -n 1`, + ); + + if (postgresContainer) { + const postgresContainerId = postgresContainer.trim(); + + emit("Disconnecting all users from database..."); + await execAsync( + `docker exec ${postgresContainerId} psql -U dokploy postgres -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'dokploy' AND pid <> pg_backend_pid();"`, + ); + + emit("Dropping existing database..."); + await execAsync( + `docker exec ${postgresContainerId} psql -U dokploy postgres -c "DROP DATABASE IF EXISTS dokploy;"`, + ); + + emit("Creating fresh database..."); + await execAsync( + `docker exec ${postgresContainerId} psql -U dokploy postgres -c "CREATE DATABASE dokploy;"`, + ); + + emit("Copying backup file into container..."); + await execAsync( + `docker cp ${tempDir}/database.sql ${postgresContainerId}:/tmp/database.sql`, + ); + + emit("Running database restore..."); + await execAsync( + `docker exec ${postgresContainerId} pg_restore -v -U dokploy -d dokploy /tmp/database.sql`, + ); + + emit("Cleaning up container temp file..."); + await execAsync( + `docker exec ${postgresContainerId} rm /tmp/database.sql`, + ); + } + } + break; + } + default: + throw new Error(`Unsupported database type: ${backup.databaseType}`); + } + + await execAsync(restoreCommand); + await execAsync(`rm -rf ${tempDir}`); + emit(`Cloud storage restore completed: ${backupPath}`); + } finally { + await execAsync(`rm -rf ${tempDir}`); + } + } catch (error) { + console.error(`Error executing cloud storage restore: ${error}`); + throw error; + } +}; + +export const listCloudStorageFiles = async ( + destination: CloudStorageDestination, + prefix: string | null, + searchTerm?: string, +): Promise< + Array<{ + path: string; + size: number; + isDir: boolean; + hashes?: { MD5?: string }; + }> +> => { + try { + const credentials = await getCloudStorageCredentials(destination); + const normalizedPrefix = normalizeCloudPath(prefix || ""); + const searchPattern = searchTerm ? `*${searchTerm}*` : "*"; + + const rclonePath = (() => { + switch (destination.provider) { + case "drive": + return `drive:${normalizedPrefix}${searchPattern}`; + case "dropbox": + return `dropbox:${normalizedPrefix}${searchPattern}`; + case "box": + return `box:${normalizedPrefix}${searchPattern}`; + case "ftp": + case "sftp": + return `${destination.provider}:${normalizedPrefix}${searchPattern}`; + default: + throw new Error( + `Unsupported cloud storage provider: ${destination.provider}`, + ); + } + })(); + + const command = `rclone lsjson ${credentials.join(" ")} "${rclonePath}"`; + const { stdout } = await execAsync(command); + + const files = JSON.parse(stdout) as Array<{ + Path: string; + Size: number; + IsDir: boolean; + Hashes?: { MD5?: string }; + }>; + + return files.map((file) => ({ + path: file.Path, + size: file.Size, + isDir: file.IsDir, + hashes: file.Hashes, + })); + } catch (error) { + console.error("Error listing cloud storage files:", error); + throw new Error("Failed to list backup files from cloud storage"); + } +}; + +export function getRcloneConfigPath( + organizationId: string, + destinationId: string, +): string { + const configDir = join( + homedir(), + ".dokploy", + "rclone", + "cloud", + organizationId, + ); + if (!existsSync(configDir)) { + mkdirSync(configDir, { recursive: true }); + } + return join(configDir, `${destinationId}.conf`); +} + +export const keepLatestNCloudStorageBackups = async ( + backup: CloudStorageBackup, + destination: CloudStorageDestination, + keepLatestCount: number, +) => { + if (!keepLatestCount) return; + + try { + const credentials = await getCloudStorageCredentials(destination); + const normalizedPrefix = normalizeCloudPath(backup.prefix || ""); + const backupFilesPath = `${destination.provider}:${normalizedPrefix}`; + + const rcloneList = `rclone lsf ${credentials.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".sql.gz"}" ${backupFilesPath}`; + const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`; + const rcloneDelete = `rclone delete ${credentials.join(" ")} ${backupFilesPath}/{}`; + + const rcloneCommand = `${rcloneList} | ${sortAndPickUnwantedBackups} ${rcloneDelete}`; + + await execAsync(rcloneCommand); + } catch (error) { + console.error("Error keeping latest backups:", error); + } +};