"""Bilder hochladen, ausliefern und löschen.""" from __future__ import annotations from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile, status from fastapi.responses import FileResponse from sqlalchemy.orm import Session from app import crud, images, security from app.config import einstellungen from app.database import get_db from app.models import Item, ItemBild from app.schemas import BildAus, ItemAus router = APIRouter(tags=["Bilder"]) def _hochladen(datei: UploadFile) -> str: cfg = einstellungen() rohdaten = datei.file.read(cfg.max_upload_bytes + 1) try: return images.speichern(rohdaten, cfg.upload_pfad, cfg.max_upload_bytes) except images.BildFehler as e: raise HTTPException(status_code=422, detail=str(e)) from e # ------------------------------------------------------------- Ausliefern --- @router.get("/uploads/{dateiname}", response_class=FileResponse) def bild_ausliefern(dateiname: str): """Liefert ein hochgeladenes Bild aus. Der Name kommt aus der URL und ist damit Nutzereingabe - auch wenn wir ihn ursprünglich selbst vergeben haben. Ohne die Musterprüfung liesse sich hier jede Datei des Systems abrufen ("../../etc/passwd"). Die Bilder liegen bewusst nicht unter static/: sonst wäre jede hochgeladene Datei unter ihrem Namen direkt abrufbar, ohne dass ein fester Content-Type gesetzt werden kann. """ if not images.dateiname_gueltig(dateiname): raise HTTPException(status_code=404, detail="Bild nicht gefunden.") pfad = einstellungen().upload_pfad / dateiname if not pfad.is_file(): raise HTTPException(status_code=404, detail="Bild nicht gefunden.") return FileResponse( pfad, media_type="image/webp", headers={ # Fest gesetzt, damit der Browser nichts anderes hineinliest. "X-Content-Type-Options": "nosniff", "Cache-Control": "public, max-age=31536000, immutable", }, ) # --------------------------------------------------------------- Hochladen --- @router.post( "/api/v1/items/{item_id}/images", response_model=list[BildAus], status_code=status.HTTP_201_CREATED, tags=["Bilder"], dependencies=[security.BetreiberNoetig], ) def bilder_hochladen( item_id: int, dateien: list[UploadFile] = File(...), db: Session = Depends(get_db), ): item = crud.item_holen(db, item_id) if item is None: raise HTTPException(status_code=404, detail="Kleidungsstück nicht gefunden.") neue: list[ItemBild] = [] for datei in dateien: name = _hochladen(datei) neue.append(crud.bild_anhaengen(db, item, name)) db.refresh(item) return neue @router.post( "/api/v1/items/batch-upload", response_model=list[ItemAus], status_code=status.HTTP_201_CREATED, tags=["Bilder"], dependencies=[security.BetreiberNoetig], ) def stapel_import( dateien: list[UploadFile] = File(...), db: Session = Depends(get_db), ): """Stapel-Import: pro Foto ein Entwurf. Der Weg für den Erstbestand - erst alles mit der Kamera-App fotografieren, dann alle Fotos auf einmal hochladen und die Angaben später nachtragen. Die Entwürfe erscheinen nicht in der Galerie. Ein unlesbares Bild lässt den ganzen Stapel scheitern, statt still übersprungen zu werden: bei zwanzig Fotos fiele sonst niemandem auf, dass eines fehlt. """ namen = [_hochladen(datei) for datei in dateien] entwuerfe = crud.entwuerfe_anlegen(db, len(namen)) for entwurf, name in zip(entwuerfe, namen): crud.bild_anhaengen(db, entwurf, name) db.refresh(entwurf) return [ ItemAus.model_validate( {**e.__dict__, "display_title": e.anzeige_titel, "kategorie": e.kategorie, "bilder": e.bilder} ) for e in entwuerfe ] # ----------------------------------------------------------------- Ändern --- @router.post("/api/v1/images/{bild_id}/primary", response_model=BildAus, tags=["Bilder"], dependencies=[security.BetreiberNoetig]) def hauptbild_setzen(bild_id: int, db: Session = Depends(get_db)): bild = db.get(ItemBild, bild_id) if bild is None: raise HTTPException(status_code=404, detail="Bild nicht gefunden.") return crud.hauptbild_setzen(db, bild) @router.delete( "/api/v1/images/{bild_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Bilder"], dependencies=[security.BetreiberNoetig], ) def bild_loeschen(bild_id: int, db: Session = Depends(get_db)): bild = db.get(ItemBild, bild_id) if bild is None: raise HTTPException(status_code=404, detail="Bild nicht gefunden.") dateiname = crud.bild_loeschen(db, bild) images.loeschen(einstellungen().upload_pfad, dateiname) return Response(status_code=status.HTTP_204_NO_CONTENT)