Files
Donkie 1a04139c1d Add update check against GitHub releases (#1192)
The server checks the latest GitHub release in the background at startup
and every 12h, logs a warning when a newer version exists, and exposes
latest_version/update_available on /info. The client_v2 footer shows a
link to the new release. Disable with SPOOLMAN_UPDATE_CHECK=FALSE.
2026-09-27 08:32:57 +02:00

121 lines
3.7 KiB
Python

"""Router setup for the v1 version of the API."""
# ruff: noqa: D103
import asyncio
import logging
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import JSONResponse
from starlette.requests import Request
from starlette.responses import Response
from spoolman import env, update_check
from spoolman.database.database import backup_global_db
from spoolman.exceptions import ItemNotFoundError
from spoolman.externaldb import get_external_db_name
from spoolman.ws import websocket_manager
from . import export, externaldb, field, filament, models, other, search, setting, spool, tag, vendor
logger = logging.getLogger(__name__)
app = FastAPI(
title="Spoolman REST API v1",
version="1.0.0",
description="""
REST API for Spoolman.
The API is served on the path `/api/v1/`.
Some endpoints also serve a websocket on the same path. The websocket is used to listen for changes to the data
that the endpoint serves. The websocket messages are JSON objects. Additionally, there is a root-level websocket
endpoint that listens for changes to any data in the database.
""",
)
@app.exception_handler(ItemNotFoundError)
async def itemnotfounderror_exception_handler(_request: Request, exc: ItemNotFoundError) -> Response:
logger.debug(exc)
return JSONResponse(
status_code=404,
content={"message": exc.args[0]},
)
# Add a general info endpoint
@app.get("/info")
async def info() -> models.Info:
"""Return general info about the API."""
return models.Info(
version=env.get_version(),
debug_mode=env.is_debug_mode(),
automatic_backups=env.is_automatic_backup_enabled(),
data_dir=str(env.get_data_dir().resolve()),
logs_dir=str(env.get_logs_dir().resolve()),
backups_dir=str(env.get_backups_dir().resolve()),
db_type=str(env.get_database_type() or "sqlite"),
external_db_name=get_external_db_name(),
git_commit=env.get_commit_hash(),
build_date=env.get_build_date(),
latest_version=update_check.latest_version,
update_available=update_check.is_update_available(),
)
# Add health check endpoint
@app.get("/health")
async def health() -> models.HealthCheck:
"""Return a health check."""
return models.HealthCheck(status="healthy")
# Add endpoint for triggering a db backup
@app.post(
"/backup",
description="Trigger a database backup. Only applicable for SQLite databases.",
response_model=models.BackupResponse,
responses={500: {"model": models.Message}},
)
async def backup(): # noqa: ANN201
"""Trigger a database backup."""
result = await backup_global_db()
if result.path is None:
return JSONResponse(
status_code=500,
content={"message": "Backup failed. See server logs for more information."},
)
return models.BackupResponse(path=str(result.path), created=result.created)
@app.websocket(
"/",
name="Listen to any changes",
)
async def notify(
websocket: WebSocket,
) -> None:
await websocket.accept()
websocket_manager.connect((), websocket)
try:
while True:
await asyncio.sleep(0.5)
if await websocket.receive_text():
await websocket.send_json({"status": "healthy"})
except WebSocketDisconnect:
websocket_manager.disconnect((), websocket)
# Add routers
app.include_router(filament.router)
app.include_router(spool.router)
app.include_router(vendor.router)
app.include_router(setting.router)
app.include_router(field.router)
app.include_router(other.router)
app.include_router(externaldb.router)
app.include_router(export.router)
app.include_router(search.router)
app.include_router(tag.router)