Files
pgames-manager/backend/app/routers/media.py
T
mrleo1nid 69f71cb836
CI / frontend (push) Failing after 19s
CI / backend (push) Failing after 41s
feat: import F95 screenshot galleries with AVIF support
2026-10-26 17:51:19 +03:00

55 lines
2.6 KiB
Python

"""Authenticated media delivery; nginx only receives validated internal paths."""
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import FileResponse, Response
from sqlalchemy import exists, or_, select, text
from app.blobs import BLOB_ID_RE, blob_path
from app.deps import DB, require
from app.models import Game, GameVersion, User
from app.routers.games import visible_versions_filter
from app.routers.versions import get_version
router = APIRouter(tags=["media"])
Player = Annotated[User, Depends(require("games.play"))]
def deliver(blob_id: str, request: Request):
if not BLOB_ID_RE.fullmatch(blob_id) or not blob_path(blob_id).is_file():
raise HTTPException(404)
headers = {"Cache-Control": "private, no-cache", "ETag": f'"{blob_id}"',
"X-Content-Type-Options": "nosniff", "Vary": "Cookie"}
if request.headers.get("if-none-match") == headers["ETag"]:
return Response(status_code=304, headers=headers)
if request.headers.get("x-pgames-accel") == "1":
headers["X-Accel-Redirect"] = f"/_protected_blobs/{blob_id[:2]}/{blob_id[2:4]}/{blob_id}"
return Response(headers=headers)
return FileResponse(blob_path(blob_id), headers=headers)
@router.api_route("/api/versions/{version_id}/files/{blob_id}", methods=["GET", "HEAD"])
async def version_file(version_id: int, blob_id: str, request: Request, user: Player, db: DB):
v = await get_version(db, user, version_id)
if not v.manifest or not any(f["hash"] == blob_id for f in v.manifest["files"].values()):
raise HTTPException(404)
return deliver(blob_id, request)
@router.api_route("/blobs/{blob_id}", methods=["GET", "HEAD"])
async def legacy_file(blob_id: str, request: Request, user: Player, db: DB):
"""Compatibility for cached cover URLs; hashes never grant access themselves."""
if not BLOB_ID_RE.fullmatch(blob_id):
raise HTTPException(404)
visible = visible_versions_filter(user)
cover = await db.scalar(select(Game.id).where(or_(Game.cover_hash == blob_id, Game.gallery_hashes.any(blob_id)),
or_(Game.created_by_id == user.id, exists().where(GameVersion.game_id == Game.id, visible))).limit(1))
if cover is None:
# Keep old clients functional, with exactly the same version visibility rules.
found = await db.scalar(select(GameVersion.id).where(visible,
text("EXISTS (SELECT 1 FROM jsonb_each(game_versions.manifest->'files') f WHERE f.value->>'hash' = :blob)")
).params(blob=blob_id).limit(1))
if found is None:
raise HTTPException(404)
return deliver(blob_id, request)