55 Commits
0.0.2 ... 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 Kodichath
2c75a017ce Merge pull request #14 from sreedevk/improved-ui
* Improved Duplicates Table Printing
* Added Prettytable-rs crate
* Versioning for 0.0.6
2023-01-09 22:48:59 -05:00
sreedev
b5bb58b3ed version changes 2023-01-09 22:47:54 -05:00
sreedev
6e412fc59c table display improvements 2023-01-09 22:40:37 -05:00
sreedev
a0083cb571 table printing added 2023-01-09 22:15:44 -05:00
sreedev
0f33b4d6b5 added interactive param 2023-01-09 11:13:29 -05:00
sreedev
96fe667d3f version 0.0.5 2023-01-09 11:08:20 -05:00
sreedev
c32e64450f bug fix: connection result issue 2023-01-09 11:04:29 -05:00
Sreedev Kodichath
85acbded46 Merge pull request #13 from beeb/panics
Fix some more unhandled errors
2023-01-09 11:03:12 -05:00
Sreedev Kodichath
f12cecc9ee Merge branch 'main' into panics 2023-01-09 11:02:56 -05:00
sreedev
05f513735e tempdir fixes 2023-01-09 11:01:52 -05:00
Valentin Bersier
552f6c73f2 Merge branch 'main' into panics 2023-01-09 16:59:42 +01:00
Valentin Bersier
138b66038f refactor: various clippy fixes 2023-01-09 16:57:43 +01:00
Sreedev Kodichath
d8e1de169d Merge pull request #12 from dhruvasagar/fix/temp_file_path
Fix Temporary File Path #6
2023-01-09 10:48:28 -05:00
Valentin Bersier
bc170f139e style: remove trailing spaces 2023-01-09 16:46:12 +01:00
Valentin Bersier
c76ad81a55 refactor: do no unwrap in database.rs 2023-01-09 16:41:55 +01:00
Valentin Bersier
254e61cabe fix: clippy warnings 2023-01-09 16:27:52 +01:00
Valentin Bersier
a76163f24f style: format 2023-01-09 16:21:13 +01:00
Valentin Bersier
27fef21be0 fix: no unwrap in params.rs 2023-01-09 16:17:50 +01:00
Dhruva Sagar
8b00faf075 Fix Temporary File Path #6
Better cross platform support
2023-01-09 10:53:20 +05:30
sreedev
e158a8267a added authors & updated version 2023-01-08 17:16:46 -05:00
Sreedev Kodichath
e6f93ce3d6 Merge pull request #9 from beeb/scanner
Refactor scanner
2023-01-08 17:11:36 -05:00
Sreedev Kodichath
011da05c59 Merge pull request #10 from beeb/output
Refactor output.rs
2023-01-08 17:11:04 -05:00
beeb
6087abe960 refactor: no need for into_iter 2023-01-08 13:24:08 +01:00
beeb
5eba7cdc30 fix: still print items even if file size or modified time cannot be retrieved 2023-01-08 13:21:47 +01:00
beeb
5a65550e58 refactor: output.rs 2023-01-08 13:20:15 +01:00
beeb
0b5effd06e refactor: no need to consume pathbuf iterator 2023-01-08 13:09:43 +01:00
beeb
cc243f413a refactor: no need to consume iterator items 2023-01-08 13:08:27 +01:00
beeb
ad467d70a8 style: combine use statements for std 2023-01-08 12:20:27 +01:00
beeb
4490ea4c69 style: combine use statements for database 2023-01-08 12:19:56 +01:00
beeb
8ed0b13ff0 style: order imports 2023-01-08 12:19:25 +01:00
beeb
be7e8d38ae refactor: avoid unwraps 2023-01-08 11:48:49 +01:00
beeb
c9694dea09 fix: clippy warnings 2023-01-08 11:22:43 +01:00
sreedev
3a7ac7a332 version 0.0.3 2023-01-07 07:04:03 -05:00
Sreedev Kodichath
51521f1641 Merge pull request #5 from sreedevk/v003
Version 0.0.3
2023-01-07 07:02:40 -05:00
sreedev
b23db06847 fix: cyrillic characters output fix 2023-01-07 07:01:29 -05:00
16 changed files with 564 additions and 149 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.

188
Cargo.lock generated
View File

