android-unlock-and-more-box/aubox/filebrowse.py

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