From 2e778e90db600bf83a4f152fa1a77d27317d7ed0 Mon Sep 17 00:00:00 2001 From: slvnlrt Date: Tue, 24 Mar 2026 17:08:31 +0100 Subject: [PATCH] fix(locations): canonicalize paths and register watcher on location add MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two upstream bugs fixed: 1. LocationManager::add_location() stored paths as-is without canonicalization. Relative paths (e.g. from cwd-dependent contexts) broke the watcher, volume manager, and indexer pipelines. Now calls tokio::fs::canonicalize() on local physical paths before storing, with UNC prefix stripping on Windows. 2. LocationAddAction::execute() never registered new locations with the FsWatcherService. The watcher only discovered locations at startup via load_library_locations(). Any location added at runtime had no filesystem monitoring — creates, deletes, and renames went undetected. Now calls fs_watcher.watch_location() after successful creation. --- core/src/location/manager.rs | 34 ++++++++++++++++++++++++++++ core/src/ops/locations/add/action.rs | 25 ++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/core/src/location/manager.rs b/core/src/location/manager.rs index c57381d19..aa8d52c72 100644 --- a/core/src/location/manager.rs +++ b/core/src/location/manager.rs @@ -46,6 +46,40 @@ impl LocationManager { job_policies: Option, volume_manager: &crate::volume::VolumeManager, ) -> LocationResult<(Uuid, String)> { + // Canonicalize local physical paths to absolute form before storing. + // Relative paths break the watcher, volume resolution, and indexer. + // Only for local device — remote paths can't be resolved locally. + let sd_path = if sd_path.is_local() { + if let crate::domain::addressing::SdPath::Physical { device_slug, path } = sd_path { + let canonical = tokio::fs::canonicalize(&path).await.map_err(|e| { + LocationError::InvalidPath(format!( + "Failed to resolve path {}: {}", + path.display(), + e + )) + })?; + // On Windows, canonicalize() returns UNC paths (\\?\C:\...) which break + // starts_with() matching throughout the codebase. Strip the prefix. + #[cfg(windows)] + let canonical = { + let s = canonical.to_string_lossy(); + if let Some(stripped) = s.strip_prefix(r"\\?\") { + std::path::PathBuf::from(stripped) + } else { + canonical + } + }; + crate::domain::addressing::SdPath::Physical { + device_slug, + path: canonical, + } + } else { + sd_path + } + } else { + sd_path + }; + info!("Adding location: {}", sd_path); // Validate the path based on type diff --git a/core/src/ops/locations/add/action.rs b/core/src/ops/locations/add/action.rs index f77b09df1..4d6edec43 100644 --- a/core/src/ops/locations/add/action.rs +++ b/core/src/ops/locations/add/action.rs @@ -102,6 +102,31 @@ impl LibraryAction for LocationAddAction { .await .map_err(|e| ActionError::Internal(e.to_string()))?; + // Register the new location with the filesystem watcher so changes + // (creates, deletes, renames) are detected in real-time. + // Without this, the watcher only learns about locations at startup. + if let Some(local_path) = self.input.path.as_local_path() { + if let Some(fs_watcher) = context.get_fs_watcher().await { + use crate::ops::indexing::handlers::LocationMeta; + use crate::ops::indexing::RuleToggles; + + // Use canonical path to match what add_location stored in DB + let root_path = tokio::fs::canonicalize(local_path) + .await + .unwrap_or_else(|_| local_path.to_path_buf()); + + let meta = LocationMeta { + id: location_id, + library_id: library.id(), + root_path, + rule_toggles: RuleToggles::default(), + }; + if let Err(e) = fs_watcher.watch_location(meta).await { + tracing::warn!("Failed to register location with watcher: {}", e); + } + } + } + // Parse the job ID from the string returned by add_location let job_id = if !job_id_string.is_empty() { Some(