diff --git a/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx index f2ca41b85..0642adff5 100644 --- a/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx @@ -39,6 +39,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { InputArray } from "@/components/ui/input-array"; import { Popover, PopoverContent, @@ -106,6 +107,7 @@ const Schema = z .optional(), }) .optional(), + additionalOptions: z.array(z.string()).nullable(), }) .superRefine((data, ctx) => { if (data.backupType === "compose" && !data.databaseType) { @@ -219,6 +221,7 @@ export const HandleBackup = ({ databaseType: backupType === "compose" ? undefined : databaseType, backupType: backupType, metadata: {}, + additionalOptions: [], }, resolver: zodResolver(Schema), }); @@ -256,6 +259,7 @@ export const HandleBackup = ({ databaseType: backup?.databaseType ?? databaseType, backupType: backup?.backupType ?? backupType, metadata: backup?.metadata ?? {}, + additionalOptions: backup?.additionalOptions ?? [], }); }, [form, form.reset, backupId, backup]); @@ -300,6 +304,7 @@ export const HandleBackup = ({ backupId: backupId ?? "", backupType, metadata: data.metadata, + additionalOptions: data.additionalOptions, }) .then(async () => { toast.success(`Backup ${backupId ? "Updated" : "Created"}`); @@ -753,6 +758,25 @@ export const HandleBackup = ({ )} )} + { + return ( + + Additional Options + + + + + Use if you want to pass additional options to the backup command + + + + + ); + }} + /> + + ))} + + + + + {errorMessage && ( + + {errorMessage} + + )} + + ); +}); + +InputArray.displayName = "InputArray"; + +export { InputArray }; diff --git a/packages/server/src/db/schema/backups.ts b/packages/server/src/db/schema/backups.ts index 9b4881924..7438a8281 100644 --- a/packages/server/src/db/schema/backups.ts +++ b/packages/server/src/db/schema/backups.ts @@ -78,23 +78,25 @@ export const backups = pgTable("backup", { // Only for compose backups metadata: jsonb("metadata").$type< | { - postgres?: { - databaseUser: string; - }; - mariadb?: { - databaseUser: string; - databasePassword: string; - }; - mongo?: { - databaseUser: string; - databasePassword: string; - }; - mysql?: { - databaseRootPassword: string; - }; - } + postgres?: { + databaseUser: string; + }; + mariadb?: { + databaseUser: string; + databasePassword: string; + }; + mongo?: { + databaseUser: string; + databasePassword: string; + }; + mysql?: { + databaseRootPassword: string; + }; + } | undefined >(), + // additional options to be passed to the backup command + additionalOptions: jsonb("additionalOptions").$type().default([]), }); export const backupsRelations = relations(backups, ({ one, many }) => ({ @@ -144,6 +146,7 @@ const createSchema = createInsertSchema(backups, { mongoId: z.string().optional(), userId: z.string().optional(), metadata: z.any().optional(), + additionalOptions: z.array(z.string()).optional(), }); export const apiCreateBackup = createSchema.pick({ @@ -163,6 +166,7 @@ export const apiCreateBackup = createSchema.pick({ composeId: true, serviceName: true, metadata: true, + additionalOptions: true, }); export const apiFindOneBackup = createSchema @@ -189,6 +193,7 @@ export const apiUpdateBackup = createSchema serviceName: true, metadata: true, databaseType: true, + additionalOptions: true, }) .required(); @@ -226,4 +231,5 @@ export const apiRestoreBackup = z.object({ .optional(), }) .optional(), + additionalOptions: z.array(z.string()).optional(), }); diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index f30577a53..98ee0feae 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -80,31 +80,76 @@ export const getS3Credentials = (destination: Destination) => { export const getPostgresBackupCommand = ( database: string, databaseUser: string, + additionalOptions: string[] | null, ) => { - return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; pg_dump -Fc --no-acl --no-owner -h localhost -U ${databaseUser} --no-password '${database}' | gzip"`; + return [ + 'docker exec -i $CONTAINER_ID bash -c "set -o pipefail; pg_dump', + '-Fc', + '--no-acl', + '--no-owner', + additionalOptions?.length ? additionalOptions.map(opt => `'${opt.trim().replace(/'/g, `'\\''`)}'`).join(' ') : '', + '-h localhost', + `-U ${databaseUser}`, + '--no-password', + database, + '| gzip"', + ].filter(Boolean).join(' '); }; export const getMariadbBackupCommand = ( database: string, databaseUser: string, databasePassword: string, + additionalOptions: string[] | null, ) => { - return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --single-transaction --quick --databases ${database} | gzip"`; + return [ + 'docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mariadb-dump', + `--user=${databaseUser}`, + `--password=${databasePassword}`, + '--single-transaction', + '--quick', + additionalOptions?.length ? additionalOptions.map(opt => `'${opt.trim().replace(/'/g, `'\\''`)}'`).join(' ') : '', + `--databases ${database}`, + '| gzip"', + ].filter(Boolean).join(' '); }; export const getMysqlBackupCommand = ( database: string, databasePassword: string, + additionalOptions: string[] | null, ) => { - return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mysqldump --default-character-set=utf8mb4 -u 'root' --password='${databasePassword}' --single-transaction --no-tablespaces --quick '${database}' | gzip"`; + return [ + 'docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mysqldump', + '--default-character-set=utf8mb4', + '-u', 'root', + `--password=${databasePassword}`, + '--single-transaction', + '--no-tablespaces', + '--quick', + additionalOptions?.length ? additionalOptions.map(opt => `'${opt.trim().replace(/'/g, `'\\''`)}'`).join(' ') : '', + database, + '| gzip"', + ].filter(Boolean).join(' '); }; export const getMongoBackupCommand = ( database: string, databaseUser: string, databasePassword: string, + additionalOptions: string[] | null, ) => { - return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --archive --authenticationDatabase admin --gzip"`; + return [ + 'docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mongodump', + `-d ${database}`, + `-u ${databaseUser}`, + `-p ${databasePassword}`, + '--archive', + '--authenticationDatabase admin', + '--gzip', + additionalOptions?.length ? additionalOptions.map(opt => `'${opt.trim().replace(/'/g, `'\\''`)}'`).join(' ') : '', + '"', + ].filter(Boolean).join(' '); }; export const getServiceContainerCommand = (appName: string) => { @@ -147,12 +192,17 @@ export const generateBackupCommand = (backup: BackupSchedule) => { case "postgres": { const postgres = backup.postgres; if (backupType === "database" && postgres) { - return getPostgresBackupCommand(backup.database, postgres.databaseUser); + return getPostgresBackupCommand( + backup.database, + postgres.databaseUser, + backup.additionalOptions, + ); } if (backupType === "compose" && backup.metadata?.postgres) { return getPostgresBackupCommand( backup.database, backup.metadata.postgres.databaseUser, + backup.additionalOptions, ); } break; @@ -163,12 +213,14 @@ export const generateBackupCommand = (backup: BackupSchedule) => { return getMysqlBackupCommand( backup.database, mysql.databaseRootPassword, + backup.additionalOptions, ); } if (backupType === "compose" && backup.metadata?.mysql) { return getMysqlBackupCommand( backup.database, backup.metadata?.mysql?.databaseRootPassword || "", + backup.additionalOptions, ); } break; @@ -180,6 +232,7 @@ export const generateBackupCommand = (backup: BackupSchedule) => { backup.database, mariadb.databaseUser, mariadb.databasePassword, + backup.additionalOptions, ); } if (backupType === "compose" && backup.metadata?.mariadb) { @@ -187,6 +240,7 @@ export const generateBackupCommand = (backup: BackupSchedule) => { backup.database, backup.metadata.mariadb.databaseUser, backup.metadata.mariadb.databasePassword, + backup.additionalOptions, ); } break; @@ -198,6 +252,7 @@ export const generateBackupCommand = (backup: BackupSchedule) => { backup.database, mongo.databaseUser, mongo.databasePassword, + backup.additionalOptions, ); } if (backupType === "compose" && backup.metadata?.mongo) { @@ -205,6 +260,7 @@ export const generateBackupCommand = (backup: BackupSchedule) => { backup.database, backup.metadata.mongo.databaseUser, backup.metadata.mongo.databasePassword, + backup.additionalOptions, ); } break; diff --git a/packages/server/src/utils/restore/compose.ts b/packages/server/src/utils/restore/compose.ts index d55b12fd8..fe87de7e0 100644 --- a/packages/server/src/utils/restore/compose.ts +++ b/packages/server/src/utils/restore/compose.ts @@ -69,6 +69,7 @@ export const restoreComposeBackup = async ( }, restoreType: composeType, rcloneCommand, + additionalOptions: backupInput.additionalOptions, }); emit("Starting restore..."); @@ -86,8 +87,7 @@ export const restoreComposeBackup = async ( } catch (error) { console.error(error); emit( - `Error: ${ - error instanceof Error ? error.message : "Error restoring mongo backup" + `Error: ${error instanceof Error ? error.message : "Error restoring mongo backup" }`, ); throw new Error( diff --git a/packages/server/src/utils/restore/mariadb.ts b/packages/server/src/utils/restore/mariadb.ts index ffbceba76..1ca238fa0 100644 --- a/packages/server/src/utils/restore/mariadb.ts +++ b/packages/server/src/utils/restore/mariadb.ts @@ -31,6 +31,7 @@ export const restoreMariadbBackup = async ( type: "mariadb", rcloneCommand, restoreType: "database", + additionalOptions: backupInput.additionalOptions, }); emit("Starting restore..."); @@ -47,10 +48,9 @@ export const restoreMariadbBackup = async ( } catch (error) { console.error(error); emit( - `Error: ${ - error instanceof Error - ? error.message - : "Error restoring mariadb backup" + `Error: ${error instanceof Error + ? error.message + : "Error restoring mariadb backup" }`, ); throw new Error( diff --git a/packages/server/src/utils/restore/mongo.ts b/packages/server/src/utils/restore/mongo.ts index 4329a4985..f03f32aba 100644 --- a/packages/server/src/utils/restore/mongo.ts +++ b/packages/server/src/utils/restore/mongo.ts @@ -31,6 +31,7 @@ export const restoreMongoBackup = async ( restoreType: "database", rcloneCommand, backupFile: backupInput.backupFile, + additionalOptions: backupInput.additionalOptions, }); emit("Starting restore..."); @@ -47,8 +48,7 @@ export const restoreMongoBackup = async ( } catch (error) { console.error(error); emit( - `Error: ${ - error instanceof Error ? error.message : "Error restoring mongo backup" + `Error: ${error instanceof Error ? error.message : "Error restoring mongo backup" }`, ); throw new Error( diff --git a/packages/server/src/utils/restore/mysql.ts b/packages/server/src/utils/restore/mysql.ts index f5187242c..f9357789a 100644 --- a/packages/server/src/utils/restore/mysql.ts +++ b/packages/server/src/utils/restore/mysql.ts @@ -30,6 +30,7 @@ export const restoreMySqlBackup = async ( }, restoreType: "database", rcloneCommand, + additionalOptions: backupInput.additionalOptions, }); emit("Starting restore..."); @@ -46,8 +47,7 @@ export const restoreMySqlBackup = async ( } catch (error) { console.error(error); emit( - `Error: ${ - error instanceof Error ? error.message : "Error restoring mysql backup" + `Error: ${error instanceof Error ? error.message : "Error restoring mysql backup" }`, ); throw new Error( diff --git a/packages/server/src/utils/restore/postgres.ts b/packages/server/src/utils/restore/postgres.ts index 19f32989f..730e93d99 100644 --- a/packages/server/src/utils/restore/postgres.ts +++ b/packages/server/src/utils/restore/postgres.ts @@ -34,6 +34,7 @@ export const restorePostgresBackup = async ( type: "postgres", rcloneCommand, restoreType: "database", + additionalOptions: backupInput.additionalOptions, }); emit(`Executing command: ${command}`); @@ -47,10 +48,9 @@ export const restorePostgresBackup = async ( emit("Restore completed successfully!"); } catch (error) { emit( - `Error: ${ - error instanceof Error - ? error.message - : "Error restoring postgres backup" + `Error: ${error instanceof Error + ? error.message + : "Error restoring postgres backup" }`, ); throw error; diff --git a/packages/server/src/utils/restore/utils.ts b/packages/server/src/utils/restore/utils.ts index 23052e642..a1ff6e978 100644 --- a/packages/server/src/utils/restore/utils.ts +++ b/packages/server/src/utils/restore/utils.ts @@ -6,6 +6,7 @@ import { export const getPostgresRestoreCommand = ( database: string, databaseUser: string, + additionalOptions?: string[], ) => { return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U '${databaseUser}' -d ${database} -O --clean --if-exists"`; }; @@ -14,6 +15,7 @@ export const getMariadbRestoreCommand = ( database: string, databaseUser: string, databasePassword: string, + additionalOptions?: string[], ) => { return `docker exec -i $CONTAINER_ID sh -c "mariadb -u '${databaseUser}' -p'${databasePassword}' ${database}"`; }; @@ -21,6 +23,7 @@ export const getMariadbRestoreCommand = ( export const getMysqlRestoreCommand = ( database: string, databasePassword: string, + additionalOptions?: string[], ) => { return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p'${databasePassword}' ${database}"`; }; @@ -29,6 +32,7 @@ export const getMongoRestoreCommand = ( database: string, databaseUser: string, databasePassword: string, + additionalOptions?: string[], ) => { return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username '${databaseUser}' --password '${databasePassword}' --authenticationDatabase admin --db ${database} --archive"`; }; @@ -53,24 +57,35 @@ interface DatabaseCredentials { const generateRestoreCommand = ( type: "postgres" | "mariadb" | "mysql" | "mongo", credentials: DatabaseCredentials, + additionalOptions?: string[], ) => { const { database, databaseUser, databasePassword } = credentials; switch (type) { case "postgres": - return getPostgresRestoreCommand(database, databaseUser || ""); + return getPostgresRestoreCommand( + database, + databaseUser || "", + additionalOptions, + ); case "mariadb": return getMariadbRestoreCommand( database, databaseUser || "", databasePassword || "", + additionalOptions, ); case "mysql": - return getMysqlRestoreCommand(database, databasePassword || ""); + return getMysqlRestoreCommand( + database, + databasePassword || "", + additionalOptions, + ); case "mongo": return getMongoRestoreCommand( database, databaseUser || "", databasePassword || "", + additionalOptions, ); } }; @@ -102,6 +117,7 @@ interface RestoreOptions { serviceName?: string; rcloneCommand: string; backupFile?: string; + additionalOptions?: string[]; } export const getRestoreCommand = ({ @@ -112,13 +128,14 @@ export const getRestoreCommand = ({ serviceName, rcloneCommand, backupFile, + additionalOptions, }: RestoreOptions) => { const containerSearch = getComposeSearchCommand( appName, restoreType, serviceName, ); - const restoreCommand = generateRestoreCommand(type, credentials); + const restoreCommand = generateRestoreCommand(type, credentials, additionalOptions); let cmd = `CONTAINER_ID=$(${containerSearch})`; if (type !== "mongo") { diff --git a/schema.dbml b/schema.dbml index d0845f3ed..cd4135576 100644 --- a/schema.dbml +++ b/schema.dbml @@ -309,6 +309,7 @@ table backup { mongoId text userId text metadata jsonb + additionalOptions jsonb } table bitbucket {