anon-flatten 0.1.1

一个简单的文件目录扁平化工具,让复杂的嵌套文件夹结构变得和爱音一样平 | A simple file directory flattening tool inspired by Anon Chihaya
Documentation
use std::collections::HashMap;
use std::path::PathBuf;

pub fn resolve_name_conflicts(files: &[PathBuf]) -> Vec<(PathBuf, String)> {
    let mut name_counts: HashMap<String, Vec<PathBuf>> = HashMap::new();

    for file_path in files {
        let original_name = file_path
            .file_name()
            .unwrap_or_default()
            .to_string_lossy()
            .to_string();
        name_counts
            .entry(original_name)
            .or_default()
            .push(file_path.clone());
    }

    let mut result = Vec::new();

    for (original_name, paths) in name_counts {
        if paths.len() == 1 {
            result.push((paths[0].clone(), original_name));
        } else {
            let base_name = get_file_stem(&original_name);
            let extension = get_file_extension(&original_name);

            for (index, path) in paths.iter().enumerate() {
                let final_name = if index == 0 {
                    original_name.clone()
                } else {
                    let parent_name = path
                        .parent()
                        .and_then(|p| p.file_name())
                        .unwrap_or_default()
                        .to_string_lossy()
                        .to_string();

                    let suffix = if parent_name.is_empty() {
                        format!("_{}", index)
                    } else {
                        format!("_{}", parent_name)
                    };

                    if extension.is_empty() {
                        format!("{}{}", base_name, suffix)
                    } else {
                        format!("{}{}.{}", base_name, suffix, extension)
                    }
                };

                result.push((path.clone(), final_name));
            }
        }
    }

    result
}

pub fn get_file_stem(filename: &str) -> &str {
    if let Some(pos) = filename.rfind('.') {
        &filename[..pos]
    } else {
        filename
    }
}

pub fn get_file_extension(filename: &str) -> String {
    if let Some(pos) = filename.rfind('.') {
        filename[pos + 1..].to_string()
    } else {
        String::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_file_stem() {
        assert_eq!(get_file_stem("file.txt"), "file");
        assert_eq!(get_file_stem("archive.tar.gz"), "archive.tar");
        assert_eq!(get_file_stem("no_extension"), "no_extension");
        assert_eq!(get_file_stem(".hidden"), "");
    }

    #[test]
    fn test_get_file_extension() {
        assert_eq!(get_file_extension("file.txt"), "txt");
        assert_eq!(get_file_extension("archive.tar.gz"), "gz");
        assert_eq!(get_file_extension("no_extension"), "");
        assert_eq!(get_file_extension(".hidden"), "hidden");
    }

    #[test]
    fn test_resolve_no_conflicts() {
        let files = vec![
            PathBuf::from("/test/file1.txt"),
            PathBuf::from("/test/file2.txt"),
        ];
        let result = resolve_name_conflicts(&files);

        assert_eq!(result.len(), 2);
        assert!(result.iter().any(|(_, name)| name == "file1.txt"));
        assert!(result.iter().any(|(_, name)| name == "file2.txt"));
    }

    #[test]
    fn test_resolve_with_conflicts() {
        let files = vec![
            PathBuf::from("/test/dir1/file.txt"),
            PathBuf::from("/test/dir2/file.txt"),
        ];
        let result = resolve_name_conflicts(&files);

        assert_eq!(result.len(), 2);

        let names: Vec<String> = result.iter().map(|(_, name)| name.clone()).collect();
        assert!(names.contains(&"file.txt".to_string()));
        assert!(
            names.contains(&"file_dir2.txt".to_string())
                || names.contains(&"file_dir1.txt".to_string())
        );
    }
}