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
This commit is contained in:
Will Miao
2026-10-10 14:35:08 +08:00
parent 6d45f9296b
commit 5796a0e2e9
15 changed files with 345 additions and 24 deletions
+31 -6
View File
@@ -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) {