Merge pull request #4092 from Dokploy/2023-add-support-for-rclone-sign_accept_encoding-option-to-fix-s3-compatible-services-behind-proxies-blocked-until-rclone-170

feat(destinations): add additionalFlags field for destination settings
This commit is contained in:
Mauricio Siu
2026-03-29 09:06:44 -06:00
committed by GitHub
10 changed files with 8365 additions and 7 deletions
@@ -130,7 +130,7 @@ export const columns: ColumnDef<Container>[] = [
</DockerTerminalModal> </DockerTerminalModal>
<RemoveContainerDialog <RemoveContainerDialog
containerId={container.containerId} containerId={container.containerId}
serverId={container.serverId} serverId={container.serverId ?? undefined}
/> />
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@@ -1,7 +1,7 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; 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 { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
@@ -35,6 +35,10 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import {
ADDITIONAL_FLAG_ERROR,
ADDITIONAL_FLAG_REGEX,
} from "@dokploy/server/db/validations/destination";
import { S3_PROVIDERS } from "./constants"; import { S3_PROVIDERS } from "./constants";
const addDestination = z.object({ const addDestination = z.object({
@@ -46,6 +50,16 @@ const addDestination = z.object({
region: z.string(), region: z.string(),
endpoint: z.string().min(1, "Endpoint is required"), endpoint: z.string().min(1, "Endpoint is required"),
serverId: z.string().optional(), 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<typeof addDestination>; type AddDestination = z.infer<typeof addDestination>;
@@ -89,9 +103,16 @@ export const HandleDestinations = ({ destinationId }: Props) => {
region: "", region: "",
secretAccessKey: "", secretAccessKey: "",
endpoint: "", endpoint: "",
additionalFlags: [],
}, },
resolver: zodResolver(addDestination), resolver: zodResolver(addDestination),
}); });
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "additionalFlags",
});
useEffect(() => { useEffect(() => {
if (destination) { if (destination) {
form.reset({ form.reset({
@@ -102,6 +123,8 @@ export const HandleDestinations = ({ destinationId }: Props) => {
bucket: destination.bucket, bucket: destination.bucket,
region: destination.region, region: destination.region,
endpoint: destination.endpoint, endpoint: destination.endpoint,
additionalFlags:
destination.additionalFlags?.map((f) => ({ value: f })) ?? [],
}); });
} else { } else {
form.reset(); form.reset();
@@ -118,6 +141,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
region: data.region, region: data.region,
secretAccessKey: data.secretAccessKey, secretAccessKey: data.secretAccessKey,
destinationId: destinationId || "", destinationId: destinationId || "",
additionalFlags: data.additionalFlags?.map((f) => f.value) ?? [],
}) })
.then(async () => { .then(async () => {
toast.success(`Destination ${destinationId ? "Updated" : "Created"}`); toast.success(`Destination ${destinationId ? "Updated" : "Created"}`);
@@ -127,9 +151,12 @@ export const HandleDestinations = ({ destinationId }: Props) => {
} }
setOpen(false); setOpen(false);
}) })
.catch(() => { .catch((e) => {
toast.error( toast.error(
`Error ${destinationId ? "Updating" : "Creating"} the Destination`, `Error ${destinationId ? "Updating" : "Creating"} the Destination`,
{
description: e.message,
},
); );
}); });
}; };
@@ -141,6 +168,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
"secretAccessKey", "secretAccessKey",
"bucket", "bucket",
"endpoint", "endpoint",
"additionalFlags",
]); ]);
if (!result) { if (!result) {
@@ -179,6 +207,8 @@ export const HandleDestinations = ({ destinationId }: Props) => {
region, region,
secretAccessKey: secretKey, secretAccessKey: secretKey,
serverId, serverId,
additionalFlags:
form.getValues("additionalFlags")?.map((f) => f.value) ?? [],
}) })
.then(() => { .then(() => {
toast.success("Connection Success"); toast.success("Connection Success");
@@ -358,6 +388,48 @@ export const HandleDestinations = ({ destinationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<FormLabel>Additional Flags (Optional)</FormLabel>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => append({ value: "" })}
>
<PlusIcon className="size-4" />
Add Flag
</Button>
</div>
{fields.map((field, index) => (
<FormField
key={field.id}
control={form.control}
name={`additionalFlags.${index}.value`}
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormControl>
<Input
placeholder="--s3-sign-accept-encoding=false"
{...field}
/>
</FormControl>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="size-4 text-muted-foreground" />
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
))}
</div>
</form> </form>
<DialogFooter <DialogFooter
@@ -0,0 +1 @@
ALTER TABLE "destination" ADD COLUMN "additionalFlags" text[];
File diff suppressed because it is too large Load Diff
+7
View File
@@ -1086,6 +1086,13 @@
"when": 1774337356154, "when": 1774337356154,
"tag": "0154_careful_eternals", "tag": "0154_careful_eternals",
"breakpoints": true "breakpoints": true
},
{
"idx": 155,
"version": "7",
"when": 1774794547865,
"tag": "0155_careless_clea",
"breakpoints": true
} }
] ]
} }
+20 -3
View File
@@ -47,8 +47,15 @@ export const destinationRouter = createTRPCRouter({
testConnection: withPermission("destination", "create") testConnection: withPermission("destination", "create")
.input(apiCreateDestination) .input(apiCreateDestination)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { secretAccessKey, bucket, region, endpoint, accessKey, provider } = const {
input; secretAccessKey,
bucket,
region,
endpoint,
accessKey,
provider,
additionalFlags,
} = input;
try { try {
const rcloneFlags = [ const rcloneFlags = [
`--s3-access-key-id="${accessKey}"`, `--s3-access-key-id="${accessKey}"`,
@@ -65,6 +72,9 @@ export const destinationRouter = createTRPCRouter({
if (provider) { if (provider) {
rcloneFlags.unshift(`--s3-provider="${provider}"`); rcloneFlags.unshift(`--s3-provider="${provider}"`);
} }
if (additionalFlags?.length) {
rcloneFlags.push(...additionalFlags);
}
const rcloneDestination = `:s3:${bucket}`; const rcloneDestination = `:s3:${bucket}`;
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`; const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
@@ -159,7 +169,14 @@ export const destinationRouter = createTRPCRouter({
}); });
return result; return result;
} catch (error) { } catch (error) {
throw error; throw new TRPCError({
code: "BAD_REQUEST",
message:
error instanceof Error
? error?.message
: "Error connecting to bucket",
cause: error,
});
} }
}), }),
}); });
@@ -3,6 +3,10 @@ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import {
ADDITIONAL_FLAG_ERROR,
ADDITIONAL_FLAG_REGEX,
} from "../validations/destination";
import { organization } from "./account"; import { organization } from "./account";
import { backups } from "./backups"; import { backups } from "./backups";
@@ -18,6 +22,7 @@ export const destinations = pgTable("destination", {
bucket: text("bucket").notNull(), bucket: text("bucket").notNull(),
region: text("region").notNull(), region: text("region").notNull(),
endpoint: text("endpoint").notNull(), endpoint: text("endpoint").notNull(),
additionalFlags: text("additionalFlags").array(),
organizationId: text("organizationId") organizationId: text("organizationId")
.notNull() .notNull()
.references(() => organization.id, { onDelete: "cascade" }), .references(() => organization.id, { onDelete: "cascade" }),
@@ -44,6 +49,9 @@ const createSchema = createInsertSchema(destinations, {
endpoint: z.string(), endpoint: z.string(),
secretAccessKey: z.string(), secretAccessKey: z.string(),
region: z.string(), region: z.string(),
additionalFlags: z
.array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR))
.default([]),
}); });
export const apiCreateDestination = createSchema export const apiCreateDestination = createSchema
@@ -55,6 +63,7 @@ export const apiCreateDestination = createSchema
region: true, region: true,
endpoint: true, endpoint: true,
secretAccessKey: true, secretAccessKey: true,
additionalFlags: true,
}) })
.required() .required()
.extend({ .extend({
@@ -81,6 +90,7 @@ export const apiUpdateDestination = createSchema
secretAccessKey: true, secretAccessKey: true,
destinationId: true, destinationId: true,
provider: true, provider: true,
additionalFlags: true,
}) })
.required() .required()
.extend({ .extend({
@@ -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)";
+1
View File
@@ -1,6 +1,7 @@
export * from "./auth/random-password"; export * from "./auth/random-password";
export * from "./constants/index"; export * from "./constants/index";
export * from "./db/constants"; export * from "./db/constants";
export * from "./db/validations/destination";
export * from "./db/validations/domain"; export * from "./db/validations/domain";
export * from "./db/validations/index"; export * from "./db/validations/index";
export * from "./lib/auth"; export * from "./lib/auth";
@@ -79,6 +79,10 @@ export const getS3Credentials = (destination: Destination) => {
rcloneFlags.unshift(`--s3-provider="${provider}"`); rcloneFlags.unshift(`--s3-provider="${provider}"`);
} }
if (destination.additionalFlags?.length) {
rcloneFlags.push(...destination.additionalFlags);
}
return rcloneFlags; return rcloneFlags;
}; };