use crate::config::{Config, ReplaceMode, RunMode};
use crate::dumpfile;
use crate::error::*;
use crate::fileutils::{cleanup_paths, create_backup, get_paths};
use crate::solver;
use crate::solver::{Operation, Operations, RenameMap};
use any_ascii::any_ascii;
use rayon::prelude::*;
use regex::Replacer;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub struct Renamer {
config: Arc<Config>,
}
impl Renamer {
pub fn new(config: &Arc<Config>) -> Result<Renamer> {
Ok(Renamer {
config: config.clone(),
})
}
pub fn process(&self) -> Result<Operations> {
let operations = match self.config.run_mode {
RunMode::Simple(_) | RunMode::Recursive { .. } => {
let input_paths = get_paths(&self.config.run_mode);
let clean_paths = cleanup_paths(input_paths, self.config.dirs);
let rename_map = self.get_rename_map(&clean_paths)?;
solver::solve_rename_order(&rename_map)?
}
RunMode::FromFile { ref path, undo } => {
let operations = dumpfile::read_from_file(&PathBuf::from(path))?;
if undo {
solver::revert_operations(&operations)?
} else {
operations
}
}
};
if self.config.dump {
dumpfile::dump_to_file(self.config.dump_prefix.clone(), &operations)?;
}
Ok(operations)
}
pub fn batch_rename(&self, operations: Operations) -> Result<()> {
for operation in operations {
self.rename(&operation)?;
}
Ok(())
}
fn replace_match(&self, path: &Path) -> PathBuf {
let file_name = path.file_name().unwrap().to_str().unwrap();
let parent = path.parent();
let target_name = match &self.config.replace_mode {
ReplaceMode::RegExp {
expression,
replacement,
limit,
transform,
} => {
let replacer = TransformReplacer {
replacement,
transform: *transform,
};
expression
.replacen(file_name, *limit, &replacer)
.to_string()
}
ReplaceMode::ToASCII => to_ascii(file_name),
ReplaceMode::None => file_name.to_string(),
};
match parent {
None => PathBuf::from(target_name),
Some(path) => path.join(Path::new(&target_name)),
}
}
fn get_rename_map(&self, paths: &[PathBuf]) -> Result<RenameMap> {
let printer = &self.config.printer;
let colors = &printer.colors;
let mut rename_map = RenameMap::new();
let mut error_string = String::new();
let targets: Vec<(PathBuf, PathBuf)> = paths
.into_par_iter()
.filter_map(|p| {
let target = self.replace_match(p);
if *p != target {
Some((p.clone(), target))
} else {
None
}
})
.collect();
for (source, target) in targets {
if let Some(previous_source) = rename_map.get(&target) {
error_string.push_str(
&colors
.error
.paint(format!(
"\n{0}->{2}\n{1}->{2}\n",
previous_source.display(),
source.display(),
target.display()
))
.to_string(),
);
} else {
rename_map.insert(target, source);
}
}
if !error_string.is_empty() {
return Err(Error {
kind: ErrorKind::SameFilename,
value: Some(error_string),
});
}
Ok(rename_map)
}
fn rename(&self, operation: &Operation) -> Result<()> {
let printer = &self.config.printer;
let colors = &printer.colors;
if self.config.force {
if self.config.backup && !&operation.source.is_dir() {
match create_backup(&operation.source) {
Ok(backup) => printer.print(&format!(
"{} Backup created - {}",
colors.info.paint("Info: "),
colors.source.paint(format!(
"{} -> {}",
operation.source.display(),
backup.display()
))
)),
Err(err) => {
return Err(err);
}
}
}
if let Err(err) = fs::rename(&operation.source, &operation.target) {
return Err(Error {
kind: ErrorKind::Rename,
value: Some(format!(
"{} -> {}\n{}",
operation.source.display(),
operation.target.display(),
err
)),
});
} else {
printer.print_operation(&operation.source, &operation.target);
}
} else {
printer.print_operation(&operation.source, &operation.target);
}
Ok(())
}
}
#[derive(Debug, Copy, Clone)]
pub enum TextTransformation {
Upper,
Lower,
Ascii,
None,
}
impl TextTransformation {
pub fn transform(&self, text: String) -> String {
match self {
TextTransformation::Upper => text.to_uppercase(),
TextTransformation::Lower => text.to_lowercase(),
TextTransformation::Ascii => to_ascii(&text),
TextTransformation::None => text,
}
}
}
struct TransformReplacer<'h> {
replacement: &'h str,
transform: TextTransformation,
}
fn to_ascii(text: &str) -> String {
any_ascii(text).replace("/", "_")
}
impl Replacer for &TransformReplacer<'_> {
fn replace_append(&mut self, caps: ®ex::Captures<'_>, dst: &mut String) {
let mut replaced = String::default();
caps.expand(self.replacement, &mut replaced);
replaced = self.transform.transform(replaced);
dst.push_str(&replaced);
}
}
#[cfg(test)]
mod test {
extern crate tempfile;
use super::*;
use crate::config::RunMode;
use crate::output::Printer;
use regex::Regex;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use tempfile::TempDir;
impl Default for Config {
fn default() -> Self {
Config {
force: true,
backup: false,
dirs: false,
dump: false,
dump_prefix: "rnr-".to_string(),
run_mode: RunMode::Simple(vec![]),
replace_mode: ReplaceMode::None,
printer: Printer::color(),
}
}
}
fn run_with_config(mock_config: Arc<Config>) {
let renamer = match Renamer::new(&mock_config) {
Ok(renamer) => renamer,
Err(err) => {
mock_config.printer.print_error(&err);
panic!("Error initializing renamer");
}
};
let operations = match renamer.process() {
Ok(operations) => operations,
Err(err) => {
mock_config.printer.print_error(&err);
panic!("Error processing");
}
};
if let Err(err) = renamer.batch_rename(operations) {
mock_config.printer.print_error(&err);
panic!("Error renaming");
}
}
fn generate_file_tree() -> (TempDir, String, Vec<String>) {
let temp_dir = tempfile::tempdir().expect("Error creating temp directory");
let temp_path = temp_dir.path().to_str().unwrap().to_string();
let mock_dir = format!("{}/test_dir", temp_path);
let mock_files: Vec<String> = vec![
format!("{}/test_file_1.txt", temp_path),
format!("{}/test_file_2.txt", temp_path),
format!("{}/test_file_1.txt", mock_dir),
format!("{}/test_file_2.txt", mock_dir),
];
fs::create_dir(&mock_dir).expect("Error creating mock directory...");
for file in &mock_files {
fs::File::create(file).expect("Error creating mock file...");
}
(temp_dir, temp_path, mock_files)
}
#[test]
fn rename_files_with_backup() {
let (_temp_dir, temp_path, mock_files) = generate_file_tree();
println!("Running test in '{}'", temp_path);
let mock_config = Arc::new(Config {
backup: true,
run_mode: RunMode::Simple(mock_files),
replace_mode: ReplaceMode::RegExp {
expression: Regex::new("test").unwrap(),
replacement: "passed".to_string(),
limit: 1,
transform: TextTransformation::None,
},
..Config::default()
});
run_with_config(mock_config);
assert!(Path::new(&format!("{}/passed_file_1.txt", temp_path)).exists());
assert!(Path::new(&format!("{}/passed_file_2.txt", temp_path)).exists());
assert!(Path::new(&format!("{}/test_dir/passed_file_1.txt", temp_path)).exists());
assert!(Path::new(&format!("{}/test_dir/passed_file_2.txt", temp_path)).exists());
assert!(Path::new(&format!("{}/test_file_1.txt.bk", temp_path)).exists());
assert!(Path::new(&format!("{}/test_file_2.txt.bk", temp_path)).exists());
assert!(Path::new(&format!("{}/test_dir/test_file_1.txt.bk", temp_path)).exists());
assert!(Path::new(&format!("{}/test_dir/test_file_2.txt.bk", temp_path)).exists());
}
#[test]
fn rename_files_and_directories_recursively_with_backup() {
let (_temp_dir, temp_path, _) = generate_file_tree();
println!("Running test in '{}'", temp_path);
let mock_config = Arc::new(Config {
dirs: true,
backup: true,
run_mode: RunMode::Recursive {
paths: vec![temp_path.clone()],
max_depth: None,
hidden: false,
},
replace_mode: ReplaceMode::RegExp {
expression: Regex::new("test").unwrap(),
replacement: "passed".to_string(),
limit: 1,
transform: TextTransformation::None,
},
..Config::default()
});
run_with_config(mock_config);
assert!(Path::new(&format!("{}/passed_file_1.txt", temp_path)).exists());
assert!(Path::new(&format!("{}/passed_file_2.txt", temp_path)).exists());
assert!(Path::new(&format!("{}/passed_dir/passed_file_1.txt", temp_path)).exists());
assert!(Path::new(&format!("{}/passed_dir/passed_file_2.txt", temp_path)).exists());
assert!(Path::new(&format!("{}/test_file_1.txt.bk", temp_path)).exists());
assert!(Path::new(&format!("{}/test_file_2.txt.bk", temp_path)).exists());
assert!(Path::new(&format!("{}/passed_dir/test_file_1.txt.bk", temp_path)).exists());
assert!(Path::new(&format!("{}/passed_dir/test_file_2.txt.bk", temp_path)).exists());
let directory_backup = &format!("{}/test_dir.bk", temp_path);
assert!(!Path::new(directory_backup).exists());
}
#[test]
fn replace_limit() {
let tempdir = tempfile::tempdir().expect("Error creating temp directory");
println!("Running test in '{:?}'", tempdir);
let temp_path = tempdir.path().to_str().unwrap();
let mock_files: Vec<String> = vec![format!("{}/replace_all_aaaaa.txt", temp_path)];
for file in &mock_files {
fs::File::create(file).expect("Error creating mock file...");
}
let mock_config = Arc::new(Config {
run_mode: RunMode::Simple(mock_files),
replace_mode: ReplaceMode::RegExp {
expression: Regex::new("a").unwrap(),
replacement: "b".to_string(),
limit: 0,
transform: TextTransformation::None,
},
..Config::default()
});
run_with_config(mock_config);
assert!(Path::new(&format!("{}/replbce_bll_bbbbb.txt", temp_path)).exists());
}
#[test]
fn to_ascii() {
let tempdir = tempfile::tempdir().expect("Error creating temp directory");
println!("Running test in '{:?}'", tempdir);
let temp_path = tempdir.path().to_str().unwrap();
let mock_files: Vec<String> = vec![
format!("{}/ǹön-âścîı-lower.txt", temp_path),
format!("{}/ǸÖN-ÂŚCÎI-UPPER.txt", temp_path),
format!("{}/with-slashes-╱.txt", temp_path),
];
for file in &mock_files {
fs::File::create(file).expect("Error creating mock file...");
}
let mock_config = Arc::new(Config {
run_mode: RunMode::Simple(mock_files),
replace_mode: ReplaceMode::ToASCII,
..Config::default()
});
run_with_config(mock_config);
assert!(Path::new(&format!("{}/non-ascii-lower.txt", temp_path)).exists());
assert!(Path::new(&format!("{}/NON-ASCII-UPPER.txt", temp_path)).exists());
assert!(Path::new(&format!("{}/with-slashes-_.txt", temp_path)).exists());
}
#[test]
fn captures_transform() {
let hay = "Thïs-Îs-my-fîle.txt";
let expression = Regex::new(r"(\w+)-(\w+)-my-fîle").unwrap();
let replacement = "${1}.${2}-a-Fïle";
let mut replacer = TransformReplacer {
replacement,
transform: TextTransformation::None,
};
let result = expression.replace(hay, &replacer);
assert_eq!(result, "Thïs.Îs-a-Fïle.txt");
replacer.transform = TextTransformation::Upper;
let result = expression.replace(hay, &replacer);
assert_eq!(result, "THÏS.ÎS-A-FÏLE.txt");
replacer.transform = TextTransformation::Lower;
let result = expression.replace(hay, &replacer);
assert_eq!(result, "thïs.îs-a-fïle.txt");
replacer.transform = TextTransformation::Ascii;
let result = expression.replace(hay, &replacer);
assert_eq!(result, "This.Is-a-File.txt");
}
}