Skip to main content

putzen_cli/
lib.rs

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