from __future__ import annotations import hashlib import mimetypes from pathlib import Path from typing import Any from uuid import uuid4 from fastapi import HTTPException, UploadFile from PIL import Image, ImageOps from app.config import settings EXTENSION_TO_FORMAT = { "jpg": "JPEG", "jpeg": "JPEG", "png": "PNG", "webp": "WEBP", } def new_verification_id() -> str: return str(uuid4()) def validate_upload_metadata(file: UploadFile) -> str: if not file.filename: raise HTTPException(status_code=400, detail="Uploaded file must have a filename.") suffix = Path(file.filename).suffix.lower().lstrip(".") if suffix not in settings.allowed_extensions: raise HTTPException(status_code=415, detail=f"Unsupported image type: {suffix or 'unknown'}") return suffix def save_upload(file: UploadFile, verification_id: str, suffix: str) -> Path: target = settings.uploads_dir / f"{verification_id}.{suffix}" size = 0 with target.open("wb") as out: while chunk := file.file.read(1024 * 1024): size += len(chunk) if size > settings.max_upload_bytes: target.unlink(missing_ok=True) raise HTTPException(status_code=413, detail="Uploaded image exceeds size limit.") out.write(chunk) file.file.seek(0) return target def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def validate_image_file(path: Path, original_filename: str, suffix: str) -> dict[str, Any]: try: with Image.open(path) as img: img.verify() with Image.open(path) as img: transposed = ImageOps.exif_transpose(img) width, height = transposed.size mode = transposed.mode fmt = img.format except Exception as exc: path.unlink(missing_ok=True) raise HTTPException(status_code=400, detail=f"Invalid or unreadable image: {exc}") from exc expected = EXTENSION_TO_FORMAT.get(suffix) if fmt not in set(EXTENSION_TO_FORMAT.values()) or (expected and fmt != expected): raise HTTPException(status_code=400, detail=f"Uploaded file content is not a valid {suffix} image.") mime_type = mimetypes.guess_type(original_filename)[0] or Image.MIME.get(fmt) or "application/octet-stream" return { "filename": original_filename, "sha256": sha256_file(path), "width": int(width), "height": int(height), "format": fmt, "mode": mode, "mime_type": mime_type, "file_size_bytes": path.stat().st_size, }