From b97186b14819629c1f396d7bf0197c5ab190989b Mon Sep 17 00:00:00 2001 From: Skyf0l <59019720+skyf0l@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:16:37 +0800 Subject: [PATCH 1/2] feat: container storage usage in `exegol info` --- exegol/console/TUI.py | 3 +++ exegol/model/ExegolContainer.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/exegol/console/TUI.py b/exegol/console/TUI.py index f720040e..744ba4bc 100644 --- a/exegol/console/TUI.py +++ b/exegol/console/TUI.py @@ -233,6 +233,7 @@ def __buildContainerTable(table: Table, data: Sequence[ExegolContainer]) -> None table.add_column("Container tag") table.add_column("State") table.add_column("Image tag") + table.add_column("Storage") table.add_column("Configurations") if verbose_mode: table.add_column("Mounts") @@ -243,6 +244,7 @@ def __buildContainerTable(table: Table, data: Sequence[ExegolContainer]) -> None for container in data: if verbose_mode: table.add_row(container.getId(), container.getDisplayName(), container.getTextStatus(), container.image.getDisplayName(), + container.getContainerStorageSize(), container.config.getTextFeatures(verbose_mode), container.config.getTextMounts(debug_mode), container.config.getTextDevices(debug_mode), @@ -250,6 +252,7 @@ def __buildContainerTable(table: Table, data: Sequence[ExegolContainer]) -> None container.config.getTextEnvs(debug_mode)) else: table.add_row(container.getDisplayName(), container.getTextStatus(), container.image.getDisplayName(), + container.getContainerStorageSize(), container.config.getTextFeatures(verbose_mode)) @staticmethod diff --git a/exegol/model/ExegolContainer.py b/exegol/model/ExegolContainer.py index d3cb91db..96a4f9e4 100644 --- a/exegol/model/ExegolContainer.py +++ b/exegol/model/ExegolContainer.py @@ -127,6 +127,28 @@ def getId(self) -> str: """Container's short id getter""" return self.__container.short_id + def getContainerStorageSize(self) -> str: + """Get the size of the container's writable layer (storage overhead). + This excludes the base image size which is shared across containers.""" + try: + from exegol.utils.DockerUtils import DockerUtils + client = DockerUtils()._DockerUtils__client + # Use low-level containers API with size=True + containers_data = client.api.containers(all=True, filters={"id": self.__id}, size=True) + if containers_data: + size_rw = containers_data[0].get('SizeRw', 0) + else: + size_rw = 0 + + if size_rw == 0: + return "[bright_black]0 B[/bright_black]" + # Reuse the existing size formatting method from ExegolImage + from exegol.model.ExegolImage import ExegolImage + return ExegolImage._ExegolImage__processSize(size_rw) + except Exception as e: + logger.debug(f"Failed to get container storage size for {self.name}: {e}") + return "[bright_black]N/A[/bright_black]" + def getKey(self) -> str: """Universal unique key getter (from SelectableInterface)""" return self.name From d4687baeece02906e78c97b34447e54d5b0f872b Mon Sep 17 00:00:00 2001 From: Skyf0l <59019720+skyf0l@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:50:13 +0800 Subject: [PATCH 2/2] feat: add workspace size in container storage --- exegol/console/TUI.py | 4 +-- exegol/model/ExegolContainer.py | 62 +++++++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/exegol/console/TUI.py b/exegol/console/TUI.py index 744ba4bc..336f7c40 100644 --- a/exegol/console/TUI.py +++ b/exegol/console/TUI.py @@ -244,7 +244,7 @@ def __buildContainerTable(table: Table, data: Sequence[ExegolContainer]) -> None for container in data: if verbose_mode: table.add_row(container.getId(), container.getDisplayName(), container.getTextStatus(), container.image.getDisplayName(), - container.getContainerStorageSize(), + container.getContainerStorageSize(verbose=True), container.config.getTextFeatures(verbose_mode), container.config.getTextMounts(debug_mode), container.config.getTextDevices(debug_mode), @@ -252,7 +252,7 @@ def __buildContainerTable(table: Table, data: Sequence[ExegolContainer]) -> None container.config.getTextEnvs(debug_mode)) else: table.add_row(container.getDisplayName(), container.getTextStatus(), container.image.getDisplayName(), - container.getContainerStorageSize(), + container.getContainerStorageSize(verbose=False), container.config.getTextFeatures(verbose_mode)) @staticmethod diff --git a/exegol/model/ExegolContainer.py b/exegol/model/ExegolContainer.py index 96a4f9e4..f1d929c9 100644 --- a/exegol/model/ExegolContainer.py +++ b/exegol/model/ExegolContainer.py @@ -127,27 +127,69 @@ def getId(self) -> str: """Container's short id getter""" return self.__container.short_id - def getContainerStorageSize(self) -> str: - """Get the size of the container's writable layer (storage overhead). + def getContainerStorageSize(self, verbose: bool = False) -> str: + """Get the size of the container's writable layer and workspace. + In normal mode, returns total size. In verbose mode, returns tree breakdown. This excludes the base image size which is shared across containers.""" try: from exegol.utils.DockerUtils import DockerUtils + from exegol.model.ExegolImage import ExegolImage + client = DockerUtils()._DockerUtils__client # Use low-level containers API with size=True containers_data = client.api.containers(all=True, filters={"id": self.__id}, size=True) - if containers_data: - size_rw = containers_data[0].get('SizeRw', 0) - else: - size_rw = 0 + size_rw = containers_data[0].get('SizeRw', 0) if containers_data else 0 + + # Get workspace size in bytes + workspace_size = self.__getWorkspaceSize(return_bytes=True) - if size_rw == 0: + # Calculate total + total_size = size_rw + workspace_size + + if verbose and (size_rw > 0 or workspace_size > 0): + # Verbose mode: show breakdown + container_str = ExegolImage._ExegolImage__processSize(size_rw) if size_rw > 0 else "0 B" + workspace_str = ExegolImage._ExegolImage__processSize(workspace_size) if workspace_size > 0 else "0 B" + return f"Container: {container_str}\nWorkspace: {workspace_str}" + + # Normal mode: show total only + if total_size == 0: return "[bright_black]0 B[/bright_black]" - # Reuse the existing size formatting method from ExegolImage - from exegol.model.ExegolImage import ExegolImage - return ExegolImage._ExegolImage__processSize(size_rw) + return ExegolImage._ExegolImage__processSize(total_size) except Exception as e: logger.debug(f"Failed to get container storage size for {self.name}: {e}") return "[bright_black]N/A[/bright_black]" + + def __getWorkspaceSize(self, return_bytes: bool = False) -> Union[str, int]: + """Calculate workspace directory size. + If return_bytes=True, returns size as int. Otherwise returns formatted string.""" + try: + workspace_path = self.config.getHostWorkspacePath() + if not workspace_path or not os.path.exists(workspace_path): + return 0 if return_bytes else "" + + # Use os.walk for efficiency (followlinks=False to avoid symlink issues) + total_size = 0 + for dirpath, dirnames, filenames in os.walk(workspace_path, followlinks=False): + for filename in filenames: + filepath = os.path.join(dirpath, filename) + try: + total_size += os.path.getsize(filepath) + except (OSError, FileNotFoundError): + # Skip files we can't read or that disappeared + continue + + if return_bytes: + return total_size + + if total_size == 0: + return "" + + from exegol.model.ExegolImage import ExegolImage + return ExegolImage._ExegolImage__processSize(total_size) + except Exception as e: + logger.debug(f"Failed to get workspace size for {self.name}: {e}") + return 0 if return_bytes else "" def getKey(self) -> str: """Universal unique key getter (from SelectableInterface)"""