fix: address review — injective mirror roots, root relocation, full preview coverage, EXDEV-safe rollback

Codex review on #1124:

- P1: mirror layout root component is now <basename>-<roothash>
  (sha256 of the normalized root path), so two roots sharing a basename
  no longer map to the same mirror directory and overwrite each other's
  sidecars
- P1: changing sidecar_storage_path while centralized no longer strands
  assets in the old root — new relocate_root migration direction moves
  the whole mirror tree, rewrites preview_url prefixes inside sidecars,
  reconciles scanner caches, and prunes the emptied old tree; the
  settings UI detects the path change and offers the relocation
- P2: migration enumerates the same preview candidates as
  find_preview_file — case-insensitive variants (model.WEBP) and the
  legacy .example.0.jpeg suffix — instead of exact lowercase
  PREVIEW_EXTENSIONS only
- P2: _rollback_model_staging restores staged files with the
  EXDEV-tolerant mover, so a failed undoable-delete staging no longer
  strands a cross-filesystem centralized sidecar copy

Tests: same-basename root injectivity, mixed-case/example preview
migration, relocate_root happy path + guards + route 400, frontend
relocation prompt flow. Verified end-to-end in a sandboxed standalone
server: uppercase/legacy previews migrate, root relocation moves the
tree and the list API serves the new locations immediately without a
rescan.
This commit is contained in:
Will Miao
2026-09-26 12:24:17 +08:00
parent 16430aef21
commit a6fca8612f
22 changed files with 592 additions and 71 deletions
+9 -2
View File
@@ -4141,7 +4141,7 @@ class NodeRegistryHandler:
class SidecarMigrationHandler:
"""Migrate sidecar metadata and previews between storage layouts."""
_VALID_DIRECTIONS = ("to_centralized", "to_alongside")
_VALID_DIRECTIONS = ("to_centralized", "to_alongside", "relocate_root")
def __init__(
self,
@@ -4168,12 +4168,18 @@ class SidecarMigrationHandler:
return web.json_response(
{
"success": False,
"error": "direction must be 'to_centralized' or 'to_alongside'",
"error": "direction must be 'to_centralized', 'to_alongside' or 'relocate_root'",
},
status=400,
)
force = params.get("force") in (True, 1, "true", "1")
old_root = str(params.get("old_root") or "").strip()
if direction == "relocate_root" and not old_root:
return web.json_response(
{"success": False, "error": "old_root is required for relocate_root"},
status=400,
)
use_case = self._use_case_factory()
progress_cb = self._progress_callback_factory()
@@ -4181,6 +4187,7 @@ class SidecarMigrationHandler:
direction=direction,
progress_cb=progress_cb,
force=force,
old_root=old_root,
)
status = 200 if result.get("success") else 400
return web.json_response(result, status=status)