diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx index 06d1983e14..59963a9eef 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx @@ -7,6 +7,8 @@ import { toast } from "sonner"; import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { CodeEditor } from "@/components/shared/code-editor"; +import { PermissionMode } from "@/components/shared/permission-mode"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -49,13 +51,35 @@ const mountSchema = z.object({ mountPath: z.string().min(1, "Mount path required"), }); +const uidSchema = z.preprocess( + (v) => (v === "" || v === null ? undefined : v), + z.coerce.number().int().positive().optional(), +); +const gidSchema = z.preprocess( + (v) => (v === "" || v === null ? undefined : v), + z.coerce.number().int().positive().optional(), +); +const modeSchema = z.preprocess( + (v) => (v === "" || v === null ? undefined : v), + z + .string() + .regex(/^[0-7]{3,4}$/, "Use octal digits like 644 or 0775") + .optional(), +); +const ownershipSchema = z.object({ + uid: uidSchema, + gid: gidSchema, + mode: modeSchema, +}); + const mySchema = z.discriminatedUnion("type", [ z .object({ type: z.literal("bind"), hostPath: z.string().min(1, "Host path required"), }) - .merge(mountSchema), + .merge(mountSchema) + .merge(ownershipSchema), z .object({ type: z.literal("volume"), @@ -67,14 +91,16 @@ const mySchema = z.discriminatedUnion("type", [ "Invalid volume name. Use letters, numbers, '._-' and start with a letter/number.", ), }) - .merge(mountSchema), + .merge(mountSchema) + .merge(ownershipSchema), z .object({ type: z.literal("file"), filePath: z.string().min(1, "File path required"), content: z.string().optional(), }) - .merge(mountSchema), + .merge(mountSchema) + .merge(ownershipSchema), ]); type AddMount = z.infer; @@ -92,6 +118,9 @@ export const AddVolumes = ({ type: serviceType === "compose" ? "file" : "bind", hostPath: "", mountPath: serviceType === "compose" ? "/" : "", + uid: undefined, + gid: undefined, + mode: "", }, resolver: zodResolver(mySchema), }); @@ -109,6 +138,9 @@ export const AddVolumes = ({ mountPath: data.mountPath, type: data.type, serviceType, + uid: data.uid, + gid: data.gid, + mode: data.mode, }) .then(() => { toast.success("Mount Created"); @@ -124,6 +156,9 @@ export const AddVolumes = ({ mountPath: data.mountPath, type: data.type, serviceType, + uid: data.uid, + gid: data.gid, + mode: data.mode, }) .then(() => { toast.success("Mount Created"); @@ -140,6 +175,9 @@ export const AddVolumes = ({ filePath: data.filePath, type: data.type, serviceType, + uid: data.uid, + gid: data.gid, + mode: data.mode, }) .then(() => { toast.success("Mount Created"); @@ -163,13 +201,13 @@ export const AddVolumes = ({ Volumes / Mounts {/* {isError && ( -
- - - {error?.message} - -
- )} */} +
+ + + {error?.message} + +
+ )} */}
)} + +
+ + Ownership / Permissions (optional) + +

+ If unset, mounts remain root-owned with default permissions. +

+
+ ( + + UID + + + + + + )} + /> + ( + + GID + + + + + + )} + /> + ( + + + Mode + + {(() => { + const v = field.value ?? ""; + if (!/^\d{3,4}$/.test(v)) return "644"; + if (v.length === 4 && v[0] === "0") + return v.slice(1); + return v.slice(-3); + })()} + + + + + field.onChange(v)} + showAdvancedInput={false} + /> + + + + )} + /> +
+
diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx index 20edfa534a..f992f600f9 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx @@ -2,6 +2,7 @@ import { Package, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { AlertBlock } from "@/components/shared/alert-block"; import { DialogAction } from "@/components/shared/dialog-action"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, @@ -92,7 +93,7 @@ export const ShowVolumes = ({ id, type }: Props) => { className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4" > {/* */} -
+
Mount Type @@ -139,6 +140,69 @@ export const ShowVolumes = ({ id, type }: Props) => { {mount.mountPath}
+ +
+ Ownership + + {(() => { + const uid = mount.uid ?? null; + const gid = mount.gid ?? null; + if (uid === null && gid === null) return "—"; + if (uid !== null && gid !== null) + return `${uid}:${gid}`; + if (uid !== null) return `${uid}`; + return `:${gid}`; + })()} + +
+ +
+ Mode +
+ + {(() => { + const v = mount.mode ?? ""; + if (!/^\d{3,4}$/.test(v)) return "644"; + if (v.length === 4 && v[0] === "0") + return v.slice(1); + return v.slice(-3); + })()} + + + {(() => { + const v = mount.mode ?? ""; + const n = (() => { + if (!/^\d{3,4}$/.test(v)) return "644"; + if (v.length === 4 && v[0] === "0") + return v.slice(1); + return v.slice(-3); + })(); + const toBits = (d: number) => ({ + r: !!(d & 4), + w: !!(d & 2), + x: !!(d & 1), + }); + const [o, g, t] = n + .split("") + .map((d) => Number.parseInt(d, 10)) as [ + number, + number, + number, + ]; + const fmt = (b: { + r: boolean; + w: boolean; + x: boolean; + }) => + `${b.r ? "r" : "-"}${b.w ? "w" : "-"}${b.x ? "x" : "-"}`; + const b0 = toBits(o); + const b1 = toBits(g); + const b2 = toBits(t); + return `${fmt(b0)} ${fmt(b1)} ${fmt(b2)}`; + })()} + +
+
{canCreate && ( diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx index 246a25da24..3c1fa5db0a 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx @@ -6,6 +6,8 @@ import { toast } from "sonner"; import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { CodeEditor } from "@/components/shared/code-editor"; +import { PermissionMode } from "@/components/shared/permission-mode"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -31,13 +33,35 @@ const mountSchema = z.object({ mountPath: z.string().min(1, "Mount path required"), }); +const uidSchema = z.preprocess( + (v) => (v === "" || v === null ? undefined : v), + z.coerce.number().int().positive().optional(), +); +const gidSchema = z.preprocess( + (v) => (v === "" || v === null ? undefined : v), + z.coerce.number().int().positive().optional(), +); +const modeSchema = z.preprocess( + (v) => (v === "" || v === null ? undefined : v), + z + .string() + .regex(/^[0-7]{3,4}$/, "Use octal digits like 644 or 0775") + .optional(), +); +const ownershipSchema = z.object({ + uid: uidSchema, + gid: gidSchema, + mode: modeSchema, +}); + const mySchema = z.discriminatedUnion("type", [ z .object({ type: z.literal("bind"), hostPath: z.string().min(1, "Host path required"), }) - .merge(mountSchema), + .merge(mountSchema) + .merge(ownershipSchema), z .object({ type: z.literal("volume"), @@ -49,14 +73,16 @@ const mySchema = z.discriminatedUnion("type", [ "Invalid volume name. Use letters, numbers, '._-' and start with a letter/number.", ), }) - .merge(mountSchema), + .merge(mountSchema) + .merge(ownershipSchema), z .object({ type: z.literal("file"), content: z.string().optional(), filePath: z.string().min(1, "File path required"), }) - .merge(mountSchema), + .merge(mountSchema) + .merge(ownershipSchema), ]); type UpdateMount = z.infer; @@ -101,6 +127,9 @@ export const UpdateVolume = ({ type, hostPath: "", mountPath: "", + uid: undefined, + gid: undefined, + mode: "", }, resolver: zodResolver(mySchema), }); @@ -114,12 +143,18 @@ export const UpdateVolume = ({ hostPath: data.hostPath || "", mountPath: data.mountPath, type: "bind", + uid: data.uid ?? undefined, + gid: data.gid ?? undefined, + mode: data.mode ?? "", }); } else if (typeForm === "volume") { form.reset({ volumeName: data.volumeName || "", mountPath: data.mountPath, type: "volume", + uid: data.uid ?? undefined, + gid: data.gid ?? undefined, + mode: data.mode ?? "", }); } else if (typeForm === "file") { form.reset({ @@ -127,6 +162,9 @@ export const UpdateVolume = ({ mountPath: serviceType === "compose" ? "/" : data.mountPath, filePath: data.filePath || "", type: "file", + uid: data.uid ?? undefined, + gid: data.gid ?? undefined, + mode: data.mode ?? "", }); } } @@ -139,6 +177,9 @@ export const UpdateVolume = ({ mountPath: data.mountPath, type: data.type, mountId, + uid: data.uid, + gid: data.gid, + mode: data.mode, }) .then(() => { toast.success("Mount Update"); @@ -153,6 +194,9 @@ export const UpdateVolume = ({ mountPath: data.mountPath, type: data.type, mountId, + uid: data.uid, + gid: data.gid, + mode: data.mode, }) .then(() => { toast.success("Mount Update"); @@ -168,6 +212,9 @@ export const UpdateVolume = ({ type: data.type, filePath: data.filePath, mountId, + uid: data.uid, + gid: data.gid, + mode: data.mode, }) .then(() => { toast.success("Mount Update"); @@ -307,6 +354,71 @@ PORT=3000 )} /> )} + +
+ + Ownership / Permissions (optional) + +

+ If unset, mounts remain root-owned with default permissions. +

+
+ ( + + UID + + + + + + )} + /> + ( + + GID + + + + + + )} + /> + ( + + + Mode + + {(() => { + const v = field.value ?? ""; + if (!/^\d{3,4}$/.test(v)) return "644"; + if (v.length === 4 && v[0] === "0") + return v.slice(1); + return v.slice(-3); + })()} + + + + field.onChange(v)} + showAdvancedInput={false} + /> + + + + )} + /> +
+