Skip to main content

src/emptydir.rs

1use std::fs;
2use std::path::Path;
4use crate::can_be_deleted::DeleteDecision;
5use colored::*;
6use walkdir::WalkDir;
8#[derive(Debug, PartialEq)]
9pub struct EmptydirResult {
10 pub count_deleted: u32,
11 pub count_errors: u32,
14/// Recurse through a given root directory, and delete any "empty" directories.
15///
16/// Returns the number of directories deleted.
17///
18pub fn emptydir(root: &Path) -> EmptydirResult {
19 let directories_to_delete = WalkDir::new(root)
20 .contents_first(true)
21 .into_iter()
22 .filter_map(|e| e.ok())
23 .filter(|e| e.file_type().is_dir())
24 .filter(|e| {
25 matches!(
26 crate::can_be_deleted::can_be_deleted(e.path()),
27 DeleteDecision::CanDelete
28 )
29 });
31 let mut count_deleted: u32 = 0;
32 let mut count_errors: u32 = 0;
34 for dir in directories_to_delete {
35 match fs::remove_dir_all(dir.path()) {
36 Ok(_) => {
37 println!("{}", dir.path().display());
38 count_deleted += 1;
39 }
40 Err(e) => {
41 let message = format!(
42 "Tried to delete {}, but got error: {}",
43 dir.path().display(),
44 e
45 );
46 eprintln!("{}", message.red());
47 count_errors += 1;
48 }
49 };
50 }
52 // Now work our way upward through the parent directories, and
53 // delete any of those which are empty.
54 let mut current_parent = root.parent();
56 while let Some(parent) = current_parent {
57 if !matches!(
58 crate::can_be_deleted::can_be_deleted(parent),
59 DeleteDecision::CanDelete
60 ) {
61 break;
62 }
64 match fs::remove_dir_all(parent) {
65 Ok(_) => {
66 println!("{}", parent.display());
67 count_deleted += 1;
68 }
69 Err(e) => {
70 let message = format!("Tried to delete {}, but got error: {}", parent.display(), e);
71 eprintln!("{}", message.red());
72 count_errors += 1;
73 }
74 };
76 current_parent = parent.parent();
77 }
79 EmptydirResult {
80 count_deleted,
81 count_errors,
82 }
85#[cfg(test)]
86mod test_emptydir {
87 use std::fs;
88 use std::path::{Path, PathBuf};
90 use super::*;
92 fn test_dir() -> PathBuf {
93 let tmp_dir = tempfile::tempdir().unwrap();
94 let path = tmp_dir.path();
95 path.to_owned()
96 }
98 fn create_dir(dir: &PathBuf) {
99 fs::create_dir_all(dir).unwrap();
100 }
102 fn create_file(path: &PathBuf) {
103 create_dir(&path.parent().unwrap().to_path_buf());
104 fs::write(&path, "this file is for testing").unwrap();
105 }
107 #[test]
108 fn it_doesnt_delete_a_non_existent_directory() {
109 let dir = Path::new("/does/not/exist");
110 assert_eq!(
111 emptydir(dir),
112 EmptydirResult {
113 count_deleted: 0,
114 count_errors: 0
115 }
116 );
117 }
119 #[test]
120 fn it_deletes_an_empty_dir() {
121 let dir = test_dir();
123 // Create the directory, but don't put anything in it
124 create_dir(&dir);
126 assert_eq!(
127 emptydir(&dir),
128 EmptydirResult {
129 count_deleted: 1,
130 count_errors: 0
131 }
132 );
133 assert_eq!(dir.exists(), false);
134 }
136 #[test]
137 fn it_ignores_a_dir_with_extra_entries() {
138 let dir = test_dir();
140 // Create the directory, then add a text file
141 create_dir(&dir);
143 create_file(&dir.join("greeting.txt"));
145 assert_eq!(
146 emptydir(&dir),
147 EmptydirResult {
148 count_deleted: 0,
149 count_errors: 0
150 }
151 );
152 assert_eq!(dir.exists(), true);
153 assert_eq!(dir.join("greeting.txt").exists(), true);
154 }
156 #[test]
157 fn it_deletes_a_dir_with_only_safe_to_delete_entries() {
158 let dir = test_dir();
160 // .
161 // ├─ .ipynb_checkpoints/
162 // │ └─ analysis-checkpoint.ipynb
163 // │
164 // ├─ .venv/
165 // │ └─ bin/
166 // │ └─ mypython.py
167 // │
168 // ├─ __pycache__
169 // │ └─ myfile.pyc
170 // │
171 // └─ .DS_Store
172 //
173 create_dir(&dir);
175 create_dir(&dir.join(".venv"));
176 create_file(&dir.join(".venv/bin/mypython.py"));
178 create_dir(&dir.join(".ipynb_checkpoints"));
179 create_file(&dir.join(".ipynb_checkpoints/analysis-checkpoint.ipynb"));
181 create_dir(&dir.join("__pycache__"));
182 create_file(&dir.join("__pycache__/myfile.pyc"));
184 create_file(&dir.join(".DS_Store"));
186 assert_eq!(
187 emptydir(&dir),
188 EmptydirResult {
189 count_deleted: 1,
190 count_errors: 0
191 }
192 );
193 assert_eq!(dir.exists(), false);
194 }
196 #[test]
197 fn it_ignores_a_dir_with_a_mix_of_safe_and_unsafe_entries() {
198 let dir = test_dir();
200 create_dir(&dir);
202 create_file(&dir.join(".DS_Store"));
203 create_file(&dir.join("greeting.txt"));
205 assert_eq!(
206 emptydir(&dir),
207 EmptydirResult {
208 count_deleted: 0,
209 count_errors: 0
210 }
211 );
212 assert!(dir.exists());
213 assert!(dir.join("greeting.txt").exists());
214 }
216 #[test]
217 fn it_deletes_a_subdir_with_only_safe_to_delete_entries() {
218 let dir = test_dir();
219 let subdir = dir.join("subdir");
221 // .
222 // ├─ subdir/
223 // │ ├─ .venv/
224 // │ │ └─ bin/
225 // │ │ └─ mypython.py
226 // │ │
227 // │ ├─ __pycache__
228 // │ │ └─ myfile.pyc
229 // │ │
230 // │ └─ .DS_Store
231 // │
232 // └─ greeting.txt
233 //
234 create_dir(&subdir);
236 create_dir(&subdir.join(".venv"));
237 create_file(&subdir.join(".venv/bin/mypython.py"));
239 create_dir(&subdir.join("__pycache__"));
240 create_file(&subdir.join("__pycache__/myfile.pyc"));
242 create_file(&subdir.join(".DS_Store"));
244 create_file(&dir.join("greeting.txt"));
246 assert_eq!(
247 emptydir(&dir),
248 EmptydirResult {
249 count_deleted: 1,
250 count_errors: 0
251 }
252 );
253 assert_eq!(dir.exists(), true);
254 assert_eq!(subdir.exists(), false);
255 assert!(dir.join("greeting.txt").exists());
256 }