mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(drag-n-drop): add support for drag n drop projects via zip #131
This commit is contained in:
@@ -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 {
|
||||
apiCreateApplication,
|
||||
@@ -52,6 +56,9 @@ import {
|
||||
import { removeDeployments } from "../services/deployment";
|
||||
import { addNewService, checkServiceAccess } from "../services/user";
|
||||
|
||||
import { unzipDrop } from "@/server/utils/builders/drop";
|
||||
import { uploadFileSchema } from "@/utils/schema";
|
||||
|
||||
export const applicationRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateApplication)
|
||||
@@ -324,6 +331,45 @@ export const applicationRouter = createTRPCRouter({
|
||||
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
|
||||
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
|
||||
@@ -157,6 +157,8 @@ export const deployApplication = async ({
|
||||
} else if (application.sourceType === "git") {
|
||||
await cloneGitRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "drop") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
@@ -216,6 +218,8 @@ export const rebuildApplication = async ({
|
||||
await buildDocker(application, deployment.logPath);
|
||||
} else if (application.sourceType === "git") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "drop") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
@@ -15,10 +15,6 @@ import { type Application, findApplicationById } from "./application";
|
||||
import { type Compose, findComposeById } from "./compose";
|
||||
|
||||
export type Deployment = typeof deployments.$inferSelect;
|
||||
type CreateDeploymentInput = Omit<
|
||||
Deployment,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>;
|
||||
|
||||
export const findDeploymentById = async (applicationId: string) => {
|
||||
const application = await db.query.deployments.findFirst({
|
||||
|
||||
@@ -12,6 +12,11 @@ import { db } from "@/server/db";
|
||||
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
|
||||
import { TRPCError, initTRPC } from "@trpc/server";
|
||||
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 superjson from "superjson";
|
||||
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 }) => {
|
||||
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
@@ -22,7 +22,12 @@ import { security } from "./security";
|
||||
import { applicationStatus } from "./shared";
|
||||
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", [
|
||||
"dockerfile",
|
||||
@@ -128,6 +133,8 @@ export const applications = pgTable("application", {
|
||||
customGitBuildPath: text("customGitBuildPath"),
|
||||
customGitSSHKey: text("customGitSSHKey"),
|
||||
dockerfile: text("dockerfile"),
|
||||
// Drop
|
||||
dropBuildPath: text("dropBuildPath"),
|
||||
// Docker swarm json
|
||||
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
|
||||
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) => {
|
||||
const { appName, buildType, sourceType, customGitBuildPath, dockerfile } =
|
||||
application;
|
||||
const buildPath =
|
||||
sourceType === "github" ? application?.buildPath : customGitBuildPath;
|
||||
let buildPath = "";
|
||||
|
||||
if (sourceType === "github") {
|
||||
buildPath = application?.buildPath || "";
|
||||
} else if (sourceType === "drop") {
|
||||
buildPath = application?.dropBuildPath || "";
|
||||
} else if (sourceType === "git") {
|
||||
buildPath = customGitBuildPath || "";
|
||||
}
|
||||
if (buildType === "dockerfile") {
|
||||
return path.join(
|
||||
APPLICATIONS_PATH,
|
||||
|
||||
Reference in New Issue
Block a user