diff --git a/backend/app/frontend.py b/backend/app/frontend.py index 879a6c9..7855148 100644 --- a/backend/app/frontend.py +++ b/backend/app/frontend.py @@ -1,14 +1,36 @@ -"""Serves the built Vue SPA from frontend/dist/ with history-mode fallback.""" +"""Serves the built Vue SPA from frontend/dist/ with history-mode fallback, +and the on-disk image library + thumbnails from /images. +""" from pathlib import Path -from quart import Blueprint, send_from_directory +from quart import Blueprint, abort, send_from_directory FRONTEND_DIST = Path(__file__).resolve().parent.parent.parent / "frontend" / "dist" +IMAGES_ROOT = Path("/images") frontend_bp = Blueprint("frontend", __name__) +@frontend_bp.route("/images/") +async def serve_image(subpath: str): + """Serve a file from the /images volume (originals + thumbnails). + + Without this route the SPA catch-all below would swallow image + requests and return index.html, leaving the browser to render the + aspect-ratio-shaped grey placeholder. + """ + target = (IMAGES_ROOT / subpath).resolve() + # Defend against path-traversal: refuse anything that escapes /images. + try: + target.relative_to(IMAGES_ROOT) + except ValueError: + abort(404) + if not target.is_file(): + abort(404) + return await send_from_directory(IMAGES_ROOT, subpath) + + @frontend_bp.route("/") @frontend_bp.route("/") async def serve_spa(subpath: str = ""):