fix(vision): limite di tipo e dimensione sui tre endpoint di upload

/execute, /preview e /reference-images leggevano l'immagine senza
limite di tipo o dimensione, e /reference-images scriveva su disco con
estensione .png fissa qualunque fosse il tipo reale. Riusa la
convenzione già esistente in files.py (ALLOWED_IMAGE_TYPES,
validate_file_size) invece di inventarne una seconda: tipo controllato
dall'header prima di leggere, dimensione controllata sul corpo letto,
un PDF non è qualcosa che un grafo di visione può misurare.
save_reference_image ora scrive con l'estensione del tipo reale
dell'upload.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
This commit is contained in:
2026-08-16 19:41:14 +02:00
parent 38ef0587f5
commit 0bd95367bb
3 changed files with 220 additions and 5 deletions
+38 -3
View File
@@ -9,6 +9,8 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.backend.api.middleware.api_key import require_maker, require_measurement_tec
from src.backend.api.routers.files import ALLOWED_IMAGE_TYPES, validate_file_size
from src.backend.config import settings
from src.backend.database import get_db
from src.backend.models.api.vision import VisionExecuteResponse, VisionMeasurementResult
from src.backend.models.orm.task import RecipeTask
@@ -17,6 +19,15 @@ from src.backend.services import vision_service
router = APIRouter(prefix="/api/vision", tags=["vision"])
# A vision graph measures pixels, not a PDF: stricter than files.py's
# ALLOWED_TYPES, which also accepts documents for technical drawings.
_IMAGE_EXTENSIONS = {
"image/jpeg": "jpg",
"image/png": "png",
"image/gif": "gif",
"image/webp": "webp",
}
async def _task_or_404(db: AsyncSession, task_id: int) -> RecipeTask:
"""Shared by the endpoints of this router."""
@@ -30,6 +41,28 @@ async def _task_or_404(db: AsyncSession, task_id: int) -> RecipeTask:
return task
async def _read_image(image: UploadFile) -> bytes:
"""Type checked from the header before reading, size checked once the body
is in hand - the same two-step convention `files.py::upload_file` already
uses, reused rather than reinvented here.
"""
if image.content_type not in ALLOWED_IMAGE_TYPES:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"File type {image.content_type} not allowed. Must be an image.",
)
content = await image.read()
if not validate_file_size(len(content)):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=(
f"File size {len(content)} bytes exceeds maximum "
f"{settings.max_upload_size_mb}MB"
),
)
return content
@router.post("/execute", response_model=VisionExecuteResponse)
async def execute(
task_id: int = Form(...),
@@ -45,7 +78,7 @@ async def execute(
result, saved = await vision_service.execute_task(
db,
task,
await image.read(),
await _read_image(image),
current_user.id,
version_id=task.version_id,
lot_number=lot_number,
@@ -80,8 +113,10 @@ async def add_reference_image(
current_user: User = Depends(require_maker),
) -> dict:
task = await _task_or_404(db, task_id)
content = await _read_image(image)
reference = await vision_service.save_reference_image(
db, task.id, await image.read(), note=note,
db, task.id, content, note=note,
extension=_IMAGE_EXTENSIONS[image.content_type],
)
await db.commit()
return {"id": reference.id, "path": reference.path, "note": reference.note}
@@ -95,4 +130,4 @@ async def preview(
current_user: User = Depends(require_maker),
) -> dict:
task = await _task_or_404(db, task_id)
return await vision_service.preview(db, task, await image.read())
return await vision_service.preview(db, task, await _read_image(image))