refactor(volumes): rework files volumes to be more simple and persistent

This commit is contained in:
Mauricio Siu 2024-07-21 18:55:57 -06:00
parent 2e79c7230f
commit 8454e4f781
11 changed files with 126 additions and 76 deletions

View File

@ -330,7 +330,7 @@ export const AddVolumes = ({
/> />
</> </>
)} )}
{serviceType === "application" && ( {serviceType !== "compose" && (
<FormField <FormField
control={form.control} control={form.control}
name="mountPath" name="mountPath"

View File

@ -73,8 +73,7 @@ export const ShowVolumes = ({ applicationId }: Props) => {
key={mount.mountId} key={mount.mountId}
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4" className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
> >
{/* <Package className="size-8 self-center text-muted-foreground" /> */} <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 flex-col gap-4 sm:gap-8">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="font-medium">Mount Type</span> <span className="font-medium">Mount Type</span>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
@ -91,12 +90,21 @@ export const ShowVolumes = ({ applicationId }: Props) => {
)} )}
{mount.type === "file" && ( {mount.type === "file" && (
<div className="flex flex-col gap-1"> <>
<span className="font-medium">Content</span> <div className="flex flex-col gap-1">
<span className="text-sm text-muted-foreground"> <span className="font-medium">Content</span>
{mount.content} <span className="text-sm text-muted-foreground">
</span> {mount.content}
</div> </span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">File Path</span>
<span className="text-sm text-muted-foreground">
{mount.filePath}
</span>
</div>
</>
)} )}
{mount.type === "bind" && ( {mount.type === "bind" && (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
@ -118,6 +126,7 @@ export const ShowVolumes = ({ applicationId }: Props) => {
mountId={mount.mountId} mountId={mount.mountId}
type={mount.type} type={mount.type}
refetch={refetch} refetch={refetch}
serviceType="application"
/> />
<DeleteVolume mountId={mount.mountId} refetch={refetch} /> <DeleteVolume mountId={mount.mountId} refetch={refetch} />
</div> </div>

View File

@ -48,6 +48,7 @@ const mySchema = z.discriminatedUnion("type", [
.object({ .object({
type: z.literal("file"), type: z.literal("file"),
content: z.string().optional(), content: z.string().optional(),
filePath: z.string().min(1, "File path required"),
}) })
.merge(mountSchema), .merge(mountSchema),
]); ]);
@ -58,9 +59,23 @@ interface Props {
mountId: string; mountId: string;
type: "bind" | "volume" | "file"; type: "bind" | "volume" | "file";
refetch: () => void; refetch: () => void;
serviceType:
| "application"
| "postgres"
| "redis"
| "mongo"
| "redis"
| "mysql"
| "mariadb"
| "compose";
} }
export const UpdateVolume = ({ mountId, type, refetch }: Props) => { export const UpdateVolume = ({
mountId,
type,
refetch,
serviceType,
}: Props) => {
const utils = api.useUtils(); const utils = api.useUtils();
const { data } = api.mounts.one.useQuery( const { data } = api.mounts.one.useQuery(
{ {
@ -103,6 +118,7 @@ export const UpdateVolume = ({ mountId, type, refetch }: Props) => {
form.reset({ form.reset({
content: data.content || "", content: data.content || "",
mountPath: data.mountPath, mountPath: data.mountPath,
filePath: data.filePath || "",
type: "file", type: "file",
}); });
} }
@ -141,6 +157,7 @@ export const UpdateVolume = ({ mountId, type, refetch }: Props) => {
content: data.content, content: data.content,
mountPath: data.mountPath, mountPath: data.mountPath,
type: data.type, type: data.type,
filePath: data.filePath,
mountId, mountId,
}) })
.then(() => { .then(() => {
@ -166,6 +183,11 @@ export const UpdateVolume = ({ mountId, type, refetch }: Props) => {
<DialogDescription>Update the mount</DialogDescription> <DialogDescription>Update the mount</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>} {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
{type === "file" && (
<AlertBlock type="warning">
Updating the mount will recreate the file or directory.
</AlertBlock>
)}
<Form {...form}> <Form {...form}>
<form <form
@ -211,40 +233,62 @@ export const UpdateVolume = ({ mountId, type, refetch }: Props) => {
)} )}
{type === "file" && ( {type === "file" && (
<FormField <>
control={form.control} <FormField
name="content" control={form.control}
render={({ field }) => ( name="content"
<FormItem> render={({ field }) => (
<FormLabel>Content</FormLabel> <FormItem>
<FormControl> <FormLabel>Content</FormLabel>
<FormControl> <FormControl>
<Textarea <FormControl>
placeholder="Any content" <Textarea
className="h-64" placeholder="Any content"
className="h-64"
{...field}
/>
</FormControl>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="filePath"
render={({ field }) => (
<FormItem>
<FormLabel>File Path</FormLabel>
<FormControl>
<Input
disabled
placeholder="Name of the file"
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{serviceType !== "compose" && (
<FormField
control={form.control}
name="mountPath"
render={({ field }) => (
<FormItem>
<FormLabel>Mount Path (In the container)</FormLabel>
<FormControl>
<Input placeholder="Mount Path" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
)} )}
<FormField
control={form.control}
name="mountPath"
render={({ field }) => (
<FormItem>
<FormLabel>Mount Path</FormLabel>
<FormControl>
<Input placeholder="Mount Path" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div> </div>
<DialogFooter> <DialogFooter>
<Button <Button

View File

@ -126,6 +126,7 @@ export const ShowVolumesCompose = ({ composeId }: Props) => {
mountId={mount.mountId} mountId={mount.mountId}
type={mount.type} type={mount.type}
refetch={refetch} refetch={refetch}
serviceType="compose"
/> />
<DeleteVolume mountId={mount.mountId} refetch={refetch} /> <DeleteVolume mountId={mount.mountId} refetch={refetch} />
</div> </div>

View File

@ -117,6 +117,7 @@ export const ShowVolumes = ({ mariadbId }: Props) => {
mountId={mount.mountId} mountId={mount.mountId}
type={mount.type} type={mount.type}
refetch={refetch} refetch={refetch}
serviceType="mariadb"
/> />
<DeleteVolume mountId={mount.mountId} refetch={refetch} /> <DeleteVolume mountId={mount.mountId} refetch={refetch} />
</div> </div>

View File

@ -113,6 +113,7 @@ export const ShowVolumes = ({ mongoId }: Props) => {
mountId={mount.mountId} mountId={mount.mountId}
type={mount.type} type={mount.type}
refetch={refetch} refetch={refetch}
serviceType="mongo"
/> />
<DeleteVolume mountId={mount.mountId} refetch={refetch} /> <DeleteVolume mountId={mount.mountId} refetch={refetch} />
</div> </div>

View File

@ -115,6 +115,7 @@ export const ShowVolumes = ({ mysqlId }: Props) => {
mountId={mount.mountId} mountId={mount.mountId}
type={mount.type} type={mount.type}
refetch={refetch} refetch={refetch}
serviceType="mysql"
/> />
<DeleteVolume mountId={mount.mountId} refetch={refetch} /> <DeleteVolume mountId={mount.mountId} refetch={refetch} />
</div> </div>

View File

@ -118,6 +118,7 @@ export const ShowVolumes = ({ postgresId }: Props) => {
mountId={mount.mountId} mountId={mount.mountId}
type={mount.type} type={mount.type}
refetch={refetch} refetch={refetch}
serviceType="postgres"
/> />
<DeleteVolume mountId={mount.mountId} refetch={refetch} /> <DeleteVolume mountId={mount.mountId} refetch={refetch} />
</div> </div>

View File

@ -115,6 +115,7 @@ export const ShowVolumes = ({ redisId }: Props) => {
mountId={mount.mountId} mountId={mount.mountId}
type={mount.type} type={mount.type}
refetch={refetch} refetch={refetch}
serviceType="redis"
/> />
<DeleteVolume mountId={mount.mountId} refetch={refetch} /> <DeleteVolume mountId={mount.mountId} refetch={refetch} />
</div> </div>

View File

@ -9,7 +9,6 @@ import {
} from "@/server/db/schema"; } from "@/server/db/schema";
import { createFile } from "@/server/utils/docker/utils"; import { createFile } from "@/server/utils/docker/utils";
import { removeFileOrDirectory } from "@/server/utils/filesystem/directory"; import { removeFileOrDirectory } from "@/server/utils/filesystem/directory";
import { execAsync } from "@/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { type SQL, eq, sql } from "drizzle-orm"; import { type SQL, eq, sql } from "drizzle-orm";
@ -71,10 +70,10 @@ export const createMount = async (input: typeof apiCreateMount._type) => {
export const createFileMount = async (mountId: string) => { export const createFileMount = async (mountId: string) => {
try { try {
const mount = await findMountById(mountId); const mount = await findMountById(mountId);
const baseFilePath = await getBaseFilesMountPath(mountId); const baseFilePath = await getBaseFilesPath(mountId);
await createFile(baseFilePath, mount.filePath || "", mount.content || ""); await createFile(baseFilePath, mount.filePath || "", mount.content || "");
} catch (error) { } catch (error) {
console.log(error); console.log(`Error to create the file mount: ${error}`);
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Error to create the mount", message: "Error to create the mount",
@ -109,28 +108,29 @@ export const updateMount = async (
mountId: string, mountId: string,
mountData: Partial<Mount>, mountData: Partial<Mount>,
) => { ) => {
const mount = await db return await db.transaction(async (transaction) => {
.update(mounts) const mount = await db
.set({ .update(mounts)
...mountData, .set({
}) ...mountData,
.where(eq(mounts.mountId, mountId)) })
.returning() .where(eq(mounts.mountId, mountId))
.then((value) => value[0]); .returning()
.then((value) => value[0]);
if (!mount) { if (!mount) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "Mount not found", message: "Mount not found",
}); });
} }
if (mount.type === "file") { if (mount.type === "file") {
await deleteFileMount(mountId); await deleteFileMount(mountId);
await createFileMount(mountId); await createFileMount(mountId);
} }
return mount;
return mount; });
}; };
export const findMountsByApplicationId = async ( export const findMountsByApplicationId = async (
@ -184,14 +184,15 @@ export const deleteMount = async (mountId: string) => {
export const deleteFileMount = async (mountId: string) => { export const deleteFileMount = async (mountId: string) => {
const mount = await findMountById(mountId); const mount = await findMountById(mountId);
const basePath = await getBaseFilesMountPath(mountId); if (!mount.filePath) return;
const fullPath = path.join(basePath, mount.filePath || ""); const basePath = await getBaseFilesPath(mountId);
const fullPath = path.join(basePath, mount.filePath);
try { try {
await removeFileOrDirectory(fullPath); await removeFileOrDirectory(fullPath);
} catch (error) {} } catch (error) {}
}; };
export const getBaseFilesMountPath = async (mountId: string) => { export const getBaseFilesPath = async (mountId: string) => {
const mount = await findMountById(mountId); const mount = await findMountById(mountId);
let absoluteBasePath = path.resolve(APPLICATIONS_PATH); let absoluteBasePath = path.resolve(APPLICATIONS_PATH);

View File

@ -1,7 +1,7 @@
import fs from "node:fs"; import fs from "node:fs";
import path, { join } from "node:path"; import path from "node:path";
import type { Readable } from "node:stream"; import type { Readable } from "node:stream";
import { APPLICATIONS_PATH, COMPOSE_PATH, docker } from "@/server/constants"; import { APPLICATIONS_PATH, docker } from "@/server/constants";
import type { ContainerInfo, ResourceRequirements } from "dockerode"; import type { ContainerInfo, ResourceRequirements } from "dockerode";
import { parse } from "dotenv"; import { parse } from "dotenv";
import type { ApplicationNested } from "../builders"; import type { ApplicationNested } from "../builders";
@ -305,13 +305,13 @@ export const generateFileMounts = (
return mounts return mounts
.filter((mount) => mount.type === "file") .filter((mount) => mount.type === "file")
.map((mount) => { .map((mount) => {
const fileName = mount.mountPath.split("/").pop(); const fileName = mount.filePath;
const absoluteBasePath = path.resolve(APPLICATIONS_PATH); const absoluteBasePath = path.resolve(APPLICATIONS_PATH);
const directory = path.join(absoluteBasePath, appName, "files"); const directory = path.join(absoluteBasePath, appName, "files");
const filePath = path.join(directory, fileName || ""); const sourcePath = path.join(directory, fileName || "");
return { return {
Type: "bind" as const, Type: "bind" as const,
Source: filePath, Source: sourcePath,
Target: mount.mountPath, Target: mount.mountPath,
}; };
}); });
@ -323,27 +323,17 @@ export const createFile = async (
content: string, content: string,
) => { ) => {
try { try {
// Unir outputPath con filePath
const fullPath = path.join(outputPath, filePath); const fullPath = path.join(outputPath, filePath);
// Verificar si la ruta termina en separador (indica que es un directorio)
if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) {
fs.mkdirSync(fullPath, { recursive: true }); fs.mkdirSync(fullPath, { recursive: true });
console.log(`Directorio creado: ${fullPath}`);
return; return;
} }
// Para archivos, obtener el directorio del archivo
const directory = path.dirname(fullPath); const directory = path.dirname(fullPath);
// Crear el directorio si no existe
fs.mkdirSync(directory, { recursive: true }); fs.mkdirSync(directory, { recursive: true });
// Escribir el archivo
fs.writeFileSync(fullPath, content || ""); fs.writeFileSync(fullPath, content || "");
console.log(`Archivo creado: ${fullPath}`);
} catch (error) { } catch (error) {
console.log(error); throw error;
} }
}; };