util.py 2.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  1. from __future__ import annotations
  2. import typing as t
  3. import urllib.parse
  4. def sanitize_filename(filename: str) -> str | None:
  5. filename = filename.strip().strip("\"'")
  6. if not filename:
  7. return None
  8. filename = filename.replace("\\", "/").split("/")[-1]
  9. if not filename or filename in {".", ".."}:
  10. return None
  11. return filename
  12. def extract_filename_from_content_disposition(header: str) -> str | None:
  13. """Extract filename from Content-Disposition header.
  14. Supports both ``filename=`` and RFC 5987 ``filename*=``; prefers ``filename*=``
  15. when present. Returned filenames are sanitized to prevent path traversal.
  16. """
  17. if not header:
  18. return None
  19. filename_star: str | None = None
  20. filename_plain: str | None = None
  21. for part in header.split(";"):
  22. part = part.strip()
  23. lower = part.lower()
  24. if lower.startswith("filename*="):
  25. value = part[len("filename*=") :].strip()
  26. value = value.strip().strip("\"'")
  27. try:
  28. charset, rest = value.split("'", 1)
  29. _language, encoded = rest.split("'", 1)
  30. except ValueError:
  31. charset = "utf-8"
  32. encoded = value
  33. try:
  34. raw = urllib.parse.unquote_to_bytes(encoded)
  35. decoded = raw.decode(charset, errors="replace")
  36. except LookupError:
  37. raw = urllib.parse.unquote_to_bytes(encoded)
  38. decoded = raw.decode("utf-8", errors="replace")
  39. filename_star = sanitize_filename(decoded)
  40. elif lower.startswith("filename="):
  41. value = part[len("filename=") :].strip()
  42. filename_plain = sanitize_filename(value)
  43. return filename_star or filename_plain
  44. def infer_filename_from_fileobj(file_obj: t.Any, fallback: str) -> str:
  45. """Infer a safe filename from a file-like object's ``.name`` attribute."""
  46. name = getattr(file_obj, "name", None)
  47. if isinstance(name, str) and name:
  48. inferred = sanitize_filename(name)
  49. if inferred:
  50. return inferred
  51. return fallback