import os import smbclient from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from datetime import datetime, timezone from src.core.PDFWorker.PDFTempPoller import PDFPollWorker from .core.config import get_settings # from .api.router import files_router from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from src.core.nats_callback import build_file_handler from fastapi.exceptions import HTTPException, StarletteHTTPException from src.api.router import le_router, files_router, cg_router, rf_office_router, osv_router, fsl_router from miniopy_async import Minio from src.core.config import settings from src.core.clients import S3Storage, PollWorker from src.core.async_cache import AsyncInMemoryCache # from src.core.logger import setup_logger from nats.aio.client import Client as NATS import base64 import nkeys from loguru import logger import os from fastapi.staticfiles import StaticFiles from starlette.middleware.gzip import GZipMiddleware from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware from src.core.exception_handlers import register_exception_handlers settings = get_settings() VERSION = "1.0.0" UP_TIME = datetime.now(timezone.utc) async def sub_cb(msg): return None protected_middleware_available = False if not settings.DEBUG: try: from raisa_fastapi_protected_api import ( # pyright: ignore[reportMissingImports] AuthorizationMiddleware, OpenEndpoint, ProtectedOAuthSettings, ProtectedRolesSettings, ProtectedSettings, SearchType, ) protected_middleware_available = True except ImportError: protected_middleware_available = False def _normalize_root_path(value: str) -> str: if not value: return "" value = value.strip() if value == "/": return "" return "/" + value.strip("/") _APP_ROOT_PATH = _normalize_root_path(settings.ROOT_PATH) @asynccontextmanager async def lifespan(app): app.state.nats_subs = [] app.state.storage = None app.state.nats = None app.state.poller = None app.state.pdf_poller = None s3_client = None smb_registered = False try: minio = settings.OPENBAO.MINIO if minio is not None: s3_client = Minio( minio.MINIO_URL, access_key=minio.MINIO_ADMIN_LOGIN, secret_key=minio.MINIO_ADMIN_PASSWORD, secure=minio.MINIO_SECURE, ) if not await s3_client.bucket_exists(minio.MINIO_BUCKET): raise RuntimeError(f"Бакет MinIO {minio.MINIO_BUCKET!r} не создан") app.state.storage = S3Storage(s3_client, minio.MINIO_BUCKET) logger.info("MinIO подключён, бакет: {}", minio.MINIO_BUCKET) smb = settings.OPENBAO.SMB if smb is not None: smbclient.register_session(smb.SMB_HOST, smb.SMB_LOGIN, smb.SMB_PASSWORD, port=smb.SMB_PORT) smb_registered = True logger.info("SMB подключён") nats_settings = settings.OPENBAO.NATS if nats_settings is not None: nc = NATS() app.state.nats = nc if not settings.DEBUG: keypair = nkeys.from_seed(nats_settings.NATS_SEED.encode("utf-8")) def user_jwt_cb(): return nats_settings.NATS_JWT.encode("utf-8") def signature_cb(nonce): return base64.b64encode(keypair.sign(nonce.encode("utf-8"))) await nc.connect(nats_settings.NATS_URL, user_jwt_cb=user_jwt_cb, signature_cb=signature_cb, max_reconnect_attempts=2) logger.info("NATS подключён") missing = [name for name in ("MINIO", "NATS", "SMB") if getattr(settings.OPENBAO, name) is None] if missing: logger.info("Распознавание файлов отключено: не настроены {}", ", ".join(missing)) else: cache = AsyncInMemoryCache() app.state.poller = PollWorker(nc, cache, app.state.storage) await app.state.poller.start() app.state.pdf_poller = PDFPollWorker(cache, app.state.storage) await app.state.pdf_poller.start() logger.info("Приложение запущено") yield finally: for worker in (app.state.poller, app.state.pdf_poller): if worker is not None: await worker.stop() if not settings.DEBUG: for sub in app.state.nats_subs: await sub.unsubscribe() if app.state.nats is not None: await app.state.nats.close() if s3_client is not None: await s3_client.close_session() if smb_registered: smbclient.reset_connection_cache() logger.info("Приложение закрывается") app = FastAPI( title="Fast API", version=VERSION, root_path=settings.ROOT_PATH, lifespan=lifespan ) @app.get( "/healthcheck", status_code=200 ) async def healthcheck(): return { "uptime": UP_TIME, "version": VERSION } app.include_router(le_router) app.include_router(files_router) app.include_router(cg_router) app.include_router(rf_office_router) app.include_router(osv_router) app.include_router(fsl_router) register_exception_handlers(app) app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=1) app.add_middleware(ProxyHeadersMiddleware, trusted_hosts=["*"]) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) #Создание каталога для web приложения os.makedirs(os.path.join('web'), exist_ok=True) #Подключаем все ендпоинты и роуты до маунта статических файлов # app.include_router(files_router) #Подключение собранного web приложения def sep_static(path, stat_dir): sep_path = path.split("/") index = sep_path.index(stat_dir) return "/".join(sep_path[index:]) class SPAStaticFiles(StaticFiles): async def get_response(self, path: str, scope): try: return await super().get_response(path, scope) except (HTTPException, StarletteHTTPException) as ex: if ex.status_code == 404: if "images" in path: return await super().get_response(sep_static(path, "images"), scope) if "static" in path: return await super().get_response(sep_static(path, "static"), scope) return await super().get_response("index.html", scope) else: raise ex app.mount("/", SPAStaticFiles(directory="web", html=True), name="web") app.mount("/", StaticFiles(directory="web", html=True), name="web")