- 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
46 lines
1.7 KiB
Python
46 lines
1.7 KiB
Python
"""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)
|