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 #[cfg(feature = "progressbar")]
195 let pb = ProgressBar::new(entries.len() as u64);
196
197 for entry in entries {
198 tree.entry(entry.depth as u64)
199 .or_insert_with(Vec::new)
200 .push(entry.path());
201 }
202
203 let pool = ThreadPool::default();
204
205 let mut handles = vec![];
206
207 for (_, entries) in tree.into_iter().rev() {
208 #[cfg(feature = "progressbar")]
209 let pb = pb.clone();
210 handles.push(pool.evaluate(move || {
211 entries.par_iter().for_each(|entry| {
212 let _ = std::fs::remove_dir_all(entry);
213 #[cfg(feature = "progressbar")]
214 pb.inc(1);
215 });
216 }));
217 }
218
219 for handle in handles {
220 handle.await_complete();
221 }
222
223 if dpath.exists() {
224 set_folder_writable(&dpath)?;
226
227 std::fs::remove_dir_all(dpath)
228 .map_err(|err| RdError::RemoveError(dpath.to_path_buf(), err))?;
229 }
230
231 #[cfg(feature = "progressbar")]
232 pb.finish();
233
234 Ok(())
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use std::fs;
241 use tempfile::TempDir;
242
243 fn create_test_structure(base: &Path) -> std::io::Result<()> {
245 fs::create_dir_all(base.join("dir1/subdir1"))?;
247 fs::create_dir_all(base.join("dir1/subdir2"))?;
248 fs::create_dir_all(base.join("dir2"))?;
249
250 fs::write(base.join("file1.txt"), "content1")?;
252 fs::write(base.join("dir1/file2.txt"), "content2")?;
253 fs::write(base.join("dir1/subdir1/file3.txt"), "content3")?;
254 fs::write(base.join("dir2/file4.txt"), "content4")?;
255
256 Ok(())
257 }
258
259 fn make_readonly(path: &Path) -> std::io::Result<()> {
261 let mut perms = fs::metadata(path)?.permissions();
262 perms.set_readonly(true);
263 fs::set_permissions(path, perms)?;
264 Ok(())
265 }
266
267 #[test]
268 fn test_delete_empty_directory() {
269 let temp_dir = TempDir::new().unwrap();
270 let test_path = temp_dir.path().join("empty_dir");
271 fs::create_dir(&test_path).unwrap();
272
273 assert!(test_path.exists());
274 delete_folder(&test_path).unwrap();
275 assert!(!test_path.exists());
276 }
277
278 #[test]
279 fn test_delete_directory_with_files() {
280 let temp_dir = TempDir::new().unwrap();
281 let test_path = temp_dir.path().join("dir_with_files");
282 fs::create_dir(&test_path).unwrap();
283 fs::write(test_path.join("file1.txt"), "content").unwrap();
284 fs::write(test_path.join("file2.txt"), "content").unwrap();
285
286 assert!(test_path.exists());
287 delete_folder(&test_path).unwrap();
288 assert!(!test_path.exists());
289 }
290
291 #[test]
292 fn test_delete_nested_directory_structure() {
293 let temp_dir = TempDir::new().unwrap();
294 let test_path = temp_dir.path().join("nested");
295 create_test_structure(&test_path).unwrap();
296
297 assert!(test_path.exists());
298 assert!(test_path.join("dir1/subdir1/file3.txt").exists());
299
300 delete_folder(&test_path).unwrap();
301
302 assert!(!test_path.exists());
303 assert!(!test_path.join("dir1").exists());
304 }
305
306 #[test]
307 fn test_delete_directory_with_readonly_files() {
308 let temp_dir = TempDir::new().unwrap();
309 let test_path = temp_dir.path().join("readonly_test");
310 create_test_structure(&test_path).unwrap();
311
312 make_readonly(&test_path.join("file1.txt")).unwrap();
314 make_readonly(&test_path.join("dir1/file2.txt")).unwrap();
315
316 assert!(test_path.exists());
317 delete_folder(&test_path).unwrap();
318 assert!(!test_path.exists());
319 }
320
321 #[test]
322 fn test_delete_nonexistent_directory() {
323 let temp_dir = TempDir::new().unwrap();
324 let test_path = temp_dir.path().join("does_not_exist");
325
326 let result = delete_folder(&test_path);
328
329 match result {
332 Ok(_) => assert!(!test_path.exists()),
333 Err(RdError::WalkdirError(_, _)) => {}
334 Err(e) => panic!("Unexpected error: {:?}", e),
335 }
336 }
337
338 #[test]
339 fn test_delete_directory_with_symlinks() {
340 let temp_dir = TempDir::new().unwrap();
341
342 let external_dir = temp_dir.path().join("external");
344 fs::create_dir(&external_dir).unwrap();
345 fs::write(external_dir.join("important.txt"), "don't delete me").unwrap();
346
347 let test_path = temp_dir.path().join("with_symlink");
349 fs::create_dir(&test_path).unwrap();
350
351 #[cfg(unix)]
352 {
353 use std::os::unix::fs::symlink;
354 symlink(&external_dir, test_path.join("link_to_external")).unwrap();
355 }
356
357 #[cfg(windows)]
358 {
359 use std::os::windows::fs::symlink_dir;
360 symlink_dir(&external_dir, test_path.join("link_to_external")).unwrap();
361 }
362
363 delete_folder(&test_path).unwrap();
365
366 assert!(!test_path.exists());
368
369 assert!(external_dir.exists());
371 assert!(external_dir.join("important.txt").exists());
372 }
373
374 #[test]
375 fn test_set_writable_on_readonly_file() {
376 let temp_dir = TempDir::new().unwrap();
377 let file_path = temp_dir.path().join("readonly_file.txt");
378 fs::write(&file_path, "content").unwrap();
379
380 make_readonly(&file_path).unwrap();
382 let perms = fs::metadata(&file_path).unwrap().permissions();
383 assert!(perms.readonly());
384
385 set_writable(&file_path).unwrap();
387
388 let perms = fs::metadata(&file_path).unwrap().permissions();
389 assert!(!perms.readonly());
390 }
391
392 #[test]
393 fn test_delete_large_directory_structure() {
394 let temp_dir = TempDir::new().unwrap();
395 let test_path = temp_dir.path().join("large_structure");
396 fs::create_dir(&test_path).unwrap();
397
398 for i in 0..10 {
400 let dir = test_path.join(format!("dir_{}", i));
401 fs::create_dir(&dir).unwrap();
402
403 for j in 0..5 {
404 fs::write(dir.join(format!("file_{}.txt", j)), "content").unwrap();
405 }
406
407 let subdir = dir.join("subdir");
409 fs::create_dir(&subdir).unwrap();
410 for k in 0..3 {
411 fs::write(subdir.join(format!("subfile_{}.txt", k)), "content").unwrap();
412 }
413 }
414
415 assert!(test_path.exists());
416 delete_folder(&test_path).unwrap();
417 assert!(!test_path.exists());
418 }
419
420 #[test]
421 fn test_error_on_invalid_path() {
422 let temp_dir = TempDir::new().unwrap();
424 let file_path = temp_dir.path().join("not_a_dir.txt");
425 fs::write(&file_path, "content").unwrap();
426
427 let result = delete_folder(&file_path);
428
429 match result {
432 Ok(_) => assert!(!file_path.exists()),
433 Err(_) => {} }
435 }
436}