71 lines
2.1 KiB
Python
71 lines
2.1 KiB
Python
"""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
|