use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use crate::scan::FileInfo;
use anyhow::Result;
pub fn organize_by_extension(download_dir: &Path, files: &[FileInfo]) -> Result<()> {
let mut category_dirs: HashMap<String, PathBuf> = HashMap::new();
for file in files {
let category = match_category(&file.name);
if category.is_none() {
continue;
}
let category = category.unwrap();
let target_dir = category_dirs
.entry(category.to_string())
.or_insert_with(|| download_dir.join(&category));
if !target_dir.exists() {
fs::create_dir_all(&target_dir)?;
}
let target_path = target_dir.join(&file.name);
if target_path.exists() {
continue;
}
fs::rename(&file.path, &target_path)?;
}
Ok(())
}
fn match_category(file_name: &str) -> Option<&'static str> {
let ext = Path::new(file_name)
.extension()
.and_then(|e| e.to_str())?
.to_ascii_lowercase();
const IMAGE_EXTS: &[&str] = &["jpg", "jpeg", "png", "gif", "bmp", "webp", "heic", "tiff", "svg", "avif"];
if IMAGE_EXTS.contains(&ext.as_str()) {
return Some("Image");
}
const MUSIC_EXTS: &[&str] = &["mp3", "wav", "flac", "aac", "ogg", "m4a"];
if MUSIC_EXTS.contains(&ext.as_str()) {
return Some("Music");
}
const VIDEO_EXTS: &[&str] = &["mp4", "mov", "avi", "mkv", "wmv", "webm"];
if VIDEO_EXTS.contains(&ext.as_str()) {
return Some("Video");
}
const APPS_EXTS: &[&str] = &["dmg", "exe", "msi", "pkg", "deb", "rpm", "appimage"];
if APPS_EXTS.contains(&ext.as_str()) {
return Some("Apps");
}
const DOC_EXTS: &[&str] = &["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "md"];
if DOC_EXTS.contains(&ext.as_str()) {
return Some("Docs");
}
const ARCHIVE_EXTS: &[&str] = &["zip", "tar", "gz", "tgz", "bz2", "7z", "rar", "xz", "lz4", "zst", "lz", "xz"];
if ARCHIVE_EXTS.contains(&ext.as_str()) {
return Some("Archive");
}
let fname_lower = file_name.to_ascii_lowercase();
const MULTI_PART_ARCHIVES: &[&str] = &[".tar.gz", ".tar.xz", ".tar.bz2", ".tar.zst"];
for p in MULTI_PART_ARCHIVES {
if fname_lower.ends_with(p) {
return Some("Archive");
}
}
if ARCHIVE_EXTS.contains(&ext.as_str()) {
return Some("Archive");
}
const SHELL_EXTS: &[&str] = &["sh", "bash", "zsh", "ps1", "bat", "cmd", "ksh", "fish"];
if SHELL_EXTS.contains(&ext.as_str()) {
return Some("Scripts");
}
if ext == "json" {
return Some("JSON");
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_match_category_basic() {
assert_eq!(match_category("photo.JPG"), Some("Image"));
assert_eq!(match_category("song.mp3"), Some("Music"));
assert_eq!(match_category("movie.mkv"), Some("Video"));
assert_eq!(match_category("installer.dmg"), Some("Apps"));
assert_eq!(match_category("document.pdf"), Some("Docs"));
assert_eq!(match_category("archive.zip"), Some("Archive"));
assert_eq!(match_category("unknown.xyz"), None);
}
#[test]
fn test_match_category_new_types() {
assert_eq!(match_category("script.sh"), Some("Scripts"));
assert_eq!(match_category("powershell.ps1"), Some("Scripts"));
assert_eq!(match_category("setup.BAT"), Some("Scripts"));
assert_eq!(match_category("data.json"), Some("JSON"));
assert_eq!(match_category("archive.tar.gz"), Some("Archive"));
assert_eq!(match_category("backup.tar.xz"), Some("Archive"));
assert_eq!(match_category("image.PNG"), Some("Image"));
}
}