mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #242 from Dokploy/131-support-for-folder-deployment-and-drag-n-drop
feat(drag-n-drop): add support for drag n drop projects via zip #131
This commit is contained in:
commit
63a1039439
98
__test__/drop/drop.test.test.ts
Normal file
98
__test__/drop/drop.test.test.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { APPLICATIONS_PATH } from "@/server/constants";
|
||||||
|
import { unzipDrop } from "@/server/utils/builders/drop";
|
||||||
|
import AdmZip from "adm-zip";
|
||||||
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
const undici = require("undici");
|
||||||
|
globalThis.File = undici.File as any;
|
||||||
|
globalThis.FileList = undici.FileList as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock("@/server/constants", () => ({
|
||||||
|
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("unzipDrop using real zip files", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly extract a zip with a single root folder", async () => {
|
||||||
|
const appName = "single-file";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, appName);
|
||||||
|
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
||||||
|
|
||||||
|
const zipBuffer = zip.toBuffer();
|
||||||
|
const file = new File([zipBuffer], "single.zip");
|
||||||
|
await unzipDrop(file, appName);
|
||||||
|
|
||||||
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
||||||
|
const appName = "folderwithfile";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, appName);
|
||||||
|
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
|
||||||
|
|
||||||
|
const zipBuffer = zip.toBuffer();
|
||||||
|
const file = new File([zipBuffer], "single.zip");
|
||||||
|
await unzipDrop(file, appName);
|
||||||
|
|
||||||
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly extract a zip with multiple root folders", async () => {
|
||||||
|
const appName = "two-folders";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, appName);
|
||||||
|
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
|
||||||
|
|
||||||
|
const zipBuffer = zip.toBuffer();
|
||||||
|
const file = new File([zipBuffer], "single.zip");
|
||||||
|
await unzipDrop(file, appName);
|
||||||
|
|
||||||
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||||
|
expect(files.some((f) => f.name === "folder2")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly extract a zip with a single root with a file", async () => {
|
||||||
|
const appName = "nested";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, appName);
|
||||||
|
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
|
||||||
|
|
||||||
|
const zipBuffer = zip.toBuffer();
|
||||||
|
const file = new File([zipBuffer], "single.zip");
|
||||||
|
await unzipDrop(file, appName);
|
||||||
|
|
||||||
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||||
|
expect(files.some((f) => f.name === "folder2")).toBe(true);
|
||||||
|
expect(files.some((f) => f.name === "folder3")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly extract a zip with a single root with a folder", async () => {
|
||||||
|
const appName = "folder-with-sibling-file";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, appName);
|
||||||
|
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
|
||||||
|
|
||||||
|
const zipBuffer = zip.toBuffer();
|
||||||
|
const file = new File([zipBuffer], "single.zip");
|
||||||
|
await unzipDrop(file, appName);
|
||||||
|
|
||||||
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||||
|
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
BIN
__test__/drop/zips/folder-with-file.zip
Normal file
BIN
__test__/drop/zips/folder-with-file.zip
Normal file
Binary file not shown.
BIN
__test__/drop/zips/folder-with-sibling-file.zip
Normal file
BIN
__test__/drop/zips/folder-with-sibling-file.zip
Normal file
Binary file not shown.
1
__test__/drop/zips/folder1/folder1.txt
Normal file
1
__test__/drop/zips/folder1/folder1.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Gogogogogogo
|
1
__test__/drop/zips/folder2/folder2.txt
Normal file
1
__test__/drop/zips/folder2/folder2.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
gogogogogog
|
1
__test__/drop/zips/folder3/file3.txt
Normal file
1
__test__/drop/zips/folder3/file3.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
gogogogogogogogogo
|
BIN
__test__/drop/zips/nested.zip
Normal file
BIN
__test__/drop/zips/nested.zip
Normal file
Binary file not shown.
BIN
__test__/drop/zips/single-file.zip
Normal file
BIN
__test__/drop/zips/single-file.zip
Normal file
Binary file not shown.
1
__test__/drop/zips/test.txt
Normal file
1
__test__/drop/zips/test.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
dsafasdfasdf
|
BIN
__test__/drop/zips/two-folders.zip
Normal file
BIN
__test__/drop/zips/two-folders.zip
Normal file
Binary file not shown.
@ -0,0 +1,141 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Dropzone } from "@/components/ui/dropzone";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { type UploadFile, uploadFileSchema } from "@/utils/schema";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { TrashIcon } from "lucide-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SaveDragNDrop = ({ applicationId }: Props) => {
|
||||||
|
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } =
|
||||||
|
api.application.dropDeployment.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<UploadFile>({
|
||||||
|
defaultValues: {},
|
||||||
|
resolver: zodResolver(uploadFileSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
dropBuildPath: data.dropBuildPath || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
const zip = form.watch("zip");
|
||||||
|
|
||||||
|
const onSubmit = async (values: UploadFile) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("zip", values.zip);
|
||||||
|
formData.append("applicationId", applicationId);
|
||||||
|
if (values.dropBuildPath) {
|
||||||
|
formData.append("dropBuildPath", values.dropBuildPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync(formData)
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Deployment saved");
|
||||||
|
await refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to save the deployment");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="flex flex-col gap-4"
|
||||||
|
>
|
||||||
|
<div className="grid md:grid-cols-2 gap-4 ">
|
||||||
|
<div className="md:col-span-2 space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dropBuildPath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full ">
|
||||||
|
<FormLabel>Build Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="Build Path" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="zip"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full ">
|
||||||
|
<FormLabel>Zip file</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Dropzone
|
||||||
|
{...field}
|
||||||
|
dropMessage="Drop files or click here"
|
||||||
|
accept=".zip"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e instanceof FileList) {
|
||||||
|
field.onChange(e[0]);
|
||||||
|
} else {
|
||||||
|
field.onChange(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
{zip instanceof File && (
|
||||||
|
<div className="flex flex-row gap-4 items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{zip.name} ({zip.size} bytes)
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="w-fit"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
field.onChange(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-fit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={!zip}
|
||||||
|
>
|
||||||
|
Deploy{" "}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
@ -7,8 +7,9 @@ import { api } from "@/utils/api";
|
|||||||
import { GitBranch, LockIcon } from "lucide-react";
|
import { GitBranch, LockIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { SaveDragNDrop } from "./save-drag-n-drop";
|
||||||
|
|
||||||
type TabState = "github" | "docker" | "git";
|
type TabState = "github" | "docker" | "git" | "drop";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
@ -62,6 +63,12 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
>
|
>
|
||||||
Git
|
Git
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="drop"
|
||||||
|
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||||
|
>
|
||||||
|
Drop
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="github" className="w-full p-2">
|
<TabsContent value="github" className="w-full p-2">
|
||||||
{haveGithubConfigured ? (
|
{haveGithubConfigured ? (
|
||||||
@ -89,6 +96,9 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
<TabsContent value="git" className="w-full p-2">
|
<TabsContent value="git" className="w-full p-2">
|
||||||
<SaveGitProvider applicationId={applicationId} />
|
<SaveGitProvider applicationId={applicationId} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="drop" className="w-full p-2">
|
||||||
|
<SaveDragNDrop applicationId={applicationId} />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
79
components/ui/dropzone.tsx
Normal file
79
components/ui/dropzone.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { FolderIcon } from "lucide-react";
|
||||||
|
import React, { type ChangeEvent, useRef } from "react";
|
||||||
|
|
||||||
|
interface DropzoneProps
|
||||||
|
extends Omit<
|
||||||
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
"value" | "onChange"
|
||||||
|
> {
|
||||||
|
classNameWrapper?: string;
|
||||||
|
className?: string;
|
||||||
|
dropMessage: string;
|
||||||
|
onChange: (acceptedFiles: FileList | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Dropzone = React.forwardRef<HTMLDivElement, DropzoneProps>(
|
||||||
|
({ className, classNameWrapper, dropMessage, onChange, ...props }, ref) => {
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
// Function to handle drag over event
|
||||||
|
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onChange(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to handle drop event
|
||||||
|
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const { files } = e.dataTransfer;
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.files = files;
|
||||||
|
onChange(files);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to simulate a click on the file input element
|
||||||
|
const handleButtonClick = () => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-2 border-dashed bg-muted/20 hover:cursor-pointer hover:border-muted-foreground/50 ",
|
||||||
|
classNameWrapper,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CardContent
|
||||||
|
className="flex flex-col items-center justify-center space-y-2 px-2 py-4 text-xs h-96"
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center text-muted-foreground">
|
||||||
|
<span className="font-medium text-xl flex items-center gap-2">
|
||||||
|
<FolderIcon className="size-6 text-muted-foreground" />
|
||||||
|
{dropMessage}
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
{...props}
|
||||||
|
value={undefined}
|
||||||
|
ref={inputRef}
|
||||||
|
type="file"
|
||||||
|
className={cn("hidden", className)}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange(e.target.files)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
1
drizzle/0022_warm_colonel_america.sql
Normal file
1
drizzle/0022_warm_colonel_america.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TYPE "sourceType" ADD VALUE 'drop';
|
1
drizzle/0023_icy_maverick.sql
Normal file
1
drizzle/0023_icy_maverick.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "dropBuildPath" text;
|
2920
drizzle/meta/0022_snapshot.json
Normal file
2920
drizzle/meta/0022_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2926
drizzle/meta/0023_snapshot.json
Normal file
2926
drizzle/meta/0023_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -155,6 +155,20 @@
|
|||||||
"when": 1721370423752,
|
"when": 1721370423752,
|
||||||
"tag": "0021_premium_sebastian_shaw",
|
"tag": "0021_premium_sebastian_shaw",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 22,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721531163852,
|
||||||
|
"tag": "0022_warm_colonel_america",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 23,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721542782659,
|
||||||
|
"tag": "0023_icy_maverick",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
10
package.json
10
package.json
@ -64,6 +64,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toggle": "^1.0.3",
|
"@radix-ui/react-toggle": "^1.0.3",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
|
"@react-email/components": "^0.0.21",
|
||||||
"@tanstack/react-query": "^4.36.1",
|
"@tanstack/react-query": "^4.36.1",
|
||||||
"@tanstack/react-table": "^8.16.0",
|
"@tanstack/react-table": "^8.16.0",
|
||||||
"@trpc/client": "^10.43.6",
|
"@trpc/client": "^10.43.6",
|
||||||
@ -74,6 +75,7 @@
|
|||||||
"@uiw/react-codemirror": "^4.22.1",
|
"@uiw/react-codemirror": "^4.22.1",
|
||||||
"@xterm/addon-attach": "0.10.0",
|
"@xterm/addon-attach": "0.10.0",
|
||||||
"@xterm/xterm": "^5.4.0",
|
"@xterm/xterm": "^5.4.0",
|
||||||
|
"adm-zip": "^0.5.14",
|
||||||
"bcrypt": "5.1.1",
|
"bcrypt": "5.1.1",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
@ -99,7 +101,6 @@
|
|||||||
"lucide-react": "^0.312.0",
|
"lucide-react": "^0.312.0",
|
||||||
"nanoid": "3",
|
"nanoid": "3",
|
||||||
"next": "^14.1.3",
|
"next": "^14.1.3",
|
||||||
"@react-email/components": "^0.0.21",
|
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"node-os-utils": "1.3.7",
|
"node-os-utils": "1.3.7",
|
||||||
"node-pty": "1.0.0",
|
"node-pty": "1.0.0",
|
||||||
@ -121,16 +122,18 @@
|
|||||||
"tailwind-merge": "^2.2.0",
|
"tailwind-merge": "^2.2.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tar-fs": "3.0.5",
|
"tar-fs": "3.0.5",
|
||||||
|
"undici": "^6.19.2",
|
||||||
"use-resize-observer": "9.1.0",
|
"use-resize-observer": "9.1.0",
|
||||||
"ws": "8.16.0",
|
"ws": "8.16.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"zod": "^3.23.4"
|
"zod": "^3.23.4",
|
||||||
|
"zod-form-data": "^2.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/nodemailer": "^6.4.15",
|
|
||||||
"@biomejs/biome": "1.8.3",
|
"@biomejs/biome": "1.8.3",
|
||||||
"@commitlint/cli": "^19.3.0",
|
"@commitlint/cli": "^19.3.0",
|
||||||
"@commitlint/config-conventional": "^19.2.2",
|
"@commitlint/config-conventional": "^19.2.2",
|
||||||
|
"@types/adm-zip": "^0.5.5",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
"@types/dockerode": "3.3.23",
|
"@types/dockerode": "3.3.23",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
@ -138,6 +141,7 @@
|
|||||||
"@types/node": "^18.17.0",
|
"@types/node": "^18.17.0",
|
||||||
"@types/node-os-utils": "1.3.4",
|
"@types/node-os-utils": "1.3.4",
|
||||||
"@types/node-schedule": "2.1.6",
|
"@types/node-schedule": "2.1.6",
|
||||||
|
"@types/nodemailer": "^6.4.15",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.2.37",
|
"@types/react": "^18.2.37",
|
||||||
"@types/react-dom": "^18.2.15",
|
"@types/react-dom": "^18.2.15",
|
||||||
|
@ -18,7 +18,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
res.status(401).json({ message: "Unauthorized" });
|
res.status(401).json({ message: "Unauthorized" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return createOpenApiNextHandler({
|
return createOpenApiNextHandler({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { createNextApiHandler } from "@trpc/server/adapters/next";
|
|
||||||
|
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { createTRPCContext } from "@/server/api/trpc";
|
import { createTRPCContext } from "@/server/api/trpc";
|
||||||
|
import { createNextApiHandler } from "@trpc/server/adapters/next";
|
||||||
|
import { nodeHTTPFormDataContentTypeHandler } from "@trpc/server/adapters/node-http/content-type/form-data";
|
||||||
|
import { nodeHTTPJSONContentTypeHandler } from "@trpc/server/adapters/node-http/content-type/json";
|
||||||
|
|
||||||
// export API handler
|
// export API handler
|
||||||
export default createNextApiHandler({
|
export default createNextApiHandler({
|
||||||
@ -15,4 +16,15 @@ export default createNextApiHandler({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
experimental_contentTypeHandlers: [
|
||||||
|
nodeHTTPFormDataContentTypeHandler(),
|
||||||
|
nodeHTTPJSONContentTypeHandler(),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
bodyParser: false,
|
||||||
|
sizeLimit: "1gb",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
@ -125,6 +125,9 @@ dependencies:
|
|||||||
'@xterm/xterm':
|
'@xterm/xterm':
|
||||||
specifier: ^5.4.0
|
specifier: ^5.4.0
|
||||||
version: 5.4.0
|
version: 5.4.0
|
||||||
|
adm-zip:
|
||||||
|
specifier: ^0.5.14
|
||||||
|
version: 0.5.14
|
||||||
bcrypt:
|
bcrypt:
|
||||||
specifier: 5.1.1
|
specifier: 5.1.1
|
||||||
version: 5.1.1
|
version: 5.1.1
|
||||||
@ -263,6 +266,9 @@ dependencies:
|
|||||||
tar-fs:
|
tar-fs:
|
||||||
specifier: 3.0.5
|
specifier: 3.0.5
|
||||||
version: 3.0.5
|
version: 3.0.5
|
||||||
|
undici:
|
||||||
|
specifier: ^6.19.2
|
||||||
|
version: 6.19.2
|
||||||
use-resize-observer:
|
use-resize-observer:
|
||||||
specifier: 9.1.0
|
specifier: 9.1.0
|
||||||
version: 9.1.0(react-dom@18.2.0)(react@18.2.0)
|
version: 9.1.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -275,6 +281,9 @@ dependencies:
|
|||||||
zod:
|
zod:
|
||||||
specifier: ^3.23.4
|
specifier: ^3.23.4
|
||||||
version: 3.23.4
|
version: 3.23.4
|
||||||
|
zod-form-data:
|
||||||
|
specifier: ^2.0.2
|
||||||
|
version: 2.0.2(zod@3.23.4)
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@biomejs/biome':
|
'@biomejs/biome':
|
||||||
@ -286,6 +295,9 @@ devDependencies:
|
|||||||
'@commitlint/config-conventional':
|
'@commitlint/config-conventional':
|
||||||
specifier: ^19.2.2
|
specifier: ^19.2.2
|
||||||
version: 19.2.2
|
version: 19.2.2
|
||||||
|
'@types/adm-zip':
|
||||||
|
specifier: ^0.5.5
|
||||||
|
version: 0.5.5
|
||||||
'@types/bcrypt':
|
'@types/bcrypt':
|
||||||
specifier: 5.0.2
|
specifier: 5.0.2
|
||||||
version: 5.0.2
|
version: 5.0.2
|
||||||
@ -5470,6 +5482,12 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@types/adm-zip@0.5.5:
|
||||||
|
resolution: {integrity: sha512-YCGstVMjc4LTY5uK9/obvxBya93axZOVOyf2GSUulADzmLhYE45u2nAssCs/fWBs1Ifq5Vat75JTPwd5XZoPJw==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.19.24
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/aws-lambda@8.10.136:
|
/@types/aws-lambda@8.10.136:
|
||||||
resolution: {integrity: sha512-cmmgqxdVGhxYK9lZMYYXYRJk6twBo53ivtXjIUEFZxfxe4TkZTZBK3RRWrY2HjJcUIix0mdifn15yjOAat5lTA==}
|
resolution: {integrity: sha512-cmmgqxdVGhxYK9lZMYYXYRJk6twBo53ivtXjIUEFZxfxe4TkZTZBK3RRWrY2HjJcUIix0mdifn15yjOAat5lTA==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -6040,6 +6058,11 @@ packages:
|
|||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
/adm-zip@0.5.14:
|
||||||
|
resolution: {integrity: sha512-DnyqqifT4Jrcvb8USYjp6FHtBpEIz1mnXu6pTRHZ0RL69LbQYiO+0lDFg5+OKA7U29oWSs3a/i8fhn8ZcceIWg==}
|
||||||
|
engines: {node: '>=12.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/agent-base@6.0.2:
|
/agent-base@6.0.2:
|
||||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||||
engines: {node: '>= 6.0.0'}
|
engines: {node: '>= 6.0.0'}
|
||||||
@ -11152,6 +11175,11 @@ packages:
|
|||||||
/undici-types@5.26.5:
|
/undici-types@5.26.5:
|
||||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||||
|
|
||||||
|
/undici@6.19.2:
|
||||||
|
resolution: {integrity: sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==}
|
||||||
|
engines: {node: '>=18.17'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/unenv@1.9.0:
|
/unenv@1.9.0:
|
||||||
resolution: {integrity: sha512-QKnFNznRxmbOF1hDgzpqrlIf6NC5sbZ2OJ+5Wl3OX8uM+LUJXbj4TXvLJCtwbPTmbMHCLIz6JLKNinNsMShK9g==}
|
resolution: {integrity: sha512-QKnFNznRxmbOF1hDgzpqrlIf6NC5sbZ2OJ+5Wl3OX8uM+LUJXbj4TXvLJCtwbPTmbMHCLIz6JLKNinNsMShK9g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -11708,6 +11736,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-jEA1znR7b4C/NnaycInCU6h/d15ZzCd1jmsruqOKnZP6WXQSMH3W2GL+OXbkruslU4h+Tzuos0HdswzRUk/Vgg==}
|
resolution: {integrity: sha512-jEA1znR7b4C/NnaycInCU6h/d15ZzCd1jmsruqOKnZP6WXQSMH3W2GL+OXbkruslU4h+Tzuos0HdswzRUk/Vgg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/zod-form-data@2.0.2(zod@3.23.4):
|
||||||
|
resolution: {integrity: sha512-sKTi+k0fvkxdakD0V5rq+9WVJA3cuTQUfEmNqvHrTzPLvjfLmkkBLfR0ed3qOi9MScJXTHIDH/jUNnEJ3CBX4g==}
|
||||||
|
peerDependencies:
|
||||||
|
zod: '>= 3.11.0'
|
||||||
|
dependencies:
|
||||||
|
zod: 3.23.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
/zod-to-json-schema@3.23.1(zod@3.23.4):
|
/zod-to-json-schema@3.23.1(zod@3.23.4):
|
||||||
resolution: {integrity: sha512-oT9INvydob1XV0v1d2IadrR74rLtDInLvDFfAa1CG0Pmg/vxATk7I2gSelfj271mbzeM4Da0uuDQE/Nkj3DWNw==}
|
resolution: {integrity: sha512-oT9INvydob1XV0v1d2IadrR74rLtDInLvDFfAa1CG0Pmg/vxATk7I2gSelfj271mbzeM4Da0uuDQE/Nkj3DWNw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import {
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
uploadProcedure,
|
||||||
|
} from "@/server/api/trpc";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import {
|
import {
|
||||||
apiCreateApplication,
|
apiCreateApplication,
|
||||||
@ -52,6 +56,9 @@ import {
|
|||||||
import { removeDeployments } from "../services/deployment";
|
import { removeDeployments } from "../services/deployment";
|
||||||
import { addNewService, checkServiceAccess } from "../services/user";
|
import { addNewService, checkServiceAccess } from "../services/user";
|
||||||
|
|
||||||
|
import { unzipDrop } from "@/server/utils/builders/drop";
|
||||||
|
import { uploadFileSchema } from "@/utils/schema";
|
||||||
|
|
||||||
export const applicationRouter = createTRPCRouter({
|
export const applicationRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreateApplication)
|
.input(apiCreateApplication)
|
||||||
@ -324,6 +331,45 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
return traefikConfig;
|
return traefikConfig;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
dropDeployment: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
path: "/drop-deployment",
|
||||||
|
method: "POST",
|
||||||
|
override: true,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.use(uploadProcedure)
|
||||||
|
.input(uploadFileSchema)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const zipFile = input.zip;
|
||||||
|
|
||||||
|
updateApplication(input.applicationId as string, {
|
||||||
|
sourceType: "drop",
|
||||||
|
dropBuildPath: input.dropBuildPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = await findApplicationById(input.applicationId as string);
|
||||||
|
await unzipDrop(zipFile, app.appName);
|
||||||
|
|
||||||
|
const jobData: DeploymentJob = {
|
||||||
|
applicationId: app.applicationId,
|
||||||
|
titleLog: "Manual deployment",
|
||||||
|
descriptionLog: "",
|
||||||
|
type: "deploy",
|
||||||
|
applicationType: "application",
|
||||||
|
};
|
||||||
|
await myQueue.add(
|
||||||
|
"deployments",
|
||||||
|
{ ...jobData },
|
||||||
|
{
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
updateTraefikConfig: protectedProcedure
|
updateTraefikConfig: protectedProcedure
|
||||||
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
|
@ -157,6 +157,8 @@ export const deployApplication = async ({
|
|||||||
} else if (application.sourceType === "git") {
|
} else if (application.sourceType === "git") {
|
||||||
await cloneGitRepository(application, deployment.logPath);
|
await cloneGitRepository(application, deployment.logPath);
|
||||||
await buildApplication(application, deployment.logPath);
|
await buildApplication(application, deployment.logPath);
|
||||||
|
} else if (application.sourceType === "drop") {
|
||||||
|
await buildApplication(application, deployment.logPath);
|
||||||
}
|
}
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
await updateApplicationStatus(applicationId, "done");
|
await updateApplicationStatus(applicationId, "done");
|
||||||
@ -216,6 +218,8 @@ export const rebuildApplication = async ({
|
|||||||
await buildDocker(application, deployment.logPath);
|
await buildDocker(application, deployment.logPath);
|
||||||
} else if (application.sourceType === "git") {
|
} else if (application.sourceType === "git") {
|
||||||
await buildApplication(application, deployment.logPath);
|
await buildApplication(application, deployment.logPath);
|
||||||
|
} else if (application.sourceType === "drop") {
|
||||||
|
await buildApplication(application, deployment.logPath);
|
||||||
}
|
}
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
await updateApplicationStatus(applicationId, "done");
|
await updateApplicationStatus(applicationId, "done");
|
||||||
|
@ -15,10 +15,6 @@ import { type Application, findApplicationById } from "./application";
|
|||||||
import { type Compose, findComposeById } from "./compose";
|
import { type Compose, findComposeById } from "./compose";
|
||||||
|
|
||||||
export type Deployment = typeof deployments.$inferSelect;
|
export type Deployment = typeof deployments.$inferSelect;
|
||||||
type CreateDeploymentInput = Omit<
|
|
||||||
Deployment,
|
|
||||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const findDeploymentById = async (applicationId: string) => {
|
export const findDeploymentById = async (applicationId: string) => {
|
||||||
const application = await db.query.deployments.findFirst({
|
const application = await db.query.deployments.findFirst({
|
||||||
|
@ -12,6 +12,11 @@ import { db } from "@/server/db";
|
|||||||
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
|
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
|
||||||
import { TRPCError, initTRPC } from "@trpc/server";
|
import { TRPCError, initTRPC } from "@trpc/server";
|
||||||
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
|
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||||
|
import {
|
||||||
|
experimental_createMemoryUploadHandler,
|
||||||
|
experimental_isMultipartFormDataRequest,
|
||||||
|
experimental_parseMultipartFormData,
|
||||||
|
} from "@trpc/server/adapters/node-http/content-type/form-data";
|
||||||
import type { Session, User } from "lucia";
|
import type { Session, User } from "lucia";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
@ -158,6 +163,24 @@ export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const uploadProcedure = async (opts: any) => {
|
||||||
|
if (!experimental_isMultipartFormDataRequest(opts.ctx.req)) {
|
||||||
|
return opts.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = await experimental_parseMultipartFormData(
|
||||||
|
opts.ctx.req,
|
||||||
|
experimental_createMemoryUploadHandler({
|
||||||
|
// 2GB
|
||||||
|
maxPartSize: 1024 * 1024 * 1024 * 2,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return opts.next({
|
||||||
|
rawInput: formData,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
||||||
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
|
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
@ -22,7 +22,12 @@ import { security } from "./security";
|
|||||||
import { applicationStatus } from "./shared";
|
import { applicationStatus } from "./shared";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
|
||||||
export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]);
|
export const sourceType = pgEnum("sourceType", [
|
||||||
|
"docker",
|
||||||
|
"git",
|
||||||
|
"github",
|
||||||
|
"drop",
|
||||||
|
]);
|
||||||
|
|
||||||
export const buildType = pgEnum("buildType", [
|
export const buildType = pgEnum("buildType", [
|
||||||
"dockerfile",
|
"dockerfile",
|
||||||
@ -128,6 +133,8 @@ export const applications = pgTable("application", {
|
|||||||
customGitBuildPath: text("customGitBuildPath"),
|
customGitBuildPath: text("customGitBuildPath"),
|
||||||
customGitSSHKey: text("customGitSSHKey"),
|
customGitSSHKey: text("customGitSSHKey"),
|
||||||
dockerfile: text("dockerfile"),
|
dockerfile: text("dockerfile"),
|
||||||
|
// Drop
|
||||||
|
dropBuildPath: text("dropBuildPath"),
|
||||||
// Docker swarm json
|
// Docker swarm json
|
||||||
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
|
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
|
||||||
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
|
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
|
||||||
|
57
server/utils/builders/drop.ts
Normal file
57
server/utils/builders/drop.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path, { join } from "node:path";
|
||||||
|
import { APPLICATIONS_PATH } from "@/server/constants";
|
||||||
|
import AdmZip from "adm-zip";
|
||||||
|
import { recreateDirectory } from "../filesystem/directory";
|
||||||
|
|
||||||
|
export const unzipDrop = async (zipFile: File, appName: string) => {
|
||||||
|
try {
|
||||||
|
const basePath = APPLICATIONS_PATH;
|
||||||
|
const outputPath = join(basePath, appName);
|
||||||
|
await recreateDirectory(outputPath);
|
||||||
|
const arrayBuffer = await zipFile.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
|
const zip = new AdmZip(buffer);
|
||||||
|
const zipEntries = zip.getEntries();
|
||||||
|
|
||||||
|
const rootEntries = zipEntries.filter(
|
||||||
|
(entry) =>
|
||||||
|
entry.entryName.split("/").length === 1 ||
|
||||||
|
(entry.entryName.split("/").length === 2 &&
|
||||||
|
entry.entryName.endsWith("/")),
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasSingleRootFolder = !!(
|
||||||
|
rootEntries.length === 1 && rootEntries[0]?.isDirectory
|
||||||
|
);
|
||||||
|
const rootFolderName = hasSingleRootFolder
|
||||||
|
? rootEntries[0]?.entryName.split("/")[0]
|
||||||
|
: "";
|
||||||
|
|
||||||
|
for (const entry of zipEntries) {
|
||||||
|
let filePath = entry.entryName;
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasSingleRootFolder &&
|
||||||
|
rootFolderName &&
|
||||||
|
filePath.startsWith(`${rootFolderName}/`)
|
||||||
|
) {
|
||||||
|
filePath = filePath.slice(rootFolderName?.length + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filePath) continue;
|
||||||
|
|
||||||
|
const fullPath = path.join(outputPath, filePath);
|
||||||
|
if (entry.isDirectory) {
|
||||||
|
await fs.mkdir(fullPath, { recursive: true });
|
||||||
|
} else {
|
||||||
|
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
||||||
|
await fs.writeFile(fullPath, entry.getData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing ZIP file:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
@ -59,8 +59,15 @@ export const removeMonitoringDirectory = async (appName: string) => {
|
|||||||
export const getBuildAppDirectory = (application: Application) => {
|
export const getBuildAppDirectory = (application: Application) => {
|
||||||
const { appName, buildType, sourceType, customGitBuildPath, dockerfile } =
|
const { appName, buildType, sourceType, customGitBuildPath, dockerfile } =
|
||||||
application;
|
application;
|
||||||
const buildPath =
|
let buildPath = "";
|
||||||
sourceType === "github" ? application?.buildPath : customGitBuildPath;
|
|
||||||
|
if (sourceType === "github") {
|
||||||
|
buildPath = application?.buildPath || "";
|
||||||
|
} else if (sourceType === "drop") {
|
||||||
|
buildPath = application?.dropBuildPath || "";
|
||||||
|
} else if (sourceType === "git") {
|
||||||
|
buildPath = customGitBuildPath || "";
|
||||||
|
}
|
||||||
if (buildType === "dockerfile") {
|
if (buildType === "dockerfile") {
|
||||||
return path.join(
|
return path.join(
|
||||||
APPLICATIONS_PATH,
|
APPLICATIONS_PATH,
|
||||||
|
17
utils/api.ts
17
utils/api.ts
@ -5,7 +5,11 @@
|
|||||||
* We also create a few inference helpers for input and output types.
|
* We also create a few inference helpers for input and output types.
|
||||||
*/
|
*/
|
||||||
import type { AppRouter } from "@/server/api/root";
|
import type { AppRouter } from "@/server/api/root";
|
||||||
import { httpBatchLink } from "@trpc/client";
|
import {
|
||||||
|
experimental_formDataLink,
|
||||||
|
httpBatchLink,
|
||||||
|
splitLink,
|
||||||
|
} from "@trpc/client";
|
||||||
import { createTRPCNext } from "@trpc/next";
|
import { createTRPCNext } from "@trpc/next";
|
||||||
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
@ -18,6 +22,7 @@ const getBaseUrl = () => {
|
|||||||
/** A set of type-safe react-query hooks for your tRPC API. */
|
/** A set of type-safe react-query hooks for your tRPC API. */
|
||||||
export const api = createTRPCNext<AppRouter>({
|
export const api = createTRPCNext<AppRouter>({
|
||||||
config() {
|
config() {
|
||||||
|
const url = `${getBaseUrl()}/api/trpc`;
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
* Transformer used for data de-serialization from the server.
|
* Transformer used for data de-serialization from the server.
|
||||||
@ -32,8 +37,14 @@ export const api = createTRPCNext<AppRouter>({
|
|||||||
* @see https://trpc.io/docs/links
|
* @see https://trpc.io/docs/links
|
||||||
*/
|
*/
|
||||||
links: [
|
links: [
|
||||||
httpBatchLink({
|
splitLink({
|
||||||
url: `${getBaseUrl()}/api/trpc`,
|
condition: (op) => op.input instanceof FormData,
|
||||||
|
true: experimental_formDataLink({
|
||||||
|
url,
|
||||||
|
}),
|
||||||
|
false: httpBatchLink({
|
||||||
|
url,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
16
utils/schema.ts
Normal file
16
utils/schema.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { zfd } from "zod-form-data";
|
||||||
|
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
const undici = require("undici");
|
||||||
|
globalThis.File = undici.File as any;
|
||||||
|
globalThis.FileList = undici.FileList as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadFileSchema = zfd.formData({
|
||||||
|
applicationId: z.string().optional(),
|
||||||
|
zip: zfd.file(),
|
||||||
|
dropBuildPath: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UploadFile = z.infer<typeof uploadFileSchema>;
|
Loading…
Reference in New Issue
Block a user