userimplce9ba609 months agoHistory
295 lines · 6.8 KB · TypeScript
RawDownload
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<Patch>,) => {	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<void> => {	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<string> => {	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<string> => {	// 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",		});	}};