Skip to main content

fast_clean/
filter.rs

1use crate::Cli;
2use filetime::FileTime;
3use std::error::Error;
4use std::fmt::Display;
5use std::time::{Duration, SystemTime};
6use walkdir::DirEntry;
7
8
9/// 组合过滤器,
10pub struct FilterEngine{
11    filters: Vec<Box<dyn Filter>>,
12}
13impl FilterEngine {
14    pub fn new(filters: Vec<Box<dyn Filter>>) -> Self {
15        Self { filters }
16    }
17    pub fn add_filter(&mut self, filter: Box<dyn Filter>) {
18        self.filters.push(filter);
19    }
20    pub fn from(cli: &Cli) -> Result<Self, Box<dyn Error>> {
21        let mut filters: Vec<Box<dyn Filter>> = vec![];
22        if let Some(ext) = cli.file_ext.as_ref() {
23            filters.push(Box::new(ExtFilter::new(ext.clone())));
24        }
25        if let Some(size) = cli.size.as_ref() {
26            filters.push(Box::new(SizeFilter::new(size.clone())))
27        }
28        if let Some(day) = cli.day.as_ref() {
29            filters.push(Box::new(DateFilter::new(day.clone())));
30        }
31        Ok(Self::new(filters))
32    }
33}
34impl Filter for FilterEngine {
35    fn filter(&self, entry: &DirEntry) -> Result<(), Box<dyn Error>> {
36        for filter in &self.filters {
37            filter.filter(entry)?;
38        }
39        Ok(())
40    }
41}
42
43/// 文件扩展名过滤器
44pub struct ExtFilter {
45    file_ext: Vec<String>,
46}
47impl ExtFilter {
48    pub fn new(file_ext: Vec<String>) -> Self {
49        Self { file_ext }
50    }
51}
52impl Filter for ExtFilter {
53    fn filter(&self, entry: &DirEntry) -> Result<(), Box<dyn Error>> {
54        if let Some(ext) = entry.path().extension().and_then(|e| e.to_str()) {
55            if self.file_ext
56                .iter()
57                .any(|target| ext.eq_ignore_ascii_case(target))
58            {
59                return Ok(());
60            }
61        }
62        Err(Box::new(FilterResult::NotMatch(None)))
63    }
64}
65
66/// 文件大小过滤策略
67pub struct SizeFilter {
68    file_size: u64,
69}
70impl SizeFilter {
71    pub fn new(size: String) -> Self {
72        Self { file_size: parse_size(&*size).unwrap() }
73    }
74}
75impl Filter for SizeFilter {
76    fn filter(&self, entry: &DirEntry) -> Result<(), Box<dyn Error>> {
77        let metadata = entry.metadata()?;
78        if metadata.len() >= self.file_size{
79            return Ok(());
80        }
81        Err(Box::from(FilterResult::NotMatch(None)))
82    }
83}
84fn parse_size(input: &str) -> Result<u64, Box<dyn Error>> {
85    let input = input.trim().to_uppercase();
86
87    let (num_part, unit) = input
88        .chars()
89        .position(|c| !c.is_ascii_digit())
90        .map(|pos| input.split_at(pos))
91        .unwrap_or((&input[..], "B"));
92
93    let number: u64 = num_part.parse()?;
94
95    let multiplier = match unit {
96        "B" | "" => 1,
97        "K" | "KB" => 1024,
98        "M" | "MB" => 1024 * 1024,
99        "G" | "GB" => 1024 * 1024 * 1024,
100        _ => return Err(format!("Invalid size unit: {}", unit).into()),
101    };
102
103    Ok(number * multiplier)
104}
105
106
107/// 日期过滤策略
108struct DateFilter{
109    day: i32,
110}
111impl DateFilter {
112    pub fn new(day: i32) -> Self {
113        Self { day }
114    }
115}
116impl Filter for DateFilter {
117    fn filter(&self, entry: &DirEntry) -> Result<(), Box<dyn Error>> {
118        let metadata = entry.metadata()?;
119        let mtime = FileTime::from_last_modification_time(&metadata);
120        let age = SystemTime::now()
121            .duration_since(SystemTime::UNIX_EPOCH + Duration::from_secs(mtime.unix_seconds() as u64))
122            .unwrap_or_default();
123        if age.as_secs() >= (self.day as u64) * 86400 {
124            return Ok(());
125        }
126        Err(Box::from(FilterResult::NotMatch(None)))
127    }
128}
129
130pub trait Filter {
131    fn filter(&self, entry: &DirEntry) -> Result<(), Box<dyn Error>>;
132}
133
134#[derive(Debug)]
135pub enum FilterResult {
136    Matched,
137    NotMatch(Option<String>),
138}
139impl Display for FilterResult {
140    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
141        write!(f, "{:?}", self)
142    }
143}
144impl Error for FilterResult {}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use std::fs::File;
150    use std::io::Write;
151    use tempfile::tempdir;
152    use walkdir::WalkDir;
153
154    // 辅助函数:创建 DirEntry
155    fn make_entry(path: &std::path::Path) -> DirEntry {
156        WalkDir::new(path)
157            .max_depth(1)
158            .into_iter()
159            .filter_map(Result::ok)
160            .find(|e| e.path() == path)
161            .unwrap()
162    }
163
164    #[test]
165    fn test_filter_file_ext() {
166        let dir = tempdir().unwrap();
167        let file_path = dir.path().join("test.txt");
168        File::create(&file_path).unwrap();
169
170        let cli = Cli {
171            dir: dir.path().to_str().unwrap().to_string(),
172            filter_dir: None,
173            file_ext: Some(vec!["txt".to_string()]),
174            day: None,
175            size: None,
176            is_skip_dir: false,
177            is_test: true
178        };
179        let filter = ExtFilter::new(cli.file_ext.expect("REASON"));
180
181        let entry = make_entry(&file_path);
182        let res = filter.filter(&entry).unwrap();
183        assert_eq!(res, ()); // 匹配成功
184
185        // 测试不匹配
186        let cli2 = Cli {
187            file_ext: Some(vec!["log".to_string()]),
188            ..cli
189        };
190        let filter2 = ExtFilter::new(cli2.file_ext.expect("REASON"));
191        let entry2 = make_entry(&file_path);
192        assert!(filter2.filter(&entry2).is_err());
193    }
194
195    #[test]
196    fn test_filter_file_size() {
197        let dir = tempdir().unwrap();
198        let file_path = dir.path().join("test.bin");
199        let mut f = File::create(&file_path).unwrap();
200        f.write_all(&[0u8; 100]).unwrap(); // 100 bytes
201
202        let cli = Cli {
203            dir: dir.path().to_str().unwrap().to_string(),
204            filter_dir: None,
205            file_ext: None,
206            day: None,
207            size: Some("200B".to_string()), // <=200 ok
208            is_skip_dir: false,
209            is_test: true
210        };
211        let filter = SizeFilter::new(cli.size.unwrap().parse().unwrap());
212
213        let entry = make_entry(&file_path);
214        assert!(filter.filter(&entry).is_ok());
215
216        let cli2 = Cli { size: Some("50B".to_string()), ..cli };
217        let filter2 = SizeFilter::new(cli2.size.unwrap());
218        let entry2 = make_entry(&file_path);
219        assert!(filter2.filter(&entry2).is_err());
220    }
221
222    #[test]
223    fn test_filter_time() {
224        let dir = tempdir().unwrap();
225        let file_path = dir.path().join("time.txt");
226        File::create(&file_path).unwrap();
227        // 10天
228        let ten_days_ago = SystemTime::now() - Duration::from_secs(10 * 24 * 3600);
229        let ft = FileTime::from_system_time(ten_days_ago);
230        filetime::set_file_mtime(&file_path, ft).unwrap();
231
232        let _cli = Cli {
233            dir: dir.path().to_str().unwrap().to_string(),
234            filter_dir: None,
235            file_ext: None,
236            day: Some(10), // 负数天数,用于确保匹配
237            size: None,
238            is_skip_dir: false,
239            is_test: true
240        };
241        let filter = DateFilter::new(7);
242
243        let entry = make_entry(&file_path);
244        assert!(filter.filter(&entry).is_ok());
245    }
246}