38 Commits
0.1.2 ... 0.1.5

Author SHA1 Message Date
Sreedev Kodichath
ab44d1ed04 Merge pull request #47 from sreedevk/development
Version 0.1.5
2023-01-25 20:35:09 -05:00
sreedev
6b06798e8e updated README.md 2023-01-23 20:15:24 -05:00
sreedev
4d27da99f3 updated README.md 2023-01-23 20:09:15 -05:00
Sreedev Kodichath
91dce29bb4 Merge pull request #46 from sreedevk/feature/add_scan_control_args
[Feature] Add scan control args
2023-01-23 20:06:54 -05:00
sreedev
f9b6d57968 added short params 2023-01-23 20:05:14 -05:00
sreedev
5c86a080c4 Merge branch 'development' into feature/add_scan_control_args 2023-01-23 20:03:30 -05:00
sreedev
9f4d9139b6 added --min-depth --max-depth --follow-links and renamed --minsize to --min-size 2023-01-23 20:03:08 -05:00
Sreedev Kodichath
153109ef57 Merge pull request #45 from sreedevk/feature/make_dir_positional
make --dir a positional argument
2023-01-23 20:02:00 -05:00
sreedev
fa12f85b6a make --dir a positional argument 2023-01-23 19:12:55 -05:00
sreedev
7d66aeef6e version 0.1.5 2023-01-23 19:02:09 -05:00
Sreedev Kodichath
163e8d85c0 Merge pull request #38 from sreedevk/development
Version 0.1.4
2023-01-23 18:35:28 -05:00
sreedev
b1c508e9e7 versioning changes 2023-01-23 18:33:06 -05:00
Sreedev Kodichath
4125b18d67 Merge pull request #42 from sreedevk/feature/add-outputgen-spinner
Added more progress bars to avoid blank states
2023-01-23 18:30:56 -05:00
sreedev
10329015f7 added more progress bars to avoid blank states 2023-01-23 18:29:41 -05:00
Sreedev Kodichath
de6d2e325e Merge pull request #41 from beeb/globwalk-builder
refactor: use globwalk builder pattern
2023-01-23 17:50:09 -05:00
beeb
850c274adc refactor: use globwalk builder pattern 2023-01-23 22:28:48 +01:00
sreedev
31d35aae51 Merge branch 'main' into development 2023-01-23 16:07:26 -05:00
Sreedev Kodichath
087f36ac52 Merge pull request #37 from beeb/globwalk
Switch to globwalk
2023-01-23 16:06:14 -05:00
beeb
6f8b1d55df feat: switch to globwalk 2023-01-23 21:53:44 +01:00
sreedev
c6318a310b added benchmarks 2023-01-23 15:37:34 -05:00
Sreedev Kodichath
76c69ef8a0 Update README.md 2023-01-23 15:14:31 -05:00
sreedev
fd4e882aa8 updated README 2023-01-23 14:54:19 -05:00
sreedev
fd1d214d62 updated github workflows 2023-01-23 03:36:47 -05:00
sreedev
1a573fbf3f release github flow changes 2023-01-23 03:24:12 -05:00
sreedev
b12b662ba1 workflow updates 2023-01-23 03:19:57 -05:00
sreedev
96337b6b48 artifacts upload options added to github workflows 2023-01-23 02:39:37 -05:00
sreedev
02f70aa55f updated release github actions 2023-01-23 02:28:57 -05:00
Sreedev Kodichath
4a18b3fd91 Merge pull request #33 from sreedevk/development
Version 0.1.3
2023-01-23 02:20:59 -05:00
sreedev
876fdc69ac added version changes 2023-01-23 02:20:42 -05:00
sreedev
8acc3f8c8f added github workflow for building binaries with releases 2023-01-23 02:18:23 -05:00
Sreedev Kodichath
38f6c48452 Merge pull request #35 from sreedevk/dependabot/cargo/tokio-1.23.1
build(deps): bump tokio from 1.23.0 to 1.23.1
2023-01-21 19:31:49 -05:00
sreedev
3e1e9c0e2e Merge branch 'main' into development 2023-01-20 21:01:50 -05:00
Sreedev Kodichath
0674ab55bf Update README.md 2023-01-20 21:01:47 -05:00
dependabot[bot]
ccff95dfc5 build(deps): bump tokio from 1.23.0 to 1.23.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.23.0 to 1.23.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.23.0...tokio-1.23.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-21 01:57:43 +00:00
Sreedev Kodichath
53d28abb62 Merge pull request #32 from sreedevk/performance/glob_improvements
Performance/glob improvements
2023-01-20 19:17:39 -05:00
Sreedev Kodichath
ea6cf94952 Update README.md 2023-01-19 23:37:13 -05:00
sreedev
7b275d40d0 min opts 2 2023-01-19 23:27:40 -05:00
sreedev
30398c3ca9 min opts 2023-01-19 23:25:16 -05:00
10 changed files with 363 additions and 152 deletions

