use clap::ArgMatches;
use dialoguer::Input;
use regex::Regex;
use std::{
cmp::Ordering,
fs,
path::{Path, PathBuf},
process,
};
pub struct Arguments {
pub path: PathBuf,
pub logfile: bool,
pub yes: bool,
pub nro: usize,
pub zeroes: usize,
pub prefix: String,
pub mode: String,
pub ordering: String,
pub ignore: Vec<usize>,
pub file_extension: Regex,
pub file_numbers_test: Regex,
pub file_numbers: Regex,
}
impl From<ArgMatches> for Arguments {
fn from(a: ArgMatches) -> Self {
let path: PathBuf = {
let folder: String = a.value_of_t_or_exit("folder");
Path::new(&folder).to_owned()
};
let logfile = a.is_present("logfile");
let yes = a.is_present("yes");
let nro: usize = if a.is_present("nro") {
a.value_of_t_or_exit("nro")
} else {
1
};
let zeroes: usize = if a.is_present("zeroes") {
a.value_of_t_or_exit("zeroes")
} else {
7
};
let prefix: String = if a.is_present("prefix") {
a.value_of_t_or_exit("prefix")
} else {
"".to_string()
};
let mode: String = if a.is_present("mode") {
a.value_of_t_or_exit("mode")
} else {
"n".to_string()
};
let ordering: String = if a.is_present("ordering") {
a.value_of_t_or_exit("ordering")
} else {
"a".to_string()
};
let ignore: Vec<usize> = if a.is_present("ignore") {
a.values_of_t_or_exit("ignore")
} else {
vec![]
};
let file_extension =
Regex::new(r"(?i)\.[0-9A-Z]+$").expect("Unable to create 'file_extension' regex");
let file_numbers_test = Regex::new(format!(r"(?i)(^{}\d+)\.[0-9A-Z]+$", prefix).as_str())
.expect("Unable to create 'file_numbers_test' regex");
let file_numbers = Regex::new(&format!(r"(?i)(\d{{{}}})\.[0-9A-Z]+$", zeroes))
.expect("Unable to create 'file_numbers' regex");
Self {
path,
logfile,
yes,
nro,
zeroes,
prefix,
mode,
ordering,
ignore,
file_extension,
file_numbers_test,
file_numbers,
}
}
}
pub fn rename(args: Arguments) {
if !args.path.exists() || !args.path.is_dir() {
eprintln!("Error: Folder doesn't exists or is not a folder");
process::exit(1);
}
let files: Vec<String> = {
let mut ready: Vec<String> = Vec::new();
let mut i: Vec<(String, u128)> = Vec::new();
for file in args
.path
.read_dir()
.expect("Unable to read path contents")
.map(|x| x.unwrap())
{
if file.file_type().unwrap().is_dir() {
continue;
}
let file_name = file
.file_name()
.into_string()
.expect("Unable to read file name");
let file_date: u128 =
fs::metadata(format!("{:?}/{}", args.path, file_name).replace("\"", ""))
.unwrap()
.modified()
.unwrap()
.elapsed()
.unwrap()
.as_micros();
i.push((file_name, file_date));
}
if args.ordering == "n" {
ready = order_by_date(i);
} else if args.ordering == "o" {
ready = order_by_date(i).into_iter().rev().collect();
} else if args.ordering == "z" {
for item in i {
ready.push(item.0);
}
ready.sort();
ready.reverse();
} else {
for item in i {
ready.push(item.0);
}
ready.sort();
}
ready
};
let total_nro: usize = files.len();
let files_to_rename: Vec<(String, String)> = match args.mode.as_str() {
"n" => normal_rename(files, total_nro, &args),
"f" => number_fixing(files, &args),
_ => panic!("Mode not found"),
};
if !files_to_rename.is_empty() {
rename_files(files_to_rename, total_nro, &args);
} else {
println!("Nothing to rename.\nExiting...");
}
}
fn order_by_date(list: Vec<(String, u128)>) -> Vec<String> {
let mut i: Vec<String> = Vec::new();
let mut numbers: Vec<u128> = Vec::new();
for item in &list {
numbers.push(item.1);
}
numbers.sort_unstable();
for number in numbers {
i.push(
list[list.iter().position(|x| x.1 == number).unwrap()]
.0
.to_owned(),
);
}
i
}
fn normal_rename(files: Vec<String>, total_nro: usize, args: &Arguments) -> Vec<(String, String)> {
let mut files_to_rename: Vec<(String, String)> = Vec::new();
let mut nro_to_skip: Vec<usize> = Vec::new();
if args.nro != 1 {
for nro in 1..args.nro {
nro_to_skip.push(nro);
}
}
if !args.ignore.is_empty() {
nro_to_skip.extend(args.ignore.iter());
}
let mut sorted_files = files.clone();
sorted_files.sort();
for file in &sorted_files {
match args.file_numbers.captures(&file) {
Some(i) => {
let i_str = i.get(1).unwrap().as_str();
let i_nro: usize = i_str.parse().unwrap();
if (!i_nro > total_nro + (args.nro - 1)
&& i_nro >= args.nro
&& i_str.len() == args.zeroes)
&& (i_nro == 1 || !nro_to_skip.is_empty() && nro_to_skip.contains(&(i_nro - 1)))
{
nro_to_skip.push(i_nro);
}
}
None => {
continue;
}
};
}
for (index, file) in files.iter().enumerate() {
let mut index = index + args.nro;
while nro_to_skip.contains(&index) {
index += 1;
}
let ext = match args.file_extension.find(&file) {
Some(i) => i.as_str().to_string(),
None => {
eprintln!("Unable to get file extension of \"{}\"with regex", &file);
process::exit(1);
}
};
if !args.file_numbers_test.is_match(&file) {
nro_to_skip.push(index);
files_to_rename.push((
file.to_owned(),
format!("{}{}", generate_name(index, args.zeroes, &args.prefix), ext),
));
continue;
}
let ii = match args.file_numbers.captures(&file) {
Some(i) => i.get(1).unwrap().as_str(),
None => {
nro_to_skip.push(index);
files_to_rename.push((
file.to_owned(),
format!("{}{}", generate_name(index, args.zeroes, &args.prefix), ext),
));
continue;
}
};
if ii
.parse::<usize>()
.expect("Unable to turn regex string into usize")
> total_nro + (args.nro - 1)
|| ii
.parse::<usize>()
.expect("Unable to turn regex string into usize")
< args.nro
|| !ii.len() == args.zeroes
{
nro_to_skip.push(index);
files_to_rename.push((
file.to_owned(),
format!("{}{}", generate_name(index, args.zeroes, &args.prefix), ext),
));
} else if !nro_to_skip.contains(
&ii.parse::<usize>()
.expect("Unable to turn regex string into usize"),
) {
nro_to_skip.push(index);
files_to_rename.push((
file.to_owned(),
format!("{}{}", generate_name(index, args.zeroes, &args.prefix), ext),
));
continue;
} else {
continue;
}
}
files_to_rename
}
fn number_fixing(files: Vec<String>, args: &Arguments) -> Vec<(String, String)> {
let mut files_to_rename: Vec<(String, String)> = Vec::new();
let mut nros_missing: Vec<usize> = Vec::new();
let mut sorted_files = files.clone();
sorted_files.sort();
let nros: Vec<usize> = {
let mut i: Vec<usize> = Vec::new();
for file in &sorted_files {
if !args.file_numbers_test.is_match(&file) {
continue;
}
match args.file_numbers.captures(&file) {
Some(j) => {
if j.get(1).unwrap().as_str().len() == args.zeroes {
i.push(j.get(1).unwrap().as_str().parse().unwrap());
}
}
None => {
continue;
}
};
}
if !args.ignore.is_empty() {
i.extend(args.ignore.iter());
i.sort_unstable();
}
i
};
let mut offset: usize = 0;
for (index, number) in nros.iter().enumerate() {
let index = index + args.nro - offset;
if *number < args.nro {
offset += 1;
} else if index != *number {
nros_missing.push(index);
}
}
if !nros_missing.is_empty() {
let mut count = 0;
for file in &files {
if count >= nros_missing.len() {
break;
}
if !args.file_numbers_test.is_match(&file) {
continue;
}
let ii = match args.file_numbers.captures(&file) {
Some(i) => i.get(1).unwrap().as_str().to_string(),
None => {
eprintln!("Unable to get numbers from \"{}\" with regex", &file);
process::exit(1);
}
};
match ii.parse::<usize>().unwrap().cmp(&nros_missing[count]) {
Ordering::Equal => {
count += 1;
}
Ordering::Less => {
continue;
}
_ => {
if ii.parse::<usize>().unwrap() < args.nro {
continue;
}
let ext = match args.file_extension.find(&file) {
Some(i) => i.as_str().to_string(),
None => {
eprintln!("Unable to get file extension from \"{}\" with regex", &file);
process::exit(1);
}
};
files_to_rename.push((
file.to_owned(),
format!(
"{}{}",
generate_name(nros_missing[count], args.zeroes, &args.prefix),
ext
),
));
count += 1;
}
}
}
let mut count: usize = match args
.file_numbers
.captures(&files_to_rename.last().unwrap().1)
{
Some(i) => {
let i: usize = i.get(1).unwrap().as_str().parse().unwrap();
i + 1
}
None => {
eprintln!(
"Unable to get numbers from \"{}\" with regex",
&files_to_rename.last().unwrap().1
);
eprintln!("Did you run normal mode first?");
process::exit(1);
}
};
for file in &files {
if !files_to_rename.iter().any(|i| &i.0 == file) {
let nro: usize = match args.file_numbers.captures(&file) {
Some(i) => i.get(1).unwrap().as_str().parse().unwrap(),
None => {
eprintln!("Unable to get numbers from \"{}\" with regex", &file);
eprintln!("Did you run normal mode first?");
process::exit(1);
}
};
if nro >= count || nro >= args.nro {
continue;
}
let ext = match args.file_extension.find(&file) {
Some(i) => i.as_str().to_string(),
None => {
eprintln!("Unable to get file extension from \"{}\" with regex", &file);
process::exit(1);
}
};
files_to_rename.push((
file.to_owned(),
format!("{}{}", generate_name(count, args.zeroes, &args.prefix), ext),
));
count += 1;
}
}
} else {
println!("Nothing to rename.\nExiting...");
process::exit(0);
}
files_to_rename
}
fn rename_files(files_to_rename: Vec<(String, String)>, total_nro: usize, args: &Arguments) {
if !args.yes {
println!("These will be renamed:");
for file in &files_to_rename {
println!("{} -> {}", file.0, file.1);
}
println!("{:_<20}", "");
println!("Renaming {} of {}", files_to_rename.len(), total_nro);
let y = Input::<String>::new()
.with_prompt("Are you sure you want to rename")
.interact()
.unwrap();
if y.to_lowercase() != "y" {
println!("Aborting");
process::exit(0);
} else if !args.logfile {
let y = Input::<String>::new()
.with_prompt("Do you want to save a log file")
.interact()
.unwrap();
if y.to_lowercase() == "y" {
create_log(&files_to_rename, &args.path);
}
} else {
create_log(&files_to_rename, &args.path);
}
} else if args.logfile {
create_log(&files_to_rename, &args.path);
}
if args.yes {
println!("Renaming:");
for file in &files_to_rename {
println!("{} -> {}", file.0, file.1);
}
println!("{:_<20}", "");
println!("Renaming {} of {}", files_to_rename.len(), total_nro);
}
for (index, file) in files_to_rename.iter().enumerate() {
if let Err(e) = fs::rename(
format!("{}/{}", args.path.display(), file.0),
format!("{}/{}{}", args.path.display(), index, file.1),
) {
eprint!("{}", e);
}
}
for (index, file) in files_to_rename.iter().enumerate() {
if let Err(e) = fs::rename(
format!("{}/{}{}", args.path.display(), index, file.1),
format!("{}/{}", args.path.display(), file.1),
) {
eprint!("{}", e);
}
}
}
fn generate_name(number: usize, zeroes: usize, prefix: &str) -> String {
format!(
"{pre}{:0<1$}{nu}",
"",
zeroes - number.to_string().len(),
nu = number,
pre = prefix
)
}
fn create_log(files: &[(String, String)], path: &Path) {
let text = {
let mut i: String = String::new();
for file in files {
i += format!("{} -> {}\n", file.0, file.1).as_str();
}
i
};
let mut file_name: String = "rename.log".to_string();
let mut count = 0;
while Path::new(&format!("{}/{}", path.display(), file_name)).exists() {
file_name = format!("rename.{}.log", count);
count += 1;
}
let file_name = format!("{}/{}", path.display(), file_name);
fs::write(file_name, text).expect("Unable to create a logfile");
}
#[cfg(test)]
mod tests;