Skip to main content

putzen_cli/
lib.rs

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