From 6be85969920d9bbff5c0a712e10d0035259437e1 Mon Sep 17 00:00:00 2001 From: sreedev Date: Mon, 17 Jul 2023 14:34:42 -0400 Subject: [PATCH] restored interactive mode --- src/formatter.rs | 14 +++- src/interactive.rs | 155 +++++++++++++++++++++++++++++++++++++++++++++ src/interative.rs | 85 ------------------------- src/main.rs | 18 ++++-- 4 files changed, 177 insertions(+), 95 deletions(-) create mode 100644 src/interactive.rs delete mode 100644 src/interative.rs diff --git a/src/formatter.rs b/src/formatter.rs index 35efcac..3f63592 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -16,7 +16,7 @@ use std::path::PathBuf; use std::time::Duration; impl Formatter { - fn human_path(file: &FileInfo, app_args: &Params, min_path_length: usize) -> Result { + pub fn human_path(file: &FileInfo, app_args: &Params, min_path_length: usize) -> Result { let base_directory: PathBuf = app_args.get_directory()?.to_path_buf(); let relative_path = diff_paths(file.path.clone(), base_directory).unwrap_or_default(); @@ -29,11 +29,11 @@ impl Formatter { Ok(formatted_path) } - fn human_filesize(file: &FileInfo) -> Result { + pub fn human_filesize(file: &FileInfo) -> Result { Ok(format!("{:>12}", bytesize::ByteSize::b(file.size))) } - fn human_mtime(file: &FileInfo) -> Result { + pub fn human_mtime(file: &FileInfo) -> Result { let modified_time: DateTime = file.filemeta.modified()?.into(); Ok(modified_time.format("%Y-%m-%d %H:%M:%S").to_string()) } @@ -110,6 +110,14 @@ impl Formatter { } pub fn print(raw: Vec, app_args: &Params) -> Result<()> { + if raw.is_empty() { + println!( + "\n\n{}\n", + "No duplicates found matching your search criteria.".green() + ); + return Ok(()); + } + let output_table = Self::generate_table(raw, app_args)?; output_table.printstd(); diff --git a/src/interactive.rs b/src/interactive.rs new file mode 100644 index 0000000..767ef7d --- /dev/null +++ b/src/interactive.rs @@ -0,0 +1,155 @@ +use crate::formatter::Formatter; +use crate::{fileinfo::FileInfo, params::Params}; +use anyhow::Result; +use colored::Colorize; +use dashmap::DashMap; +use indicatif::{ParallelProgressIterator, ProgressBar, ProgressFinish, ProgressStyle}; +use prettytable::{format, row, Table}; +use rayon::prelude::*; +use std::{ + borrow::Cow, + io::{self, Write}, + time::Duration, +}; + +pub fn scan_group_confirmation() -> Result { + print!("\nconfirm? [y/N]: "); + std::io::stdout().flush()?; + let mut user_input = String::new(); + io::stdin().read_line(&mut user_input)?; + + match user_input.trim() { + "Y" | "y" => Ok(true), + _ => Ok(false), + } +} + +pub fn scan_group_instruction() -> Result { + println!("\nEnter the indices of the files you want to delete."); + println!("You can enter multiple files using commas to seperate file indices."); + println!("example: 1,2"); + print!("\n> "); + std::io::stdout().flush()?; + let mut user_input = String::new(); + io::stdin().read_line(&mut user_input)?; + + Ok(user_input) +} + +pub fn init(result: Vec, app_args: &Params) -> Result<()> { + let basepath_length = app_args.get_directory()?.to_str().unwrap_or_default().len(); + let max_filepath_length = result + .iter() + .map(|file| file.path.to_str().unwrap_or_default().len()) + .max() + .unwrap_or_default(); + + let min_path_length = if max_filepath_length > basepath_length { + max_filepath_length - basepath_length + } else { + 0 + }; + + let progress_style = ProgressStyle::with_template( + "[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}", + )?; + let progress_bar = ProgressBar::new(result.len() as u64); + progress_bar.set_style(progress_style); + progress_bar.enable_steady_tick(Duration::from_millis(50)); + progress_bar.set_message("reconciling data"); + + let duplicates: DashMap> = DashMap::new(); + result + .clone() + .into_par_iter() + .progress_with(progress_bar) + .with_finish(ProgressFinish::WithMessage(Cow::from("data reconciled"))) + .map(|file| file.hash()) + .filter_map(Result::ok) + .for_each(|file| { + duplicates + .entry(file.hash.clone().unwrap_or_default()) + .and_modify(|fileset| fileset.push(file.clone())) + .or_insert_with(|| vec![file]); + }); + + duplicates + .clone() + .into_iter() + .enumerate() + .for_each(|(gindex, (_, group))| { + let mut itable = Table::new(); + itable.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + itable.set_titles(row!["index", "filename", "size", "updated_at"]); + group.iter().enumerate().for_each(|(index, file)| { + itable.add_row(row![ + index, + Formatter::human_path(&file, app_args, max_filepath_length) + .unwrap_or_default() + .blue(), + Formatter::human_filesize(file).unwrap_or_default().red(), + Formatter::human_mtime(&file).unwrap_or_default().yellow() + ]); + }); + + process_group_action(&group, gindex, duplicates.len(), itable); + }); + + Ok(()) +} + +pub fn process_group_action( + duplicates: &Vec, + dup_index: usize, + dup_size: usize, + table: Table, +) { + println!("\nDuplicate Set {} of {}\n", dup_index + 1, dup_size); + table.printstd(); + let files_to_delete = scan_group_instruction().unwrap_or_default(); + let parsed_file_indices = files_to_delete + .trim() + .split(',') + .filter(|element| !element.is_empty()) + .map(|index| index.parse::().unwrap_or_default()) + .collect::>(); + + if parsed_file_indices + .clone() + .into_iter() + .any(|index| index > (duplicates.len() - 1)) + { + println!("{}", "Err: File Index Out of Bounds!".red()); + return process_group_action(duplicates, dup_index, dup_size, table); + } + + print!("{esc}[2J{esc}[1;1H", esc = 27 as char); + + if parsed_file_indices.is_empty() { + return; + } + + let files_to_delete = parsed_file_indices + .into_iter() + .map(|index| duplicates[index].clone()); + + println!("\n{}", "The following files will be deleted:".red()); + files_to_delete + .clone() + .enumerate() + .for_each(|(index, file)| { + println!("{}: {}", index.to_string().blue(), file.path.display()); + }); + + match scan_group_confirmation().unwrap() { + true => { + files_to_delete.into_iter().for_each(|file| { + match std::fs::remove_file(file.path.clone()) { + Ok(_) => println!("{}: {}", "DELETED".green(), file.path.display()), + Err(_) => println!("{}: {}", "FAILED".red(), file.path.display()), + } + }); + } + false => println!("{}", "\nCancelled Delete Operation.".red()), + } +} diff --git a/src/interative.rs b/src/interative.rs deleted file mode 100644 index d2beea3..0000000 --- a/src/interative.rs +++ /dev/null @@ -1,85 +0,0 @@ -use anyhow::Result; -use colored::Colorize; -use crate::fileinfo::FileInfo; -use prettytable::Table; -use std::io::{self, Write}; - -pub fn scan_group_confirmation() -> Result { - print!("\nconfirm? [y/N]: "); - std::io::stdout().flush()?; - let mut user_input = String::new(); - io::stdin().read_line(&mut user_input)?; - - match user_input.trim() { - "Y" | "y" => Ok(true), - _ => Ok(false), - } -} - -pub fn scan_group_instruction() -> Result { - println!("\nEnter the indices of the files you want to delete."); - println!("You can enter multiple files using commas to seperate file indices."); - println!("example: 1,2"); - print!("\n> "); - std::io::stdout().flush()?; - let mut user_input = String::new(); - io::stdin().read_line(&mut user_input)?; - - Ok(user_input) -} - -pub fn process_group_action( - duplicates: &Vec, - dup_index: usize, - dup_size: usize, - table: Table, -) { - println!("\nDuplicate Set {} of {}\n", dup_index + 1, dup_size); - table.printstd(); - let files_to_delete = scan_group_instruction().unwrap_or_default(); - let parsed_file_indices = files_to_delete - .trim() - .split(',') - .filter(|element| !element.is_empty()) - .map(|index| index.parse::().unwrap_or_default()) - .collect::>(); - - if parsed_file_indices - .clone() - .into_iter() - .any(|index| index > (duplicates.len() - 1)) - { - println!("{}", "Err: File Index Out of Bounds!".red()); - return process_group_action(duplicates, dup_index, dup_size, table); - } - - print!("{esc}[2J{esc}[1;1H", esc = 27 as char); - - if parsed_file_indices.is_empty() { - return; - } - - let files_to_delete = parsed_file_indices - .into_iter() - .map(|index| duplicates[index].clone()); - - println!("\n{}", "The following files will be deleted:".red()); - files_to_delete - .clone() - .enumerate() - .for_each(|(index, file)| { - println!("{}: {}", index.to_string().blue(), file.path.display()); - }); - - match scan_group_confirmation().unwrap() { - true => { - files_to_delete.into_iter().for_each(|file| { - match std::fs::remove_file(file.path.clone()) { - Ok(_) => println!("{}: {}", "DELETED".green(), file.path.display()), - Err(_) => println!("{}: {}", "FAILED".red(), file.path.display()), - } - }); - } - false => println!("{}", "\nCancelled Delete Operation.".red()), - } -} diff --git a/src/main.rs b/src/main.rs index 2ffe1c3..f462459 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,16 @@ mod fileinfo; +mod formatter; +mod interactive; +mod params; mod processor; mod scanner; -mod params; -mod formatter; -mod interative; use anyhow::Result; -use params::Params; -use scanner::Scanner; -use processor::Processor; use clap::Parser; use formatter::Formatter; +use params::Params; +use processor::Processor; +use scanner::Scanner; fn main() -> Result<()> { let app_args = Params::parse(); @@ -18,6 +18,10 @@ fn main() -> Result<()> { let processor = Processor::new(scan_results); let results = processor.sizewise()?.hashwise()?; - Formatter::print(results.files, &app_args)?; + match app_args.interactive { + false => { Formatter::print(results.files, &app_args)?; } + true => { interactive::init(results.files, &app_args)?; } + } + Ok(()) }