ocy_core/
walker.rs

1use std::{collections::HashSet, path::PathBuf};
2
3use crate::{
4    filesystem::FileSystem,
5    matcher::{CleanStrategy, Matcher},
6    models::RemovalCandidate,
7    models::{FileInfo, SimpleFileKind},
8};
9use eyre::Report;
10use eyre::Result;
11
12pub struct Walker<FS: FileSystem, N: WalkNotifier> {
13    fs: FS,
14    matchers: Vec<Matcher>,
15    notifier: N,
16    ignores: HashSet<PathBuf>,
17    walk_all: bool,
18}
19
20pub trait WalkNotifier {
21    fn notify_entered_directory(&self, dir: &FileInfo);
22    fn notify_candidate_for_removal(&self, candidate: RemovalCandidate);
23    fn notify_fail_to_scan(&self, e: &FileInfo, report: Report);
24    fn notify_walk_finish(&self);
25}
26
27impl<FS: FileSystem, N: WalkNotifier> Walker<FS, N> {
28    pub fn new(
29        fs: FS,
30        matchers: Vec<Matcher>,
31        notifier: N,
32        ignores: HashSet<PathBuf>,
33        walk_all: bool,
34    ) -> Self {
35        Self {
36            fs,
37            matchers,
38            notifier,
39            ignores,
40            walk_all,
41        }
42    }
43
44    pub fn walk_from_path(&self, path: &FileInfo) {
45        self.process_dir(path);
46        self.notifier.notify_walk_finish();
47    }
48
49    fn process_dir(&self, file: &FileInfo) {
50        if self.ignores.contains(&file.path) {
51            return;
52        }
53        match self.process_entries(file) {
54            Ok(children) => {
55                children.iter().for_each(|d| self.process_dir(d));
56            }
57            Err(report) => self.notifier.notify_fail_to_scan(file, report),
58        }
59    }
60
61    fn process_entries(&self, file: &FileInfo) -> Result<Vec<FileInfo>> {
62        self.notifier.notify_entered_directory(file);
63        let mut entries = self.fs.list_files(file)?;
64
65        for matcher in &self.matchers {
66            entries = self.process_matcher(file, matcher, entries);
67        }
68        entries.retain(|f| self.is_walkable(f));
69        Ok(entries)
70    }
71
72    fn process_matcher(
73        &self,
74        work_dir: &FileInfo,
75        matcher: &Matcher,
76        entries: Vec<FileInfo>,
77    ) -> Vec<FileInfo> {
78        if matcher.any_entry_match(&entries) {
79            match &matcher.clean_strategy {
80                CleanStrategy::Remove(pattern) => {
81                    let (mut to_remove, remaining) = pattern.find_files_to_remove(entries);
82                    to_remove.retain(|p| !self.ignores.contains(&p.path));
83                    self.notify_removal_candidates(matcher, to_remove);
84                    remaining
85                }
86                CleanStrategy::RunCommand(cmd) => {
87                    let candidate = RemovalCandidate::new_cmd(
88                        matcher.name.clone(),
89                        work_dir.clone(),
90                        cmd.clone(),
91                    );
92                    self.notifier.notify_candidate_for_removal(candidate);
93                    entries
94                }
95            }
96        } else {
97            entries
98        }
99    }
100
101    fn notify_removal_candidates(&self, matcher: &Matcher, to_remove: Vec<FileInfo>) {
102        to_remove
103            .into_iter()
104            .map(|f| self.removal_candidate(matcher, f))
105            .for_each(|c| self.notifier.notify_candidate_for_removal(c));
106    }
107
108    fn removal_candidate(&self, matcher: &Matcher, file: FileInfo) -> RemovalCandidate {
109        let size = self.fs.file_size(&file).ok();
110        RemovalCandidate::new(matcher.name.clone(), file, size)
111    }
112
113    fn is_walkable(&self, file: &FileInfo) -> bool {
114        file.kind == SimpleFileKind::Directory && (self.walk_all || !file.name.starts_with('.'))
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use std::{cell::RefCell, collections::HashSet, path::PathBuf, str::FromStr};
121
122    use glob::Pattern;
123
124    use crate::{
125        filesystem::FileSystem,
126        matcher::Matcher,
127        models::FileInfo,
128        test_utils::{MockFS, MockFSNode},
129        walker::Walker,
130    };
131
132    use super::WalkNotifier;
133    use crate::models::{RemovalAction, RemovalCandidate};
134
135    #[derive(Debug, Default)]
136    struct VecWalkNotifier {
137        pub to_remove: RefCell<Vec<RemovalCandidate>>,
138    }
139
140    impl WalkNotifier for &VecWalkNotifier {
141        fn notify_entered_directory(&self, _dir: &FileInfo) {}
142
143        fn notify_candidate_for_removal(&self, candidate: RemovalCandidate) {
144            self.to_remove.borrow_mut().push(candidate);
145        }
146
147        fn notify_fail_to_scan(&self, _e: &FileInfo, _report: eyre::Error) {}
148
149        fn notify_walk_finish(&self) {}
150    }
151
152    fn setup_mock_fs() -> MockFS {
153        MockFS::new(MockFSNode::dir(
154            "/",
155            vec![MockFSNode::dir(
156                "home",
157                vec![MockFSNode::dir(
158                    "user",
159                    vec![
160                        MockFSNode::dir(
161                            "projectA",
162                            vec![MockFSNode::file("Cargo.toml"), MockFSNode::file("target")],
163                        ),
164                        MockFSNode::dir("projectB", vec![MockFSNode::file("target")]),
165                    ],
166                )],
167            )],
168        ))
169    }
170
171    #[test]
172    fn test() -> eyre::Result<()> {
173        let fs = setup_mock_fs();
174        let current_dir = setup_mock_fs().current_directory()?;
175        let notifier = VecWalkNotifier::default();
176        let walker = Walker::new(
177            fs,
178            vec![Matcher::with_remove_strategy(
179                "Cargo".into(),
180                Pattern::new("Cargo.toml")?,
181                Pattern::new("target")?,
182            )],
183            &notifier,
184            HashSet::new(),
185            false,
186        );
187        walker.walk_from_path(&current_dir);
188
189        let to_remove = notifier.to_remove.into_inner();
190
191        assert_eq!(1, to_remove.len());
192        let c = to_remove.into_iter().next().unwrap();
193        assert_eq!(c.matcher_name.as_ref(), "Cargo");
194
195        match c.action {
196            RemovalAction::Delete { file_info, .. } => {
197                assert_eq!(
198                    file_info.path,
199                    PathBuf::from_str("/home/user/projectA/target").unwrap()
200                )
201            }
202            RemovalAction::RunCommand { work_dir, command } => {
203                panic!("should be delete")
204            }
205        }
206
207        Ok(())
208    }
209}