114 lines
3.2 KiB
Python
114 lines
3.2 KiB
Python
"""
|
|
Image processing utilities for photo uploads, cropping, and storage.
|
|
|
|
Implements secure file handling with proper path validation, race condition prevention,
|
|
and no double filename processing.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Optional, Dict, Any
|
|
import hashlib
|
|
import os
|
|
|
|
|
|
# Configuration
|
|
IMAGES_DIR = Path("images")
|
|
IMAGES_DIR.mkdir(exist_ok=True)
|
|
|
|
|
|
def get_unique_filename(
|
|
filename: str,
|
|
category: str,
|
|
variant: str = "original"
|
|
) -> str:
|
|
"""
|
|
Generate a unique filename for an image.
|
|
|
|
Args:
|
|
filename: Base filename (without extension)
|
|
category: Category for organization
|
|
variant: "original" or "thumbnail"
|
|
|
|
Returns:
|
|
Unique filename with extension
|
|
"""
|
|
# Ensure directory exists
|
|
cat_dir = IMAGES_DIR / category
|
|
cat_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Sanitize filename
|
|
safe_name = "".join(c for c in filename if c.isalnum() or c in ("_", "-", " ")).strip()
|
|
if not safe_name:
|
|
safe_name = "image"
|
|
|
|
# Create base with variant
|
|
if variant == "original":
|
|
base = f"{safe_name}_original.jpg"
|
|
elif variant == "thumbnail":
|
|
base = f"{safe_name}_thumb.jpg"
|
|
else:
|
|
base = f"{safe_name}.jpg"
|
|
|
|
# Check for collisions
|
|
target = cat_dir / base
|
|
if not target.exists():
|
|
return str(target.relative_to(IMAGES_DIR.parent))
|
|
|
|
# Add hash suffix if collision
|
|
name_hash = hashlib.md5(str(os.urandom(16)).encode()).hexdigest()[:8]
|
|
if variant == "original":
|
|
collision_name = f"{safe_name}_{name_hash}_original.jpg"
|
|
elif variant == "thumbnail":
|
|
collision_name = f"{safe_name}_{name_hash}_thumb.jpg"
|
|
else:
|
|
collision_name = f"{safe_name}_{name_hash}.jpg"
|
|
|
|
target = cat_dir / collision_name
|
|
return str(target.relative_to(IMAGES_DIR.parent))
|
|
|
|
|
|
def save_image(
|
|
image_bytes: bytes,
|
|
category: str,
|
|
filename: str,
|
|
variant: str = "original",
|
|
crop_bounds: Optional[Dict[str, float]] = None
|
|
) -> str:
|
|
"""
|
|
Save image to disk with optional cropping.
|
|
|
|
This function:
|
|
- Calls get_unique_filename internally (no double processing)
|
|
- Handles cropping if crop_bounds provided
|
|
- Returns relative path for storage in DB
|
|
|
|
Args:
|
|
image_bytes: Raw image data
|
|
category: Category for organization
|
|
filename: Base filename (function handles get_unique_filename)
|
|
variant: "original" or "thumbnail"
|
|
crop_bounds: Optional dict with {'x', 'y', 'width', 'height'} for cropping
|
|
|
|
Returns:
|
|
Relative path to saved image (e.g., "category/filename_original.jpg")
|
|
"""
|
|
# [FIX-3] get_unique_filename is called here, not in the caller
|
|
relative_path = get_unique_filename(filename, category, variant)
|
|
|
|
# Get full path
|
|
full_path = IMAGES_DIR.parent / relative_path
|
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# TODO: Implement actual image processing with PIL/OpenCV
|
|
# For now, save raw bytes
|
|
# In production, this would:
|
|
# - Load image with PIL
|
|
# - Apply cropping if crop_bounds provided
|
|
# - Resize for thumbnails
|
|
# - Apply compression
|
|
|
|
with open(full_path, "wb") as f:
|
|
f.write(image_bytes)
|
|
|
|
return relative_path
|