use crate::error::NFLZError;
use crate::file_info::{FileInfo, FileInfoWithRenameAdvice};
use crate::math::count_digits_without_leading_zeroes;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct NFLZAssistant {
path: PathBuf,
files_with_rename_info: Vec<FileInfoWithRenameAdvice>,
}
impl NFLZAssistant {
pub fn new<P: AsRef<Path>>(working_dir: P) -> Result<Self, NFLZError> {
let paths = crate::fsutil::read_directory_flat(working_dir.as_ref()).map_err(|err| {
NFLZError::CantReadDirectory(PathBuf::from(working_dir.as_ref()), err)
})?;
let files = files_to_nflz_file_info_vec(paths)?;
let max_digits = find_max_digits(&files);
let mut files = files
.into_iter()
.map(|info| FileInfoWithRenameAdvice::new(info, max_digits))
.collect::<Vec<_>>();
files.sort();
Ok(Self {
path: PathBuf::from(working_dir.as_ref()),
files_with_rename_info: files,
})
}
pub fn check_can_rename_all(&self) -> Result<(), NFLZError> {
check_no_destination_file_already_exists(&self.files_with_rename_info)?;
check_suffixes_and_prefixes_are_unambiguous(&self.files_with_rename_info)?;
Ok(())
}
pub fn rename_all(self) -> Result<Vec<FileInfoWithRenameAdvice>, NFLZError> {
self.check_can_rename_all()?;
for file in self.files_to_rename() {
std::fs::rename(
file.file_info().path(),
file.path_with_new_filename()
.expect("Must be present at this point! Programming error?!"),
)
.map_err(|io_err| {
NFLZError::RenameFailed(
file.file_info().original_filename().to_string(),
file.new_filename().unwrap().to_string(),
io_err,
)
})?;
}
Ok(self.files_with_rename_info)
}
pub fn files_to_rename(&self) -> Vec<&FileInfoWithRenameAdvice> {
self.files_with_rename_info
.iter()
.filter(|new_filename| new_filename.needs_rename())
.collect()
}
pub fn files_without_rename(&self) -> Vec<&FileInfoWithRenameAdvice> {
self.files_with_rename_info
.iter()
.filter(|new_filename| new_filename.is_already_properly_named())
.collect()
}
pub const fn path(&self) -> &PathBuf {
&self.path
}
}
fn files_to_nflz_file_info_vec(paths: Vec<PathBuf>) -> Result<Vec<FileInfo>, NFLZError> {
let mut vec = Vec::with_capacity(paths.len());
for path in paths {
let file = FileInfo::new(path);
match file {
Ok(file) => {
vec.push(file);
}
Err(err) => {
match err {
NFLZError::FilenameMustIncludeExactlyOneNumberedGroup(filename) => {
log::info!("Skipping file '{}'", filename);
continue;
}
NFLZError::ValueInNumberedGroupNotANumber(filename) => {
log::warn!(
"Skipping file '{}' because of invalid number within number group.",
filename
);
continue;
}
_ => (),
}
return Err(err);
}
}
}
Ok(vec)
}
fn find_max_digits(files: &[FileInfo]) -> u64 {
let max_number = files
.iter()
.map(|pf| pf.number_group_value())
.max()
.unwrap_or(0);
count_digits_without_leading_zeroes(max_number)
}
fn check_no_destination_file_already_exists(
files: &[FileInfoWithRenameAdvice],
) -> Result<(), NFLZError> {
let files = files
.iter()
.filter(|file| file.renamed_file_already_exists())
.collect::<Vec<_>>();
if files.is_empty() {
Ok(())
} else {
let paths = files
.iter()
.map(|info| PathBuf::from(info.file_info().path()))
.collect::<Vec<_>>();
Err(NFLZError::ConflictingFiles(paths))
}
}
fn check_suffixes_and_prefixes_are_unambiguous(
pf_list: &[FileInfoWithRenameAdvice],
) -> Result<(), NFLZError> {
let mut prefix_set = HashSet::new();
let mut suffix_set = HashSet::new();
for pf in pf_list {
prefix_set.insert(pf.file_info().filename_prefix());
suffix_set.insert(pf.file_info().filename_suffix());
}
let two_suffixes_only_differ_in_case = {
if suffix_set.len() == 2 {
let mut iter = suffix_set.iter();
let suffix1 = iter.next().unwrap();
let suffix2 = iter.next().unwrap();
suffix1.to_lowercase() == suffix2.to_lowercase()
} else {
false
}
};
if prefix_set.len() > 1 {
Err(NFLZError::AmbiguousPrefixes(
prefix_set
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>(),
))
} else if suffix_set.len() > 1 && !two_suffixes_only_differ_in_case {
Err(NFLZError::AmbiguousSuffixes(
suffix_set
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>(),
))
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::file_info::{FileInfo, FileInfoWithRenameAdvice};
use crate::nflz::check_suffixes_and_prefixes_are_unambiguous;
use crate::NFLZAssistant;
use std::path::Path;
const TEST_DIR_SRC: &str = "./test-resources";
const TEST_DIR_RT: &str = "./.test-resources";
#[test]
fn test_nflz() {
if Path::is_dir(TEST_DIR_RT.as_ref()) {
fs_extra::dir::remove(TEST_DIR_RT).unwrap();
}
fs_extra::dir::copy(
TEST_DIR_SRC,
TEST_DIR_RT,
&fs_extra::dir::CopyOptions {
copy_inside: true,
..Default::default()
},
)
.unwrap();
let assistant = NFLZAssistant::new(TEST_DIR_RT).unwrap();
let files_to_rename = assistant.files_to_rename();
let files_without_rename = assistant.files_without_rename();
assert_eq!(
files_without_rename.len() + files_to_rename.len(),
11,
"must skip file invalid file that doesn't match the pattern!"
);
let actual = files_to_rename
.iter()
.map(|f| f.new_filename().expect("must be available at this point"))
.collect::<Vec<_>>();
assert_eq!(
[
"paris (001).jpg",
"paris (002).jpg",
"paris (003).jpg",
"paris (004).jpg",
"paris (005).jpg",
"paris (006).jpg",
"paris (007).jpg",
"paris (008).jpg",
"paris (009).jpg",
"paris (010).jpg",
],
actual.as_slice()
);
let actual = files_without_rename
.iter()
.map(|f| f.file_info().original_filename())
.collect::<Vec<_>>();
assert_eq!(["paris (734).jpg"], actual.as_slice());
assert!(assistant.check_can_rename_all().is_ok());
let renamed = assistant.rename_all().unwrap();
assert_eq!(renamed.len(), 11);
}
#[test]
#[allow(non_snake_case)]
fn test_check_suffixes_or_prefixes_are_ambiguous__allow_different_font_casing() {
let input = [
FileInfoWithRenameAdvice::new(FileInfo::new("img (1).jpg").unwrap(), 1),
FileInfoWithRenameAdvice::new(FileInfo::new("img (2).JPG").unwrap(), 1),
FileInfoWithRenameAdvice::new(FileInfo::new("img (3).jpg").unwrap(), 1),
];
check_suffixes_and_prefixes_are_unambiguous(&input)
.expect("different font case for file type is allowed");
let input = [
FileInfoWithRenameAdvice::new(FileInfo::new("img (1).jpg").unwrap(), 1),
FileInfoWithRenameAdvice::new(FileInfo::new("IMG (2).jpg").unwrap(), 1),
FileInfoWithRenameAdvice::new(FileInfo::new("img (3).jpg").unwrap(), 1),
];
check_suffixes_and_prefixes_are_unambiguous(&input).expect_err("must fail because different prefixes are used (only different font casing is also an error)");
}
}