Bitcheck-image / app /services /file_validator.py
BitCheck Codex
feat: add file validation and filename analysis
94db30d
Raw
History Blame Contribute Delete
2.75 kB
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,
}