Now supports running under a sub path

Just set the SPOOLMAN_BASE_PATH environment variable and it should work.

Resolves #95
This commit is contained in:
Donkie
2024-05-10 11:36:33 +02:00
parent fc532ff697
commit bc32f1e890
18 changed files with 158 additions and 43 deletions

View File

@@ -2,24 +2,37 @@
# ruff: noqa: PTH118
import logging
import os
from pathlib import Path
from typing import Union
from fastapi.staticfiles import StaticFiles
logger = logging.getLogger(__name__)
class SinglePageApplication(StaticFiles):
"""Serve a single page application."""
def __init__(self, directory: str) -> None:
def __init__(self, directory: str, base_path: str) -> None:
"""Construct."""
super().__init__(directory=directory, packages=None, html=True, check_dir=True)
self.base_path = base_path.removeprefix("/")
def lookup_path(self, path: str) -> tuple[str, Union[os.stat_result, None]]:
"""Return index.html if the requested file cannot be found."""
path = path.removeprefix(self.base_path).removeprefix("/")
full_path, stat_result = super().lookup_path(path)
if stat_result is None:
ext = Path(path).suffix
# Check if user is looking for some specific non-document file
if len(ext) > 1 and ext != ".html":
# If so, return 404
return ("", None)
# Otherwise, they did look for a document, lead them to index.html
return super().lookup_path("index.html")
return (full_path, stat_result)

View File

@@ -398,3 +398,19 @@ def is_metrics_enabled() -> bool:
raise ValueError(
f"Failed to parse SPOOLMAN_METRICS_ENABLED variable: Unknown metrics enabled '{metrics_enabled}'.",
)
def get_base_path() -> str:
"""Get the base path.
This is formated so that it always starts with a /, and does not end with a /
Returns:
str: The base path.
"""
path = os.getenv("SPOOLMAN_BASE_PATH", "")
if len(path) == 0:
return ""
# Ensure it starts with / and does not end with /
return "/" + path.strip("/")

View File

@@ -1,4 +1,5 @@
"""Main entrypoint to the server."""
import logging
import subprocess
from logging.handlers import TimedRotatingFileHandler
@@ -8,7 +9,7 @@ import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import PlainTextResponse
from fastapi.responses import PlainTextResponse, RedirectResponse, Response
from prometheus_client import generate_latest
from scheduler.asyncio.scheduler import Scheduler
@@ -38,12 +39,12 @@ app = FastAPI(
version=env.get_version(),
)
app.add_middleware(GZipMiddleware)
app.mount("/api/v1", v1_app)
app.mount(env.get_base_path() + "/api/v1", v1_app)
# WA for prometheus /metrics bind with SinglePageApp at root
@app.get(
"/metrics",
env.get_base_path() + "/metrics",
response_class=PlainTextResponse,
name="Get metrics for prometheus",
description=(
@@ -55,8 +56,36 @@ def get_metrics() -> bytes:
return generate_latest(registry)
app.mount("", app=SinglePageApplication(directory="client/dist"))
base_path = env.get_base_path()
if base_path != "":
logger.info("Base path is: %s", base_path)
# If base path is set, add a redirect from non-slash suffix to slash
# suffix. Otherwise it won't work.
@app.get(base_path)
def root_redirect() -> Response:
"""Redirect to base path."""
return RedirectResponse(base_path + "/")
# Return a dynamic js config file
# This is so that the client side can access the base path variable.
@app.get(env.get_base_path() + "/config.js")
def get_configjs() -> Response:
"""Return a dynamic js config file."""
if '"' in base_path:
raise ValueError("Base path contains quotes, which are not allowed.")
return Response(
content=f"""
window.SPOOLMAN_BASE_PATH = "{base_path}";
""",
media_type="text/javascript",
)
# Mount the client side app
app.mount(base_path, app=SinglePageApplication(directory="client/dist", base_path=env.get_base_path()))
# Allow all origins if in debug mode
if env.is_debug_mode():