From 5796a0e2e92591e03adacfd6c11457483d06f678 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Sat, 10 Oct 2026 14:35:08 +0800 Subject: [PATCH] fix(sidecars): report over-long migration paths and fix root relocation (#1142) - Surface Windows WinError 206 / ENAMETOOLONG during sidecar migration as an actionable per-model error instead of a bare OSError - Offer root relocation based on the resolved sidecar root, so moving off (or back to) the default storage directory relocates existing sidecars - Hint in the migration confirm dialog when the destination is the default location, so a custom folder can be set before migrating --- locales/de.json | 3 +- locales/en.json | 3 +- locales/es.json | 3 +- locales/fr.json | 3 +- locales/he.json | 3 +- locales/ja.json | 3 +- locales/ko.json | 3 +- locales/ru.json | 3 +- locales/zh-CN.json | 3 +- locales/zh-TW.json | 3 +- .../use_cases/sidecar_migration_use_case.py | 22 ++- static/js/managers/SettingsManager.js | 37 +++- .../components/modals/confirm_modals.html | 1 + .../settingsManager.sidecarStorage.test.js | 170 +++++++++++++++++- .../test_sidecar_migration_use_case.py | 109 +++++++++++ 15 files changed, 345 insertions(+), 24 deletions(-) diff --git a/locales/de.json b/locales/de.json index ee079276..6c1b6b49 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1749,7 +1749,8 @@ "titleToAlongside": "Sidecar-Dateien zurück neben die Modelldateien verschieben?", "confirmButton": "Jetzt verschieben", "titleRelocateRoot": "Sidecar-Dateien in das neue Speicherverzeichnis verschieben?", - "destination": "Ziel: {path}" + "destination": "Ziel: {path}", + "defaultLocationHint": "Es wird der Standard-Speicherort verwendet. Um einen eigenen Ordner zu nutzen, brechen Sie ab, setzen Sie oben den Speicherpfad und klicken Sie dann auf „Sidecar-Dateien jetzt verschieben“." }, "sidecarMigrationResult": { "title": "Sidecar-Migrationsübersicht", diff --git a/locales/en.json b/locales/en.json index 70de63db..3230a31a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1749,7 +1749,8 @@ "titleToAlongside": "Move sidecars back next to model files?", "confirmButton": "Migrate Now", "titleRelocateRoot": "Move sidecars to the new storage directory?", - "destination": "Destination: {path}" + "destination": "Destination: {path}", + "defaultLocationHint": "This uses the default location. To use a custom folder, cancel, set the storage path above, then click \"Migrate Sidecars Now\"." }, "sidecarMigrationResult": { "title": "Sidecar Migration Summary", diff --git a/locales/es.json b/locales/es.json index 4cfae69d..eba2c5eb 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1749,7 +1749,8 @@ "titleToAlongside": "¿Devolver los archivos sidecar junto a los archivos de modelo?", "confirmButton": "Migrar ahora", "titleRelocateRoot": "¿Mover los archivos sidecar al nuevo directorio de almacenamiento?", - "destination": "Destino: {path}" + "destination": "Destino: {path}", + "defaultLocationHint": "Se usará la ubicación predeterminada. Para usar una carpeta personalizada, cancela, define la ruta de almacenamiento arriba y luego haz clic en «Migrar archivos sidecar ahora»." }, "sidecarMigrationResult": { "title": "Resumen de la migración de archivos sidecar", diff --git a/locales/fr.json b/locales/fr.json index 275207d1..4b1101ef 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1749,7 +1749,8 @@ "titleToAlongside": "Remettre les fichiers sidecar à côté des fichiers de modèle ?", "confirmButton": "Migrer maintenant", "titleRelocateRoot": "Déplacer les fichiers sidecar vers le nouveau dossier de stockage ?", - "destination": "Destination : {path}" + "destination": "Destination : {path}", + "defaultLocationHint": "L’emplacement par défaut sera utilisé. Pour utiliser un dossier personnalisé, annulez, définissez le chemin de stockage ci-dessus, puis cliquez sur « Migrer les fichiers sidecar maintenant »." }, "sidecarMigrationResult": { "title": "Résumé de la migration des fichiers sidecar", diff --git a/locales/he.json b/locales/he.json index fc33e681..67dbe538 100644 --- a/locales/he.json +++ b/locales/he.json @@ -1749,7 +1749,8 @@ "titleToAlongside": "להחזיר את קובצי הלוואי לצד קובצי המודל?", "confirmButton": "העבר כעת", "titleRelocateRoot": "להעביר את קובצי הלוואי לתיקיית האחסון החדשה?", - "destination": "יעד: {path}" + "destination": "יעד: {path}", + "defaultLocationHint": "נעשה שימוש במיקום ברירת המחדל. כדי להשתמש בתיקייה מותאמת אישית, בטל, הגדר את נתיב האחסון למעלה ולאחר מכן לחץ על «העבר קובצי לוואי כעת»." }, "sidecarMigrationResult": { "title": "סיכום העברת קובצי לוואי", diff --git a/locales/ja.json b/locales/ja.json index 15b7d1df..2ced2b9d 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1749,7 +1749,8 @@ "titleToAlongside": "サイドカーファイルをモデルファイルの隣に戻しますか?", "confirmButton": "今すぐ移動", "titleRelocateRoot": "サイドカーファイルを新しい保存ディレクトリに移動しますか?", - "destination": "移動先:{path}" + "destination": "移動先:{path}", + "defaultLocationHint": "デフォルトの保存場所が使用されます。カスタムフォルダを使用するには、キャンセルして上の保存先パスを設定してから、「今すぐサイドカーファイルを移動」をクリックしてください。" }, "sidecarMigrationResult": { "title": "サイドカーファイル移動の概要", diff --git a/locales/ko.json b/locales/ko.json index 1778d1f2..0dcabf5e 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -1749,7 +1749,8 @@ "titleToAlongside": "사이드카 파일을 모델 파일 옆으로 되돌릴까요?", "confirmButton": "지금 이동", "titleRelocateRoot": "사이드카 파일을 새 저장 디렉터리로 이동할까요?", - "destination": "대상 위치: {path}" + "destination": "대상 위치: {path}", + "defaultLocationHint": "기본 저장 위치가 사용됩니다. 사용자 지정 폴더를 사용하려면 취소하고 위에서 저장 경로를 설정한 다음 '지금 사이드카 파일 이동'을 클릭하세요." }, "sidecarMigrationResult": { "title": "사이드카 파일 이동 요약", diff --git a/locales/ru.json b/locales/ru.json index 724c57a4..49956639 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1749,7 +1749,8 @@ "titleToAlongside": "Вернуть sidecar-файлы рядом с файлами моделей?", "confirmButton": "Перенести сейчас", "titleRelocateRoot": "Перенести sidecar-файлы в новый каталог хранилища?", - "destination": "Назначение: {path}" + "destination": "Назначение: {path}", + "defaultLocationHint": "Будет использовано расположение по умолчанию. Чтобы использовать свою папку, отмените, укажите путь хранилища выше, затем нажмите «Перенести sidecar-файлы сейчас»." }, "sidecarMigrationResult": { "title": "Сводка переноса sidecar-файлов", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 2551a147..f3ea0b0b 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -1749,7 +1749,8 @@ "titleToAlongside": "要将附属文件移回模型文件旁边吗?", "confirmButton": "立即迁移", "titleRelocateRoot": "要将附属文件移到新的存储目录吗?", - "destination": "目标位置:{path}" + "destination": "目标位置:{path}", + "defaultLocationHint": "将使用默认存储位置。如需使用自定义文件夹,请取消,在上方设置存储路径,然后点击“立即迁移附属文件”。" }, "sidecarMigrationResult": { "title": "附属文件迁移摘要", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 080b26f2..557e3d67 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -1749,7 +1749,8 @@ "titleToAlongside": "要將附屬檔案移回模型檔案旁邊嗎?", "confirmButton": "立即遷移", "titleRelocateRoot": "要將附屬檔案移到新的儲存目錄嗎?", - "destination": "目標位置:{path}" + "destination": "目標位置:{path}", + "defaultLocationHint": "將使用預設儲存位置。如需使用自訂資料夾,請取消,在上方設定儲存路徑,然後點擊「立即遷移附屬檔案」。" }, "sidecarMigrationResult": { "title": "附屬檔案遷移摘要", diff --git a/py/services/use_cases/sidecar_migration_use_case.py b/py/services/use_cases/sidecar_migration_use_case.py index 20bda333..dc896476 100644 --- a/py/services/use_cases/sidecar_migration_use_case.py +++ b/py/services/use_cases/sidecar_migration_use_case.py @@ -645,6 +645,23 @@ class SidecarMigrationUseCase: exc_info=True, ) + @staticmethod + def _raise_path_error(exc: OSError, dst: str) -> None: + """Re-raise path-length OS errors with an actionable message. + + Windows rejects paths over MAX_PATH (260 chars) with WinError 206 + (ERROR_FILENAME_EXCED_RANGE); Linux reports ENAMETOOLONG. A bare + OSError gives no hint that the sidecar root is simply too deep. + """ + + if getattr(exc, "winerror", None) == 206 or exc.errno == errno.ENAMETOOLONG: + raise OSError( + f"Destination path is too long for the OS ({len(dst)} chars; " + f"Windows limit is 260): {dst}. Choose a shallower sidecar " + f"storage directory or shorten the file name." + ) from exc + raise exc + def _transfer(self, src: str, dst: str, result: Dict[str, Any]) -> bool: """Move ``src`` to ``dst`` with keep-newer conflict resolution. @@ -667,7 +684,10 @@ class SidecarMigrationUseCase: ) os.remove(src) return False - self._move_file(src, dst) + try: + self._move_file(src, dst) + except OSError as exc: + self._raise_path_error(exc, dst) result["moved"] += 1 return True diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index a0c9baa1..98e71018 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -3419,8 +3419,13 @@ export class SettingsManager { } // Baseline used to detect a mode change in handleSidecarStorageModeChange this._loadedSidecarStorageMode = currentMode; - // Baseline used to detect a root change in handleSidecarStoragePathChange + // Baseline of the raw path input, refreshed in handleSidecarStoragePathChange this._loadedSidecarStoragePath = state.global.settings.sidecar_storage_path || ''; + // Baseline used to detect a root change in handleSidecarStoragePathChange. + // Must be the RESOLVED root, not the raw setting: with no custom path the + // setting is empty while sidecars live in the default root, and that + // default->custom transition is exactly the relocation we must offer. + this._loadedSidecarStorageRoot = state.global.settings.sidecar_storage_root || ''; const pathInput = document.getElementById('sidecarStoragePath'); if (pathInput) { @@ -3527,26 +3532,30 @@ export class SettingsManager { // Path change while centralized storage is active: the assets under the // previous root do not move by themselves, so offer a root relocation. + // Compare RESOLVED roots (the raw setting is empty for the default root, + // which still holds sidecars and must be relocatable). async handleSidecarStoragePathChange() { const pathInput = document.getElementById('sidecarStoragePath'); if (!pathInput) return; - const previousPath = this._loadedSidecarStoragePath || ''; + const previousRoot = this._loadedSidecarStorageRoot || ''; await this.saveInputSetting('sidecarStoragePath', 'sidecar_storage_path'); - const newPath = pathInput.value.trim(); - this._loadedSidecarStoragePath = newPath; + this._loadedSidecarStoragePath = pathInput.value.trim(); // The resolved root is server-side; refresh before any relocate // confirm so the dialog can name the real destination. await this.refreshSidecarStorageInfo(); + const newRoot = state.global.settings.sidecar_storage_root || ''; + this._loadedSidecarStorageRoot = newRoot; + const centralized = state.global.settings.sidecar_storage_mode === 'centralized'; - if (centralized && previousPath && previousPath !== newPath) { + if (centralized && previousRoot && previousRoot !== newRoot) { const confirmed = await this.confirmSidecarMigration('relocate_root'); if (confirmed) { - await this.migrateSidecars('relocate_root', { old_root: previousPath }); + await this.migrateSidecars('relocate_root', { old_root: previousRoot }); } else { showToast('settings.sidecarStorage.migrationDeferred', {}, 'info'); } @@ -3606,6 +3615,22 @@ export class SettingsManager { } } + // Migrating into the default location: let users pick a custom folder + // first instead of migrating now and relocating afterwards. + const hintElement = modalElement.querySelector('[data-role="default-location-hint"]'); + if (hintElement) { + if (isToCentralized && state.global.settings.sidecar_storage_root_is_default) { + hintElement.textContent = translate( + 'modals.sidecarMigrationConfirm.defaultLocationHint', + {}, + 'This uses the default location. To use a custom folder, cancel, set the storage path above, then click "Migrate Sidecars Now".' + ); + hintElement.style.display = 'block'; + } else { + hintElement.style.display = 'none'; + } + } + const confirmButton = modalElement.querySelector('[data-action="confirm-sidecar-migration"]'); const cancelButton = modalElement.querySelector('[data-action="cancel-sidecar-migration"]'); if (!confirmButton || !cancelButton) { diff --git a/templates/components/modals/confirm_modals.html b/templates/components/modals/confirm_modals.html index 047e54a3..ed28157c 100644 --- a/templates/components/modals/confirm_modals.html +++ b/templates/components/modals/confirm_modals.html @@ -104,6 +104,7 @@

+