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;
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();
let cli = Cli {
dir: dir.path().to_str().unwrap().to_string(),
filter_dir: None,
file_ext: None,
day: None,
size: Some("200B".to_string()), 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();
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());
}
}