fix(vision): reference image sopravvive al copy-on-write (#38.6)
_copy_tasks_to_version non copiava le righe VisionReferenceImage del task sorgente: il task copiato perdeva le immagini di riferimento del grafo di visione. Ora copia le righe sul task nuovo, stesso path e stessi metadati (stesso file su disco, non duplicato). Di conseguenza il file va cancellato solo se nessun'altra riga VisionReferenceImage referenzia lo stesso path: nuova vision_service.file_still_referenced(), usata dalla route DELETE prima di remove_reference_file. Test: copy-on-write elenca l'immagine sulla nuova versione; DELETE di una delle due righe lascia il file; DELETE dell'ultima lo rimuove. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ from src.backend.models.orm.recipe import Recipe, RecipeVersion
|
||||
from src.backend.models.orm.setting import RecipeVersionAudit
|
||||
from src.backend.models.orm.task import RecipeSubtask, RecipeTask
|
||||
from src.backend.models.orm.user import User
|
||||
from src.backend.models.orm.vision import VisionReferenceImage
|
||||
from src.backend.models.api.recipe import RecipeCreate, RecipeUpdate
|
||||
|
||||
|
||||
@@ -118,6 +119,28 @@ async def _copy_tasks_to_version(
|
||||
db.add(new_task)
|
||||
await db.flush() # get new_task.id
|
||||
|
||||
# Reference images (#38.6): the copied task points at the same files
|
||||
# on disk as the source - same `path`, same acquisition metadata. A
|
||||
# graph composed against a reference must keep working after the
|
||||
# recipe is next edited, exactly like task_type/vision_json above.
|
||||
# The file itself is only ever removed once no row references its
|
||||
# path any more (see vision_service.file_still_referenced).
|
||||
ref_images = (await db.execute(
|
||||
select(VisionReferenceImage).where(VisionReferenceImage.task_id == task.id)
|
||||
)).scalars().all()
|
||||
for ref in ref_images:
|
||||
db.add(VisionReferenceImage(
|
||||
task_id=new_task.id,
|
||||
path=ref.path,
|
||||
station_id=ref.station_id,
|
||||
device_code=ref.device_code,
|
||||
calibration_snapshot=ref.calibration_snapshot,
|
||||
engine_version=ref.engine_version,
|
||||
note=ref.note,
|
||||
expected_json=ref.expected_json,
|
||||
acquired_at=ref.acquired_at,
|
||||
))
|
||||
|
||||
for sub in task.subtasks:
|
||||
new_sub = RecipeSubtask(
|
||||
task_id=new_task.id,
|
||||
|
||||
@@ -275,6 +275,22 @@ def remove_reference_file(reference: VisionReferenceImage) -> None:
|
||||
_reference_file_path(reference).unlink(missing_ok=True)
|
||||
|
||||
|
||||
async def file_still_referenced(db: AsyncSession, path: str) -> bool:
|
||||
"""True if some VisionReferenceImage row still points at *path*.
|
||||
|
||||
Copy-on-write (#38.6) can leave several rows sharing the same file on
|
||||
disk (the copy carries over the source's `path` rather than duplicating
|
||||
the file). The router must call this, after the row being deleted has
|
||||
been committed, before it calls `remove_reference_file` - otherwise
|
||||
deleting one task's reference would take the file out from under every
|
||||
other task that still references it.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(VisionReferenceImage.id).where(VisionReferenceImage.path == path).limit(1)
|
||||
)
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
|
||||
async def save_reference_image(
|
||||
db: AsyncSession,
|
||||
task_id: int,
|
||||
|
||||
Reference in New Issue
Block a user