fast-clean 1.2.0

A tool for quickly clean the target dir
Documentation
pub mod filter;
pub mod remover;

use crate::filter::{Filter, FilterEngine};
use crate::remover::MockRemover;
pub use crate::remover::RealRemover;
use clap::Parser;
use remover::Remover;
use std::error::Error;
use std::path::PathBuf;
use walkdir::DirEntry;
use walkdir::WalkDir;

#[derive(Parser)]
pub struct Cli {
    /// 目标文件夹
    #[arg(short = 'd')]
    pub dir: String,
    /// 过滤的文件夹
    #[arg(long = "fd")]
    pub filter_dir: Option<Vec<String>>,
    /// 过滤的扩展名
    #[arg(long = "ext")]
    pub file_ext: Option<Vec<String>>,
    /// 过滤的天数(超过此天数才被删除)
    #[arg(long = "day")]
    pub day: Option<i32>,
    /// 过滤大小(超过此大小才被删除)
    #[arg(long = "size")]
    pub size: Option<String>,
    /// 是否删除空文件夹
    #[arg(long = "skip_dir")]
    pub is_skip_dir: bool,
    /// 是否是测试
    #[arg(long = "is_test")]
    pub is_test: bool,
}

/// 删除文件主逻辑
pub fn run(args: &Cli) -> Result<Vec<PathBuf>, Box<dyn Error>> {
    if args.is_test{
        let mut mock_remover = MockRemover::new();
        remove_files(args, &mut mock_remover)?;
        Ok(mock_remover.removed.borrow().to_vec())
    }else{
        let mut real_remover = RealRemover {
            is_skip_dir: args.is_skip_dir.clone(),
        };
        remove_files(args, &mut real_remover)?;
        Ok(Vec::new())
    }
}

pub fn remove_files<T:Remover>(args: &Cli, remover: &mut T) -> Result<(), Box<dyn Error>> {
    let filter_engine = FilterEngine::from(&args)?;
    let skip_dirs = args.filter_dir.clone().unwrap_or_default();
    for entry in WalkDir::new(&args.dir)
        .into_iter()
        .filter_entry(|e| is_not_target_dir(e, &skip_dirs))
        .filter_map(Result::ok)
        .skip(1) // 第一个是dir,需要跳过
    {
        if let Ok(_) = filter_engine.filter(&entry) {
            remover.remove(entry.path())?;
        }
    }
    Ok(())
}

/// 判断是否跳过文件夹
fn is_not_target_dir(entry: &DirEntry, skip_dirs: &Vec<String>) -> bool {
    if skip_dirs.is_empty() {
        return true;
    }
    if entry.file_type().is_dir() {
        if let Some(name) = entry.file_name().to_str() {
            return !skip_dirs.contains(&name.to_string());
        }
    }
    true
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::{create_dir, File};
    use tempfile::tempdir;

    /// 创建测试用 Cli
    fn make_cli(dir: &str) -> Cli {
        Cli {
            dir: dir.to_string(),
            filter_dir: None,
            file_ext: None,
            day: None,
            size: None,
            is_skip_dir: false,
            is_test: true
        }
    }

    #[test]
    fn test_remove_all_files() {
        let temp = tempdir().unwrap();
        let file1 = temp.path().join("a.txt");
        let file2 = temp.path().join("b.txt");

        File::create(&file1).unwrap();
        File::create(&file2).unwrap();

        let cli = make_cli(temp.path().to_str().unwrap());

        let ret = run(&cli).unwrap();
        assert_eq!(ret.len(), 2);
    }

    #[test]
    fn test_skip_specific_dir() {
        let temp = tempdir().unwrap();

        let skip_dir = temp.path().join("skip_me");
        let normal_dir = temp.path().join("delete_me");

        create_dir(&skip_dir).unwrap();
        create_dir(&normal_dir).unwrap();

        let file1 = skip_dir.join("a.txt");
        let file2 = normal_dir.join("b.txt");

        File::create(&file1).unwrap();
        File::create(&file2).unwrap();

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

        let ret = run(&cli).unwrap();
        // 只应该删除 delete_me 里面的文件
        assert_eq!(ret.len(), 2);
        // 1 个文件 + 1 个目录 delete_me
        // skip_me 整个目录树应该被 filter_entry 过滤掉
    }
}