Created
February 15, 2026 02:21
-
-
Save FNGarvin/536ca974e3d2c17e1d961d7c9d6a8ee5 to your computer and use it in GitHub Desktop.
Podman Overlay Snoop: Maps physical hex-folders to containers/images with accurate size reporting.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Author: FNGarvin | |
| License: MIT | |
| Usage: snoop_overlays | |
| Maps every folder in the overlay directory to Podman objects with | |
| accurate size reporting using a robust find-sum method. | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import subprocess | |
| import pwd | |
| # --- Configuration --- | |
| def get_current_username(): | |
| try: | |
| return os.environ.get("SUDO_USER") or pwd.getpwuid(os.getuid()).pw_name | |
| except (KeyError, AttributeError): | |
| return os.environ.get("USER") or "user" | |
| REAL_USER = get_current_username() | |
| OVERLAY_PATH = os.path.expanduser(f"~{REAL_USER}/.local/share/containers/storage/overlay") | |
| def format_size(size_bytes): | |
| """Converts bytes to human readable format.""" | |
| for unit in ['B', 'KB', 'MB', 'GB', 'TB']: | |
| if size_bytes < 1024.0: | |
| return f"{size_bytes:3.1f} {unit}" | |
| size_bytes /= 1024.0 | |
| return f"{size_bytes:3.1f} PB" | |
| def get_folder_size(path): | |
| """ | |
| Calculates folder size by summing file bytes via find -printf. | |
| This is the most resilient method for rootless overlay storage. | |
| """ | |
| try: | |
| # Sums %s (size in bytes) of all files, ignoring errors | |
| cmd = f"find '{path}' -printf '%s\\n' 2>/dev/null | awk '{{sum += $1}} END {{print sum}}'" | |
| out = subprocess.check_output(cmd, shell=True, text=True).strip() | |
| return int(out) if out and out != "0" else 0 | |
| except: | |
| return 0 | |
| def main(): | |
| if not os.path.exists(OVERLAY_PATH): | |
| print(f"[!] Error: Path not found: {OVERLAY_PATH}") | |
| sys.exit(1) | |
| print("[*] Fetching Podman metadata...") | |
| try: | |
| # Avoid --all flag by passing specific IDs | |
| c_ids = subprocess.check_output(["podman", "ps", "-aq"], text=True).split() | |
| i_ids = subprocess.check_output(["podman", "images", "-aq"], text=True).split() | |
| all_ids = list(set(c_ids + i_ids)) | |
| if not all_ids: | |
| print("[!] No containers or images found.") | |
| return | |
| raw_inspect = subprocess.check_output(["podman", "inspect"] + all_ids, text=True) | |
| metadata = json.loads(raw_inspect) | |
| except Exception as e: | |
| print(f"[!] Metadata error: {e}") | |
| return | |
| # Get physical folders (hex IDs are 64 chars) | |
| try: | |
| physical_folders = [f for f in os.listdir(OVERLAY_PATH) if os.path.isdir(os.path.join(OVERLAY_PATH, f)) and len(f) > 60] | |
| except PermissionError: | |
| print("[!] Permission Denied."); return | |
| total_folders = len(physical_folders) | |
| results = [] | |
| print(f"Analyzing {total_folders} folders...") | |
| for i, folder in enumerate(physical_folders): | |
| full_path = os.path.join(OVERLAY_PATH, folder) | |
| found_images = [] | |
| found_containers = [] | |
| # Verified matching logic from singleton | |
| for item in metadata: | |
| item_type = "Container" if item.get("State") else "Image" | |
| item_name = item.get("Name", "").lstrip("/") | |
| if not item_name and "RepoTags" in item: | |
| item_name = ", ".join(item.get("RepoTags", ["<none>"])) | |
| gd = item.get("GraphDriver", {}).get("Data", {}) | |
| match = False | |
| for key in ["UpperDir", "LowerDir", "MergedDir", "WorkDir"]: | |
| val = gd.get(key) | |
| if val and folder in val: | |
| match = True | |
| break | |
| if match: | |
| if item_type == "Container": | |
| found_containers.append(item_name) | |
| else: | |
| found_images.append(item_name) | |
| results.append({ | |
| 'folder': folder, | |
| 'size': get_folder_size(full_path), | |
| 'image': ", ".join(list(set(found_images))), | |
| 'container': ", ".join(list(set(found_containers))) | |
| }) | |
| # Progress bar | |
| progress = (i + 1) / total_folders | |
| bar_len = 40 | |
| filled = int(bar_len * progress) | |
| bar = '█' * filled + '-' * (bar_len - filled) | |
| sys.stdout.write(f"\r|{bar}| {int(progress * 100)}% Complete") | |
| sys.stdout.flush() | |
| # Final sort: Smallest to Largest | |
| results.sort(key=lambda x: x['size']) | |
| print("\n" + "_" * 75) | |
| for res in results: | |
| # Show folders with associations or size > 10MB | |
| if res['image'] or res['container'] or res['size'] > 10485760: | |
| print(f"\nFolder: | {res['folder']} ({format_size(res['size'])})") | |
| print(f"Image: | {res['image']}") | |
| print(f"Container: | {res['container']}") | |
| print("_" * 75) | |
| if __name__ == "__main__": | |
| main() | |
| # END OF snoop_overlays |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment