diff --git a/Cargo.lock b/Cargo.lock index 25328e7..becd524 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,6 +40,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.11.1" @@ -223,6 +235,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "csv" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +dependencies = [ + "bstr", + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + [[package]] name = "cxx" version = "1.0.85" @@ -269,7 +303,7 @@ dependencies = [ [[package]] name = "deduplicator" -version = "0.0.4" +version = "0.0.6" dependencies = [ "anyhow", "chrono", @@ -280,6 +314,7 @@ dependencies = [ "glob", "humansize", "itertools", + "prettytable-rs", "rayon", "sqlite", "thiserror", @@ -287,12 +322,39 @@ dependencies = [ "tui", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "either" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "errno" version = "0.2.8" @@ -323,6 +385,17 @@ dependencies = [ "byteorder", ] +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + [[package]] name = "glob" version = "0.3.0" @@ -417,6 +490,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "js-sys" version = "0.3.60" @@ -581,6 +660,20 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +[[package]] +name = "prettytable-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" +dependencies = [ + "csv", + "encode_unicode", + "is-terminal", + "lazy_static", + "term", + "unicode-width", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -654,6 +747,23 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "rustix" version = "0.36.5" @@ -668,6 +778,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + [[package]] name = "scopeguard" version = "1.1.0" @@ -680,6 +802,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" + [[package]] name = "signal-hook" version = "0.3.14" @@ -773,6 +901,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "termcolor" version = "1.1.3" diff --git a/Cargo.toml b/Cargo.toml index 8fe0b60..a4f2297 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "deduplicator" -version = "0.0.4" +version = "0.0.6" edition = "2021" description = "find,filter,delete Duplicates" license = "MIT" -authors = ["Sreedev Kodichath ", "Valentin Bersier "] +authors = ["Sreedev Kodichath ", "Valentin Bersier ", "Dhruva Sagar "] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -18,6 +18,7 @@ fxhash = "0.2.1" glob = "0.3.0" humansize = "2.1.2" itertools = "0.10.5" +prettytable-rs = "0.10.0" rayon = "1.6.1" sqlite = "0.30.3" thiserror = "1.0.38" diff --git a/src/app/event_handler.rs b/src/app/event_handler.rs index 69260ae..da7129b 100644 --- a/src/app/event_handler.rs +++ b/src/app/event_handler.rs @@ -1,7 +1,8 @@ use std::time::Duration; -use crossterm::event::{self, KeyCode, KeyEvent}; use anyhow::Result; +use crossterm::event::{self, KeyCode, KeyEvent}; + use super::events; pub struct EventHandler; @@ -21,8 +22,7 @@ impl EventHandler { fn handle_keypress(keyevent: KeyEvent) -> Result { match keyevent.code { KeyCode::Char('q') => Ok(events::Event::Exit), - _ => Ok(events::Event::Noop) + _ => Ok(events::Event::Noop), } } } - diff --git a/src/app/events.rs b/src/app/events.rs index a889cc3..c7167d0 100644 --- a/src/app/events.rs +++ b/src/app/events.rs @@ -1,4 +1,4 @@ pub enum Event { Exit, - Noop + Noop, } diff --git a/src/app/mod.rs b/src/app/mod.rs index 92f6736..3a8b102 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,18 +1,13 @@ mod event_handler; mod events; -mod ui; mod formatter; +mod ui; + +use std::{io, thread, time::Duration}; -use crate::database; -use crate::output; -use crate::params::Params; -use crate::scanner; use anyhow::{anyhow, Result}; use crossterm::{event, execute, terminal}; use event_handler::EventHandler; -use std::io; -use std::thread; -use std::time::Duration; use tui::{ backend::CrosstermBackend, widgets::{Block, Borders, Widget}, @@ -20,19 +15,24 @@ use tui::{ }; use ui::Ui; +use crate::database; +use crate::output; +use crate::params::Params; +use crate::scanner; + pub struct App; impl App { pub fn init(app_args: &Params) -> Result<()> { // let mut term = Self::init_terminal()?; - let connection = database::get_connection(&app_args)?; - let duplicates = scanner::duplicates(&app_args, &connection)?; + let connection = database::get_connection(app_args)?; + let duplicates = scanner::duplicates(app_args, &connection)?; // Self::init_render_loop(&mut term)?; // Self::cleanup(&mut term)?; - output::print(duplicates, &app_args); /* TODO: APP TUI INIT FUNCTION */ + output::print(duplicates, app_args); /* TODO: APP TUI INIT FUNCTION */ Ok(()) } @@ -56,6 +56,8 @@ impl App { } fn init_render_loop(term: &mut Terminal>) -> Result<()> { + // this could be simplified with a `while Self::render_cycle(term).is_ok() {}` in the current state, but maybe + // it's good to keep it to handle errors in the future loop { match Self::render_cycle(term) { Ok(_) => continue, diff --git a/src/app/ui.rs b/src/app/ui.rs index 19413a4..9b8c63b 100644 --- a/src/app/ui.rs +++ b/src/app/ui.rs @@ -1,5 +1,6 @@ -use anyhow::Result; use std::io; + +use anyhow::Result; use tui::{ backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout, Rect}, diff --git a/src/database.rs b/src/database.rs index 4713aa7..371d41f 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,4 +1,7 @@ +use std::env::temp_dir; + use anyhow::Result; + use crate::params::Params; #[derive(Debug, Clone)] @@ -7,13 +10,18 @@ pub struct File { pub hash: String, } -pub fn get_connection(args: &Params) -> Result { - let connection_url = match args.nocache { - false => "/tmp/deduplicator.db", - true => ":memory:" - }; +fn db_connection_url(args: &Params) -> String { + match args.nocache { + true => String::from(":memory:"), + false => { + let temp_dir_path = temp_dir(); + format!("{}/deduplicator.db", temp_dir_path.display()) + } + } +} - sqlite::open(connection_url).and_then(|conn| { +pub fn get_connection(args: &Params) -> Result { + sqlite::open(db_connection_url(args)).and_then(|conn| { setup(&conn).ok(); Ok(conn) }) @@ -30,48 +38,46 @@ pub fn put(file: &File, connection: &sqlite::Connection) -> Result<()> { "INSERT INTO files (file_identifier, hash) VALUES (\"{}\", \"{}\")", file.path, file.hash ); - let result = connection.execute(query)?; - - Ok(result) + connection.execute(query)?; + Ok(()) } pub fn indexed_paths(connection: &sqlite::Connection) -> Result> { - let query = format!( - "SELECT * FROM files" - ); + let query = "SELECT * FROM files"; let result: Vec = connection .prepare(query)? .into_iter() - .map(|row_result| row_result.unwrap()) - .map(|row| { - let path = row.read::<&str, _>("file_identifier").to_string(); - let hash = row.read::("hash").to_string(); - File { path, hash } - }) - .collect(); - - Ok(result) -} - -pub fn duplicate_hashes(connection: &sqlite::Connection, path: &String) -> Result> { - let query = format!( - " - SELECT a.* FROM files a - JOIN (SELECT file_identifier, hash, COUNT(*) - FROM files - GROUP BY hash - HAVING count(*) > 1 ) b - ON a.hash = b.hash - WHERE a.file_identifier LIKE \"{}%\" - ORDER BY a.file_identifier - ", path - ); - - let result: Vec = connection - .prepare(query)? - .into_iter() - .map(|row_result| row_result.unwrap()) + .filter_map(|row_result| row_result.ok()) + .map(|row| { + let path = row.read::<&str, _>("file_identifier").to_string(); + let hash = row.read::("hash").to_string(); + File { path, hash } + }) + .collect(); + + Ok(result) +} + +pub fn duplicate_hashes(connection: &sqlite::Connection, path: &str) -> Result> { + let query = format!( + " + SELECT a.* FROM files a + JOIN (SELECT file_identifier, hash, COUNT(*) + FROM files + GROUP BY hash + HAVING count(*) > 1 ) b + ON a.hash = b.hash + WHERE a.file_identifier LIKE \"{}%\" + ORDER BY a.file_identifier + ", + path + ); + + let result: Vec = connection + .prepare(query)? + .into_iter() + .filter_map(|row_result| row_result.ok()) .map(|row| { let path = row.read::<&str, _>("file_identifier").to_string(); let hash = row.read::("hash").to_string(); diff --git a/src/main.rs b/src/main.rs index 7cb8c91..21ac960 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,13 @@ -mod params; +#![allow(unused)] // TODO: remove this once TUI is implemented +mod app; mod database; mod output; +mod params; mod scanner; -mod app; use anyhow::Result; -use clap::Parser; use app::App; +use clap::Parser; #[tokio::main] async fn main() -> Result<()> { diff --git a/src/output.rs b/src/output.rs index 4824921..0fedfea 100644 --- a/src/output.rs +++ b/src/output.rs @@ -8,6 +8,7 @@ use humansize::{format_size, DECIMAL}; use crate::database::File; use crate::params::Params; +use prettytable::{row, Cell, Row, format, Table}; fn format_path(path: &str, opts: &Params) -> Result { let display_path = path.replace(&opts.get_directory()?, ""); @@ -21,12 +22,12 @@ fn format_path(path: &str, opts: &Params) -> Result { display_path }; - Ok(format!("...{}", display_range)) + Ok(format!("...{:<32}", display_range)) } fn file_size(path: &String) -> Result { let mdata = fs::metadata(path)?; - let formatted_size = format_size(mdata.len(), DECIMAL); + let formatted_size = format!("{:>12}", format_size(mdata.len(), DECIMAL)); Ok(formatted_size) } @@ -37,37 +38,37 @@ fn modified_time(path: &String) -> Result { Ok(modified_time.format("%Y-%m-%d %H:%M:%S").to_string()) } -fn print_divider() { - println!("-------------------+-------------------------------------+------------------+----------------------------------+"); -} - -pub fn print(duplicates: Vec, opts: &Params) { - print_divider(); - println!( - "| {0: <16} | {1: <35} | {2: <16} | {3: <32} |", - "hash", "filename", "size", "updated_at" - ); - print_divider(); - - let mut dup_index: HashMap> = HashMap::new(); - +fn group_duplicates(duplicates: Vec) -> HashMap> { + let mut duplicate_mapper: HashMap> = HashMap::new(); duplicates.into_iter().for_each(|file| { - dup_index + duplicate_mapper .entry(file.hash.clone()) .and_modify(|value| value.push(file.clone())) .or_insert_with(|| vec![file]); }); - dup_index.iter().for_each(|(_, group)| { - group.iter().for_each(|file| { - println!( - "| {0: <16} | {1: <35} | {2: <16} | {3: <32} |", - file.hash.red(), - format_path(&file.path, opts).unwrap_or_default().yellow(), - file_size(&file.path).unwrap_or_default().blue(), - modified_time(&file.path).unwrap_or_default().blue() - ); - }); - print_divider(); - }); + duplicate_mapper +} + +pub fn print(duplicates: Vec, opts: &Params) { + let mut output_table = Table::new(); + let grouped_duplicates: HashMap> = group_duplicates(duplicates); + + output_table.set_titles(row!["hash", "duplicates"]); + grouped_duplicates.iter().for_each(|(hash, group)| { + let mut inner_table = Table::new(); + // inner_table.set_format(inner_table_format); + inner_table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + //inner_table.set_titles(row!["filename", "size", "updated_at"]); + group.iter().for_each(|file| { + inner_table.add_row(row![ + format_path(&file.path, opts).unwrap_or_default().blue(), + file_size(&file.path).unwrap_or_default().red(), + modified_time(&file.path).unwrap_or_default().yellow() + ]); + }); + output_table.add_row(row![hash.green(), inner_table]); + }); + + output_table.printstd(); } diff --git a/src/params.rs b/src/params.rs index d5f79f6..11741d9 100644 --- a/src/params.rs +++ b/src/params.rs @@ -1,7 +1,7 @@ -use std::path::PathBuf; +use std::{fs, path::PathBuf}; + +use anyhow::{anyhow, Result}; use clap::Parser; -use anyhow::Result; -use std::fs; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -22,20 +22,17 @@ pub struct Params { impl Params { pub fn get_directory(&self) -> Result { - let dir_string: String = self + let dir_pathbuf: PathBuf = self .dir .clone() .unwrap_or(std::env::current_dir()?) .as_os_str() - .to_str() - .unwrap() - .to_string(); + .into(); - let dir_pathbuf = PathBuf::from(&dir_string); - let dir = fs::canonicalize(&dir_pathbuf)? + let dir = fs::canonicalize(dir_pathbuf)? .as_os_str() .to_str() - .unwrap() + .ok_or_else(|| anyhow!("Invalid directory"))? .to_string(); Ok(dir)