| from __future__ import annotations |
|
|
| import os |
| import tempfile |
| from pathlib import Path |
| from typing import Iterable |
|
|
| from fastapi import HTTPException, UploadFile |
|
|
|
|
| IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp"} |
| VIDEO_EXTENSIONS = {".mp4", ".mov", ".webm"} |
| MAX_IMAGE_BYTES = 15 * 1024 * 1024 |
| MAX_VIDEO_BYTES = 80 * 1024 * 1024 |
|
|
|
|
| def safe_filename(filename: str | None) -> str: |
| if not filename: |
| return "upload" |
| return os.path.basename(filename) |
|
|
|
|
| def validate_extension(filename: str | None, allowed_extensions: Iterable[str]) -> str: |
| name = safe_filename(filename) |
| suffix = Path(name).suffix.lower() |
| if suffix not in set(allowed_extensions): |
| allowed = ", ".join(sorted(allowed_extensions)) |
| raise HTTPException(status_code=400, detail=f"Unsupported file type. Allowed: {allowed}") |
| return suffix |
|
|
|
|
| async def read_upload_bytes(upload: UploadFile, max_bytes: int) -> bytes: |
| data = bytearray() |
| while True: |
| chunk = await upload.read(1024 * 1024) |
| if not chunk: |
| break |
| data.extend(chunk) |
| if len(data) > max_bytes: |
| raise HTTPException(status_code=413, detail="File is too large for this MVP analyzer.") |
| if not data: |
| raise HTTPException(status_code=400, detail="The uploaded file was empty.") |
| return bytes(data) |
|
|
|
|
| async def save_upload_to_temp_file(upload: UploadFile, suffix: str, max_bytes: int) -> str: |
| temp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix) |
| path = temp.name |
| size = 0 |
| try: |
| while True: |
| chunk = await upload.read(1024 * 1024) |
| if not chunk: |
| break |
| size += len(chunk) |
| if size > max_bytes: |
| raise HTTPException(status_code=413, detail="File is too large for this MVP analyzer.") |
| temp.write(chunk) |
| if size == 0: |
| raise HTTPException(status_code=400, detail="The uploaded file was empty.") |
| return path |
| except Exception: |
| try: |
| os.unlink(path) |
| except OSError: |
| pass |
| raise |
| finally: |
| temp.close() |
|
|
|
|
| def remove_temp_file(path: str | None) -> None: |
| if not path: |
| return |
| try: |
| os.unlink(path) |
| except OSError: |
| pass |
|
|