"""Serve cached DOM.РФ photos. GET /api/v1/photos/{obj_id}/{file_id}?size=thumb|full - size=thumb (default): WebP 320×240 from local cache. If thumb is missing but the original is present locally, generate one on the fly. If neither is present, redirect to the upstream DOM.РФ URL. - size=full: original PNG/JPG if cached locally, else redirect upstream. This isolates the frontend grid from upstream latency and from Next.js dev-mode optimizer cold-hit cost. """ from __future__ import annotations import logging from pathlib import Path from typing import Annotated, Literal from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import FileResponse, RedirectResponse, Response from sqlalchemy import text from sqlalchemy.orm import Session from app.core.db import get_db from app.services.photos.thumbs import make_thumbnail logger = logging.getLogger(__name__) router = APIRouter() _PHOTO_LOOKUP_SQL = text( """ SELECT local_path, thumb_path, photo_url FROM domrf_kn_photos WHERE obj_id = :obj AND obj_file_id = :fid LIMIT 1 """ ) @router.get("/{obj_id}/{file_id}") def get_photo( db: Annotated[Session, Depends(get_db)], obj_id: int, file_id: str, size: Annotated[Literal["thumb", "full"], Query()] = "thumb", ) -> Response: row = db.execute(_PHOTO_LOOKUP_SQL, {"obj": obj_id, "fid": file_id}).mappings().first() if not row: raise HTTPException(status_code=404, detail="photo not registered") local_path = row["local_path"] thumb_path = row["thumb_path"] upstream = row["photo_url"] headers = {"Cache-Control": "public, max-age=604800, immutable"} if size == "full": if local_path and Path(local_path).exists(): return FileResponse(local_path, headers=headers) if upstream: return RedirectResponse(upstream, status_code=302) raise HTTPException(status_code=404, detail="full photo unavailable") # size=thumb if thumb_path and Path(thumb_path).exists(): return FileResponse(thumb_path, media_type="image/webp", headers=headers) # Generate thumb on the fly from cached original if local_path and Path(local_path).exists(): generated = make_thumbnail(Path(local_path)) if generated and generated.exists(): db.execute( text( "UPDATE domrf_kn_photos SET thumb_path = :tp" " WHERE obj_id = :o AND obj_file_id = :f" ), {"tp": str(generated), "o": obj_id, "f": file_id}, ) db.commit() return FileResponse(str(generated), media_type="image/webp", headers=headers) # Last resort — redirect to upstream so the page still renders if upstream: return RedirectResponse(upstream, status_code=302) raise HTTPException(status_code=404, detail="photo unavailable")