diff --git a/apps/dokploy/components/dashboard/docker/show/colums.tsx b/apps/dokploy/components/dashboard/docker/show/colums.tsx index 8507261a2..d506d3742 100644 --- a/apps/dokploy/components/dashboard/docker/show/colums.tsx +++ b/apps/dokploy/components/dashboard/docker/show/colums.tsx @@ -130,7 +130,7 @@ export const columns: ColumnDef[] = [ diff --git a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx index 966c8e5f5..25a3a1048 100644 --- a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx @@ -1,7 +1,7 @@ import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; -import { PenBoxIcon, PlusIcon } from "lucide-react"; +import { PenBoxIcon, PlusIcon, Trash2 } from "lucide-react"; import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; +import { useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; @@ -35,6 +35,10 @@ import { } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; +import { + ADDITIONAL_FLAG_ERROR, + ADDITIONAL_FLAG_REGEX, +} from "@dokploy/server/db/validations/destination"; import { S3_PROVIDERS } from "./constants"; const addDestination = z.object({ @@ -46,6 +50,16 @@ const addDestination = z.object({ region: z.string(), endpoint: z.string().min(1, "Endpoint is required"), serverId: z.string().optional(), + additionalFlags: z + .array( + z.object({ + value: z + .string() + .min(1, "Flag cannot be empty") + .regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR), + }), + ) + .optional(), }); type AddDestination = z.infer; @@ -89,9 +103,16 @@ export const HandleDestinations = ({ destinationId }: Props) => { region: "", secretAccessKey: "", endpoint: "", + additionalFlags: [], }, resolver: zodResolver(addDestination), }); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "additionalFlags", + }); + useEffect(() => { if (destination) { form.reset({ @@ -102,6 +123,8 @@ export const HandleDestinations = ({ destinationId }: Props) => { bucket: destination.bucket, region: destination.region, endpoint: destination.endpoint, + additionalFlags: + destination.additionalFlags?.map((f) => ({ value: f })) ?? [], }); } else { form.reset(); @@ -118,6 +141,7 @@ export const HandleDestinations = ({ destinationId }: Props) => { region: data.region, secretAccessKey: data.secretAccessKey, destinationId: destinationId || "", + additionalFlags: data.additionalFlags?.map((f) => f.value) ?? [], }) .then(async () => { toast.success(`Destination ${destinationId ? "Updated" : "Created"}`); @@ -127,9 +151,12 @@ export const HandleDestinations = ({ destinationId }: Props) => { } setOpen(false); }) - .catch(() => { + .catch((e) => { toast.error( `Error ${destinationId ? "Updating" : "Creating"} the Destination`, + { + description: e.message, + }, ); }); }; @@ -141,6 +168,7 @@ export const HandleDestinations = ({ destinationId }: Props) => { "secretAccessKey", "bucket", "endpoint", + "additionalFlags", ]); if (!result) { @@ -179,6 +207,8 @@ export const HandleDestinations = ({ destinationId }: Props) => { region, secretAccessKey: secretKey, serverId, + additionalFlags: + form.getValues("additionalFlags")?.map((f) => f.value) ?? [], }) .then(() => { toast.success("Connection Success"); @@ -358,6 +388,48 @@ export const HandleDestinations = ({ destinationId }: Props) => { )} /> +
+
+ Additional Flags (Optional) + +
+ {fields.map((field, index) => ( + ( + +
+ + + + +
+ +
+ )} + /> + ))} +
{ - const { secretAccessKey, bucket, region, endpoint, accessKey, provider } = - input; + const { + secretAccessKey, + bucket, + region, + endpoint, + accessKey, + provider, + additionalFlags, + } = input; try { const rcloneFlags = [ `--s3-access-key-id="${accessKey}"`, @@ -65,6 +72,9 @@ export const destinationRouter = createTRPCRouter({ if (provider) { rcloneFlags.unshift(`--s3-provider="${provider}"`); } + if (additionalFlags?.length) { + rcloneFlags.push(...additionalFlags); + } const rcloneDestination = `:s3:${bucket}`; const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`; @@ -159,7 +169,14 @@ export const destinationRouter = createTRPCRouter({ }); return result; } catch (error) { - throw error; + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error + ? error?.message + : "Error connecting to bucket", + cause: error, + }); } }), }); diff --git a/packages/server/src/db/schema/destination.ts b/packages/server/src/db/schema/destination.ts index 8e51aef91..c479679fe 100644 --- a/packages/server/src/db/schema/destination.ts +++ b/packages/server/src/db/schema/destination.ts @@ -3,6 +3,10 @@ import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; +import { + ADDITIONAL_FLAG_ERROR, + ADDITIONAL_FLAG_REGEX, +} from "../validations/destination"; import { organization } from "./account"; import { backups } from "./backups"; @@ -18,6 +22,7 @@ export const destinations = pgTable("destination", { bucket: text("bucket").notNull(), region: text("region").notNull(), endpoint: text("endpoint").notNull(), + additionalFlags: text("additionalFlags").array(), organizationId: text("organizationId") .notNull() .references(() => organization.id, { onDelete: "cascade" }), @@ -44,6 +49,9 @@ const createSchema = createInsertSchema(destinations, { endpoint: z.string(), secretAccessKey: z.string(), region: z.string(), + additionalFlags: z + .array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR)) + .default([]), }); export const apiCreateDestination = createSchema @@ -55,6 +63,7 @@ export const apiCreateDestination = createSchema region: true, endpoint: true, secretAccessKey: true, + additionalFlags: true, }) .required() .extend({ @@ -81,6 +90,7 @@ export const apiUpdateDestination = createSchema secretAccessKey: true, destinationId: true, provider: true, + additionalFlags: true, }) .required() .extend({ diff --git a/packages/server/src/db/validations/destination.ts b/packages/server/src/db/validations/destination.ts new file mode 100644 index 000000000..d342646b4 --- /dev/null +++ b/packages/server/src/db/validations/destination.ts @@ -0,0 +1,3 @@ +export const ADDITIONAL_FLAG_REGEX = /^--[a-zA-Z0-9-]+(=[a-zA-Z0-9._:/@-]+)?$/; +export const ADDITIONAL_FLAG_ERROR = + "Invalid flag format. Must start with -- (e.g. --s3-sign-accept-encoding=false)"; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 4ccdb0b99..22e8872bd 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,6 +1,7 @@ export * from "./auth/random-password"; export * from "./constants/index"; export * from "./db/constants"; +export * from "./db/validations/destination"; export * from "./db/validations/domain"; export * from "./db/validations/index"; export * from "./lib/auth"; diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index b117fbf39..9d6a5373d 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -79,6 +79,10 @@ export const getS3Credentials = (destination: Destination) => { rcloneFlags.unshift(`--s3-provider="${provider}"`); } + if (destination.additionalFlags?.length) { + rcloneFlags.push(...destination.additionalFlags); + } + return rcloneFlags; };