fast-clean 1.1.2

A tool for quickly clean the target dir
Documentation
use crate::Cli;
use filetime::FileTime;
use std::error::Error;
use std::fmt::Display;
use std::time::{Duration, SystemTime};
use walkdir::DirEntry;


/// 组合过滤器,
pub struct FilterEngine{
    filters: Vec<Box<dyn Filter>>,
}
impl FilterEngine {
    pub fn new(filters: Vec<Box<dyn Filter>>) -> Self {
        Self { filters }
    }
    pub fn add_filter(&mut self, filter: Box<dyn Filter>) {
        self.filters.push(filter);
    }
    pub fn from(cli: &Cli) -> Result<Self, Box<dyn Error>> {
        let mut filters: Vec<Box<dyn Filter>> = vec![];
        if let Some(ext) = cli.file_ext.as_ref() {
            filters.push(Box::new(ExtFilter::new(ext.clone())));
        }
        if let Some(size) = cli.size.as_ref() {
            filters.push(Box::new(SizeFilter::new(size.clone())))
        }
        if let Some(day) = cli.day.as_ref() {
            filters.push(Box::new(DateFilter::new(day.clone())));
        }
        Ok(Self::new(filters))
    }
}
impl Filter for FilterEngine {
    fn filter(&self, entry: &DirEntry) -> Result<(), Box<dyn Error>> {
        for filter in &self.filters {
            filter.filter(entry)?;
        }
        Ok(())
    }
}

/// 文件扩展名过滤器
pub struct ExtFilter {
    file_ext: Vec<String>,
}
impl ExtFilter {
    pub fn new(file_ext: Vec<String>) -> Self {
        Self { file_ext }
    }
}
impl Filter for ExtFilter {
    fn filter(&self, entry: &DirEntry) -> Result<(), Box<dyn Error>> {
        if let Some(ext) = entry.path().extension().and_then(|e| e.to_str()) {
            if self.file_ext
                .iter()
                .any(|target| ext.eq_ignore_ascii_case(target))
            {
                return Ok(());
            }
        }
        Err(Box::new(FilterResult::NotMatch(None)))
    }
}

/// 文件大小过滤策略
pub struct SizeFilter {
    file_size: u64,
}
impl SizeFilter {
    pub fn new(size: String) -> Self {
        Self { file_size: parse_size(&*size).unwrap() }
    }
}
impl Filter for SizeFilter {
    fn filter(&self, entry: &DirEntry) -> Result<(), Box<dyn Error>> {
        let metadata = entry.metadata()?;
        if metadata.len() >= self.file_size{
            return Ok(());
        }
        Err(Box::from(FilterResult::NotMatch(None)))
    }
}
fn parse_size(input: &str) -> Result<u64, Box<dyn Error>> {
    let input = input.trim().to_uppercase();

    let (num_part, unit) = input
        .chars()
        .position(|c| !c.is_ascii_digit())
        .map(|pos| input.split_at(pos))
        .unwrap_or((&input[..], "B"));

    let number: u64 = num_part.parse()?;

    let multiplier = match unit {
        "B" | "" => 1,
        "K" | "KB" => 1024,
        "M" | "MB" => 1024 * 1024,
        "G" | "GB" => 1024 * 1024 * 1024,
        _ => return Err(format!("Invalid size unit: {}", unit).into()),
    };

    Ok(number * multiplier)
}


/// 日期过滤策略
struct DateFilter{
    day: i32,
}
impl DateFilter {
    pub fn new(day: i32) -> Self {
        Self { day }
    }
}
impl Filter for DateFilter {
    fn filter(&self, entry: &DirEntry) -> Result<(), Box<dyn Error>> {
        let metadata = entry.metadata()?;
        let mtime = FileTime::from_last_modification_time(&metadata);
        let age = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH + Duration::from_secs(mtime.unix_seconds() as u64))
            .unwrap_or_default();
        if age.as_secs() >= (self.day as u64) * 86400 {
            return Ok(());
        }
        Err(Box::from(FilterResult::NotMatch(None)))
    }
}

pub trait Filter {
    fn filter(&self, entry: &DirEntry) -> Result<(), Box<dyn Error>>;
}

#[derive(Debug)]
pub enum FilterResult {
    Matched,
    NotMatch(Option<String>),
}
impl Display for FilterResult {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}
impl Error for FilterResult {}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::File;
    use std::io::Write;
    use tempfile::tempdir;
    use walkdir::WalkDir;

    // 辅助函数:创建 DirEntry
    fn make_entry(path: &std::path::Path) -> DirEntry {
        WalkDir::new(path)
            .max_depth(1)
            .into_iter()
            .filter_map(Result::ok)
            .find(|e| e.path() == path)
            .unwrap()
    }

    #[test]
    fn test_filter_file_ext() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("test.txt");
        File::create(&file_path).unwrap();

        let cli = Cli {
            dir: dir.path().to_str().unwrap().to_string(),
            filter_dir: None,
            file_ext: Some(vec!["txt".to_string()]),
            day: None,
            size: None,
            is_skip_dir: false,
            is_test: true
        };
        let filter = ExtFilter::new(cli.file_ext.expect("REASON"));

        let entry = make_entry(&file_path);
        let res = filter.filter(&entry).unwrap();
        assert_eq!(res, ()); // 匹配成功

        // 测试不匹配
        let cli2 = Cli {
            file_ext: Some(vec!["log".to_string()]),
            ..cli
        };
        let filter2 = ExtFilter::new(cli2.file_ext.expect("REASON"));
        let entry2 = make_entry(&file_path);
        assert!(filter2.filter(&entry2).is_err());
    }

    #[test]
    fn test_filter_file_size() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("test.bin");
        let mut f = File::create(&file_path).unwrap();
        f.write_all(&[0u8; 100]).unwrap(); // 100 bytes

        let cli = Cli {
            dir: dir.path().to_str().unwrap().to_string(),
            filter_dir: None,
            file_ext: None,
            day: None,
            size: Some("200B".to_string()), // <=200 ok
            is_skip_dir: false,
            is_test: true
        };
        let filter = SizeFilter::new(cli.size.unwrap().parse().unwrap());

        let entry = make_entry(&file_path);
        assert!(filter.filter(&entry).is_ok());

        let cli2 = Cli { size: Some("50B".to_string()), ..cli };
        let filter2 = SizeFilter::new(cli2.size.unwrap());
        let entry2 = make_entry(&file_path);
        assert!(filter2.filter(&entry2).is_err());
    }

    #[test]
    fn test_filter_time() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("time.txt");
        File::create(&file_path).unwrap();
        // 10天
        let ten_days_ago = SystemTime::now() - Duration::from_secs(10 * 24 * 3600);
        let ft = FileTime::from_system_time(ten_days_ago);
        filetime::set_file_mtime(&file_path, ft).unwrap();

        let _cli = Cli {
            dir: dir.path().to_str().unwrap().to_string(),
            filter_dir: None,
            file_ext: None,
            day: Some(10), // 负数天数,用于确保匹配
            size: None,
            is_skip_dir: false,
            is_test: true
        };
        let filter = DateFilter::new(7);

        let entry = make_entry(&file_path);
        assert!(filter.filter(&entry).is_ok());
    }
}