1mod cleaner;
2mod decider;
3
4pub use crate::cleaner::*;
5pub use crate::decider::*;
6
7use jwalk::{ClientState, DirEntry, Parallelism};
8use std::convert::{TryFrom, TryInto};
9use std::fmt::{Display, Formatter};
10use std::io::{Error, ErrorKind, Result};
11use std::path::{Path, PathBuf};
12use std::time::Duration;
13
14pub struct FileToFolderMatch {
15 file_to_check: &'static str,
16 folder_to_remove: &'static str,
17}
18
19pub enum FolderProcessed {
20 Cleaned(usize),
22 NoRuleMatch,
24 Skipped,
26 Abort,
28}
29
30impl FileToFolderMatch {
31 pub const fn new(file_to_check: &'static str, folder_to_remove: &'static str) -> Self {
32 Self {
33 file_to_check,
34 folder_to_remove,
35 }
36 }
37
38 pub fn path_to_remove(&self, folder: impl AsRef<Path>) -> Option<impl AsRef<Path>> {
40 folder
41 .as_ref()
42 .canonicalize()
43 .map(|x| x.join(self.folder_to_remove))
44 .ok()
45 }
46}
47
48#[derive(Debug, Eq, PartialEq, Hash)]
49pub struct Folder(PathBuf);
50
51impl Folder {
52 pub fn accept(
53 &self,
54 ctx: &DecisionContext,
55 rule: &FileToFolderMatch,
56 cleaner: &dyn DoCleanUp,
57 decider: &mut impl Decide,
58 ) -> Result<FolderProcessed> {
59 if !rule.is_folder_to_remove(self) {
61 return Ok(FolderProcessed::NoRuleMatch);
62 }
63
64 let size_amount = self.calculate_size();
65 let size = size_amount.as_human_readable();
66 let folder = self.as_ref().display().to_string();
67 let folder = ctx
68 .working_dir
69 .components()
70 .take(ctx.working_dir.components().count() - 1)
71 .fold(folder, |acc, component| {
72 if let Some(s) = component.as_os_str().to_str() {
74 acc.replace(s, s.chars().next().unwrap_or(' ').to_string().as_str())
75 } else {
76 acc
77 }
78 });
79
80 ctx.println(format!("Cleaning {folder} with {size}"));
81 ctx.println(format!(
82 " ├─ because of {}",
83 PathBuf::from("..").join(rule.file_to_check).display()
84 ));
85
86 let result = match decider.obtain_decision(ctx, "├─ delete directory recursively?") {
87 Ok(Decision::Yes) => match cleaner.do_cleanup(self.as_ref())? {
88 Clean::Cleaned => {
89 ctx.println(format!(" └─ deleted {size}"));
90 FolderProcessed::Cleaned(size_amount)
91 }
92 Clean::NotCleaned => {
93 ctx.println(format!(
94 " └─ not deleted{}{size}",
95 if ctx.is_dry_run { " [dry-run] " } else { "" }
96 ));
97 FolderProcessed::Skipped
98 }
99 },
100 Ok(Decision::Quit) => {
101 ctx.println(" └─ quiting");
102 FolderProcessed::Abort
103 }
104 _ => {
105 ctx.println(" └─ skipped");
106 FolderProcessed::Skipped
107 }
108 };
109 ctx.println("");
110 Ok(result)
111 }
112
113 fn calculate_size(&self) -> usize {
114 jwalk::WalkDirGeneric::<((), Option<usize>)>::new(self.as_ref())
115 .skip_hidden(false)
116 .follow_links(false)
117 .parallelism(Parallelism::RayonDefaultPool {
118 busy_timeout: Duration::from_secs(60),
119 })
120 .process_read_dir(|_, _, _, dir_entry_results| {
121 dir_entry_results.iter_mut().for_each(|dir_entry_result| {
122 if let Ok(dir_entry) = dir_entry_result {
123 if !dir_entry.file_type.is_dir() {
124 dir_entry.client_state = Some(
125 dir_entry
126 .metadata()
127 .map(|m| m.len() as usize)
128 .unwrap_or_default(),
129 );
130 }
131 }
132 })
133 })
134 .into_iter()
135 .filter_map(|f| f.ok())
136 .filter_map(|e| e.client_state)
137 .sum()
138 }
139}
140
141impl Display for Folder {
142 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
143 write!(f, "{}", self.0.display())
144 }
145}
146
147impl<A: ClientState> TryFrom<DirEntry<A>> for Folder {
148 type Error = std::io::Error;
149
150 fn try_from(value: DirEntry<A>) -> std::result::Result<Self, Self::Error> {
151 let path = value.path();
152 path.try_into() }
154}
155
156impl TryFrom<PathBuf> for Folder {
157 type Error = std::io::Error;
158
159 fn try_from(path: PathBuf) -> std::result::Result<Self, Self::Error> {
160 if !path.is_dir() || path.eq(Path::new(".")) || path.eq(Path::new("..")) {
161 Err(Error::from(ErrorKind::Unsupported))
162 } else {
163 let p = path.canonicalize()?;
164 Ok(Self(p))
165 }
166 }
167}
168
169impl TryFrom<&str> for Folder {
170 type Error = std::io::Error;
171
172 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
173 Folder::try_from(PathBuf::from(value))
174 }
175}
176
177impl AsRef<Path> for Folder {
178 fn as_ref(&self) -> &Path {
179 self.0.as_ref()
180 }
181}
182
183#[deprecated(since = "2.0.0", note = "use trait `IsFolderToRemove` instead")]
184pub trait PathToRemoveResolver {
185 fn resolve_path_to_remove(&self, folder: impl AsRef<Path>) -> Result<Folder>;
186}
187
188#[allow(deprecated)]
189impl PathToRemoveResolver for FileToFolderMatch {
190 fn resolve_path_to_remove(&self, folder: impl AsRef<Path>) -> Result<Folder> {
191 let folder = folder.as_ref();
192 let file_to_check = folder.join(self.file_to_check);
193
194 if file_to_check.exists() {
195 let path_to_remove = folder.join(self.folder_to_remove);
196 if path_to_remove.exists() {
197 return path_to_remove.try_into();
198 }
199 }
200
201 Err(Error::from(ErrorKind::Unsupported))
202 }
203}
204
205pub trait IsFolderToRemove {
211 fn is_folder_to_remove(&self, folder: &Folder) -> bool;
212}
213
214impl IsFolderToRemove for FileToFolderMatch {
215 fn is_folder_to_remove(&self, folder: &Folder) -> bool {
216 folder.as_ref().parent().map_or_else(
217 || false,
218 |parent| {
219 parent.join(self.file_to_check).exists()
220 && parent
221 .join(self.folder_to_remove)
222 .starts_with(folder.as_ref())
223 },
224 )
225 }
226}
227
228pub trait HumanReadable {
229 fn as_human_readable(&self) -> String;
230}
231
232impl HumanReadable for usize {
233 fn as_human_readable(&self) -> String {
234 const KIBIBYTE: usize = 1024;
235 const MEBIBYTE: usize = KIBIBYTE << 10;
236 const GIBIBYTE: usize = MEBIBYTE << 10;
237 const TEBIBYTE: usize = GIBIBYTE << 10;
238 const PEBIBYTE: usize = TEBIBYTE << 10;
239 const EXBIBYTE: usize = PEBIBYTE << 10;
240
241 let size = *self;
242 let (size, symbol) = match size {
243 size if size < KIBIBYTE => (size as f64, "B"),
244 size if size < MEBIBYTE => (size as f64 / KIBIBYTE as f64, "KiB"),
245 size if size < GIBIBYTE => (size as f64 / MEBIBYTE as f64, "MiB"),
246 size if size < TEBIBYTE => (size as f64 / GIBIBYTE as f64, "GiB"),
247 size if size < PEBIBYTE => (size as f64 / TEBIBYTE as f64, "TiB"),
248 size if size < EXBIBYTE => (size as f64 / PEBIBYTE as f64, "PiB"),
249 _ => (size as f64 / EXBIBYTE as f64, "EiB"),
250 };
251
252 format!("{size:.1}{symbol}")
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn should_size() {
262 assert_eq!(1_048_576, 1024 << 10);
263 }
264
265 #[test]
266 fn test_trait_is_folder_to_remove() {
267 let rule = FileToFolderMatch::new("Cargo.toml", "target");
268
269 let target_folder =
270 Folder::try_from(Path::new(".").canonicalize().unwrap().join("target")).unwrap();
271 assert!(rule.is_folder_to_remove(&target_folder));
272
273 let crate_root_folder = Folder::try_from(Path::new(".").canonicalize().unwrap()).unwrap();
274 assert!(!rule.is_folder_to_remove(&crate_root_folder));
275 }
276}