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; // CRUD Operations 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.endsWith("\n") ? input.content : `${input.content}\n`, }) .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, applicationId?: string, composeId?: string, ) => { if (applicationId) { return await db.query.patch.findFirst({ where: and( eq(patch.filePath, filePath), eq(patch.applicationId, applicationId), ), }); } if (composeId) { return await db.query.patch.findFirst({ where: and(eq(patch.filePath, filePath), eq(patch.composeId, composeId)), }); } return null; }; 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); } }; interface GeneratePatchOptions { codePath: string; filePath: string; newContent: string; serverId?: string | null; } /** * Generate a patch from modified file content using git diff */ export const generatePatch = async ({ codePath, filePath, newContent, serverId, }: GeneratePatchOptions): Promise => { const fullPath = join(codePath, filePath); // Write new content to the file const encodedContent = Buffer.from(newContent).toString("base64"); const writeCommand = `echo "${encodedContent}" | base64 -d > "${fullPath}"`; if (serverId) { await execAsyncRemote(serverId, writeCommand); } else { await execAsync(writeCommand); } // Generate diff const diffCommand = `cd "${codePath}" && git diff -- "${filePath}"`; let diffResult: string; if (serverId) { const result = await execAsyncRemote(serverId, diffCommand); diffResult = result.stdout; } else { const result = await execAsync(diffCommand); diffResult = result.stdout; } // Reset the file to original state const resetCommand = `cd "${codePath}" && git checkout -- "${filePath}"`; if (serverId) { await execAsyncRemote(serverId, resetCommand); } else { await execAsync(resetCommand); } return diffResult; }; interface ApplyPatchToContentOptions { originalContent: string; patchContent: string; } /** * Apply a patch to content in memory (for preview purposes) * Returns the patched content or throws an error if patch fails */ export const applyPatchToContent = async ({ originalContent, patchContent, }: ApplyPatchToContentOptions): Promise => { // Create temp files and apply patch const tempDir = "/tmp/patch_preview_" + Date.now(); const tempFile = `${tempDir}/file`; const patchFile = `${tempDir}/patch.diff`; const encodedOriginal = Buffer.from(originalContent).toString("base64"); const encodedPatch = Buffer.from(patchContent).toString("base64"); const command = ` mkdir -p "${tempDir}"; echo "${encodedOriginal}" | base64 -d > "${tempFile}"; echo "${encodedPatch}" | base64 -d > "${patchFile}"; cd "${tempDir}" && patch -p0 < "${patchFile}" 2>/dev/null; cat "${tempFile}"; rm -rf "${tempDir}"; `; try { const result = await execAsync(command); return result.stdout; } catch { throw new TRPCError({ code: "BAD_REQUEST", message: "Failed to apply patch to content", }); } };