init
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
"""Точка входа NVR: FastAPI + супервизор записи + индексатор + retention."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
|
||||
|
||||
from .config import load_config
|
||||
from .db import Database
|
||||
from .recorder.indexer import Indexer
|
||||
from .recorder.supervisor import Supervisor
|
||||
from .services.retention import Retention
|
||||
from .runtime import runtime
|
||||
from . import auth, prefs
|
||||
from .api import cameras, recordings, system, ws
|
||||
from .api.ws import ConnectionManager
|
||||
from .services import go2rtc
|
||||
|
||||
logging.basicConfig(
|
||||
level=os.environ.get("NVR_LOG_LEVEL", "INFO"),
|
||||
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
||||
)
|
||||
log = logging.getLogger("nvr")
|
||||
|
||||
FRONTEND_DIR = os.path.join(os.path.dirname(__file__), "..", "frontend")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
runtime.config = load_config()
|
||||
runtime.db = Database()
|
||||
await runtime.db.connect()
|
||||
|
||||
runtime.ws = ConnectionManager()
|
||||
runtime.ws.bind_loop(asyncio.get_running_loop())
|
||||
|
||||
def on_status(status) -> None:
|
||||
runtime.ws.broadcast_threadsafe({"type": "camera", "camera": status.to_dict()})
|
||||
|
||||
runtime.supervisor = Supervisor(runtime.config, on_status=on_status)
|
||||
runtime.indexer = Indexer(runtime.config, runtime.db)
|
||||
runtime.retention = Retention(runtime.config, runtime.db)
|
||||
|
||||
os.makedirs(runtime.config.storage.path, exist_ok=True)
|
||||
await go2rtc.sync_all(runtime.config) # потоки (вкл. <cam>_main) в go2rtc для рестрима
|
||||
runtime.supervisor.start_all()
|
||||
runtime.indexer.start()
|
||||
runtime.retention.start()
|
||||
log.info("NVR запущен: камер=%d, хранилище=%s",
|
||||
len(runtime.config.cameras), runtime.config.storage.path)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
log.info("остановка NVR…")
|
||||
await runtime.indexer.stop()
|
||||
await runtime.retention.stop()
|
||||
await runtime.supervisor.stop_all()
|
||||
await runtime.db.close()
|
||||
|
||||
|
||||
app = FastAPI(title="CoRE.Vision", version="1.0", lifespan=lifespan)
|
||||
|
||||
# публичные пути (без авторизации)
|
||||
PUBLIC_PATHS = {"/login", "/api/login", "/api/health"}
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def cross_origin_isolation(request: Request, call_next):
|
||||
# COOP/COEP включают crossOriginIsolated → доступен SharedArrayBuffer,
|
||||
# который нужен многопоточному WASM-декодеру H.265 (свой плеер jessibuca).
|
||||
response = await call_next(request)
|
||||
response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
|
||||
response.headers["Cross-Origin-Embedder-Policy"] = "credentialless"
|
||||
return response
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def require_auth(request: Request, call_next):
|
||||
path = request.url.path
|
||||
if path in PUBLIC_PATHS or path.startswith("/static/"):
|
||||
return await call_next(request)
|
||||
if auth.auth_manager.validate(request.cookies.get(auth.COOKIE_NAME)):
|
||||
return await call_next(request)
|
||||
if path.startswith("/api/"):
|
||||
return JSONResponse({"detail": "unauthorized"}, status_code=401)
|
||||
return RedirectResponse("/login")
|
||||
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(prefs.router)
|
||||
app.include_router(cameras.router)
|
||||
app.include_router(recordings.router)
|
||||
app.include_router(system.router)
|
||||
app.include_router(ws.router)
|
||||
|
||||
|
||||
# ── фронтенд ────────────────────────────────────────────────────
|
||||
@app.get("/")
|
||||
async def index() -> FileResponse:
|
||||
return FileResponse(os.path.join(FRONTEND_DIR, "index.html"))
|
||||
|
||||
|
||||
@app.get("/archive")
|
||||
async def archive() -> FileResponse:
|
||||
return FileResponse(os.path.join(FRONTEND_DIR, "archive.html"))
|
||||
|
||||
|
||||
@app.get("/settings")
|
||||
async def settings() -> FileResponse:
|
||||
return FileResponse(os.path.join(FRONTEND_DIR, "settings.html"))
|
||||
|
||||
|
||||
@app.get("/cams")
|
||||
async def cams() -> FileResponse:
|
||||
return FileResponse(os.path.join(FRONTEND_DIR, "cams.html"))
|
||||
|
||||
|
||||
@app.get("/layout")
|
||||
async def layout() -> RedirectResponse:
|
||||
# раскладка переехала во вкладку «Настройки → Раскладка»
|
||||
return RedirectResponse("/settings")
|
||||
|
||||
|
||||
@app.get("/login")
|
||||
async def login_page() -> FileResponse:
|
||||
return FileResponse(os.path.join(FRONTEND_DIR, "login.html"))
|
||||
|
||||
|
||||
if os.path.isdir(os.path.join(FRONTEND_DIR, "static")):
|
||||
app.mount("/static", StaticFiles(directory=os.path.join(FRONTEND_DIR, "static")), name="static")
|
||||
Reference in New Issue
Block a user