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"; export type Patch = typeof patch.$inferSelect; export const createPatch = async (input: typeof apiCreatePatch._type) => { if (!input.applicationId && !input.composeId) { throw new TRPCError({ code: "BAD_REQUEST", message: "Either applicationId or composeId must be provided", }); } const newPatch = await db .insert(patch) .values({ ...input, content: input.content, enabled: true, }) .returning() .then((value) => value[0]); if (!newPatch) { throw new TRPCError({ code: "BAD_REQUEST", message: "Error creating the patch", }); } return newPatch; }; export const findPatchById = async (patchId: string) => { const result = await db.query.patch.findFirst({ where: eq(patch.patchId, patchId), }); if (!result) { throw new TRPCError({ code: "NOT_FOUND", message: "Patch not found", }); } return result; }; export const findPatchesByApplicationId = async (applicationId: string) => { return await db.query.patch.findMany({ where: and( eq(patch.applicationId, applicationId), isNotNull(patch.applicationId), ), 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)), }); }; export const updatePatch = async (patchId: string, data: Partial) => { const result = await db .update(patch) .set({ ...data, ...(data.content && { content: data.content.endsWith("\n") ? data.content : `${data.content}\n`, }), updatedAt: new Date().toISOString(), }) .where(eq(patch.patchId, patchId)) .returning(); return result[0]; }; export const deletePatch = async (patchId: string) => { const result = await db .delete(patch) .where(eq(patch.patchId, patchId)) .returning(); return result[0]; }; // Patch Application Functions interface ApplyPatchesOptions { appName: 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, type, patches, serverId, }: ApplyPatchesOptions): string => { 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"); 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}; `; } 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); } };