feat(logos): Provider-Kette, lokaler Cache und Markenfarben
- simple-icons als kompakter Index im Repository (3.459 Marken, 2 MB gzip),
erzeugt von scripts/vendor_simple_icons.py bzw. `make vendor-icons`
- logo.dev und Brandfetch als optionale Adapter, ohne Schlüssel übersprungen
- Favicon-Fallback und generierter Buchstaben-Avatar als Garantie
- Bei eindeutigem Offline-Treffer unterbleiben Anfragen nach außen komplett
- Cache im Dateisystem nach SHA-256, Auslieferung nur über /api/logos/{id}
mit immutable-Header und ETag
- Markenfarbe aus SVG-Fills bzw. per k-Means (k=4) über 64x64 Pixel, dazu eine
aufgehellte Variante mit mindestens 4,5:1 Kontrast auf dunklem Grund
- Kandidatensuche mit Vorauswahl, Auswahl, Upload und Zurücksetzen
- Bildtyp wird nur noch am Inhalt bestimmt, nicht an der gemeldeten Kopfzeile
- 60 neue Tests, insgesamt 208 grün
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
"""Auslieferung der zwischengespeicherten Logos.
|
||||
|
||||
Die Dateien kommen ausschließlich von der lokalen Platte; beim Seitenaufruf
|
||||
entsteht kein Zugriff auf fremde Dienste. Der Dateiname ist der Inhaltshash,
|
||||
daher darf unbegrenzt zwischengespeichert werden.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, Response, status
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from app.api.deps import DbSession
|
||||
from app.core.config import settings
|
||||
from app.core.errors import NotFoundError
|
||||
from app.models import LogoAsset
|
||||
from app.schemas.common import ErrorResponse
|
||||
from app.services.crud import get_or_404
|
||||
from app.services.logos import cache_headers
|
||||
|
||||
router = APIRouter(prefix="/logos", tags=["logos"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{asset_id}",
|
||||
responses={
|
||||
status.HTTP_200_OK: {
|
||||
"content": {"image/svg+xml": {}, "image/png": {}, "image/jpeg": {}},
|
||||
"description": "Die Logodatei aus dem lokalen Cache.",
|
||||
},
|
||||
status.HTTP_404_NOT_FOUND: {"model": ErrorResponse},
|
||||
},
|
||||
summary="Logo ausliefern",
|
||||
description="Liefert die Datei mit `Cache-Control: public, max-age=31536000, immutable`.",
|
||||
)
|
||||
async def read_logo(asset_id: int, request: Request, session: DbSession) -> Response:
|
||||
asset = await get_or_404(session, LogoAsset, asset_id)
|
||||
pfad = settings.logo_storage_dir / asset.file_path
|
||||
if not pfad.exists():
|
||||
raise NotFoundError("Die Logodatei fehlt im Cache.", code="logo_file_missing")
|
||||
|
||||
headers = cache_headers(asset)
|
||||
# Unveränderliche Inhalte: ein passendes ETag beantwortet die Anfrage sofort.
|
||||
if request.headers.get("if-none-match") == headers["ETag"]:
|
||||
return Response(status_code=status.HTTP_304_NOT_MODIFIED, headers=headers)
|
||||
|
||||
return FileResponse(pfad, media_type=asset.mime, headers=headers)
|
||||
Reference in New Issue
Block a user