US04-03: Apply and Verify Guarded Renames

This commit is contained in:
2026-08-16 11:26:06 +02:00
parent daaa4ca67b
commit 4735013278
7 changed files with 1222 additions and 0 deletions

View File

@@ -9,6 +9,12 @@ from __future__ import annotations
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
from photo_pipeline.schemas import ApplyPlanRequest
from photo_pipeline.services.rename_apply import (
ApplyConflict,
ApplyError,
RenameApplyService,
)
from photo_pipeline.services.renames import RenameError, RenameService
router = APIRouter(tags=["renames"])
@@ -21,6 +27,13 @@ def _service(request: Request) -> RenameService:
)
def _apply_service(request: Request) -> RenameApplyService:
return RenameApplyService(
request.app.state.session_factory,
library_roots=tuple(request.app.state.config.library_roots),
)
def _error(status: int, code: str, message: str) -> JSONResponse:
return JSONResponse(status_code=status, content={"error": {"code": code, "message": message}})
@@ -52,3 +65,42 @@ def export_plan(plan_id: str, request: Request):
return _service(request).export(plan_id)
except RenameError as error:
return _error(404, "not_found", str(error))
@router.post("/rename-plans/{plan_id}/apply")
def apply_plan(plan_id: str, body: ApplyPlanRequest, request: Request):
"""Apply a confirmed plan. The confirmation must carry the current plan version
(and may carry the checksum) so stale browser state cannot run a changed plan."""
try:
return _apply_service(request).apply(
plan_id,
expected_version=body.expected_version,
expected_checksum=body.expected_checksum,
)
except ApplyConflict as error:
return _error(409, "version_conflict", str(error))
except ApplyError as error:
return _error(422, "cannot_apply", str(error))
@router.get("/rename-recovery")
def recovery_status(request: Request) -> dict:
"""Every unresolved rename with its evidence-derived classification."""
journal = _apply_service(request).journal
return {
"blocks_mutation": journal.blocks_mutation(),
"items": journal.classify_all(),
}
@router.post("/rename-recovery/resolve")
def resolve_recovery(request: Request) -> dict:
return _apply_service(request).recover()
@router.post("/rename-plans/{plan_id}/rollback")
def rollback_plan(plan_id: str, request: Request):
try:
return _apply_service(request).rollback_plan(plan_id)
except ApplyError as error:
return _error(422, "cannot_rollback", str(error))