1#[cfg(feature = "progressbar")]
45use indicatif::ProgressIterator;
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
73fn set_writable(path: &Path) -> Result<(), RdError> {
85 let mut perms = std::fs::metadata(path)
86 .map_err(|err| RdError::MetadataError(path.to_path_buf(), err))?
87 .permissions();
88
89 perms.set_readonly(false);
90 std::fs::set_permissions(path, perms)
91 .map_err(|err| RdError::PermissionError(path.to_path_buf(), err))?;
92
93 Ok(())
94}
95
96fn set_folder_writable(path: &Path) -> Result<(), RdError> {
111 let entries: Vec<DirEntry<((), ())>> = jwalk::WalkDir::new(&path)
112 .skip_hidden(false)
113 .into_iter()
114 .filter_map(|i| match i {
115 Ok(entry) if entry.file_type().is_file() => Some(Ok(entry)),
116 Ok(_) => None,
117 Err(e) => Some(Err(e)),
118 })
119 .collect::<Result<Vec<_>, _>>()
120 .map_err(|err| RdError::WalkdirError(path.to_path_buf(), err.to_string()))?;
121
122 let errors: Vec<_> = entries
123 .par_iter()
124 .filter_map(|entry| set_writable(&entry.path()).err())
125 .collect();
126
127 if let Some(err) = errors.into_iter().next() {
128 return Err(err);
129 }
130
131 Ok(())
132}
133
134pub fn delete_folder(dpath: &Path) -> Result<(), RdError> {
165 let mut tree: BTreeMap<u64, Vec<PathBuf>> = BTreeMap::new();
166
167 let entries: Vec<DirEntry<((), ())>> = jwalk::WalkDir::new(&dpath)
168 .skip_hidden(false)
169 .into_iter()
170 .par_bridge()
171 .filter_map(|i| match i {
172 Ok(entry) if entry.path().is_dir() => Some(Ok(entry)),
173 Ok(_) => None,
174 Err(e) => Some(Err(e)),
175 })
176 .collect::<Result<Vec<_>, _>>()
177 .map_err(|err| RdError::WalkdirError(dpath.to_path_buf(), err.to_string()))?;
178
179 for entry in entries {
180 tree.entry(entry.depth as u64)
181 .or_insert_with(Vec::new)
182 .push(entry.path());
183 }
184
185 let pool = ThreadPool::default();
186
187 let mut handles = vec![];
188
189 let itr = tree.into_iter().rev();
190
191 #[cfg(feature = "progressbar")]
192 let itr = itr.progress();
193
194 for (_, entries) in itr {
195 handles.push(pool.evaluate(move || {
196 entries.par_iter().for_each(|entry| {
197 let _ = std::fs::remove_dir_all(entry);
198 });
199 }));
200 }
201
202 for handle in handles {
203 handle.await_complete();
204 }
205
206 if dpath.exists() {
207 set_folder_writable(&dpath)?;
209
210 std::fs::remove_dir_all(dpath)
211 .map_err(|err| RdError::RemoveError(dpath.to_path_buf(), err))?;
212 }
213
214 Ok(())
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use std::fs;
221 use tempfile::TempDir;
222
223 fn create_test_structure(base: &Path) -> std::io::Result<()> {
225 fs::create_dir_all(base.join("dir1/subdir1"))?;
227 fs::create_dir_all(base.join("dir1/subdir2"))?;
228 fs::create_dir_all(base.join("dir2"))?;
229
230 fs::write(base.join("file1.txt"), "content1")?;
232 fs::write(base.join("dir1/file2.txt"), "content2")?;
233 fs::write(base.join("dir1/subdir1/file3.txt"), "content3")?;
234 fs::write(base.join("dir2/file4.txt"), "content4")?;
235
236 Ok(())
237 }
238
239 fn make_readonly(path: &Path) -> std::io::Result<()> {
241 let mut perms = fs::metadata(path)?.permissions();
242 perms.set_readonly(true);
243 fs::set_permissions(path, perms)?;
244 Ok(())
245 }
246
247 #[test]
248 fn test_delete_empty_directory() {
249 let temp_dir = TempDir::new().unwrap();
250 let test_path = temp_dir.path().join("empty_dir");
251 fs::create_dir(&test_path).unwrap();
252
253 assert!(test_path.exists());
254 delete_folder(&test_path).unwrap();
255 assert!(!test_path.exists());
256 }
257
258 #[test]
259 fn test_delete_directory_with_files() {
260 let temp_dir = TempDir::new().unwrap();
261 let test_path = temp_dir.path().join("dir_with_files");
262 fs::create_dir(&test_path).unwrap();
263 fs::write(test_path.join("file1.txt"), "content").unwrap();
264 fs::write(test_path.join("file2.txt"), "content").unwrap();
265
266 assert!(test_path.exists());
267 delete_folder(&test_path).unwrap();
268 assert!(!test_path.exists());
269 }
270
271 #[test]
272 fn test_delete_nested_directory_structure() {
273 let temp_dir = TempDir::new().unwrap();
274 let test_path = temp_dir.path().join("nested");
275 create_test_structure(&test_path).unwrap();
276
277 assert!(test_path.exists());
278 assert!(test_path.join("dir1/subdir1/file3.txt").exists());
279
280 delete_folder(&test_path).unwrap();
281
282 assert!(!test_path.exists());
283 assert!(!test_path.join("dir1").exists());
284 }
285
286 #[test]
287 fn test_delete_directory_with_readonly_files() {
288 let temp_dir = TempDir::new().unwrap();
289 let test_path = temp_dir.path().join("readonly_test");
290 create_test_structure(&test_path).unwrap();
291
292 make_readonly(&test_path.join("file1.txt")).unwrap();
294 make_readonly(&test_path.join("dir1/file2.txt")).unwrap();
295
296 assert!(test_path.exists());
297 delete_folder(&test_path).unwrap();
298 assert!(!test_path.exists());
299 }
300
301 #[test]
302 fn test_delete_nonexistent_directory() {
303 let temp_dir = TempDir::new().unwrap();
304 let test_path = temp_dir.path().join("does_not_exist");
305
306 let result = delete_folder(&test_path);
308
309 match result {
312 Ok(_) => assert!(!test_path.exists()),
313 Err(RdError::WalkdirError(_, _)) => {}
314 Err(e) => panic!("Unexpected error: {:?}", e),
315 }
316 }
317
318 #[test]
319 fn test_delete_directory_with_symlinks() {
320 let temp_dir = TempDir::new().unwrap();
321
322 let external_dir = temp_dir.path().join("external");
324 fs::create_dir(&external_dir).unwrap();
325 fs::write(external_dir.join("important.txt"), "don't delete me").unwrap();
326
327 let test_path = temp_dir.path().join("with_symlink");
329 fs::create_dir(&test_path).unwrap();
330
331 #[cfg(unix)]
332 {
333 use std::os::unix::fs::symlink;
334 symlink(&external_dir, test_path.join("link_to_external")).unwrap();
335 }
336
337 #[cfg(windows)]
338 {
339 use std::os::windows::fs::symlink_dir;
340 symlink_dir(&external_dir, test_path.join("link_to_external")).unwrap();
341 }
342
343 delete_folder(&test_path).unwrap();
345
346 assert!(!test_path.exists());
348
349 assert!(external_dir.exists());
351 assert!(external_dir.join("important.txt").exists());
352 }
353
354 #[test]
355 fn test_set_writable_on_readonly_file() {
356 let temp_dir = TempDir::new().unwrap();
357 let file_path = temp_dir.path().join("readonly_file.txt");
358 fs::write(&file_path, "content").unwrap();
359
360 make_readonly(&file_path).unwrap();
362 let perms = fs::metadata(&file_path).unwrap().permissions();
363 assert!(perms.readonly());
364
365 set_writable(&file_path).unwrap();
367
368 let perms = fs::metadata(&file_path).unwrap().permissions();
369 assert!(!perms.readonly());
370 }
371
372 #[test]
373 fn test_delete_large_directory_structure() {
374 let temp_dir = TempDir::new().unwrap();
375 let test_path = temp_dir.path().join("large_structure");
376 fs::create_dir(&test_path).unwrap();
377
378 for i in 0..10 {
380 let dir = test_path.join(format!("dir_{}", i));
381 fs::create_dir(&dir).unwrap();
382
383 for j in 0..5 {
384 fs::write(dir.join(format!("file_{}.txt", j)), "content").unwrap();
385 }
386
387 let subdir = dir.join("subdir");
389 fs::create_dir(&subdir).unwrap();
390 for k in 0..3 {
391 fs::write(subdir.join(format!("subfile_{}.txt", k)), "content").unwrap();
392 }
393 }
394
395 assert!(test_path.exists());
396 delete_folder(&test_path).unwrap();
397 assert!(!test_path.exists());
398 }
399
400 #[test]
401 fn test_error_on_invalid_path() {
402 let temp_dir = TempDir::new().unwrap();
404 let file_path = temp_dir.path().join("not_a_dir.txt");
405 fs::write(&file_path, "content").unwrap();
406
407 let result = delete_folder(&file_path);
408
409 match result {
412 Ok(_) => assert!(!file_path.exists()),
413 Err(_) => {} }
415 }
416}