Some checks failed
Build and Upload filesorters Binaries / Build for Linux (push) Has been cancelled
307 lines
8.6 KiB
Rust
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)))
|
|
}
|
|
}
|