import asyncio from contextlib import asynccontextmanager, suppress from pathlib import Path from botocore.exceptions import BotoCoreError, ClientError from fastapi import FastAPI, Request from fastapi.responses import JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware from app import process_lock from app.api import v1 from app.config import get_settings, insecure_settings from app.db import init_db from app.services import backups, jobs, poller, rotation from app.ui import routes as ui @asynccontextmanager async def lifespan(app: FastAPI): problems = insecure_settings(get_settings()) if problems: raise RuntimeError("Небезопасная конфигурация: " + "; ".join(problems) + "; см. README «Настройки»") process_lock.acquire(get_settings().database_url) # один процесс на файловую БД (--workers 1) try: init_db() jobs.fail_stale_jobs() task = asyncio.create_task(poller.run()) if get_settings().poll_interval > 0 else None reconcile = asyncio.create_task(backups.reconcile()) # связать файлы бакета с метаданными (best-effort) rotator = asyncio.create_task(rotation.run()) # ротация журнала по настройкам yield for t in (task, reconcile, rotator): if t: t.cancel() with suppress(asyncio.CancelledError): await t finally: process_lock.release() def create_app() -> FastAPI: app = FastAPI(title="ros_control", lifespan=lifespan) app.add_middleware( SessionMiddleware, secret_key=get_settings().session_secret, same_site="strict", https_only=get_settings().session_cookie_secure, ) app.mount("/static", StaticFiles(directory=Path(__file__).parent / "ui" / "static"), name="static") app.include_router(v1.router) app.include_router(ui.router) @app.exception_handler(ui.LoginRequired) async def _login(_: Request, e: ui.LoginRequired): return RedirectResponse("/login", status_code=303) @app.exception_handler(LookupError) async def _404(_: Request, e: LookupError): return JSONResponse({"detail": str(e).strip("'\"")}, status_code=404) @app.exception_handler(ValueError) async def _400(_: Request, e: ValueError): return JSONResponse({"detail": str(e)}, status_code=400) @app.exception_handler(BotoCoreError) @app.exception_handler(ClientError) async def _502(_: Request, e: Exception): return JSONResponse({"detail": f"S3: {e}"}, status_code=502) return app app = create_app()