fix(locations): canonicalize paths and register watcher on location add

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.
This commit is contained in:
slvnlrt
2026-03-24 17:08:31 +01:00
parent 4d87617a3e
commit 2e778e90db
2 changed files with 59 additions and 0 deletions

View File

@@ -46,6 +46,40 @@ impl LocationManager {
job_policies: Option<String>,
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

View File

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