use std::fs; use std::fs::File; use std::path::{Path, PathBuf}; use std::io::{ErrorKind, Write, BufRead, BufReader}; use std::collections::HashMap; use rand::distributions::{Alphanumeric, DistString}; const LOCK_FILE: &str = ".mvwrap"; enum DirListEntry { Path(PathBuf), Text(String) } impl DirListEntry { fn to_string(&self) -> String { match self { DirListEntry::Text(line) => line.clone(), // TODO only strip first to chars if they're `./` DirListEntry::Path(path) => { let checkstring = path.display().to_string(); if &checkstring[0..2] == "./" { path.display().to_string()[2..].to_string() } else { path.display().to_string() } }, } } fn exists(&self) -> bool { match self { DirListEntry::Text(filename) => Path::new(filename).exists(), DirListEntry::Path(path) => path.exists(), } } } pub struct DirList { safe_source: bool, noop: bool, verbose: bool, entries: Vec, } impl DirList { pub fn from_current_dir() -> Self { let paths : Vec = match fs::read_dir(".") { Err(e) if e.kind() == ErrorKind::NotFound => Vec::new(), Err(e) => panic!("Unexpected Error! {:?}", e), Ok(entries) => entries.filter_map(|e| e.ok()) .map(|e| e.path()) .collect() }; let mut path_list : Vec = vec![]; for path in paths { let path_string = path.display().to_string(); if path_string != format!("./{}", LOCK_FILE) { path_list.push(DirListEntry::Path(path)); } } path_list.sort_by_key(|dir| dir.to_string()); Self { safe_source: true, noop: false, verbose: false, entries: path_list, } } pub fn from_file(src_file: &String) -> Self { let file = File::open(src_file).expect("no such file"); let buf = BufReader::new(file); let lines = buf.lines() .map(|l| DirListEntry::Text(l.expect("Could not parse line"))) .collect(); Self { safe_source: true, noop: false, verbose: false, entries: lines, } } pub fn from_list(list: &[PathBuf]) -> Self { Self { safe_source: false, noop: false, verbose: false, entries: list.iter().map(|l| DirListEntry::Path(l.clone())).collect(), } } pub fn set_noop(&mut self) { self.noop = true; } pub fn set_verbose(&mut self) { self.verbose = true; } pub fn to_file(&self, target_file: &String) { let mut file = File::create(target_file).expect("no such file"); for entry in &self.entries { writeln!(file, "{}", entry.to_string()).expect("Unable to write to file"); } } pub fn move_to(&mut self, target_list: &DirList) -> Result<(), String> { let src_len = self.entries.len(); let mut intermediate_files: HashMap = HashMap::new(); self.run_basic_checks(target_list)?; if ! self.safe_source { // if target == source => next // if target exists and is not in source list => Err // if target exists and is in source list => use intermediate files, which is normal functionality below let tgt_len = target_list.entries.len(); for i in 0..tgt_len { if self.entries[i].to_string() != target_list.entries[i].to_string() { if target_list.entries[i].exists() { return Err(format!("ERROR: Target '{}' already exists", target_list.entries[i].to_string()).to_string()); } } } } for i in 0..src_len { if self.entries[i].to_string() != target_list.entries[i].to_string() { // is the destination already in the source list? // if so, an intermediate file is needed if self.entries.iter().any(|j| j.to_string() == target_list.entries[i].to_string()) { let unique = Self::get_unique_entry(target_list); intermediate_files.insert(unique.clone(), target_list.entries[i].to_string().clone()); if ! self.noop { match &self.entries[i] { DirListEntry::Text(name) => fs::rename(name.to_string(), &unique).expect("failed to rename file (text based, unique)"), DirListEntry::Path(path) => fs::rename(path.as_path(), &unique).expect("failed to rename file (path based, unique)"), } } if self.verbose { println!("Moving {} -> {}", self.entries[i].to_string(), unique); } } else { if ! self.noop { match &self.entries[i] { DirListEntry::Text(name) => fs::rename(name.to_string(), &target_list.entries[i].to_string()).expect("failed to rename file (text based)"), DirListEntry::Path(path) => fs::rename(path.as_path(), &target_list.entries[i].to_string()).expect("failed to rename file (path based)"), } } if self.verbose { println!("Moving {} -> {}", self.entries[i].to_string(), target_list.entries[i].to_string()); } } } } for (src, dst) in intermediate_files.iter() { if ! self.noop { fs::rename(src, dst).expect("failed to rename file (intermediate name)"); } if self.verbose { println!("Moving {} -> {}", src, dst); } } Ok(()) } fn get_unique_entry(target_list: &DirList) -> String { let mut string = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); // Generate unique name that does not exist in current dir and is not in target_list while target_list.entries.iter().any(|j| j.to_string() == string) || Path::new(&string).is_file() { string = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); } string } fn run_basic_checks(&self, target_list: &DirList) -> Result<(), String> { // Make sure there are an equal number of sources and destinations if self.entries.len() != target_list.entries.len() { return Err("ERROR: Source and target list don't have the same number of lines.".to_string()); } // Make sure destination names are not empty if target_list.entries.iter().any(|i| i.to_string().is_empty()) { return Err("ERROR: You can't move to empty names.".to_string()); } // Make sure all destination files are unique in target_list let unique_entries = target_list.entries.iter().map(|x| (x.to_string(), x.to_string())).collect::>(); if target_list.entries.len() != unique_entries.len() { let doubles = self.show_doubles(target_list); let error = format!("ERROR: You're trying to move multiple files to the same name.\n{}", doubles); return Err(error); } Ok(()) } fn show_doubles(&self, target_list: &DirList) -> String { let mut paths = HashMap::new(); let mut doubles = String::new(); for (linenr, _) in target_list.entries.iter().enumerate() { let path_name = target_list.entries[linenr].to_string().clone(); paths.entry(target_list.entries[linenr].to_string()).or_insert(Vec::new()); paths.get_mut(&path_name).unwrap().push(linenr); } for (path, lines) in paths.iter() { if lines.len() > 1 { for i in lines.iter() { doubles = format!("{doubles}\n[line: {}] {} -> {}", i+1, self.entries[*i].to_string(), path); } doubles = format!("{doubles}\n"); } } doubles } }