mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-20 20:14:29 +00:00
feat: add volumes section and file explorer for containers and volumes
This commit is contained in:
@@ -17,6 +17,7 @@ export * from "./services/compose";
|
||||
export * from "./services/deployment";
|
||||
export * from "./services/destination";
|
||||
export * from "./services/docker";
|
||||
export * from "./services/docker-volume";
|
||||
export * from "./services/domain";
|
||||
export * from "./services/environment";
|
||||
export * from "./services/git-provider";
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { quote } from "shell-quote";
|
||||
import { CONTAINER_FILE_SIZE_LIMIT } from "./docker";
|
||||
|
||||
export interface DockerVolume {
|
||||
Name: string;
|
||||
Driver: string;
|
||||
Scope: string;
|
||||
Mountpoint: string;
|
||||
Labels: string;
|
||||
Availability?: string;
|
||||
Group?: string;
|
||||
Links?: string;
|
||||
Size?: string;
|
||||
Status?: string;
|
||||
}
|
||||
|
||||
const VOLUME_MOUNT = "/__volume";
|
||||
|
||||
export const getVolumes = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker volume ls --format '{{json .}}'";
|
||||
const { stdout } = serverId
|
||||
? await execAsyncRemote(serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
return stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line) as DockerVolume);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getVolumesSize = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker system df -v --format '{{json .}}'";
|
||||
const { stdout } = serverId
|
||||
? await execAsyncRemote(serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
const diskUsage = JSON.parse(stdout.trim());
|
||||
return ((diskUsage?.Volumes ?? []) as Partial<DockerVolume>[])
|
||||
.filter((volume) => volume?.Name)
|
||||
.map((volume) => ({
|
||||
name: volume.Name as string,
|
||||
size: volume.Size ?? null,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getVolumeConfig = async (
|
||||
volumeName: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
const command = `docker volume inspect ${quote([String(volumeName ?? "")])}`;
|
||||
const { stdout } = serverId
|
||||
? await execAsyncRemote(serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
return JSON.parse(stdout.trim())[0];
|
||||
};
|
||||
|
||||
export const removeVolume = async (volumeName: string, serverId?: string) => {
|
||||
const command = `docker volume rm ${quote([String(volumeName ?? "")])}`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
};
|
||||
|
||||
export const listVolumeFiles = async (
|
||||
volumeName: string,
|
||||
path: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
const command = `docker run --rm -v ${quote([`${volumeName}:${VOLUME_MOUNT}:ro`])} busybox ls -1Ap ${quote([`${VOLUME_MOUNT}${path}`])}`;
|
||||
const { stdout } = serverId
|
||||
? await execAsyncRemote(serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
return stdout
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((entry) => ({
|
||||
name: entry.endsWith("/") ? entry.slice(0, -1) : entry,
|
||||
isDirectory: entry.endsWith("/"),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.isDirectory !== b.isDirectory) {
|
||||
return a.isDirectory ? -1 : 1;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
};
|
||||
|
||||
export const readVolumeFile = async (
|
||||
volumeName: string,
|
||||
filePath: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
const command = `docker run --rm -v ${quote([`${volumeName}:${VOLUME_MOUNT}:ro`])} busybox cat ${quote([`${VOLUME_MOUNT}${filePath}`])} | head -c ${CONTAINER_FILE_SIZE_LIMIT + 1} | base64 | tr -d '\\n'`;
|
||||
const { stdout, stderr } = serverId
|
||||
? await execAsyncRemote(serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
if (stderr && !stdout) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(stdout.trim(), "base64");
|
||||
return {
|
||||
content: buffer.subarray(0, CONTAINER_FILE_SIZE_LIMIT).toString("base64"),
|
||||
truncated: buffer.byteLength > CONTAINER_FILE_SIZE_LIMIT,
|
||||
};
|
||||
};
|
||||
|
||||
export const writeVolumeFile = async (
|
||||
volumeName: string,
|
||||
filePath: string,
|
||||
content: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
const base64Content = Buffer.from(content, "utf8").toString("base64");
|
||||
if (base64Content.length > CONTAINER_FILE_SIZE_LIMIT * 2) {
|
||||
throw new Error("File is too large to save from the editor (max 512KB)");
|
||||
}
|
||||
|
||||
const innerCommand = `printf '%s' ${base64Content} | base64 -d > ${quote([`${VOLUME_MOUNT}${filePath}`])}`;
|
||||
const command = `docker run --rm -v ${quote([`${volumeName}:${VOLUME_MOUNT}`])} busybox sh -c ${quote([innerCommand])}`;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteVolumeFile = async (
|
||||
volumeName: string,
|
||||
path: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
const command = `docker run --rm -v ${quote([`${volumeName}:${VOLUME_MOUNT}`])} busybox rm -rf ${quote([`${VOLUME_MOUNT}${path}`])}`;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
ExecError,
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
@@ -760,3 +761,113 @@ export const uploadFileToContainer = async (
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const CONTAINER_FILE_SIZE_LIMIT = 512 * 1024;
|
||||
|
||||
const NO_SHELL_UTILITIES_ERROR =
|
||||
"This container image has no shell utilities and its filesystem is not accessible from the Dokploy host.";
|
||||
|
||||
const isMissingBinaryError = (error: unknown) =>
|
||||
error instanceof ExecError &&
|
||||
(error.exitCode === 126 ||
|
||||
error.exitCode === 127 ||
|
||||
!!error.stderr?.includes("executable file not found"));
|
||||
|
||||
export const listContainerFiles = async (
|
||||
containerId: string,
|
||||
path: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
const command = `docker exec ${quote([containerId])} ls -1Ap ${quote([path])}`;
|
||||
let stdout: string;
|
||||
try {
|
||||
({ stdout } = serverId
|
||||
? await execAsyncRemote(serverId, command)
|
||||
: await execAsync(command));
|
||||
} catch (error) {
|
||||
if (isMissingBinaryError(error)) {
|
||||
throw new Error(NO_SHELL_UTILITIES_ERROR);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return stdout
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((entry) => ({
|
||||
name: entry.endsWith("/") ? entry.slice(0, -1) : entry,
|
||||
isDirectory: entry.endsWith("/"),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.isDirectory !== b.isDirectory) {
|
||||
return a.isDirectory ? -1 : 1;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
};
|
||||
|
||||
export const readContainerFile = async (
|
||||
containerId: string,
|
||||
filePath: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
const command = `docker exec ${quote([containerId])} cat ${quote([filePath])} | head -c ${CONTAINER_FILE_SIZE_LIMIT + 1} | base64 | tr -d '\\n'`;
|
||||
const { stdout, stderr } = serverId
|
||||
? await execAsyncRemote(serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
if (stderr && !stdout) {
|
||||
if (stderr.includes("executable file not found")) {
|
||||
throw new Error(NO_SHELL_UTILITIES_ERROR);
|
||||
}
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(stdout.trim(), "base64");
|
||||
return {
|
||||
content: buffer.subarray(0, CONTAINER_FILE_SIZE_LIMIT).toString("base64"),
|
||||
truncated: buffer.byteLength > CONTAINER_FILE_SIZE_LIMIT,
|
||||
};
|
||||
};
|
||||
|
||||
export const writeContainerFile = async (
|
||||
containerId: string,
|
||||
filePath: string,
|
||||
content: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
const base64Content = Buffer.from(content, "utf8").toString("base64");
|
||||
if (base64Content.length > CONTAINER_FILE_SIZE_LIMIT * 2) {
|
||||
throw new Error("File is too large to save from the editor (max 512KB)");
|
||||
}
|
||||
|
||||
const tempPath = `/tmp/dokploy-edit-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
const command = `printf '%s' ${quote([base64Content])} | base64 -d > ${quote([tempPath])} && docker cp ${quote([tempPath])} ${quote([`${containerId}:${filePath}`])}; status=$?; rm -f ${quote([tempPath])}; exit $status`;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteContainerFile = async (
|
||||
containerId: string,
|
||||
path: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
const command = `docker exec ${quote([containerId])} rm -rf ${quote([path])}`;
|
||||
|
||||
try {
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isMissingBinaryError(error)) {
|
||||
throw new Error(NO_SHELL_UTILITIES_ERROR);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user