From f653c26680584acf923c975e9057ad909a4204c1 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 23 May 2026 11:51:08 -0400 Subject: [PATCH] fix(web): serve /images/ from disk so thumbnails+originals render instead of grey placeholders Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/app/frontend.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) 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 = ""):