2024-11-26 22:17:15 +01:00
|
|
|
use std::fs;
|
|
|
|
|
use std::fs::File;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
2024-11-27 09:50:16 +01:00
|
|
|
use std::io::{ErrorKind, Write, BufRead, BufReader};
|
2024-11-26 22:17:15 +01:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use rand::distributions::{Alphanumeric, DistString};
|
|
|
|
|
|
2024-11-27 11:40:37 +01:00
|
|
|
const LOCK_FILE: &str = ".mvwrap";
|
2024-11-26 22:17:15 +01:00
|
|
|
|
2025-04-29 15:18:45 +02:00
|
|
|
enum DirListEntry {
|
|
|
|
|
Path(PathBuf),
|
|
|
|
|
Text(String)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ToString for DirListEntry {
|
|
|
|
|
fn to_string(&self) -> String {
|
|
|
|
|
match self {
|
|
|
|
|
DirListEntry::Text(line) => line.clone(),
|
|
|
|
|
DirListEntry::Path(path) => path.display().to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-26 22:17:15 +01:00
|
|
|
pub struct DirList {
|
|
|
|
|
safe_source: bool,
|
2024-11-27 11:40:37 +01:00
|
|
|
noop: bool,
|
|
|
|
|
verbose: bool,
|
2025-04-29 15:18:45 +02:00
|
|
|
entries: Vec<DirListEntry>,
|
2024-11-26 22:17:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DirList {
|
|
|
|
|
pub fn from_current_dir() -> Self {
|
|
|
|
|
let paths : Vec<PathBuf> = 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()
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-29 15:18:45 +02:00
|
|
|
let mut path_list : Vec<DirListEntry> = vec![];
|
2024-11-26 22:17:15 +01:00
|
|
|
for path in paths {
|
|
|
|
|
let path_string = path.display().to_string();
|
2024-11-27 11:40:37 +01:00
|
|
|
if path_string != format!("./{}", LOCK_FILE) {
|
2025-04-29 15:18:45 +02:00
|
|
|
path_list.push(DirListEntry::Path(path));
|
2024-11-26 22:17:15 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-04-29 15:18:45 +02:00
|
|
|
// This sort is probably not /really/ needed, but just nice
|
|
|
|
|
//path_list.sort();
|
2024-11-26 22:17:15 +01:00
|
|
|
|
|
|
|
|
Self {
|
|
|
|
|
safe_source: true,
|
2024-11-27 11:40:37 +01:00
|
|
|
noop: false,
|
|
|
|
|
verbose: false,
|
2024-11-26 22:17:15 +01:00
|
|
|
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()
|
2025-04-29 15:18:45 +02:00
|
|
|
.map(|l| DirListEntry::Text(l.expect("Could not parse line")))
|
2024-11-26 22:17:15 +01:00
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
Self {
|
|
|
|
|
safe_source: true,
|
2024-11-27 11:40:37 +01:00
|
|
|
noop: false,
|
|
|
|
|
verbose: false,
|
2024-11-26 22:17:15 +01:00
|
|
|
entries: lines,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-28 09:37:51 +01:00
|
|
|
pub fn from_list(list: &[String]) -> Self {
|
2024-11-26 22:17:15 +01:00
|
|
|
Self {
|
|
|
|
|
safe_source: false,
|
2024-11-27 11:40:37 +01:00
|
|
|
noop: false,
|
|
|
|
|
verbose: false,
|
2025-04-29 15:18:45 +02:00
|
|
|
entries: list.iter().map(|l| DirListEntry::Text(l.to_string())).collect(),
|
2024-11-26 22:17:15 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-27 11:40:37 +01:00
|
|
|
pub fn set_noop(&mut self) {
|
|
|
|
|
self.noop = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set_verbose(&mut self) {
|
|
|
|
|
self.verbose = true;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-27 09:50:16 +01:00
|
|
|
pub fn to_file(&self, target_file: &String) {
|
|
|
|
|
let mut file = File::create(target_file).expect("no such file");
|
|
|
|
|
|
|
|
|
|
for entry in &self.entries {
|
2025-04-29 15:18:45 +02:00
|
|
|
match entry {
|
|
|
|
|
DirListEntry::Text(line) => writeln!(file, "{}", line).expect("Unable to write to file"),
|
|
|
|
|
DirListEntry::Path(path) => writeln!(file, "{}", path.display().to_string()).expect("Unable to write to file"),
|
|
|
|
|
}
|
2024-11-27 09:50:16 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-26 22:17:15 +01:00
|
|
|
pub fn move_to(&mut self, target_list: &DirList) -> Result<(), String> {
|
|
|
|
|
let src_len = self.entries.len();
|
|
|
|
|
let mut intermediate_files: HashMap<String, String> = HashMap::new();
|
|
|
|
|
|
2024-11-28 09:37:51 +01:00
|
|
|
self.run_basic_checks(target_list)?;
|
2024-11-27 09:50:16 +01:00
|
|
|
if ! self.safe_source {
|
|
|
|
|
println!("Implement check if target files already exist");
|
|
|
|
|
}
|
2024-11-26 22:17:15 +01:00
|
|
|
|
|
|
|
|
for i in 0..src_len {
|
2025-04-29 15:18:45 +02:00
|
|
|
if self.entries[i].to_string() != target_list.entries[i].to_string() {
|
2024-11-26 22:17:15 +01:00
|
|
|
// is the destination already in the source list?
|
|
|
|
|
// if so, an intermediate file is needed
|
2025-04-29 15:18:45 +02:00
|
|
|
if self.entries.iter().any(|j| j.to_string() == target_list.entries[i].to_string()) {
|
2024-11-28 09:37:51 +01:00
|
|
|
let unique = Self::get_unique_entry(target_list);
|
2025-04-29 15:18:45 +02:00
|
|
|
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"),
|
|
|
|
|
DirListEntry::Path(path) => fs::rename(path.as_path(), &unique).expect("failed to rename file"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if self.verbose { println!("Moving {} -> {}", self.entries[i].to_string(), unique); }
|
2024-11-26 22:17:15 +01:00
|
|
|
} else {
|
2025-04-29 15:18:45 +02:00
|
|
|
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"),
|
|
|
|
|
DirListEntry::Path(path) => fs::rename(path.as_path(), &target_list.entries[i].to_string()).expect("failed to rename file"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if self.verbose { println!("Moving {} -> {}", self.entries[i].to_string(), target_list.entries[i].to_string()); }
|
2024-11-26 22:17:15 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (src, dst) in intermediate_files.iter() {
|
2024-11-27 11:40:37 +01:00
|
|
|
if ! self.noop { fs::rename(src, dst).expect("failed to rename file"); }
|
|
|
|
|
if self.verbose { println!("Moving {} -> {}", src, dst); }
|
2024-11-26 22:17:15 +01:00
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-27 09:50:16 +01:00
|
|
|
fn get_unique_entry(target_list: &DirList) -> String {
|
2024-11-26 22:17:15 +01:00
|
|
|
let mut string = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
|
|
|
|
|
|
2024-11-27 09:50:16 +01:00
|
|
|
// Generate unique name that does not exist in current dir and is not in target_list
|
2025-04-29 15:18:45 +02:00
|
|
|
while target_list.entries.iter().any(|j| j.to_string() == string) || Path::new(&string).is_file() {
|
2024-11-26 22:17:15 +01:00
|
|
|
string = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
|
|
|
|
|
}
|
|
|
|
|
string
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-27 09:50:16 +01:00
|
|
|
fn run_basic_checks(&self, target_list: &DirList) -> Result<(), String> {
|
2024-11-26 22:17:15 +01:00
|
|
|
// 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
|
2025-04-29 15:18:45 +02:00
|
|
|
if target_list.entries.iter().any(|i| i.to_string().is_empty()) {
|
2024-11-26 22:17:15 +01:00
|
|
|
return Err("ERROR: You can't move to empty names.".to_string());
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-27 09:50:16 +01:00
|
|
|
// Make sure all destination files are unique in target_list
|
2025-04-29 15:18:45 +02:00
|
|
|
let unique_entries = target_list.entries.iter().map(|x| (x.to_string(), x.to_string())).collect::<HashMap<_, _>>();
|
2024-11-27 09:50:16 +01:00
|
|
|
if target_list.entries.len() != unique_entries.len() {
|
2024-11-28 09:37:51 +01:00
|
|
|
let doubles = self.show_doubles(target_list);
|
2024-11-26 22:17:15 +01:00
|
|
|
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();
|
|
|
|
|
|
2025-04-29 15:18:45 +02:00
|
|
|
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);
|
2024-11-26 22:17:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (path, lines) in paths.iter() {
|
|
|
|
|
if lines.len() > 1 {
|
|
|
|
|
for i in lines.iter() {
|
2025-04-29 15:18:45 +02:00
|
|
|
doubles = format!("{doubles}\n[line: {}] {} -> {}", i+1, self.entries[*i].to_string(), path);
|
2024-11-26 22:17:15 +01:00
|
|
|
}
|
|
|
|
|
doubles = format!("{doubles}\n");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
doubles
|
|
|
|
|
}
|
|
|
|
|
}
|