34 Commits
0.1.3 ... 0.1.6

Author SHA1 Message Date
Sreedev Kodichath
7bd88f1642 Merge pull request #49 from sreedevk/development
Version 0.1.6
2023-02-06 09:37:01 -05:00
sreedev
a65e22269c stabilize ui progress bars 2023-02-06 09:35:24 -05:00
sreedev
6e3fe4a37d cargo dist 2023-02-03 10:18:26 -05:00
sreedev
eadffb5fea doc fixes 2023-01-27 09:56:10 -05:00
sreedev
5532ded331 updated benchmarks 2023-01-27 09:54:39 -05:00
sreedev
7119f019d3 benchmark updates 2023-01-27 09:52:14 -05:00
sreedev
493cac1762 upgraded dependencies 2023-01-26 14:17:54 -05:00
sreedev
f0dbf05705 removed tokio & other unused dependencies 2023-01-26 14:07:50 -05:00
sreedev
ef1e9a1fce replace path String in File type with PathBuf 2023-01-26 00:04:17 -05:00
sreedev
d06ca7897d fix critical error 2023-01-25 23:12:52 -05:00
sreedev
b49940998b version 0.1.6 2023-01-25 21:27:31 -05:00
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
11 changed files with 562 additions and 272 deletions

View File

@@ -1,41 +1,136 @@
# CI that:
#
# * checks for a Git Tag that looks like a release ("v1.2.0")
# * creates a Github Release™
# * builds binaries/packages with cargo-dist
# * uploads those packages to the Github Release™
#
# Note that the Github Release™ will be created before the packages,
# so there will be a few minutes where the release has no packages
# and then they will slowly trickle in, possibly failing. To make
# this more pleasant we mark the release as a "draft" until all
# artifacts have been successfully uploaded. This allows you to
# choose what to do with partial successes and avoids spamming
# anyone with notifications before the release is actually ready.
name: Release name: Release
env: permissions:
PROJECT_NAME: deduplicator contents: write
PROJECT_DESC: "Filter, Sort & Delete Duplicate Files Recursively"
PROJECT_AUTH: "sreedevk"
# This task will run whenever you push a git tag that looks like
# a version number. We just look for `v` followed by at least one number
# and then whatever. so `v1`, `v1.0.0`, and `v1.0.0-prerelease` all work.
#
# If there's a prerelease-style suffix to the version then the Github Release™
# will be marked as a prerelease (handled by taiki-e/create-gh-release-action).
#
# Note that when generating links to uploaded artifacts, cargo-dist will currently
# assume that your git tag is always v{VERSION} where VERSION is the version in
# the published package's Cargo.toml (this is the default behaviour of cargo-release).
# In the future this may be made more robust/configurable.
on: on:
push: push:
tags: tags:
- "[0-9]+.*" - v[0-9]+.*
env:
ALL_CARGO_DIST_TARGET_ARGS: --target=x86_64-unknown-linux-gnu --target=x86_64-apple-darwin --target=x86_64-pc-windows-msvc
ALL_CARGO_DIST_INSTALLER_ARGS:
jobs: jobs:
# Create the Github Release™ so the packages have something to be uploaded to
create-release: create-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
tag: ${{ steps.create-gh-release.outputs.computed-prefix }}${{ steps.create-gh-release.outputs.version }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: taiki-e/create-gh-release-action@v1.6.2 - id: create-gh-release
uses: taiki-e/create-gh-release-action@v1
with: with:
changelog: CHANGELOG.md draft: true
# (required) GitHub token for creating GitHub Releases.
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
branch: main
upload-assets:
# Build and packages all the things
upload-artifacts:
needs: create-release
strategy: strategy:
matrix: matrix:
os: # For these target platforms
- ubuntu-latest include:
- macos-latest - target: x86_64-unknown-linux-gnu
- windows-latest os: ubuntu-20.04
install-dist: curl --proto '=https' --tlsv1.2 -L -sSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.2/installer.sh | sh
- target: x86_64-apple-darwin
os: macos-11
install-dist: curl --proto '=https' --tlsv1.2 -L -sSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.2/installer.sh | sh
- target: x86_64-pc-windows-msvc
os: windows-2019
install-dist: irm 'https://github.com/axodotdev/cargo-dist/releases/download/v0.0.2/installer.ps1' | iex
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: taiki-e/upload-rust-binary-action@v1 - name: Install Rust
with: run: rustup update stable && rustup default stable
bin: deduplicator - name: Install cargo-dist
tar: unix run: ${{ matrix.install-dist }}
zip: windows - name: Run cargo-dist
token: ${{ secrets.GITHUB_TOKEN }} # This logic is a bit janky because it's trying to be a polyglot between
env: # powershell and bash since this will run on windows, macos, and linux!
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # The two platforms don't agree on how to talk about env vars but they
# do agree on 'cat' and '$()' so we use that to marshal values between commmands.
run: |
# Actually do builds and make zips and whatnot
cargo dist --target=${{ matrix.target }} --output-format=json > dist-manifest.json
echo "dist ran successfully"
cat dist-manifest.json
# Parse out what we just built and upload it to the Github Release™
cat dist-manifest.json | jq --raw-output ".releases[].artifacts[].path" > uploads.txt
echo "uploading..."
cat uploads.txt
gh release upload ${{ needs.create-release.outputs.tag }} $(cat uploads.txt)
echo "uploaded!"
# Compute and upload the manifest for everything
upload-manifest:
needs: create-release
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v3
- name: Install Rust
run: rustup update stable && rustup default stable
- name: Install cargo-dist
run: curl --proto '=https' --tlsv1.2 -L -sSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.2/installer.sh | sh
- name: Run cargo-dist manifest
run: |
# Generate a manifest describing everything
cargo dist manifest --no-local-paths --output-format=json $ALL_CARGO_DIST_TARGET_ARGS $ALL_CARGO_DIST_INSTALLER_ARGS > dist-manifest.json
echo "dist manifest ran successfully"
cat dist-manifest.json
# Upload the manifest to the Github Release™
gh release upload ${{ needs.create-release.outputs.tag }} dist-manifest.json
echo "uploaded manifest!"
# Edit the Github Release™ title/body to match what cargo-dist thinks it should be
CHANGELOG_TITLE=$(cat dist-manifest.json | jq --raw-output ".releases[].changelog_title")
cat dist-manifest.json | jq --raw-output ".releases[].changelog_body" > new_dist_changelog.md
gh release edit ${{ needs.create-release.outputs.tag }} --title="$CHANGELOG_TITLE" --notes-file=new_dist_changelog.md
echo "updated release notes!"
# Mark the Github Release™ as a non-draft now that everything has succeeded!
publish-release:
needs: [create-release, upload-artifacts, upload-manifest]
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v3
- name: mark release as non-draft
run: |
gh release edit ${{ needs.create-release.outputs.tag }} --draft=false

1
.gitignore vendored
View File

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

307
Cargo.lock generated
View File

@@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 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]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@@ -53,10 +62,20 @@ dependencies = [
] ]
[[package]] [[package]]
name = "bumpalo" name = "bstr"
version = "3.11.1" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" checksum = "b45ea9b00a7b3f2988e9a65ad3917e62123c38dba709b666506207be96d1790b"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
@@ -64,12 +83,6 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
[[package]] [[package]]
name = "bytesize" name = "bytesize"
version = "1.1.0" version = "1.1.0"
@@ -105,9 +118,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.0.32" version = "4.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39" checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"clap_derive", "clap_derive",
@@ -120,9 +133,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.0.21" version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro-error", "proc-macro-error",
@@ -133,9 +146,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade"
dependencies = [ dependencies = [
"os_str_bytes", "os_str_bytes",
] ]
@@ -163,9 +176,9 @@ dependencies = [
[[package]] [[package]]
name = "console" name = "console"
version = "0.15.4" version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9b6515d269224923b26b5febea2ed42b2d5f2ce37284a4dd670fedd6cb8347a" checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60"
dependencies = [ dependencies = [
"encode_unicode 0.3.6", "encode_unicode 0.3.6",
"lazy_static", "lazy_static",
@@ -229,7 +242,7 @@ version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
dependencies = [ dependencies = [
"bstr", "bstr 0.2.17",
"csv-core", "csv-core",
"itoa", "itoa",
"ryu", "ryu",
@@ -247,9 +260,9 @@ dependencies = [
[[package]] [[package]]
name = "cxx" name = "cxx"
version = "1.0.85" version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd" checksum = "b61a7545f753a88bcbe0a70de1fcc0221e10bfc752f576754fa91e663db1622e"
dependencies = [ dependencies = [
"cc", "cc",
"cxxbridge-flags", "cxxbridge-flags",
@@ -259,9 +272,9 @@ dependencies = [
[[package]] [[package]]
name = "cxx-build" name = "cxx-build"
version = "1.0.85" version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0" checksum = "f464457d494b5ed6905c63b0c4704842aba319084a0a3561cdc1359536b53200"
dependencies = [ dependencies = [
"cc", "cc",
"codespan-reporting", "codespan-reporting",
@@ -274,15 +287,15 @@ dependencies = [
[[package]] [[package]]
name = "cxxbridge-flags" name = "cxxbridge-flags"
version = "1.0.85" version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59" checksum = "43c7119ce3a3701ed81aca8410b9acf6fc399d2629d057b87e2efa4e63a3aaea"
[[package]] [[package]]
name = "cxxbridge-macro" name = "cxxbridge-macro"
version = "1.0.85" version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6" checksum = "65e07508b90551e610910fa648a1878991d367064997a596135b86df30daf07e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -305,7 +318,7 @@ dependencies = [
[[package]] [[package]]
name = "deduplicator" name = "deduplicator"
version = "0.1.3" version = "0.1.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytesize", "bytesize",
@@ -314,14 +327,12 @@ dependencies = [
"colored", "colored",
"dashmap", "dashmap",
"fxhash", "fxhash",
"glob", "globwalk",
"indicatif", "indicatif",
"itertools", "itertools",
"memmap2", "memmap2",
"prettytable-rs", "prettytable-rs",
"rayon", "rayon",
"thiserror",
"tokio",
"unicode-segmentation", "unicode-segmentation",
] ]
@@ -385,6 +396,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "fxhash" name = "fxhash"
version = "0.2.1" version = "0.2.1"
@@ -406,10 +423,28 @@ dependencies = [
] ]
[[package]] [[package]]
name = "glob" name = "globset"
version = "0.3.0" version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "hashbrown" name = "hashbrown"
@@ -466,24 +501,40 @@ dependencies = [
] ]
[[package]] [[package]]
name = "indicatif" name = "ignore"
version = "0.17.2" version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4295cbb7573c16d310e99e713cf9e75101eb190ab31fccd35f2d2691b4352b19" checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492"
dependencies = [
"globset",
"lazy_static",
"log",
"memchr",
"regex",
"same-file",
"thread_local",
"walkdir",
"winapi-util",
]
[[package]]
name = "indicatif"
version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729"
dependencies = [ dependencies = [
"console", "console",
"number_prefix", "number_prefix",
"portable-atomic", "portable-atomic",
"rayon", "rayon",
"tokio",
"unicode-width", "unicode-width",
] ]
[[package]] [[package]]
name = "io-lifetimes" name = "io-lifetimes"
version = "1.0.3" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys", "windows-sys",
@@ -595,18 +646,6 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "mio"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
]
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.45" version = "0.1.45"
@@ -644,9 +683,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.16.0" version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]] [[package]]
name = "os_str_bytes" name = "os_str_bytes"
@@ -654,21 +693,11 @@ version = "6.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]] [[package]]
name = "parking_lot_core" name = "parking_lot_core"
version = "0.9.5" version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
@@ -677,12 +706,6 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "pin-project-lite"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "0.3.19" version = "0.3.19"
@@ -729,9 +752,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.49" version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -757,9 +780,9 @@ dependencies = [
[[package]] [[package]]
name = "rayon-core" name = "rayon-core"
version = "1.10.1" version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"crossbeam-deque", "crossbeam-deque",
@@ -787,6 +810,17 @@ dependencies = [
"thiserror", "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]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.1.10" version = "0.1.10"
@@ -794,10 +828,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]] [[package]]
name = "rustix" name = "regex-syntax"
version = "0.36.5" version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "rustix"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
@@ -819,6 +859,15 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" 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]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@@ -837,31 +886,12 @@ version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.10.0" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "socket2"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
@@ -892,9 +922,9 @@ dependencies = [
[[package]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.1.3" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
dependencies = [ dependencies = [
"winapi-util", "winapi-util",
] ]
@@ -919,6 +949,15 @@ dependencies = [
"syn", "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]] [[package]]
name = "time" name = "time"
version = "0.1.45" version = "0.1.45"
@@ -930,37 +969,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "tokio"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38a54aca0c15d014013256222ba0ebed095673f89345dd79119d912eb561b7a8"
dependencies = [
"autocfg",
"bytes",
"libc",
"memchr",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.6" version = "1.0.6"
@@ -985,6 +993,17 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 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]] [[package]]
name = "wasi" name = "wasi"
version = "0.10.0+wasi-snapshot-preview1" version = "0.10.0+wasi-snapshot-preview1"
@@ -1099,42 +1118,42 @@ dependencies = [
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.42.0" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.42.0" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.42.0" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.42.0" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.42.0" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.42.0" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.42.0" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"

View File

@@ -1,10 +1,14 @@
[package] [package]
name = "deduplicator" name = "deduplicator"
version = "0.1.3" version = "0.1.6"
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>"] 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,12 +20,17 @@ clap = { version = "4.0.32", features = ["derive"] }
colored = "2.0.0" colored = "2.0.0"
dashmap = { version = "5.4.0", features = ["rayon"] } dashmap = { version = "5.4.0", features = ["rayon"] }
fxhash = "0.2.1" fxhash = "0.2.1"
glob = "0.3.0" globwalk = "0.8.1"
indicatif = { version = "0.17.2", features = ["rayon", "tokio"] } indicatif = { version = "0.17.2", features = ["rayon"] }
itertools = "0.10.5" itertools = "0.10.5"
memmap2 = "0.5.8" memmap2 = "0.5.8"
prettytable-rs = "0.10.0" prettytable-rs = "0.10.0"
rayon = "1.6.1" rayon = "1.6.1"
thiserror = "1.0.38"
tokio = { version = "1.23.1", features = ["full"] }
unicode-segmentation = "1.10.0" unicode-segmentation = "1.10.0"
# generated by 'cargo dist init'
[profile.dist]
inherits = "release"
debug = true
split-debuginfo = "packed"

135
README.md
View File

@@ -4,45 +4,120 @@
Find, Sort, Filter & Delete duplicate files Find, Sort, Filter & Delete duplicate files
</p> </p>
<p align="center"> ## Usage
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>
```bash ```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: 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 -i, --interactive Delete files interactively
-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]
-m, --minsize <MINSIZE> Minimum filesize of duplicates to scan (e.g., 100B/1K/2M/3G/4T). [default = 0] -d, --max-depth <MAX_DEPTH> Max Depth to scan while looking for duplicates
-h, --help Print help information --min-depth <MIN_DEPTH> Min Depth to scan while looking for duplicates
-V, --version Print version information -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 | Filecount | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|:---|---:|---:|---:|---:|---:|
| `deduplicator ~/Data/tmp` | (~120G) | 721 files | 33.5 ± 28.6 | 25.3 | 151.5 | 1.87 ± 1.60 |
| `deduplicator ~/Data/books` | (~8.6G) | 1419 files | 24.5 ± 1.0 | 22.9 | 28.1 | 1.37 ± 0.08 |
| `deduplicator ~/Data/books --min-size 10M` | (~8.6G) | 1419 files | 17.9 ± 0.7 | 16.8 | 20.0 | 1.00 |
| `deduplicator ~/Data/ --types pdf,jpg,png,jpeg` | (~290G) | 104222 files | 1207.2 ± 37.0 | 1172.2 | 1287.7 | 67.27 ± 3.33 |
* 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> ## Screenshots
<p align="center"> ![](https://user-images.githubusercontent.com/36154121/213618143-e5182e39-731e-4817-87dd-1a6a0f38a449.gif)
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/213618143-e5182e39-731e-4817-87dd-1a6a0f38a449.gif" />

View File

@@ -1,9 +1,10 @@
use anyhow::Result; use anyhow::Result;
use colored::Colorize; use colored::Colorize;
use std::path::PathBuf;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct File { pub struct File {
pub path: String, pub path: PathBuf,
pub size: Option<u64>, pub size: Option<u64>,
pub hash: Option<String>, pub hash: Option<String>,
} }
@@ -11,8 +12,8 @@ pub struct File {
pub fn delete_files(files: Vec<File>) -> Result<()> { pub fn delete_files(files: Vec<File>) -> Result<()> {
files.into_iter().for_each(|file| { files.into_iter().for_each(|file| {
match std::fs::remove_file(file.path.clone()) { match std::fs::remove_file(file.path.clone()) {
Ok(_) => println!("{}: {}", "DELETED".green(), file.path), Ok(_) => println!("{}: {}", "DELETED".green(), file.path.display()),
Err(_) => println!("{}: {}", "FAILED".red(), file.path) Err(_) => println!("{}: {}", "FAILED".red(), file.path.display())
} }
}); });

View File

@@ -1,8 +1,8 @@
use crate::file_manager::File; use crate::file_manager::File;
use crate::params::Params; use crate::params::Params;
pub fn is_file_gt_minsize(app_opts: &Params, file: &File) -> bool { pub fn is_file_gt_min_size(app_opts: &Params, file: &File) -> bool {
match app_opts.get_minsize() { match app_opts.get_min_size() {
Some(msize) => match file.size { Some(msize) => match file.size {
Some(fsize) => fsize >= msize, Some(fsize) => fsize >= msize,
None => true, None => true,

View File

@@ -9,7 +9,6 @@ use anyhow::Result;
use app::App; use app::App;
use clap::Parser; use clap::Parser;
#[tokio::main] fn main() -> Result<()> {
async fn main() -> Result<()> {
App::init(&params::Params::parse()) App::init(&params::Params::parse())
} }

View File

@@ -5,14 +5,19 @@ use chrono::offset::Utc;
use chrono::DateTime; use chrono::DateTime;
use colored::Colorize; use colored::Colorize;
use dashmap::DashMap; use dashmap::DashMap;
use indicatif::{ProgressBar, ProgressIterator, ProgressStyle};
use itertools::Itertools; use itertools::Itertools;
use prettytable::{format, row, Table}; use prettytable::{format, row, Table};
use std::io::Write; use std::io::Write;
use std::path::Path;
use std::time::Duration;
use std::{fs, io}; use std::{fs, io};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
fn format_path(path: &str, opts: &Params) -> Result<String> { fn format_path(path: &Path, opts: &Params) -> Result<String> {
let display_path = path.replace(&opts.get_directory()?, ""); let display_path = path
.to_string_lossy()
.replace(opts.get_directory()?.to_string_lossy().as_ref(), "");
let display_range = if display_path.chars().count() > 32 { let display_range = if display_path.chars().count() > 32 {
display_path display_path
.graphemes(true) .graphemes(true)
@@ -26,14 +31,14 @@ fn format_path(path: &str, opts: &Params) -> Result<String> {
display_path display_path
}; };
Ok(format!("...{:<32}", display_range)) Ok(format!("...{display_range:<32}"))
} }
fn file_size(file: &File) -> Result<String> { fn file_size(file: &File) -> Result<String> {
Ok(format!("{:>12}", bytesize::ByteSize::b(file.size.unwrap()))) Ok(format!("{:>12}", bytesize::ByteSize::b(file.size.unwrap())))
} }
fn modified_time(path: &String) -> Result<String> { fn modified_time(path: &Path) -> Result<String> {
let mdata = fs::metadata(path)?; let mdata = fs::metadata(path)?;
let modified_time: DateTime<Utc> = mdata.modified()?.into(); let modified_time: DateTime<Utc> = mdata.modified()?.into();
@@ -73,7 +78,7 @@ fn process_group_action(duplicates: &Vec<File>, dup_index: usize, dup_size: usiz
.split(',') .split(',')
.filter(|element| !element.is_empty()) .filter(|element| !element.is_empty())
.map(|index| index.parse::<usize>().unwrap_or_default()) .map(|index| index.parse::<usize>().unwrap_or_default())
.collect_vec(); .collect::<Vec<usize>>();
if parsed_file_indices if parsed_file_indices
.clone() .clone()
@@ -99,12 +104,12 @@ fn process_group_action(duplicates: &Vec<File>, dup_index: usize, dup_size: usiz
.clone() .clone()
.enumerate() .enumerate()
.for_each(|(index, file)| { .for_each(|(index, file)| {
println!("{}: {}", index.to_string().blue(), file.path); println!("{}: {}", index.to_string().blue(), file.path.display());
}); });
match scan_group_confirmation().unwrap() { match scan_group_confirmation().unwrap() {
true => { true => {
file_manager::delete_files(files_to_delete.collect_vec()).ok(); file_manager::delete_files(files_to_delete.collect::<Vec<File>>()).ok();
} }
false => println!("{}", "\nCancelled Delete Operation.".red()), false => println!("{}", "\nCancelled Delete Operation.".red()),
} }
@@ -153,10 +158,19 @@ pub fn print(duplicates: DashMap<String, Vec<File>>, opts: &Params) {
} }
let mut output_table = Table::new(); let mut output_table = Table::new();
let progress_bar = ProgressBar::new(duplicates.len() as u64);
progress_bar.enable_steady_tick(Duration::from_millis(50));
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"]); output_table.set_titles(row!["hash", "duplicates"]);
duplicates duplicates
.into_iter() .into_iter()
.sorted_unstable_by_key(|(_, f)| f.first().and_then(|ff| ff.size).unwrap_or_default()) .sorted_unstable_by_key(|(_, f)| f.first().and_then(|ff| ff.size).unwrap_or_default())
.progress_with(progress_bar)
.for_each(|(hash, group)| { .for_each(|(hash, group)| {
let mut inner_table = Table::new(); let mut inner_table = Table::new();
inner_table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); inner_table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
@@ -172,3 +186,32 @@ pub fn print(duplicates: DashMap<String, Vec<File>>, opts: &Params) {
output_table.printstd(); output_table.printstd();
} }
#[allow(unused)]
pub fn raw(duplicates: DashMap<String, Vec<File>>, opts: &Params) -> Result<()> {
if duplicates.is_empty() {
println!(
"\n{}",
"No duplicates found matching your search criteria.".green()
);
return Ok(());
}
duplicates
.into_iter()
.sorted_unstable_by_key(|(_, f)| f.first().and_then(|ff| ff.size).unwrap_or_default())
.for_each(|(_hash, group)| {
group.iter().for_each(|file| {
println!(
"{}\t{}\t{}",
format_path(&file.path, opts).unwrap_or_default().blue(),
file_size(file).unwrap_or_default().red(),
modified_time(&file.path).unwrap_or_default().yellow()
)
});
println!("---");
});
Ok(())
}

View File

@@ -1,27 +1,38 @@
use std::{fs, path::PathBuf};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use clap::{Parser, ValueHint}; use clap::{Parser, ValueHint};
use std::{fs, path::PathBuf}; use globwalk::{GlobWalker, GlobWalkerBuilder};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
pub struct Params { pub struct Params {
/// Filetypes to deduplicate (default = all) /// Filetypes to deduplicate [default = all]
#[arg(short, long)] #[arg(short, long)]
pub types: Option<String>, pub types: Option<String>,
/// Run Deduplicator on dir different from pwd /// Run Deduplicator on dir different from pwd (e.g., ~/Pictures )
#[arg(long, value_hint = ValueHint::DirPath)] #[arg(value_hint = ValueHint::DirPath, value_name = "scan_dir_path")]
pub dir: Option<PathBuf>, pub dir: Option<PathBuf>,
/// Delete files interactively /// Delete files interactively
#[arg(long, short)] #[arg(long, short)]
pub interactive: bool, 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)] #[arg(long, short)]
pub minsize: Option<String>, pub follow_links: bool,
} }
impl Params { impl Params {
pub fn get_minsize(&self) -> Option<u64> { pub fn get_min_size(&self) -> Option<u64> {
match &self.minsize { match &self.min_size {
Some(msize) => match msize.parse::<bytesize::ByteSize>() { Some(msize) => match msize.parse::<bytesize::ByteSize>() {
Ok(units) => Some(units.0), Ok(units) => Some(units.0),
Err(_) => None, Err(_) => None,
@@ -30,35 +41,48 @@ impl Params {
} }
} }
pub fn get_directory(&self) -> Result<String> { pub fn get_directory(&self) -> Result<PathBuf> {
let dir_pathbuf: PathBuf = self let current_dir = std::env::current_dir()?;
.dir let dir_path = self.dir.as_ref().unwrap_or(&current_dir).as_path();
.as_ref() let dir = fs::canonicalize(dir_path)?;
.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();
Ok(dir) Ok(dir)
} }
pub fn get_glob_patterns(&self) -> PathBuf { fn add_glob_min_depth(&self, builder: GlobWalkerBuilder) -> Result<GlobWalkerBuilder> {
match self.types.as_ref() { match self.min_depth {
Some(filetypes) => vec![ Some(mindepth) => Ok(builder.min_depth(mindepth)),
self.get_directory().unwrap(), None => Ok(builder),
String::from("**"),
format!("{{{}}}", filetypes),
]
.iter()
.collect::<PathBuf>(),
None => vec![self.get_directory().unwrap().as_str(), "**", "*"]
.iter()
.collect::<PathBuf>(),
} }
} }
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,12 +2,15 @@ use crate::{file_manager::File, filters, params::Params};
use anyhow::Result; use anyhow::Result;
use dashmap::DashMap; use dashmap::DashMap;
use fxhash::hash64 as hasher; use fxhash::hash64 as hasher;
use glob::glob; use indicatif::{ParallelProgressIterator, ProgressBar, ProgressIterator, ProgressStyle};
use indicatif::{ParallelProgressIterator, ProgressStyle};
use memmap2::Mmap; use memmap2::Mmap;
use rayon::prelude::*; use rayon::prelude::*;
use std::hash::Hasher; use std::hash::Hasher;
use std::{fs, path::PathBuf}; use std::time::Duration;
use std::{
fs,
path::{Path, PathBuf},
};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
enum IndexCritera { enum IndexCritera {
@@ -40,27 +43,43 @@ pub fn duplicates(app_opts: &Params) -> Result<DashMap<String, Vec<File>>> {
} }
fn scan(app_opts: &Params) -> Result<Vec<File>> { fn scan(app_opts: &Params) -> Result<Vec<File>> {
let glob_patterns = app_opts.get_glob_patterns().display().to_string(); let walker = app_opts.get_glob_walker()?;
let glob_iter = glob(&glob_patterns)?; let progress = ProgressBar::new_spinner();
let files = glob_iter let progress_style =
.filter(Result::is_ok) ProgressStyle::with_template("{spinner:.green} [mapping paths] {pos} paths")?;
.map(|file| file.unwrap()) progress.set_style(progress_style);
progress.enable_steady_tick(Duration::from_millis(50));
let files = walker
.progress_with(progress)
.filter_map(Result::ok)
.map(|file| file.into_path())
.filter(|fpath| fpath.is_file()) .filter(|fpath| fpath.is_file())
.collect::<Vec<PathBuf>>() .collect::<Vec<PathBuf>>();
let scan_progress = ProgressBar::new(files.len() as u64);
let scan_progress_style = ProgressStyle::with_template(
"{spinner:.green} [processing mapped paths] [{wide_bar:.cyan/blue}] {pos}/{len} files",
)?;
scan_progress.set_style(scan_progress_style);
scan_progress.enable_steady_tick(Duration::from_millis(50));
let scan_results = files
.into_par_iter() .into_par_iter()
.progress_with_style(ProgressStyle::with_template( .progress_with(scan_progress)
"{spinner:.green} [processing scan results] [{wide_bar:.cyan/blue}] {pos}/{len} files",
)?)
.map(|fpath| fpath.display().to_string())
.map(|fpath| File { .map(|fpath| File {
path: fpath.clone(), path: fpath.clone(),
hash: None, hash: None,
size: Some(fs::metadata(fpath).unwrap().len()), size: Some(
fs::metadata(fpath)
.map(|metadata| metadata.len())
.unwrap_or_default(),
),
}) })
.filter(|file| filters::is_file_gt_minsize(app_opts, file)) .filter(|file| filters::is_file_gt_min_size(app_opts, file))
.collect(); .collect();
Ok(files) Ok(scan_results)
} }
fn process_file_index( fn process_file_index(
@@ -78,7 +97,7 @@ fn process_file_index(
IndexCritera::Hash => { IndexCritera::Hash => {
file.hash = Some(hash_file(&file.path).unwrap_or_default()); file.hash = Some(hash_file(&file.path).unwrap_or_default());
store store
.entry(file.clone().hash.unwrap()) .entry(file.clone().hash.unwrap_or_default())
.and_modify(|fileset| fileset.push(file.clone())) .and_modify(|fileset| fileset.push(file.clone()))
.or_insert_with(|| vec![file]); .or_insert_with(|| vec![file]);
} }
@@ -90,17 +109,22 @@ fn index_files(
index_criteria: IndexCritera, index_criteria: IndexCritera,
) -> Result<DashMap<String, Vec<File>>> { ) -> Result<DashMap<String, Vec<File>>> {
let store: DashMap<String, Vec<File>> = DashMap::new(); let store: DashMap<String, Vec<File>> = DashMap::new();
let index_progress = ProgressBar::new(files.len() as u64);
let index_progress_style = ProgressStyle::with_template(
"{spinner:.green} [indexing files] [{wide_bar:.cyan/blue}] {pos}/{len} files",
)?;
index_progress.set_style(index_progress_style);
index_progress.enable_steady_tick(Duration::from_millis(50));
files files
.into_par_iter() .into_par_iter()
.progress_with_style(ProgressStyle::with_template( .progress_with(index_progress)
"{spinner:.green} [indexing files] [{wide_bar:.cyan/blue}] {pos}/{len} files",
)?)
.for_each(|file| process_file_index(file, &store, index_criteria)); .for_each(|file| process_file_index(file, &store, index_criteria));
Ok(store) Ok(store)
} }
fn incremental_hashing(filepath: &str) -> Result<String> { fn incremental_hashing(filepath: &Path) -> Result<String> {
let file = fs::File::open(filepath)?; let file = fs::File::open(filepath)?;
let fmap = unsafe { Mmap::map(&file)? }; let fmap = unsafe { Mmap::map(&file)? };
let mut inchasher = fxhash::FxHasher::default(); let mut inchasher = fxhash::FxHasher::default();
@@ -111,12 +135,12 @@ fn incremental_hashing(filepath: &str) -> Result<String> {
Ok(format!("{}", inchasher.finish())) Ok(format!("{}", inchasher.finish()))
} }
fn standard_hashing(filepath: &str) -> Result<String> { fn standard_hashing(filepath: &Path) -> Result<String> {
let file = fs::read(filepath)?; let file = fs::read(filepath)?;
Ok(hasher(&*file).to_string()) Ok(hasher(&*file).to_string())
} }
fn hash_file(filepath: &str) -> Result<String> { fn hash_file(filepath: &Path) -> Result<String> {
let filemeta = fs::metadata(filepath)?; let filemeta = fs::metadata(filepath)?;
// NOTE: USE INCREMENTAL HASHING ONLY FOR FILES > 100MB // NOTE: USE INCREMENTAL HASHING ONLY FOR FILES > 100MB