use std::fs;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use rayon::prelude::*;
use crate::progress::Progress;
static REMOVE_POOL: LazyLock<rayon::ThreadPool> = LazyLock::new(|| {
rayon::ThreadPoolBuilder::new()
.num_threads(4)
.build()
.expect("failed to build remove thread pool")
});
pub fn remove_dir_with_progress(path: &Path, progress: &Progress) -> (usize, u64) {
let mut leaves: Vec<PathBuf> = Vec::new();
let mut dirs: Vec<PathBuf> = Vec::new();
let mut stack = vec![path.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(entries) = fs::read_dir(&dir) else {
dirs.push(dir);
continue;
};
dirs.push(dir);
for entry in entries.flatten() {
let Ok(file_type) = entry.file_type() else {
continue;
};
let entry_path = entry.path();
if file_type.is_dir() {
stack.push(entry_path);
} else {
leaves.push(entry_path);
}
}
}
let removed_files = AtomicUsize::new(0);
let removed_bytes = AtomicU64::new(0);
REMOVE_POOL.install(|| {
leaves.par_iter().for_each(|leaf| {
let bytes = leaf.symlink_metadata().map(|m| m.len()).unwrap_or(0);
if fs::remove_file(leaf).is_ok() {
removed_files.fetch_add(1, Ordering::Relaxed);
removed_bytes.fetch_add(bytes, Ordering::Relaxed);
progress.record(bytes);
}
});
});
for dir in dirs.iter().rev() {
let _ = fs::remove_dir(dir);
}
(removed_files.into_inner(), removed_bytes.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_remove_dir_with_progress_empty_dir() {
let temp = tempfile::tempdir().unwrap();
let dir = temp.path().join("empty");
std::fs::create_dir(&dir).unwrap();
let (files, bytes) = remove_dir_with_progress(&dir, &Progress::disabled());
assert_eq!(files, 0);
assert_eq!(bytes, 0);
assert!(!dir.exists());
}
#[test]
fn test_remove_dir_with_progress_counts_files_and_bytes() {
let temp = tempfile::tempdir().unwrap();
let root = temp.path().join("tree");
std::fs::create_dir_all(root.join("a/b")).unwrap();
std::fs::write(root.join("a/file1.txt"), b"hello").unwrap(); std::fs::write(root.join("a/b/file2.txt"), b"world!").unwrap(); std::fs::write(root.join("top.txt"), b"x").unwrap();
let (files, bytes) = remove_dir_with_progress(&root, &Progress::disabled());
assert_eq!(files, 3);
assert_eq!(bytes, 12);
assert!(!root.exists());
}
#[test]
fn test_remove_dir_with_progress_missing_root_is_ok() {
let temp = tempfile::tempdir().unwrap();
let missing = temp.path().join("does-not-exist");
let (files, bytes) = remove_dir_with_progress(&missing, &Progress::disabled());
assert_eq!(files, 0);
assert_eq!(bytes, 0);
}
#[cfg(unix)]
#[test]
fn test_remove_dir_with_progress_handles_symlinks() {
let temp = tempfile::tempdir().unwrap();
let root = temp.path().join("tree");
std::fs::create_dir(&root).unwrap();
std::fs::write(root.join("real.txt"), b"abc").unwrap();
std::os::unix::fs::symlink(root.join("real.txt"), root.join("link")).unwrap();
let (files, _bytes) = remove_dir_with_progress(&root, &Progress::disabled());
assert_eq!(files, 2);
assert!(!root.exists());
}
#[cfg(unix)]
#[test]
fn test_remove_dir_with_progress_skips_unreadable_subtree() {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir().unwrap();
let root = temp.path().join("tree");
let blocked = root.join("blocked");
std::fs::create_dir_all(&blocked).unwrap();
std::fs::write(blocked.join("hidden.txt"), b"x").unwrap();
std::fs::set_permissions(&blocked, std::fs::Permissions::from_mode(0o000)).unwrap();
if std::fs::read_dir(&blocked).is_ok() {
std::fs::set_permissions(&blocked, std::fs::Permissions::from_mode(0o755)).ok();
eprintln!("Skipping - running with elevated privileges");
return;
}
let (files, bytes) = remove_dir_with_progress(&root, &Progress::disabled());
std::fs::set_permissions(&blocked, std::fs::Permissions::from_mode(0o755)).ok();
assert_eq!(files, 0, "no leaves should be unlinked when blocked");
assert_eq!(bytes, 0);
}
}