21 Commits
0.0.6 ... 0.0.9

Author SHA1 Message Date
Sreedev Kodichath
a6511f2cf3 Merge pull request #20 from sreedevk/development
Version 0.0.9
2023-01-13 00:55:25 -05:00
sreedev
64d0106765 version changes 2023-01-13 00:53:46 -05:00
Sreedev Kodichath
c75b2eb1c9 Merge pull request #17 from ghfghfg23/main
Fix of "thread 'main' panicked at 'range start index 130 out of range…
2023-01-13 00:49:13 -05:00
Andrey Ryabov
b736853dfe Take 32 grapheme clusters instead of characters 2023-01-12 21:43:31 +02:00
Sreedev Kodichath
92290480a8 Update README.md 2023-01-11 19:41:23 -05:00
Sreedev Kodichath
b7f775e04c Update README.md 2023-01-11 19:39:02 -05:00
sreedev
12295d7847 version 0.0.8 2023-01-11 19:28:34 -05:00
Sreedev Kodichath
d81f499db3 Create CONTRIBUTING.md 2023-01-11 19:14:39 -05:00
Sreedev Kodichath
9e1360aeb4 Merge pull request #18 from sreedevk/progress-bar
[Feature] Add Progress Bar When Indexing Files
2023-01-11 18:56:40 -05:00
Sreedev Kodichath
38ea37711f Update README.md 2023-01-11 18:37:10 -05:00
Andrey Ryabov
2294471b50 Fix of "thread 'main' panicked at 'range start index 130 out of range for slice of length 104', src/output.rs:21:9" 2023-01-11 23:01:10 +02:00
sreedev
dbf504fc17 added progress bar 2023-01-11 15:55:31 -05:00
Sreedev Kodichath
06efb2ed6f Update issue templates 2023-01-11 07:45:51 -05:00
sreedev
95af6c4a70 updated docs to show interactive mode 2023-01-10 20:28:53 -05:00
Sreedev Kodichath
dd4b051378 Merge pull request #16 from sreedevk/interactive-mode
Interactive mode
2023-01-10 20:26:38 -05:00
sreedev
533f81f724 version 0.0.7 2023-01-10 20:26:03 -05:00
sreedev
471e60fa6c added delete options 2023-01-10 20:23:53 -05:00
sreedev
3fd4869869 UI setup complete 2023-01-10 20:13:45 -05:00
sreedev
99b87cf7fa Merge branch 'main' into interactive-mode 2023-01-09 22:55:14 -05:00
Sreedev Kodichath
b826dbe118 Update README.md 2023-01-09 22:54:19 -05:00
sreedev
0f33b4d6b5 added interactive param 2023-01-09 11:13:29 -05:00
11 changed files with 273 additions and 20 deletions

30
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,30 @@
---
name: Bug report
about: Create a report to help us improve
title: "[Bug] Title"
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
** Runtime Info **
App Arguments: [e.g. `-i --nocache`]
Install Type: [e.g. `cargo install`]
App Version: [e.g. v0.0.7]
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Platform Details (please complete the following information):**
- OS: [e.g. Arch Linux]
- Terminal Emulator: [e.g Alacritty]
- Shell [e.g. Zshell]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature] Title"
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

