/ mureo / _image_validation.py
_image_validation.py
  1  """Common media file upload validation
  2  
  3  File validation logic shared by Meta Ads / Google Ads (supports images and videos).
  4  """
  5  
  6  from __future__ import annotations
  7  
  8  from pathlib import Path
  9  
 10  
 11  def _validate_media_file(
 12      file_path: str,
 13      *,
 14      max_size_bytes: int,
 15      max_size_label: str,
 16      allowed_extensions: frozenset[str],
 17      media_type_label: str,
 18  ) -> Path:
 19      """Validate a media file (shared by images and videos).
 20  
 21      Args:
 22          file_path: Local file path
 23          max_size_bytes: Maximum file size in bytes
 24          max_size_label: Size label for error messages (e.g. "30MB")
 25          allowed_extensions: Set of allowed extensions (lowercase, no dot)
 26          media_type_label: Media type label for error messages (e.g. "image", "video")
 27  
 28      Returns:
 29          Validated Path object.
 30  
 31      Raises:
 32          ValueError: Path traversal, unsupported format, or size exceeded.
 33          FileNotFoundError: File does not exist.
 34      """
 35      # Prevent path traversal (.. check + resolve() normalization)
 36      if ".." in file_path:
 37          raise ValueError(f"Invalid file path: path must not contain '..' : {file_path}")
 38  
 39      path = Path(file_path)
 40  
 41      # File existence check
 42      if not path.exists():
 43          raise FileNotFoundError(f"File not found: {file_path}")
 44  
 45      # Verify it is a regular file after resolving symlinks
 46      resolved = path.resolve()
 47      if not resolved.is_file():
 48          raise ValueError(f"Invalid file path: not a regular file: {file_path}")
 49  
 50      # Extension check
 51      ext = path.suffix.lower().lstrip(".")
 52      if ext not in allowed_extensions:
 53          allowed_str = ", ".join(sorted(allowed_extensions))
 54          raise ValueError(
 55              f"Unsupported {media_type_label} format: .{ext} "
 56              f"(supported formats: {allowed_str})"
 57          )
 58  
 59      # File size check
 60      size = path.stat().st_size
 61      if size > max_size_bytes:
 62          raise ValueError(
 63              f"File size exceeds the limit: " f"{size:,} bytes (limit: {max_size_label})"
 64          )
 65  
 66      return path
 67  
 68  
 69  def validate_image_file(
 70      file_path: str,
 71      *,
 72      max_size_bytes: int,
 73      max_size_label: str,
 74      allowed_extensions: frozenset[str],
 75  ) -> Path:
 76      """Validate an image file.
 77  
 78      Args:
 79          file_path: Local image file path
 80          max_size_bytes: Maximum file size in bytes
 81          max_size_label: Size label for error messages (e.g. "30MB")
 82          allowed_extensions: Set of allowed extensions (lowercase, no dot)
 83  
 84      Returns:
 85          Validated Path object.
 86  
 87      Raises:
 88          ValueError: Path traversal, unsupported format, or size exceeded.
 89          FileNotFoundError: File does not exist.
 90      """
 91      return _validate_media_file(
 92          file_path,
 93          max_size_bytes=max_size_bytes,
 94          max_size_label=max_size_label,
 95          allowed_extensions=allowed_extensions,
 96          media_type_label="image",
 97      )
 98  
 99  
100  def validate_video_file(
101      file_path: str,
102      *,
103      max_size_bytes: int,
104      max_size_label: str,
105      allowed_extensions: frozenset[str],
106  ) -> Path:
107      """Validate a video file.
108  
109      Args:
110          file_path: Local video file path
111          max_size_bytes: Maximum file size in bytes
112          max_size_label: Size label for error messages (e.g. "100MB")
113          allowed_extensions: Set of allowed extensions (lowercase, no dot)
114  
115      Returns:
116          Validated Path object.
117  
118      Raises:
119          ValueError: Path traversal, unsupported format, or size exceeded.
120          FileNotFoundError: File does not exist.
121      """
122      return _validate_media_file(
123          file_path,
124          max_size_bytes=max_size_bytes,
125          max_size_label=max_size_label,
126          allowed_extensions=allowed_extensions,
127          media_type_label="video",
128      )