mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor(destinations): start of multiple destinations
This commit is contained in:
358
apps/dokploy/components/dashboard/application/domains/schema.tsx
Normal file
358
apps/dokploy/components/dashboard/application/domains/schema.tsx
Normal file
@@ -0,0 +1,358 @@
|
||||
import { UseFormGetValues } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
export const providersData = [
|
||||
{
|
||||
name: "S3",
|
||||
type: "s3",
|
||||
properties: [
|
||||
{
|
||||
name: "accessKey",
|
||||
type: "text",
|
||||
label: "Access Key",
|
||||
description: "Your S3 Access Key",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "secretAccessKey",
|
||||
type: "password",
|
||||
label: "Secret Access Key",
|
||||
description: "Your S3 Secret Access Key",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "region",
|
||||
type: "text",
|
||||
label: "Region",
|
||||
description: "AWS Region, e.g., us-east-1",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "endpoint",
|
||||
type: "text",
|
||||
label: "Endpoint",
|
||||
description: "S3 Endpoint URL",
|
||||
required: true,
|
||||
default: "https://s3.amazonaws.com",
|
||||
},
|
||||
{
|
||||
name: "bucket",
|
||||
type: "text",
|
||||
label: "Bucket Name",
|
||||
description: "Name of the S3 bucket",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "provider",
|
||||
type: "select",
|
||||
label: "S3 Provider",
|
||||
description: "Select your S3 provider",
|
||||
required: false,
|
||||
default: "AWS",
|
||||
options: ["AWS", "Ceph", "Minio", "Alibaba", "Other"],
|
||||
},
|
||||
{
|
||||
name: "storageClass",
|
||||
type: "text",
|
||||
label: "Storage Class",
|
||||
description: "S3 Storage Class, e.g., STANDARD, REDUCED_REDUNDANCY",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "acl",
|
||||
type: "text",
|
||||
label: "ACL",
|
||||
description: "Access Control List settings for S3",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "GCS",
|
||||
type: "gcs",
|
||||
properties: [
|
||||
{
|
||||
name: "serviceAccountFile",
|
||||
type: "text",
|
||||
label: "Service Account File",
|
||||
description:
|
||||
"Path to the JSON file containing your service account key",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "clientId",
|
||||
type: "text",
|
||||
label: "Client ID",
|
||||
description:
|
||||
"Your GCS OAuth Client ID (required if Service Account File not provided)",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "clientSecret",
|
||||
type: "password",
|
||||
label: "Client Secret",
|
||||
description:
|
||||
"Your GCS OAuth Client Secret (required if Service Account File not provided)",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "projectNumber",
|
||||
type: "text",
|
||||
label: "Project Number",
|
||||
description:
|
||||
"Your GCS Project Number (required if Service Account File not provided)",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "bucket",
|
||||
type: "text",
|
||||
label: "Bucket Name",
|
||||
description: "Name of the GCS bucket",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "objectAcl",
|
||||
type: "text",
|
||||
label: "Object ACL",
|
||||
description: "Access Control List for objects uploaded to GCS",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "bucketAcl",
|
||||
type: "text",
|
||||
label: "Bucket ACL",
|
||||
description: "Access Control List for the GCS bucket",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Azure Blob",
|
||||
type: "azureblob",
|
||||
properties: [
|
||||
{
|
||||
name: "account",
|
||||
type: "text",
|
||||
label: "Account Name",
|
||||
description: "Your Azure Storage account name",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "key",
|
||||
type: "password",
|
||||
label: "Account Key",
|
||||
description: "Your Azure Storage account access key",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "endpoint",
|
||||
type: "text",
|
||||
label: "Endpoint",
|
||||
description: "Custom endpoint for Azure Blob Storage (if any)",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "container",
|
||||
type: "text",
|
||||
label: "Container Name",
|
||||
description: "Name of the Azure Blob container",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Dropbox",
|
||||
type: "dropbox",
|
||||
properties: [
|
||||
{
|
||||
name: "token",
|
||||
type: "password",
|
||||
label: "Access Token",
|
||||
description: "Your Dropbox access token",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "path",
|
||||
type: "text",
|
||||
label: "Destination Path",
|
||||
description: "Path in Dropbox where the files will be uploaded",
|
||||
required: false,
|
||||
default: "/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "FTP",
|
||||
type: "ftp",
|
||||
properties: [
|
||||
{
|
||||
name: "host",
|
||||
type: "text",
|
||||
label: "FTP Host",
|
||||
description: "Hostname or IP address of the FTP server",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "port",
|
||||
type: "number",
|
||||
label: "FTP Port",
|
||||
description: "Port number of the FTP server",
|
||||
required: false,
|
||||
default: 21,
|
||||
},
|
||||
{
|
||||
name: "user",
|
||||
type: "text",
|
||||
label: "Username",
|
||||
description: "FTP username",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "pass",
|
||||
type: "password",
|
||||
label: "Password",
|
||||
description: "FTP password",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "secure",
|
||||
type: "checkbox",
|
||||
label: "Use FTPS",
|
||||
description: "Enable FTPS (FTP over SSL/TLS)",
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
name: "path",
|
||||
type: "text",
|
||||
label: "Destination Path",
|
||||
description: "Remote path on the FTP server",
|
||||
required: false,
|
||||
default: "/",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* S3 Provider Schema
|
||||
*/
|
||||
export const s3Schema = z.object({
|
||||
accessKey: z.string().nonempty({ message: "Access Key is required" }),
|
||||
secretAccessKey: z
|
||||
.string()
|
||||
.nonempty({ message: "Secret Access Key is required" }),
|
||||
region: z.string().nonempty({ message: "Region is required" }),
|
||||
endpoint: z
|
||||
.string()
|
||||
.nonempty({ message: "Endpoint is required" })
|
||||
.default("https://s3.amazonaws.com"),
|
||||
bucket: z.string().nonempty({ message: "Bucket Name is required" }),
|
||||
provider: z
|
||||
.enum(["AWS", "Ceph", "Minio", "Alibaba", "Other"])
|
||||
.optional()
|
||||
.default("AWS"),
|
||||
storageClass: z.string().optional(),
|
||||
acl: z.string().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Azure Blob Storage Provider Schema
|
||||
*/
|
||||
export const azureBlobSchema = z.object({
|
||||
account: z.string().nonempty({ message: "Account Name is required" }),
|
||||
key: z.string().nonempty({ message: "Account Key is required" }),
|
||||
endpoint: z.string().optional(),
|
||||
container: z.string().nonempty({ message: "Container Name is required" }),
|
||||
});
|
||||
|
||||
/**
|
||||
* Dropbox Provider Schema
|
||||
*/
|
||||
export const dropboxSchema = z.object({
|
||||
token: z.string().nonempty({ message: "Access Token is required" }),
|
||||
path: z.string().optional().default("/"),
|
||||
});
|
||||
|
||||
/**
|
||||
* FTP Provider Schema
|
||||
*/
|
||||
export const ftpSchema = z.object({
|
||||
host: z.string().nonempty({ message: "FTP Host is required" }),
|
||||
port: z.number().optional().default(21),
|
||||
user: z.string().nonempty({ message: "Username is required" }),
|
||||
pass: z.string().nonempty({ message: "Password is required" }),
|
||||
secure: z.boolean().optional().default(false),
|
||||
path: z.string().optional().default("/"),
|
||||
});
|
||||
|
||||
/**
|
||||
* Exporting all schemas in a single object for convenience
|
||||
*/
|
||||
|
||||
export const providerSchemas = {
|
||||
s3: s3Schema,
|
||||
azureblob: azureBlobSchema,
|
||||
dropbox: dropboxSchema,
|
||||
ftp: ftpSchema,
|
||||
};
|
||||
|
||||
export const getObjectSchema = (schema: z.ZodTypeAny) => {
|
||||
const initialValues: any = {};
|
||||
|
||||
if (schema instanceof z.ZodObject) {
|
||||
const shape = schema._def.shape();
|
||||
|
||||
for (const [key, fieldSchema] of Object.entries(shape)) {
|
||||
if ("_def" in fieldSchema && "defaultValue" in fieldSchema._def) {
|
||||
initialValues[key] = fieldSchema._def.defaultValue();
|
||||
} else {
|
||||
initialValues[key] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return initialValues;
|
||||
};
|
||||
|
||||
export const mergeFormValues = (
|
||||
schema: z.ZodTypeAny,
|
||||
values: Record<string, any>,
|
||||
) => {
|
||||
const initialSchemaObj = getObjectSchema(schema);
|
||||
|
||||
const properties: any = {};
|
||||
|
||||
for (const key in values) {
|
||||
const keysMatch = Object.keys(initialSchemaObj).filter((k) => k === key);
|
||||
if (keysMatch.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
properties[keysMatch[0] as keyof typeof initialSchemaObj] =
|
||||
values[key] || "";
|
||||
}
|
||||
|
||||
return properties;
|
||||
};
|
||||
@@ -17,13 +17,29 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
getObjectSchema,
|
||||
mergeFormValues,
|
||||
providerSchemas,
|
||||
providersData,
|
||||
} from "../../application/domains/schema";
|
||||
import { capitalize } from "lodash";
|
||||
|
||||
const addDestination = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
@@ -38,43 +54,45 @@ type AddDestination = z.infer<typeof addDestination>;
|
||||
|
||||
export const AddDestination = () => {
|
||||
const utils = api.useUtils();
|
||||
const [provider, setProviders] = useState<keyof typeof providerSchemas>("s3");
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.destination.create.useMutation();
|
||||
const { mutateAsync: testConnection, isLoading: isLoadingConnection } =
|
||||
api.destination.testConnection.useMutation();
|
||||
const form = useForm<AddDestination>({
|
||||
const schema = providerSchemas[provider];
|
||||
const form = useForm<z.infer<typeof schema>>({
|
||||
defaultValues: {
|
||||
accessKeyId: "",
|
||||
bucket: "",
|
||||
name: "",
|
||||
region: "",
|
||||
secretAccessKey: "",
|
||||
endpoint: "",
|
||||
...getObjectSchema(schema),
|
||||
},
|
||||
resolver: zodResolver(addDestination),
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = form;
|
||||
|
||||
const onSubmit = async (data: AddDestination) => {
|
||||
await mutateAsync({
|
||||
accessKey: data.accessKeyId,
|
||||
bucket: data.bucket,
|
||||
endpoint: data.endpoint,
|
||||
name: data.name,
|
||||
region: data.region,
|
||||
secretAccessKey: data.secretAccessKey,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Destination Created");
|
||||
await utils.destination.all.invalidate();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the Destination");
|
||||
});
|
||||
const onSubmit = async (data: z.infer<typeof schema>) => {
|
||||
// await mutateAsync({
|
||||
// accessKey: data.accessKeyId,
|
||||
// bucket: data.bucket,
|
||||
// endpoint: data.endpoint,
|
||||
// name: data.name,
|
||||
// region: data.region,
|
||||
// secretAccessKey: data.secretAccessKey,
|
||||
// })
|
||||
// .then(async () => {
|
||||
// toast.success("Destination Created");
|
||||
// await utils.destination.all.invalidate();
|
||||
// })
|
||||
// .catch(() => {
|
||||
// toast.error("Error to create the Destination");
|
||||
// });
|
||||
};
|
||||
|
||||
const fields = Object.keys(schema.shape);
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
@@ -88,140 +106,117 @@ export const AddDestination = () => {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-destination-add"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
className="grid w-full gap-8 "
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"S3 Bucket"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accessKeyId"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Access Key Id</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"xcas41dasde"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="secretAccessKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Secret Access Key</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input placeholder={"asd123asdasw"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bucket"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Bucket</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input placeholder={"dokploy-bucket"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="region"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Region</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input placeholder={"us-east-1"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="endpoint"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Endpoint</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"https://us.bucket.aws/s3"}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
{fields.map((input) => (
|
||||
<FormField
|
||||
control={control}
|
||||
key={`${provider}.${input}`}
|
||||
name={`${provider}.${input}`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>{capitalize(input)}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"Value"} {...field} />
|
||||
</FormControl>
|
||||
<span className="text-sm font-medium text-destructive">
|
||||
{errors[input]?.message}
|
||||
</span>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||
<Button
|
||||
isLoading={isLoadingConnection}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
name: "Test",
|
||||
region: form.getValues("region"),
|
||||
secretAccessKey: form.getValues("secretAccessKey"),
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Connection Success");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to connect the provider");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-destination-add"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
<Select
|
||||
onValueChange={(e) => {
|
||||
setProviders(e as keyof typeof providerSchemas);
|
||||
}}
|
||||
value={provider}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{Object.keys(providerSchemas).map((registry) => (
|
||||
<SelectItem key={registry} value={registry}>
|
||||
{registry}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Providers ({providersData?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||
<Button
|
||||
isLoading={isLoadingConnection}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
const result = form.getValues()[provider];
|
||||
const hola = mergeFormValues(schema, result);
|
||||
console.log(hola);
|
||||
|
||||
// const getPropertiesByForm = (form: any) => {
|
||||
// const initialValues = getInitialValues(schema);
|
||||
// console.log(form, initialValues);
|
||||
// const properties: any = {};
|
||||
// for (const key in form) {
|
||||
// const keysMatch = Object.keys(initialValues).filter(
|
||||
// (k) => k === key,
|
||||
// );
|
||||
// if (keysMatch.length === 0) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// properties[keysMatch[0]] = form[key] || "";
|
||||
// console.log(key);
|
||||
// }
|
||||
// return properties;
|
||||
// };
|
||||
// const result = form.getValues();
|
||||
// const properties = getPropertiesByForm(result);
|
||||
// console.log(properties);
|
||||
await testConnection({
|
||||
json: {
|
||||
...hola,
|
||||
provider: provider,
|
||||
},
|
||||
// accessKey: form.getValues("accessKeyId"),
|
||||
// bucket: form.getValues("bucket"),
|
||||
// endpoint: form.getValues("endpoint"),
|
||||
// name: "Test",
|
||||
// region: form.getValues("region"),
|
||||
// secretAccessKey: form.getValues("secretAccessKey"),
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Connection Success");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to connect the provider");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
<Button
|
||||
// isLoading={isLoading}
|
||||
form="hook-form-destination-add"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
{/* */}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
1
apps/dokploy/drizzle/0038_familiar_shockwave.sql
Normal file
1
apps/dokploy/drizzle/0038_familiar_shockwave.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "destination" ADD COLUMN "schema" json;
|
||||
3829
apps/dokploy/drizzle/meta/0038_snapshot.json
Normal file
3829
apps/dokploy/drizzle/meta/0038_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -267,6 +267,13 @@
|
||||
"when": 1726988289562,
|
||||
"tag": "0037_legal_namor",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 38,
|
||||
"version": "6",
|
||||
"when": 1727036227151,
|
||||
"tag": "0038_familiar_shockwave",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -38,29 +38,31 @@ export const destinationRouter = createTRPCRouter({
|
||||
testConnection: adminProcedure
|
||||
.input(apiCreateDestination)
|
||||
.mutation(async ({ input }) => {
|
||||
const { secretAccessKey, bucket, region, endpoint, accessKey } = input;
|
||||
|
||||
try {
|
||||
const rcloneFlags = [
|
||||
// `--s3-provider=Cloudflare`,
|
||||
`--s3-access-key-id=${accessKey}`,
|
||||
`--s3-secret-access-key=${secretAccessKey}`,
|
||||
`--s3-region=${region}`,
|
||||
`--s3-endpoint=${endpoint}`,
|
||||
"--s3-no-check-bucket",
|
||||
"--s3-force-path-style",
|
||||
];
|
||||
const rcloneDestination = `:s3:${bucket}`;
|
||||
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
await execAsync(rcloneCommand);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to connect to bucket",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
console.log(input);
|
||||
// const { secretAccessKey, bucket, region, endpoint, accessKey } = input;
|
||||
// try {
|
||||
// const rcloneFlags = [
|
||||
// // `--s3-provider=Cloudflare`,
|
||||
// `--s3-access-key-id=${accessKey}`,
|
||||
// `--s3-secret-access-key=${secretAccessKey}`,
|
||||
// `--s3-region=${region}`,
|
||||
// `--s3-endpoint=${endpoint}`,
|
||||
// "--s3-no-check-bucket",
|
||||
// "--s3-force-path-style",
|
||||
// ];
|
||||
const connextion = buildRcloneCommand(input.json.provider, input.json);
|
||||
console.log(connextion);
|
||||
// const rcloneDestination = `:s3:${bucket}`;
|
||||
// const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
await execAsync(connextion);
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// throw new TRPCError({
|
||||
// code: "BAD_REQUEST",
|
||||
// message: "Error to connect to bucket",
|
||||
// cause: error,
|
||||
// });
|
||||
// }
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneDestination)
|
||||
@@ -97,3 +99,151 @@ export const destinationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
});
|
||||
function buildRcloneCommand(
|
||||
providerType: string,
|
||||
credentials: Record<string, string>,
|
||||
): string {
|
||||
let rcloneFlags: string[] = [];
|
||||
let rcloneDestination = "";
|
||||
let rcloneCommand = "";
|
||||
|
||||
switch (providerType) {
|
||||
case "s3":
|
||||
{
|
||||
const {
|
||||
accessKey,
|
||||
secretAccessKey,
|
||||
region,
|
||||
endpoint,
|
||||
bucket,
|
||||
provider,
|
||||
storageClass,
|
||||
acl,
|
||||
} = credentials;
|
||||
|
||||
if (!accessKey || !secretAccessKey || !region || !endpoint || !bucket) {
|
||||
throw new Error("Missing required S3 credentials.");
|
||||
}
|
||||
|
||||
rcloneFlags.push(`--s3-access-key-id=${accessKey}`);
|
||||
rcloneFlags.push(`--s3-secret-access-key=${secretAccessKey}`);
|
||||
rcloneFlags.push(`--s3-region=${region}`);
|
||||
rcloneFlags.push(`--s3-endpoint=${endpoint}`);
|
||||
rcloneFlags.push("--s3-no-check-bucket");
|
||||
rcloneFlags.push("--s3-force-path-style");
|
||||
|
||||
if (provider && provider !== "AWS") {
|
||||
rcloneFlags.push(`--s3-provider=${provider}`);
|
||||
}
|
||||
if (storageClass) {
|
||||
rcloneFlags.push(`--s3-storage-class=${storageClass}`);
|
||||
}
|
||||
if (acl) {
|
||||
rcloneFlags.push(`--s3-acl=${acl}`);
|
||||
}
|
||||
|
||||
rcloneDestination = `:s3:${bucket}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case "azureblob":
|
||||
{
|
||||
const { account, key, endpoint, container } = credentials;
|
||||
|
||||
if (!account || !key || !container) {
|
||||
throw new Error("Missing required Azure Blob Storage credentials.");
|
||||
}
|
||||
|
||||
rcloneFlags.push(`--azureblob-account=${account}`);
|
||||
rcloneFlags.push(`--azureblob-key=${key}`);
|
||||
if (endpoint) {
|
||||
rcloneFlags.push(`--azureblob-endpoint=${endpoint}`);
|
||||
}
|
||||
|
||||
rcloneDestination = `:azureblob:${container}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case "ftp":
|
||||
{
|
||||
const { host, port, user, pass, secure, path } = credentials;
|
||||
|
||||
if (!host || !user || !pass) {
|
||||
throw new Error("Missing required FTP credentials.");
|
||||
}
|
||||
|
||||
rcloneFlags.push(`--ftp-host=${host}`);
|
||||
rcloneFlags.push(`--ftp-user=${user}`);
|
||||
rcloneFlags.push(`--ftp-pass=${pass}`);
|
||||
if (port) {
|
||||
rcloneFlags.push(`--ftp-port=${port}`);
|
||||
}
|
||||
if (secure === "true" || secure === "1") {
|
||||
rcloneFlags.push("--ftp-tls");
|
||||
}
|
||||
|
||||
rcloneDestination = `:ftp:${path || "/"}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case "gcs":
|
||||
{
|
||||
const {
|
||||
serviceAccountFile,
|
||||
clientId,
|
||||
clientSecret,
|
||||
projectNumber,
|
||||
bucket,
|
||||
objectAcl,
|
||||
bucketAcl,
|
||||
} = credentials;
|
||||
|
||||
if (serviceAccountFile) {
|
||||
rcloneFlags.push(`--gcs-service-account-file=${serviceAccountFile}`);
|
||||
} else if (clientId && clientSecret && projectNumber) {
|
||||
rcloneFlags.push(`--gcs-client-id=${clientId}`);
|
||||
rcloneFlags.push(`--gcs-client-secret=${clientSecret}`);
|
||||
rcloneFlags.push(`--gcs-project-number=${projectNumber}`);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Missing required GCS credentials. Provide either serviceAccountFile or clientId, clientSecret, and projectNumber.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!bucket) {
|
||||
throw new Error("Bucket name is required for GCS.");
|
||||
}
|
||||
|
||||
if (objectAcl) {
|
||||
rcloneFlags.push(`--gcs-object-acl=${objectAcl}`);
|
||||
}
|
||||
if (bucketAcl) {
|
||||
rcloneFlags.push(`--gcs-bucket-acl=${bucketAcl}`);
|
||||
}
|
||||
|
||||
rcloneDestination = `:gcs:${bucket}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case "dropbox":
|
||||
{
|
||||
const { token, path } = credentials;
|
||||
|
||||
if (!token) {
|
||||
throw new Error("Access token is required for Dropbox.");
|
||||
}
|
||||
|
||||
// Warning: Passing tokens via command line can be insecure.
|
||||
rcloneFlags.push(`--dropbox-token='{"access_token":"${token}"}'`);
|
||||
rcloneDestination = `:dropbox:${path || "/"}`;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported provider type: ${providerType}`);
|
||||
}
|
||||
|
||||
// Assemble the Rclone command
|
||||
rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
return rcloneCommand;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { json, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -18,6 +18,7 @@ export const destinations = pgTable("destination", {
|
||||
region: text("region").notNull(),
|
||||
// maybe it can be null
|
||||
endpoint: text("endpoint").notNull(),
|
||||
schema: json("schema"),
|
||||
adminId: text("adminId")
|
||||
.notNull()
|
||||
.references(() => admins.adminId, { onDelete: "cascade" }),
|
||||
@@ -46,12 +47,15 @@ const createSchema = createInsertSchema(destinations, {
|
||||
|
||||
export const apiCreateDestination = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
accessKey: true,
|
||||
bucket: true,
|
||||
region: true,
|
||||
endpoint: true,
|
||||
secretAccessKey: true,
|
||||
// name: true,
|
||||
// accessKey: true,
|
||||
// bucket: true,
|
||||
// region: true,
|
||||
// endpoint: true,
|
||||
// secretAccessKey: true,
|
||||
})
|
||||
.extend({
|
||||
json: z.any(),
|
||||
})
|
||||
.required();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user