1use jwalk::DirEntry;
45use rayon::iter::{IntoParallelRefIterator, ParallelBridge, ParallelIterator};
46use rusty_pool::ThreadPool;
47use std::{
48 collections::BTreeMap,
49 path::{Path, PathBuf},
50};
51use thiserror::Error;
52
53#[derive(Error, Debug)]
54pub enum RdError {
55 #[error("Failed to read metadata for {0}: {1}")]
56 MetadataError(PathBuf, std::io::Error),
57
58 #[error("Failed to set permissions for: {0}: {1}")]
59 PermissionError(PathBuf, std::io::Error),
60
61 #[error("Failed to remove item {0}: {1}")]
62 RemoveError(PathBuf, std::io::Error),
63
64 #[error("Failed to walk directory: {0}: {1}")]
65 WalkdirError(PathBuf, String),
66
67 #[error("IO error: {0}")]
68 Io(#[from] std::io::Error),
69}
70
71fn set_writable(path: &Path) -> Result<(), RdError> {
83 let mut perms = std::fs::metadata(path)
84 .map_err(|err| RdError::MetadataError(path.to_path_buf(), err))?
85 .permissions();
86
87 perms.set_readonly(false);
88 std::fs::set_permissions(path, perms)
89 .map_err(|err| RdError::PermissionError(path.to_path_buf(), err))?;
90
91 Ok(())
92}
93
94fn set_folder_writable(path: &Path) -> Result<(), RdError> {
109 let entries: Vec<DirEntry<((), ())>> = jwalk::WalkDir::new(&path)
110 .skip_hidden(false)
111 .into_iter()
112 .filter_map(|i| match i {
113 Ok(entry) if entry.file_type().is_file() => Some(Ok(entry)),
114 Ok(_) => None,
115 Err(e) => Some(Err(e)),
116 })
117 .collect::<Result<Vec<_>, _>>()
118 .map_err(|err| RdError::WalkdirError(path.to_path_buf(), err.to_string()))?;
119
120 let errors: Vec<_> = entries
121 .par_iter()
122 .filter_map(|entry| set_writable(&entry.path()).err())
123 .collect();
124
125 if let Some(err) = errors.into_iter().next() {
126 return Err(err);
127 }
128
129 Ok(())
130}
131
132pub fn delete_folder(dpath: &Path) -> Result<(), RdError> {
163 let mut tree: BTreeMap<u64, Vec<PathBuf>> = BTreeMap::new();
164
165 let entries: Vec<DirEntry<((), ())>> = jwalk::WalkDir::new(&dpath)
166 .skip_hidden(false)
167 .into_iter()
168 .par_bridge()
169 .filter_map(|i| match i {
170 Ok(entry) if entry.path().is_dir() => Some(Ok(entry)),
171 Ok(_) => None,
172 Err(e) => Some(Err(e)),
173 })
174 .collect::<Result<Vec<_>, _>>()
175 .map_err(|err| RdError::WalkdirError(dpath.to_path_buf(), err.to_string()))?;
176
177 for entry in entries {
178 tree.entry(entry.depth as u64)
179 .or_insert_with(Vec::new)
180 .push(entry.path());
181 }
182
183 let pool = ThreadPool::default();
184
185 let mut handles = vec![];
186
187 for (_, entries) in tree.into_iter().rev() {
188 handles.push(pool.evaluate(move || {
189 entries.par_iter().for_each(|entry| {
190 let _ = std::fs::remove_dir_all(entry);
191 });
192 }));
193 }
194
195 for handle in handles {
196 handle.await_complete();
197 }
198
199 if dpath.exists() {
200 set_folder_writable(&dpath)?;
202
203 std::fs::remove_dir_all(dpath)
204 .map_err(|err| RdError::RemoveError(dpath.to_path_buf(), err))?;
205 }
206
207 Ok(())
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use std::fs;
214 use tempfile::TempDir;
215
216 fn create_test_structure(base: &Path) -> std::io::Result<()> {
218 fs::create_dir_all(base.join("dir1/subdir1"))?;
220 fs::create_dir_all(base.join("dir1/subdir2"))?;
221 fs::create_dir_all(base.join("dir2"))?;
222
223 fs::write(base.join("file1.txt"), "content1")?;
225 fs::write(base.join("dir1/file2.txt"), "content2")?;
226 fs::write(base.join("dir1/subdir1/file3.txt"), "content3")?;
227 fs::write(base.join("dir2/file4.txt"), "content4")?;
228
229 Ok(())
230 }
231
232 fn make_readonly(path: &Path) -> std::io::Result<()> {
234 let mut perms = fs::metadata(path)?.permissions();
235 perms.set_readonly(true);
236 fs::set_permissions(path, perms)?;
237 Ok(())
238 }
239
240 #[test]
241 fn test_delete_empty_directory() {
242 let temp_dir = TempDir::new().unwrap();
243 let test_path = temp_dir.path().join("empty_dir");
244 fs::create_dir(&test_path).unwrap();
245
246 assert!(test_path.exists());
247 delete_folder(&test_path).unwrap();
248 assert!(!test_path.exists());
249 }
250
251 #[test]
252 fn test_delete_directory_with_files() {
253 let temp_dir = TempDir::new().unwrap();
254 let test_path = temp_dir.path().join("dir_with_files");
255 fs::create_dir(&test_path).unwrap();
256 fs::write(test_path.join("file1.txt"), "content").unwrap();
257 fs::write(test_path.join("file2.txt"), "content").unwrap();
258
259 assert!(test_path.exists());
260 delete_folder(&test_path).unwrap();
261 assert!(!test_path.exists());
262 }
263
264 #[test]
265 fn test_delete_nested_directory_structure() {
266 let temp_dir = TempDir::new().unwrap();
267 let test_path = temp_dir.path().join("nested");
268 create_test_structure(&test_path).unwrap();
269
270 assert!(test_path.exists());
271 assert!(test_path.join("dir1/subdir1/file3.txt").exists());
272
273 delete_folder(&test_path).unwrap();
274
275 assert!(!test_path.exists());
276 assert!(!test_path.join("dir1").exists());
277 }
278
279 #[test]
280 fn test_delete_directory_with_readonly_files() {
281 let temp_dir = TempDir::new().unwrap();
282 let test_path = temp_dir.path().join("readonly_test");
283 create_test_structure(&test_path).unwrap();
284
285 make_readonly(&test_path.join("file1.txt")).unwrap();
287 make_readonly(&test_path.join("dir1/file2.txt")).unwrap();
288
289 assert!(test_path.exists());
290 delete_folder(&test_path).unwrap();
291 assert!(!test_path.exists());
292 }
293
294 #[test]
295 fn test_delete_nonexistent_directory() {
296 let temp_dir = TempDir::new().unwrap();
297 let test_path = temp_dir.path().join("does_not_exist");
298
299 let result = delete_folder(&test_path);
301
302 match result {
305 Ok(_) => assert!(!test_path.exists()),
306 Err(RdError::WalkdirError(_, _)) => {}
307 Err(e) => panic!("Unexpected error: {:?}", e),
308 }
309 }
310
311 #[test]
312 fn test_delete_directory_with_symlinks() {
313 let temp_dir = TempDir::new().unwrap();
314
315 let external_dir = temp_dir.path().join("external");
317 fs::create_dir(&external_dir).unwrap();
318 fs::write(external_dir.join("important.txt"), "don't delete me").unwrap();
319
320 let test_path = temp_dir.path().join("with_symlink");
322 fs::create_dir(&test_path).unwrap();
323
324 #[cfg(unix)]
325 {
326 use std::os::unix::fs::symlink;
327 symlink(&external_dir, test_path.join("link_to_external")).unwrap();
328 }
329
330 #[cfg(windows)]
331 {
332 use std::os::windows::fs::symlink_dir;
333 symlink_dir(&external_dir, test_path.join("link_to_external")).unwrap();
334 }
335
336 delete_folder(&test_path).unwrap();
338
339 assert!(!test_path.exists());
341
342 assert!(external_dir.exists());
344 assert!(external_dir.join("important.txt").exists());
345 }
346
347 #[test]
348 fn test_set_writable_on_readonly_file() {
349 let temp_dir = TempDir::new().unwrap();
350 let file_path = temp_dir.path().join("readonly_file.txt");
351 fs::write(&file_path, "content").unwrap();
352
353 make_readonly(&file_path).unwrap();
355 let perms = fs::metadata(&file_path).unwrap().permissions();
356 assert!(perms.readonly());
357
358 set_writable(&file_path).unwrap();
360
361 let perms = fs::metadata(&file_path).unwrap().permissions();
362 assert!(!perms.readonly());
363 }
364
365 #[test]
366 fn test_delete_large_directory_structure() {
367 let temp_dir = TempDir::new().unwrap();
368 let test_path = temp_dir.path().join("large_structure");
369 fs::create_dir(&test_path).unwrap();
370
371 for i in 0..10 {
373 let dir = test_path.join(format!("dir_{}", i));
374 fs::create_dir(&dir).unwrap();
375
376 for j in 0..5 {
377 fs::write(dir.join(format!("file_{}.txt", j)), "content").unwrap();
378 }
379
380 let subdir = dir.join("subdir");
382 fs::create_dir(&subdir).unwrap();
383 for k in 0..3 {
384 fs::write(subdir.join(format!("subfile_{}.txt", k)), "content").unwrap();
385 }
386 }
387
388 assert!(test_path.exists());
389 delete_folder(&test_path).unwrap();
390 assert!(!test_path.exists());
391 }
392
393 #[test]
394 fn test_error_on_invalid_path() {
395 let temp_dir = TempDir::new().unwrap();
397 let file_path = temp_dir.path().join("not_a_dir.txt");
398 fs::write(&file_path, "content").unwrap();
399
400 let result = delete_folder(&file_path);
401
402 match result {
405 Ok(_) => assert!(!file_path.exists()),
406 Err(_) => {} }
408 }
409}