"""HTTP endpoint for chat attachment downloads / previews. The chat turn runtime persists every uploaded attachment to the :class:`~deeptutor.services.storage.AttachmentStore` and records the public URL on the message. The frontend preview drawer loads files via this router, which only serves paths the store hands back — every component is sanitised to defend against directory traversal. URL shape:: GET /api/attachments/{session_id}/{attachment_id}/{filename} The session id functions as the ACL boundary, mirroring how the rest of the app treats sessions today (single-tenant, session ownership is local trust). Once multi-user auth lands we should swap this for signed URLs. """ from __future__ import annotations import logging import mimetypes from urllib.parse import quote from fastapi import APIRouter, HTTPException from fastapi.responses import FileResponse from deeptutor.services.storage import ( LocalDiskAttachmentStore, get_attachment_store, ) logger = logging.getLogger(__name__) router = APIRouter() def _content_disposition(filename: str, *, disposition: str = "inline") -> str: """Build a Content-Disposition header that survives non-ASCII filenames. HTTP/1.1 headers are latin-1, so dropping a Chinese / accented filename straight into ``filename="..."`` blows up with UnicodeEncodeError. RFC 6266 / RFC 5987 cover this: emit ``filename*=UTF-8''`` plus an ASCII fallback on ``filename=`` for legacy clients. """ ascii_fallback = filename.encode("ascii", errors="replace").decode("ascii") # Quotes / backslashes break the simple-quoted-string form; collapse them. ascii_fallback = ascii_fallback.replace('"', "_").replace("\\", "_") encoded = quote(filename, safe="") return f"{disposition}; filename=\"{ascii_fallback}\"; filename*=UTF-8''{encoded}" @router.get("/{session_id}/{attachment_id}/{filename:path}") async def get_attachment( session_id: str, attachment_id: str, filename: str, ): """Serve a previously uploaded chat attachment. Responds with ``Content-Disposition: inline`` so browsers preview PDFs and images directly in an ``