135 Commits

Author SHA1 Message Date
sreedevk
e41e5d35fb typo fix 2025-07-13 23:02:06 +00:00
Sreedev Kodichath
884f88d749 Update README.md 2025-07-13 19:00:58 -04:00
sreedevk
ab15cdfcd7 added benchmarks 2025-07-13 22:53:26 +00:00
sreedevk
7c5b106784 add more goals on readme 2025-07-13 21:42:22 +00:00
sreedevk
add53a3be1 updates documentation 2025-07-13 21:40:18 +00:00
sreedevk
d99d783327 add more tests 2025-07-13 21:24:31 +00:00
sreedevk
ed55aaa005 speeeeeeeeeeed 2025-07-13 20:21:17 +00:00
sreedevk
fe79a07b43 changed hash type to u128 2025-07-13 18:11:07 +00:00
sreedevk
47b2fc4a87 move max path size update to a later stage in the pipeline for accurate results 2025-07-13 14:24:24 +00:00
sreedevk
184efdcbbc simplify scanner 2025-07-13 14:02:19 +00:00
sreedevk
81e13a14a2 reduce memuse: fileinfo uses boxed path instead of pathbuf
reduce memuse: fileinfo does not store full metadata
2025-07-13 13:45:30 +00:00
sreedevk
5daec2f531 multi progress box also hidden if --progress not set 2025-07-13 13:37:03 +00:00
sreedevk
fcf69ae1cc progress bar issues fixed with multiprogress 2025-07-13 13:21:20 +00:00
sreedevk
3a03208f47 parallelization progress representation on readme updated 2025-07-13 13:05:34 +00:00
sreedevk
95ecfa0ccc disable progress sprinners by default 2025-07-13 12:59:22 +00:00
sreedevk
ce115fb8dc better organization of interactive module 2025-07-13 12:47:17 +00:00
sreedevk
3785a87cee add more goals to readme 2025-07-13 12:44:49 +00:00
sreedevk
d42166a4fd added first test 2025-07-12 22:23:37 +00:00
sreedevk
4d85f0bbc9 memory usage improvements 2025-07-12 21:30:05 +00:00
sreedevk
8826a87f32 added strict mode with partial hashing default 2025-07-12 21:17:00 +00:00
sreedevk
4aa536e89b hashing restored 2025-07-12 21:04:43 +00:00
sreedevk
9d67a91423 formatter speed improved (new bug: does not output hash) 2025-07-12 20:50:04 +00:00
sreedevk
4a6aadb78e parallelization improvements 2025-07-12 20:21:44 +00:00
sreedevk
1b06eb8e85 multithreading improvements 2025-07-12 18:26:10 +00:00
sreedevk
f9e86f8522 performance improvements 2025-07-12 00:24:25 +00:00
sreedev
b4bf5d4fb3 docs updated 2024-07-05 04:34:12 +00:00
Sreedev Kodichath
028b868ea9 version 0.2.2 (#59)
* replaced fxhash with gxhash
2024-07-04 23:23:01 -04:00
Sreedev Kodichath
a56d194ee3 Merge pull request #56 from sreedevk/feature/add-json-output
Version 0.2.1
2023-11-14 18:38:50 -05:00
sreedev
080cd791dc removed early return 2023-11-14 18:37:48 -05:00
sreedev
b16236763c updated readme + version number 2023-11-14 18:36:27 -05:00
sreedev
3e407f69c8 added json output for further processing using other tools 2023-11-14 18:33:40 -05:00
Sreedev Kodichath
7d386c9420 Merge pull request #54 from sreedevk/distribution-improvements
Improve Distribution Methods & Create Compiled Binaries for More Platforms
2023-08-10 19:54:29 -04:00
sreedev
0dc681d4e7 added release yml 2023-08-10 19:51:57 -04:00
sreedev
442cb4b519 added cargo dist options 2023-08-10 13:05:29 -04:00
sreedev
8463e72f2d removed debug information from distrbution & release profiles to reduce binary size 2023-08-10 12:59:07 -04:00
sreedev
05c95bb67a added roadmap to readme 2023-08-02 09:41:15 -04:00
Sreedev Kodichath
062d44acd9 Merge pull request #53 from sreedevk/v0.2.0
v0.2.0  Architecture Improvements
2023-07-17 18:17:57 -04:00
sreedev
ee6655de9b removed examples 2023-07-17 18:14:13 -04:00
sreedev
ffa5295598 clippy 2023-07-17 14:40:10 -04:00
sreedev
6be8596992 restored interactive mode 2023-07-17 14:34:42 -04:00
sreedev
a40f251e30 subtract overflow issue fixed 2023-07-17 14:19:57 -04:00
sreedev
02d05172da minsize issues fixed 2023-07-17 14:13:18 -04:00
sreedev
dcc709a666 added filetype filter 2023-07-17 14:06:57 -04:00
sreedev
e3d48ec505 v0.2.0 2023-07-17 13:59:58 -04:00
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
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
sreedev
c0042fc9f7 version 0.1.2 release changes 2023-01-19 09:40:58 -05:00
Sreedev Kodichath
5aea0eb6f4 Merge pull request #28 from sreedevk/development
Version 0.1.2
2023-01-19 09:39:59 -05:00
Sreedev Kodichath
f0ff1ec325 Merge pull request #27 from beeb/sorted_by_size
Sort table results by ascending file size
2023-01-18 10:36:11 -05:00
Sreedev Kodichath
b267fcdedf Merge pull request #29 from beeb/cli_value_hint
feat(cli): add value hint for --dir argument
2023-01-18 10:28:15 -05:00
Valentin Bersier
84b194efca feat(cli): add value hint for --dir argument 2023-01-18 15:40:10 +01:00
Valentin Bersier
d162ca98ef docs: comment 2023-01-18 15:34:58 +01:00
Valentin Bersier
dfb73ceb5c feat: sort interactive results by descending size 2023-01-18 15:33:23 +01:00
Valentin Bersier
72ca7c7a44 Merge branch 'development' into sorted_by_size 2023-01-18 15:24:47 +01:00
Sreedev Kodichath
2f3cfd3162 Merge pull request #26 from sreedevk/feature/min-filesize
Min Size Filter Feature
2023-01-18 02:40:17 -05:00
sreedev
9b1f591c20 adds minsize option to README.md 2023-01-18 02:39:29 -05:00
sreedev
e24603c645 output enhancements 2023-01-18 02:34:08 -05:00
sreedev
0e624b042d added minsize filter 2023-01-18 02:30:48 -05:00
Valentin Bersier
e61d1f1475 perf: unwrap the size once 2023-01-18 08:07:51 +01:00
Valentin Bersier
8c76c6a3c8 refactor: simplify iterator to get size 2023-01-18 08:00:53 +01:00
Valentin Bersier
df3bea3d4f feat: display results in ascending size order 2023-01-18 07:59:51 +01:00
sreedev
7ace0bde63 scanner optimizations 2023-01-18 01:33:51 -05:00
sreedev
70de402eab performance updates in README.md 2023-01-18 00:45:24 -05:00
Sreedev Kodichath
01dd93a0ea Merge pull request #22 from sreedevk/development
Version 0.1.1
2023-01-18 00:35:51 -05:00
sreedev
8ac78fb856 version 0.1.1 2023-01-18 00:32:53 -05:00
Sreedev Kodichath
37a4c4d52d Merge pull request #23 from sreedevk/performance/incremental-hashing
Performance Improvements
2023-01-18 00:31:27 -05:00
sreedev
1ea5705474 fix confusing delete confirmation 2023-01-18 00:30:39 -05:00
sreedev
ea748c60d8 performance tweaks 2023-01-18 00:24:50 -05:00
sreedev
51f5f8e61a incremental hashing + remove sqlite + mmap file reads 2023-01-17 23:40:57 -05:00
Sreedev Kodichath
175c7579c4 Merge pull request #21 from sreedevk/code-opts
Code Optimizations
2023-01-13 21:03:58 -05:00
sreedev
81c96ce3b8 code optimizations 2023-01-13 20:54:09 -05:00
sreedev
0031891b8b remove unused code 2023-01-13 20:47:51 -05:00
Sreedev Kodichath
ae87e4e830 Create rust.yml 2023-01-13 07:32:28 -05:00
Sreedev Kodichath
284e168453 Update CONTRIBUTING.md 2023-01-13 01:09:31 -05:00
Sreedev Kodichath
a6511f2cf3 Merge pull request #20 from sreedevk/development
Version 0.0.9
2023-01-13 00:55:25 -05:00
sreedev
64d0106765 version changes 2023-01-13 00:53:46 -05:00
Sreedev Kodichath
c75b2eb1c9 Merge pull request #17 from ghfghfg23/main
Fix of "thread 'main' panicked at 'range start index 130 out of range…
2023-01-13 00:49:13 -05:00
Andrey Ryabov
b736853dfe Take 32 grapheme clusters instead of characters 2023-01-12 21:43:31 +02:00
Sreedev Kodichath
92290480a8 Update README.md 2023-01-11 19:41:23 -05:00
Sreedev Kodichath
b7f775e04c Update README.md 2023-01-11 19:39:02 -05:00
sreedev
12295d7847 version 0.0.8 2023-01-11 19:28:34 -05:00
Sreedev Kodichath
d81f499db3 Create CONTRIBUTING.md 2023-01-11 19:14:39 -05:00
Sreedev Kodichath
9e1360aeb4 Merge pull request #18 from sreedevk/progress-bar
[Feature] Add Progress Bar When Indexing Files
2023-01-11 18:56:40 -05:00
Sreedev Kodichath
38ea37711f Update README.md 2023-01-11 18:37:10 -05:00
Andrey Ryabov
2294471b50 Fix of "thread 'main' panicked at 'range start index 130 out of range for slice of length 104', src/output.rs:21:9" 2023-01-11 23:01:10 +02:00
sreedev
dbf504fc17 added progress bar 2023-01-11 15:55:31 -05:00
Sreedev Kodichath
06efb2ed6f Update issue templates 2023-01-11 07:45:51 -05:00
sreedev
95af6c4a70 updated docs to show interactive mode 2023-01-10 20:28:53 -05:00
27 changed files with 1948 additions and 1168 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[build]
rustflags = ["-C", "target-feature=+aes,+sse2"]

30
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,30 @@
---
name: Bug report
about: Create a report to help us improve
title: "[Bug] Title"
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
** Runtime Info **
App Arguments: [e.g. `-i --nocache`]
Install Type: [e.g. `cargo install`]
App Version: [e.g. v0.0.7]
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Platform Details (please complete the following information):**
- OS: [e.g. Arch Linux]
- Terminal Emulator: [e.g Alacritty]
- Shell [e.g. Zshell]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature] Title"
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

