Skip to content

Commit 7b2ef81

Browse files
authored
Merge pull request #3043 from slvnlrt/fix-location-watcher
fix(locations): register watcher on location add, canonicalize paths
2 parents 4d87617 + 7b867ef commit 7b2ef81

5 files changed

Lines changed: 100 additions & 13 deletions

File tree

‎core/src/common/utils.rs‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,41 @@
33
// Note: Device ID management has been moved to device::manager for better
44
// module organization. Import from there instead:
55
// use crate::device::manager::{get_current_device_id, set_current_device_id};
6+
7+
/// Strip Windows extended path prefixes produced by `std::fs::canonicalize()`.
8+
///
9+
/// On Windows, `canonicalize()` returns paths like `\\?\C:\...` (local) or
10+
/// `\\?\UNC\server\share\...` (network). These prefixes break `starts_with()`
11+
/// matching throughout the codebase and must be normalized.
12+
///
13+
/// - `\\?\UNC\server\share\...` → `\\server\share\...`
14+
/// - `\\?\C:\...` → `C:\...`
15+
/// - All other paths are returned unchanged.
16+
#[cfg(windows)]
17+
pub fn strip_windows_extended_prefix(path: std::path::PathBuf) -> std::path::PathBuf {
18+
if let Some(s) = path.to_str() {
19+
if s.starts_with(r"\\?\UNC\") {
20+
// \\?\UNC\server\share\... → \\server\share\...
21+
std::path::PathBuf::from(format!(r"\\{}", &s[8..]))
22+
} else if let Some(stripped) = s.strip_prefix(r"\\?\") {
23+
// Only strip \\?\ when followed by a drive letter (e.g. C:\).
24+
// Leave volume GUIDs (\\?\Volume{...}\) and other verbatim
25+
// forms untouched — they are invalid without the prefix.
26+
if stripped.as_bytes().get(1) == Some(&b':') {
27+
std::path::PathBuf::from(stripped)
28+
} else {
29+
path
30+
}
31+
} else {
32+
path
33+
}
34+
} else {
35+
path
36+
}
37+
}
38+
39+
/// No-op on non-Windows platforms.
40+
#[cfg(not(windows))]
41+
pub fn strip_windows_extended_prefix(path: std::path::PathBuf) -> std::path::PathBuf {
42+
path
43+
}

‎core/src/location/manager.rs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,30 @@ impl LocationManager {
4646
job_policies: Option<String>,
4747
volume_manager: &crate::volume::VolumeManager,
4848
) -> LocationResult<(Uuid, String)> {
49+
// Canonicalize local physical paths to absolute form before storing.
50+
// Relative paths break the watcher, volume resolution, and indexer.
51+
// Only for local device — remote paths can't be resolved locally.
52+
let sd_path = if sd_path.is_local() {
53+
if let crate::domain::addressing::SdPath::Physical { device_slug, path } = sd_path {
54+
let canonical = tokio::fs::canonicalize(&path).await.map_err(|e| {
55+
LocationError::InvalidPath(format!(
56+
"Failed to resolve path {}: {}",
57+
path.display(),
58+
e
59+
))
60+
})?;
61+
let canonical = crate::common::utils::strip_windows_extended_prefix(canonical);
62+
crate::domain::addressing::SdPath::Physical {
63+
device_slug,
64+
path: canonical,
65+
}
66+
} else {
67+
sd_path
68+
}
69+
} else {
70+
sd_path
71+
};
72+
4973
info!("Adding location: {}", sd_path);
5074

5175
// Validate the path based on type

‎core/src/ops/locations/add/action.rs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,31 @@ impl LibraryAction for LocationAddAction {
102102
.await
103103
.map_err(|e| ActionError::Internal(e.to_string()))?;
104104

105+
// Register the new location with the filesystem watcher so changes
106+
// (creates, deletes, renames) are detected in real-time.
107+
// Without this, the watcher only learns about locations at startup.
108+
if let Some(local_path) = self.input.path.as_local_path() {
109+
if let Some(fs_watcher) = context.get_fs_watcher().await {
110+
use crate::ops::indexing::handlers::LocationMeta;
111+
use crate::ops::indexing::RuleToggles;
112+
113+
let root_path = tokio::fs::canonicalize(local_path)
114+
.await
115+
.unwrap_or_else(|_| local_path.to_path_buf());
116+
let root_path = crate::common::utils::strip_windows_extended_prefix(root_path);
117+
118+
let meta = LocationMeta {
119+
id: location_id,
120+
library_id: library.id(),
121+
root_path,
122+
rule_toggles: RuleToggles::default(),
123+
};
124+
if let Err(e) = fs_watcher.watch_location(meta).await {
125+
tracing::warn!("Failed to register location with watcher: {}", e);
126+
}
127+
}
128+
}
129+
105130
// Parse the job ID from the string returned by add_location
106131
let job_id = if !job_id_string.is_empty() {
107132
Some(

‎core/src/ops/locations/remove/action.rs‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,24 @@ impl LibraryAction for LocationRemoveAction {
4343
library: std::sync::Arc<crate::library::Library>,
4444
context: Arc<CoreContext>,
4545
) -> Result<Self::Output, ActionError> {
46-
// Remove the location
46+
// Remove the location from DB
4747
let location_manager = LocationManager::new(context.events.as_ref().clone());
4848
location_manager
4949
.remove_location(&library, self.input.location_id)
5050
.await
5151
.map_err(|e| ActionError::Internal(e.to_string()))?;
5252

53+
// Unwatch the location from the filesystem watcher
54+
if let Some(watcher) = context.get_fs_watcher().await {
55+
if let Err(e) = watcher.unwatch_location(self.input.location_id).await {
56+
tracing::warn!(
57+
"Failed to unwatch location {}: {}",
58+
self.input.location_id,
59+
e
60+
);
61+
}
62+
}
63+
5364
Ok(LocationRemoveOutput::new(self.input.location_id, None))
5465
}
5566

‎core/src/volume/fs/refs.rs‎

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -293,18 +293,7 @@ impl super::FilesystemHandler for RefsHandler {
293293
}
294294

295295
fn contains_path(&self, volume: &Volume, path: &std::path::Path) -> bool {
296-
// Strip Windows extended path prefix (\\?\) produced by canonicalize()
297-
let normalized_path = if let Some(path_str) = path.to_str() {
298-
if path_str.starts_with("\\\\?\\UNC\\") {
299-
PathBuf::from(format!("\\\\{}", &path_str[8..]))
300-
} else if let Some(stripped) = path_str.strip_prefix("\\\\?\\") {
301-
PathBuf::from(stripped)
302-
} else {
303-
path.to_path_buf()
304-
}
305-
} else {
306-
path.to_path_buf()
307-
};
296+
let normalized_path = crate::common::utils::strip_windows_extended_prefix(path.to_path_buf());
308297

309298
if normalized_path.starts_with(&volume.mount_point) {
310299
return true;

0 commit comments

Comments
 (0)