mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor(volumes): rework files volumes to be more simple and persistent
This commit is contained in:
parent
2e79c7230f
commit
8454e4f781
@ -330,7 +330,7 @@ export const AddVolumes = ({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{serviceType === "application" && (
|
{serviceType !== "compose" && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="mountPath"
|
name="mountPath"
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user