import mimetypes import os import posixpath import ssl from functools import lru_cache from datetime import datetime, timezone from pathlib import Path from typing import Generator, Optional, Tuple from urllib.parse import quote from urllib.parse import urlparse import jwt from bson import ObjectId from fastapi import FastAPI, Header, HTTPException, Request, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, StreamingResponse from pymongo import MongoClient from pymongo.errors import PyMongoError def _env_str(name: str, default: str = "") -> str: v = os.environ.get(name) return v.strip() if isinstance(v, str) and v.strip() else default def _jwt_secret() -> str: # Read env at runtime (important on Windows/uvicorn worker reloads). return _env_str("JWT_SECRET") def _mongo_uri() -> str: # Read env at runtime (important on Windows/uvicorn worker reloads). return _env_str("MONGO_URI") def _mongo_db_name() -> str: # Prefer explicit env var, otherwise try to parse from URI path. explicit = _env_str("MONGO_DB_NAME") if explicit: return explicit uri = _mongo_uri() if not uri: return "" try: parsed = urlparse(uri) # Path is like "/dbname" db = (parsed.path or "").lstrip("/") # Strip any extra segments just in case db = db.split("/")[0].strip() return db except Exception: return "" def _env_int(name: str, default: int) -> int: try: return int(_env_str(name, str(default))) except Exception: return default def _video_files_dir() -> Path: # Read env at runtime (important on Windows/uvicorn worker reloads). return Path(_env_str("VIDEO_FILES_DIR", str(Path.cwd() / "public" / "video"))).resolve() def _parse_bearer(authorization: Optional[str]) -> str: raw = (authorization or "").strip() if raw.lower().startswith("bearer "): return raw[7:].strip() return "" def _is_safe_relative_file(p: str) -> bool: if not isinstance(p, str): return False s = p.strip() if not s or "\x00" in s: return False if s.startswith("/") or s.startswith("\\"): return False norm = posixpath.normpath(s.replace("\\", "/")) if norm == ".." or norm.startswith("../"): return False return True def _guess_content_type(file_path: Path) -> str: ctype, _ = mimetypes.guess_type(str(file_path)) return ctype or "application/octet-stream" def _parse_range(range_header: str, file_size: int) -> Optional[Tuple[int, int]]: # Only supports a single bytes range: "bytes=start-end" raw = (range_header or "").strip() if not raw: return None if not raw.lower().startswith("bytes="): return None spec = raw[6:].strip() if "," in spec: return None if "-" not in spec: return None start_s, end_s = spec.split("-", 1) start = int(start_s) if start_s else 0 end = int(end_s) if end_s else (file_size - 1) if start < 0 or end < 0 or start > end or start >= file_size: return None end = min(end, file_size - 1) return start, end def _file_iter(path: Path, start: int, end: int, chunk_size: int = 1024 * 1024) -> Generator[bytes, None, None]: with path.open("rb") as f: f.seek(start) remaining = end - start + 1 while remaining > 0: n = min(chunk_size, remaining) data = f.read(n) if not data: break remaining -= len(data) yield data def _get_token(req: Request, authorization: Optional[str]) -> str: # Prefer cookie (for