use std::{ env, fs, io::{self, Write}, path::{Path, PathBuf}, process, sync::{Arc, Mutex, 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.clone()) { Ok(parsed_config) => { CONFIG.set(parsed_config).unwrap(); } Err(err) => { eprintln!("Error reading config: {}", err); process::exit(1); } } if cfg!(debug_assertions) { println!("config:{:#?}", CONFIG.get().unwrap()) } { let mut counter = 1; for alias in CONFIG.get().unwrap().sources.keys() { println!("{}: {}", counter, alias); counter += 1; } } if CONFIG.get().unwrap().sources.is_empty() { println!( "No sources found, please specify them in {}", config_path.display() ); process::exit(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", "")) .map(|y| y.replace("\r", "")) .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; } println!("Selected: {}", actual_selection.join(", ")); 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"); } let mut thread_pool: Vec> = Vec::new(); let total_files_sorted: Arc> = Arc::new(Mutex::new(0)); for selection in actual_selection { if cfg!(debug_assertions) { println!("spawned a thread with selection: {}", selection); } let tfs = Arc::clone(&total_files_sorted); let thread = thread::spawn(move || match sort_files(selection, recursive) { Ok(files_sorted) => { let mut num = tfs.lock().unwrap(); *num += files_sorted } Err(err) => eprintln!("Error sorting files: {}", err), }); thread_pool.push(thread); } for thread in thread_pool { thread.join().unwrap(); } if *total_files_sorted.lock().unwrap() == 0 { println!("No sortable files found!"); } else { println!("Sorted files: {}", total_files_sorted.lock().unwrap()); } } fn sort_files(selection: String, recursive: bool) -> Result> { 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().video_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> { let mut files_sorted: usize = 0; 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 cfg!(debug_assertions) { println!("{} - {}", file_path.display(), &extension); } if valid_extensions.contains(&extension) { move_file_to_directory(&file_path, target_dir); files_sorted += 1; } } } } } else if metadata.is_dir() && recursive { // Recurse into the directory files_sorted += process_directory(&entry.path(), file_types, recursive)?; } } Ok(files_sorted) } 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_all(dir) { Ok(_) => {} Err(err) => eprintln!( "Error creating destination directory({}): {}", dir.display(), err ), } return; } let destination = dir.join(path.file_name().unwrap_or_default()); if cfg!(debug_assertions) { println!( "{} - {} - {}", path.display(), dir.display(), destination.display() ); } 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.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))) } }