From 39a283fbced9cde26b05e9e12330e1e3364b83a7 Mon Sep 17 00:00:00 2001 From: cxymds Date: Mon, 1 Jun 2026 21:04:40 +0800 Subject: [PATCH] fix(windows): handle zfs volume paths (#3157) * fix(windows): handle zfs volume paths * fix(windows): address volume path review * refactor(windows): share fallback path resolution --------- Co-authored-by: houseme --- crates/ecstore/src/disk/endpoint.rs | 17 ++++++++ crates/ecstore/src/disk/local.rs | 62 ++++++++++++++++++++++++----- crates/ecstore/src/endpoints.rs | 35 +++++++++++++++- crates/utils/src/string.rs | 29 +++++++++++++- 4 files changed, 128 insertions(+), 15 deletions(-) diff --git a/crates/ecstore/src/disk/endpoint.rs b/crates/ecstore/src/disk/endpoint.rs index eb7ee49e4..6a8e86e3e 100644 --- a/crates/ecstore/src/disk/endpoint.rs +++ b/crates/ecstore/src/disk/endpoint.rs @@ -19,6 +19,23 @@ use std::{fmt::Display, path::Path}; use tracing::debug; use url::{ParseError, Url}; +#[cfg(windows)] +pub(crate) fn windows_fallback_local_path( + path: &str, + canonicalize_error: &std::io::Error, + context: &'static str, +) -> std::io::Result { + let absolute = Path::new(path).absolutize()?.to_path_buf(); + tracing::warn!( + path = %path, + canonicalize_error = %canonicalize_error, + resolved = ?absolute, + context = context, + "using windows fallback path resolution for local endpoint" + ); + Ok(absolute) +} + /// enum for endpoint type. #[derive(PartialEq, Eq, Debug)] pub enum EndpointType { diff --git a/crates/ecstore/src/disk/local.rs b/crates/ecstore/src/disk/local.rs index 5983cebea..89ddc5df1 100644 --- a/crates/ecstore/src/disk/local.rs +++ b/crates/ecstore/src/disk/local.rs @@ -356,20 +356,60 @@ impl Debug for LocalDisk { } } +/// Resolve the local disk root path from an endpoint path. +/// +/// Tries `canonicalize` first (fast path). On Windows, if canonicalization reports +/// `NotFound` for paths that may still be valid mount roots, falls back to +/// `absolutize` + metadata check to accept valid local directory roots that +/// don't support full canonicalization. +fn resolve_local_disk_root(ep_path: &str) -> Result { + match rustfs_utils::canonicalize(ep_path) { + Ok(path) => Ok(path), + Err(err) => { + if err.kind() != ErrorKind::NotFound { + return Err(to_file_error(err).into()); + } + + #[cfg(windows)] + { + // On Windows, canonicalize can fail for ZFS volumes, junction points, + // subst drives, and other non-standard filesystem mounts. Try a fallback + // path resolution using absolutize + metadata check. + let absolute = match crate::disk::endpoint::windows_fallback_local_path(ep_path, &err, "local disk root") { + Ok(path) => path, + Err(_) => { + return Err(DiskError::VolumeNotFound); + } + }; + + match std::fs::metadata(&absolute) { + Ok(metadata) => { + if !metadata.is_dir() { + return Err(DiskError::DiskNotDir); + } + return Ok(absolute); + } + Err(meta_err) => { + if meta_err.kind() == ErrorKind::NotFound { + return Err(DiskError::VolumeNotFound); + } + return Err(to_file_error(meta_err).into()); + } + } + } + + #[cfg(not(windows))] + { + Err(DiskError::VolumeNotFound) + } + } + } +} + impl LocalDisk { pub async fn new(ep: &Endpoint, cleanup: bool) -> Result { debug!("Creating local disk"); - // Use optimized path resolution instead of absolutize() for better performance - // Use dunce::canonicalize instead of std::fs::canonicalize to avoid UNC paths on Windows - let root = match rustfs_utils::canonicalize(ep.get_file_path()) { - Ok(path) => path, - Err(e) => { - if e.kind() == ErrorKind::NotFound { - return Err(DiskError::VolumeNotFound); - } - return Err(to_file_error(e).into()); - } - }; + let root = resolve_local_disk_root(&ep.get_file_path())?; ensure_data_usage_layout(&root).await.map_err(DiskError::from)?; diff --git a/crates/ecstore/src/endpoints.rs b/crates/ecstore/src/endpoints.rs index ecea4e994..44f842485 100644 --- a/crates/ecstore/src/endpoints.rs +++ b/crates/ecstore/src/endpoints.rs @@ -689,8 +689,39 @@ fn validate_local_physical_disk_independence(pools: &[Endpoints]) -> Result<()> let canonical = match rustfs_utils::canonicalize(path) { Ok(path) => path, Err(err) if err.kind() == ErrorKind::NotFound => { - missing_paths.push(path.clone()); - continue; + // On Windows, canonicalize can fail for ZFS volumes, junction points, + // subst drives, and other non-standard mounts. Try absolutize as fallback. + #[cfg(windows)] + { + match crate::disk::endpoint::windows_fallback_local_path(path, &err, "disk independence validation") { + Ok(absolute) => { + let abs_path = absolute.to_string_lossy().into_owned(); + match rustfs_utils::os::get_physical_device_ids(&abs_path) { + Ok(ids) => { + for device_id in ids { + device_paths.entry(device_id).or_default().insert(abs_path.clone()); + } + } + Err(device_err) => { + return Err(Error::other(format!( + "failed to inspect physical disk for local endpoint '{abs_path}' after fallback path resolution: {device_err}" + ))); + } + } + continue; + } + Err(fallback_err) => { + return Err(Error::other(format!( + "failed to resolve local endpoint path '{path}' for disk validation: {err}; fallback resolution failed: {fallback_err}" + ))); + } + } + } + #[cfg(not(windows))] + { + missing_paths.push(path.clone()); + continue; + } } Err(err) => { return Err(Error::other(format!( diff --git a/crates/utils/src/string.rs b/crates/utils/src/string.rs index bf1440d80..c606ac987 100644 --- a/crates/utils/src/string.rs +++ b/crates/utils/src/string.rs @@ -423,9 +423,30 @@ pub fn find_ellipses_patterns(arg: &str) -> Result { /// ``` /// pub fn has_ellipses>(s: &[T]) -> bool { - let pattern = [ELLIPSES, OPEN_BRACES, CLOSE_BRACES]; + s.iter().any(|v| { + let v = v.as_ref(); - s.iter().any(|v| pattern.iter().any(|p| v.as_ref().contains(p))) + let mut remaining = v; + while let Some(start) = remaining.find(OPEN_BRACES) { + let after_start = &remaining[start + OPEN_BRACES.len()..]; + let Some(end) = after_start.find(CLOSE_BRACES) else { + break; + }; + // Dotted brace content covers both valid ellipsis patterns like + // {1...64} and invalid attempts like {1..64}, while excluding + // Windows Volume{GUID} paths (GUIDs use hyphens, not dots). + if after_start[..end].contains('.') { + return true; + } + remaining = &after_start[end + CLOSE_BRACES.len()..]; + } + + // Bare `...` without braces (e.g. "1...64") is still an ellipsis attempt. + if v.contains(ELLIPSES) { + return true; + } + false + }) } /// Parses an ellipses range pattern of following style @@ -569,6 +590,10 @@ mod tests { ), (15, vec!["mydisk-{a...z}{1...20}"], true), (16, vec!["mydisk-{1...4}{1..2.}"], true), + // Windows Volume{GUID} paths must not match; GUIDs use hyphens, not dots. + (17, vec![r"\\?\Volume{9425a190-4bb3-11f1-86de-803253b6a8b8}\"], false), + (18, vec![r"//./Volume{9425a190-4bb3-11f1-86de-803253b6a8b8}/"], false), + (19, vec![r"\\?\Volume{9425a190-4bb3-11f1-86de-803253b6a8b8}\disk-{1..4}"], true), ]; for (i, args, expected) in test_cases {