1#[cfg(feature = "progressbar")]
45use indicatif::ProgressBar;
46use jwalk::DirEntry;
47use rayon::iter::{IntoParallelRefIterator, ParallelBridge, ParallelIterator};
48use rusty_pool::ThreadPool;
49use std::{
50 collections::BTreeMap,
51 path::{Path, PathBuf},
52};
53use thiserror::Error;
54
55#[derive(Error, Debug)]
56pub enum RdError {
57 #[error("Failed to read metadata for {0}: {1}")]
58 MetadataError(PathBuf, std::io::Error),
59
60 #[error("Failed to set permissions for: {0}: {1}")]
61 PermissionError(PathBuf, std::io::Error),
62
63 #[error("Failed to remove item {0}: {1}")]
64 RemoveError(PathBuf, std::io::Error),
65
66 #[error("Failed to walk directory: {0}: {1}")]
67 WalkdirError(PathBuf, String),
68
69 #[error("IO error: {0}")]
70 Io(#[from] std::io::Error),
71}
72
73impl From<RdError> for std::io::Error {
74 fn from(err: RdError) -> Self {
75 match err {
76 RdError::Io(e) => e,
77 RdError::MetadataError(_, e) => e,
78 RdError::PermissionError(_, e) => e,
79 RdError::RemoveError(_, e) => e,
80 RdError::WalkdirError(path, msg) => std::io::Error::new(
81 std::io::ErrorKind::Other,
82 format!("Failed to walk directory {}: {}", path.display(), msg),
83 ),
84 }
85 }
86}
87
88fn set_writable(path: &Path) -> Result<(), RdError> {
100 let mut perms = std::fs::metadata(path)
101 .map_err(|err| RdError::MetadataError(path.to_path_buf(), err))?
102 .permissions();
103
104 perms.set_readonly(false);
105 std::fs::set_permissions(path, perms)
106 .map_err(|err| RdError::PermissionError(path.to_path_buf(), err))?;
107
108 Ok(())
109}
110
111fn set_folder_writable(path: &Path) -> Result<(), RdError> {
126 let entries: Vec<DirEntry<((), ())>> = jwalk::WalkDir::new(&path)
127 .skip_hidden(false)
128 .into_iter()
129 .filter_map(|i| match i {
130 Ok(entry) if entry.file_type().is_file() => Some(Ok(entry)),
131 Ok(_) => None,
132 Err(e) => Some(Err(e)),
133 })
134 .collect::<Result<Vec<_>, _>>()
135 .map_err(|err| RdError::WalkdirError(path.to_path_buf(), err.to_string()))?;
136
137 let errors: Vec<_> = entries
138 .par_iter()
139 .filter_map(|entry| set_writable(&entry.path()).err())
140 .collect();
141
142 if let Some(err) = errors.into_iter().next() {
143 return Err(err);
144 }
145
146 Ok(())
147}
148
149pub fn delete_folder(dpath: &Path) -> Result<(), RdError> {
180 let mut tree: BTreeMap<u64, Vec<PathBuf>> = BTreeMap::new();
181
182 let entries: Vec<DirEntry<((), ())>> = jwalk::WalkDir::new(&dpath)
183 .skip_hidden(false)
184 .into_iter()
185 .par_bridge()
186 .filter_map(|i| match i {
187 Ok(entry) if entry.path().is_dir() => Some(Ok(entry)),
188 Ok(_) => None,
189 Err(e) => Some(Err(e)),
190 })
191 .collect::<Result<Vec<_>, _>>()
192 .map_err(|err| RdError::WalkdirError(dpath.to_path_buf(), err.to_string()))?;
193
194 for entry in entries {
195 tree.entry(entry.depth as u64)
196 .or_insert_with(Vec::new)
197 .push(entry.path());
198 }
199
200 let pool = ThreadPool::default();
201
202 let mut handles = vec![];
203
204 #[cfg(feature = "progressbar")]
205 let pb = ProgressBar::new(entries.len() as u64);
206
207 for (_, entries) in tree.into_iter().rev() {
208 handles.push(pool.evaluate(move || {
209 entries.par_iter().for_each(|entry| {
210 let _ = std::fs::remove_dir_all(entry);
211 #[cfg(feature = "progressbar")]
212 pb.inc(1);
213 });
214 }));
215 }
216
217 for handle in handles {
218 handle.await_complete();
219 }
220
221 if dpath.exists() {
222 set_folder_writable(&dpath)?;
224
225 std::fs::remove_dir_all(dpath)
226 .map_err(|err| RdError::RemoveError(dpath.to_path_buf(), err))?;
227 }
228
229 #[cfg(feature = "progressbar")]
230 pb.finish();
231
232 Ok(())
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use std::fs;
239 use tempfile::TempDir;
240
241 fn create_test_structure(base: &Path) -> std::io::Result<()> {
243 fs::create_dir_all(base.join("dir1/subdir1"))?;
245 fs::create_dir_all(base.join("dir1/subdir2"))?;
246 fs::create_dir_all(base.join("dir2"))?;
247
248 fs::write(base.join("file1.txt"), "content1")?;
250 fs::write(base.join("dir1/file2.txt"), "content2")?;
251 fs::write(base.join("dir1/subdir1/file3.txt"), "content3")?;
252 fs::write(base.join("dir2/file4.txt"), "content4")?;
253
254 Ok(())
255 }
256
257 fn make_readonly(path: &Path) -> std::io::Result<()> {
259 let mut perms = fs::metadata(path)?.permissions();
260 perms.set_readonly(true);
261 fs::set_permissions(path, perms)?;
262 Ok(())
263 }
264
265 #[test]
266 fn test_delete_empty_directory() {
267 let temp_dir = TempDir::new().unwrap();
268 let test_path = temp_dir.path().join("empty_dir");
269 fs::create_dir(&test_path).unwrap();
270
271 assert!(test_path.exists());
272 delete_folder(&test_path).unwrap();
273 assert!(!test_path.exists());
274 }
275
276 #[test]
277 fn test_delete_directory_with_files() {
278 let temp_dir = TempDir::new().unwrap();
279 let test_path = temp_dir.path().join("dir_with_files");
280 fs::create_dir(&test_path).unwrap();
281 fs::write(test_path.join("file1.txt"), "content").unwrap();
282 fs::write(test_path.join("file2.txt"), "content").unwrap();
283
284 assert!(test_path.exists());
285 delete_folder(&test_path).unwrap();
286 assert!(!test_path.exists());
287 }
288
289 #[test]
290 fn test_delete_nested_directory_structure() {
291 let temp_dir = TempDir::new().unwrap();
292 let test_path = temp_dir.path().join("nested");
293 create_test_structure(&test_path).unwrap();
294
295 assert!(test_path.exists());
296 assert!(test_path.join("dir1/subdir1/file3.txt").exists());
297
298 delete_folder(&test_path).unwrap();
299
300 assert!(!test_path.exists());
301 assert!(!test_path.join("dir1").exists());
302 }
303
304 #[test]
305 fn test_delete_directory_with_readonly_files() {
306 let temp_dir = TempDir::new().unwrap();
307 let test_path = temp_dir.path().join("readonly_test");
308 create_test_structure(&test_path).unwrap();
309
310 make_readonly(&test_path.join("file1.txt")).unwrap();
312 make_readonly(&test_path.join("dir1/file2.txt")).unwrap();
313
314 assert!(test_path.exists());
315 delete_folder(&test_path).unwrap();
316 assert!(!test_path.exists());
317 }
318
319 #[test]
320 fn test_delete_nonexistent_directory() {
321 let temp_dir = TempDir::new().unwrap();
322 let test_path = temp_dir.path().join("does_not_exist");
323
324 let result = delete_folder(&test_path);
326
327 match result {
330 Ok(_) => assert!(!test_path.exists()),
331 Err(RdError::WalkdirError(_, _)) => {}
332 Err(e) => panic!("Unexpected error: {:?}", e),
333 }
334 }
335
336 #[test]
337 fn test_delete_directory_with_symlinks() {
338 let temp_dir = TempDir::new().unwrap();
339
340 let external_dir = temp_dir.path().join("external");
342 fs::create_dir(&external_dir).unwrap();
343 fs::write(external_dir.join("important.txt"), "don't delete me").unwrap();
344
345 let test_path = temp_dir.path().join("with_symlink");
347 fs::create_dir(&test_path).unwrap();
348
349 #[cfg(unix)]
350 {
351 use std::os::unix::fs::symlink;
352 symlink(&external_dir, test_path.join("link_to_external")).unwrap();
353 }
354
355 #[cfg(windows)]
356 {
357 use std::os::windows::fs::symlink_dir;
358 symlink_dir(&external_dir, test_path.join("link_to_external")).unwrap();
359 }
360
361 delete_folder(&test_path).unwrap();
363
364 assert!(!test_path.exists());
366
367 assert!(external_dir.exists());
369 assert!(external_dir.join("important.txt").exists());
370 }
371
372 #[test]
373 fn test_set_writable_on_readonly_file() {
374 let temp_dir = TempDir::new().unwrap();
375 let file_path = temp_dir.path().join("readonly_file.txt");
376 fs::write(&file_path, "content").unwrap();
377
378 make_readonly(&file_path).unwrap();
380 let perms = fs::metadata(&file_path).unwrap().permissions();
381 assert!(perms.readonly());
382
383 set_writable(&file_path).unwrap();
385
386 let perms = fs::metadata(&file_path).unwrap().permissions();
387 assert!(!perms.readonly());
388 }
389
390 #[test]
391 fn test_delete_large_directory_structure() {
392 let temp_dir = TempDir::new().unwrap();
393 let test_path = temp_dir.path().join("large_structure");
394 fs::create_dir(&test_path).unwrap();
395
396 for i in 0..10 {
398 let dir = test_path.join(format!("dir_{}", i));
399 fs::create_dir(&dir).unwrap();
400
401 for j in 0..5 {
402 fs::write(dir.join(format!("file_{}.txt", j)), "content").unwrap();
403 }
404
405 let subdir = dir.join("subdir");
407 fs::create_dir(&subdir).unwrap();
408 for k in 0..3 {
409 fs::write(subdir.join(format!("subfile_{}.txt", k)), "content").unwrap();
410 }
411 }
412
413 assert!(test_path.exists());
414 delete_folder(&test_path).unwrap();
415 assert!(!test_path.exists());
416 }
417
418 #[test]
419 fn test_error_on_invalid_path() {
420 let temp_dir = TempDir::new().unwrap();
422 let file_path = temp_dir.path().join("not_a_dir.txt");
423 fs::write(&file_path, "content").unwrap();
424
425 let result = delete_folder(&file_path);
426
427 match result {
430 Ok(_) => assert!(!file_path.exists()),
431 Err(_) => {} }
433 }
434}