"""Sandbox-fähiger Datei-Browser. Egal welcher Pfad reinkommt — er wird gegen ein konfiguriertes Root-Verzeichnis geprüft. Path-Traversal (`../..`) ist damit ausgeschlossen. """ from __future__ import annotations from dataclasses import dataclass from pathlib import Path class PathEscapeError(PermissionError): """Pfad würde aus dem erlaubten Root ausbrechen.""" @dataclass class Entry: name: str rel_path: str # relativ zum Root, mit '/' Separator is_dir: bool size: int # Bytes (0 für Dirs) def safe_resolve(root: Path, rel: str) -> Path: """Pfad relativ zum Root auflösen und verifizieren, dass er drin bleibt.""" root = root.resolve() rel_clean = rel.lstrip("/").strip() target = (root / rel_clean).resolve() try: target.relative_to(root) except ValueError as e: raise PathEscapeError(f"Pfad {rel!r} verlässt Root {root}") from e return target def list_dir(root: Path, rel: str = "") -> tuple[Path, list[Entry]]: """Inhalt eines Verzeichnisses listen, Dirs zuerst, dann Files alphabetisch.""" target = safe_resolve(root, rel) if not target.exists(): raise FileNotFoundError(target) if not target.is_dir(): raise NotADirectoryError(target) entries: list[Entry] = [] for child in sorted(target.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())): if child.name.startswith("."): continue rel_child = str(child.relative_to(root.resolve())).replace("\\", "/") try: size = child.stat().st_size if child.is_file() else 0 except OSError: size = 0 entries.append(Entry( name=child.name, rel_path=rel_child, is_dir=child.is_dir(), size=size, )) return target, entries def breadcrumbs(rel: str) -> list[tuple[str, str]]: """Liste von (Label, Pfad) für die Anzeige als Breadcrumb-Navigation.""" parts = [p for p in rel.split("/") if p] out = [("/", "")] cur = "" for p in parts: cur = f"{cur}/{p}" if cur else p out.append((p, cur)) return out