filesorters/src/main.rs
Vladimir Rubin bffc4a6ab8
Some checks failed
Build and Upload filesorters Binaries / Build for Linux (push) Has been cancelled
fix: check if there are no sources
2024-12-30 19:41:29 +02:00

307 lines
8.6 KiB
Rust

use std::{
env, fs,
io::{self, Write},
path::{Path, PathBuf},
process,
sync::{Arc, Mutex, OnceLock},
thread,
};
use filesorters::Config;
static CONFIG: OnceLock<Config> = OnceLock::new();
const VERSION: &str = env!("CARGO_PKG_VERSION");
fn main() {
let config_path = get_config_path();
let args: Vec<String> = 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<String> = 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<usize> = selected
.split(" ")
.filter(|y| !y.is_empty())
.map(|y| y.replace("\n", ""))
.map(|y| y.replace("\r", ""))
.flat_map(|x| x.parse::<usize>())
.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<thread::JoinHandle<()>> = Vec::new();
let total_files_sorted: Arc<Mutex<usize>> = 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<usize, Box<dyn std::error::Error>> {
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<std::path::PathBuf>)],
recursive: bool,
) -> Result<usize, Box<dyn std::error::Error>> {
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)))
}
}