use anyhow::Result;
use humansize::{format_size, DECIMAL};
use simplelog::LevelFilter;
use std::path::Path;
use std::str::FromStr;
use dedups::config::DedupConfig;
use dedups::file_utils;
use dedups::tui_app;
use dedups::Cli;
fn setup_logger(verbosity: u8, log_file: Option<&Path>) -> Result<()> {
let level = match verbosity {
0 => LevelFilter::Info,
1 => LevelFilter::Debug,
_ => LevelFilter::Trace,
};
let mut builder = env_logger::Builder::new();
builder.filter_level(level);
builder.format_timestamp_millis();
builder.format_target(false);
if let Some(log_path) = log_file {
let file = std::fs::File::create(log_path)?;
builder.target(env_logger::Target::Pipe(Box::new(file)));
}
builder.init();
Ok(())
}
fn main() -> Result<()> {
let cli = Cli::with_config()?;
if cli.interactive {
let log_file = Some(Path::new("dedups.log"));
setup_logger(cli.verbose, log_file)?;
} else if cli.log || cli.log_file.is_some() {
let log_path = if let Some(path) = &cli.log_file {
path.as_path()
} else {
Path::new("dedups.log")
};
setup_logger(cli.verbose, Some(log_path))?;
} else if cli.progress {
simplelog::TermLogger::init(
match cli.verbose {
0 => LevelFilter::Info,
1 => LevelFilter::Debug,
_ => LevelFilter::Trace,
},
simplelog::Config::default(),
simplelog::TerminalMode::Mixed,
simplelog::ColorChoice::Auto,
)?;
} else {
simplelog::SimpleLogger::init(
match cli.verbose {
0 => LevelFilter::Info,
1 => LevelFilter::Debug,
_ => LevelFilter::Trace,
},
simplelog::Config::default(),
)?;
}
log::info!("Logger initialized. Application starting.");
log::debug!("CLI args: {:#?}", cli);
if let Some(config_path) = &cli.config_file {
log::info!("Using custom config file: {:?}", config_path);
if !config_path.exists() {
log::warn!("Custom config file does not exist: {:?}", config_path);
log::info!("Will use default configuration values");
}
} else {
match DedupConfig::get_config_path() {
Ok(path) => {
log::debug!("Config file path: {:?}", path);
if path.exists() {
log::debug!("Using configuration from {:?}", path);
} else {
log::debug!("No config file found at {:?}, using defaults", path);
}
}
Err(e) => log::warn!("Could not determine config file path: {}", e),
}
}
for dir in &cli.directories {
if !dir.exists() {
log::error!("Target directory {:?} does not exist.", dir);
return Err(anyhow::anyhow!(
"Target directory does not exist: {:?}",
dir
));
}
if !dir.is_dir() {
log::error!("Target path {:?} is not a directory.", dir);
return Err(anyhow::anyhow!("Target path is not a directory: {:?}", dir));
}
}
let is_multi_directory = cli.directories.len() > 1 || cli.target.is_some();
if cli.interactive {
log::info!(
"Interactive mode selected for directories: {:?}",
cli.directories
);
tui_app::run_tui_app(&cli)?
} else if is_multi_directory {
handle_multi_directory_mode(&cli)?;
} else {
log::info!(
"Non-interactive mode selected for directory: {:?}",
cli.directories[0]
);
let (tx, _rx) = std::sync::mpsc::channel();
match file_utils::find_duplicate_files_with_progress(&cli, tx) {
Ok(duplicate_sets) => {
if duplicate_sets.is_empty() {
log::info!("No duplicate files found.");
println!("No duplicate files found.");
} else {
handle_duplicate_sets(&cli, &duplicate_sets)?;
}
}
Err(e) => {
log::error!("Error finding duplicate files: {}", e);
eprintln!("Error: {}", e);
}
}
}
Ok(())
}
fn handle_multi_directory_mode(cli: &Cli) -> Result<()> {
log::info!("Multi-directory mode: Comparing directories");
println!("Comparing directories for missing files or duplicates...");
if cli.dry_run {
log::info!("Running in DRY RUN mode - no files will be modified");
println!("\n===== DRY RUN MODE - NO FILES WILL BE MODIFIED =====\n");
}
let target_dir = file_utils::determine_target_directory(cli)?;
let source_dirs = file_utils::get_source_directories(cli, &target_dir);
println!("Source directories: {:?}", source_dirs);
println!("Target directory: {:?}", target_dir);
let comparison_result = file_utils::compare_directories(cli)?;
if !comparison_result.missing_in_target.is_empty() {
println!(
"Found {} files that exist in source but not in target.",
comparison_result.missing_in_target.len()
);
if cli.deduplicate {
println!("Deduplication mode enabled. Missing files will be considered separately from duplicates.");
}
if cli.delete {
println!("Warning: Delete flag is ignored for missing files. Use --deduplicate to handle duplicates.");
} else if cli.move_to.is_some() {
println!("Warning: Move flag is ignored for missing files. They will be copied to the target directory.");
}
match file_utils::copy_missing_files(
&comparison_result.missing_in_target,
&target_dir,
cli.dry_run,
) {
Ok((count, logs)) => {
for log_msg in logs {
if !log_msg.starts_with("[DRY RUN]") {
log::info!("{}", log_msg);
}
println!("{}", log_msg);
}
let action_prefix = if cli.dry_run {
"[DRY RUN] Would have copied"
} else {
"Successfully copied"
};
println!("\n{} {} files to target directory.", action_prefix, count);
}
Err(e) => {
log::error!("Failed to copy files: {}", e);
eprintln!("Error copying files: {}", e);
}
}
} else {
println!("No missing files found in target directory.");
}
if cli.deduplicate && !comparison_result.duplicates.is_empty() {
println!(
"Found {} duplicate sets across source and target directories.",
comparison_result.duplicates.len()
);
handle_duplicate_sets(cli, &comparison_result.duplicates)?;
} else if cli.deduplicate {
println!("No duplicate files found across source and target directories.");
}
if cli.dry_run {
println!("\nThis was a dry run. No files were actually modified.");
println!("Run without --dry-run to perform actual operations.");
log::info!("Dry run completed - no files were modified");
}
Ok(())
}
fn handle_duplicate_sets(cli: &Cli, duplicate_sets: &[file_utils::DuplicateSet]) -> Result<()> {
log::info!("Found {} sets of duplicate files.", duplicate_sets.len());
println!("Found {} sets of duplicate files:", duplicate_sets.len());
for set in duplicate_sets {
println!(
" Duplicates ({} files, size: {}, hash: {}...):",
set.files.len(),
format_size(set.size, DECIMAL),
set.hash.chars().take(16).collect::<String>()
);
for file_info in &set.files {
println!(" - {}", file_info.path.display());
}
}
if let Some(output_path) = &cli.output {
match file_utils::output_duplicates(duplicate_sets, output_path, &cli.format) {
Ok(_) => {
log::info!("Successfully wrote duplicate list to {:?}", output_path);
println!("Duplicate list saved to {:?}", output_path);
}
Err(e) => {
log::error!("Failed to write duplicate list to {:?}: {}", output_path, e);
eprintln!("Failed to write output file: {}", e);
}
}
}
if cli.delete || cli.move_to.is_some() {
if cli.dry_run {
log::info!("Running in DRY RUN mode - no files will be modified");
println!("\n===== DRY RUN MODE - NO FILES WILL BE MODIFIED =====\n");
}
let strategy = file_utils::SelectionStrategy::from_str(&cli.mode)?;
let mut total_deleted = 0;
let mut total_moved = 0;
for set in duplicate_sets {
if set.files.len() < 2 {
continue;
}
match file_utils::determine_action_targets(set, strategy) {
Ok((kept_file, files_to_action)) => {
log::info!(
"For duplicate set (hash: {}...), keeping file: {:?}",
set.hash.chars().take(8).collect::<String>(),
kept_file.path
);
println!("Keeping: {}", kept_file.path.display());
if cli.delete {
match file_utils::delete_files(&files_to_action, cli.dry_run) {
Ok((count, logs)) => {
total_deleted += count;
for log_msg in logs {
log::info!("{}", log_msg);
println!("{}", log_msg);
}
}
Err(e) => {
log::error!("Error during deletion batch: {}", e);
eprintln!("Error: {}", e);
}
}
} else if let Some(ref target_move_dir) = cli.move_to {
match file_utils::move_files(&files_to_action, target_move_dir, cli.dry_run)
{
Ok((count, logs)) => {
total_moved += count;
for log_msg in logs {
log::info!("{}", log_msg);
println!("{}", log_msg);
}
}
Err(e) => {
log::error!("Error during move batch: {}", e);
eprintln!("Error: {}", e);
}
}
}
}
Err(e) => {
log::error!("Could not determine action targets for a set: {}", e);
eprintln!(
"Error: Could not determine which files to keep/delete: {}",
e
);
}
}
}
let action_prefix = if cli.dry_run {
"[DRY RUN] Would have "
} else {
""
};
if cli.delete {
let msg = format!("{}deleted {} files", action_prefix, total_deleted);
log::info!("{}", msg);
println!("\n{}", msg);
}
if cli.move_to.is_some() {
let msg = format!("{}moved {} files", action_prefix, total_moved);
log::info!("{}", msg);
println!("\n{}", msg);
}
if cli.dry_run {
println!("\nThis was a dry run. No files were actually modified.");
println!("Run without --dry-run to perform actual operations.");
log::info!("Dry run completed - no files were modified");
}
} else {
log::info!("No action flags (--delete or --move-to) specified. Listing duplicates only.");
}
Ok(())
}