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

View File

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

View File

@@ -16,7 +16,8 @@ Usage: deduplicator [OPTIONS]
Options:
-t, --types <TYPES> Filetypes to deduplicate (default = all)
--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
-V, --version Print version information
```
@@ -37,9 +38,13 @@ cargo install deduplicator
<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>
<p align="center">
While testing, Deduplicator was able to go through 8.6GB of pdf files and detect duplicates in 2.9 seconds
</p>
<p align="center">
While testing, Deduplicator was able to go through 8.6GB of pdf files and detect duplicates in 2.9 seconds
</p>
<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 events;
mod formatter;
mod ui;
pub mod file_manager;
use std::{io, thread, time::Duration};
@@ -32,7 +35,11 @@ impl App {
// Self::init_render_loop(&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(())
}

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 chrono::offset::Utc;
use chrono::DateTime;
use colored::Colorize;
use humansize::{format_size, DECIMAL};
use itertools::Itertools;
use crate::app::file_manager;
use crate::database::File;
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> {
let display_path = path.replace(&opts.get_directory()?, "");
let text_vec = display_path.chars().collect::<Vec<_>>();
let display_range = if text_vec.len() > 32 {
text_vec[(display_path.len() - 32)..]
.iter()
.collect::<String>()
let display_range = if display_path.chars().count() > 32 {
display_path
.graphemes(true)
.collect::<Vec<&str>>()
.into_iter()
.rev()
.take(32)
.rev()
.collect()
} else {
display_path
};
@@ -50,16 +58,104 @@ fn group_duplicates(duplicates: Vec<File>) -> HashMap<String, Vec<File>> {
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) {
print_meta_info(&duplicates, opts);
let mut output_table = Table::new();
let grouped_duplicates: HashMap<String, Vec<File>> = 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(),

View File

@@ -12,9 +12,12 @@ pub struct Params {
/// Run Deduplicator on dir different from pwd
#[arg(long)]
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)]
pub nocache: bool,
/// Delete files interactively
#[arg(long, short)]
pub interactive: bool
}
impl Params {

View File

@@ -1,5 +1,5 @@
use std::{fs, path::PathBuf};
use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle, ParallelProgressIterator};
use anyhow::Result;
use fxhash::hash32 as hasher;
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 files: Vec<String> = glob_patterns
.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())
.flat_map(|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<()> {
let hashed: Vec<File> = files
.into_par_iter()
.progress_with_style(ProgressStyle::with_template("{spinner:.green} [indexing files] [{wide_bar:.cyan/blue}] {pos}/{len} files").unwrap())
.filter_map(|file| {
let hash = hash_file(&file).ok()?;
Some(database::File { path: file, hash })