fix(window): Compatible with Windows Path (#2691)

Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
唐小鸭
2026-04-28 22:36:59 +08:00
committed by GitHub
parent d79720da1d
commit e16f1ae639
4 changed files with 64 additions and 25 deletions
+12 -17
View File
@@ -52,7 +52,7 @@ use rmp_serde::Serializer;
use rustfs_common::defer;
use rustfs_common::heal_channel::HealOpts;
use rustfs_filemeta::{FileInfoVersions, MetaCacheEntries, MetaCacheEntry, MetadataResolutionParams};
use rustfs_utils::path::{SLASH_SEPARATOR, encode_dir_object, path_join};
use rustfs_utils::path::{encode_dir_object, path_join, path_to_bucket_object, path_to_bucket_object_with_base_path};
use rustfs_workers::workers::Workers;
use s3s::dto::{BucketLifecycleConfiguration, DefaultRetention, ReplicationConfiguration};
use serde::{Deserialize, Serialize};
@@ -986,25 +986,11 @@ impl PoolMeta {
}
pub fn path2_bucket_object(name: &str) -> (String, String) {
path2_bucket_object_with_base_path("", name)
path_to_bucket_object(name)
}
pub fn path2_bucket_object_with_base_path(base_path: &str, path: &str) -> (String, String) {
// Trim the base path and leading slash
let trimmed_path = path
.strip_prefix(base_path)
.unwrap_or(path)
.strip_prefix(SLASH_SEPARATOR)
.unwrap_or(path);
// Find the position of the first '/'
let Some(pos) = trimmed_path.find(SLASH_SEPARATOR) else {
return (trimmed_path.to_string(), "".to_string());
};
// Split into bucket and prefix
let bucket = &trimmed_path[0..pos];
let prefix = &trimmed_path[pos + 1..]; // +1 to skip the '/' character if it exists
(bucket.to_string(), prefix.to_string())
path_to_bucket_object_with_base_path(base_path, path)
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
@@ -3746,4 +3732,13 @@ mod pools_tests {
.contains("failed to start decommission routines: scheduled 1 of 2 expected workers")
);
}
#[test]
#[cfg(windows)]
fn test_path2_bucket_object_with_base_path_supports_windows_separators() {
let (bucket, object) = super::path2_bucket_object_with_base_path("C:\\data", "C:\\data\\my-bucket\\nested\\object.txt");
assert_eq!(bucket, "my-bucket");
assert_eq!(object, "nested/object.txt");
}
}
+21 -2
View File
@@ -40,9 +40,10 @@ use rustfs_ecstore::set_disk::SetDisks;
use rustfs_ecstore::store_api::{BucketInfo, BucketOperations, BucketOptions, ObjectInfo};
use rustfs_ecstore::{StorageAPI, error::Result, store::ECStore};
use rustfs_filemeta::FileMeta;
use rustfs_utils::path::{SLASH_SEPARATOR, path_join_buf};
use rustfs_utils::path::path_join_buf;
use s3s::dto::{BucketLifecycleConfiguration, ReplicationConfiguration};
use std::collections::HashMap;
use std::path::Path;
use std::time::SystemTime;
use std::{fmt::Debug, sync::Arc};
use time::OffsetDateTime;
@@ -69,6 +70,13 @@ fn finalize_nsscanner_result(results: &[DataUsageCache], first_err: Option<Error
Ok(())
}
fn is_xl_meta_path(path: &str) -> bool {
Path::new(path)
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name == STORAGE_FORMAT_FILE)
}
async fn persist_and_publish_cache_snapshot<S: StorageAPI>(
store: Arc<S>,
updates: &mpsc::Sender<DataUsageCache>,
@@ -540,7 +548,7 @@ impl ScannerIODisk for Disk {
async fn get_size(&self, mut item: ScannerItem) -> Result<SizeSummary> {
let done_object = Metrics::time(Metric::ScanObject);
if !item.path.ends_with(&format!("{SLASH_SEPARATOR}{STORAGE_FORMAT_FILE}")) {
if !is_xl_meta_path(&item.path) {
return Err(StorageError::other("skip file".to_string()));
}
@@ -741,4 +749,15 @@ mod tests {
.expect_err("all failed sets should bubble first error");
assert!(err.to_string().contains("set failed"));
}
#[test]
#[cfg(windows)]
fn is_xl_meta_path_accepts_windows_separator() {
assert!(is_xl_meta_path("D:\\data\\bucket\\object\\xl.meta"));
}
#[test]
fn is_xl_meta_path_accepts_forward_separator() {
assert!(is_xl_meta_path("/data/bucket/object/xl.meta"));
}
}
+18 -1
View File
@@ -90,7 +90,12 @@ pub fn retain_slash(s: &str) -> String {
/// Checks if string `s` starts with `prefix` using case-insensitive comparison.
pub fn strings_has_prefix_fold(s: &str, prefix: &str) -> bool {
s.len() >= prefix.len() && (s[..prefix.len()] == *prefix || s[..prefix.len()].eq_ignore_ascii_case(prefix))
if s.starts_with(prefix) {
return true;
}
s.get(..prefix.len())
.is_some_and(|s_prefix| s_prefix.eq_ignore_ascii_case(prefix))
}
/// Checks if string `s` starts with `prefix`.
@@ -875,4 +880,16 @@ mod tests {
assert_eq!(bucket, "bucket");
assert_eq!(object, "object");
}
#[test]
#[cfg(target_os = "windows")]
fn test_path_to_bucket_object_with_base_path_handles_unicode_without_panicking() {
let (bucket, object) = path_to_bucket_object_with_base_path(
"D:\\Github\\rustfs\\target\\volumes\\test1",
"s3-test-bucket/中文/日本語/한글-9cd5599a-f8eb-4e24-9df7-32ecd8d8ad1f",
);
assert_eq!(bucket, "s3-test-bucket");
assert_eq!(object, "中文/日本語/한글-9cd5599a-f8eb-4e24-9df7-32ecd8d8ad1f");
}
}
+13 -5
View File
@@ -506,13 +506,12 @@ pub fn parse_ellipses_range(pattern: &str) -> Result<Vec<String>> {
/// ```
///
pub fn strings_has_prefix_fold(s: &str, prefix: &str) -> bool {
if s.len() < prefix.len() {
return false;
if s.starts_with(prefix) {
return true;
}
let s_prefix = &s[..prefix.len()];
// Test match with case first, then case-insensitive
s_prefix == prefix || s_prefix.to_lowercase() == prefix.to_lowercase()
s.get(..prefix.len())
.is_some_and(|s_prefix| s_prefix.eq_ignore_ascii_case(prefix))
}
#[cfg(test)]
@@ -872,4 +871,13 @@ mod tests {
}
}
}
#[test]
#[cfg(target_os = "windows")]
fn test_strings_has_prefix_fold_handles_unicode_without_panicking() {
assert!(!strings_has_prefix_fold(
"s3-test-bucket/中文/日本語/한글-9cd5599a-f8eb-4e24-9df7-32ecd8d8ad1f",
"D:\\Github\\rustfs\\target\\volumes\\test1",
));
}
}