diff --git a/apps/dokploy/components/dashboard/application/patches/show-patches.tsx b/apps/dokploy/components/dashboard/application/patches/show-patches.tsx index ade0edb87..2a090e39f 100644 --- a/apps/dokploy/components/dashboard/application/patches/show-patches.tsx +++ b/apps/dokploy/components/dashboard/application/patches/show-patches.tsx @@ -33,22 +33,8 @@ export const ShowPatches = ({ id, type }: Props) => { const utils = api.useUtils(); - const queryMap = { - application: () => - api.patch.byApplicationId.useQuery( - { applicationId: id }, - { enabled: !!id }, - ), - compose: () => - api.patch.byComposeId.useQuery({ composeId: id }, { enabled: !!id }), - }; - - const { data: patches, isLoading: isPatchesLoading } = queryMap[type] - ? queryMap[type]() - : api.patch.byApplicationId.useQuery( - { applicationId: id }, - { enabled: !!id }, - ); + const { data: patches, isLoading: isPatchesLoading } = + api.patch.byEntityId.useQuery({ id, type }, { enabled: !!id }); const mutationMap = { application: () => api.patch.delete.useMutation(), @@ -166,11 +152,9 @@ export const ShowPatches = ({ id, type }: Props) => { }) .then(() => { toast.success("Patch updated"); - utils.patch.byApplicationId.invalidate({ - applicationId: id, - }); - utils.patch.byComposeId.invalidate({ - composeId: id, + utils.patch.byEntityId.invalidate({ + id, + type, }); }) .catch((err) => { @@ -190,11 +174,9 @@ export const ShowPatches = ({ id, type }: Props) => { mutateAsync({ patchId: patch.patchId }) .then(() => { toast.success("Patch deleted"); - utils.patch.byApplicationId.invalidate({ - applicationId: id, - }); - utils.patch.byComposeId.invalidate({ - composeId: id, + utils.patch.byEntityId.invalidate({ + id, + type, }); }) .catch((err) => { diff --git a/apps/dokploy/server/api/routers/patch.ts b/apps/dokploy/server/api/routers/patch.ts index 4c8dfefe5..1c9eb4fe3 100644 --- a/apps/dokploy/server/api/routers/patch.ts +++ b/apps/dokploy/server/api/routers/patch.ts @@ -8,9 +8,7 @@ import { findComposeById, findPatchByFilePath, findPatchById, - findPatchesByApplicationId, - findPatchesByComposeId, - generatePatch, + findPatchesByEntityId, readPatchRepoDirectory, readPatchRepoFile, updatePatch, @@ -26,8 +24,6 @@ import { apiCreatePatch, apiDeletePatch, apiFindPatch, - apiFindPatchesByApplicationId, - apiFindPatchesByComposeId, apiTogglePatchEnabled, apiUpdatePatch, } from "@/server/db/schema"; @@ -77,38 +73,37 @@ export const patchRouter = createTRPCRouter({ return await findPatchById(input.patchId); }), - byApplicationId: protectedProcedure - .input(apiFindPatchesByApplicationId) + byEntityId: protectedProcedure + .input( + z.object({ id: z.string(), type: z.enum(["application", "compose"]) }), + ) .query(async ({ input, ctx }) => { - const app = await findApplicationById(input.applicationId); - if ( - app.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); + if (input.type === "application") { + const app = await findApplicationById(input.id); + if ( + app.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + } else if (input.type === "compose") { + const compose = await findComposeById(input.id); + if ( + compose.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this compose", + }); + } } + const result = await findPatchesByEntityId(input.id, input.type); - return await findPatchesByApplicationId(input.applicationId); - }), - - byComposeId: protectedProcedure - .input(apiFindPatchesByComposeId) - .query(async ({ input, ctx }) => { - const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this compose", - }); - } - - return await findPatchesByComposeId(input.composeId); + return result; }), update: protectedProcedure diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index a1517514b..458ab34f5 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -44,10 +44,7 @@ import { issueCommentExists, updateIssueComment, } from "./github"; -import { - findPatchesByApplicationId, - generateApplyPatchesCommand, -} from "./patch"; +import { generateApplyPatchesCommand } from "./patch"; import { findPreviewDeploymentById, updatePreviewDeployment, @@ -206,20 +203,12 @@ export const deployApplication = async ({ command += await buildRemoteDocker(application); } - // Apply patches after cloning (for non-docker sources only) if (application.sourceType !== "docker") { - const patches = await findPatchesByApplicationId( - application.applicationId, - ); - const enabledPatches = patches.filter((p) => p.enabled); - if (enabledPatches.length > 0) { - command += generateApplyPatchesCommand({ - appName: application.appName, - type: "application", - serverId, - patches: enabledPatches, - }); - } + command += await generateApplyPatchesCommand({ + id: application.applicationId, + type: "application", + serverId, + }); } command += await getBuildCommand(application); diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 7ab7c1745..e6aab7672 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -40,7 +40,7 @@ import { updateDeployment, updateDeploymentStatus, } from "./deployment"; -import { findPatchesByComposeId, generateApplyPatchesCommand } from "./patch"; +import { generateApplyPatchesCommand } from "./patch"; import { validUniqueServerAppName } from "./project"; export type Compose = typeof compose.$inferSelect; @@ -249,24 +249,12 @@ export const deployCompose = async ({ await execAsync(commandWithLog); } - // Apply patches after cloning (for non-raw sources only) if (compose.sourceType !== "raw") { - const patches = await findPatchesByComposeId(compose.composeId); - const enabledPatches = patches.filter((p) => p.enabled); - if (enabledPatches.length > 0) { - const patchCommand = generateApplyPatchesCommand({ - appName: compose.appName, - type: "compose", - serverId: compose.serverId, - patches: enabledPatches, - }); - const patchCommandWithLog = `(${patchCommand}) >> ${deployment.logPath} 2>&1`; - if (compose.serverId) { - await execAsyncRemote(compose.serverId, patchCommandWithLog); - } else { - await execAsync(patchCommandWithLog); - } - } + command += await generateApplyPatchesCommand({ + id: compose.composeId, + type: "compose", + serverId: compose.serverId, + }); } command = "set -e;"; diff --git a/packages/server/src/services/patch.ts b/packages/server/src/services/patch.ts index 0be127a58..f14a6b082 100644 --- a/packages/server/src/services/patch.ts +++ b/packages/server/src/services/patch.ts @@ -2,12 +2,11 @@ import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import { db } from "@dokploy/server/db"; import { type apiCreatePatch, patch } from "@dokploy/server/db/schema"; -import { - execAsync, - execAsyncRemote, -} from "@dokploy/server/utils/process/execAsync"; import { TRPCError } from "@trpc/server"; -import { and, eq, isNotNull } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; +import { encodeBase64 } from "../utils/docker/utils"; +import { findApplicationById } from "./application"; +import { findComposeById } from "./compose"; export type Patch = typeof patch.$inferSelect; @@ -54,33 +53,29 @@ export const findPatchById = async (patchId: string) => { return result; }; -export const findPatchesByApplicationId = async (applicationId: string) => { +export const findPatchesByEntityId = async ( + id: string, + type: "application" | "compose", +) => { return await db.query.patch.findMany({ - where: and( - eq(patch.applicationId, applicationId), - isNotNull(patch.applicationId), + where: eq( + type === "application" ? patch.applicationId : patch.composeId, + id, ), orderBy: (patch, { asc }) => [asc(patch.filePath)], }); }; -export const findPatchesByComposeId = async (composeId: string) => { - return await db.query.patch.findMany({ - where: and(eq(patch.composeId, composeId), isNotNull(patch.composeId)), - orderBy: (patch, { asc }) => [asc(patch.filePath)], - }); -}; - export const findPatchByFilePath = async ( filePath: string, id: string, type: "application" | "compose", ) => { return await db.query.patch.findFirst({ - where: - type === "application" - ? and(eq(patch.filePath, filePath), eq(patch.applicationId, id)) - : and(eq(patch.filePath, filePath), eq(patch.composeId, id)), + where: and( + eq(patch.filePath, filePath), + eq(type === "application" ? patch.applicationId : patch.composeId, id), + ), }); }; @@ -111,76 +106,45 @@ export const deletePatch = async (patchId: string) => { return result[0]; }; -// Patch Application Functions - interface ApplyPatchesOptions { - appName: string; + id: string; type: "application" | "compose"; serverId: string | null; - patches: Patch[]; } -/** - * Generate shell commands to apply patches to cloned repository - * Uses git apply to apply unified diff patches - */ -export const generateApplyPatchesCommand = ({ - appName, +export const generateApplyPatchesCommand = async ({ + id, type, - patches, serverId, -}: ApplyPatchesOptions): string => { +}: ApplyPatchesOptions) => { + const entity = + type === "application" + ? await findApplicationById(id) + : await findComposeById(id); + const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId); + const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; + const codePath = join(basePath, entity.appName, "code"); + + const patches = (await findPatchesByEntityId(id, type)).filter( + (p) => p.enabled, + ); + if (patches.length === 0) { return ""; } - const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId); - const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; - const codePath = join(basePath, appName, "code"); - let command = `echo "Applying ${patches.length} patch(es)...";`; for (const p of patches) { - // Create a temporary patch file and apply it - const patchFileName = `/tmp/patch_${p.patchId}.patch`; - // Escape content for shell - use base64 encoding - const encodedContent = Buffer.from(p.content).toString("base64"); - + if (!p.enabled) { + continue; + } + const filePath = join(codePath, p.filePath); command += ` -echo "${encodedContent}" | base64 -d > ${patchFileName}; -cd ${codePath} && git apply --whitespace=fix ${patchFileName} && echo "✅ Applied patch for: ${p.filePath}" || echo "⚠️ Warning: Failed to apply patch for: ${p.filePath}"; -rm -f ${patchFileName}; +rm -f ${filePath}; +echo "${encodeBase64(p.content)}" | base64 -d > ${filePath}; `; } return command; }; - -/** - * Apply patches during build process - */ -export const applyPatches = async ({ - appName, - type, - serverId, - patches, -}: ApplyPatchesOptions): Promise => { - const enabledPatches = patches.filter((p) => p.enabled); - - if (enabledPatches.length === 0) { - return; - } - - const command = generateApplyPatchesCommand({ - appName, - type, - serverId, - patches: enabledPatches, - }); - - if (serverId) { - await execAsyncRemote(serverId, command); - } else { - await execAsync(command); - } -};