upload_validate.py 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. """上传文件校验:扩展名白名单与大小限制。"""
  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. MAX_UPLOAD_BYTES = 20 * 1024 * 1024 # 20MB
  27. def _starts_with(prefix: bytes) -> Callable[[bytes], bool]:
  28. def check(content: bytes) -> bool:
  29. return content.startswith(prefix)
  30. return check
  31. def _starts_with_any(*prefixes: bytes) -> Callable[[bytes], bool]:
  32. def check(content: bytes) -> bool:
  33. return any(content.startswith(p) for p in prefixes)
  34. return check
  35. def _webp(content: bytes) -> bool:
  36. return len(content) >= 12 and content[:4] == b"RIFF" and content[8:12] == b"WEBP"
  37. _MAGIC_CHECKS: dict[str, Callable[[bytes], bool]] = {
  38. ".png": _starts_with(b"\x89PNG\r\n\x1a\n"),
  39. ".jpg": _starts_with(b"\xff\xd8\xff"),
  40. ".jpeg": _starts_with(b"\xff\xd8\xff"),
  41. ".gif": _starts_with_any(b"GIF87a", b"GIF89a"),
  42. ".webp": _webp,
  43. ".bmp": _starts_with(b"BM"),
  44. ".pdf": _starts_with(b"%PDF"),
  45. }
  46. def file_extension(filename: str) -> str:
  47. return os.path.splitext(filename or "")[1].lower()
  48. def validate_upload(filename: Optional[str], content: bytes) -> None:
  49. if not filename:
  50. raise AppHTTPError(400, "请选择文件")
  51. ext = file_extension(filename)
  52. if ext not in ALLOWED_EXTENSIONS:
  53. raise AppHTTPError(400, "不支持的文件类型")
  54. if len(content) > MAX_UPLOAD_BYTES:
  55. raise AppHTTPError(400, "文件过大,单文件不超过 20MB")
  56. magic_check = _MAGIC_CHECKS.get(ext)
  57. if magic_check is not None and not magic_check(content):
  58. raise AppHTTPError(400, "文件内容与扩展名不符")
  59. def validate_image_upload(filename: Optional[str], content: bytes) -> None:
  60. if not filename:
  61. raise AppHTTPError(400, "请选择文件")
  62. ext = file_extension(filename)
  63. if ext not in IMAGE_EXTENSIONS:
  64. raise AppHTTPError(400, "不支持的文件类型")
  65. validate_upload(filename, content)