use error::*;
use fileutils::{is_same_file, PathList};
use path_abs::PathAbs;
use std::collections::HashMap;
use std::path::PathBuf;
pub type RenameMap = HashMap<PathBuf, PathBuf>;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Operation {
pub source: PathBuf,
pub target: PathBuf,
}
pub type Operations = Vec<Operation>;
pub fn solve_rename_order(rename_map: &RenameMap) -> Result<Operations> {
let mut level_list: Vec<usize> = rename_map
.values()
.map(|p| p.components().count())
.collect();
level_list.sort();
level_list.dedup();
level_list.reverse();
let mut rename_order = PathList::new();
for level in level_list {
let level_targets: Vec<PathBuf> = rename_map
.keys()
.filter_map(|p| {
if p.components().count() == level {
Some(p.clone())
} else {
None
}
})
.collect();
let mut existing_targets = get_existing_targets(&level_targets, &rename_map)?;
rename_order.append(
&mut level_targets
.iter()
.filter_map(|p| {
if !existing_targets.contains(p) {
Some(p.clone())
} else {
None
}
})
.collect(),
);
match sort_existing_targets(&rename_map, &mut existing_targets) {
Ok(mut targets) => rename_order.append(&mut targets),
Err(err) => return Err(err),
}
}
let mut operations = Operations::new();
for target in rename_order {
operations.push(Operation {
source: rename_map[&target].clone(),
target,
});
}
Ok(operations)
}
pub fn revert_operations(operations: &[Operation]) -> Result<Operations> {
let mut reverse_operations = operations.to_owned();
reverse_operations.reverse();
let inverse_operations = reverse_operations
.into_iter()
.map(|Operation { source, target }| Operation {
source: target,
target: source,
})
.collect();
Ok(inverse_operations)
}
fn get_existing_targets(targets: &[PathBuf], rename_map: &RenameMap) -> Result<PathList> {
let mut existing_targets: PathList = Vec::new();
let sources: PathList = rename_map.values().cloned().collect();
for target in targets {
if target.symlink_metadata().is_ok() {
if !sources.contains(&target) {
let source = rename_map.get(target).cloned().unwrap();
if is_same_file(&source, &target) {
continue;
}
return Err(Error {
kind: ErrorKind::ExistingPath,
value: Some(format!("{} -> {}", source.display(), target.display())),
});
}
existing_targets.push(target.clone());
}
}
Ok(existing_targets)
}
fn sort_existing_targets(
rename_map: &RenameMap,
existing_targets: &mut PathList,
) -> Result<PathList> {
let mut ordered_targets: PathList = Vec::new();
while !existing_targets.is_empty() {
let mut selected_index: Option<usize> = None;
let sources: PathList = existing_targets
.iter()
.map(|x| rename_map.get(x).cloned().unwrap())
.map(|p| PathAbs::new(p).unwrap().to_path_buf())
.collect();
for (index, target) in existing_targets.iter().enumerate() {
if sources.contains(&PathAbs::new(target).unwrap().to_path_buf()) {
continue;
} else {
selected_index = Some(index);
break;
}
}
match selected_index {
Some(index) => ordered_targets.push(existing_targets.swap_remove(index)),
None => {
return Err(Error {
kind: ErrorKind::SolveOrder,
value: None,
})
}
}
}
Ok(ordered_targets)
}
#[cfg(test)]
mod test {
extern crate tempfile;
use super::*;
use fileutils::create_symlink;
use std::fs;
#[test]
fn test_existing_targets() {
let tempdir = tempfile::tempdir().expect("Error creating temp directory");
println!("Running test in '{:?}'", tempdir);
let temp_path = tempdir.path().to_str().unwrap();
let mock_sources: PathList = vec![
[temp_path, "a.txt"].iter().collect(),
[temp_path, "aa.txt"].iter().collect(),
[temp_path, "aaa.txt"].iter().collect(),
[temp_path, "aaaa.txt"].iter().collect(),
[temp_path, "aaaaa.txt"].iter().collect(),
[temp_path, "test.txt"].iter().collect(),
];
for file in &mock_sources {
fs::File::create(&file).expect("Error creating mock file...");
}
let mock_targets: PathList = vec![
[temp_path, "aa.txt"].iter().collect(),
[temp_path, "aaa.txt"].iter().collect(),
[temp_path, "aaaa.txt"].iter().collect(),
[temp_path, "aaaaa.txt"].iter().collect(),
[temp_path, "aaaaaa.txt"].iter().collect(),
[temp_path, "atest.txt"].iter().collect(),
];
let mock_rename_map: RenameMap = mock_targets
.clone()
.into_iter()
.zip(mock_sources.into_iter())
.collect();
let existing_targets = get_existing_targets(&mock_targets, &mock_rename_map)
.expect("Error getting existing targets.");
assert!(existing_targets.contains(&mock_targets[0]));
assert!(existing_targets.contains(&mock_targets[1]));
assert!(existing_targets.contains(&mock_targets[2]));
assert!(existing_targets.contains(&mock_targets[3]));
assert!(!existing_targets.contains(&mock_targets[4]));
assert!(!existing_targets.contains(&mock_targets[5]));
}
#[test]
fn test_existing_targets_symlinks() {
let tempdir = tempfile::tempdir().expect("Error creating temp directory");
println!("Running test in '{:?}'", tempdir);
let temp_path = tempdir.path().to_str().unwrap();
let mock_sources: PathList = vec![
[temp_path, "a.txt"].iter().collect(),
[temp_path, "aa.txt"].iter().collect(),
[temp_path, "aaa.txt"].iter().collect(),
];
fs::File::create(&mock_sources[0]).expect("Error creating mock file...");
create_symlink(&mock_sources[0], &mock_sources[1]).expect("Error creating symlink.");
create_symlink(&PathBuf::from("broken_link"), &mock_sources[2])
.expect("Error creating broken symlink.");
let mock_targets: PathList = vec![
[temp_path, "aa.txt"].iter().collect(),
[temp_path, "aaa.txt"].iter().collect(),
[temp_path, "aaaa.txt"].iter().collect(),
];
let mock_rename_map: RenameMap = mock_targets
.clone()
.into_iter()
.zip(mock_sources.into_iter())
.collect();
let existing_targets = get_existing_targets(&mock_targets, &mock_rename_map)
.expect("Error getting existing targets.");
assert!(existing_targets.contains(&mock_targets[0]));
assert!(existing_targets.contains(&mock_targets[1]));
assert!(!existing_targets.contains(&mock_targets[2]));
}
#[test]
fn test_sort_existing_targets() {
let tempdir = tempfile::tempdir().expect("Error creating temp directory");
println!("Running test in '{:?}'", tempdir);
let temp_path = tempdir.path().to_str().unwrap();
let mock_sources: PathList = vec![
[temp_path, "a.txt"].iter().collect(),
[temp_path, "aa.txt"].iter().collect(),
[temp_path, "aaa.txt"].iter().collect(),
[temp_path, "aaaa.txt"].iter().collect(),
];
for file in &mock_sources {
fs::File::create(&file).expect("Error creating mock file...");
}
let mock_targets: PathList = vec![
[temp_path, "aa.txt"].iter().collect(),
[temp_path, "aaa.txt"].iter().collect(),
[temp_path, "aaaa.txt"].iter().collect(),
[temp_path, "aaaaa.txt"].iter().collect(),
];
let mock_rename_map: RenameMap = mock_targets
.clone()
.into_iter()
.zip(mock_sources.into_iter())
.collect();
let mut mock_existing_targets: PathList = vec![
[temp_path, "aa.txt"].iter().collect(),
[temp_path, "aaa.txt"].iter().collect(),
[temp_path, "aaaa.txt"].iter().collect(),
];
let ordered_targets = sort_existing_targets(&mock_rename_map, &mut mock_existing_targets)
.expect("Failed to order existing_targets.");
assert_eq!(
ordered_targets[0],
[temp_path, "aaaa.txt"].iter().collect::<PathBuf>()
);
assert_eq!(
ordered_targets[1],
[temp_path, "aaa.txt"].iter().collect::<PathBuf>()
);
assert_eq!(
ordered_targets[2],
[temp_path, "aa.txt"].iter().collect::<PathBuf>()
);
}
#[test]
fn test_solve_rename_order() {
let tempdir = tempfile::tempdir().expect("Error creating temp directory");
println!("Running test in '{:?}'", tempdir);
let temp_path = tempdir.path().to_str().unwrap();
let mock_sources: PathList = vec![
[temp_path, "a.txt"].iter().collect(),
[temp_path, "aa.txt"].iter().collect(),
[temp_path, "aaa.txt"].iter().collect(),
[temp_path, "aaaa.txt"].iter().collect(),
[temp_path, "aaaaa.txt"].iter().collect(),
];
for file in &mock_sources {
fs::File::create(&file).expect("Error creating mock file...");
}
let mock_targets: PathList = vec![
[temp_path, "aa.txt"].iter().collect(),
[temp_path, "aaa.txt"].iter().collect(),
[temp_path, "aaaa.txt"].iter().collect(),
[temp_path, "aaaaa.txt"].iter().collect(),
[temp_path, "aaaaaa.txt"].iter().collect(),
];
let mock_rename_map: RenameMap = mock_targets
.clone()
.into_iter()
.zip(mock_sources.into_iter())
.collect();
let operations =
solve_rename_order(&mock_rename_map).expect("Failed to solve rename order.");
assert_eq!(operations[0].target, mock_targets[4]);
assert_eq!(operations[1].target, mock_targets[3]);
assert_eq!(operations[2].target, mock_targets[2]);
assert_eq!(operations[3].target, mock_targets[1]);
assert_eq!(operations[4].target, mock_targets[0]);
}
}