From 9ed6487e2faf46aa8864ab61cee1b7621c4f985b Mon Sep 17 00:00:00 2001 From: weisd Date: Tue, 7 Apr 2026 14:19:26 +0800 Subject: [PATCH] fix(scanner): continue scans after traversal errors (#2414) --- crates/scanner/src/scanner_folder.rs | 150 ++++++++++++++++++++++++--- 1 file changed, 138 insertions(+), 12 deletions(-) diff --git a/crates/scanner/src/scanner_folder.rs b/crates/scanner/src/scanner_folder.rs index 7ba6ca3ca..a45bdb1ad 100644 --- a/crates/scanner/src/scanner_folder.rs +++ b/crates/scanner/src/scanner_folder.rs @@ -14,6 +14,7 @@ use std::collections::HashSet; use std::fs::FileType; +use std::io::ErrorKind; use std::sync::Arc; use std::time::{Duration, SystemTime}; @@ -608,15 +609,29 @@ impl FolderScanner { debug!("scan_folder: dir_path: {:?}", dir_path); - let mut dir_reader = tokio::fs::read_dir(&dir_path) - .await - .map_err(|e| ScannerError::Other(e.to_string()))?; + let mut dir_reader = match tokio::fs::read_dir(&dir_path).await { + Ok(dir_reader) => dir_reader, + Err(e) if e.kind() == ErrorKind::NotFound => { + warn!("scan_folder: directory disappeared before read {}: {}", dir_path, e); + return Ok(()); + } + Err(e) => return Err(ScannerError::Io(e)), + }; - while let Some(entry) = dir_reader - .next_entry() - .await - .map_err(|e| ScannerError::Other(e.to_string()))? - { + loop { + let entry = match dir_reader.next_entry().await { + Ok(Some(entry)) => entry, + Ok(None) => break, + Err(e) if e.kind() == ErrorKind::NotFound => { + warn!("scan_folder: directory disappeared during iteration {}: {}", dir_path, e); + break; + } + Err(e) if e.kind() == ErrorKind::NotADirectory => { + warn!("scan_folder: path became non-directory during iteration {}: {}", dir_path, e); + break; + } + Err(e) => return Err(ScannerError::Io(e)), + }; let file_name = entry.file_name().to_string_lossy().to_string(); if file_name.is_empty() || file_name == "." || file_name == ".." { continue; @@ -632,7 +647,42 @@ impl FolderScanner { continue; } - let entry_type = entry.file_type().await.map_err(|e| ScannerError::Other(e.to_string()))?; + // Ignore entries that disappeared during traversal or hit symlink + // loops, but propagate other walk errors. + let mut entry_type = match entry.file_type().await { + Ok(entry_type) => entry_type, + Err(e) if e.kind() == ErrorKind::NotFound => { + warn!("scan_folder: entry disappeared before type lookup {}: {}", entry_name, e); + continue; + } + Err(e) if e.kind() == ErrorKind::TooManyLinks => { + warn!("scan_folder: entry hit symlink loop before type lookup {}: {}", entry_name, e); + continue; + } + Err(e) => return Err(ScannerError::Io(e)), + }; + + if entry_type.is_symlink() { + let metadata = match tokio::fs::metadata(&file_path).await { + Ok(metadata) => metadata, + Err(e) if e.kind() == ErrorKind::NotFound => { + warn!("scan_folder: symlink target disappeared before metadata lookup {}: {}", file_path, e); + continue; + } + Err(e) if e.kind() == ErrorKind::TooManyLinks => { + warn!("scan_folder: symlink target hit loop before metadata lookup {}: {}", file_path, e); + continue; + } + Err(e) => return Err(ScannerError::Io(e)), + }; + + if metadata.is_dir() { + warn!("scan_folder: ignoring symlinked directory {}", file_path); + continue; + } + + entry_type = metadata.file_type(); + } // ok @@ -816,7 +866,10 @@ impl FolderScanner { // Use Box::pin for recursive async call let fut = Box::pin(self.scan_folder(ctx.clone(), folder_item.clone(), &mut dst)); - fut.await.map_err(|e| ScannerError::Other(e.to_string()))?; + if let Err(e) = fut.await { + warn!("scan_folder: failed to scan child folder {}: {}", folder_item.name, e); + continue; + } tokio::task::yield_now().await; if !into.compacted { @@ -865,7 +918,10 @@ impl FolderScanner { // Use Box::pin for recursive async call let fut = Box::pin(self.scan_folder(ctx.clone(), folder_item.clone(), &mut dst)); - fut.await.map_err(|e| ScannerError::Other(e.to_string()))?; + if let Err(e) = fut.await { + warn!("scan_folder: failed to scan child folder {}: {}", folder_item.name, e); + continue; + } tokio::task::yield_now().await; if !into.compacted { @@ -1076,7 +1132,10 @@ impl FolderScanner { // Use Box::pin for recursive async call let fut = Box::pin(self.scan_folder(ctx.clone(), folder_item.clone(), &mut dst)); - fut.await.map_err(|e| ScannerError::Other(e.to_string()))?; + if let Err(e) = fut.await { + warn!("scan_folder: failed to scan child folder {}: {}", folder_item.name, e); + continue; + } tokio::task::yield_now().await; if !into.compacted { @@ -1264,6 +1323,8 @@ mod tests { use super::*; use rustfs_ecstore::disk::{DiskOption, endpoint::Endpoint, new_disk}; use serial_test::serial; + #[cfg(unix)] + use std::os::unix::fs::{PermissionsExt, symlink}; use std::sync::atomic::AtomicBool; use uuid::Uuid; @@ -1453,4 +1514,69 @@ mod tests { assert!(scanner.new_cache.info.failed_objects.contains_key("fresh2")); assert!(!scanner.new_cache.info.failed_objects.contains_key("expired")); } + + #[tokio::test] + #[serial] + #[cfg(unix)] + async fn test_scan_folder_skips_unreadable_child_directory() { + let (mut scanner, temp_dir) = build_test_scanner().await; + let _guard = TestGuard::new(60, 0, &mut scanner, temp_dir.clone()); + + let bucket_dir = temp_dir.join("bucket"); + let good_dir = bucket_dir.join("good"); + let bad_dir = bucket_dir.join("bad"); + + std::fs::create_dir_all(&good_dir).expect("failed to create good dir"); + std::fs::create_dir_all(&bad_dir).expect("failed to create bad dir"); + std::fs::set_permissions(&bad_dir, std::fs::Permissions::from_mode(0o000)).expect("failed to remove bad dir permissions"); + + scanner.old_cache.info.name = "bucket".to_string(); + scanner.new_cache.info.name = "bucket".to_string(); + scanner.update_cache.info.name = "bucket".to_string(); + + let folder = CachedFolder { + name: "bucket".to_string(), + parent: None, + object_heal_prob_div: 1, + }; + + let mut into = DataUsageEntry::default(); + let result = scanner.scan_folder(CancellationToken::new(), folder, &mut into).await; + + std::fs::set_permissions(&bad_dir, std::fs::Permissions::from_mode(0o755)) + .expect("failed to restore bad dir permissions"); + + assert!(result.is_ok(), "expected unreadable child directory to be skipped"); + } + + #[tokio::test] + #[serial] + #[cfg(unix)] + async fn test_scan_folder_ignores_symlinked_child_directory() { + let (mut scanner, temp_dir) = build_test_scanner().await; + let _guard = TestGuard::new(60, 0, &mut scanner, temp_dir.clone()); + + let bucket_dir = temp_dir.join("bucket"); + let target_dir = bucket_dir.join("target"); + let link_dir = bucket_dir.join("link"); + + std::fs::create_dir_all(&target_dir).expect("failed to create target dir"); + symlink(&target_dir, &link_dir).expect("failed to create symlinked dir"); + + scanner.old_cache.info.name = "bucket".to_string(); + scanner.new_cache.info.name = "bucket".to_string(); + scanner.update_cache.info.name = "bucket".to_string(); + + let folder = CachedFolder { + name: "bucket".to_string(), + parent: None, + object_heal_prob_div: 1, + }; + + let mut into = DataUsageEntry::default(); + let result = scanner.scan_folder(CancellationToken::new(), folder, &mut into).await; + + assert!(result.is_ok(), "expected symlinked child directory to be ignored"); + assert_eq!(into.failed_objects, 0, "expected ignored symlink not to count as a failed object"); + } }