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};
12
13pub struct FileToFolderMatch {
14 file_to_check: &'static str,
15 folder_to_remove: &'static str,
16}
17
18pub enum FolderProcessed {
19 Cleaned(usize),
20 Skipped,
21 Abort,
22}
23
24impl FileToFolderMatch {
25 pub const fn new(file_to_check: &'static str, folder_to_remove: &'static str) -> Self {
26 Self {
27 file_to_check,
28 folder_to_remove,
29 }
30 }
31
32 pub fn path_to_remove(&self, folder: impl AsRef<Path>) -> Option<impl AsRef<Path>> {
34 folder
35 .as_ref()
36 .canonicalize()
37 .map(|x| x.join(self.folder_to_remove))
38 .ok()
39 }
40}
41
42#[derive(Debug)]
43pub struct Folder(PathBuf);
44
45impl Folder {
46 pub fn accept(
47 &self,
48 ctx: &DecisionContext,
49 rule: &FileToFolderMatch,
50 cleaner: &impl DoCleanUp,
51 decider: &mut impl Decide,
52 ) -> Result<FolderProcessed> {
53 if let Ok(folder_to_remove) = rule.resolve_path_to_remove(self) {
54 let size_amount = folder_to_remove.calculate_size();
55 let size = size_amount.as_human_readable();
56 println!("{} ({})", folder_to_remove, size);
57 println!(
58 " ├─ because of {}",
59 PathBuf::from("..").join(rule.file_to_check).display()
60 );
61
62 let result = match decider.obtain_decision(ctx, "├─ delete directory recursively?")
63 {
64 Ok(Decision::Yes) => match cleaner.do_cleanup(folder_to_remove)? {
65 Clean::Cleaned => {
66 println!(" └─ deleted {}", size);
67 FolderProcessed::Cleaned(size_amount)
68 }
69 Clean::NotCleaned => {
70 println!(
71 " └─ not deleted{}{}",
72 if ctx.is_dry_run { " [dry-run] " } else { "" },
73 size
74 );
75 FolderProcessed::Skipped
76 }
77 },
78 Ok(Decision::Quit) => {
79 println!(" └─ quiting");
80 FolderProcessed::Abort
81 }
82 _ => {
83 println!(" └─ skipped");
84 FolderProcessed::Skipped
85 }
86 };
87 println!();
88 Ok(result)
89 } else {
90 Err(Error::from(ErrorKind::Unsupported))
91 }
92 }
93
94 fn calculate_size(&self) -> usize {
95 jwalk::WalkDirGeneric::<((), Option<usize>)>::new(self.0.as_path())
96 .skip_hidden(false)
97 .follow_links(false)
98 .parallelism(Parallelism::RayonNewPool(0))
99 .process_read_dir(|_, _, _, dir_entry_results| {
100 dir_entry_results.iter_mut().for_each(|dir_entry_result| {
101 if let Ok(dir_entry) = dir_entry_result {
102 if !dir_entry.file_type.is_dir() {
103 dir_entry.client_state = Some(
104 dir_entry
105 .metadata()
106 .map(|m| m.len() as usize)
107 .unwrap_or_default(),
108 );
109 }
110 }
111 })
112 })
113 .into_iter()
114 .filter(|f| f.is_ok())
115 .filter_map(|e| e.unwrap().client_state)
116 .sum()
117 }
118}
119
120impl Display for Folder {
121 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
122 write!(f, "{}", self.0.display())
123 }
124}
125
126impl<A: ClientState> TryFrom<DirEntry<A>> for Folder {
127 type Error = std::io::Error;
128
129 fn try_from(value: DirEntry<A>) -> std::result::Result<Self, Self::Error> {
130 let path = value.path();
131 path.try_into() }
133}
134
135impl TryFrom<PathBuf> for Folder {
136 type Error = std::io::Error;
137
138 fn try_from(path: PathBuf) -> std::result::Result<Self, Self::Error> {
139 if !path.is_dir() || path.eq(Path::new(".")) || path.eq(Path::new("..")) {
140 Err(Error::from(ErrorKind::Unsupported))
141 } else {
142 let p = path.canonicalize()?;
143 Ok(Self(p))
144 }
145 }
146}
147
148impl TryFrom<&str> for Folder {
149 type Error = std::io::Error;
150
151 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
152 Folder::try_from(PathBuf::from(value))
153 }
154}
155
156impl AsRef<Path> for Folder {
157 fn as_ref(&self) -> &Path {
158 self.0.as_ref()
159 }
160}
161
162pub trait PathToRemoveResolver {
163 fn resolve_path_to_remove(&self, folder: impl AsRef<Path>) -> Result<Folder>;
164}
165
166impl PathToRemoveResolver for FileToFolderMatch {
167 fn resolve_path_to_remove(&self, folder: impl AsRef<Path>) -> Result<Folder> {
168 let folder = folder.as_ref();
169 let file_to_check = folder.join(self.file_to_check);
170
171 if file_to_check.exists() {
172 let path_to_remove = folder.join(self.folder_to_remove);
173 if path_to_remove.exists() {
174 return path_to_remove.try_into();
175 }
176 }
177
178 Err(Error::from(ErrorKind::Unsupported))
179 }
180}
181
182pub trait HumanReadable {
183 fn as_human_readable(&self) -> String;
184}
185
186impl HumanReadable for usize {
187 fn as_human_readable(&self) -> String {
188 const KIBIBYTE: usize = 1024;
189 const MEBIBYTE: usize = KIBIBYTE << 10;
190 const GIBIBYTE: usize = MEBIBYTE << 10;
191 const TEBIBYTE: usize = GIBIBYTE << 10;
192 const PEBIBYTE: usize = TEBIBYTE << 10;
193 const EXBIBYTE: usize = PEBIBYTE << 10;
194
195 let size = *self;
196 let (size, symbol) = match size {
197 size if size < KIBIBYTE => (size as f64, "B"),
198 size if size < MEBIBYTE => (size as f64 / KIBIBYTE as f64, "KiB"),
199 size if size < GIBIBYTE => (size as f64 / MEBIBYTE as f64, "MiB"),
200 size if size < TEBIBYTE => (size as f64 / GIBIBYTE as f64, "GiB"),
201 size if size < PEBIBYTE => (size as f64 / TEBIBYTE as f64, "TiB"),
202 size if size < EXBIBYTE => (size as f64 / PEBIBYTE as f64, "PiB"),
203 _ => (size as f64 / EXBIBYTE as f64, "EiB"),
204 };
205
206 format!("{:.1}{}", size, symbol)
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn should_tell_if_folder_is_to_remove() {
216 let rule = FileToFolderMatch::new("Cargo.toml", "target");
217 let folder = Folder::try_from(Path::new("..").join("..")).unwrap();
218
219 assert!(rule.resolve_path_to_remove(folder).is_err());
220
221 let folder = Folder::try_from(Path::new(".").canonicalize().unwrap()).unwrap();
222 assert!(rule.resolve_path_to_remove(folder).is_ok());
223 }
224
225 #[test]
226 fn should_size() {
227 assert_eq!(1_048_576, 1024 << 10);
228 }
229}