_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 )