"""Shared helpers for the file routers (ROADMAP #85, tranche 6a). Petites fonctions pures extraites de :mod:`backend.main` sans changement de comportement. Regroupées ici car utilisées par plusieurs routers (``files_read`` aujourd'hui, ``files_media`` / mutations ensuite) : - :func:`content_disposition` — aussi utilisée par ``_stream_file_with_range`` (resté dans ``main`` jusqu'à la tranche media). - :func:`media_max_inline_bytes` — aussi utilisée par ``/api/media``. """ from __future__ import annotations import os from pathlib import Path def content_disposition(disposition: str, filename: str) -> str: """Build a header-safe Content-Disposition value. HTTP header values must be ASCII. Unicode filenames are sent per RFC 5987 via ``filename*`` (percent-encoded UTF-8) with a pure-ASCII ``filename`` fallback. This avoids a UnicodeDecodeError / HTTP 500 when the filename contains accented characters (e.g. 'Bière blonde…pdf'). """ from urllib.parse import quote ascii_name = "".join(c for c in filename if c.isascii() and (c.isalnum() or c in " _-.")).strip() or "file" ext = Path(filename).suffix if ext and not Path(ascii_name).suffix: ascii_name = ascii_name + ext return f"{disposition}; filename=\"{ascii_name}\"; filename*=UTF-8''{quote(filename)}" def media_max_inline_bytes() -> int: """Maximum size (bytes) for inline audio/video playback (roadmap #109-A3). Configurable via ``OBSIGATE_MEDIA_MAX_INLINE_MB`` (default 500 MB). Files above the limit are not streamed in the viewer (the UI falls back to the download button), which keeps a single uvicorn worker from being pinned by multi-gigabyte media. Invalid or non-positive values fall back to default. """ default_mb = 500 raw = os.environ.get("OBSIGATE_MEDIA_MAX_INLINE_MB", "").strip() if not raw: return default_mb * 1024 * 1024 try: mb = float(raw) except ValueError: return default_mb * 1024 * 1024 if mb <= 0: return default_mb * 1024 * 1024 return int(mb * 1024 * 1024)