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};
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    /// The folder was cleaned and the amount of bytes removed is given
21    Cleaned(usize),
22    /// The folder was not cleaned because it did not match any rule
23    NoRuleMatch,
24    /// The folder was skipped, e.g. user decided to skip it
25    Skipped,
26    /// The folder was aborted, e.g. user decided to abort the whole process
27    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    /// builds the absolut path, that is to be removed, in the given folder
39    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        // better double check here
60        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        println!("{} ({})", self, size);
67        println!(
68            "  ├─ because of {}",
69            PathBuf::from("..").join(rule.file_to_check).display()
70        );
71
72        let result = match decider.obtain_decision(ctx, "├─ delete directory recursively?") {
73            Ok(Decision::Yes) => match cleaner.do_cleanup(self.as_ref())? {
74                Clean::Cleaned => {
75                    println!("  └─ deleted {}", size);
76                    FolderProcessed::Cleaned(size_amount)
77                }
78                Clean::NotCleaned => {
79                    println!(
80                        "  └─ not deleted{}{}",
81                        if ctx.is_dry_run { " [dry-run] " } else { "" },
82                        size
83                    );
84                    FolderProcessed::Skipped
85                }
86            },
87            Ok(Decision::Quit) => {
88                println!("  └─ quiting");
89                FolderProcessed::Abort
90            }
91            _ => {
92                println!("  └─ skipped");
93                FolderProcessed::Skipped
94            }
95        };
96        println!();
97        Ok(result)
98    }
99
100    fn calculate_size(&self) -> usize {
101        jwalk::WalkDirGeneric::<((), Option<usize>)>::new(self.as_ref())
102            .skip_hidden(false)
103            .follow_links(false)
104            .parallelism(Parallelism::RayonDefaultPool {
105                busy_timeout: Duration::from_secs(60),
106            })
107            .process_read_dir(|_, _, _, dir_entry_results| {
108                dir_entry_results.iter_mut().for_each(|dir_entry_result| {
109                    if let Ok(dir_entry) = dir_entry_result {
110                        if !dir_entry.file_type.is_dir() {
111                            dir_entry.client_state = Some(
112                                dir_entry
113                                    .metadata()
114                                    .map(|m| m.len() as usize)
115                                    .unwrap_or_default(),
116                            );
117                        }
118                    }
119                })
120            })
121            .into_iter()
122            .filter_map(|f| f.ok())
123            .filter_map(|e| e.client_state)
124            .sum()
125    }
126}
127
128impl Display for Folder {
129    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
130        write!(f, "{}", self.0.display())
131    }
132}
133
134impl<A: ClientState> TryFrom<DirEntry<A>> for Folder {
135    type Error = std::io::Error;
136
137    fn try_from(value: DirEntry<A>) -> std::result::Result<Self, Self::Error> {
138        let path = value.path();
139        path.try_into() // see below..
140    }
141}
142
143impl TryFrom<PathBuf> for Folder {
144    type Error = std::io::Error;
145
146    fn try_from(path: PathBuf) -> std::result::Result<Self, Self::Error> {
147        if !path.is_dir() || path.eq(Path::new(".")) || path.eq(Path::new("..")) {
148            Err(Error::from(ErrorKind::Unsupported))
149        } else {
150            let p = path.canonicalize()?;
151            Ok(Self(p))
152        }
153    }
154}
155
156impl TryFrom<&str> for Folder {
157    type Error = std::io::Error;
158
159    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
160        Folder::try_from(PathBuf::from(value))
161    }
162}
163
164impl AsRef<Path> for Folder {
165    fn as_ref(&self) -> &Path {
166        self.0.as_ref()
167    }
168}
169
170#[deprecated(since = "2.0.0", note = "use trait `IsFolderToRemove` instead")]
171pub trait PathToRemoveResolver {
172    fn resolve_path_to_remove(&self, folder: impl AsRef<Path>) -> Result<Folder>;
173}
174
175#[allow(deprecated)]
176impl PathToRemoveResolver for FileToFolderMatch {
177    fn resolve_path_to_remove(&self, folder: impl AsRef<Path>) -> Result<Folder> {
178        let folder = folder.as_ref();
179        let file_to_check = folder.join(self.file_to_check);
180
181        if file_to_check.exists() {
182            let path_to_remove = folder.join(self.folder_to_remove);
183            if path_to_remove.exists() {
184                return path_to_remove.try_into();
185            }
186        }
187
188        Err(Error::from(ErrorKind::Unsupported))
189    }
190}
191
192/// Trait to check if a folder should be removed
193/// This is the successor of the deprecated `PathToRemoveResolver` and should be used instead.
194///
195/// The trait is implemented for `FileToFolderMatch` and can be used to check if a folder should be removed
196/// according to the rules defined in the `FileToFolderMatch` instance.
197pub trait IsFolderToRemove {
198    fn is_folder_to_remove(&self, folder: &Folder) -> bool;
199}
200
201impl IsFolderToRemove for FileToFolderMatch {
202    fn is_folder_to_remove(&self, folder: &Folder) -> bool {
203        folder.as_ref().parent().map_or_else(
204            || false,
205            |parent| {
206                parent.join(self.file_to_check).exists()
207                    && parent
208                        .join(self.folder_to_remove)
209                        .starts_with(folder.as_ref())
210            },
211        )
212    }
213}
214
215pub trait HumanReadable {
216    fn as_human_readable(&self) -> String;
217}
218
219impl HumanReadable for usize {
220    fn as_human_readable(&self) -> String {
221        const KIBIBYTE: usize = 1024;
222        const MEBIBYTE: usize = KIBIBYTE << 10;
223        const GIBIBYTE: usize = MEBIBYTE << 10;
224        const TEBIBYTE: usize = GIBIBYTE << 10;
225        const PEBIBYTE: usize = TEBIBYTE << 10;
226        const EXBIBYTE: usize = PEBIBYTE << 10;
227
228        let size = *self;
229        let (size, symbol) = match size {
230            size if size < KIBIBYTE => (size as f64, "B"),
231            size if size < MEBIBYTE => (size as f64 / KIBIBYTE as f64, "KiB"),
232            size if size < GIBIBYTE => (size as f64 / MEBIBYTE as f64, "MiB"),
233            size if size < TEBIBYTE => (size as f64 / GIBIBYTE as f64, "GiB"),
234            size if size < PEBIBYTE => (size as f64 / TEBIBYTE as f64, "TiB"),
235            size if size < EXBIBYTE => (size as f64 / PEBIBYTE as f64, "PiB"),
236            _ => (size as f64 / EXBIBYTE as f64, "EiB"),
237        };
238
239        format!("{:.1}{}", size, symbol)
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn should_size() {
249        assert_eq!(1_048_576, 1024 << 10);
250    }
251
252    #[test]
253    fn test_trait_is_folder_to_remove() {
254        let rule = FileToFolderMatch::new("Cargo.toml", "target");
255
256        let target_folder =
257            Folder::try_from(Path::new(".").canonicalize().unwrap().join("target")).unwrap();
258        assert!(rule.is_folder_to_remove(&target_folder));
259
260        let crate_root_folder = Folder::try_from(Path::new(".").canonicalize().unwrap()).unwrap();
261        assert!(!rule.is_folder_to_remove(&crate_root_folder));
262    }
263}