@@ -40,6 +40,18 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 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]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.11.1" version = "3.11.1"
@@ -149,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"
@@ -223,6 +248,28 @@ dependencies = [
"winapi", "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]] [[package]]
name = "cxx" name = "cxx"
version = "1.0.85" version = "1.0.85"
@@ -269,7 +316,7 @@ dependencies = [
[[package]] [[package]]
name = "deduplicator" name = "deduplicator"
version = "0.0.1" version = "0.0.9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -279,12 +326,36 @@ dependencies = [
"fxhash", "fxhash",
"glob", "glob",
"humansize", "humansize",
"indicatif",
"itertools", "itertools",
"prettytable-rs",
"rayon", "rayon",
"sqlite", "sqlite",
"thiserror", "thiserror",
"tokio", "tokio",
"tui", "tui",
"unicode-segmentation",
]
[[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]] [[package]]
@@ -293,6 +364,18 @@ 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]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.2.8" version = "0.2.8"
@@ -323,6 +406,17 @@ dependencies = [
"byteorder", "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]] [[package]]
name = "glob" name = "glob"
version = "0.3.0" version = "0.3.0"
@@ -386,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"
@@ -417,6 +525,12 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.60" version = "0.3.60"
@@ -534,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"
@@ -581,6 +701,26 @@ 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]]
name = "prettytable-rs"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a"
dependencies = [
"csv",
"encode_unicode 1.0.0",
"is-terminal",
"lazy_static",
"term",
"unicode-width",
]
[[package]] [[package]]
name = "proc-macro-error" name = "proc-macro-error"
version = "1.0.4" version = "1.0.4"
@@ -654,6 +794,23 @@ dependencies = [
"bitflags", "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]] [[package]]
name = "rustix" name = "rustix"
version = "0.36.5" version = "0.36.5"
@@ -668,6 +825,18 @@ dependencies = [
"windows-sys", "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]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@@ -680,6 +849,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2"
[[package]]
name = "serde"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.14" version = "0.3.14"
@@ -773,6 +948,17 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.1.3" version = "1.1.3"

View File

@@ -1,9 +1,10 @@
[package] [package]
name = "deduplicator" name = "deduplicator"
version = "0.0.2" version = "0.0.9"
edition = "2021" edition = "2021"
description = "find,filter,delete Duplicates" description = "find,filter,delete Duplicates"
license = "MIT" license = "MIT"
authors = ["Sreedev Kodichath <sreedevpadmakumar@gmail.com>", "Valentin Bersier <vbersier@gmail.com>", "Dhruva Sagar <dhruva.sagar@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -16,9 +17,12 @@ 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"
rayon = "1.6.1" rayon = "1.6.1"
sqlite = "0.30.3" 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" />

View File

@@ -1,7 +1,8 @@
use std::time::Duration; use std::time::Duration;
use crossterm::event::{self, KeyCode, KeyEvent};
use anyhow::Result; use anyhow::Result;
use crossterm::event::{self, KeyCode, KeyEvent};
use super::events; use super::events;
pub struct EventHandler; pub struct EventHandler;
@@ -21,8 +22,7 @@ impl EventHandler {
fn handle_keypress(keyevent: KeyEvent) -> Result<events::Event> { fn handle_keypress(keyevent: KeyEvent) -> Result<events::Event> {
match keyevent.code { match keyevent.code {
KeyCode::Char('q') => Ok(events::Event::Exit), KeyCode::Char('q') => Ok(events::Event::Exit),
_ => Ok(events::Event::Noop) _ => Ok(events::Event::Noop),
} }
} }
} }

View File

@@ -1,4 +1,4 @@
pub enum Event { pub enum Event {
Exit, Exit,
Noop Noop,
} }

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,18 +1,16 @@
#![allow(unused)]
mod event_handler; mod event_handler;
mod events; mod events;
mod ui;
mod formatter; mod formatter;
mod ui;
pub mod file_manager;
use std::{io, thread, time::Duration};
use crate::database;
use crate::output;
use crate::params::Params;
use crate::scanner;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use crossterm::{event, execute, terminal}; use crossterm::{event, execute, terminal};
use event_handler::EventHandler; use event_handler::EventHandler;
use std::io;
use std::thread;
use std::time::Duration;
use tui::{ use tui::{
backend::CrosstermBackend, backend::CrosstermBackend,
widgets::{Block, Borders, Widget}, widgets::{Block, Borders, Widget},
@@ -20,19 +18,28 @@ use tui::{
}; };
use ui::Ui; use ui::Ui;
use crate::database;
use crate::output;
use crate::params::Params;
use crate::scanner;
pub struct App; pub struct App;
impl App { impl App {
pub fn init(app_args: &Params) -> Result<()> { pub fn init(app_args: &Params) -> Result<()> {
// let mut term = Self::init_terminal()?; // let mut term = Self::init_terminal()?;
let connection = database::get_connection(&app_args)?; let connection = database::get_connection(app_args)?;
let duplicates = scanner::duplicates(&app_args, &connection)?; let duplicates = scanner::duplicates(app_args, &connection)?;
// 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(())
} }
@@ -56,6 +63,8 @@ impl App {
} }
fn init_render_loop(term: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> { fn init_render_loop(term: &mut Terminal<CrosstermBackend<io::Stdout>>) -> 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 { loop {
match Self::render_cycle(term) { match Self::render_cycle(term) {
Ok(_) => continue, Ok(_) => continue,

View File

@@ -1,5 +1,6 @@
use anyhow::Result;
use std::io; use std::io;
use anyhow::Result;
use tui::{ use tui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},

View File

@@ -1,4 +1,7 @@
use std::env::temp_dir;
use anyhow::Result; use anyhow::Result;
use crate::params::Params; use crate::params::Params;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -7,13 +10,18 @@ pub struct File {
pub hash: String, pub hash: String,
} }
pub fn get_connection(args: &Params) -> Result<sqlite::Connection, sqlite::Error> { fn db_connection_url(args: &Params) -> String {
let connection_url = match args.nocache { match args.nocache {
false => "/tmp/deduplicator.db", true => String::from(":memory:"),
true => ":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::Connection, sqlite::Error> {
sqlite::open(db_connection_url(args)).and_then(|conn| {
setup(&conn).ok(); setup(&conn).ok();
Ok(conn) Ok(conn)
}) })
@@ -30,31 +38,28 @@ pub fn put(file: &File, connection: &sqlite::Connection) -> Result<()> {
"INSERT INTO files (file_identifier, hash) VALUES (\"{}\", \"{}\")", "INSERT INTO files (file_identifier, hash) VALUES (\"{}\", \"{}\")",
file.path, file.hash file.path, file.hash
); );
let result = connection.execute(query)?; connection.execute(query)?;
Ok(())
Ok(result)
} }
pub fn indexed_paths(connection: &sqlite::Connection) -> Result<Vec<File>> { pub fn indexed_paths(connection: &sqlite::Connection) -> Result<Vec<File>> {
let query = format!( let query = "SELECT * FROM files";
"SELECT * FROM files"
);
let result: Vec<File> = connection let result: Vec<File> = connection
.prepare(query)? .prepare(query)?
.into_iter() .into_iter()
.map(|row_result| row_result.unwrap()) .filter_map(|row_result| row_result.ok())
.map(|row| { .map(|row| {
let path = row.read::<&str, _>("file_identifier").to_string(); let path = row.read::<&str, _>("file_identifier").to_string();
let hash = row.read::<i64, _>("hash").to_string(); let hash = row.read::<i64, _>("hash").to_string();
File { path, hash } File { path, hash }
}) })
.collect(); .collect();
Ok(result) Ok(result)
} }
pub fn duplicate_hashes(connection: &sqlite::Connection, path: &String) -> Result<Vec<File>> { pub fn duplicate_hashes(connection: &sqlite::Connection, path: &str) -> Result<Vec<File>> {
let query = format!( let query = format!(
" "
SELECT a.* FROM files a SELECT a.* FROM files a
@@ -65,13 +70,14 @@ pub fn duplicate_hashes(connection: &sqlite::Connection, path: &String) -> Resul
ON a.hash = b.hash ON a.hash = b.hash
WHERE a.file_identifier LIKE \"{}%\" WHERE a.file_identifier LIKE \"{}%\"
ORDER BY a.file_identifier ORDER BY a.file_identifier
", path ",
path
); );
let result: Vec<File> = connection let result: Vec<File> = connection
.prepare(query)? .prepare(query)?
.into_iter() .into_iter()
.map(|row_result| row_result.unwrap()) .filter_map(|row_result| row_result.ok())
.map(|row| { .map(|row| {
let path = row.read::<&str, _>("file_identifier").to_string(); let path = row.read::<&str, _>("file_identifier").to_string();
let hash = row.read::<i64, _>("hash").to_string(); let hash = row.read::<i64, _>("hash").to_string();

View File

@@ -1,12 +1,13 @@
mod params; #![allow(unused)] // TODO: remove this once TUI is implemented
mod app;
mod database; mod database;
mod output; mod output;
mod params;
mod scanner; mod scanner;
mod app;
use anyhow::Result; use anyhow::Result;
use clap::Parser;
use app::App; use app::App;
use clap::Parser;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {

View File

@@ -1,66 +1,170 @@
use crate::database::File; use std::{collections::HashMap, fs, io};
use std::io::Write;
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 std::{collections::HashMap, fs}; use itertools::Itertools;
use crate::params::Params;
fn format_path(path: &String, opts: &Params) -> String { use crate::app::file_manager;
let display_path = path.replace(&opts.get_directory().unwrap(), ""); use crate::database::File;
let display_range = if display_path.len() > 32 { use crate::params::Params;
&display_path[(display_path.len() - 32)..] 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 display_range = if display_path.chars().count() > 32 {
display_path
.graphemes(true)
.collect::<Vec<&str>>()
.into_iter()
.rev()
.take(32)
.rev()
.collect()
} else { } else {
&display_path[..] display_path
}; };
format!("...{}", display_range) Ok(format!("...{:<32}", display_range))
} }
fn file_size(path: &String) -> String { fn file_size(path: &String) -> Result<String> {
let mdata = fs::metadata(path).unwrap(); let mdata = fs::metadata(path)?;
let formatted_size = format_size(mdata.len(), DECIMAL); let formatted_size = format!("{:>12}", format_size(mdata.len(), DECIMAL));
format!("{}", formatted_size) Ok(formatted_size)
} }
fn modified_time(path: &String) -> String { fn modified_time(path: &String) -> Result<String> {
let mdata = fs::metadata(path).unwrap(); let mdata = fs::metadata(path)?;
let modified_time: DateTime<Utc> = mdata.modified().unwrap().into(); let modified_time: DateTime<Utc> = mdata.modified()?.into();
modified_time.format("%Y-%m-%d %H:%M:%S").to_string() Ok(modified_time.format("%Y-%m-%d %H:%M:%S").to_string())
} }
fn print_divider() { fn group_duplicates(duplicates: Vec<File>) -> HashMap<String, Vec<File>> {
println!("-------------------+-------------------------------------+------------------+----------------------------------+"); let mut duplicate_mapper: HashMap<String, Vec<File>> = HashMap::new();
duplicates.into_iter().for_each(|file| {
duplicate_mapper
.entry(file.hash.clone())
.and_modify(|value| value.push(file.clone()))
.or_insert_with(|| 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) { pub fn print(duplicates: Vec<File>, opts: &Params) {
print_divider(); print_meta_info(&duplicates, opts);
println!(
"| {0: <16} | {1: <35} | {2: <16} | {3: <32} |",
"hash", "filename", "size", "updated_at"
);
print_divider();
let mut dup_index: HashMap<String, Vec<File>> = HashMap::new(); let mut output_table = Table::new();
let grouped_duplicates: HashMap<String, Vec<File>> = group_duplicates(duplicates);
duplicates.into_iter().for_each(|file| { output_table.set_titles(row!["hash", "duplicates"]);
dup_index grouped_duplicates.iter().for_each(|(hash, group)| {
.entry(file.hash.clone()) let mut inner_table = Table::new();
.and_modify(|value| value.push(file.clone())) inner_table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
.or_insert(vec![file]); group.iter().for_each(|file| {
}); inner_table.add_row(row![
format_path(&file.path, opts).unwrap_or_default().blue(),
dup_index.into_iter().for_each(|(_, group)| { file_size(&file.path).unwrap_or_default().red(),
group.into_iter().for_each(|file| { modified_time(&file.path).unwrap_or_default().yellow()
println!( ]);
"| {0: <16} | {1: <35} | {2: <16} | {3: <32} |",
file.hash.red(),
format_path(&file.path, opts).yellow(),
file_size(&file.path).blue(),
modified_time(&file.path).blue()
);
}); });
print_divider(); output_table.add_row(row![hash.green(), inner_table]);
}); });
output_table.printstd();
} }

View File

@@ -1,7 +1,7 @@
use std::path::PathBuf; use std::{fs, path::PathBuf};
use anyhow::{anyhow, Result};
use clap::Parser; use clap::Parser;
use anyhow::Result;
use std::fs;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
@@ -12,27 +12,27 @@ 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 {
pub fn get_directory(&self) -> Result<String> { pub fn get_directory(&self) -> Result<String> {
let dir_string: String = self let dir_pathbuf: PathBuf = self
.dir .dir
.clone() .clone()
.unwrap_or(std::env::current_dir()?) .unwrap_or(std::env::current_dir()?)
.as_os_str() .as_os_str()
.to_str() .into();
.unwrap()
.to_string();
let dir_pathbuf = PathBuf::from(&dir_string); let dir = fs::canonicalize(dir_pathbuf)?
let dir = fs::canonicalize(&dir_pathbuf)?
.as_os_str() .as_os_str()
.to_str() .to_str()
.unwrap() .ok_or_else(|| anyhow!("Invalid directory"))?
.to_string(); .to_string();
Ok(dir) Ok(dir)

View File

@@ -1,55 +1,62 @@
use crate::database; use std::{fs, path::PathBuf};
use crate::{params::Params, database::File}; use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle, ParallelProgressIterator};
use anyhow::Result; use anyhow::Result;
use fxhash::hash32 as hasher;
use glob::glob; use glob::glob;
use itertools::Itertools; use itertools::Itertools;
use rayon::prelude::*; use rayon::prelude::*;
use std::fs;
use std::path::PathBuf; use crate::{
use fxhash::hash32 as hasher; database::{self, File},
params::Params,
};
pub fn duplicates(app_opts: &Params, connection: &sqlite::Connection) -> Result<Vec<File>> { pub fn duplicates(app_opts: &Params, connection: &sqlite::Connection) -> Result<Vec<File>> {
let scan_results = scan(app_opts, connection)?; let scan_results = scan(app_opts, connection)?;
let base_path = app_opts.get_directory()?; let base_path = app_opts.get_directory()?;
index_files(scan_results, connection); index_files(scan_results, connection)?;
database::duplicate_hashes(connection, &base_path) database::duplicate_hashes(connection, &base_path)
} }
fn get_glob_patterns(opts: &Params, directory: &String) -> Vec<PathBuf> { fn get_glob_patterns(opts: &Params, directory: &str) -> Vec<PathBuf> {
opts.types opts.types
.clone() .clone()
.unwrap_or(String::from("*")) .unwrap_or_else(|| String::from("*"))
.split(",") .split(',')
.map(|filetype| format!("*.{}", filetype)) .map(|filetype| format!("*.{}", filetype))
.map(|filetype| { .map(|filetype| {
vec![directory.clone(), String::from("**"), filetype] vec![directory.to_owned(), String::from("**"), filetype]
.iter() .iter()
.collect() .collect()
}) })
.collect() .collect()
} }
fn is_indexed_file(path: &String, indexed: &Vec<File>) -> bool { fn is_indexed_file(path: impl Into<String>, indexed: &[File]) -> bool {
indexed indexed
.into_iter() .iter()
.map(|file| file.path.clone()) .map(|file| file.path.clone())
.contains(path) .contains(&path.into())
} }
fn scan(app_opts: &Params, connection: &sqlite::Connection) -> Result<Vec<String>> { fn scan(app_opts: &Params, connection: &sqlite::Connection) -> Result<Vec<String>> {
let directory = app_opts.get_directory()?; let directory = app_opts.get_directory()?;
let glob_patterns: Vec<PathBuf> = get_glob_patterns(&app_opts, &directory); let glob_patterns: Vec<PathBuf> = get_glob_patterns(app_opts, &directory);
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
.into_par_iter() .par_iter()
.map(|glob_pattern| glob(&glob_pattern.as_os_str().to_str().unwrap())) .progress_with_style(ProgressStyle::with_template("{spinner:.green} [scanning files] [{wide_bar:.cyan/blue}] {pos}/{len} files").unwrap())
.map(|glob_result| glob_result.unwrap()) .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
.map(|x| x.unwrap().as_os_str().to_str().unwrap().to_string()) .filter_map(|x| Some(x.ok()?.as_os_str().to_str()?.to_string()))
.filter(|fpath| !is_indexed_file(fpath, &indexed_paths)) .filter(|fpath| !is_indexed_file(fpath, &indexed_paths))
.filter(|glob_result| fs::metadata(glob_result).unwrap().is_file()) .filter(|glob_result| {
fs::metadata(glob_result)
.map(|f| f.is_file())
.unwrap_or(false)
})
.collect::<Vec<String>>() .collect::<Vec<String>>()
}) })
.collect(); .collect();
@@ -57,18 +64,19 @@ fn scan(app_opts: &Params, connection: &sqlite::Connection) -> Result<Vec<String
Ok(files) Ok(files)
} }
fn index_files(files: Vec<String>, connection: &sqlite::Connection) { 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()
.map(|file| { .progress_with_style(ProgressStyle::with_template("{spinner:.green} [indexing files] [{wide_bar:.cyan/blue}] {pos}/{len} files").unwrap())
let hash = hash_file(&file).unwrap(); .filter_map(|file| {
database::File { path: file, hash } let hash = hash_file(&file).ok()?;
Some(database::File { path: file, hash })
}) })
.collect(); .collect();
hashed.into_iter().for_each(|file| { hashed
database::put(&file, connection).unwrap(); .iter()
}); .try_for_each(|file| database::put(file, connection))
} }
pub fn hash_file(filepath: &str) -> Result<String> { pub fn hash_file(filepath: &str) -> Result<String> {