diff --git a/src/main.rs b/src/main.rs index 195c702..758f374 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,6 @@ use config::Config; use glob::Pattern; use indicatif::{ProgressBar, ProgressStyle}; use log::{debug, error, info}; -use rayon::prelude::*; use sanitizer::{clean_filename, is_excluded, is_safe_rename, Sequence}; use std::fs; use std::io::IsTerminal; @@ -32,9 +31,6 @@ const DEFAULT_EXCLUDES: &[&str] = &[ "__pycache__/**", ]; -// Schwellwert für parallele Verarbeitung (bei weniger Dateien lohnt sich Overhead nicht) -const PARALLEL_THRESHOLD: usize = 100; - /// Repräsentiert eine geplante Umbenennungsoperation #[derive(Debug)] struct RenameOperation { @@ -84,8 +80,12 @@ fn main() -> Result<()> { // Config-Datei laden: entweder --conf oder Standard-Hierarchie let config = if let Some(config_path) = &args.config_file { - Config::from_file(config_path, args.verbose) - .with_context(|| format!("Fehler beim Laden der Konfiguration: {}", config_path.display()))? + Config::from_file(config_path, args.verbose).with_context(|| { + format!( + "Fehler beim Laden der Konfiguration: {}", + config_path.display() + ) + })? } else { Config::from_default_locations(args.verbose)? }; @@ -139,7 +139,10 @@ fn main() -> Result<()> { if let Ok(entry) = entry_result { entries.push(entry); } else if let Err(e) = entry_result { - error!("{}", format!("Fehler beim Durchlaufen von {}: {}", path.display(), e).red()); + error!( + "{}", + format!("Fehler beim Durchlaufen von {}: {}", path.display(), e).red() + ); } } @@ -158,69 +161,39 @@ fn main() -> Result<()> { None }; - // Berechne Umbenennungen (parallel bei vielen Dateien) - let rename_ops: Vec = if entries.len() >= PARALLEL_THRESHOLD { - // Parallel mit rayon - entries - .par_iter() - .filter_map(|entry| { - let old_path = entry.path(); + // Berechne Umbenennungen + // Hinweis: Die Reihenfolge (tiefste zuerst) muss erhalten bleiben, + // damit Parent-Verzeichnisse nicht vor ihren Kindern umbenannt werden. + let rename_ops: Vec = entries + .iter() + .filter_map(|entry| { + let old_path = entry.path(); - // Ebenentiefe 0 -> überspringen - if entry.depth() == 0 && entry.file_type().is_dir() && !args.modify_root { - return None; + // Ebenentiefe 0 -> überspringen + if entry.depth() == 0 && entry.file_type().is_dir() && !args.modify_root { + return None; + } + + // Special Files (Symlinks, Sockets, etc.) nur mit --special + let file_type = entry.file_type(); + if !args.special && (!file_type.is_file() && !file_type.is_dir()) { + if args.verbose && file_type.is_symlink() { + debug!("Überspringe Symlink: {} (nutze --special um Symlink-Namen zu bereinigen)", old_path.display()); } + return None; + } - // Special Files (Symlinks, Sockets, etc.) nur mit --special - let file_type = entry.file_type(); - if !args.special && (!file_type.is_file() && !file_type.is_dir()) { - if args.verbose && file_type.is_symlink() { - debug!("Überspringe Symlink: {} (nutze --special um Symlink-Namen zu bereinigen)", old_path.display()); - } - return None; - } + // Dateiname ermitteln und bereinigen + let filename = old_path.file_name()?; + let new_name = clean_filename(filename, &config, &sequence, false)?; + let new_path = old_path.with_file_name(&new_name); - // Dateiname ermitteln und bereinigen - let filename = old_path.file_name()?; - let new_name = clean_filename(filename, &config, &sequence, false)?; - let new_path = old_path.with_file_name(&new_name); - - Some(RenameOperation { - old_path: old_path.to_path_buf(), - new_path, - }) + Some(RenameOperation { + old_path: old_path.to_path_buf(), + new_path, }) - .collect() - } else { - // Sequenziell bei wenigen Dateien - entries - .iter() - .filter_map(|entry| { - let old_path = entry.path(); - - if entry.depth() == 0 && entry.file_type().is_dir() && !args.modify_root { - return None; - } - - let file_type = entry.file_type(); - if !args.special && (!file_type.is_file() && !file_type.is_dir()) { - if args.verbose && file_type.is_symlink() { - debug!("Überspringe Symlink: {} (nutze --special um Symlink-Namen zu bereinigen)", old_path.display()); - } - return None; - } - - let filename = old_path.file_name()?; - let new_name = clean_filename(filename, &config, &sequence, false)?; - let new_path = old_path.with_file_name(&new_name); - - Some(RenameOperation { - old_path: old_path.to_path_buf(), - new_path, - }) - }) - .collect() - }; + }) + .collect(); // Statistiken let total_processed = entries.len(); @@ -236,13 +209,15 @@ fn main() -> Result<()> { if !args.quiet { if args.dry_run { - info!("{} {} {}", + info!( + "{} {} {}", op.old_path.display().to_string().dimmed(), "->".yellow(), op.new_path.display().to_string().yellow() ); } else { - info!("{} {} {}", + info!( + "{} {} {}", op.old_path.display().to_string().dimmed(), "->".green(), op.new_path.display().to_string().green() @@ -259,13 +234,15 @@ fn main() -> Result<()> { match fs::rename(&op.old_path, &op.new_path) { Ok(_) => renamed_count += 1, Err(e) => { - error!("{}", + error!( + "{}", format!( "Fehler beim Umbenennen: {} -> {}: {}", op.old_path.display(), op.new_path.display(), e - ).red() + ) + .red() ); skipped_count += 1; } @@ -283,15 +260,35 @@ fn main() -> Result<()> { // Zusammenfassung ausgeben (außer im quiet mode) if !args.quiet { info!(""); - info!("{}", format!("=== Zusammenfassung für {} ===", path.display()).cyan().bold()); - info!("Verarbeitete Dateien/Verzeichnisse: {}", total_processed.to_string().cyan()); - info!("Umbenennungen geplant: {}", total_planned.to_string().cyan()); + info!( + "{}", + format!("=== Zusammenfassung für {} ===", path.display()) + .cyan() + .bold() + ); + info!( + "Verarbeitete Dateien/Verzeichnisse: {}", + total_processed.to_string().cyan() + ); + info!( + "Umbenennungen geplant: {}", + total_planned.to_string().cyan() + ); if args.dry_run { - info!("Modus: {}", "Dry-run (keine Änderungen)".yellow()); + info!( + "Modus: {}", + "Dry-run (keine Änderungen)".yellow() + ); } else { - info!("Erfolgreich umbenannt: {}", renamed_count.to_string().green().bold()); + info!( + "Erfolgreich umbenannt: {}", + renamed_count.to_string().green().bold() + ); if skipped_count > 0 { - info!("Übersprungen/Fehler: {}", skipped_count.to_string().red()); + info!( + "Übersprungen/Fehler: {}", + skipped_count.to_string().red() + ); } } } @@ -317,11 +314,7 @@ fn list_sequences(args: &Cli) { println!(" Case transform: {:?}", seq.apply_case); println!( " Emoji handling: {}", - if seq.apply_emojis { - "replace" - } else { - "keep" - } + if seq.apply_emojis { "replace" } else { "keep" } ); println!( " Mode: {}", diff --git a/src/sanitizer.rs b/src/sanitizer.rs index 16b2d85..2382178 100644 --- a/src/sanitizer.rs +++ b/src/sanitizer.rs @@ -91,12 +91,7 @@ impl Sequence { // Bekannte Doppel-Extensions (z.B. .tar.gz) const DOUBLE_EXTENSIONS: &[&str] = &[ - ".tar.gz", - ".tar.bz2", - ".tar.xz", - ".tar.zst", - ".tar.lz", - ".tar.Z", + ".tar.gz", ".tar.bz2", ".tar.xz", ".tar.zst", ".tar.lz", ".tar.Z", ]; /// Trennt Dateiname in Basis und Extension, berücksichtigt Doppel-Extensions @@ -106,10 +101,7 @@ fn split_filename(filename: &str) -> (String, String) { if filename.ends_with(double_ext) { let base_len = filename.len() - double_ext.len(); if base_len > 0 { - return ( - filename[..base_len].to_string(), - double_ext.to_string(), - ); + return (filename[..base_len].to_string(), double_ext.to_string()); } } } @@ -179,11 +171,7 @@ pub fn clean_filename( // Minimal: Nur Leerzeichen und gefährliche Zeichen base = base.replace(' ', "_"); // Entferne nur absolut gefährliche Zeichen - base = base - .replace('/', "_") - .replace('\\', "_") - .replace('\0', "_") - .replace('\n', "_"); + base = base.replace(['/', '\\', '\0', '\n'], "_"); } else { // Standard: Alle ungültigen Zeichen → Unterstrich base = RE_INVALID.replace_all(&base, "_").to_string(); @@ -194,13 +182,16 @@ pub fn clean_filename( // Mehrfache Punkte/Unterstriche auf einen reduzieren base = RE_MULTI - .replace_all(&base, |caps: &Captures| { - if caps[0].contains('.') { - "." - } else { - "_" - } - }) + .replace_all( + &base, + |caps: &Captures| { + if caps[0].contains('.') { + "." + } else { + "_" + } + }, + ) .to_string(); // Führender Punkt soll bleiben, führende Unterstriche sollen verschwinden @@ -354,10 +345,7 @@ pub fn is_safe_rename(src: &Path, dst: &Path, force: bool) -> bool { use std::os::unix::fs::PermissionsExt; let permissions = metadata.permissions(); if permissions.mode() & 0o200 == 0 { - warn!( - "Keine Schreibrechte im Verzeichnis: {}", - parent.display() - ); + warn!("Keine Schreibrechte im Verzeichnis: {}", parent.display()); return false; } } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 9c1e241..4955f09 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,4 +1,5 @@ use assert_cmd::assert::OutputAssertExt; +#[allow(deprecated)] use assert_cmd::cargo::cargo_bin; use predicates::prelude::*; use std::fs; @@ -141,9 +142,7 @@ fn test_quiet_mode() { let mut cmd = Command::new(cargo_bin!("ntu")); cmd.arg("--quiet").arg("-r").arg(temp_dir.path()); - cmd.assert() - .success() - .stdout(predicate::str::is_empty()); + cmd.assert().success().stdout(predicate::str::is_empty()); } #[test] @@ -228,7 +227,11 @@ fn test_recursive_flag() { // Beide umbenannt assert!(temp_dir.path().join("top_file.txt").exists()); - assert!(temp_dir.path().join("subdir").join("nested_file.txt").exists()); + assert!(temp_dir + .path() + .join("subdir") + .join("nested_file.txt") + .exists()); } #[test] @@ -423,7 +426,10 @@ fn test_max_depth_option() { assert!(temp_dir.path().join("level_1/level_2/level_3").exists()); // Level 3/file sollte NICHT umbenannt sein (depth 4 > max 3) - assert!(temp_dir.path().join("level_1/level_2/level_3/file 3.txt").exists()); + assert!(temp_dir + .path() + .join("level_1/level_2/level_3/file 3.txt") + .exists()); } #[test] @@ -432,16 +438,11 @@ fn test_max_depth_requires_recursive() { // --max-depth ohne -r sollte fehlschlagen let mut cmd = Command::new(cargo_bin!("ntu")); - cmd.arg("--max-depth") - .arg("2") - .arg(temp_dir.path()); + cmd.arg("--max-depth").arg("2").arg(temp_dir.path()); let assert = cmd.assert().failure(); // Prüfe dass die Fehlermeldung "required" oder "recursive" enthält - assert.stderr( - predicate::str::contains("required") - .or(predicate::str::contains("recursive")) - ); + assert.stderr(predicate::str::contains("required").or(predicate::str::contains("recursive"))); } #[test] @@ -467,7 +468,11 @@ fn test_symlinks_default_behavior() { // Verwende symlink_metadata() um zu prüfen ob der Link selbst existiert let link_unchanged = temp_dir.path().join("link to file"); assert!(link_unchanged.symlink_metadata().is_ok()); - assert!(!temp_dir.path().join("link_to_file").symlink_metadata().is_ok()); + assert!(!temp_dir + .path() + .join("link_to_file") + .symlink_metadata() + .is_ok()); } #[test]