putzen_cli/
lib.rs

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    /// builds the absolut path, that is to be removed, in the given folder
33    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() // see below..
132    }
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}