from pathlib import Path from contextlib import asynccontextmanager import anyio from fastapi import Depends, FastAPI, Request from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from app.api.router import api_router from app.api.dependencies import require_current_user from app.api.routes.tus_uploads import router as tus_upload_router from app.web.routes import router as web_router from app.core.config import get_settings from app.core.exceptions import AppError from app.db.session import create_session_factory from app.services.directory_service import DirectoryService from app.services.auth_service import AuthService from app.services.job_service import JobService from app.services.local_storage_service import LocalStorageService from app.services.policy_service import PolicyService from app.services.storage_service import StorageService from app.services.upload_service import UploadService WEB_DIST_DIR = Path(__file__).resolve().parent / "web" / "dist" @asynccontextmanager async def lifespan(_app: FastAPI): session_factory = create_session_factory() async with session_factory() as session: service = DirectoryService(session) await service.ensure_root_directory() auth_service = AuthService(session) await auth_service.ensure_bootstrap_user() upload_service = UploadService(session) await upload_service.ensure_temp_dir() local_storage_service = LocalStorageService(session) await local_storage_service.ensure_backend_ready() policy_service = PolicyService(session) await policy_service.ensure_default_policies() storage_service = StorageService(session) await storage_service.ensure_cache_dir() settings = get_settings() async def _job_loop() -> None: while True: async with session_factory() as session: service = JobService(session) await service.run_pending_jobs(limit=settings.job_batch_size) await anyio.sleep(settings.job_poll_interval_seconds) async with anyio.create_task_group() as task_group: task_group.start_soon(_job_loop) yield task_group.cancel_scope.cancel() def create_app() -> FastAPI: settings = get_settings() app = FastAPI( title=settings.app_name, debug=settings.debug, version="0.1.0", lifespan=lifespan, ) @app.exception_handler(AppError) async def app_error_handler(_request: Request, exc: AppError) -> JSONResponse: status_code = 400 if exc.code.endswith("_not_found") or exc.code == "not_found": status_code = 404 elif exc.code.endswith("_conflict") or exc.code == "conflict": status_code = 409 elif exc.code == "unauthorized" or exc.code.startswith("auth_") or exc.code == "authentication_failed": status_code = 401 elif exc.code == "range_not_satisfiable": status_code = 416 return JSONResponse( status_code=status_code, content={ "error": { "code": exc.code, "message": exc.detail, "details": None, } }, ) app.include_router(api_router, prefix=settings.api_prefix) app.include_router(web_router) app.include_router( tus_upload_router, prefix="/files", tags=["tus"], dependencies=[Depends(require_current_user)], ) if (WEB_DIST_DIR / "assets").exists(): app.mount( "/assets", StaticFiles(directory=WEB_DIST_DIR / "assets"), name="web-assets", ) return app app = create_app()