Files
FabledCurator/backend/app/frontend.py
T

82 lines
2.7 KiB
Python

"""Serves the built Vue SPA from frontend/dist/ with history-mode fallback,
the on-disk image library + thumbnails from /images, and the signed
Firefox extension XPI from frontend/dist/extension/.
"""
import re
from pathlib import Path
from quart import Blueprint, abort, send_file, send_from_directory
FRONTEND_DIST = Path(__file__).resolve().parent.parent.parent / "frontend" / "dist"
IMAGES_ROOT = Path("/images")
XPI_DIR = FRONTEND_DIST / "extension"
_XPI_NAME_RE = re.compile(r"^fabledcurator-[\w.-]+\.xpi$")
frontend_bp = Blueprint("frontend", __name__)
@frontend_bp.route("/images/<path:subpath>")
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("/extension/<filename>")
async def serve_extension(filename: str):
"""Serve the signed FC Firefox extension XPI.
Path whitelist: filename must match fabledcurator-*.xpi. The special
name fabledcurator-latest.xpi serves the most-recently-modified XPI
in the directory.
The application/x-xpinstall MIME tells Firefox to show its native
install prompt instead of downloading the file as a blob.
"""
if not _XPI_NAME_RE.fullmatch(filename):
abort(404)
if not XPI_DIR.is_dir():
abort(404)
if filename == "fabledcurator-latest.xpi":
xpis = sorted(XPI_DIR.glob("fabledcurator-*.xpi"), key=lambda p: p.stat().st_mtime)
if not xpis:
abort(404)
latest = xpis[-1]
return await send_file(
latest, mimetype="application/x-xpinstall",
attachment_filename=latest.name,
)
target = (XPI_DIR / filename).resolve()
try:
target.relative_to(XPI_DIR)
except ValueError:
abort(404)
if not target.is_file():
abort(404)
return await send_file(
target, mimetype="application/x-xpinstall",
attachment_filename=filename,
)
@frontend_bp.route("/")
@frontend_bp.route("/<path:subpath>")
async def serve_spa(subpath: str = ""):
if subpath and (FRONTEND_DIST / subpath).is_file():
return await send_from_directory(FRONTEND_DIST, subpath)
return await send_from_directory(FRONTEND_DIST, "index.html")