ntu/src/main.rs

342 lines
11 KiB
Rust
Raw Normal View History

2025-03-18 03:05:18 +01:00
// Verwende nun die Module
mod cli;
mod config;
mod sanitizer;
use anyhow::{Context, Result};
use clap::Parser;
use cli::Cli;
use colored::*;
2025-03-18 03:05:18 +01:00
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};
2025-03-18 03:05:18 +01:00
use std::fs;
use std::io::IsTerminal;
use std::path::PathBuf;
2025-03-18 03:05:18 +01:00
use walkdir::WalkDir;
// Standard-Ausschlussmuster (ähnlich wie detox)
const DEFAULT_EXCLUDES: &[&str] = &[
".git",
".git/**",
".svn",
".svn/**",
"node_modules",
"node_modules/**",
".cache",
".cache/**",
"__pycache__",
"__pycache__/**",
];
// Schwellwert für parallele Verarbeitung (bei weniger Dateien lohnt sich der Overhead nicht)
const PARALLEL_THRESHOLD: usize = 100;
/// Repräsentiert eine geplante Umbenennungsoperation
#[derive(Debug)]
struct RenameOperation {
old_path: PathBuf,
new_path: PathBuf,
}
/// Prüft ob farbige Ausgabe aktiviert sein soll
fn should_use_color(no_color_flag: bool) -> bool {
!no_color_flag && std::io::stdout().is_terminal()
}
2025-03-18 03:05:18 +01:00
/// Startpunkt des Programms
fn main() -> Result<()> {
// Initialisiere Logger
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
// Argumente parsen
let args = Cli::parse();
// Farben konfigurieren
if !should_use_color(args.no_color) {
colored::control::set_override(false);
}
// -L Option: Liste Sequences und beende
if args.list_sequences {
list_sequences(&args);
return Ok(());
}
// Sequence auswählen
let sequence = if let Some(seq_name) = &args.sequence {
Sequence::find(seq_name).ok_or_else(|| {
anyhow::anyhow!(
"Unbekannte Sequence: '{}'. Nutze -L um verfügbare Sequences anzuzeigen.",
seq_name
)
})?
} else {
Sequence::default()
};
if args.verbose {
info!("Verwende Sequence: {}", sequence.name);
}
// 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()
)
})?
} else {
Config::from_default_locations(args.verbose)?
};
2025-03-18 03:05:18 +01:00
// Ausschlussmuster (Glob-Patterns) vorbereiten - Default-Excludes + User-Excludes
let mut all_excludes = DEFAULT_EXCLUDES
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
all_excludes.extend(args.exclude.clone());
let exclude_patterns = all_excludes
2025-03-18 03:05:18 +01:00
.iter()
.map(|pattern| {
Pattern::new(pattern)
.with_context(|| format!("Ungültiges Ausschlussmuster: {}", pattern))
})
.collect::<Result<Vec<_>>>()?;
if args.verbose && !exclude_patterns.is_empty() {
debug!("Folgende Exclude-Pattern werden genutzt:");
for p in &exclude_patterns {
debug!(" {}", p.as_str());
}
}
// Für alle angegebenen Pfade
for path in &args.paths {
// Alle Einträge sammeln, damit zuerst die tiefsten umbenannt werden
let mut entries = Vec::new();
let walker = if args.recursive {
// Recursive: mit optionaler max_depth
let mut w = WalkDir::new(path).follow_links(false); // Symlinks nicht folgen
if let Some(depth) = args.max_depth {
w = w.max_depth(depth);
if args.verbose {
info!("Maximale Rekursionstiefe: {}", depth);
}
}
w
} else {
// Non-recursive: max_depth(1) verarbeitet nur direkte Kinder
WalkDir::new(path).max_depth(1).follow_links(false)
};
for entry_result in walker
2025-03-18 03:05:18 +01:00
.into_iter()
.filter_entry(|e| !is_excluded(e, &exclude_patterns))
{
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()
);
2025-03-18 03:05:18 +01:00
}
}
// Aufsteigend nach Tiefe sortieren, dann umkehren => tiefste Einträge zuerst
entries.sort_by_key(|e| e.depth());
entries.reverse();
// Fortschrittsbalken bei größeren Dateimengen
let progress_bar = if !args.quiet && entries.len() > 50 {
let bar = ProgressBar::new(entries.len() as u64);
bar.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) {msg}")?
.progress_chars("#>-"));
Some(bar)
} else {
None
};
// Berechne Umbenennungen (parallel bei vielen Dateien, sequentiell bei wenigen)
// Hinweis: rayon's par_iter() auf indexierten Collections bewahrt die Reihenfolge,
// sodass die tiefenbasierte Sortierung erhalten bleibt.
let map_entry = |entry: &walkdir::DirEntry| -> Option<RenameOperation> {
let old_path = entry.path();
// 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;
}
// 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,
})
};
let rename_ops: Vec<RenameOperation> = if entries.len() >= PARALLEL_THRESHOLD {
entries.par_iter().filter_map(map_entry).collect()
} else {
entries.iter().filter_map(map_entry).collect()
};
// Statistiken
let total_processed = entries.len();
let total_planned = rename_ops.len();
let mut renamed_count = 0;
let mut skipped_count = 0;
// Umbenennungen sequenziell ausführen
for op in rename_ops {
2025-03-18 03:05:18 +01:00
if let Some(bar) = &progress_bar {
bar.inc(1);
}
if !args.quiet {
if args.dry_run {
info!(
"{} {} {}",
op.old_path.display().to_string().dimmed(),
"->".yellow(),
op.new_path.display().to_string().yellow()
);
} else {
info!(
"{} {} {}",
op.old_path.display().to_string().dimmed(),
"->".green(),
op.new_path.display().to_string().green()
);
}
2025-03-18 03:05:18 +01:00
}
if args.verbose {
debug!("Rename: {:?} -> {:?}", op.old_path, op.new_path);
}
if !args.dry_run {
if is_safe_rename(&op.old_path, &op.new_path, args.force) {
match fs::rename(&op.old_path, &op.new_path) {
Ok(_) => renamed_count += 1,
Err(e) => {
error!(
"{}",
format!(
"Fehler beim Umbenennen: {} -> {}: {}",
op.old_path.display(),
op.new_path.display(),
e
)
.red()
);
skipped_count += 1;
}
}
} else {
skipped_count += 1;
}
2025-03-18 03:05:18 +01:00
}
}
if let Some(bar) = &progress_bar {
bar.finish_with_message("Verarbeitung abgeschlossen");
}
// 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()
);
if args.dry_run {
info!(
"Modus: {}",
"Dry-run (keine Änderungen)".yellow()
);
} else {
info!(
"Erfolgreich umbenannt: {}",
renamed_count.to_string().green().bold()
);
if skipped_count > 0 {
info!(
"Übersprungen/Fehler: {}",
skipped_count.to_string().red()
);
}
}
2025-03-18 03:05:18 +01:00
}
}
Ok(())
}
/// Listet alle verfügbaren Sequences auf
fn list_sequences(args: &Cli) {
println!("Verfügbare Sequences:");
println!();
for seq in Sequence::all() {
println!(" {}", seq.name.bold());
if args.verbose {
println!(" Description: {}", seq.description);
println!(
" Umlauts → ASCII: {}",
if seq.apply_umlauts { "yes" } else { "no" }
);
println!(" Case transform: {:?}", seq.apply_case);
println!(
" Emoji handling: {}",
if seq.apply_emojis { "replace" } else { "keep" }
);
println!(
" Mode: {}",
if seq.minimal_mode { "minimal" } else { "full" }
);
} else {
println!(" {}", seq.description);
}
println!();
}
if !args.verbose {
println!("Nutze -L -v für detaillierte Informationen über jede Sequence.");
}
}