upload_validate.py 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. """上传文件校验:扩展名白名单与内容 magic。"""
  2. from __future__ import annotations
  3. import os
  4. from typing import Callable, Optional
  5. from app.core.response import AppHTTPError
  6. # 对齐原型:图片、视频、常见办公文档与压缩包
  7. ALLOWED_EXTENSIONS = {
  8. ".jpg",
  9. ".jpeg",
  10. ".png",
  11. ".gif",
  12. ".webp",
  13. ".bmp",
  14. ".mp4",
  15. ".mov",
  16. ".avi",
  17. ".webm",
  18. ".pdf",
  19. ".doc",
  20. ".docx",
  21. ".xls",
  22. ".xlsx",
  23. ".zip",
  24. }
  25. IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}
  26. def _starts_with(prefix: bytes) -> Callable[[bytes], bool]:
  27. def check(content: bytes) -> bool:
  28. return content.startswith(prefix)
  29. return check
  30. def _starts_with_any(*prefixes: bytes) -> Callable[[bytes], bool]:
  31. def check(content: bytes) -> bool:
  32. return any(content.startswith(p) for p in prefixes)
  33. return check
  34. def _webp(content: bytes) -> bool:
  35. return len(content) >= 12 and content[:4] == b"RIFF" and content[8:12] == b"WEBP"
  36. _MAGIC_CHECKS: dict[str, Callable[[bytes], bool]] = {
  37. ".png": _starts_with(b"\x89PNG\r\n\x1a\n"),
  38. ".jpg": _starts_with(b"\xff\xd8\xff"),
  39. ".jpeg": _starts_with(b"\xff\xd8\xff"),
  40. ".gif": _starts_with_any(b"GIF87a", b"GIF89a"),
  41. ".webp": _webp,
  42. ".bmp": _starts_with(b"BM"),
  43. ".pdf": _starts_with(b"%PDF"),
  44. }
  45. def file_extension(filename: str) -> str:
  46. return os.path.splitext(filename or "")[1].lower()
  47. def validate_upload(filename: Optional[str], content: bytes) -> None:
  48. if not filename:
  49. raise AppHTTPError(400, "请选择文件")
  50. ext = file_extension(filename)
  51. if ext not in ALLOWED_EXTENSIONS:
  52. raise AppHTTPError(400, "不支持的文件类型")
  53. magic_check = _MAGIC_CHECKS.get(ext)
  54. if magic_check is not None and not magic_check(content):
  55. raise AppHTTPError(400, "文件内容与扩展名不符")
  56. def validate_image_upload(filename: Optional[str], content: bytes) -> None:
  57. if not filename:
  58. raise AppHTTPError(400, "请选择文件")
  59. ext = file_extension(filename)
  60. if ext not in IMAGE_EXTENSIONS:
  61. raise AppHTTPError(400, "不支持的文件类型")
  62. validate_upload(filename, content)