87 lines
2.9 KiB
Python
87 lines
2.9 KiB
Python
"""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")
|