fix(ecstore): avoid duplicate keys in ListObjectsV2 (#2467)

This commit is contained in:
weisd
2026-04-10 11:29:23 +08:00
committed by GitHub
parent 2bf4b14394
commit a8a2aaa460
2 changed files with 132 additions and 2 deletions
+66 -2
View File
@@ -1477,6 +1477,16 @@ impl LocalDisk {
}
let mut dir_stack: Vec<(String, bool)> = Vec::with_capacity(5);
// Explicit directory markers and real directories can resolve to the same logical path.
let schedule_dir = |dir_stack: &mut Vec<(String, bool)>, dir_name: String, skip_object: bool| {
if let Some((last_dir_name, existing_skip_object)) = dir_stack.last_mut()
&& *last_dir_name == dir_name
{
*existing_skip_object |= skip_object;
} else {
dir_stack.push((dir_name, skip_object));
}
};
prefix = "".to_owned();
for entry in entries.iter() {
@@ -1545,7 +1555,7 @@ impl LocalDisk {
if !dir_name.ends_with(SLASH_SEPARATOR) {
dir_name.push_str(SLASH_SEPARATOR);
}
dir_stack.push((dir_name, true));
schedule_dir(&mut dir_stack, dir_name, true);
}
}
Err(err) => {
@@ -1554,7 +1564,7 @@ impl LocalDisk {
// If dirObject, but no metadata (which is unexpected) we skip it.
if !is_dir_obj && !is_empty_dir(self.get_object_path(&opts.bucket, &meta.name)?).await {
meta.name.push_str(SLASH_SEPARATOR);
dir_stack.push((meta.name, false));
schedule_dir(&mut dir_stack, meta.name, false);
}
}
@@ -3169,6 +3179,60 @@ mod test {
assert!(names.contains(&"quux/thud".to_string()));
}
#[tokio::test]
async fn test_scan_dir_deduplicates_explicit_dir_marker_recursion() {
use rustfs_filemeta::MetacacheReader;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let bucket = "test-bucket";
let bucket_dir = dir.path().join(bucket);
fs::create_dir_all(bucket_dir.join("marker/file.txt")).await.unwrap();
fs::create_dir_all(bucket_dir.join("marker/subdir/file.txt")).await.unwrap();
fs::create_dir_all(bucket_dir.join(format!("marker/subdir{GLOBAL_DIR_SUFFIX}")))
.await
.unwrap();
fs::write(bucket_dir.join("marker/file.txt/xl.meta"), b"meta").await.unwrap();
fs::write(bucket_dir.join("marker/subdir/file.txt/xl.meta"), b"meta")
.await
.unwrap();
fs::write(bucket_dir.join(format!("marker/subdir{GLOBAL_DIR_SUFFIX}/xl.meta")), b"meta")
.await
.unwrap();
let endpoint = Endpoint::try_from(dir.path().to_str().unwrap()).unwrap();
let disk = LocalDisk::new(&endpoint, false).await.unwrap();
let (reader, mut writer) = tokio::io::duplex(4096);
let mut out = MetacacheWriter::new(&mut writer);
let opts = WalkDirOptions {
bucket: bucket.to_string(),
base_dir: "marker/".to_string(),
recursive: true,
..Default::default()
};
let mut objs_returned = 0;
disk.scan_dir("marker/".to_string(), "".to_string(), &opts, &mut out, &mut objs_returned, false)
.await
.unwrap();
out.close().await.unwrap();
let mut reader = MetacacheReader::new(reader);
let entries = reader.read_all().await.unwrap();
let names: Vec<String> = entries
.into_iter()
.filter(|entry| !entry.metadata.is_empty())
.map(|entry| entry.name)
.collect();
assert_eq!(names.iter().filter(|name| *name == "marker/subdir/file.txt").count(), 1);
assert_eq!(names.iter().filter(|name| *name == "marker/subdir/").count(), 1);
assert_eq!(names.iter().filter(|name| *name == "marker/file.txt").count(), 1);
}
#[tokio::test]
async fn test_make_volume() {
let p = "./testv0";