2e1aaffd93
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
2.7 KiB
Python
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")
|