"""Permission-checked document downloads. Neither portal can serve a stored document today, and the reason is structural rather than a missing link. The portals sit behind nginx servers that proxy ``location /`` to the Django app **with a path prefix** (``/portal/`` and ``/ops/``), so a stored ``/media/...`` URL arrives at Django as ``/portal/media/...`` — a route that does not exist. Django's own DEBUG media route is registered at ``/media/``, not under either prefix, so even a file that is present on disk comes back as a page-not-found. Serving media directly from the web server would not be the answer either: every document row carries a visibility setting (buyer / ops / admin) and every certificate carries a status, so the file behind a row is not public. The download has to pass through the same permission gate as the page that links to it. So: these helpers resolve a stored ``file_url`` to a real path under ``MEDIA_ROOT`` — refusing anything that escapes it — and stream the bytes back to a caller that has already passed a portal permission gate. """ from pathlib import Path from django.conf import settings from django.http import FileResponse, Http404 def resolve_media_path(file_url): """Absolute path for a stored ``/media/...`` file_url, or None if unacceptable. None means "do not serve this": an empty value, a path outside MEDIA_URL, or a path that resolves outside MEDIA_ROOT (``../`` and symlink escapes). """ if not file_url: return None media_url = settings.MEDIA_URL.rstrip("/") # "/media" rel = file_url.strip() if rel.startswith(media_url + "/"): rel = rel[len(media_url) + 1:] elif rel.startswith("/"): # An absolute filesystem path, rather than a path under MEDIA_URL. return None base = Path(settings.MEDIA_ROOT).resolve() target = (base / rel).resolve() if target != base and base not in target.parents: return None return target def file_response(file_url, download_name=None): """Stream a stored file back as an inline response. Raises 404 when the value cannot be resolved or the file is not on disk, so a broken row reads as "not found" rather than as a server error. """ path = resolve_media_path(file_url) if path is None or not path.is_file(): raise Http404("Document file not found") return FileResponse(open(path, "rb"), filename=download_name or path.name)