use std::{ env, fs, io::{self, Write}, path::{Path, PathBuf}, process, sync::OnceLock, thread, }; use filesorters::Config; static CONFIG: OnceLock = OnceLock::new(); const VERSION: &str = env!("CARGO_PKG_VERSION"); fn main() { let config_path = get_config_path(); let args: Vec = env::args().collect(); if args.len() > 1 && (args.contains(&"-v".to_string()) || args.contains(&"--version".to_string())) { println!("Filesorters version {}", VERSION); return; } if args.len() > 1 && (args.contains(&"-c".to_string()) || args.contains(&"--config".to_string())) { println!("Config location: {}", config_path.display()); return; } if args.len() > 1 && (args.contains(&"-h".to_string()) || args.contains(&"--help".to_string())) { print!( "Options: -h, --help - show this help message -v, --version - show the version -c, --config-path - config file location " ); return; } if !config_path.exists() { println!("Config file does not exist, creating..."); filesorters::Config::create(&config_path).unwrap(); } match filesorters::Config::parse(config_path) { Ok(parsed_config) => { CONFIG.set(parsed_config).unwrap(); println!("Config file loaded successfully"); } Err(err) => { eprintln!("Error: {}", err); process::exit(1); } } { let mut counter = 1; for alias in CONFIG.get().unwrap().sources.keys() { println!("{}: {}", counter, alias); counter += 1; } } let mut actual_selection: Vec = Vec::new(); loop { print!("Enter: "); io::stdout().flush().unwrap(); let mut selected = String::new(); io::stdin() .read_line(&mut selected) .expect("Failed to read line"); let parsed_selection: Vec = selected .split(" ") .filter(|y| !y.is_empty()) .map(|y| y.replace("\n", "")) .flat_map(|x| x.parse::()) .collect(); if parsed_selection.is_empty() { println!("Invalid selection"); continue; } if parsed_selection.len() > CONFIG.get().unwrap().sources.keys().len() { println!("Too many selections made"); continue; } if parsed_selection .iter() .any(|x| *x > CONFIG.get().unwrap().sources.keys().len()) { println!("Invalid selection"); continue; } for selection in parsed_selection { actual_selection.push( CONFIG .get() .unwrap() .sources .keys() .nth(selection - 1) .unwrap() .to_string(), ); } break; } let recursive: bool; loop { print!("Recursive (yes/no): "); io::stdout().flush().unwrap(); let mut selected = String::new(); io::stdin() .read_line(&mut selected) .expect("Failed to read line"); if selected.trim().starts_with('y') { recursive = true; break; } else if selected.trim().starts_with('n') { recursive = false; break; } println!("Invalid answer"); } println!("Selected: {}", actual_selection.join(", ")); let mut thread_pool: Vec> = Vec::new(); for selection in actual_selection { let thread = thread::spawn(move || match sort_files(selection, recursive) { Ok(_) => {} Err(err) => eprintln!("{}", err), }); thread_pool.push(thread); } for thread in thread_pool { thread.join().unwrap(); } } fn sort_files(selection: String, recursive: bool) -> Result<(), Box> { let search_path = CONFIG .get() .and_then(|config| config.sources.get(&selection)) .cloned() .unwrap(); let file_types = [ ( "Picture", filesorters::PICTURE_EXTENTIONS.to_vec(), CONFIG.get().unwrap().pictures_dir.clone(), ), ( "Book", filesorters::BOOK_EXTENTIONS.to_vec(), CONFIG.get().unwrap().books_dir.clone(), ), ( "Video", filesorters::VIDEO_EXTENTIONS.to_vec(), CONFIG.get().unwrap().videos_dir.clone(), ), ( "Music", filesorters::SOUND_EXTENTIONS.to_vec(), CONFIG.get().unwrap().music_dir.clone(), ), ]; fn process_directory( path: &std::path::Path, file_types: &[(&str, Vec<&str>, Option)], recursive: bool, ) -> Result<(), Box> { for entry in fs::read_dir(path)?.flatten() { let metadata = entry.metadata()?; if metadata.is_file() { if let Some(extension) = entry.path().extension().and_then(|e| e.to_str()) { let file_path = entry.path(); for (_label, valid_extensions, target_dir_option) in file_types { if let Some(target_dir) = target_dir_option { if valid_extensions.contains(&extension) { move_file_to_directory(&file_path, target_dir); } } } } } else if metadata.is_dir() && recursive { // Recurse into the directory process_directory(&entry.path(), file_types, recursive)?; } } Ok(()) } process_directory(&search_path, &file_types, recursive) } fn move_file_to_directory(path: &Path, dir: &Path) { if !path.exists() { eprintln!("Source file does not exist"); return; } if !dir.exists() { match fs::create_dir(dir) { Ok(_) => {} Err(err) => eprintln!("Error creating destination directory: {}", err), } return; } let destination = dir.join(path.file_name().unwrap_or_default()); match fs::rename(path, &destination) { Ok(()) => {} Err(err) => eprintln!("Error moving file: {}", err), } } #[inline] fn get_config_path() -> PathBuf { if cfg!(target_os = "windows") { let username = env::var("USERNAME").unwrap_or_else(|_| "Default".to_string()); PathBuf::from(format!( "C:\\Users\\{}\\AppData\\Local\\filesorters\\filesorters.toml", username )) } else { let xdg_config_home = env::var("XDG_CONFIG_HOME").ok(); let home = env::var("HOME").ok(); let user = env::var("USER").unwrap_or_else(|_| "default".to_string()); xdg_config_home .map(|x| PathBuf::from(format!("{}/filesorters.toml", x))) .or_else(|| home.map(|h| PathBuf::from(format!("{}/.config/filesorters.toml", h)))) .unwrap_or_else(|| PathBuf::from(format!("/home/{}/.config/filesorters.toml", user))) } }