31
.github/workflows/release.yml vendored Normal file
View 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 }}

View File

@@ -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
View File

@@ -1,2 +1,3 @@
/target
/test_data
.envrc

122
Cargo.lock generated
View File

@@ -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"
@@ -229,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",
@@ -305,7 +324,7 @@ dependencies = [
[[package]]
name = "deduplicator"
version = "0.1.2"
version = "0.1.5"
dependencies = [
"anyhow",
"bytesize",
@@ -314,7 +333,7 @@ dependencies = [
"colored",
"dashmap",
"fxhash",
"glob",
"globwalk",
"indicatif",
"itertools",
"memmap2",
@@ -385,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"
@@ -406,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"
@@ -465,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"
@@ -787,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"
@@ -819,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"
@@ -919,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"
@@ -932,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",
@@ -985,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"

View File

@@ -1,10 +1,14 @@
[package]
name = "deduplicator"
version = "0.1.2"
version = "0.1.5"
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
@@ -16,12 +20,12 @@ 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"
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"

138
README.md
View File

@@ -4,48 +4,120 @@
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]
Usage: deduplicator [OPTIONS] [scan_dir_path]
Arguments:
[scan_dir_path] Run Deduplicator on dir different from pwd (e.g., ~/Pictures )
Options:
-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
-t, --types <TYPES> Filetypes to deduplicate [default = all]
-i, --interactive Delete files interactively
-s, --min-size <MIN_SIZE> Minimum filesize of duplicates to scan (e.g., 100B/1K/2M/3G/4T) [default: 1b]
-d, --max-depth <MAX_DEPTH> Max Depth to scan while looking for duplicates
--min-depth <MIN_DEPTH> Min Depth to scan while looking for duplicates
-f, --follow-links Follow links while scanning directories
-h, --help Print help information
-V, --version Print version information
```
### Examples
```bash
# Scan for duplicates recursively from the current dir, only look for png, jpg & pdf file types & interactively delete files
deduplicator -t pdf,jpg,png -i
# Scan for duplicates recursively from the ~/Pictures dir, only look for png, jpeg, jpg & pdf file types & interactively delete files
deduplicator ~/Pictures/ -t png,jpeg,jpg,pdf -i
# Scan for duplicates in the ~/Pictures without recursing into subdirectories
deduplicator ~/Pictures --max-depth 0
# look for duplicates in the ~/.config directory while also recursing into symbolic link paths
deduplicator ~/.config --follow-links
# scan for duplicates that are greater than 100mb in the ~/Media directory
deduplicator ~/Media --min-size 100mb
```
<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 --min-size 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 <del>couple of seconds.</del> few milliseconds.</p>
<p align="center">
<del>While testing, Deduplicator was able to go through 8.6GB of pdf files and detect duplicates in 2.9 seconds</del>
As of version 0.1.1, on testing locally, deduplicator was able to process and find duplicates in 120GB of files (Videos, PDFs, Images) in ~300ms
</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
![](https://user-images.githubusercontent.com/36154121/213618143-e5182e39-731e-4817-87dd-1a6a0f38a449.gif)

View File

@@ -1,8 +1,8 @@
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() {
pub fn is_file_gt_min_size(app_opts: &Params, file: &File) -> bool {
match app_opts.get_min_size() {
Some(msize) => match file.size {
Some(fsize) => fsize >= msize,
None => true,

View File

@@ -5,6 +5,7 @@ use chrono::offset::Utc;
use chrono::DateTime;
use colored::Colorize;
use dashmap::DashMap;
use indicatif::{ProgressBar, ProgressIterator, ProgressStyle};
use itertools::Itertools;
use prettytable::{format, row, Table};
use std::io::Write;
@@ -12,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)
@@ -26,7 +27,7 @@ fn format_path(path: &str, opts: &Params) -> Result<String> {
display_path
};
Ok(format!("...{:<32}", display_range))
Ok(format!("...{display_range:<32}"))
}
fn file_size(file: &File) -> Result<String> {
@@ -40,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.");
@@ -115,8 +112,6 @@ 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{}",
@@ -128,8 +123,8 @@ pub fn interactive(duplicates: DashMap<String, Vec<File>>, opts: &Params) {
duplicates
.clone()
.into_iter()
.sorted_unstable_by_key(|f| {
-(f.1.first().and_then(|ff| ff.size).unwrap_or_default() as i64)
.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))| {
@@ -140,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).unwrap_or_default().red(),
file_size(file).unwrap_or_default().red(),
modified_time(&file.path).unwrap_or_default().yellow()
]);
});
@@ -150,8 +145,6 @@ 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{}",
@@ -161,17 +154,25 @@ pub fn print(duplicates: DashMap<String, Vec<File>>, opts: &Params) {
}
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()
.sorted_unstable_by_key(|f| f.1.first().and_then(|ff| ff.size).unwrap_or_default()) // sort by ascending size
.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(),
file_size(file).unwrap_or_default().red(),
modified_time(&file.path).unwrap_or_default().yellow()
]);
});

View File

@@ -1,27 +1,38 @@
use std::{fs, path::PathBuf};
use anyhow::{anyhow, Result};
use clap::{Parser, ValueHint};
use std::{fs, path::PathBuf};
use globwalk::{GlobWalker, GlobWalkerBuilder};
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Params {
/// Filetypes to deduplicate (default = all)
/// Filetypes to deduplicate [default = all]
#[arg(short, long)]
pub types: Option<String>,
/// Run Deduplicator on dir different from pwd
#[arg(long, value_hint = ValueHint::DirPath)]
/// Run Deduplicator on dir different from pwd (e.g., ~/Pictures )
#[arg(value_hint = ValueHint::DirPath, value_name = "scan_dir_path")]
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]
/// Minimum filesize of duplicates to scan (e.g., 100B/1K/2M/3G/4T).
#[arg(long, short = 's', default_value = "1b")]
pub min_size: Option<String>,
/// Max Depth to scan while looking for duplicates
#[arg(long, short = 'd')]
pub max_depth: Option<usize>,
/// Min Depth to scan while looking for duplicates
#[arg(long)]
pub min_depth: Option<usize>,
/// Follow links while scanning directories
#[arg(long, short)]
pub minsize: Option<String>,
pub follow_links: bool,
}
impl Params {
pub fn get_minsize(&self) -> Option<u64> {
match &self.minsize {
pub fn get_min_size(&self) -> Option<u64> {
match &self.min_size {
Some(msize) => match msize.parse::<bytesize::ByteSize>() {
Ok(units) => Some(units.0),
Err(_) => None,
@@ -30,34 +41,48 @@ 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_directory(&self) -> Result<PathBuf> {
let current_dir = std::env::current_dir()?;
let dir_path = self.dir.as_ref().unwrap_or(&current_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()
fn add_glob_min_depth(&self, builder: GlobWalkerBuilder) -> Result<GlobWalkerBuilder> {
match self.min_depth {
Some(mindepth) => Ok(builder.min_depth(mindepth)),
None => Ok(builder),
}
}
fn add_glob_max_depth(&self, builder: GlobWalkerBuilder) -> Result<GlobWalkerBuilder> {
match self.max_depth {
Some(maxdepth) => Ok(builder.max_depth(maxdepth)),
None => Ok(builder),
}
}
fn add_glob_follow_links(&self, builder: GlobWalkerBuilder) -> Result<GlobWalkerBuilder> {
match self.follow_links {
true => Ok(builder.follow_links(true)),
false => Ok(builder.follow_links(false)),
}
}
pub fn get_glob_walker(&self) -> Result<GlobWalker> {
let pattern: String = match self.types.as_ref() {
Some(filetypes) => format!("**/*{{{filetypes}}}"),
None => "**/*".to_string(),
};
let glob_walker_builder = self
.add_glob_min_depth(GlobWalkerBuilder::from_patterns(
self.get_directory()?,
&[pattern],
))
.and_then(|builder| self.add_glob_max_depth(builder))
.and_then(|builder| self.add_glob_follow_links(builder))?;
glob_walker_builder.build().map_err(|e| anyhow!(e))
}
}

View File

@@ -2,11 +2,11 @@ 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};
#[derive(Clone, Copy)]
@@ -40,44 +40,37 @@ pub fn duplicates(app_opts: &Params) -> Result<DashMap<String, Vec<File>>> {
}
fn scan(app_opts: &Params) -> Result<Vec<File>> {
let glob_patterns: Vec<PathBuf> = app_opts.get_glob_patterns();
let files: Vec<File> = glob_patterns
.par_iter()
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(|file_path| File {
path: file_path.clone(),
.map(|fpath| fpath.display().to_string())
.map(|fpath| File {
path: fpath.clone(),
hash: None,
size: Some(fs::metadata(file_path).unwrap().len()),
size: Some(fs::metadata(fpath).unwrap().len()),
})
.filter(|file| filters::is_file_gt_minsize(app_opts, file))
.filter(|file| filters::is_file_gt_min_size(app_opts, file))
.collect();
Ok(files)
}
fn process_file_hash_index(file: &File) -> Result<File> {
Ok(File {
path: file.path.clone(),
size: file.size,
hash: Some(hash_file(&file.path).unwrap_or_default()),
})
}
fn process_file_index(
file: File,
mut file: File,
store: &DashMap<String, Vec<File>>,
index_criteria: IndexCritera,
) {
@@ -89,13 +82,11 @@ fn process_file_index(
.or_insert_with(|| vec![file]);
}
IndexCritera::Hash => {
let processed_file = process_file_hash_index(&file).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]);
}
}
}