mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-27 08:38:58 +00:00
fix(scanner): continue scans after traversal errors (#2414)
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user