mirror of
https://github.com/sreedevk/deduplicator.git
synced 2026-08-26 18:15:33 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
163e8d85c0 | ||
|
|
b1c508e9e7 | ||
|
|
4125b18d67 | ||
|
|
10329015f7 | ||
|
|
de6d2e325e | ||
|
|
850c274adc | ||
|
|
31d35aae51 | ||
|
|
087f36ac52 | ||
|
|
6f8b1d55df | ||
|
|
c6318a310b | ||
|
|
76c69ef8a0 | ||
|
|
fd4e882aa8 | ||
|
|
fd1d214d62 | ||
|
|
1a573fbf3f | ||
|
|
b12b662ba1 | ||
|
|
96337b6b48 | ||
|
|
02f70aa55f | ||
|
|
4a18b3fd91 | ||
|
|
876fdc69ac | ||
|
|
8acc3f8c8f | ||
|
|
38f6c48452 | ||
|
|
3e1e9c0e2e | ||
|
|
0674ab55bf | ||
|
|
ccff95dfc5 | ||
|
|
53d28abb62 | ||
|
|
ea6cf94952 | ||
|
|
7b275d40d0 | ||
|
|
30398c3ca9 | ||
|
|
c0042fc9f7 | ||
|
|
5aea0eb6f4 | ||
|
|
f0ff1ec325 | ||
|
|
b267fcdedf | ||
|
|
84b194efca | ||
|
|
d162ca98ef | ||
|
|
dfb73ceb5c | ||
|
|
72ca7c7a44 | ||
|
|
2f3cfd3162 | ||
|
|
9b1f591c20 | ||
|
|
e24603c645 | ||
|
|
0e624b042d | ||
|
|
e61d1f1475 | ||
|
|
8c76c6a3c8 | ||
|
|
df3bea3d4f | ||
|
|
7ace0bde63 | ||
|
|
70de402eab |
31
.github/workflows/release.yml
vendored
Normal file
31
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Release
|
||||
|
||||
env:
|
||||
PROJECT_NAME: deduplicator
|
||||
PROJECT_DESC: "Filter, Sort & Delete Duplicate Files Recursively"
|
||||
PROJECT_AUTH: "sreedevk"
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- created
|
||||
|
||||
jobs:
|
||||
upload-assets:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: taiki-e/upload-rust-binary-action@v1
|
||||
with:
|
||||
bin: deduplicator
|
||||
tar: unix
|
||||
zip: windows
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
20
.github/workflows/rust.yml
vendored
20
.github/workflows/rust.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: Rust
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/target
|
||||
/test_data
|
||||
.envrc
|
||||
|
||||
145
Cargo.lock
generated
145
Cargo.lock
generated
@@ -2,6 +2,15 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
@@ -52,6 +61,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b45ea9b00a7b3f2988e9a65ad3917e62123c38dba709b666506207be96d1790b"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.11.1"
|
||||
@@ -70,6 +89,12 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
|
||||
|
||||
[[package]]
|
||||
name = "bytesize"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.78"
|
||||
@@ -223,7 +248,7 @@ version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"bstr 0.2.17",
|
||||
"csv-core",
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -299,16 +324,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deduplicator"
|
||||
version = "0.1.1"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytesize",
|
||||
"chrono",
|
||||
"clap",
|
||||
"colored",
|
||||
"dashmap",
|
||||
"fxhash",
|
||||
"glob",
|
||||
"humansize",
|
||||
"globwalk",
|
||||
"indicatif",
|
||||
"itertools",
|
||||
"memmap2",
|
||||
@@ -379,6 +404,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
@@ -400,10 +431,28 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.0"
|
||||
name = "globset"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
|
||||
checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr 1.1.0",
|
||||
"fnv",
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globwalk"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
@@ -435,15 +484,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e682e2bd70ecbcce5209f11a992a4ba001fea8e60acf7860ce007629e6d2756"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.53"
|
||||
@@ -468,6 +508,23 @@ dependencies = [
|
||||
"cxx-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492"
|
||||
dependencies = [
|
||||
"globset",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex",
|
||||
"same-file",
|
||||
"thread_local",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.17.2"
|
||||
@@ -540,12 +597,6 @@ version = "0.2.139"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
|
||||
|
||||
[[package]]
|
||||
name = "link-cplusplus"
|
||||
version = "1.0.8"
|
||||
@@ -796,12 +847,29 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.36.5"
|
||||
@@ -828,6 +896,15 @@ version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
@@ -928,6 +1005,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.45"
|
||||
@@ -941,9 +1027,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.23.0"
|
||||
version = "1.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
|
||||
checksum = "38a54aca0c15d014013256222ba0ebed095673f89345dd79119d912eb561b7a8"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
@@ -994,6 +1080,17 @@ version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
|
||||
14
Cargo.toml
14
Cargo.toml
@@ -1,27 +1,31 @@
|
||||
[package]
|
||||
name = "deduplicator"
|
||||
version = "0.1.1"
|
||||
version = "0.1.4"
|
||||
edition = "2021"
|
||||
description = "find,filter,delete Duplicates"
|
||||
license = "MIT"
|
||||
authors = ["Sreedev Kodichath <sreedevpadmakumar@gmail.com>", "Valentin Bersier <vbersier@gmail.com>", "Dhruva Sagar <dhruva.sagar@gmail.com>"]
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.68"
|
||||
bytesize = "1.1.0"
|
||||
chrono = "0.4.23"
|
||||
clap = { version = "4.0.32", features = ["derive"] }
|
||||
colored = "2.0.0"
|
||||
dashmap = { version = "5.4.0", features = ["rayon"] }
|
||||
fxhash = "0.2.1"
|
||||
glob = "0.3.0"
|
||||
humansize = "2.1.2"
|
||||
globwalk = "0.8.1"
|
||||
indicatif = { version = "0.17.2", features = ["rayon", "tokio"] }
|
||||
itertools = "0.10.5"
|
||||
memmap2 = "0.5.8"
|
||||
prettytable-rs = "0.10.0"
|
||||
rayon = "1.6.1"
|
||||
thiserror = "1.0.38"
|
||||
tokio = { version = "1.23.0", features = ["full"] }
|
||||
tokio = { version = "1.23.1", features = ["full"] }
|
||||
unicode-segmentation = "1.10.0"
|
||||
|
||||
111
README.md
111
README.md
@@ -4,46 +4,97 @@
|
||||
Find, Sort, Filter & Delete duplicate files
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
NOTE: This project is still being developed. At the moment, as shown in the screenshot below, deduplicator is able to scan through and list duplicates with and without caching. Contributions are welcome.
|
||||
</p>
|
||||
|
||||
<h2 align="center">Usage</h2>
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
Usage: deduplicator [OPTIONS]
|
||||
|
||||
Options:
|
||||
-t, --types <TYPES> Filetypes to deduplicate (default = all)
|
||||
--dir <DIR> Run Deduplicator on dir different from pwd
|
||||
-i, --interactive Delete files interactively
|
||||
-h, --help Print help information
|
||||
-V, --version Print version information
|
||||
-t, --types <TYPES> Filetypes to deduplicate (default = all)
|
||||
--dir <DIR> Run Deduplicator on dir different from pwd
|
||||
-i, --interactive Delete files interactively
|
||||
-m, --minsize <MINSIZE> Minimum filesize of duplicates to scan (e.g., 100B/1K/2M/3G/4T). [default = 0]
|
||||
-h, --help Print help information
|
||||
-V, --version Print version information
|
||||
```
|
||||
|
||||
<h2 align="center">Installation</h2>
|
||||
## Installation
|
||||
|
||||
<p align="center">Currently, deduplicator is only installable via rust's cargo package manager</p>
|
||||
### Cargo Install
|
||||
|
||||
#### Stable
|
||||
|
||||
```bash
|
||||
$ cargo install deduplicator
|
||||
```
|
||||
|
||||
#### Nightly
|
||||
|
||||
if you'd like to install with nightly features, you can use
|
||||
|
||||
```bash
|
||||
$ cargo install --git https://github.com/sreedevk/deduplicator
|
||||
```
|
||||
Please note that if you use a version manager to install rust (like asdf), you need to reshim (`asdf reshim rust`).
|
||||
|
||||
### Linux (Pre-built Binary)
|
||||
|
||||
you can download the pre-built binary from the [Releases](https://github.com/sreedevk/deduplicator/releases) page.
|
||||
download the `deduplicator-x86_64-unknown-linux-gnu.tar.gz` for linux. Once you have the tarball file with the executable,
|
||||
you can follow these steps to install:
|
||||
|
||||
```bash
|
||||
$ tar -zxvf deduplicator-x86_64-unknown-linux-gnu.tar.gz
|
||||
$ sudo mv deduplicator /usr/bin/
|
||||
```
|
||||
|
||||
### Mac OS (Pre-built Binary)
|
||||
|
||||
you can download the pre-build binary from the [Releases](https://github.com/sreedevk/deduplicator/releases) page.
|
||||
download the `deduplicator-x86_64-apple-darwin.tar.gz` tarball for mac os. Once you have the tarball file with the executable, you can follow these steps to install:
|
||||
|
||||
```bash
|
||||
$ tar -zxvf deduplicator-x86_64-unknown-linux-gnu.tar.gz
|
||||
$ sudo mv deduplicator /usr/bin/
|
||||
```
|
||||
|
||||
### Windows (Pre-built Binary)
|
||||
|
||||
you can download the pre-build binary from the [Releases](https://github.com/sreedevk/deduplicator/releases) page.
|
||||
download the `deduplicator-x86_64-pc-windows-msvc.zip` zip file for windows. unzip the `zip` file & move the `deduplicator.exe` to a location in the PATH system environment variable.
|
||||
|
||||
Note: If you Run into an msvc error, please install MSCV from [here](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170)
|
||||
|
||||
## Performance
|
||||
|
||||
Deduplicator uses size comparison and fxhash (a non non-cryptographic hashing algo) to quickly scan through large number of files to find duplicates. its also highly parallel (uses rayon and dashmap). I was able to scan through 120GB of files (Videos, PDFs, Images) in ~300ms. checkout the benchmarks
|
||||
|
||||
## benchmarks
|
||||
|
||||
| Command | Dirsize | Mean [ms] | Min [ms] | Max [ms] | Relative |
|
||||
|:---|:---|---:|---:|---:|---:|
|
||||
| `deduplicator --dir ~/Data/tmp` | (~120G) | 27.5 ± 1.0 | 26.0 | 32.1 | 1.70 ± 0.09 |
|
||||
| `deduplicator --dir ~/Data/books` | (~8.6G) | 21.8 ± 0.7 | 20.5 | 24.4 | 1.35 ± 0.07 |
|
||||
| `deduplicator --dir ~/Data/books --minsize 10M` | (~8.6G) | 16.1 ± 0.6 | 14.9 | 18.8 | 1.00 |
|
||||
| `deduplicator --dir ~/Data/ --types pdf,jpg,png,jpeg` | (~290G) | 1857.4 ± 24.5 | 1817.0 | 1895.5 | 115.07 ± 4.64 |
|
||||
|
||||
* The last entry is lower because of the number of files deduplicator had to go through (~660895 Files). The average size of the files rarely affect the performance of deduplicator.
|
||||
|
||||
These benchmarks were run using [hyperfine](https://github.com/sharkdp/hyperfine). Here are the specs of the machine used to benchmark deduplicator:
|
||||
|
||||
```
|
||||
cargo install deduplicator
|
||||
OS: Arch Linux x86_64
|
||||
Host: Precision 5540
|
||||
Kernel: 5.15.89-1-lts
|
||||
Uptime: 4 hours, 44 mins
|
||||
Shell: zsh 5.9
|
||||
Terminal: kitty
|
||||
CPU: Intel i9-9880H (16) @ 4.800GHz
|
||||
GPU: NVIDIA Quadro T2000 Mobile / Max-Q
|
||||
GPU: Intel CoffeeLake-H GT2 [UHD Graphics 630]
|
||||
Memory: 31731MiB (~32GiB)
|
||||
```
|
||||
<p align="center">
|
||||
note that if you use a version manager to install rust (like asdf), you need to reshim (`asdf reshim rust`).
|
||||
</p>
|
||||
|
||||
<h2 align="center">Performance</h2>
|
||||
|
||||
<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>
|
||||
|
||||
<h2 align="center">Screenshots</h2>
|
||||
|
||||
|
||||
<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" />
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||
12
src/filters.rs
Normal file
12
src/filters.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use crate::file_manager::File;
|
||||
use crate::params::Params;
|
||||
|
||||
pub fn is_file_gt_minsize(app_opts: &Params, file: &File) -> bool {
|
||||
match app_opts.get_minsize() {
|
||||
Some(msize) => match file.size {
|
||||
Some(fsize) => fsize >= msize,
|
||||
None => true,
|
||||
},
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ mod file_manager;
|
||||
mod output;
|
||||
mod params;
|
||||
mod scanner;
|
||||
mod filters;
|
||||
|
||||
use anyhow::Result;
|
||||
use app::App;
|
||||
|
||||
@@ -5,7 +5,7 @@ use chrono::offset::Utc;
|
||||
use chrono::DateTime;
|
||||
use colored::Colorize;
|
||||
use dashmap::DashMap;
|
||||
use humansize::{format_size, DECIMAL};
|
||||
use indicatif::{ProgressBar, ProgressIterator, ProgressStyle};
|
||||
use itertools::Itertools;
|
||||
use prettytable::{format, row, Table};
|
||||
use std::io::Write;
|
||||
@@ -13,7 +13,7 @@ use std::{fs, io};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
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()?.to_string_lossy().as_ref(), "");
|
||||
let display_range = if display_path.chars().count() > 32 {
|
||||
display_path
|
||||
.graphemes(true)
|
||||
@@ -27,13 +27,11 @@ fn format_path(path: &str, opts: &Params) -> Result<String> {
|
||||
display_path
|
||||
};
|
||||
|
||||
Ok(format!("...{:<32}", display_range))
|
||||
Ok(format!("...{display_range:<32}"))
|
||||
}
|
||||
|
||||
fn file_size(path: &String) -> Result<String> {
|
||||
let mdata = fs::metadata(path)?;
|
||||
let formatted_size = format!("{:>12}", format_size(mdata.len(), DECIMAL));
|
||||
Ok(formatted_size)
|
||||
fn file_size(file: &File) -> Result<String> {
|
||||
Ok(format!("{:>12}", bytesize::ByteSize::b(file.size.unwrap())))
|
||||
}
|
||||
|
||||
fn modified_time(path: &String) -> Result<String> {
|
||||
@@ -43,10 +41,6 @@ fn modified_time(path: &String) -> Result<String> {
|
||||
Ok(modified_time.format("%Y-%m-%d %H:%M:%S").to_string())
|
||||
}
|
||||
|
||||
fn print_meta_info() {
|
||||
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.");
|
||||
@@ -118,10 +112,20 @@ fn process_group_action(duplicates: &Vec<File>, dup_index: usize, dup_size: usiz
|
||||
}
|
||||
|
||||
pub fn interactive(duplicates: DashMap<String, Vec<File>>, opts: &Params) {
|
||||
print_meta_info();
|
||||
if duplicates.is_empty() {
|
||||
println!(
|
||||
"\n{}",
|
||||
"No duplicates found matching your search criteria.".green()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
duplicates
|
||||
.clone()
|
||||
.into_iter()
|
||||
.sorted_unstable_by_key(|(_, f)| {
|
||||
-(f.first().and_then(|ff| ff.size).unwrap_or_default() as i64)
|
||||
}) // sort by descending file size in interactive mode
|
||||
.enumerate()
|
||||
.for_each(|(gindex, (_, group))| {
|
||||
let mut itable = Table::new();
|
||||
@@ -131,7 +135,7 @@ pub fn interactive(duplicates: DashMap<String, Vec<File>>, opts: &Params) {
|
||||
itable.add_row(row![
|
||||
index,
|
||||
format_path(&file.path, opts).unwrap_or_default().blue(),
|
||||
file_size(&file.path).unwrap_or_default().red(),
|
||||
file_size(file).unwrap_or_default().red(),
|
||||
modified_time(&file.path).unwrap_or_default().yellow()
|
||||
]);
|
||||
});
|
||||
@@ -141,22 +145,39 @@ pub fn interactive(duplicates: DashMap<String, Vec<File>>, opts: &Params) {
|
||||
}
|
||||
|
||||
pub fn print(duplicates: DashMap<String, Vec<File>>, opts: &Params) {
|
||||
print_meta_info();
|
||||
if duplicates.is_empty() {
|
||||
println!(
|
||||
"\n{}",
|
||||
"No duplicates found matching your search criteria.".green()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut output_table = Table::new();
|
||||
let progress_bar = ProgressBar::new(duplicates.len() as u64);
|
||||
let progress_style = ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [generating output] [{wide_bar:.cyan/blue}] {pos}/{len} files")
|
||||
.unwrap();
|
||||
|
||||
progress_bar.set_style(progress_style);
|
||||
output_table.set_titles(row!["hash", "duplicates"]);
|
||||
duplicates.into_iter().for_each(|(hash, group)| {
|
||||
let mut inner_table = Table::new();
|
||||
inner_table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
||||
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()
|
||||
]);
|
||||
|
||||
duplicates
|
||||
.into_iter()
|
||||
.sorted_unstable_by_key(|(_, f)| f.first().and_then(|ff| ff.size).unwrap_or_default())
|
||||
.progress_with(progress_bar)
|
||||
.for_each(|(hash, group)| {
|
||||
let mut inner_table = Table::new();
|
||||
inner_table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
||||
group.iter().for_each(|file| {
|
||||
inner_table.add_row(row![
|
||||
format_path(&file.path, opts).unwrap_or_default().blue(),
|
||||
file_size(file).unwrap_or_default().red(),
|
||||
modified_time(&file.path).unwrap_or_default().yellow()
|
||||
]);
|
||||
});
|
||||
output_table.add_row(row![hash.green(), inner_table]);
|
||||
});
|
||||
output_table.add_row(row![hash.green(), inner_table]);
|
||||
});
|
||||
|
||||
output_table.printstd();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::Parser;
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::{Parser, ValueHint};
|
||||
use globwalk::{GlobWalker, GlobWalkerBuilder};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
pub struct Params {
|
||||
@@ -9,42 +11,42 @@ pub struct Params {
|
||||
#[arg(short, long)]
|
||||
pub types: Option<String>,
|
||||
/// Run Deduplicator on dir different from pwd
|
||||
#[arg(long)]
|
||||
#[arg(long, value_hint = ValueHint::DirPath)]
|
||||
pub dir: Option<PathBuf>,
|
||||
/// Delete files interactively
|
||||
#[arg(long, short)]
|
||||
pub interactive: bool,
|
||||
/// Minimum filesize of duplicates to scan (e.g., 100B/1K/2M/3G/4T). [default = 0]
|
||||
#[arg(long, short)]
|
||||
pub minsize: Option<String>,
|
||||
}
|
||||
|
||||
impl Params {
|
||||
pub fn get_directory(&self) -> Result<String> {
|
||||
let dir_pathbuf: PathBuf = self
|
||||
.dir
|
||||
.clone()
|
||||
.unwrap_or(std::env::current_dir()?)
|
||||
.as_os_str()
|
||||
.into();
|
||||
|
||||
let dir = fs::canonicalize(dir_pathbuf)?
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Invalid directory"))?
|
||||
.to_string();
|
||||
pub fn get_minsize(&self) -> Option<u64> {
|
||||
match &self.minsize {
|
||||
Some(msize) => match msize.parse::<bytesize::ByteSize>() {
|
||||
Ok(units) => Some(units.0),
|
||||
Err(_) => None,
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_directory(&self) -> Result<PathBuf> {
|
||||
let current_dir = std::env::current_dir()?;
|
||||
let dir_path = self.dir.as_ref().unwrap_or(¤t_dir).as_path();
|
||||
let dir = fs::canonicalize(dir_path)?;
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
pub fn get_glob_patterns(&self) -> Vec<PathBuf> {
|
||||
self.types
|
||||
.clone()
|
||||
.unwrap_or_else(|| String::from("*"))
|
||||
.split(',')
|
||||
.map(|filetype| format!("*.{}", filetype))
|
||||
.map(|filetype| {
|
||||
vec![self.get_directory().unwrap(), String::from("**"), filetype]
|
||||
.iter()
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
pub fn get_glob_walker(&self) -> Result<GlobWalker> {
|
||||
let pattern: String = match self.types.as_ref() {
|
||||
Some(filetypes) => format!("**/*{{{filetypes}}}"),
|
||||
None => "**/*".to_string(),
|
||||
};
|
||||
// TODO: add params for maximum depth and following symlinks, then pass them to this builder
|
||||
GlobWalkerBuilder::from_patterns(self.get_directory()?, &[pattern])
|
||||
.build()
|
||||
.map_err(|e| anyhow!(e))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use crate::{file_manager::File, filters, params::Params};
|
||||
use anyhow::Result;
|
||||
use dashmap::DashMap;
|
||||
use fxhash::hash64 as hasher;
|
||||
use glob::glob;
|
||||
use indicatif::{ParallelProgressIterator, ProgressStyle};
|
||||
use indicatif::{ParallelProgressIterator, ProgressBar, ProgressIterator, ProgressStyle};
|
||||
use memmap2::Mmap;
|
||||
use rayon::prelude::*;
|
||||
use std::hash::Hasher;
|
||||
use std::time::Duration;
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use crate::{file_manager::File, params::Params};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum IndexCritera {
|
||||
Size,
|
||||
@@ -28,12 +27,7 @@ pub fn duplicates(app_opts: &Params) -> Result<DashMap<String, Vec<File>>> {
|
||||
.collect::<Vec<File>>();
|
||||
|
||||
if sizewize_duplicate_files.len() > 1 {
|
||||
let size_wise_duplicate_paths = sizewize_duplicate_files
|
||||
.into_par_iter()
|
||||
.map(|file| file.path)
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let hash_index_store = index_files(size_wise_duplicate_paths, IndexCritera::Hash)?;
|
||||
let hash_index_store = index_files(sizewize_duplicate_files, IndexCritera::Hash)?;
|
||||
let duplicate_files = hash_index_store
|
||||
.into_par_iter()
|
||||
.filter(|(_, files)| files.len() > 1)
|
||||
@@ -45,72 +39,60 @@ pub fn duplicates(app_opts: &Params) -> Result<DashMap<String, Vec<File>>> {
|
||||
}
|
||||
}
|
||||
|
||||
fn scan(app_opts: &Params) -> Result<Vec<String>> {
|
||||
let glob_patterns: Vec<PathBuf> = app_opts.get_glob_patterns();
|
||||
let files: Vec<String> = glob_patterns
|
||||
.par_iter()
|
||||
fn scan(app_opts: &Params) -> Result<Vec<File>> {
|
||||
let walker = app_opts.get_glob_walker()?;
|
||||
let progress = ProgressBar::new_spinner();
|
||||
let progress_style =
|
||||
ProgressStyle::with_template("{spinner:.green} [mapping paths] {pos} paths")?;
|
||||
progress.set_style(progress_style);
|
||||
progress.enable_steady_tick(Duration::from_millis(100));
|
||||
|
||||
let files = walker
|
||||
.progress_with(progress)
|
||||
.filter_map(Result::ok)
|
||||
.map(|file| file.into_path())
|
||||
.filter(|fpath| fpath.is_file())
|
||||
.collect::<Vec<PathBuf>>()
|
||||
.into_par_iter()
|
||||
.progress_with_style(ProgressStyle::with_template(
|
||||
"{spinner:.green} [scanning files] [{wide_bar:.cyan/blue}] {pos}/{len} files",
|
||||
"{spinner:.green} [processing mapped paths] [{wide_bar:.cyan/blue}] {pos}/{len} files",
|
||||
)?)
|
||||
.filter_map(|glob_pattern| glob(glob_pattern.as_os_str().to_str()?).ok())
|
||||
.flat_map(|file_vec| {
|
||||
file_vec
|
||||
.filter_map(|x| Some(x.ok()?.as_os_str().to_str()?.to_string()))
|
||||
.filter(|glob_result| {
|
||||
fs::metadata(glob_result)
|
||||
.map(|f| f.is_file())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.map(|fpath| fpath.display().to_string())
|
||||
.map(|fpath| File {
|
||||
path: fpath.clone(),
|
||||
hash: None,
|
||||
size: Some(fs::metadata(fpath).unwrap().len()),
|
||||
})
|
||||
.filter(|file| filters::is_file_gt_minsize(app_opts, file))
|
||||
.collect();
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
fn process_file_size_index(fpath: String) -> Result<File> {
|
||||
Ok(File {
|
||||
path: fpath.clone(),
|
||||
size: Some(fs::metadata(fpath)?.len()),
|
||||
hash: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn process_file_hash_index(fpath: String) -> Result<File> {
|
||||
Ok(File {
|
||||
path: fpath.clone(),
|
||||
size: None,
|
||||
hash: Some(hash_file(&fpath).unwrap_or_default()),
|
||||
})
|
||||
}
|
||||
|
||||
fn process_file_index(
|
||||
fpath: String,
|
||||
mut file: File,
|
||||
store: &DashMap<String, Vec<File>>,
|
||||
index_criteria: IndexCritera,
|
||||
) {
|
||||
match index_criteria {
|
||||
IndexCritera::Size => {
|
||||
let processed_file = process_file_size_index(fpath).unwrap();
|
||||
store
|
||||
.entry(processed_file.size.unwrap_or_default().to_string())
|
||||
.and_modify(|fileset| fileset.push(processed_file.clone()))
|
||||
.or_insert_with(|| vec![processed_file]);
|
||||
.entry(file.size.unwrap_or_default().to_string())
|
||||
.and_modify(|fileset| fileset.push(file.clone()))
|
||||
.or_insert_with(|| vec![file]);
|
||||
}
|
||||
IndexCritera::Hash => {
|
||||
let processed_file = process_file_hash_index(fpath).unwrap();
|
||||
let indexhash = processed_file.clone().hash.unwrap_or_default();
|
||||
|
||||
file.hash = Some(hash_file(&file.path).unwrap_or_default());
|
||||
store
|
||||
.entry(indexhash)
|
||||
.and_modify(|fileset| fileset.push(processed_file.clone()))
|
||||
.or_insert_with(|| vec![processed_file]);
|
||||
.entry(file.clone().hash.unwrap())
|
||||
.and_modify(|fileset| fileset.push(file.clone()))
|
||||
.or_insert_with(|| vec![file]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn index_files(
|
||||
files: Vec<String>,
|
||||
files: Vec<File>,
|
||||
index_criteria: IndexCritera,
|
||||
) -> Result<DashMap<String, Vec<File>>> {
|
||||
let store: DashMap<String, Vec<File>> = DashMap::new();
|
||||
@@ -124,7 +106,7 @@ fn index_files(
|
||||
Ok(store)
|
||||
}
|
||||
|
||||
pub fn incremental_hashing(filepath: &str) -> Result<String> {
|
||||
fn incremental_hashing(filepath: &str) -> Result<String> {
|
||||
let file = fs::File::open(filepath)?;
|
||||
let fmap = unsafe { Mmap::map(&file)? };
|
||||
let mut inchasher = fxhash::FxHasher::default();
|
||||
@@ -135,12 +117,12 @@ pub fn incremental_hashing(filepath: &str) -> Result<String> {
|
||||
Ok(format!("{}", inchasher.finish()))
|
||||
}
|
||||
|
||||
pub fn standard_hashing(filepath: &str) -> Result<String> {
|
||||
fn standard_hashing(filepath: &str) -> Result<String> {
|
||||
let file = fs::read(filepath)?;
|
||||
Ok(hasher(&*file).to_string())
|
||||
}
|
||||
|
||||
pub fn hash_file(filepath: &str) -> Result<String> {
|
||||
fn hash_file(filepath: &str) -> Result<String> {
|
||||
let filemeta = fs::metadata(filepath)?;
|
||||
|
||||
// NOTE: USE INCREMENTAL HASHING ONLY FOR FILES > 100MB
|
||||
|
||||
Reference in New Issue
Block a user