Skip to content

Commit b76b20e

Browse files
committed
Started on image upload support
1 parent 3064612 commit b76b20e

4 files changed

Lines changed: 784 additions & 1 deletion

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import requests
2+
import uuid
3+
import os
4+
from io import BytesIO
5+
from typing import Dict, List, Optional
6+
from PIL import Image
7+
from dotenv import load_dotenv
8+
9+
load_dotenv()
10+
11+
MIME_TO_FORMAT: Dict[str, List[str]] = {
12+
'image/jpeg': ['JPEG', 'JPG'],
13+
'image/png': ['PNG'],
14+
'image/gif': ['GIF'],
15+
'image/bmp': ['BMP'],
16+
'image/webp': ['WEBP'],
17+
'image/tiff': ['TIFF', 'TIF'],
18+
'image/x-icon': ['ICO'],
19+
}
20+
21+
FORMAT_TO_EXTENSION: Dict[str, List[str]] = {
22+
'JPEG': ['.jpg', '.jpeg', '.jpe'],
23+
'PNG': ['.png'],
24+
'GIF': ['.gif'],
25+
'BMP': ['.bmp'],
26+
'WEBP': ['.webp'],
27+
'TIFF': ['.tiff', '.tif'],
28+
'ICO': ['.ico'],
29+
}
30+
31+
32+
class ImageUploadError(Exception):
33+
"""Custom exception for image upload failures"""
34+
pass
35+
36+
37+
class InvalidMimeTypeError(ImageUploadError):
38+
"""Exception for invalid MIME type"""
39+
pass
40+
41+
42+
class MissingEnvironmentVariableError(ImageUploadError):
43+
"""Exception for missing environment variables"""
44+
pass
45+
46+
47+
def generate_file_name(img: Image.Image) -> str:
48+
"""Generate filename for the image
49+
50+
Args:
51+
img: PIL Image object
52+
53+
Returns:
54+
Generated filename string
55+
"""
56+
unique_id: str = str(uuid.uuid4())
57+
format_ext: str = img.format.lower() if img.format else 'png'
58+
return f"{unique_id}.{format_ext}"
59+
60+
61+
def validate_mime_type(mime_type: str, img: Image.Image, filename: str) -> bool:
62+
"""Validate MIME type against image format and filename
63+
64+
Args:
65+
mime_type: MIME type string to validate
66+
img: PIL Image object
67+
filename: Name of the file
68+
69+
Returns:
70+
True if validation passes
71+
72+
Raises:
73+
InvalidMimeTypeError: If MIME type is invalid or doesn't match image
74+
"""
75+
if mime_type not in MIME_TO_FORMAT:
76+
raise InvalidMimeTypeError(
77+
f"Invalid MIME type '{mime_type}'. "
78+
f"Supported types: {', '.join(MIME_TO_FORMAT.keys())}"
79+
)
80+
81+
img_format: Optional[str] = img.format.upper() if img.format else None
82+
83+
if img_format:
84+
allowed_formats: List[str] = MIME_TO_FORMAT[mime_type]
85+
if img_format not in allowed_formats:
86+
raise InvalidMimeTypeError(
87+
f"MIME type '{mime_type}' does not match image format '{img_format}'. "
88+
f"Expected formats for {mime_type}: {', '.join(allowed_formats)}"
89+
)
90+
91+
file_ext: str = filename[filename.rfind('.'):].lower()
92+
93+
if img_format and img_format in FORMAT_TO_EXTENSION:
94+
valid_extensions: List[str] = FORMAT_TO_EXTENSION[img_format]
95+
if file_ext not in valid_extensions:
96+
raise InvalidMimeTypeError(
97+
f"File extension '{file_ext}' does not match format '{img_format}'. "
98+
f"Expected extensions: {', '.join(valid_extensions)}"
99+
)
100+
101+
return True
102+
103+
104+
def get_s3_bucket_uri() -> str:
105+
"""Get S3 bucket URI from environment variable"""
106+
s3_uri: Optional[str] = os.getenv('S3_BUCKET_URI')
107+
108+
if not s3_uri:
109+
raise MissingEnvironmentVariableError(
110+
"S3_BUCKET_URI environment variable is not set"
111+
)
112+
113+
return s3_uri
114+
115+
116+
def upload_image(img: Image.Image, mime_type: str) -> Dict:
117+
"""Upload PIL image with comprehensive MIME type validation
118+
119+
Args:
120+
img: PIL Image object to upload
121+
mime_type: MIME type for the upload
122+
123+
Returns:
124+
JSON response from the server as a dictionary
125+
126+
Raises:
127+
InvalidMimeTypeError: If MIME type validation fails
128+
MissingEnvironmentVariableError: If S3_BUCKET_URI is not set
129+
ImageUploadError: If upload fails for any reason
130+
"""
131+
try:
132+
# Get URL from environment variable
133+
url: str = get_s3_bucket_uri()
134+
135+
filename: str = generate_file_name(img)
136+
137+
validate_mime_type(mime_type, img, filename)
138+
139+
buffer: BytesIO = BytesIO()
140+
img_format: str = img.format if img.format else 'PNG'
141+
img.save(buffer, format=img_format)
142+
buffer.seek(0)
143+
144+
files: Dict[str, tuple] = {'file': (filename, buffer, mime_type)}
145+
response: requests.Response = requests.post(url, files=files, timeout=30)
146+
147+
if response.status_code != 200:
148+
raise ImageUploadError(
149+
f"Upload failed with status code {response.status_code}: {response.text}"
150+
)
151+
152+
return response.json()['url']
153+
154+
except (InvalidMimeTypeError, MissingEnvironmentVariableError):
155+
raise
156+
except requests.exceptions.RequestException as e:
157+
raise ImageUploadError(f"Network error: {str(e)}")
158+
except Exception as e:
159+
raise ImageUploadError(f"Unexpected error: {str(e)}")
160+

0 commit comments

Comments
 (0)