1use crate::Cli;
2use filetime::FileTime;
3use std::error::Error;
4use std::fmt::Display;
5use std::time::{Duration, SystemTime};
6use walkdir::DirEntry;
7
8
9pub 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
43pub 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
66pub 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
107struct 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 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, ()); 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(); 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()), 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 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), 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}