27
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,27 @@
## How to contribute to Deduplicator
#### **Did you find a bug?**
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/sreedevk/deduplicator/issues).
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/sreedevk/deduplicator/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
* If possible, use the [bug report template](https://github.com/sreedevk/deduplicator/blob/main/.github/ISSUE_TEMPLATE/bug_report.md) to create the issue.
#### **Would you like to write a fix for the bug?**
* Assign the Issue to yourself (if unassigned) before you start working in order to avoid any conficts.
* Open a new GitHub pull request with the patch.
* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number.
#### **Did you fix whitespace, format code, or make a purely cosmetic patch?**
Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Deduplicator will generally not be accepted/
#### **Do you intend to add a new feature or change an existing one?**
* First open an issue with the sugggestion using the [feature request template](https://github.com/sreedevk/deduplicator/blob/main/.github/ISSUE_TEMPLATE/feature-request.md)
* Do not create a PR before one of the core contributors has conveyed acceptance for a feature request.
#### **Do you have questions about the source code?**
* If you have a question, raise an issue in the repository with a "question" label.

51
Cargo.lock generated
View File

@@ -161,6 +161,19 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "console"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9b6515d269224923b26b5febea2ed42b2d5f2ce37284a4dd670fedd6cb8347a"
dependencies = [
"encode_unicode 0.3.6",
"lazy_static",
"libc",
"unicode-width",
"windows-sys",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.3" version = "0.8.3"
@@ -303,7 +316,7 @@ dependencies = [
[[package]] [[package]]
name = "deduplicator" name = "deduplicator"
version = "0.0.6" version = "0.0.9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -313,6 +326,7 @@ dependencies = [
"fxhash", "fxhash",
"glob", "glob",
"humansize", "humansize",
"indicatif",
"itertools", "itertools",
"prettytable-rs", "prettytable-rs",
"rayon", "rayon",
@@ -320,6 +334,7 @@ dependencies = [
"thiserror", "thiserror",
"tokio", "tokio",
"tui", "tui",
"unicode-segmentation",
] ]
[[package]] [[package]]
@@ -349,6 +364,12 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]] [[package]]
name = "encode_unicode" name = "encode_unicode"
version = "1.0.0" version = "1.0.0"
@@ -459,6 +480,20 @@ dependencies = [
"cxx-build", "cxx-build",
] ]
[[package]]
name = "indicatif"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4295cbb7573c16d310e99e713cf9e75101eb190ab31fccd35f2d2691b4352b19"
dependencies = [
"console",
"number_prefix",
"portable-atomic",
"rayon",
"tokio",
"unicode-width",
]
[[package]] [[package]]
name = "io-lifetimes" name = "io-lifetimes"
version = "1.0.3" version = "1.0.3"
@@ -613,6 +648,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.16.0" version = "1.16.0"
@@ -660,6 +701,12 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "portable-atomic"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b"
[[package]] [[package]]
name = "prettytable-rs" name = "prettytable-rs"
version = "0.10.0" version = "0.10.0"
@@ -667,7 +714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a"
dependencies = [ dependencies = [
"csv", "csv",
"encode_unicode", "encode_unicode 1.0.0",
"is-terminal", "is-terminal",
"lazy_static", "lazy_static",
"term", "term",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "deduplicator" name = "deduplicator"
version = "0.0.6" version = "0.0.9"
edition = "2021" edition = "2021"
description = "find,filter,delete Duplicates" description = "find,filter,delete Duplicates"
license = "MIT" license = "MIT"
@@ -17,6 +17,7 @@ crossterm = "0.25.0"
fxhash = "0.2.1" fxhash = "0.2.1"
glob = "0.3.0" glob = "0.3.0"
humansize = "2.1.2" humansize = "2.1.2"
indicatif = { version = "0.17.2", features = ["rayon", "tokio"] }
itertools = "0.10.5" itertools = "0.10.5"
prettytable-rs = "0.10.0" prettytable-rs = "0.10.0"
rayon = "1.6.1" rayon = "1.6.1"
@@ -24,3 +25,4 @@ sqlite = "0.30.3"
thiserror = "1.0.38" thiserror = "1.0.38"
tokio = { version = "1.23.0", features = ["full"] } tokio = { version = "1.23.0", features = ["full"] }
tui = "0.19.0" tui = "0.19.0"
unicode-segmentation = "1.10.0"

View File

@@ -16,7 +16,8 @@ Usage: deduplicator [OPTIONS]
Options: Options:
-t, --types <TYPES> Filetypes to deduplicate (default = all) -t, --types <TYPES> Filetypes to deduplicate (default = all)
--dir <DIR> Run Deduplicator on dir different from pwd --dir <DIR> Run Deduplicator on dir different from pwd
-n, --nocache Don't use cache for indexing files (default = true) -n, --nocache Don't use cache for indexing files (default = false)
-i, --interactive Delete files interactively
-h, --help Print help information -h, --help Print help information
-V, --version Print version information -V, --version Print version information
``` ```
@@ -37,9 +38,13 @@ cargo install deduplicator
<p align="center"> <p align="center">
Deduplicator uses fxhash (a non-cryptographic hashing algorithm) which is extremely fast. As a result, deduplicator is able to process huge amounts of data in a couple of seconds.</p> Deduplicator uses fxhash (a non-cryptographic hashing algorithm) which is extremely fast. As a result, deduplicator is able to process huge amounts of data in a couple of seconds.</p>
<p align="center"> <p align="center">
While testing, Deduplicator was able to go through 8.6GB of pdf files and detect duplicates in 2.9 seconds While testing, Deduplicator was able to go through 8.6GB of pdf files and detect duplicates in 2.9 seconds
</p> </p>
<h2 align="center">Screenshots</h2> <h2 align="center">Screenshots</h2>
![_039](https://user-images.githubusercontent.com/36154121/210031222-d8b79143-5a1e-47ca-926e-8855d5bbab60.png)
<img src="https://user-images.githubusercontent.com/36154121/211948081-63c12b94-6251-487b-a49f-ac5418169d5a.gif" />
<img src="https://user-images.githubusercontent.com/36154121/211458077-90092aa3-496c-492f-a061-618059890d5f.png" />

14
src/app/file_manager.rs Normal file
View File

@@ -0,0 +1,14 @@
use crate::database::File;
use anyhow::Result;
use colored::Colorize;
pub fn delete_files(files: Vec<File>) -> Result<()> {
files.into_iter().for_each(|file| {
match std::fs::remove_file(file.path.clone()) {
Ok(_) => println!("{}: {}", "DELETED".green(), file.path),
Err(e) => println!("{}: {}", "FAILED".red(), file.path)
}
});
Ok(())
}

View File

@@ -1,7 +1,10 @@
#![allow(unused)]
mod event_handler; mod event_handler;
mod events; mod events;
mod formatter; mod formatter;
mod ui; mod ui;
pub mod file_manager;
use std::{io, thread, time::Duration}; use std::{io, thread, time::Duration};
@@ -32,7 +35,11 @@ impl App {
// Self::init_render_loop(&mut term)?; // Self::init_render_loop(&mut term)?;
// Self::cleanup(&mut term)?; // Self::cleanup(&mut term)?;
output::print(duplicates, app_args); /* TODO: APP TUI INIT FUNCTION */ match app_args.interactive {
true => output::interactive(duplicates, app_args),
false => output::print(duplicates, app_args) /* TODO: APP TUI INIT FUNCTION */
}
Ok(()) Ok(())
} }

View File

@@ -1,23 +1,31 @@
use std::{collections::HashMap, fs}; use std::{collections::HashMap, fs, io};
use std::io::Write;
use anyhow::Result; use anyhow::Result;
use chrono::offset::Utc; use chrono::offset::Utc;
use chrono::DateTime; use chrono::DateTime;
use colored::Colorize; use colored::Colorize;
use humansize::{format_size, DECIMAL}; use humansize::{format_size, DECIMAL};
use itertools::Itertools;
use crate::app::file_manager;
use crate::database::File; use crate::database::File;
use crate::params::Params; use crate::params::Params;
use prettytable::{row, Cell, Row, format, Table}; use prettytable::{format, row, Cell, Row, Table};
use unicode_segmentation::UnicodeSegmentation;
fn format_path(path: &str, opts: &Params) -> Result<String> { fn format_path(path: &str, opts: &Params) -> Result<String> {
let display_path = path.replace(&opts.get_directory()?, ""); let display_path = path.replace(&opts.get_directory()?, "");
let text_vec = display_path.chars().collect::<Vec<_>>();
let display_range = if text_vec.len() > 32 { let display_range = if display_path.chars().count() > 32 {
text_vec[(display_path.len() - 32)..] display_path
.iter() .graphemes(true)
.collect::<String>() .collect::<Vec<&str>>()
.into_iter()
.rev()
.take(32)
.rev()
.collect()
} else { } else {
display_path display_path
}; };
@@ -50,16 +58,104 @@ fn group_duplicates(duplicates: Vec<File>) -> HashMap<String, Vec<File>> {
duplicate_mapper duplicate_mapper
} }
fn print_meta_info(duplicates: &Vec<File>, opts: &Params) {
println!("Deduplicator v{}", std::env!("CARGO_PKG_VERSION"));
}
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)
}
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)
}
}
fn process_group_action(duplicates: &Vec<File>, 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();
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);
});
match scan_group_confirmation().unwrap() {
true => { file_manager::delete_files(files_to_delete.collect_vec()); },
false => println!("{}", "\nCancelled Delete Operation.".red())
}
}
pub fn interactive(duplicates: Vec<File>, opts: &Params) {
print_meta_info(&duplicates, opts);
let grouped_duplicates = group_duplicates(duplicates);
grouped_duplicates.iter().enumerate().for_each(|(gindex, (hash, 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,
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()
]);
});
process_group_action(group, gindex, grouped_duplicates.len(), itable);
});
}
pub fn print(duplicates: Vec<File>, opts: &Params) { pub fn print(duplicates: Vec<File>, opts: &Params) {
print_meta_info(&duplicates, opts);
let mut output_table = Table::new(); let mut output_table = Table::new();
let grouped_duplicates: HashMap<String, Vec<File>> = group_duplicates(duplicates); let grouped_duplicates: HashMap<String, Vec<File>> = group_duplicates(duplicates);
output_table.set_titles(row!["hash", "duplicates"]); output_table.set_titles(row!["hash", "duplicates"]);
grouped_duplicates.iter().for_each(|(hash, group)| { grouped_duplicates.iter().for_each(|(hash, group)| {
let mut inner_table = Table::new(); 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_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
//inner_table.set_titles(row!["filename", "size", "updated_at"]);
group.iter().for_each(|file| { group.iter().for_each(|file| {
inner_table.add_row(row![ inner_table.add_row(row![
format_path(&file.path, opts).unwrap_or_default().blue(), format_path(&file.path, opts).unwrap_or_default().blue(),

View File

@@ -12,9 +12,12 @@ pub struct Params {
/// Run Deduplicator on dir different from pwd /// Run Deduplicator on dir different from pwd
#[arg(long)] #[arg(long)]
pub dir: Option<PathBuf>, pub dir: Option<PathBuf>,
/// Don't use cache for indexing files (default = true) /// Don't use cache for indexing files (default = false)
#[arg(long, short)] #[arg(long, short)]
pub nocache: bool, pub nocache: bool,
/// Delete files interactively
#[arg(long, short)]
pub interactive: bool
} }
impl Params { impl Params {

View File

@@ -1,5 +1,5 @@
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle, ParallelProgressIterator};
use anyhow::Result; use anyhow::Result;
use fxhash::hash32 as hasher; use fxhash::hash32 as hasher;
use glob::glob; use glob::glob;
@@ -46,6 +46,7 @@ fn scan(app_opts: &Params, connection: &sqlite::Connection) -> Result<Vec<String
let indexed_paths = database::indexed_paths(connection)?; let indexed_paths = database::indexed_paths(connection)?;
let files: Vec<String> = glob_patterns let files: Vec<String> = glob_patterns
.par_iter() .par_iter()
.progress_with_style(ProgressStyle::with_template("{spinner:.green} [scanning files] [{wide_bar:.cyan/blue}] {pos}/{len} files").unwrap())
.filter_map(|glob_pattern| glob(glob_pattern.as_os_str().to_str()?).ok()) .filter_map(|glob_pattern| glob(glob_pattern.as_os_str().to_str()?).ok())
.flat_map(|file_vec| { .flat_map(|file_vec| {
file_vec file_vec
@@ -66,6 +67,7 @@ fn scan(app_opts: &Params, connection: &sqlite::Connection) -> Result<Vec<String
fn index_files(files: Vec<String>, connection: &sqlite::Connection) -> Result<()> { fn index_files(files: Vec<String>, connection: &sqlite::Connection) -> Result<()> {
let hashed: Vec<File> = files let hashed: Vec<File> = files
.into_par_iter() .into_par_iter()
.progress_with_style(ProgressStyle::with_template("{spinner:.green} [indexing files] [{wide_bar:.cyan/blue}] {pos}/{len} files").unwrap())
.filter_map(|file| { .filter_map(|file| {
let hash = hash_file(&file).ok()?; let hash = hash_file(&file).ok()?;
Some(database::File { path: file, hash }) Some(database::File { path: file, hash })