projclean 0.2.0

Project cache finder and cleaner
Documentation
use anyhow::Result;
use jwalk::WalkDirGeneric;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::mpsc::{Receiver, Sender};

use crate::{Config, Message, PathItem};

pub fn search(entry: PathBuf, config: Config, tx: Sender<Message>) -> Result<()> {
    let walk_dir = WalkDirGeneric::<((), Option<()>)>::new(entry.clone())
        .skip_hidden(false)
        .process_read_dir(move |_depth, _path, _state, children| {
            let mut checker = Checker::new(&config);
            for dir_entry in children.iter().flatten() {
                if let Some(name) = dir_entry.file_name.to_str() {
                    checker.check(name);
                }
            }
            let matches = checker.to_matches();
            children.iter_mut().for_each(|dir_entry_result| {
                if let Ok(dir_entry) = dir_entry_result {
                    if let Some(name) = dir_entry.file_name.to_str() {
                        if matches.get(name).is_some() {
                            dir_entry.read_children_path = None;
                            dir_entry.client_state = Some(());
                        }
                    }
                }
            });
        });

    for dir_entry_result in walk_dir {
        if let Ok(dir_entry) = &dir_entry_result {
            if let Some(()) = dir_entry.client_state.as_ref() {
                let path = dir_entry.path();
                let size = du(&path).ok();
                let relative_path = path.strip_prefix(&entry)?.to_path_buf();
                let _ = tx.send(Message::AddPath(PathItem::new(path, relative_path, size)));
            }
        }
    }

    let _ = tx.send(Message::DoneSearch);

    Ok(())
}

pub fn ls(rx: Receiver<Message>) -> Result<()> {
    for message in rx {
        match message {
            Message::AddPath(path) => {
                println!("{}", path.path.display());
            }
            Message::DoneSearch => break,
            _ => {}
        }
    }
    Ok(())
}

#[derive(Debug)]
struct Checker<'a, 'b> {
    matches: HashMap<&'a str, (HashSet<&'b str>, HashSet<&'b str>)>,
    config: &'a Config,
}

impl<'a, 'b> Checker<'a, 'b> {
    fn new(config: &'a Config) -> Self {
        Self {
            config,
            matches: Default::default(),
        }
    }

    fn check(&mut self, name: &'b str) {
        for rule in &self.config.rules {
            let (purge_matches, check_matches) = self.matches.entry(rule.get_id()).or_default();
            if rule.test_purge(name) {
                purge_matches.insert(name);
            }
            if rule.test_check(name) {
                check_matches.insert(name);
            }
        }
    }

    fn to_matches(&self) -> HashMap<String, &'a str> {
        let mut matches: HashMap<String, &'a str> = HashMap::new();
        for (rule_id, (purge_matches, check_matches)) in &self.matches {
            if !purge_matches.is_empty()
                && (!check_matches.is_empty() || self.config.is_rule_no_check(rule_id))
            {
                for name in purge_matches {
                    if !matches.contains_key(*name) {
                        matches.insert(name.to_string(), rule_id);
                    }
                }
            }
        }
        matches
    }
}

fn du(path: &Path) -> Result<u64> {
    let mut total: u64 = 0;

    for dir_entry_result in WalkDirGeneric::<((), Option<u64>)>::new(path)
        .skip_hidden(false)
        .process_read_dir(|_, _, _, dir_entry_results| {
            dir_entry_results.iter_mut().for_each(|dir_entry_result| {
                if let Ok(dir_entry) = dir_entry_result {
                    if !dir_entry.file_type.is_dir() {
                        dir_entry.client_state =
                            Some(dir_entry.metadata().map(|m| m.len()).unwrap_or_default());
                    }
                }
            })
        })
    {
        let dir_entry = dir_entry_result?;
        if let Some(len) = &dir_entry.client_state {
            total += len;
        }
    }
    Ok(total)
}

#[cfg(test)]
mod tests {
    use super::*;
    macro_rules! assert_match_paths {
        ($id:literal, $names:expr) => {
            let none: &[&str] = &[];
            assert_match_paths!($id, $names, none);
        };
        ($id:literal, $names:expr, $matched:expr) => {
            let mut config = Config::default();
            let ret = config.add_rule($id);
            assert!(ret.is_ok());
            let mut checker = Checker::new(&config);
            for name in $names {
                checker.check(name);
            }
            let matches = checker.to_matches();
            let matched_names: Vec<&str> = matches.keys().map(|v| v.as_str()).collect();
            assert_eq!(matched_names, $matched);
        };
    }

    #[test]
    fn test_match_paths() {
        assert_match_paths!(
            "^target$@Cargo.toml",
            &["target", "Cargo.toml"],
            &["target"]
        );
        assert_match_paths!("target@Cargo.toml", &["target.rs", "Cargo.toml"]);
        assert_match_paths!(
            "^(Debug|Release)$@.*\\.sln",
            &["Debug", "Demo.sln"],
            &["Debug"]
        );
    }
}