fix: Race Condition bei paralleler Umbenennung, Clippy/Deprecation-Warnings und Formatierung
- Parallele Berechnung (par_iter) entfernt, da sie die tiefenbasierte Sortierung zerstörte und Parent-Verzeichnisse vor ihren Kindern umbenannt werden konnten - Duplizierten Code zwischen parallelem und sequentiellem Pfad entfernt - Clippy-Warning behoben: collapsible str::replace in sanitizer.rs - Deprecation-Warning behoben: #[allow(deprecated)] für cargo_bin Import - cargo fmt angewendet Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0c26e5244c
commit
ad44139e21
3 changed files with 105 additions and 119 deletions
153
src/main.rs
153
src/main.rs
|
|
@ -11,7 +11,6 @@ use config::Config;
|
||||||
use glob::Pattern;
|
use glob::Pattern;
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use rayon::prelude::*;
|
|
||||||
use sanitizer::{clean_filename, is_excluded, is_safe_rename, Sequence};
|
use sanitizer::{clean_filename, is_excluded, is_safe_rename, Sequence};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::IsTerminal;
|
use std::io::IsTerminal;
|
||||||
|
|
@ -32,9 +31,6 @@ const DEFAULT_EXCLUDES: &[&str] = &[
|
||||||
"__pycache__/**",
|
"__pycache__/**",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Schwellwert für parallele Verarbeitung (bei weniger Dateien lohnt sich Overhead nicht)
|
|
||||||
const PARALLEL_THRESHOLD: usize = 100;
|
|
||||||
|
|
||||||
/// Repräsentiert eine geplante Umbenennungsoperation
|
/// Repräsentiert eine geplante Umbenennungsoperation
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct RenameOperation {
|
struct RenameOperation {
|
||||||
|
|
@ -84,8 +80,12 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
// Config-Datei laden: entweder --conf oder Standard-Hierarchie
|
// Config-Datei laden: entweder --conf oder Standard-Hierarchie
|
||||||
let config = if let Some(config_path) = &args.config_file {
|
let config = if let Some(config_path) = &args.config_file {
|
||||||
Config::from_file(config_path, args.verbose)
|
Config::from_file(config_path, args.verbose).with_context(|| {
|
||||||
.with_context(|| format!("Fehler beim Laden der Konfiguration: {}", config_path.display()))?
|
format!(
|
||||||
|
"Fehler beim Laden der Konfiguration: {}",
|
||||||
|
config_path.display()
|
||||||
|
)
|
||||||
|
})?
|
||||||
} else {
|
} else {
|
||||||
Config::from_default_locations(args.verbose)?
|
Config::from_default_locations(args.verbose)?
|
||||||
};
|
};
|
||||||
|
|
@ -139,7 +139,10 @@ fn main() -> Result<()> {
|
||||||
if let Ok(entry) = entry_result {
|
if let Ok(entry) = entry_result {
|
||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
} else if let Err(e) = entry_result {
|
} 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
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Berechne Umbenennungen (parallel bei vielen Dateien)
|
// Berechne Umbenennungen
|
||||||
let rename_ops: Vec<RenameOperation> = if entries.len() >= PARALLEL_THRESHOLD {
|
// Hinweis: Die Reihenfolge (tiefste zuerst) muss erhalten bleiben,
|
||||||
// Parallel mit rayon
|
// damit Parent-Verzeichnisse nicht vor ihren Kindern umbenannt werden.
|
||||||
entries
|
let rename_ops: Vec<RenameOperation> = entries
|
||||||
.par_iter()
|
.iter()
|
||||||
.filter_map(|entry| {
|
.filter_map(|entry| {
|
||||||
let old_path = entry.path();
|
let old_path = entry.path();
|
||||||
|
|
||||||
// Ebenentiefe 0 -> überspringen
|
// Ebenentiefe 0 -> überspringen
|
||||||
if entry.depth() == 0 && entry.file_type().is_dir() && !args.modify_root {
|
if entry.depth() == 0 && entry.file_type().is_dir() && !args.modify_root {
|
||||||
return None;
|
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
|
// Dateiname ermitteln und bereinigen
|
||||||
let file_type = entry.file_type();
|
let filename = old_path.file_name()?;
|
||||||
if !args.special && (!file_type.is_file() && !file_type.is_dir()) {
|
let new_name = clean_filename(filename, &config, &sequence, false)?;
|
||||||
if args.verbose && file_type.is_symlink() {
|
let new_path = old_path.with_file_name(&new_name);
|
||||||
debug!("Überspringe Symlink: {} (nutze --special um Symlink-Namen zu bereinigen)", old_path.display());
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dateiname ermitteln und bereinigen
|
Some(RenameOperation {
|
||||||
let filename = old_path.file_name()?;
|
old_path: old_path.to_path_buf(),
|
||||||
let new_name = clean_filename(filename, &config, &sequence, false)?;
|
new_path,
|
||||||
let new_path = old_path.with_file_name(&new_name);
|
|
||||||
|
|
||||||
Some(RenameOperation {
|
|
||||||
old_path: old_path.to_path_buf(),
|
|
||||||
new_path,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect()
|
})
|
||||||
} else {
|
.collect();
|
||||||
// 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()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Statistiken
|
// Statistiken
|
||||||
let total_processed = entries.len();
|
let total_processed = entries.len();
|
||||||
|
|
@ -236,13 +209,15 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
if !args.quiet {
|
if !args.quiet {
|
||||||
if args.dry_run {
|
if args.dry_run {
|
||||||
info!("{} {} {}",
|
info!(
|
||||||
|
"{} {} {}",
|
||||||
op.old_path.display().to_string().dimmed(),
|
op.old_path.display().to_string().dimmed(),
|
||||||
"->".yellow(),
|
"->".yellow(),
|
||||||
op.new_path.display().to_string().yellow()
|
op.new_path.display().to_string().yellow()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
info!("{} {} {}",
|
info!(
|
||||||
|
"{} {} {}",
|
||||||
op.old_path.display().to_string().dimmed(),
|
op.old_path.display().to_string().dimmed(),
|
||||||
"->".green(),
|
"->".green(),
|
||||||
op.new_path.display().to_string().green()
|
op.new_path.display().to_string().green()
|
||||||
|
|
@ -259,13 +234,15 @@ fn main() -> Result<()> {
|
||||||
match fs::rename(&op.old_path, &op.new_path) {
|
match fs::rename(&op.old_path, &op.new_path) {
|
||||||
Ok(_) => renamed_count += 1,
|
Ok(_) => renamed_count += 1,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{}",
|
error!(
|
||||||
|
"{}",
|
||||||
format!(
|
format!(
|
||||||
"Fehler beim Umbenennen: {} -> {}: {}",
|
"Fehler beim Umbenennen: {} -> {}: {}",
|
||||||
op.old_path.display(),
|
op.old_path.display(),
|
||||||
op.new_path.display(),
|
op.new_path.display(),
|
||||||
e
|
e
|
||||||
).red()
|
)
|
||||||
|
.red()
|
||||||
);
|
);
|
||||||
skipped_count += 1;
|
skipped_count += 1;
|
||||||
}
|
}
|
||||||
|
|
@ -283,15 +260,35 @@ fn main() -> Result<()> {
|
||||||
// Zusammenfassung ausgeben (außer im quiet mode)
|
// Zusammenfassung ausgeben (außer im quiet mode)
|
||||||
if !args.quiet {
|
if !args.quiet {
|
||||||
info!("");
|
info!("");
|
||||||
info!("{}", format!("=== Zusammenfassung für {} ===", path.display()).cyan().bold());
|
info!(
|
||||||
info!("Verarbeitete Dateien/Verzeichnisse: {}", total_processed.to_string().cyan());
|
"{}",
|
||||||
info!("Umbenennungen geplant: {}", total_planned.to_string().cyan());
|
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 {
|
if args.dry_run {
|
||||||
info!("Modus: {}", "Dry-run (keine Änderungen)".yellow());
|
info!(
|
||||||
|
"Modus: {}",
|
||||||
|
"Dry-run (keine Änderungen)".yellow()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
info!("Erfolgreich umbenannt: {}", renamed_count.to_string().green().bold());
|
info!(
|
||||||
|
"Erfolgreich umbenannt: {}",
|
||||||
|
renamed_count.to_string().green().bold()
|
||||||
|
);
|
||||||
if skipped_count > 0 {
|
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!(" Case transform: {:?}", seq.apply_case);
|
||||||
println!(
|
println!(
|
||||||
" Emoji handling: {}",
|
" Emoji handling: {}",
|
||||||
if seq.apply_emojis {
|
if seq.apply_emojis { "replace" } else { "keep" }
|
||||||
"replace"
|
|
||||||
} else {
|
|
||||||
"keep"
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
" Mode: {}",
|
" Mode: {}",
|
||||||
|
|
|
||||||
|
|
@ -91,12 +91,7 @@ impl Sequence {
|
||||||
|
|
||||||
// Bekannte Doppel-Extensions (z.B. .tar.gz)
|
// Bekannte Doppel-Extensions (z.B. .tar.gz)
|
||||||
const DOUBLE_EXTENSIONS: &[&str] = &[
|
const DOUBLE_EXTENSIONS: &[&str] = &[
|
||||||
".tar.gz",
|
".tar.gz", ".tar.bz2", ".tar.xz", ".tar.zst", ".tar.lz", ".tar.Z",
|
||||||
".tar.bz2",
|
|
||||||
".tar.xz",
|
|
||||||
".tar.zst",
|
|
||||||
".tar.lz",
|
|
||||||
".tar.Z",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Trennt Dateiname in Basis und Extension, berücksichtigt Doppel-Extensions
|
/// 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) {
|
if filename.ends_with(double_ext) {
|
||||||
let base_len = filename.len() - double_ext.len();
|
let base_len = filename.len() - double_ext.len();
|
||||||
if base_len > 0 {
|
if base_len > 0 {
|
||||||
return (
|
return (filename[..base_len].to_string(), double_ext.to_string());
|
||||||
filename[..base_len].to_string(),
|
|
||||||
double_ext.to_string(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -179,11 +171,7 @@ pub fn clean_filename(
|
||||||
// Minimal: Nur Leerzeichen und gefährliche Zeichen
|
// Minimal: Nur Leerzeichen und gefährliche Zeichen
|
||||||
base = base.replace(' ', "_");
|
base = base.replace(' ', "_");
|
||||||
// Entferne nur absolut gefährliche Zeichen
|
// Entferne nur absolut gefährliche Zeichen
|
||||||
base = base
|
base = base.replace(['/', '\\', '\0', '\n'], "_");
|
||||||
.replace('/', "_")
|
|
||||||
.replace('\\', "_")
|
|
||||||
.replace('\0', "_")
|
|
||||||
.replace('\n', "_");
|
|
||||||
} else {
|
} else {
|
||||||
// Standard: Alle ungültigen Zeichen → Unterstrich
|
// Standard: Alle ungültigen Zeichen → Unterstrich
|
||||||
base = RE_INVALID.replace_all(&base, "_").to_string();
|
base = RE_INVALID.replace_all(&base, "_").to_string();
|
||||||
|
|
@ -194,13 +182,16 @@ pub fn clean_filename(
|
||||||
|
|
||||||
// Mehrfache Punkte/Unterstriche auf einen reduzieren
|
// Mehrfache Punkte/Unterstriche auf einen reduzieren
|
||||||
base = RE_MULTI
|
base = RE_MULTI
|
||||||
.replace_all(&base, |caps: &Captures| {
|
.replace_all(
|
||||||
if caps[0].contains('.') {
|
&base,
|
||||||
"."
|
|caps: &Captures| {
|
||||||
} else {
|
if caps[0].contains('.') {
|
||||||
"_"
|
"."
|
||||||
}
|
} else {
|
||||||
})
|
"_"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
// Führender Punkt soll bleiben, führende Unterstriche sollen verschwinden
|
// 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;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
let permissions = metadata.permissions();
|
let permissions = metadata.permissions();
|
||||||
if permissions.mode() & 0o200 == 0 {
|
if permissions.mode() & 0o200 == 0 {
|
||||||
warn!(
|
warn!("Keine Schreibrechte im Verzeichnis: {}", parent.display());
|
||||||
"Keine Schreibrechte im Verzeichnis: {}",
|
|
||||||
parent.display()
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use assert_cmd::assert::OutputAssertExt;
|
use assert_cmd::assert::OutputAssertExt;
|
||||||
|
#[allow(deprecated)]
|
||||||
use assert_cmd::cargo::cargo_bin;
|
use assert_cmd::cargo::cargo_bin;
|
||||||
use predicates::prelude::*;
|
use predicates::prelude::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
@ -141,9 +142,7 @@ fn test_quiet_mode() {
|
||||||
|
|
||||||
let mut cmd = Command::new(cargo_bin!("ntu"));
|
let mut cmd = Command::new(cargo_bin!("ntu"));
|
||||||
cmd.arg("--quiet").arg("-r").arg(temp_dir.path());
|
cmd.arg("--quiet").arg("-r").arg(temp_dir.path());
|
||||||
cmd.assert()
|
cmd.assert().success().stdout(predicate::str::is_empty());
|
||||||
.success()
|
|
||||||
.stdout(predicate::str::is_empty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -228,7 +227,11 @@ fn test_recursive_flag() {
|
||||||
|
|
||||||
// Beide umbenannt
|
// Beide umbenannt
|
||||||
assert!(temp_dir.path().join("top_file.txt").exists());
|
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]
|
#[test]
|
||||||
|
|
@ -423,7 +426,10 @@ fn test_max_depth_option() {
|
||||||
assert!(temp_dir.path().join("level_1/level_2/level_3").exists());
|
assert!(temp_dir.path().join("level_1/level_2/level_3").exists());
|
||||||
|
|
||||||
// Level 3/file sollte NICHT umbenannt sein (depth 4 > max 3)
|
// 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]
|
#[test]
|
||||||
|
|
@ -432,16 +438,11 @@ fn test_max_depth_requires_recursive() {
|
||||||
|
|
||||||
// --max-depth ohne -r sollte fehlschlagen
|
// --max-depth ohne -r sollte fehlschlagen
|
||||||
let mut cmd = Command::new(cargo_bin!("ntu"));
|
let mut cmd = Command::new(cargo_bin!("ntu"));
|
||||||
cmd.arg("--max-depth")
|
cmd.arg("--max-depth").arg("2").arg(temp_dir.path());
|
||||||
.arg("2")
|
|
||||||
.arg(temp_dir.path());
|
|
||||||
|
|
||||||
let assert = cmd.assert().failure();
|
let assert = cmd.assert().failure();
|
||||||
// Prüfe dass die Fehlermeldung "required" oder "recursive" enthält
|
// Prüfe dass die Fehlermeldung "required" oder "recursive" enthält
|
||||||
assert.stderr(
|
assert.stderr(predicate::str::contains("required").or(predicate::str::contains("recursive")));
|
||||||
predicate::str::contains("required")
|
|
||||||
.or(predicate::str::contains("recursive"))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -467,7 +468,11 @@ fn test_symlinks_default_behavior() {
|
||||||
// Verwende symlink_metadata() um zu prüfen ob der Link selbst existiert
|
// Verwende symlink_metadata() um zu prüfen ob der Link selbst existiert
|
||||||
let link_unchanged = temp_dir.path().join("link to file");
|
let link_unchanged = temp_dir.path().join("link to file");
|
||||||
assert!(link_unchanged.symlink_metadata().is_ok());
|
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]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue