"""Findet Bilder im Vault und lädt sie in die BookStack-Bildergalerie.""" from __future__ import annotations import os from pathlib import Path from bookstack.api import ApiFehler, BookStackApi # In D:\Claude\skills\ liegt ein grosses Fremd-Repo — laut CLAUDE.md haengt sich # ein rekursiver Lauf dort praktisch endlos auf. Diese Ordner werden uebersprungen. UEBERSPRINGEN = {"skills", ".git", ".venv", "node_modules", ".obsidian", "__pycache__", ".pytest_cache"} TYPEN = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".avif": "image/avif", } # Die API-Doku nennt „max 50KB"; das ist mit hoher Wahrscheinlichkeit die # Laravel-Regel max:50000, die bei Dateien in Kilobyte rechnet (~48,8 MB). # Deshalb hier eine Grenze knapp darunter — die echte Ablehnung der API wird # ohnehin woertlich weitergereicht. MAX_BYTES = 48 * 1024 * 1024 class BildFehler(Exception): """Ein Bild konnte nicht übertragen werden.""" def finde_bilddatei(vault: Path, notiz_pfad: Path, name: str) -> Path | None: """Sucht erst neben der Notiz, dann in der Vault-Wurzel, dann im ganzen Vault.""" for kandidat in (notiz_pfad.parent / name, vault / name): if kandidat.is_file(): return kandidat for wurzel, ordner, dateien in os.walk(vault): ordner[:] = [eintrag for eintrag in ordner if eintrag not in UEBERSPRINGEN] if name in dateien: return Path(wurzel) / name return None class Bilduploader: def __init__(self, api: BookStackApi, vault: Path) -> None: self._api = api self._vault = vault def _vorhandene(self, seiten_id: int) -> dict[str, str]: """Bereits an dieser Seite hängende Galeriebilder, Name → URL.""" try: eintraege = self._api.liste(f"/api/image-gallery?filter[uploaded_to]={seiten_id}") except ApiFehler: return {} return {str(eintrag.get("name", "")): str(eintrag.get("url", "")) for eintrag in eintraege} def lade_hoch( self, notiz_pfad: Path, seiten_id: int, namen: list[str] ) -> tuple[dict[str, str], list[str]]: """Liefert (Name → URL) und eine Liste von Hinweisen für den Bericht.""" tabelle: dict[str, str] = {} hinweise: list[str] = [] if not namen: return tabelle, hinweise vorhanden = self._vorhandene(seiten_id) for name in dict.fromkeys(namen): if name in vorhanden and vorhanden[name]: tabelle[name] = vorhanden[name] continue pfad = finde_bilddatei(self._vault, notiz_pfad, name) if pfad is None: hinweise.append(f"Bild nicht gefunden im Vault: {name}") continue mime = TYPEN.get(pfad.suffix.lower()) if mime is None: hinweise.append(f"Bildtyp nicht unterstützt: {name}") continue groesse = pfad.stat().st_size if groesse > MAX_BYTES: hinweise.append(f"Bild zu groß ({groesse // (1024 * 1024)} MB): {name}") continue try: antwort = self._api.sende_formular( "/api/image-gallery", {"type": "gallery", "uploaded_to": str(seiten_id), "name": name}, ("image", name, pfad.read_bytes(), mime), ) except ApiFehler as fehler: hinweise.append(f"Bild abgelehnt ({name}): {fehler.meldung}") continue url = str(antwort.get("url", "")) if url: tabelle[name] = url else: hinweise.append(f"Bild ohne URL zurückgekommen: {name}") return tabelle, hinweise