use clap::ArgMatches;
use colored::*;
use dialoguer::Input;
use lazy_regex::{regex, Lazy, 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: &'static Lazy<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.get_one("folder").unwrap();
Path::new(folder).to_owned()
};
let logfile = a.get_flag("logfile");
let yes = a.get_flag("yes");
let nro: usize = *a.get_one("nro").unwrap_or(&1);
let zeroes: usize = *a.get_one("zeroes").unwrap_or(&7);
let prefix: String = a.get_one("prefix").unwrap_or(&"".to_string()).to_owned();
let mode: String = a.get_one("mode").unwrap_or(&"n".to_string()).to_owned();
let ordering: String = a.get_one("ordering").unwrap_or(&"a".to_string()).to_owned();
let ignore: Vec<usize> = if let Some(i) = a.try_get_many("ignore").unwrap() {
i.copied().collect()
} else {
vec![]
};
let file_extension = regex!(r"(?i)\.[0-9A-Z]+$");
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).as_str())
.expect("Unable to create 'file_numbers' regex");
Self {
path,
logfile,
yes,
nro,
zeroes,
prefix,
mode,
ordering,
ignore,
file_extension,
file_numbers_test,
file_numbers,
}
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
pub enum LogLevel {
Verbose,
Standard,
Quiet,
}
impl LogLevel {
pub fn is_verbose(&self) -> bool {
matches!(self, LogLevel::Verbose)
}
pub fn is_standard(&self) -> bool {
matches!(self, LogLevel::Standard)
}
pub fn is_quiet(&self) -> bool {
matches!(self, LogLevel::Quiet)
}
}
pub fn rename(args: Arguments, log_level: LogLevel) {
if !args.path.exists() || !args.path.is_dir() {
eprintln!(
"{} {}",
"[Error]:".red(),
"Folder doesn't exists or is not a folder".bright_red()
);
process::exit(1);
}
if log_level.is_verbose() {
println!("{} {}", "[Verbose]".bright_black(), "Checking files".blue());
}
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()
|| file.file_name().to_str().unwrap().starts_with('.')
{
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();
if log_level.is_verbose() {
println!(
"{} {}",
"[Verbose]".bright_black(),
"Generating file names".blue()
);
}
let files_to_rename: Vec<(String, String)> = match args.mode.as_str() {
"n" => normal_rename(files, total_nro, &args),
"a" => add_to_the_end(files, total_nro, &args),
"f" => number_fixing(files, &args, log_level),
_ => panic!("Mode not found"),
};
if !files_to_rename.is_empty() {
rename_files(files_to_rename, total_nro, &args, log_level);
} else if !log_level.is_quiet() {
println!(
"{} {}",
"[Info]:".green(),
"Nothing to rename. Exiting...".white()
)
}
}
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!(
"{} {}{}{}",
"[Error]:".red(),
"Unable to get file extension of \"".bright_red(),
&file.yellow(),
"\" with regex".bright_red()
);
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 add_to_the_end(files: Vec<String>, total_nro: usize, args: &Arguments) -> Vec<(String, String)> {
let mut files_to_rename: Vec<(String, String)> = Vec::new();
let mut files_to_edit: Vec<String> = Vec::new();
let mut files_numbered: Vec<String> = Vec::new();
for file in &files {
if !args.file_numbers_test.is_match(file) {
files_to_edit.push(file.to_owned());
} else {
files_numbered.push(file.to_owned());
}
}
files_numbered.sort();
let biggest_number: usize = match args.file_numbers.captures(files_numbered.last().unwrap()) {
Some(i) => i.get(1).unwrap().as_str().parse().unwrap(),
None => {
eprintln!(
"{} {}{}{}",
"[Error]:".red(),
"Unable to get numbers from \"".bright_red(),
&files_numbered.last().unwrap().yellow(),
"\" with regex".bright_red()
);
process::exit(1);
}
};
for (index, file) in files_to_edit.iter().enumerate() {
let index = index + biggest_number + 1;
let ext = match args.file_extension.find(file) {
Some(i) => i.as_str().to_string(),
None => {
eprintln!(
"{} {}{}{}",
"[Error]:".red(),
"Unable to get file extension of \"".bright_red(),
&file.yellow(),
"\" with regex".bright_red()
);
process::exit(1);
}
};
if !args.file_numbers_test.is_match(file) {
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 => {
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")
> biggest_number
|| ii
.parse::<usize>()
.expect("Unable to turn regex string into usize")
< args.nro
|| !ii.len() == args.zeroes
{
files_to_rename.push((
file.to_owned(),
format!("{}{}", generate_name(index, args.zeroes, &args.prefix), ext),
));
} else if !total_nro
== ii
.parse::<usize>()
.expect("Unable to turn regex string into usize")
{
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,
log_level: LogLevel,
) -> 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!(
"{} {}{}{}",
"[Error]".red(),
"Unable to get numbers from \"".bright_red(),
&file.yellow(),
"\" with regex".bright_red()
);
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!(
"{} {}{}{}",
"[Error]:".red(),
"Unable to get file extension from \"".bright_red(),
&file.yellow(),
"\" with regex".bright_red()
);
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!(
"{} {}{}{}",
"[Error]:".red(),
"Unable to get numbers from \"".bright_red(),
&files_to_rename.last().unwrap().1.yellow(),
"\" with regex".bright_red()
);
if !log_level.is_quiet() {
println!(
"{} {}",
"[Info]:".green(),
"Did you run normal mode first?".white()
);
}
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!(
"{} {}{}{}",
"[Error]:".red(),
"Unable to get numbers from \"".bright_red(),
&file.yellow(),
"\" with regex".bright_red()
);
if !log_level.is_quiet() {
println!(
"{} {}",
"[Info]:".green(),
"Did you run normal mode first?".white()
);
}
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!(
"{} {}{}{}",
"[Error]:".red(),
"Unable to get file extension from \"".bright_red(),
&file.yellow(),
"\" with regex".bright_red()
);
process::exit(1);
}
};
files_to_rename.push((
file.to_owned(),
format!("{}{}", generate_name(count, args.zeroes, &args.prefix), ext),
));
count += 1;
}
}
} else {
if !log_level.is_quiet() {
println!(
"{} {}",
"[Info]:".green(),
"Nothing to rename. Exiting...".white()
)
}
process::exit(0);
}
files_to_rename
}
fn rename_files(
files_to_rename: Vec<(String, String)>,
total_nro: usize,
args: &Arguments,
log_level: LogLevel,
) {
if !args.yes {
if !log_level.is_quiet() {
println!("{} {}", "[Info]:".green(), "These will be renamed:".white());
for file in &files_to_rename {
println!("{}{}{}", file.0.yellow(), " -> ".white(), file.1.yellow());
}
println!("{:_<20}", "");
}
println!(
"{}{}{}{}",
"Renaming ".white(),
format!("{}", files_to_rename.len()).yellow(),
" of ".white(),
format!("{}", total_nro).yellow(),
);
let y = Input::<String>::new()
.with_prompt("Are you sure you want to rename [y/N]")
.default("n".to_string())
.show_default(false)
.interact()
.unwrap();
if y.to_lowercase() != "y" {
println!("{}", "Aborting".bright_red());
process::exit(0);
} else if !args.logfile {
let y = Input::<String>::new()
.with_prompt("Do you want to save a log file [y/N]")
.default("n".to_string())
.show_default(false)
.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 && log_level.is_verbose() {
println!("{} {}", "[Verbose]:".bright_black(), "Renaming".blue());
for file in &files_to_rename {
println!("{}{}{}", file.0.yellow(), " -> ".blue(), file.1.yellow());
}
println!("{:_<20}", "");
println!(
"{}{}{}{}",
"Renaming ".white(),
format!("{}", files_to_rename.len()).yellow(),
" of ".white(),
format!("{}", total_nro).yellow(),
);
}
if log_level.is_verbose() {
println!("{} {}", "[Verbose]".bright_black(), "Renaming files".blue());
}
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),
) {
eprintln!("{} {}", "[Error]:".red(), e.to_string().bright_red());
}
}
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),
) {
eprintln!("{} {}", "[Error]:".red(), e.to_string().bright_red());
}
}
}
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;