mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 04:35:24 +02:00
177 lines
4.0 KiB
TypeScript
177 lines
4.0 KiB
TypeScript
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 { TRPCError } from "@trpc/server";
|
|
import { and, eq } from "drizzle-orm";
|
|
import type { z } from "zod";
|
|
import { encodeBase64 } from "../utils/docker/utils";
|
|
import { findApplicationById } from "./application";
|
|
import { findComposeById } from "./compose";
|
|
|
|
export type Patch = typeof patch.$inferSelect;
|
|
|
|
export const createPatch = async (input: z.infer<typeof apiCreatePatch>) => {
|
|
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 findPatchesByEntityId = async (
|
|
id: string,
|
|
type: "application" | "compose",
|
|
) => {
|
|
return await db.query.patch.findMany({
|
|
where: eq(
|
|
type === "application" ? patch.applicationId : patch.composeId,
|
|
id,
|
|
),
|
|
orderBy: (patch, { asc }) => [asc(patch.filePath)],
|
|
});
|
|
};
|
|
|
|
export const findPatchByFilePath = async (
|
|
filePath: string,
|
|
id: string,
|
|
type: "application" | "compose",
|
|
) => {
|
|
return await db.query.patch.findFirst({
|
|
where: and(
|
|
eq(patch.filePath, filePath),
|
|
eq(type === "application" ? patch.applicationId : patch.composeId, id),
|
|
),
|
|
});
|
|
};
|
|
|
|
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];
|
|
};
|
|
|
|
export const markPatchForDeletion = async (
|
|
filePath: string,
|
|
entityId: string,
|
|
entityType: "application" | "compose",
|
|
) => {
|
|
const existing = await findPatchByFilePath(filePath, entityId, entityType);
|
|
|
|
if (existing) {
|
|
return await updatePatch(existing.patchId, { type: "delete", content: "" });
|
|
}
|
|
|
|
return await createPatch({
|
|
filePath,
|
|
content: "",
|
|
type: "delete",
|
|
applicationId: entityType === "application" ? entityId : undefined,
|
|
composeId: entityType === "compose" ? entityId : undefined,
|
|
});
|
|
};
|
|
|
|
interface ApplyPatchesOptions {
|
|
id: string;
|
|
type: "application" | "compose";
|
|
serverId: string | null;
|
|
}
|
|
|
|
export const generateApplyPatchesCommand = async ({
|
|
id,
|
|
type,
|
|
serverId,
|
|
}: 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 resultPatches = await findPatchesByEntityId(id, type);
|
|
const patches = resultPatches.filter((p) => p.enabled);
|
|
|
|
if (patches.length === 0) {
|
|
return "";
|
|
}
|
|
|
|
let command = `echo "Applying ${patches.length} patch(es)...";`;
|
|
|
|
for (const p of patches) {
|
|
const filePath = join(codePath, p.filePath);
|
|
|
|
if (p.type === "delete") {
|
|
command += `
|
|
rm -f "${filePath}";
|
|
`;
|
|
} else {
|
|
command += `
|
|
file="${filePath}"
|
|
dir="$(dirname "$file")"
|
|
mkdir -p "$dir"
|
|
echo "${encodeBase64(p.content)}" | base64 -d > "$file"
|
|
`;
|
|
}
|
|
}
|
|
|
|
return command;
|
|
};
|