diff --git a/apps/dokploy/components/dashboard/docker/show/columns.tsx b/apps/dokploy/components/dashboard/docker/show/columns.tsx index d506d37427..33c104d970 100644 --- a/apps/dokploy/components/dashboard/docker/show/columns.tsx +++ b/apps/dokploy/components/dashboard/docker/show/columns.tsx @@ -12,6 +12,7 @@ import { ShowContainerConfig } from "../config/show-container-config"; import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; import { RemoveContainerDialog } from "../remove/remove-container"; import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; +import { UploadFileModal } from "../upload/upload-file-modal"; import type { Container } from "./show-containers"; export const columns: ColumnDef[] = [ @@ -128,6 +129,12 @@ export const columns: ColumnDef[] = [ > Terminal + + Upload File + { + const [open, setOpen] = useState(false); + + const { mutateAsync: uploadFile, isPending: isLoading } = + api.docker.uploadFileToContainer.useMutation({ + onSuccess: () => { + toast.success("File uploaded successfully"); + setOpen(false); + form.reset(); + }, + onError: (error) => { + toast.error(error.message || "Failed to upload file to container"); + }, + }); + + const form = useForm({ + resolver: zodResolver(uploadFileToContainerSchema), + defaultValues: { + containerId, + destinationPath: "/", + serverId: serverId || undefined, + }, + }); + + const file = form.watch("file"); + + const onSubmit = async (values: UploadFileToContainer) => { + if (!values.file) { + toast.error("Please select a file to upload"); + return; + } + + const formData = new FormData(); + formData.append("containerId", values.containerId); + formData.append("file", values.file); + formData.append("destinationPath", values.destinationPath); + if (values.serverId) { + formData.append("serverId", values.serverId); + } + + await uploadFile(formData); + }; + + return ( + + + e.preventDefault()} + > + {children} + + + + + + + Upload File to Container + + + Upload a file directly into the container's filesystem + + + +
+ + ( + + Destination Path + + + + +

+ Enter the full path where the file should be uploaded in the + container (e.g., /app/config.json) +

+
+ )} + /> + + ( + + File + + { + if (files && files.length > 0) { + field.onChange(files[0]); + } else { + field.onChange(null); + } + }} + /> + + + {file instanceof File && ( +
+ + {file.name} ({(file.size / 1024).toFixed(2)} KB) + + +
+ )} +
+ )} + /> + + + + + + + +
+
+ ); +}; diff --git a/apps/dokploy/components/ui/dropzone.tsx b/apps/dokploy/components/ui/dropzone.tsx index b7546d73c6..6d9c05ef67 100644 --- a/apps/dokploy/components/ui/dropzone.tsx +++ b/apps/dokploy/components/ui/dropzone.tsx @@ -56,9 +56,9 @@ export const Dropzone = React.forwardRef( onDrop={handleDrop} onClick={handleButtonClick} > -
- - +
+ + {dropMessage} { + if (input.serverId) { + const server = await findServerById(input.serverId); + if (server.organizationId !== ctx.session?.activeOrganizationId) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + } + + const file = input.file; + if (!(file instanceof File)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Invalid file provided", + }); + } + + // Convert File to Buffer + const arrayBuffer = await file.arrayBuffer(); + const fileBuffer = Buffer.from(arrayBuffer); + + await uploadFileToContainer( + input.containerId, + fileBuffer, + file.name, + input.destinationPath, + input.serverId || null, + ); + + return { success: true, message: "File uploaded successfully" }; + }), }); diff --git a/apps/dokploy/utils/schema.ts b/apps/dokploy/utils/schema.ts index 10b13e1a00..73df59d6f4 100644 --- a/apps/dokploy/utils/schema.ts +++ b/apps/dokploy/utils/schema.ts @@ -17,3 +17,15 @@ export const uploadFileSchema = zfd.formData({ }); export type UploadFile = z.infer; + +export const uploadFileToContainerSchema = zfd.formData({ + containerId: z + .string() + .min(1) + .regex(/^[a-zA-Z0-9.\-_]+$/, "Invalid container ID"), + file: zfd.file(), + destinationPath: z.string().min(1), + serverId: z.string().optional(), +}); + +export type UploadFileToContainer = z.infer; diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index 92f5e3d2ef..db6099d693 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -505,3 +505,39 @@ export const getApplicationInfo = async ( return appArray; } catch {} }; + +export const uploadFileToContainer = async ( + containerId: string, + fileBuffer: Buffer, + fileName: string, + destinationPath: string, + serverId?: string | null, +): Promise => { + const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/; + if (!containerIdRegex.test(containerId)) { + throw new Error("Invalid container ID"); + } + + // Ensure destination path starts with / + const normalizedPath = destinationPath.startsWith("/") + ? destinationPath + : `/${destinationPath}`; + + const base64Content = fileBuffer.toString("base64"); + const tempFileName = `dokploy-upload-${Date.now()}-${fileName.replace(/[^a-zA-Z0-9.-]/g, "_")}`; + const tempPath = `/tmp/${tempFileName}`; + + const command = `echo '${base64Content}' | base64 -d > "${tempPath}" && docker cp "${tempPath}" "${containerId}:${normalizedPath}" ; rm -f "${tempPath}"`; + + try { + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } + } catch (error) { + throw new Error( + `Failed to upload file to container: ${error instanceof Error ? error.message : String(error)}`, + ); + } +};