mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-13 15:35:17 +00:00
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:
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user