File size: 3,500 Bytes
e7f1d57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# Dependencies
import magic
from PIL import Image
from pathlib import Path
from typing import Tuple
from utils.logger import get_logger
from config.settings import settings
from config.constants import MIN_IMAGE_DIMENSION
from config.constants import MAX_IMAGE_DIMENSION


# Setup Logging
logger = get_logger(__name__)


class ValidationError(Exception):
    """
    Custom validation error
    """
    pass


class ImageValidator:
    """
    Validate uploaded images
    """
    @staticmethod
    def validate_file_size(file_size: int) -> None:
        """
        Validate file size
        """
        if (file_size > settings.max_file_size_bytes):
            raise ValidationError(f"File size {file_size} bytes exceeds maximum {settings.max_file_size_bytes} bytes")

        if (file_size == 0):
            raise ValidationError("File is empty")
    

    @staticmethod
    def validate_file_extension(filename: str) -> None:
        """
        Validate file extension
        """
        extension = Path(filename).suffix.lower()
        
        if (extension not in settings.ALLOWED_EXTENSIONS):
            raise ValidationError(f"File extension {extension} not allowed. Allowed: {', '.join(settings.ALLOWED_EXTENSIONS)}")
    

    @staticmethod
    def validate_image_content(file_path: Path) -> Tuple[int, int]:
        """
        Validate image can be opened and get dimensions
        """
        try:
            with Image.open(file_path) as image:
                width, height = image.size
                
                # Validate dimensions
                if ((width < MIN_IMAGE_DIMENSION) or (height < MIN_IMAGE_DIMENSION)):
                    raise ValidationError(f"Image dimensions ({width}x{height}) too small. Minimum: {MIN_IMAGE_DIMENSION}px")
                
                if ((width > MAX_IMAGE_DIMENSION) or (height > MAX_IMAGE_DIMENSION)):
                    raise ValidationError(f"Image dimensions ({width}x{height}) too large. Maximum: {MAX_IMAGE_DIMENSION}px")
                
                # Verify format
                if (image.format.lower() not in ['jpeg', 'png', 'webp']):
                    raise ValidationError(f"Unsupported image format: {image.format}")
                
                return width, height
                
        except ValidationError:
            raise

        except Exception as e:
            raise ValidationError(f"Cannot open image: {str(e)}")
    

    @staticmethod
    def validate_mime_type(file_path: Path) -> None:
        """
        Validate MIME type matches image
        """
        try:
            mime = magic.from_file(str(file_path), mime = True)

            if (not mime.startswith('image/')):
                raise ValidationError(f"File is not an image. MIME type: {mime}")
        
        except Exception as e:
            logger.warning(f"MIME type validation failed: {e}")
            # Don't fail if python-magic is not available
    

    @classmethod
    def validate_image(cls, file_path: Path, filename: str, file_size: int) -> Tuple[int, int]:
        """
        Comprehensive image validation
        """
        cls.validate_file_size(file_size)
        cls.validate_file_extension(filename)

        dimensions = cls.validate_image_content(file_path)
        cls.validate_mime_type(file_path)  # Optional, commented out if python-magic not available
        
        logger.debug(f"Validated image: {filename} ({dimensions[0]}x{dimensions[1]})")

        return dimensions