#[cfg(feature = "progressbar")]
use indicatif::ProgressBar;
use jwalk::DirEntry;
use rayon::iter::{IntoParallelRefIterator, ParallelBridge, ParallelIterator};
use rusty_pool::ThreadPool;
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum RdError {
#[error("Failed to read metadata for {0}: {1}")]
MetadataError(PathBuf, std::io::Error),
#[error("Failed to set permissions for: {0}: {1}")]
PermissionError(PathBuf, std::io::Error),
#[error("Failed to remove item {0}: {1}")]
RemoveError(PathBuf, std::io::Error),
#[error("Failed to walk directory: {0}: {1}")]
WalkdirError(PathBuf, String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
impl From<RdError> for std::io::Error {
fn from(err: RdError) -> Self {
match err {
RdError::Io(e) => e,
RdError::MetadataError(_, e) => e,
RdError::PermissionError(_, e) => e,
RdError::RemoveError(_, e) => e,
RdError::WalkdirError(path, msg) => std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to walk directory {}: {}", path.display(), msg),
),
}
}
}
fn set_writable(path: &Path) -> Result<(), RdError> {
let mut perms = std::fs::metadata(path)
.map_err(|err| RdError::MetadataError(path.to_path_buf(), err))?
.permissions();
perms.set_readonly(false);
std::fs::set_permissions(path, perms)
.map_err(|err| RdError::PermissionError(path.to_path_buf(), err))?;
Ok(())
}
fn set_folder_writable(path: &Path) -> Result<(), RdError> {
let entries: Vec<DirEntry<((), ())>> = jwalk::WalkDir::new(&path)
.skip_hidden(false)
.into_iter()
.filter_map(|i| match i {
Ok(entry) if entry.file_type().is_file() => Some(Ok(entry)),
Ok(_) => None,
Err(e) => Some(Err(e)),
})
.collect::<Result<Vec<_>, _>>()
.map_err(|err| RdError::WalkdirError(path.to_path_buf(), err.to_string()))?;
let errors: Vec<_> = entries
.par_iter()
.filter_map(|entry| set_writable(&entry.path()).err())
.collect();
if let Some(err) = errors.into_iter().next() {
return Err(err);
}
Ok(())
}
pub fn delete_folder(dpath: &Path) -> Result<(), RdError> {
let mut tree: BTreeMap<u64, Vec<PathBuf>> = BTreeMap::new();
let entries: Vec<DirEntry<((), ())>> = jwalk::WalkDir::new(&dpath)
.skip_hidden(false)
.into_iter()
.par_bridge()
.filter_map(|i| match i {
Ok(entry) if entry.path().is_dir() => Some(Ok(entry)),
Ok(_) => None,
Err(e) => Some(Err(e)),
})
.collect::<Result<Vec<_>, _>>()
.map_err(|err| RdError::WalkdirError(dpath.to_path_buf(), err.to_string()))?;
#[cfg(feature = "progressbar")]
let pb = ProgressBar::new(entries.len() as u64);
for entry in entries {
tree.entry(entry.depth as u64)
.or_insert_with(Vec::new)
.push(entry.path());
}
let pool = ThreadPool::default();
let mut handles = vec![];
for (_, entries) in tree.into_iter().rev() {
#[cfg(feature = "progressbar")]
let pb = pb.clone();
handles.push(pool.evaluate(move || {
entries.par_iter().for_each(|entry| {
let _ = std::fs::remove_dir_all(entry);
#[cfg(feature = "progressbar")]
pb.inc(1);
});
}));
}
for handle in handles {
handle.await_complete();
}
if dpath.exists() {
set_folder_writable(&dpath)?;
std::fs::remove_dir_all(dpath)
.map_err(|err| RdError::RemoveError(dpath.to_path_buf(), err))?;
}
#[cfg(feature = "progressbar")]
pb.finish();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn create_test_structure(base: &Path) -> std::io::Result<()> {
fs::create_dir_all(base.join("dir1/subdir1"))?;
fs::create_dir_all(base.join("dir1/subdir2"))?;
fs::create_dir_all(base.join("dir2"))?;
fs::write(base.join("file1.txt"), "content1")?;
fs::write(base.join("dir1/file2.txt"), "content2")?;
fs::write(base.join("dir1/subdir1/file3.txt"), "content3")?;
fs::write(base.join("dir2/file4.txt"), "content4")?;
Ok(())
}
fn make_readonly(path: &Path) -> std::io::Result<()> {
let mut perms = fs::metadata(path)?.permissions();
perms.set_readonly(true);
fs::set_permissions(path, perms)?;
Ok(())
}
#[test]
fn test_delete_empty_directory() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join("empty_dir");
fs::create_dir(&test_path).unwrap();
assert!(test_path.exists());
delete_folder(&test_path).unwrap();
assert!(!test_path.exists());
}
#[test]
fn test_delete_directory_with_files() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join("dir_with_files");
fs::create_dir(&test_path).unwrap();
fs::write(test_path.join("file1.txt"), "content").unwrap();
fs::write(test_path.join("file2.txt"), "content").unwrap();
assert!(test_path.exists());
delete_folder(&test_path).unwrap();
assert!(!test_path.exists());
}
#[test]
fn test_delete_nested_directory_structure() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join("nested");
create_test_structure(&test_path).unwrap();
assert!(test_path.exists());
assert!(test_path.join("dir1/subdir1/file3.txt").exists());
delete_folder(&test_path).unwrap();
assert!(!test_path.exists());
assert!(!test_path.join("dir1").exists());
}
#[test]
fn test_delete_directory_with_readonly_files() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join("readonly_test");
create_test_structure(&test_path).unwrap();
make_readonly(&test_path.join("file1.txt")).unwrap();
make_readonly(&test_path.join("dir1/file2.txt")).unwrap();
assert!(test_path.exists());
delete_folder(&test_path).unwrap();
assert!(!test_path.exists());
}
#[test]
fn test_delete_nonexistent_directory() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join("does_not_exist");
let result = delete_folder(&test_path);
match result {
Ok(_) => assert!(!test_path.exists()),
Err(RdError::WalkdirError(_, _)) => {}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[test]
fn test_delete_directory_with_symlinks() {
let temp_dir = TempDir::new().unwrap();
let external_dir = temp_dir.path().join("external");
fs::create_dir(&external_dir).unwrap();
fs::write(external_dir.join("important.txt"), "don't delete me").unwrap();
let test_path = temp_dir.path().join("with_symlink");
fs::create_dir(&test_path).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
symlink(&external_dir, test_path.join("link_to_external")).unwrap();
}
#[cfg(windows)]
{
use std::os::windows::fs::symlink_dir;
symlink_dir(&external_dir, test_path.join("link_to_external")).unwrap();
}
delete_folder(&test_path).unwrap();
assert!(!test_path.exists());
assert!(external_dir.exists());
assert!(external_dir.join("important.txt").exists());
}
#[test]
fn test_set_writable_on_readonly_file() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("readonly_file.txt");
fs::write(&file_path, "content").unwrap();
make_readonly(&file_path).unwrap();
let perms = fs::metadata(&file_path).unwrap().permissions();
assert!(perms.readonly());
set_writable(&file_path).unwrap();
let perms = fs::metadata(&file_path).unwrap().permissions();
assert!(!perms.readonly());
}
#[test]
fn test_delete_large_directory_structure() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join("large_structure");
fs::create_dir(&test_path).unwrap();
for i in 0..10 {
let dir = test_path.join(format!("dir_{}", i));
fs::create_dir(&dir).unwrap();
for j in 0..5 {
fs::write(dir.join(format!("file_{}.txt", j)), "content").unwrap();
}
let subdir = dir.join("subdir");
fs::create_dir(&subdir).unwrap();
for k in 0..3 {
fs::write(subdir.join(format!("subfile_{}.txt", k)), "content").unwrap();
}
}
assert!(test_path.exists());
delete_folder(&test_path).unwrap();
assert!(!test_path.exists());
}
#[test]
fn test_error_on_invalid_path() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("not_a_dir.txt");
fs::write(&file_path, "content").unwrap();
let result = delete_folder(&file_path);
match result {
Ok(_) => assert!(!file_path.exists()),
Err(_) => {} }
}
}