use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CaseTransform {
Lowercase,
Uppercase,
Capitalize,
None,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SpaceReplace {
Underscore,
Hyphen,
None,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TimestampFormat {
Long,
Short,
None,
}
#[derive(Debug, Clone)]
pub struct RenameOptions {
pub case_transform: CaseTransform,
pub space_replace: SpaceReplace,
pub add_prefix: Option<String>,
pub remove_prefix: Option<String>,
pub add_suffix: Option<String>,
pub remove_suffix: Option<String>,
pub replace_prefix: Option<(String, String)>,
pub replace_suffix: Option<(String, String)>,
pub timestamp_format: TimestampFormat,
pub recursive: bool,
pub dry_run: bool,
pub include_symlinks: bool,
}
impl Default for RenameOptions {
fn default() -> Self {
RenameOptions {
case_transform: CaseTransform::None,
space_replace: SpaceReplace::None,
add_prefix: None,
remove_prefix: None,
add_suffix: None,
remove_suffix: None,
replace_prefix: None,
replace_suffix: None,
timestamp_format: TimestampFormat::None,
recursive: true,
dry_run: false,
include_symlinks: false,
}
}
}
pub struct FileRenamer {
options: RenameOptions,
}
impl FileRenamer {
pub fn new(options: RenameOptions) -> Self {
FileRenamer { options }
}
pub fn with_defaults() -> Self {
FileRenamer {
options: RenameOptions::default(),
}
}
fn should_process(&self, path: &Path, is_symlink: bool) -> bool {
if is_symlink && !self.options.include_symlinks {
return false;
}
if !is_symlink && !path.is_file() {
return false;
}
if let Some(name) = path.file_name() {
if name.to_str().map(|s| s.starts_with('.')).unwrap_or(false) {
return false;
}
}
true
}
fn detect_separator(name: &str) -> char {
let hyphen_count = name.chars().filter(|&c| c == '-').count();
let underscore_count = name.chars().filter(|&c| c == '_').count();
let space_count = name.chars().filter(|&c| c == ' ').count();
if space_count > 0 {
return '-';
}
if hyphen_count > underscore_count {
'-'
} else if underscore_count > hyphen_count {
'_'
} else {
'-'
}
}
fn format_timestamp(&self, path: &Path, separator: char) -> Option<String> {
use std::time::SystemTime;
match self.options.timestamp_format {
TimestampFormat::None => None,
TimestampFormat::Long | TimestampFormat::Short => {
let metadata = fs::metadata(path).ok()?;
let created = metadata.created().or_else(|_| metadata.modified()).ok()?;
let duration = created.duration_since(SystemTime::UNIX_EPOCH).ok()?;
let secs = duration.as_secs();
let days = secs / 86400;
let mut year = 1970;
let mut remaining_days = days;
loop {
let days_in_year = if Self::is_leap_year(year) { 366 } else { 365 };
if remaining_days < days_in_year {
break;
}
remaining_days -= days_in_year;
year += 1;
}
let days_in_months = if Self::is_leap_year(year) {
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
} else {
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
let mut month = 1;
let mut day_of_month = remaining_days + 1;
for days_in_month in days_in_months.iter() {
if day_of_month <= *days_in_month as u64 {
break;
}
day_of_month -= *days_in_month as u64;
month += 1;
}
match self.options.timestamp_format {
TimestampFormat::Long => Some(format!(
"{:04}{:02}{:02}{}",
year, month, day_of_month, separator
)),
TimestampFormat::Short => Some(format!(
"{:02}{:02}{:02}{}",
year % 100,
month,
day_of_month,
separator
)),
TimestampFormat::None => None,
}
}
}
}
fn is_leap_year(year: u64) -> bool {
year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
}
fn transform_name(
&self,
name: &str,
extension: Option<&str>,
timestamp: Option<String>,
) -> String {
let mut result = name.to_string();
if let Some(prefix) = &self.options.remove_prefix {
if result.starts_with(prefix) {
result = result[prefix.len()..].to_string();
}
}
if let Some(suffix) = &self.options.remove_suffix {
if result.ends_with(suffix) {
result = result[..result.len() - suffix.len()].to_string();
}
}
if let Some((old_prefix, new_prefix)) = &self.options.replace_prefix {
if result.starts_with(old_prefix) {
result = format!("{}{}", new_prefix, &result[old_prefix.len()..]);
}
}
if let Some((old_suffix, new_suffix)) = &self.options.replace_suffix {
if result.ends_with(old_suffix) {
result = format!(
"{}{}",
&result[..result.len() - old_suffix.len()],
new_suffix
);
}
}
match self.options.space_replace {
SpaceReplace::Underscore => {
result = result.replace([' ', '-'], "_");
}
SpaceReplace::Hyphen => {
result = result.replace([' ', '_'], "-");
}
SpaceReplace::None => {}
}
match self.options.case_transform {
CaseTransform::Lowercase => {
result = result.to_lowercase();
}
CaseTransform::Uppercase => {
result = result.to_uppercase();
}
CaseTransform::Capitalize => {
if !result.is_empty() {
let mut chars = result.chars();
if let Some(first) = chars.next() {
result = first.to_uppercase().collect::<String>()
+ &chars.as_str().to_lowercase();
}
}
}
CaseTransform::None => {}
}
if let Some(ts) = timestamp {
result = format!("{}{}", ts, result);
}
if let Some(prefix) = &self.options.add_prefix {
result = format!("{}{}", prefix, result);
}
if let Some(suffix) = &self.options.add_suffix {
result = format!("{}{}", result, suffix);
}
if let Some(ext) = extension {
result = format!("{}.{}", result, ext);
}
result
}
pub fn rename_file(&self, path: &Path, is_symlink: bool) -> crate::Result<bool> {
if !self.should_process(path, is_symlink) {
return Ok(false);
}
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| anyhow::anyhow!("Invalid filename"))?;
let (name, extension) = if let Some(pos) = file_name.rfind('.') {
let name = &file_name[..pos];
let ext = &file_name[pos + 1..];
(name, Some(ext))
} else {
(file_name, None)
};
let separator = Self::detect_separator(name);
let timestamp = self.format_timestamp(path, separator);
let new_name = self.transform_name(name, extension, timestamp);
if new_name == file_name {
return Ok(false);
}
let parent = path
.parent()
.ok_or_else(|| anyhow::anyhow!("No parent directory"))?;
let new_path = parent.join(&new_name);
if new_path.exists() {
let same_file = match (path.canonicalize(), new_path.canonicalize()) {
(Ok(p1), Ok(p2)) => p1 == p2,
_ => false,
};
if !same_file {
return Err(anyhow::anyhow!(
"Target file already exists: '{}'",
new_path.display()
));
}
}
if self.options.dry_run {
println!(
"Would rename '{}' -> '{}'",
path.display(),
new_path.display()
);
} else {
fs::rename(path, &new_path)?;
println!("Renamed '{}' -> '{}'", path.display(), new_path.display());
}
Ok(true)
}
pub fn process(&self, path: &Path) -> crate::Result<usize> {
let mut renamed_count = 0;
let path_is_symlink = path
.symlink_metadata()
.map(|m| m.file_type().is_symlink())
.unwrap_or(false);
if path.is_file() || path_is_symlink {
if self.rename_file(path, path_is_symlink)? {
renamed_count = 1;
}
} else if path.is_dir() {
if self.options.recursive {
let include_symlinks = self.options.include_symlinks;
let mut files: Vec<(PathBuf, bool)> = WalkDir::new(path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
let ft = e.file_type();
ft.is_file() || (include_symlinks && ft.is_symlink())
})
.map(|e| {
let is_symlink = e.file_type().is_symlink();
(e.path().to_path_buf(), is_symlink)
})
.collect();
files.sort_by(|a, b| b.0.components().count().cmp(&a.0.components().count()));
for (file_path, is_symlink) in files {
if self.rename_file(&file_path, is_symlink)? {
renamed_count += 1;
}
}
} else {
let include_symlinks = self.options.include_symlinks;
let mut files: Vec<(PathBuf, bool)> = fs::read_dir(path)?
.filter_map(|e| e.ok())
.filter_map(|e| {
let path = e.path();
let ft = e.file_type().ok()?;
let is_symlink = ft.is_symlink();
if ft.is_file() || (include_symlinks && is_symlink) {
Some((path, is_symlink))
} else {
None
}
})
.collect();
files.sort_by(|a, b| a.0.cmp(&b.0));
for (file_path, is_symlink) in files {
if self.rename_file(&file_path, is_symlink)? {
renamed_count += 1;
}
}
}
}
Ok(renamed_count)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_lowercase_transform() {
let test_dir = std::env::temp_dir().join("reformat_rename_lowercase");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("TestFile.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.case_transform = CaseTransform::Lowercase;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
let new_file = test_dir.join("testfile.txt");
assert!(new_file.exists());
assert_eq!(fs::read_to_string(&new_file).unwrap(), "content");
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_uppercase_transform() {
let test_dir = std::env::temp_dir().join("reformat_rename_uppercase");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("testfile.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.case_transform = CaseTransform::Uppercase;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
let new_file = test_dir.join("TESTFILE.txt");
assert!(new_file.exists());
assert_eq!(fs::read_to_string(&new_file).unwrap(), "content");
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_capitalize_transform() {
let test_dir = std::env::temp_dir().join("reformat_rename_capitalize");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("testFile.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.case_transform = CaseTransform::Capitalize;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
let new_file = test_dir.join("Testfile.txt");
assert!(new_file.exists());
assert_eq!(fs::read_to_string(&new_file).unwrap(), "content");
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_separators_to_underscore() {
let test_dir = std::env::temp_dir().join("reformat_rename_underscore");
fs::create_dir_all(&test_dir).unwrap();
let test_file1 = test_dir.join("test file.txt");
fs::write(&test_file1, "content").unwrap();
let test_file2 = test_dir.join("test-file2.txt");
fs::write(&test_file2, "content").unwrap();
let test_file3 = test_dir.join("test-file 3.txt");
fs::write(&test_file3, "content").unwrap();
let mut opts = RenameOptions::default();
opts.space_replace = SpaceReplace::Underscore;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_dir).unwrap();
assert_eq!(count, 3);
assert!(test_dir.join("test_file.txt").exists());
assert!(test_dir.join("test_file2.txt").exists());
assert!(test_dir.join("test_file_3.txt").exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_separators_to_hyphen() {
let test_dir = std::env::temp_dir().join("reformat_rename_hyphen");
fs::create_dir_all(&test_dir).unwrap();
let test_file1 = test_dir.join("test file.txt");
fs::write(&test_file1, "content").unwrap();
let test_file2 = test_dir.join("test_file2.txt");
fs::write(&test_file2, "content").unwrap();
let test_file3 = test_dir.join("test_file 3.txt");
fs::write(&test_file3, "content").unwrap();
let mut opts = RenameOptions::default();
opts.space_replace = SpaceReplace::Hyphen;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_dir).unwrap();
assert_eq!(count, 3);
assert!(test_dir.join("test-file.txt").exists());
assert!(test_dir.join("test-file2.txt").exists());
assert!(test_dir.join("test-file-3.txt").exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_add_prefix() {
let test_dir = std::env::temp_dir().join("reformat_rename_add_prefix");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("file.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.add_prefix = Some("new_".to_string());
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
assert!(test_dir.join("new_file.txt").exists());
assert!(!test_file.exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_remove_prefix() {
let test_dir = std::env::temp_dir().join("reformat_rename_rm_prefix");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("old_file.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.remove_prefix = Some("old_".to_string());
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
assert!(test_dir.join("file.txt").exists());
assert!(!test_file.exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_add_suffix() {
let test_dir = std::env::temp_dir().join("reformat_rename_add_suffix");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("file.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.add_suffix = Some("_backup".to_string());
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
assert!(test_dir.join("file_backup.txt").exists());
assert!(!test_file.exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_remove_suffix() {
let test_dir = std::env::temp_dir().join("reformat_rename_rm_suffix");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("file_old.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.remove_suffix = Some("_old".to_string());
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
assert!(test_dir.join("file.txt").exists());
assert!(!test_file.exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_combined_transforms() {
let test_dir = std::env::temp_dir().join("reformat_rename_combined");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("old_Test File.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.remove_prefix = Some("old_".to_string());
opts.space_replace = SpaceReplace::Underscore;
opts.case_transform = CaseTransform::Lowercase;
opts.add_suffix = Some("_new".to_string());
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
assert!(test_dir.join("test_file_new.txt").exists());
assert!(!test_file.exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_dry_run_mode() {
let test_dir = std::env::temp_dir().join("reformat_rename_dry");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("TestFile.txt");
let original_content = "content";
fs::write(&test_file, original_content).unwrap();
let mut opts = RenameOptions::default();
opts.case_transform = CaseTransform::Lowercase;
opts.dry_run = true;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
assert!(test_file.exists());
assert_eq!(fs::read_to_string(&test_file).unwrap(), original_content);
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_skip_hidden_files() {
let test_dir = std::env::temp_dir().join("reformat_rename_hidden");
fs::create_dir_all(&test_dir).unwrap();
let hidden_file = test_dir.join(".hidden.txt");
fs::write(&hidden_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.case_transform = CaseTransform::Uppercase;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&hidden_file).unwrap();
assert_eq!(count, 0);
assert!(hidden_file.exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_recursive_processing() {
let test_dir = std::env::temp_dir().join("reformat_rename_recursive");
fs::create_dir_all(&test_dir).unwrap();
let sub_dir = test_dir.join("subdir");
fs::create_dir_all(&sub_dir).unwrap();
let file1 = test_dir.join("File1.txt");
let file2 = sub_dir.join("File2.txt");
fs::write(&file1, "content1").unwrap();
fs::write(&file2, "content2").unwrap();
let mut opts = RenameOptions::default();
opts.case_transform = CaseTransform::Lowercase;
opts.recursive = true;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_dir).unwrap();
assert_eq!(count, 2);
assert!(test_dir.join("file1.txt").exists());
assert!(sub_dir.join("file2.txt").exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_no_extension_file() {
let test_dir = std::env::temp_dir().join("reformat_rename_no_ext");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("TestFile");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.case_transform = CaseTransform::Lowercase;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
let new_file = test_dir.join("testfile");
assert!(new_file.exists());
assert_eq!(fs::read_to_string(&new_file).unwrap(), "content");
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_timestamp_long_format() {
let test_dir = std::env::temp_dir().join("reformat_rename_timestamp_long");
let _ = fs::remove_dir_all(&test_dir); fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("document.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.timestamp_format = TimestampFormat::Long;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
let entries: Vec<_> = fs::read_dir(&test_dir).unwrap().collect();
assert_eq!(entries.len(), 1);
let renamed_file = entries[0].as_ref().unwrap().path();
let file_name = renamed_file.file_name().unwrap().to_str().unwrap();
assert!(
file_name.len() >= 9,
"Filename should have at least 9 characters (YYYYMMDD-)"
);
assert!(
file_name.starts_with(|c: char| c.is_ascii_digit()),
"Should start with digit"
);
assert_eq!(
&file_name[8..9],
"-",
"Should have hyphen after date (default separator)"
);
assert!(
file_name.ends_with("document.txt"),
"Should end with original name"
);
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_timestamp_short_format() {
let test_dir = std::env::temp_dir().join("reformat_rename_timestamp_short");
let _ = fs::remove_dir_all(&test_dir); fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("notes.md");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.timestamp_format = TimestampFormat::Short;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
let entries: Vec<_> = fs::read_dir(&test_dir).unwrap().collect();
assert_eq!(entries.len(), 1);
let renamed_file = entries[0].as_ref().unwrap().path();
let file_name = renamed_file.file_name().unwrap().to_str().unwrap();
assert!(
file_name.len() >= 7,
"Filename should have at least 7 characters (YYMMDD-)"
);
assert!(
file_name.starts_with(|c: char| c.is_ascii_digit()),
"Should start with digit"
);
assert_eq!(
&file_name[6..7],
"-",
"Should have hyphen after date (default separator)"
);
assert!(
file_name.ends_with("notes.md"),
"Should end with original name"
);
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_timestamp_with_other_transforms() {
let test_dir = std::env::temp_dir().join("reformat_rename_timestamp_combined");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("My Document.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.timestamp_format = TimestampFormat::Long;
opts.space_replace = SpaceReplace::Underscore;
opts.case_transform = CaseTransform::Lowercase;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
let entries: Vec<_> = fs::read_dir(&test_dir).unwrap().collect();
assert_eq!(entries.len(), 1);
let renamed_file = entries[0].as_ref().unwrap().path();
let file_name = renamed_file.file_name().unwrap().to_str().unwrap();
assert!(file_name.starts_with(|c: char| c.is_ascii_digit()));
assert!(file_name.contains("my_document.txt"));
assert!(!file_name.contains(" "));
assert!(!file_name.contains("My"));
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_timestamp_separator_detection_hyphen() {
let test_dir = std::env::temp_dir().join("reformat_rename_timestamp_hyphen");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("my-document-file.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.timestamp_format = TimestampFormat::Long;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
let entries: Vec<_> = fs::read_dir(&test_dir).unwrap().collect();
assert_eq!(entries.len(), 1);
let renamed_file = entries[0].as_ref().unwrap().path();
let file_name = renamed_file.file_name().unwrap().to_str().unwrap();
assert!(file_name.starts_with(|c: char| c.is_ascii_digit()));
assert_eq!(
&file_name[8..9],
"-",
"Timestamp should use hyphen separator"
);
assert!(file_name.ends_with("my-document-file.txt"));
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_timestamp_separator_detection_underscore() {
let test_dir = std::env::temp_dir().join("reformat_rename_timestamp_underscore");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("my_document_file.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.timestamp_format = TimestampFormat::Short;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
let entries: Vec<_> = fs::read_dir(&test_dir).unwrap().collect();
assert_eq!(entries.len(), 1);
let renamed_file = entries[0].as_ref().unwrap().path();
let file_name = renamed_file.file_name().unwrap().to_str().unwrap();
assert!(file_name.starts_with(|c: char| c.is_ascii_digit()));
assert_eq!(
&file_name[6..7],
"_",
"Timestamp should use underscore separator"
);
assert!(file_name.ends_with("my_document_file.txt"));
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_timestamp_separator_detection_mixed() {
let test_dir = std::env::temp_dir().join("reformat_rename_timestamp_mixed");
let _ = fs::remove_dir_all(&test_dir); fs::create_dir_all(&test_dir).unwrap();
let test_file1 = test_dir.join("my-document-file_v2.txt");
fs::write(&test_file1, "content").unwrap();
let mut opts = RenameOptions::default();
opts.timestamp_format = TimestampFormat::Long;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file1).unwrap();
assert_eq!(count, 1);
let entries: Vec<_> = fs::read_dir(&test_dir).unwrap().collect();
assert_eq!(entries.len(), 1);
let renamed_file = entries[0].as_ref().unwrap().path();
let file_name = renamed_file.file_name().unwrap().to_str().unwrap();
assert_eq!(
&file_name[8..9],
"-",
"Should use hyphen for mixed with more hyphens"
);
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_timestamp_separator_detection_no_separator() {
let test_dir = std::env::temp_dir().join("reformat_rename_timestamp_nosep");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("mydocument.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.timestamp_format = TimestampFormat::Long;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
let entries: Vec<_> = fs::read_dir(&test_dir).unwrap().collect();
assert_eq!(entries.len(), 1);
let renamed_file = entries[0].as_ref().unwrap().path();
let file_name = renamed_file.file_name().unwrap().to_str().unwrap();
assert_eq!(&file_name[8..9], "-", "Should default to hyphen");
assert!(file_name.ends_with("mydocument.txt"));
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_timestamp_separator_detection_spaces() {
let test_dir = std::env::temp_dir().join("reformat_rename_timestamp_spaces");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("my document file.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.timestamp_format = TimestampFormat::Long;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
let entries: Vec<_> = fs::read_dir(&test_dir).unwrap().collect();
assert_eq!(entries.len(), 1);
let renamed_file = entries[0].as_ref().unwrap().path();
let file_name = renamed_file.file_name().unwrap().to_str().unwrap();
assert_eq!(
&file_name[8..9],
"-",
"Should use hyphen for space-separated files"
);
assert!(file_name.ends_with("my document file.txt"));
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_replace_prefix() {
let test_dir = std::env::temp_dir().join("reformat_rename_replace_prefix");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("old_file.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.replace_prefix = Some(("old_".to_string(), "new_".to_string()));
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
assert!(test_dir.join("new_file.txt").exists());
assert!(!test_file.exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_replace_prefix_no_match() {
let test_dir = std::env::temp_dir().join("reformat_rename_replace_prefix_nomatch");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("other_file.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.replace_prefix = Some(("old_".to_string(), "new_".to_string()));
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 0);
assert!(test_file.exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_replace_suffix() {
let test_dir = std::env::temp_dir().join("reformat_rename_replace_suffix");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("file_old.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.replace_suffix = Some(("_old".to_string(), "_new".to_string()));
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
assert!(test_dir.join("file_new.txt").exists());
assert!(!test_file.exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_replace_suffix_no_match() {
let test_dir = std::env::temp_dir().join("reformat_rename_replace_suffix_nomatch");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("file_other.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.replace_suffix = Some(("_old".to_string(), "_new".to_string()));
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 0);
assert!(test_file.exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_replace_prefix_and_suffix_combined() {
let test_dir = std::env::temp_dir().join("reformat_rename_replace_both");
fs::create_dir_all(&test_dir).unwrap();
let test_file = test_dir.join("old_file_v1.txt");
fs::write(&test_file, "content").unwrap();
let mut opts = RenameOptions::default();
opts.replace_prefix = Some(("old_".to_string(), "new_".to_string()));
opts.replace_suffix = Some(("_v1".to_string(), "_v2".to_string()));
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_file).unwrap();
assert_eq!(count, 1);
assert!(test_dir.join("new_file_v2.txt").exists());
assert!(!test_file.exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[cfg(unix)]
#[test]
fn test_symlinks_skipped_by_default() {
use std::os::unix::fs::symlink;
let test_dir = std::env::temp_dir().join("reformat_rename_symlink_skip");
let _ = fs::remove_dir_all(&test_dir);
fs::create_dir_all(&test_dir).unwrap();
let target_file = test_dir.join("Target.txt");
fs::write(&target_file, "content").unwrap();
let symlink_file = test_dir.join("SymLink.txt");
symlink(&target_file, &symlink_file).unwrap();
let mut opts = RenameOptions::default();
opts.case_transform = CaseTransform::Lowercase;
opts.include_symlinks = false;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_dir).unwrap();
assert_eq!(count, 1);
assert!(test_dir.join("target.txt").exists());
let entries: Vec<_> = fs::read_dir(&test_dir)
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
assert!(
entries.iter().any(|n| n == "SymLink.txt"),
"Symlink should retain original uppercase name"
);
fs::remove_dir_all(&test_dir).unwrap();
}
#[cfg(unix)]
#[test]
fn test_symlinks_included_when_enabled() {
use std::os::unix::fs::symlink;
let test_dir = std::env::temp_dir().join("reformat_rename_symlink_include");
let _ = fs::remove_dir_all(&test_dir);
fs::create_dir_all(&test_dir).unwrap();
let target_file = test_dir.join("target.txt");
fs::write(&target_file, "content").unwrap();
let symlink_file = test_dir.join("SymLink.txt");
symlink(&target_file, &symlink_file).unwrap();
let mut opts = RenameOptions::default();
opts.case_transform = CaseTransform::Lowercase;
opts.include_symlinks = true;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_dir).unwrap();
assert_eq!(count, 1);
assert!(test_dir.join("target.txt").exists());
let entries: Vec<_> = fs::read_dir(&test_dir)
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
assert!(
entries.iter().any(|n| n == "symlink.txt"),
"Symlink should be renamed to lowercase"
);
assert!(
!entries.iter().any(|n| n == "SymLink.txt"),
"Original uppercase symlink name should be gone"
);
fs::remove_dir_all(&test_dir).unwrap();
}
#[cfg(unix)]
#[test]
fn test_symlink_with_uppercase_target() {
use std::os::unix::fs::symlink;
let test_dir = std::env::temp_dir().join("reformat_rename_symlink_both");
let _ = fs::remove_dir_all(&test_dir);
fs::create_dir_all(&test_dir).unwrap();
let target_file = test_dir.join("Target.txt");
fs::write(&target_file, "content").unwrap();
let symlink_file = test_dir.join("SymLink.txt");
symlink(&target_file, &symlink_file).unwrap();
let mut opts = RenameOptions::default();
opts.case_transform = CaseTransform::Lowercase;
opts.include_symlinks = true;
let renamer = FileRenamer::new(opts);
let count = renamer.process(&test_dir).unwrap();
assert_eq!(count, 2);
let entries: Vec<_> = fs::read_dir(&test_dir)
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
assert!(
entries.iter().any(|n| n == "target.txt"),
"Target file should be renamed to lowercase"
);
assert!(
entries.iter().any(|n| n == "symlink.txt"),
"Symlink should be renamed to lowercase"
);
fs::remove_dir_all(&test_dir).unwrap();
}
}