mirror of
https://github.com/sreedevk/deduplicator.git
synced 2026-09-01 13:06:43 +00:00
restored interactive mode
This commit is contained in:
@@ -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<String> {
|
||||
pub fn human_path(file: &FileInfo, app_args: &Params, min_path_length: usize) -> Result<String> {
|
||||
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<String> {
|
||||
pub fn human_filesize(file: &FileInfo) -> Result<String> {
|
||||
Ok(format!("{:>12}", bytesize::ByteSize::b(file.size)))
|
||||
}
|
||||
|
||||
fn human_mtime(file: &FileInfo) -> Result<String> {
|
||||
pub fn human_mtime(file: &FileInfo) -> Result<String> {
|
||||
let modified_time: DateTime<Utc> = 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<FileInfo>, 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();
|
||||
|
||||
|
||||
155
src/interactive.rs
Normal file
155
src/interactive.rs
Normal file
@@ -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<bool> {
|
||||
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<String> {
|
||||
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<FileInfo>, 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<String, Vec<FileInfo>> = 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<FileInfo>,
|
||||
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::<usize>().unwrap_or_default())
|
||||
.collect::<Vec<usize>>();
|
||||
|
||||
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()),
|
||||
}
|
||||
}
|
||||
@@ -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<bool> {
|
||||
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<String> {
|
||||
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<FileInfo>,
|
||||
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::<usize>().unwrap_or_default())
|
||||
.collect::<Vec<usize>>();
|
||||
|
||||
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()),
|
||||
}
|
||||
}
|
||||
18
src/main.rs
18
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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user