file-organizer-lib 0.1.0

A library for organizing files by their extensions
Documentation
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use thiserror::Error;
use walkdir::WalkDir;

#[derive(Error, Debug)]
pub enum FileOrganizerError {
    #[error("Invalid path: {0}")]
    InvalidPath(String),
    #[error("IO error: {0}")]
    IoError(#[from] std::io::Error),
}

pub type Result<T> = std::result::Result<T, FileOrganizerError>;

pub fn categorize_files<P: AsRef<Path>>(
    source_dir: P,
    recursive: bool,
) -> Result<HashMap<String, Vec<PathBuf>>> {
    let source_path = source_dir.as_ref();
    
    if !source_path.exists() {
        return Err(FileOrganizerError::InvalidPath(format!(
            "Direktori tidak ditemukan: {}",
            source_path.display()
        )));
    }

    if !source_path.is_dir() {
        return Err(FileOrganizerError::InvalidPath(format!(
            "Path bukan direktori: {}",
            source_path.display()
        )));
    }

    let mut file_map: HashMap<String, Vec<PathBuf>> = HashMap::new();
    let walker = if recursive {
        WalkDir::new(source_path)
    } else {
        WalkDir::new(source_path).max_depth(1)
    };

    for entry in walker.into_iter().filter_map(|e| e.ok()) {
        let file_type = entry.file_type();
        
        if !file_type.is_file() {
            continue;
        }

        let path = entry.path().to_path_buf();
        let ext = path.extension()
            .and_then(OsStr::to_str)
            .map(|e| e.to_lowercase())
            .unwrap_or_else(|| "no_extension".to_string());

        file_map.entry(ext).or_default().push(path);
    }

    Ok(file_map)
}

pub fn organize_file<P: AsRef<Path>>(
    source_file: P,
    target_dir: P,
) -> Result<PathBuf> {
    let source = source_file.as_ref();
    if !source.is_file() {
        return Err(FileOrganizerError::InvalidPath(
            "Source is not a file".into(),
        ));
    }

    fs::create_dir_all(target_dir.as_ref())?;

    let file_name = source
        .file_name()
        .and_then(OsStr::to_str)
        .ok_or_else(|| FileOrganizerError::InvalidPath("Invalid file name".into()))?;

    let target_path = target_dir.as_ref().join(file_name);
    fs::rename(source, &target_path)?;

    Ok(target_path)
}

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

    #[test]
    fn test_categorize_files() -> Result<()> {
        let dir = tempdir()?;
        let dir_path = dir.path();

        File::create(dir_path.join("test1.txt"))?;
        File::create(dir_path.join("test2.txt"))?;
        File::create(dir_path.join("image.jpg"))?;

        let files = categorize_files(dir_path, false)?;
        
        assert_eq!(files.get("txt").unwrap().len(), 2);
        assert_eq!(files.get("jpg").unwrap().len(), 1);
        Ok(())
    }

    #[test]
    fn test_organize_file() -> Result<()> {
        let temp_dir = tempdir()?;
        let source_file = temp_dir.path().join("test.txt");
        let target_dir = temp_dir.path().join("organized");
        
        File::create(&source_file)?;
        organize_file(&source_file, &target_dir)?;

        assert!(target_dir.join("test.txt").exists());
        assert!(!source_file.exists());
        Ok(())
    }
}