feat(vision): immagini di riferimento non orfane, GET/DELETE (#38.5)

save_reference_image scrive il file dopo il flush riuscito; il router
lo cancella se il commit successivo fallisce. Aggiunti GET
/api/vision/reference-images?task_id= (lista, Maker) e DELETE
/api/vision/reference-images/{id} (cancella riga e file, Maker).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 22:43:35 +02:00
parent 5ca3dc14cd
commit 65f6a4b18b
4 changed files with 217 additions and 8 deletions
+34 -3
View File
@@ -12,7 +12,9 @@ from src.backend.api.middleware.api_key import require_maker, require_measuremen
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.api.vision import (
VisionExecuteResponse, VisionMeasurementResult, VisionReferenceImageResponse,
)
from src.backend.models.orm.task import RecipeTask
from src.backend.models.orm.user import User
from src.backend.services import vision_service
@@ -128,7 +130,7 @@ async def execute(
# Composing a recipe and keeping a reference image are Maker operations, not
# measuring: both endpoints below gate on `require_maker`, not the
# measuring: every endpoint below gates on `require_maker`, not the
# `require_measurement_tec` that `execute` above uses.
@@ -146,10 +148,39 @@ async def add_reference_image(
db, task.id, content, note=note,
extension=_IMAGE_EXTENSIONS[image.content_type],
)
await db.commit()
try:
await db.commit()
except Exception:
# The row never became durable - the file `save_reference_image`
# wrote after its flush would otherwise outlive it as an orphan.
vision_service.remove_reference_file(reference)
raise
return {"id": reference.id, "path": reference.path, "note": reference.note}
@router.get("/reference-images", response_model=list[VisionReferenceImageResponse])
async def get_reference_images(
task_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_maker),
) -> list[VisionReferenceImageResponse]:
await _task_or_404(db, task_id)
return await vision_service.list_reference_images(db, task_id)
@router.delete("/reference-images/{reference_id}", status_code=204)
async def delete_reference_image(
reference_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_maker),
) -> None:
reference = await vision_service.delete_reference_image(db, reference_id)
await db.commit()
# Only now: if the commit above had failed, the row would still exist and
# the file must still be there for it.
vision_service.remove_reference_file(reference)
@router.post("/preview")
async def preview(
task_id: int = Form(...),