mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-20 13:25:32 +02:00
impl
This commit is contained in:
295
packages/server/src/services/patch.ts
Normal file
295
packages/server/src/services/patch.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
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",
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user