137
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,137 @@
# CI that:
#
# * checks for a Git Tag that looks like a release
# * creates a Github Release™ and fills in its text
# * builds artifacts with cargo-dist (executable-zips, installers)
# * uploads those artifacts to the Github Release™
#
# Note that the Github Release™ will be created before the artifacts,
# so there will be a few minutes where the release has no artifacts
# 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
permissions:
contents: write
# This task will run whenever you push a git tag that looks like a version
# like "v1", "v1.2.0", "v0.1.0-prerelease01", "my-app-v1.0.0", etc.
# The version will be roughly parsed as ({PACKAGE_NAME}-)?v{VERSION}, where
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
# must be a Cargo-style SemVer Version.
#
# If PACKAGE_NAME is specified, then we will create a Github Release™ for that
# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
#
# If PACKAGE_NAME isn't specified, then we will create a Github Release™ for all
# (cargo-dist-able) packages in the workspace with that version (this is mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
# If you push multiple tags at once, separate instances of this workflow will
# spin up, creating an independent Github Release™ for each one.
#
# If there's a prerelease-style suffix to the version then the Github Release™
# will be marked as a prerelease.
on:
push:
tags:
- '*-?v[0-9]+*'
jobs:
# Create the Github Release™ so the packages have something to be uploaded to
create-release:
runs-on: ubuntu-latest
outputs:
has-releases: ${{ steps.create-release.outputs.has-releases }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v3
- name: Install Rust
run: rustup update 1.71.0 --no-self-update && rustup default 1.71.0
- name: Install cargo-dist
run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.sh | sh
- id: create-release
run: |
cargo dist plan --tag=${{ github.ref_name }} --output-format=json > dist-manifest.json
echo "dist plan ran successfully"
cat dist-manifest.json
# Create the Github Release™ based on what cargo-dist thinks it should be
ANNOUNCEMENT_TITLE=$(jq --raw-output ".announcement_title" dist-manifest.json)
IS_PRERELEASE=$(jq --raw-output ".announcement_is_prerelease" dist-manifest.json)
jq --raw-output ".announcement_github_body" dist-manifest.json > new_dist_announcement.md
gh release create ${{ github.ref_name }} --draft --prerelease="$IS_PRERELEASE" --title="$ANNOUNCEMENT_TITLE" --notes-file=new_dist_announcement.md
echo "created announcement!"
# Upload the manifest to the Github Release™
gh release upload ${{ github.ref_name }} dist-manifest.json
echo "uploaded manifest!"
# Disable all the upload-artifacts tasks if we have no actual releases
HAS_RELEASES=$(jq --raw-output ".releases != null" dist-manifest.json)
echo "has-releases=$HAS_RELEASES" >> "$GITHUB_OUTPUT"
# Build and packages all the things
upload-artifacts:
# Let the initial task tell us to not run (currently very blunt)
needs: create-release
if: ${{ needs.create-release.outputs.has-releases == 'true' }}
strategy:
matrix:
# For these target platforms
include:
- os: macos-11
dist-args: --artifacts=local --target=aarch64-apple-darwin --target=x86_64-apple-darwin
install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.sh | sh
- os: ubuntu-20.04
dist-args: --artifacts=local --target=x86_64-unknown-linux-gnu
install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.sh | sh
- os: windows-2019
dist-args: --artifacts=local --target=x86_64-pc-windows-msvc
install-dist: irm https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.ps1 | iex
runs-on: ${{ matrix.os }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v3
- name: Install Rust
run: rustup update 1.71.0 --no-self-update && rustup default 1.71.0
- name: Install cargo-dist
run: ${{ matrix.install-dist }}
- name: Run cargo-dist
# This logic is a bit janky because it's trying to be a polyglot between
# powershell and bash since this will run on windows, macos, and linux!
# 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 commands.
run: |
# Actually do builds and make zips and whatnot
cargo dist build --tag=${{ github.ref_name }} --output-format=json ${{ matrix.dist-args }} > dist-manifest.json
echo "dist ran successfully"
cat dist-manifest.json
# Parse out what we just built and upload it to the Github Release™
jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt
echo "uploading..."
cat uploads.txt
gh release upload ${{ github.ref_name }} $(cat uploads.txt)
echo "uploaded!"
# Mark the Github Release™ as a non-draft now that everything has succeeded!
publish-release:
# Only run after all the other tasks, but it's ok if upload-artifacts was skipped
needs: [create-release, upload-artifacts]
if: ${{ always() && needs.create-release.result == 'success' && (needs.upload-artifacts.result == 'skipped' || needs.upload-artifacts.result == 'success') }}
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 ${{ github.ref_name }} --draft=false

4
.gitignore vendored
View File

@@ -1,2 +1,4 @@
/target
/test_data
/Cargo.lock
/result-bin
/.bacon-locations

28
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,28 @@
## How to contribute to Deduplicator
#### **Did you find a bug?**
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/sreedevk/deduplicator/issues).
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/sreedevk/deduplicator/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
* If possible, use the [bug report template](https://github.com/sreedevk/deduplicator/blob/main/.github/ISSUE_TEMPLATE/bug_report.md) to create the issue.
#### **Would you like to write a fix for the bug?**
* Assign the Issue to yourself (if unassigned) before you start working in order to avoid any conficts.
* Open a new GitHub pull request with the patch.
* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number.
* Make sure that the PR points to the development branch.
#### **Did you fix whitespace, format code, or make a purely cosmetic patch?**
Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Deduplicator will generally not be accepted/
#### **Do you intend to add a new feature or change an existing one?**
* First open an issue with the sugggestion using the [feature request template](https://github.com/sreedevk/deduplicator/blob/main/.github/ISSUE_TEMPLATE/feature-request.md)
* Do not create a PR before one of the core contributors has conveyed acceptance for a feature request.
#### **Do you have questions about the source code?**
* If you have a question, raise an issue in the repository with a "question" label.

1125
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,58 @@
[package]
name = "deduplicator"
version = "0.0.7"
version = "0.3.0"
edition = "2021"
description = "find,filter,delete Duplicates"
description = "find,filter and delete duplicate files"
repository = "https://github.com/sreedevk/deduplicator"
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>",
]
[[bin]]
name = "deduplicator"
path = "src/main.rs"
# 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"
crossterm = "0.25.0"
fxhash = "0.2.1"
glob = "0.3.0"
humansize = "2.1.2"
itertools = "0.10.5"
dashmap = { version = "5.4.0", features = ["rayon"] }
globwalk = "0.8.1"
gxhash = { version = "3.4.1", default-features = false }
indicatif = { version = "0.17.2", features = ["rayon"] }
memmap2 = "0.5.8"
pathdiff = "0.2.1"
prettytable-rs = "0.10.0"
rand = "0.9.1"
rayon = "1.6.1"
sqlite = "0.30.3"
thiserror = "1.0.38"
tokio = { version = "1.23.0", features = ["full"] }
tui = "0.19.0"
threadpool = "1.8.1"
[profile.release]
strip = true
opt-level = 3
lto = "thin"
debug = false
codegen-units = 1
# generated by 'cargo dist init'
[profile.dist]
inherits = "release"
[workspace.metadata.dist]
rust-toolchain-version = "1.87.0"
ci = ["github"]
targets = [
"x86_64-unknown-linux-gnu",
"x86_64-apple-darwin",
"x86_64-pc-windows-msvc",
"aarch64-apple-darwin",
]
cargo-dist-version = "0.0.7"
[dev-dependencies]
tempfile = "3.20.0"

168
README.md
View File

@@ -4,45 +4,159 @@
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]
find,filter and delete duplicate files
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
-n, --nocache Don't use cache for indexing files (default = true)
-h, --help Print help information
-V, --version Print version information
-t, --types <TYPES> Filetypes to deduplicate [default = all]
-i, --interactive Delete files interactively
-m, --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
-d, --min-depth <MIN_DEPTH> Min Depth to scan while looking for duplicates
-f, --follow-links Follow links while scanning directories
-s, --strict Guarantees that two files are duplicate (performs a full hash)
-p, --progress Show Progress spinners & metrics
-h, --help Print help
-V, --version Print version
```
### 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>
## Demo
![record](https://github.com/user-attachments/assets/fcfdd9bf-4d05-41b6-a82e-367634eeaa73)
<p align="center">Currently, deduplicator is only installable via rust's cargo package manager</p>
## Installation
Currently, you can only install deduplicator using cargo package manager.
### Cargo
> GxHash relies on aes hardware acceleration, so please set `RUSTFLAGS` to `"-C target-feature=+aes"` or `"-C target-cpu=native"` before
> installing.
#### install from crates.io
```bash
$ RUSTFLAGS="-C target-cpu=native" cargo install deduplicator
```
cargo install deduplicator
#### install from git
```bash
$ RUSTFLAGS="-C target-cpu=native" cargo install deduplicator --git https://github.com/sreedevk/deduplicator
```
<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>
## Performance
Deduplicator uses size comparison and [GxHash](https://docs.rs/gxhash/latest/gxhash/) to quickly check a large number of files to find duplicates. its also heavily parallelized. The default behavior of deduplicator is to only hash the first page (4K) of the file. This is to ensure that performance is the default priority. You can modify this behavior by using the `--strict` flag which will hash the whole file and ensure that 2 files are indeed duplicates. I'll add benchmarks in future versions.
<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>
### Benchmarks
I've used hyperfine to run deduplicator on files generated by the rake file at `rakelib/benchmark.rake`. The Benchmarking accuracy can further be improved by isolating runs inside restricted docker containers. I'll include that in the future. For now, here's the hyperfine output on my i7-12800H laptop with 32G of RAM.
<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>
#### Fewer Large Files
```
# hyperfine -N --warmup 80 './target/release/deduplicator bench_artifacts'
Benchmark 1: ./target/release/deduplicator bench_artifacts
Time (mean ± σ): 2.5 ms ± 0.4 ms [User: 2.2 ms, System: 4.8 ms]
Range (min … max): 1.9 ms … 6.8 ms 1322 runs
<p align="center">
<img align="center" src="https://user-images.githubusercontent.com/36154121/211458077-90092aa3-496c-492f-a061-618059890d5f.png" width="500" height="400" />
</p>
# dust 'bench_artifacts'
37M ┌── file_1_fwds.bin │██ │ 1%
201M ├── file_0_fwds.bin │██████████ │ 8%
390M ├── file_0_fwdcbss.bin│████████████████████ │ 15%
390M ├── file_0_fwscas.bin │████████████████████ │ 15%
390M ├── file_0_fwss.bin │████████████████████ │ 15%
390M ├── file_1_fwdcbss.bin│████████████████████ │ 15%
390M ├── file_1_fwscas.bin │████████████████████ │ 15%
390M ├── file_1_fwss.bin │████████████████████ │ 15%
2.5G ┌─┴ bench_artifacts │███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████ │ 100%
```
#### Many Small Files
```
# hyperfine --warmup 20 './target/release/deduplicator bench_artifacts'
Benchmark 1: ./target/release/deduplicator bench_artifacts
Time (mean ± σ): 22.3 ms ± 1.7 ms [User: 35.8 ms, System: 46.3 ms]
Range (min … max): 18.9 ms … 27.2 ms 112 runs
# dust 'bench_artifacts'
3.9M ┌── file_992_fwss.bin │█ │ 0%
3.9M ├── file_993_fwdcbss.bin│█ │ 0%
3.9M ├── file_993_fwscas.bin │█ │ 0%
3.9M ├── file_993_fwss.bin │█ │ 0%
3.9M ├── file_994_fwdcbss.bin│█ │ 0%
3.9M ├── file_994_fwscas.bin │█ │ 0%
3.9M ├── file_994_fwss.bin │█ │ 0%
3.9M ├── file_995_fwdcbss.bin│█ │ 0%
3.9M ├── file_995_fwscas.bin │█ │ 0%
3.9M ├── file_995_fwss.bin │█ │ 0%
3.9M ├── file_996_fwdcbss.bin│█ │ 0%
3.9M ├── file_996_fwscas.bin │█ │ 0%
3.9M ├── file_996_fwss.bin │█ │ 0%
3.9M ├── file_997_fwdcbss.bin│█ │ 0%
3.9M ├── file_997_fwscas.bin │█ │ 0%
3.9M ├── file_997_fwss.bin │█ │ 0%
3.9M ├── file_998_fwdcbss.bin│█ │ 0%
3.9M ├── file_998_fwscas.bin │█ │ 0%
3.9M ├── file_998_fwss.bin │█ │ 0%
3.9M ├── file_999_fwdcbss.bin│█ │ 0%
3.9M ├── file_999_fwscas.bin │█ │ 0%
3.9M ├── file_999_fwss.bin │█ │ 0%
3.9M ├── file_99_fwdcbss.bin │█ │ 0%
3.9M ├── file_99_fwscas.bin │█ │ 0%
3.9M ├── file_99_fwss.bin │█ │ 0%
3.9M ├── file_9_fwdcbss.bin │█ │ 0%
3.9M ├── file_9_fwscas.bin │█ │ 0%
3.9M ├── file_9_fwss.bin │█ │ 0%
11G ┌─┴ bench_artifacts │█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████ │ 100%
```
## proposed
- [ ] parallelization
- [ ] (scanning + processing sw + processing hw) & formatting & printing
- [ ] scanning + processing sw + processing hw + formatting + printing
- [ ] max file path size should use the last set of duplicates
- [ ] add more unit tests
- [ ] test against different filesystems
- [ ] test against different file name encodings
- [ ] restore json output (was removed in 0.3)
- [ ] fix memory leak on very large filesystems
- [ ] maybe use a bloom filter
- [ ] reduce FileInfo size
- [ ] output in a tree format
- [ ] tui
- [ ] change the default hashing method to include the first & last page of a file (8K)
## v0.3
- [x] parallelization
- [x] (scanning) + (processing sw & processing hw & formatting & printing)
- [x] reduce cloning values on the heap
- [x] add a partial hashing mode (--strict)
- [x] add unit tests
- [x] add silent mode
- [x] update documentation
- [x] remove color output
- [x] progress bar improvements
- [x] use progress bar groups
- [x] remove broken json rendering
- [x] add benchmarks

1
Rakefile Normal file
View File

@@ -0,0 +1 @@
# tasks are contained in the `rakelib` directory

93
rakelib/benchmark.rake Normal file
View File

@@ -0,0 +1,93 @@
require 'tempfile'
require 'fileutils'
require 'securerandom'
namespace :benchmark do
file 'target/release/deduplicator' do
sh "cargo build --release"
end
task :few_large_files => 'target/release/deduplicator' do
# Benchmark 1: ./target/release/deduplicator bench_artifacts
# Time (mean ± σ): 2.5 ms ± 0.6 ms [User: 2.4 ms, System: 4.8 ms]
# Range (min … max): 1.8 ms … 9.7 ms 1474 runs
root = "bench_artifacts"
Dir.mkdir(root)
# files with same size
2.times.map do |i|
File.open(File.join(root, "file_#{i}_fwss.bin"), 'wb') do |f|
f.write(SecureRandom.bytes(4096 * 100_000))
end
end
# files with different sizes
2.times.map do |i|
File.open(File.join(root, "file_#{i}_fwds.bin"), 'wb') do |f|
f.write(SecureRandom.bytes(4096 * (rand * 100_000).ceil))
end
end
# files with same content & size
2.times.each do |i|
File.open(File.join(root, "file_#{i}_fwscas.bin"), 'wb') do |f|
f.write("\0" * (4096 * 100_000))
end
end
# files with different content but same size
2.times.each do |i|
File.open(File.join(root, "file_#{i}_fwdcbss.bin"), 'wb') do |f|
f.write(SecureRandom.bytes(4096 * 100_000))
end
end
sh("hyperfine -N --warmup 80 './target/release/deduplicator #{root}'")
sh("dust '#{root}'")
FileUtils.rm_rf(root)
end
task :many_small_files => 'target/release/deduplicator' do
# Benchmark 1: ./target/release/deduplicator bench_artifacts
# Time (mean ± σ): 10.6 ms ± 1.0 ms [User: 20.0 ms, System: 22.5 ms]
# Range (min … max): 8.4 ms … 14.2 ms 235 runs
root = "bench_artifacts"
Dir.mkdir(root)
# files with same size
1000.times.each do |i|
File.open(File.join(root, "file_#{i}_fwss.bin"), 'wb') do |f|
f.write(SecureRandom.bytes(4096 * 1000))
end
end
# files with different sizes
1000.times.each do |i|
File.open(File.join(root, "file_#{i}_fwds.bin"), 'wb') do |f|
f.write(SecureRandom.bytes(4096 * (rand * 100).ceil))
end
end
# files with same content & size
1000.times.each do |i|
File.open(File.join(root, "file_#{i}_fwscas.bin"), 'wb') do |f|
f.write("\0" * (4096 * 1000))
end
end
# files with different content but same size
1000.times.each do |i|
File.open(File.join(root, "file_#{i}_fwdcbss.bin"), 'wb') do |f|
f.write(SecureRandom.bytes(4096 * 1000))
end
end
sh("hyperfine --warmup 20 './target/release/deduplicator #{root}'")
sh("dust '#{root}'")
FileUtils.rm_rf(root)
end
end

View File

@@ -1,28 +0,0 @@
use std::time::Duration;
use anyhow::Result;
use crossterm::event::{self, KeyCode, KeyEvent};
use super::events;
pub struct EventHandler;
impl EventHandler {
pub fn init() -> Result<events::Event> {
if crossterm::event::poll(Duration::from_millis(10))? {
match event::read()? {
event::Event::Key(keycode) => Self::handle_keypress(keycode),
_ => Ok(events::Event::Noop),
}
} else {
Ok(events::Event::Noop)
}
}
fn handle_keypress(keyevent: KeyEvent) -> Result<events::Event> {
match keyevent.code {
KeyCode::Char('q') => Ok(events::Event::Exit),
_ => Ok(events::Event::Noop),
}
}
}

View File

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

View File

@@ -1,14 +0,0 @@
use crate::database::File;
use anyhow::Result;
use colored::Colorize;
pub fn delete_files(files: Vec<File>) -> Result<()> {
files.into_iter().for_each(|file| {
match std::fs::remove_file(file.path.clone()) {
Ok(_) => println!("{}: {}", "DELETED".green(), file.path),
Err(e) => println!("{}: {}", "FAILED".red(), file.path)
}
});
Ok(())
}

View File

View File

@@ -1,89 +0,0 @@
#![allow(unused)]
mod event_handler;
mod events;
mod formatter;
mod ui;
pub mod file_manager;
use std::{io, thread, time::Duration};
use anyhow::{anyhow, Result};
use crossterm::{event, execute, terminal};
use event_handler::EventHandler;
use tui::{
backend::CrosstermBackend,
widgets::{Block, Borders, Widget},
Terminal,
};
use ui::Ui;
use crate::database;
use crate::output;
use crate::params::Params;
use crate::scanner;
pub struct App;
impl App {
pub fn init(app_args: &Params) -> Result<()> {
// let mut term = Self::init_terminal()?;
let connection = database::get_connection(app_args)?;
let duplicates = scanner::duplicates(app_args, &connection)?;
// Self::init_render_loop(&mut term)?;
// Self::cleanup(&mut term)?;
match app_args.interactive {
true => output::interactive(duplicates, app_args),
false => output::print(duplicates, app_args) /* TODO: APP TUI INIT FUNCTION */
}
Ok(())
}
fn cleanup(term: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
terminal::disable_raw_mode()?;
execute!(
term.backend_mut(),
terminal::LeaveAlternateScreen,
event::DisableMouseCapture
)?;
term.show_cursor()?;
Ok(())
}
fn render_cycle(term: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
match EventHandler::init()? {
events::Event::Noop => Ui::render_frame(term),
events::Event::Exit => Err(anyhow!("Exit")),
}
}
fn init_render_loop(term: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
// this could be simplified with a `while Self::render_cycle(term).is_ok() {}` in the current state, but maybe
// it's good to keep it to handle errors in the future
loop {
match Self::render_cycle(term) {
Ok(_) => continue,
Err(_) => break,
}
}
Ok(())
}
fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
terminal::enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(
stdout,
terminal::EnterAlternateScreen,
event::EnableMouseCapture
)?;
let backend = CrosstermBackend::new(stdout);
Ok(Terminal::new(backend)?)
}
}

View File

@@ -1,54 +0,0 @@
use std::io;
use anyhow::Result;
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, List, ListItem, Widget},
Frame, Terminal,
};
pub struct Ui;
impl Ui {
fn generate_file_list() -> impl Widget {
let tasks: Vec<ListItem> = vec!["Sreedev"; 100]
.into_iter()
.map(|item| ListItem::new(vec![Spans::from(Span::raw(item))]))
.collect();
List::new(tasks)
.block(Block::default().borders(Borders::ALL).title("List"))
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol("> ")
}
fn generate_info_bar() -> impl Widget {
Block::default().title("Description").borders(Borders::ALL)
}
fn generate_file_desc() -> impl Widget {
Block::default().title("Description").borders(Borders::ALL)
}
pub fn render_frame(term: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
term.draw(|f| {
let windows = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Ratio(2, 16), Constraint::Ratio(14, 16)].as_ref())
.split(f.size());
let subwindows = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 4), Constraint::Ratio(3, 4)].as_ref())
.split(windows[1]);
f.render_widget(Self::generate_info_bar(), windows[0]);
f.render_widget(Self::generate_file_list(), subwindows[0]);
f.render_widget(Self::generate_file_desc(), subwindows[1]);
})?;
Ok(())
}
}

View File

@@ -1,89 +0,0 @@
use std::env::temp_dir;
use anyhow::Result;
use crate::params::Params;
#[derive(Debug, Clone)]
pub struct File {
pub path: String,
pub hash: String,
}
fn db_connection_url(args: &Params) -> String {
match args.nocache {
true => String::from(":memory:"),
false => {
let temp_dir_path = temp_dir();
format!("{}/deduplicator.db", temp_dir_path.display())
}
}
}
pub fn get_connection(args: &Params) -> Result<sqlite::Connection, sqlite::Error> {
sqlite::open(db_connection_url(args)).and_then(|conn| {
setup(&conn).ok();
Ok(conn)
})
}
pub fn setup(connection: &sqlite::Connection) -> Result<()> {
let query = "CREATE TABLE files (file_identifier STRING, hash STRING)";
connection.execute(query).ok();
Ok(())
}
pub fn put(file: &File, connection: &sqlite::Connection) -> Result<()> {
let query = format!(
"INSERT INTO files (file_identifier, hash) VALUES (\"{}\", \"{}\")",
file.path, file.hash
);
connection.execute(query)?;
Ok(())
}
pub fn indexed_paths(connection: &sqlite::Connection) -> Result<Vec<File>> {
let query = "SELECT * FROM files";
let result: Vec<File> = connection
.prepare(query)?
.into_iter()
.filter_map(|row_result| row_result.ok())
.map(|row| {
let path = row.read::<&str, _>("file_identifier").to_string();
let hash = row.read::<i64, _>("hash").to_string();
File { path, hash }
})
.collect();
Ok(result)
}
pub fn duplicate_hashes(connection: &sqlite::Connection, path: &str) -> Result<Vec<File>> {
let query = format!(
"
SELECT a.* FROM files a
JOIN (SELECT file_identifier, hash, COUNT(*)
FROM files
GROUP BY hash
HAVING count(*) > 1 ) b
ON a.hash = b.hash
WHERE a.file_identifier LIKE \"{}%\"
ORDER BY a.file_identifier
",
path
);
let result: Vec<File> = connection
.prepare(query)?
.into_iter()
.filter_map(|row_result| row_result.ok())
.map(|row| {
let path = row.read::<&str, _>("file_identifier").to_string();
let hash = row.read::<i64, _>("hash").to_string();
File { path, hash }
})
.collect();
Ok(result)
}

45
src/fileinfo.rs Normal file
View File

@@ -0,0 +1,45 @@
use anyhow::Result;
use gxhash::gxhash128;
use memmap2::Mmap;
use std::{
fs,
io::Read,
path::{Path, PathBuf},
time::SystemTime,
};
#[derive(Debug, Clone)]
pub struct FileInfo {
pub path: Box<Path>,
pub size: u64,
pub modified: SystemTime,
}
impl FileInfo {
pub fn hash(&self, seed: i64) -> Result<u128> {
let file = fs::File::open(&self.path)?;
let mapper = unsafe { Mmap::map(&file)? };
let final_hash = mapper.chunks(4096).fold(0u128, |acc, chunk: &[u8]| {
acc.wrapping_add(gxhash128(chunk, seed))
});
Ok(final_hash)
}
pub fn initial_page_hash(&self, seed: i64) -> Result<u128> {
let mut file = fs::File::open(&self.path)?;
let mut buffer = [0; 4096];
let bytes_read = file.read(&mut buffer)?;
Ok(gxhash128(&buffer[..bytes_read], seed))
}
pub fn new(path: PathBuf) -> Result<Self> {
let filemeta = std::fs::metadata(&path)?;
Ok(Self {
path: path.into_boxed_path(),
size: filemeta.len(),
modified: filemeta.modified()?,
})
}
}

101
src/formatter.rs Normal file
View File

@@ -0,0 +1,101 @@
use crate::{fileinfo::FileInfo, params::Params};
use anyhow::Result;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use indicatif::{ParallelProgressIterator, ProgressBar, ProgressFinish, ProgressStyle};
use pathdiff::diff_paths;
use prettytable::{format, row, Row, Table};
use rayon::prelude::*;
use std::{borrow::Cow, path::PathBuf, sync::Arc, time::Duration};
pub struct Formatter;
impl Formatter {
pub fn human_path(
file: &FileInfo,
app_args: &Params,
min_path_length: usize,
) -> Result<String> {
let base_directory: PathBuf = app_args.get_directory()?;
let relative_path = diff_paths(&file.path, base_directory).unwrap_or_default();
let formatted_path = format!(
"{:<0width$}",
relative_path.to_str().unwrap_or_default().to_string(),
width = min_path_length
);
Ok(formatted_path)
}
pub fn human_filesize(file: &FileInfo) -> Result<String> {
Ok(format!("{:>12}", bytesize::ByteSize::b(file.size)))
}
pub fn human_mtime(file: &FileInfo) -> Result<String> {
let modified_time: DateTime<Utc> = file.modified.into();
Ok(modified_time.format("%Y-%m-%d %H:%M:%S").to_string())
}
pub fn gen_sub_tbl(items: Vec<FileInfo>, app_args: &Params, max_path_len: u64) -> Table {
let mut inner_table = Table::new();
inner_table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
items.iter().for_each(|file| {
inner_table.add_row(row![
Self::human_path(file, app_args, max_path_len as usize).unwrap_or_default(),
Self::human_filesize(file).unwrap_or_default(),
Self::human_mtime(file).unwrap_or_default()
]);
});
inner_table
}
pub fn generate_table(
raw: Arc<DashMap<u128, Vec<FileInfo>>>,
mpath_len: u64,
args: &Params,
) -> Result<Table> {
let progress_bar = match args.progress {
true => ProgressBar::new_spinner(),
false => ProgressBar::hidden(),
};
let progress_style = ProgressStyle::with_template("[{elapsed_precise}] {pos:>7} {msg}")?;
progress_bar.set_style(progress_style);
progress_bar.enable_steady_tick(Duration::from_millis(50));
progress_bar.set_message("generating output");
let rows = raw
.par_iter_mut()
.progress_with(progress_bar)
.with_finish(ProgressFinish::WithMessage(Cow::from("output generated")))
.filter(|i| i.value().len() > 1)
.map(|i| {
row![
i.key(),
Self::gen_sub_tbl(i.value().to_vec(), args, mpath_len)
]
})
.collect::<Vec<Row>>();
let mut output_table = Table::new();
output_table.set_titles(row!["hash", "duplicates"]);
output_table.extend(rows);
Ok(output_table)
}
pub fn print(
raw: Arc<DashMap<u128, Vec<FileInfo>>>,
max_path_len: u64,
app_args: &Params,
) -> Result<()> {
if raw.is_empty() {
println!("\n\nNo duplicates found matching your search criteria.\n");
return Ok(());
}
let output_table = Self::generate_table(raw, max_path_len, app_args)?;
output_table.printstd();
Ok(())
}
}

125
src/interactive.rs Normal file
View File

@@ -0,0 +1,125 @@
use crate::{fileinfo::FileInfo, formatter::Formatter, params::Params};
use anyhow::Result;
use dashmap::DashMap;
use prettytable::{format, row, Table};
use std::{
io::{self, Write},
sync::Arc,
};
pub struct Interactive;
impl Interactive {
pub fn init(result: Arc<DashMap<u128, Vec<FileInfo>>>, app_args: &Params) -> Result<()> {
result
.clone()
.iter()
.filter(|i| i.value().len() > 1)
.enumerate()
.for_each(|(gindex, i)| {
let group = i.value();
let mut itable = Table::new();
itable.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
itable.set_titles(row!["index", "filename", "size", "updated_at"]);
let max_path_size = group
.iter()
.map(|f| f.path.iter().count())
.max()
.unwrap_or_default();
group.iter().enumerate().for_each(|(index, file)| {
itable.add_row(row![
index,
Formatter::human_path(file, app_args, max_path_size).unwrap_or_default(),
Formatter::human_filesize(file).unwrap_or_default(),
Formatter::human_mtime(file).unwrap_or_default()
]);
});
Self::process_group_action(group, gindex, result.len(), itable);
});
Ok(())
}
pub fn scan_group_confirmation() -> Result<bool> {
print!("\nconfirm? [y/N]: ");
std::io::stdout().flush()?;
let mut user_input = String::new();
io::stdin().read_line(&mut user_input)?;
match user_input.trim() {
"Y" | "y" => Ok(true),
_ => Ok(false),
}
}
pub fn scan_group_instruction() -> Result<String> {
println!("\nEnter the indices of the files you want to delete.");
println!("You can enter multiple files using commas to seperate file indices.");
println!("example: 1,2");
print!("\n> ");
std::io::stdout().flush()?;
let mut user_input = String::new();
io::stdin().read_line(&mut user_input)?;
Ok(user_input)
}
pub fn process_group_action(
duplicates: &Vec<FileInfo>,
dup_index: usize,
dup_size: usize,
table: Table,
) {
println!("\nDuplicate Set {} of {}\n", dup_index + 1, dup_size);
table.printstd();
let files_to_delete = Self::scan_group_instruction().unwrap_or_default();
let parsed_file_indices = files_to_delete
.trim()
.split(',')
.filter(|element| !element.is_empty())
.map(|index| index.parse::<usize>().unwrap_or_default())
.collect::<Vec<usize>>();
if parsed_file_indices
.clone()
.into_iter()
.any(|index| index > (duplicates.len() - 1))
{
println!("Err: File Index Out of Bounds!");
return Self::process_group_action(duplicates, dup_index, dup_size, table);
}
print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
if parsed_file_indices.is_empty() {
return;
}
let files_to_delete = parsed_file_indices
.into_iter()
.map(|index| duplicates[index].clone());
println!("\nThe following files will be deleted:");
files_to_delete
.clone()
.enumerate()
.for_each(|(index, file)| {
println!("{}: {}", index, file.path.display());
});
match Self::scan_group_confirmation().unwrap() {
true => {
files_to_delete.into_iter().for_each(|file| {
match std::fs::remove_file(file.path.clone()) {
Ok(_) => println!("DELETED: {}", file.path.display()),
Err(_) => println!("FAILED: {}", file.path.display()),
}
});
}
false => println!("\nCancelled Delete Operation."),
}
}
}

View File

@@ -1,15 +1,35 @@
#![allow(unused)] // TODO: remove this once TUI is implemented
mod app;
mod database;
mod output;
mod fileinfo;
mod formatter;
mod interactive;
mod params;
mod processor;
mod scanner;
mod server;
use self::{formatter::Formatter, interactive::Interactive, server::Server};
use anyhow::Result;
use app::App;
use clap::Parser;
use params::Params;
use std::sync::atomic::Ordering;
#[tokio::main]
async fn main() -> Result<()> {
App::init(&params::Params::parse())
fn main() -> Result<()> {
let app_args = Params::parse();
let server = Server::new(app_args.clone());
server.start()?;
match app_args.interactive {
false => {
Formatter::print(
server.hw_duplicate_set,
server.max_file_path_len.load(Ordering::Acquire),
&app_args,
)?;
}
true => {
Interactive::init(server.hw_duplicate_set, &app_args)?;
}
};
Ok(())
}

View File

@@ -1,165 +0,0 @@
use std::{collections::HashMap, fs, io};
use std::io::Write;
use anyhow::Result;
use chrono::offset::Utc;
use chrono::DateTime;
use colored::Colorize;
use humansize::{format_size, DECIMAL};
use itertools::Itertools;
use crate::app::file_manager;
use crate::database::File;
use crate::params::Params;
use prettytable::{format, row, Cell, Row, Table};
fn format_path(path: &str, opts: &Params) -> Result<String> {
let display_path = path.replace(&opts.get_directory()?, "");
let text_vec = display_path.chars().collect::<Vec<_>>();
let display_range = if text_vec.len() > 32 {
text_vec[(display_path.len() - 32)..]
.iter()
.collect::<String>()
} else {
display_path
};
Ok(format!("...{:<32}", display_range))
}
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 modified_time(path: &String) -> Result<String> {
let mdata = fs::metadata(path)?;
let modified_time: DateTime<Utc> = mdata.modified()?.into();
Ok(modified_time.format("%Y-%m-%d %H:%M:%S").to_string())
}
fn group_duplicates(duplicates: Vec<File>) -> HashMap<String, Vec<File>> {
let mut duplicate_mapper: HashMap<String, Vec<File>> = HashMap::new();
duplicates.into_iter().for_each(|file| {
duplicate_mapper
.entry(file.hash.clone())
.and_modify(|value| value.push(file.clone()))
.or_insert_with(|| vec![file]);
});
duplicate_mapper
}
fn print_meta_info(duplicates: &Vec<File>, opts: &Params) {
println!("Deduplicator v{}", std::env!("CARGO_PKG_VERSION"));
}
fn scan_group_instruction() -> Result<String> {
println!("\nEnter the indices of the files you want to delete.");
println!("You can enter multiple files using commas to seperate file indices.");
println!("example: 1,2");
print!("\n> ");
std::io::stdout().flush()?;
let mut user_input = String::new();
io::stdin().read_line(&mut user_input)?;
Ok(user_input)
}
fn scan_group_confirmation() -> Result<bool> {
print!("\nconfirm? [Y/n]: ");
std::io::stdout().flush()?;
let mut user_input = String::new();
io::stdin().read_line(&mut user_input)?;
match user_input.trim() {
"Y" | "y" => Ok(true),
_ => Ok(false)
}
}
fn process_group_action(duplicates: &Vec<File>, dup_index: usize, dup_size: usize, table: Table) {
println!("\nDuplicate Set {} of {}\n", dup_index + 1, dup_size);
table.printstd();
let files_to_delete = scan_group_instruction().unwrap_or_default();
let parsed_file_indices = files_to_delete
.trim()
.split(',')
.filter(|element| !element.is_empty())
.map(|index| index.parse::<usize>().unwrap_or_default())
.collect_vec();
if parsed_file_indices
.clone()
.into_iter()
.any(|index| index > (duplicates.len() - 1))
{
println!("{}", "Err: File Index Out of Bounds!".red());
return process_group_action(duplicates, dup_index, dup_size, table);
}
print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
if parsed_file_indices.is_empty() { return }
let files_to_delete = parsed_file_indices
.into_iter()
.map(|index| duplicates[index].clone());
println!("\n{}", "The following files will be deleted:".red());
files_to_delete.clone().enumerate().for_each(|(index, file)| {
println!("{}: {}", index.to_string().blue(), file.path);
});
match scan_group_confirmation().unwrap() {
true => { file_manager::delete_files(files_to_delete.collect_vec()); },
false => println!("{}", "\nCancelled Delete Operation.".red())
}
}
pub fn interactive(duplicates: Vec<File>, opts: &Params) {
print_meta_info(&duplicates, opts);
let grouped_duplicates = group_duplicates(duplicates);
grouped_duplicates.iter().enumerate().for_each(|(gindex, (hash, group))| {
let mut itable = Table::new();
itable.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
itable.set_titles(row!["index", "filename", "size", "updated_at"]);
group.iter().enumerate().for_each(|(index, file)| {
itable.add_row(row![
index,
format_path(&file.path, opts).unwrap_or_default().blue(),
file_size(&file.path).unwrap_or_default().red(),
modified_time(&file.path).unwrap_or_default().yellow()
]);
});
process_group_action(group, gindex, grouped_duplicates.len(), itable);
});
}
pub fn print(duplicates: Vec<File>, opts: &Params) {
print_meta_info(&duplicates, opts);
let mut output_table = Table::new();
let grouped_duplicates: HashMap<String, Vec<File>> = group_duplicates(duplicates);
output_table.set_titles(row!["hash", "duplicates"]);
grouped_duplicates.iter().for_each(|(hash, group)| {
let mut inner_table = Table::new();
inner_table.set_format(*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()
]);
});
output_table.add_row(row![hash.green(), inner_table]);
});
output_table.printstd();
}

View File

@@ -1,40 +1,59 @@
use std::{fs, path::PathBuf};
use anyhow::{anyhow, Result};
use clap::Parser;
use anyhow::Result;
use clap::{Parser, ValueHint};
#[derive(Parser, Debug)]
#[derive(Parser, Debug, Default, Clone)]
#[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)]
/// Run Deduplicator on dir different from pwd (e.g., ~/Pictures )
#[arg(value_hint = ValueHint::DirPath, value_name = "scan_dir_path")]
pub dir: Option<PathBuf>,
/// Don't use cache for indexing files (default = false)
#[arg(long, short)]
pub nocache: bool,
/// Delete files interactively
#[arg(long, short)]
pub interactive: bool
pub interactive: bool,
/// Minimum filesize of duplicates to scan (e.g., 100B/1K/2M/3G/4T).
#[arg(long, short = 'm', 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, short = 'd')]
pub min_depth: Option<usize>,
/// Follow links while scanning directories
#[arg(long, short)]
pub follow_links: bool,
/// Guarantees that two files are duplicate (performs a full hash)
#[arg(long, short = 's', default_value = "false")]
pub strict: bool,
/// Show Progress spinners & metrics
#[arg(long, short = 'p', default_value = "false")]
pub progress: bool,
}
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_min_size(&self) -> Option<u64> {
match &self.min_size {
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(&current_dir).as_path();
let dir = fs::canonicalize(dir_path)?;
Ok(dir)
}
pub fn get_types(&self) -> Option<String> {
self.types.clone()
}
}

369
src/processor.rs Normal file
View File

@@ -0,0 +1,369 @@
use anyhow::Result;
use dashmap::DashMap;
use indicatif::{
MultiProgress, ParallelProgressIterator, ProgressBar, ProgressFinish, ProgressStyle,
};
use rayon::prelude::{IntoParallelIterator, ParallelIterator};
use std::borrow::Cow;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, TryLockError, TryLockResult};
use std::time::Duration;
use crate::fileinfo::FileInfo;
use crate::params::Params;
pub struct Processor {}
impl Processor {
pub fn hashwise(
app_args: Arc<Params>,
sw_store: Arc<DashMap<u64, Vec<FileInfo>>>,
hw_store: Arc<DashMap<u128, Vec<FileInfo>>>,
progress_bar_box: Arc<MultiProgress>,
max_file_size: Arc<AtomicU64>,
seed: i64,
) -> Result<()> {
let progress_bar = match app_args.progress {
true => progress_bar_box.add(ProgressBar::new_spinner()),
false => ProgressBar::hidden(),
};
let keys: Vec<u64> = sw_store.clone().iter().map(|i| *i.key()).collect();
let progress_style = ProgressStyle::with_template("[{elapsed_precise}] {pos:>7} {msg}")?;
progress_bar.set_style(progress_style);
progress_bar.enable_steady_tick(Duration::from_millis(50));
progress_bar.set_message("files grouped by hash.");
keys.into_par_iter()
.progress_with(progress_bar)
.with_finish(ProgressFinish::WithMessage(Cow::from(
"files grouped by hash.",
)))
.for_each(|key| {
let group: Vec<FileInfo> = sw_store.get(&key).unwrap().to_vec();
if group.len() > 1 {
group.into_par_iter().for_each(|file| {
let fhash = if app_args.strict {
file.hash(seed).expect("hashing file failed.")
} else {
file.initial_page_hash(seed).expect("hashing file failed.")
};
Self::compare_and_update_max_path_len(
max_file_size.clone(),
file.path.to_string_lossy().len() as u64,
)
.unwrap();
hw_store
.entry(fhash)
.and_modify(|fileset| fileset.push(file.clone()))
.or_insert_with(|| vec![file]);
});
}
});
Ok(())
}
pub fn compare_and_update_max_path_len(current: Arc<AtomicU64>, next: u64) -> Result<()> {
if current.load(Ordering::Relaxed) < next {
current.store(next, Ordering::Release);
}
Ok(())
}
pub fn sizewise(
app_args: Arc<Params>,
scanner_finished: Arc<AtomicBool>,
store: Arc<DashMap<u64, Vec<FileInfo>>>,
files: Arc<Mutex<Vec<FileInfo>>>,
progress_bar_box: Arc<MultiProgress>,
) -> Result<()> {
let progress_bar = match app_args.progress {
true => progress_bar_box.add(ProgressBar::new_spinner()),
false => ProgressBar::hidden(),
};
let progress_style = ProgressStyle::with_template("[{elapsed_precise}] {pos:>7} {msg}")?;
progress_bar.set_style(progress_style);
progress_bar.enable_steady_tick(Duration::from_millis(50));
progress_bar.set_message("files grouped by size");
loop {
let fileopt: Option<FileInfo> = {
match files.try_lock() {
Ok(mut flist) => flist.pop(),
TryLockResult::Err(TryLockError::WouldBlock) => None,
_ => None,
}
};
match fileopt {
Some(file) => {
progress_bar.inc(1);
store
.entry(file.size)
.and_modify(|fileset| fileset.push(file.clone()))
.or_insert_with(|| vec![file]);
continue;
}
None => match scanner_finished.load(std::sync::atomic::Ordering::Relaxed) {
true => {
progress_bar.finish_with_message("files grouped by size");
break Ok(());
}
false => continue,
},
}
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use dashmap::DashMap;
use indicatif::MultiProgress;
use rand::Rng;
use std::fs::File;
use std::io::Write;
use std::sync::atomic::{AtomicBool, AtomicU64};
use std::sync::{Arc, Mutex};
use tempfile::TempDir;
use crate::{fileinfo::FileInfo, params::Params};
use super::Processor;
fn generate_bytes(size: usize) -> Vec<u8> {
let mut rng = rand::rng();
(0..size).map(|_| rng.random::<u8>()).collect::<Vec<u8>>()
}
#[test]
fn hashwise_sorting_two_files_with_identical_init_page_only_strict_mode() -> Result<()> {
let root = TempDir::new()?;
let content = generate_bytes(4096);
let mut content_x = content.clone();
let mut content_y = content.clone();
content_x.extend(generate_bytes(1720320));
content_y.extend(generate_bytes(1720320));
let files = [
(root.path().join("fileone.bin"), content_x),
(root.path().join("filetwo.bin"), content_y),
];
for (fpath, content) in files.iter() {
let mut f = File::create_new(fpath)?;
f.write_all(content)?;
}
let dupstore = Arc::new(DashMap::new());
let file_queue = Arc::new(Mutex::new(
files
.iter()
.map(|f| FileInfo::new(f.0.clone()).unwrap())
.collect::<Vec<FileInfo>>(),
));
let hw_dupstore = Arc::new(DashMap::new());
Processor::sizewise(
Arc::new(Params::default()),
Arc::new(AtomicBool::new(true)),
dupstore.clone(),
file_queue,
Arc::new(MultiProgress::new()),
)?;
let args = Params {
strict: true,
..Default::default()
};
Processor::hashwise(
Arc::new(args),
dupstore.clone(),
hw_dupstore.clone(),
Arc::new(MultiProgress::new()),
Arc::new(AtomicU64::new(32)),
300,
)?;
assert_eq!(hw_dupstore.len(), 2);
Ok(())
}
#[test]
fn hashwise_sorting_two_files_with_identical_init_page_only_fast_mode() -> Result<()> {
let root = TempDir::new()?;
let content = generate_bytes(4096);
let mut content_x = content.clone();
let mut content_y = content.clone();
content_x.extend(generate_bytes(1720320));
content_y.extend(generate_bytes(1720320));
let files = [
(root.path().join("fileone.bin"), content_x),
(root.path().join("filetwo.bin"), content_y),
];
for (fpath, content) in files.iter() {
let mut f = File::create_new(fpath)?;
f.write_all(content)?;
}
let dupstore = Arc::new(DashMap::new());
let file_queue = Arc::new(Mutex::new(
files
.iter()
.map(|f| FileInfo::new(f.0.clone()).unwrap())
.collect::<Vec<FileInfo>>(),
));
let hw_dupstore = Arc::new(DashMap::new());
Processor::sizewise(
Arc::new(Params::default()),
Arc::new(AtomicBool::new(true)),
dupstore.clone(),
file_queue,
Arc::new(MultiProgress::new()),
)?;
Processor::hashwise(
Arc::new(Params::default()),
dupstore.clone(),
hw_dupstore.clone(),
Arc::new(MultiProgress::new()),
Arc::new(AtomicU64::new(32)),
300,
)?;
assert_eq!(hw_dupstore.len(), 1);
Ok(())
}
#[test]
fn hashwise_sorting_two_files_with_identical_data() -> Result<()> {
let root = TempDir::new()?;
let content = generate_bytes(282624);
let files = [
(root.path().join("fileone.bin"), content.clone()),
(root.path().join("filetwo.bin"), content.clone()),
];
for (fpath, content) in files.iter() {
let mut f = File::create_new(fpath)?;
f.write_all(content)?;
}
let dupstore = Arc::new(DashMap::new());
let file_queue = Arc::new(Mutex::new(
files
.iter()
.map(|f| FileInfo::new(f.0.clone()).unwrap())
.collect::<Vec<FileInfo>>(),
));
let hw_dupstore = Arc::new(DashMap::new());
Processor::sizewise(
Arc::new(Params::default()),
Arc::new(AtomicBool::new(true)),
dupstore.clone(),
file_queue,
Arc::new(MultiProgress::new()),
)?;
Processor::hashwise(
Arc::new(Params::default()),
dupstore.clone(),
hw_dupstore.clone(),
Arc::new(MultiProgress::new()),
Arc::new(AtomicU64::new(32)),
300,
)?;
assert_eq!(hw_dupstore.len(), 1);
Ok(())
}
#[test]
fn sizewise_sorting_two_files_of_different_sizes() -> Result<()> {
let root = TempDir::new()?;
let files = [
(root.path().join("fileone.bin"), generate_bytes(282624)),
(root.path().join("filetwo.bin"), generate_bytes(1720320)),
];
for (fpath, content) in files.iter() {
let mut f = File::create_new(fpath)?;
f.write_all(content)?;
}
let file_queue = Arc::new(Mutex::new(
files
.iter()
.map(|f| FileInfo::new(f.0.clone()).unwrap())
.collect::<Vec<FileInfo>>(),
));
let dupstore = Arc::new(DashMap::new());
Processor::sizewise(
Arc::new(Params::default()),
Arc::new(AtomicBool::new(true)),
dupstore.clone(),
file_queue,
Arc::new(MultiProgress::new()),
)?;
assert_eq!(dupstore.len(), 2);
Ok(())
}
#[test]
fn sizewise_sorting_two_files_of_same_size() -> Result<()> {
let root = TempDir::new()?;
let files = [
(root.path().join("fileone.bin"), generate_bytes(282624)),
(root.path().join("filetwo.bin"), generate_bytes(282624)),
];
for (fpath, content) in files.iter() {
let mut f = File::create_new(fpath)?;
f.write_all(content)?;
}
let file_queue = Arc::new(Mutex::new(
files
.iter()
.map(|f| FileInfo::new(f.0.clone()).unwrap())
.collect::<Vec<FileInfo>>(),
));
let dupstore = Arc::new(DashMap::new());
Processor::sizewise(
Arc::new(Params::default()),
Arc::new(AtomicBool::new(true)),
dupstore.clone(),
file_queue,
Arc::new(MultiProgress::new()),
)?;
assert_eq!(dupstore.len(), 1);
Ok(())
}
}

View File

@@ -1,85 +1,100 @@
use std::{fs, path::PathBuf};
use crate::{fileinfo::FileInfo, params::Params};
use anyhow::Result;
use fxhash::hash32 as hasher;
use glob::glob;
use itertools::Itertools;
use rayon::prelude::*;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::sync::{Arc, Mutex};
use std::{path::Path, time::Duration};
use crate::{
database::{self, File},
params::Params,
};
use globwalk::{GlobWalker, GlobWalkerBuilder};
pub fn duplicates(app_opts: &Params, connection: &sqlite::Connection) -> Result<Vec<File>> {
let scan_results = scan(app_opts, connection)?;
let base_path = app_opts.get_directory()?;
index_files(scan_results, connection)?;
database::duplicate_hashes(connection, &base_path)
pub struct Scanner {
pub directory: Box<Path>,
pub filetypes: Option<String>,
pub min_depth: Option<usize>,
pub max_depth: Option<usize>,
pub min_size: Option<u64>,
pub follow_links: bool,
pub progress: bool,
}
fn get_glob_patterns(opts: &Params, directory: &str) -> Vec<PathBuf> {
opts.types
.clone()
.unwrap_or_else(|| String::from("*"))
.split(',')
.map(|filetype| format!("*.{}", filetype))
.map(|filetype| {
vec![directory.to_owned(), String::from("**"), filetype]
.iter()
.collect()
impl Scanner {
pub fn new(app_args: Arc<Params>) -> Result<Self> {
Ok(Self {
directory: app_args.get_directory()?.into_boxed_path(),
filetypes: app_args.get_types(),
min_depth: app_args.min_depth,
max_depth: app_args.max_depth,
min_size: app_args.get_min_size(),
follow_links: app_args.follow_links,
progress: app_args.progress,
})
.collect()
}
}
fn is_indexed_file(path: impl Into<String>, indexed: &[File]) -> bool {
indexed
.iter()
.map(|file| file.path.clone())
.contains(&path.into())
}
fn scan(app_opts: &Params, connection: &sqlite::Connection) -> Result<Vec<String>> {
let directory = app_opts.get_directory()?;
let glob_patterns: Vec<PathBuf> = get_glob_patterns(app_opts, &directory);
let indexed_paths = database::indexed_paths(connection)?;
let files: Vec<String> = glob_patterns
.par_iter()
.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(|fpath| !is_indexed_file(fpath, &indexed_paths))
.filter(|glob_result| {
fs::metadata(glob_result)
.map(|f| f.is_file())
.unwrap_or(false)
})
.collect::<Vec<String>>()
fn scan_patterns(&self) -> Result<String> {
Ok(match &self.filetypes {
Some(ftypes) => format!("**/*{{{ftypes}}}"),
None => "**/*".to_string(),
})
.collect();
}
Ok(files)
}
fn index_files(files: Vec<String>, connection: &sqlite::Connection) -> Result<()> {
let hashed: Vec<File> = files
.into_par_iter()
.filter_map(|file| {
let hash = hash_file(&file).ok()?;
Some(database::File { path: file, hash })
})
.collect();
hashed
.iter()
.try_for_each(|file| database::put(file, connection))
}
pub fn hash_file(filepath: &str) -> Result<String> {
let file = fs::read(filepath)?;
let hash = hasher(&*file).to_string();
Ok(hash)
fn attach_link_opts(&self, walker: GlobWalkerBuilder) -> Result<GlobWalkerBuilder> {
Ok(walker.follow_links(self.follow_links))
}
fn attach_walker_min_depth(&self, walker: GlobWalkerBuilder) -> Result<GlobWalkerBuilder> {
match self.min_depth {
Some(min_depth) => Ok(walker.min_depth(min_depth)),
None => Ok(walker),
}
}
fn attach_walker_max_depth(&self, walker: GlobWalkerBuilder) -> Result<GlobWalkerBuilder> {
match self.max_depth {
Some(max_depth) => Ok(walker.max_depth(max_depth)),
None => Ok(walker),
}
}
fn build_walker(&self) -> Result<GlobWalker> {
let walker = Ok(GlobWalkerBuilder::from_patterns(
self.directory.clone(),
&[self.scan_patterns()?],
))
.and_then(|walker| self.attach_walker_min_depth(walker))
.and_then(|walker| self.attach_walker_max_depth(walker))
.and_then(|walker| self.attach_link_opts(walker))?;
Ok(walker.build()?)
}
pub fn scan(
&self,
files: Arc<Mutex<Vec<FileInfo>>>,
progress_bar_box: Arc<MultiProgress>,
) -> Result<()> {
let progress_bar = match self.progress {
true => progress_bar_box.add(ProgressBar::new_spinner()),
false => ProgressBar::hidden(),
};
let progress_style = ProgressStyle::with_template("[{elapsed_precise}] {pos:>7} {msg}")?;
progress_bar.set_style(progress_style);
progress_bar.enable_steady_tick(Duration::from_millis(50));
progress_bar.set_message("paths mapped");
let min_size = self.min_size.unwrap_or_default();
self.build_walker()?
.filter_map(Result::ok)
.map(|entity| entity.into_path())
.inspect(|_path| progress_bar.inc(1))
.filter(|path| path.is_file())
.map(FileInfo::new)
.filter_map(Result::ok)
.filter(|file| file.size > min_size)
.for_each(|file| {
let mut flock = files.lock().unwrap();
flock.push(file);
});
progress_bar.finish_with_message("paths mapped");
Ok(())
}
}

99
src/server.rs Normal file
View File

@@ -0,0 +1,99 @@
use std::sync::atomic::{AtomicBool, AtomicU64};
use std::sync::{Arc, Mutex};
use crate::processor::Processor;
use crate::scanner::Scanner;
use anyhow::Result;
use dashmap::DashMap;
use indicatif::{MultiProgress, ProgressDrawTarget};
use rand::Rng;
use threadpool::ThreadPool;
use crate::fileinfo::FileInfo;
use crate::params::Params;
pub struct Server {
filequeue: Arc<Mutex<Vec<FileInfo>>>,
sw_duplicate_set: Arc<DashMap<u64, Vec<FileInfo>>>,
pub hw_duplicate_set: Arc<DashMap<u128, Vec<FileInfo>>>,
threadpool: ThreadPool,
app_args: Arc<Params>,
pub max_file_path_len: Arc<AtomicU64>,
}
impl Server {
pub fn new(opts: Params) -> Self {
Self {
filequeue: Arc::new(Mutex::new(Vec::new())),
sw_duplicate_set: Arc::new(DashMap::new()),
hw_duplicate_set: Arc::new(DashMap::new()),
threadpool: ThreadPool::new(4),
app_args: Arc::new(opts),
max_file_path_len: Arc::new(AtomicU64::new(0)),
}
}
pub fn start(&self) -> Result<()> {
let progbarbox = Arc::new(MultiProgress::new());
let mut rng = rand::rng();
let seed: i64 = rng.random();
if !self.app_args.progress {
progbarbox.set_draw_target(ProgressDrawTarget::hidden());
}
let app_args_clone_for_sc = self.app_args.clone();
let app_args_clone_for_pr = self.app_args.clone();
let file_queue_clone_sc = self.filequeue.clone();
let file_queue_clone_pr = self.filequeue.clone();
let scanner_finished = Arc::new(AtomicBool::new(false));
let sfin_sc_tr_cl = scanner_finished.clone();
let sfin_pr_tr_cl = scanner_finished.clone();
let store_dupl_sw_for_sw = self.sw_duplicate_set.clone();
let store_dupl_sw_for_hw = self.sw_duplicate_set.clone();
let store_dupl_hw = self.hw_duplicate_set.clone();
let max_file_path_len_clone = self.max_file_path_len.clone();
let progbarbox_sc_clone = progbarbox.clone();
self.threadpool.execute(move || {
Scanner::new(app_args_clone_for_sc)
.unwrap()
.scan(file_queue_clone_sc, progbarbox_sc_clone)
.unwrap();
sfin_sc_tr_cl.store(true, std::sync::atomic::Ordering::Relaxed);
});
let progbarbox_pr_clone = progbarbox.clone();
self.threadpool.execute(move || {
Processor::sizewise(
app_args_clone_for_pr.clone(),
sfin_pr_tr_cl,
store_dupl_sw_for_sw,
file_queue_clone_pr,
progbarbox_pr_clone.clone(),
)
.unwrap();
Processor::hashwise(
app_args_clone_for_pr,
store_dupl_sw_for_hw,
store_dupl_hw,
progbarbox_pr_clone,
max_file_path_len_clone,
seed,
)
.unwrap();
});
progbarbox.clear()?;
self.threadpool.join();
Ok(())
}
}