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 @@