mirror of
https://github.com/sreedevk/deduplicator.git
synced 2026-08-26 18:15:33 +00:00
Compare commits
26 Commits
0.2.0
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9effdc9681 | ||
|
|
936c631623 | ||
|
|
0d62361132 | ||
|
|
b103d7d634 | ||
|
|
c42538a8c6 | ||
|
|
05437e0256 | ||
|
|
5942cf0b6c | ||
|
|
79c0c2be3d | ||
|
|
49b46f37f3 | ||
|
|
a37d59308f | ||
|
|
8a5b540e40 | ||
|
|
5ce0a2774a | ||
|
|
8d32ac5151 | ||
|
|
0c70e27835 | ||
|
|
53282c50df | ||
|
|
b4bf5d4fb3 | ||
|
|
028b868ea9 | ||
|
|
a56d194ee3 | ||
|
|
080cd791dc | ||
|
|
b16236763c | ||
|
|
3e407f69c8 | ||
|
|
7d386c9420 | ||
|
|
0dc681d4e7 | ||
|
|
442cb4b519 | ||
|
|
8463e72f2d | ||
|
|
05c95bb67a |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[build]
|
||||||
|
rustflags = ["-C", "target-feature=+aes,+sse2"]
|
||||||
149
.github/workflows/release.yml
vendored
149
.github/workflows/release.yml
vendored
@@ -1,12 +1,12 @@
|
|||||||
# CI that:
|
# CI that:
|
||||||
#
|
#
|
||||||
# * checks for a Git Tag that looks like a release ("v1.2.0")
|
# * checks for a Git Tag that looks like a release
|
||||||
# * creates a Github Release™️
|
# * creates a Github Release™ and fills in its text
|
||||||
# * builds binaries/packages with cargo-dist
|
# * builds artifacts with cargo-dist (executable-zips, installers)
|
||||||
# * uploads those packages to the Github Release™️
|
# * uploads those artifacts to the Github Release™
|
||||||
#
|
#
|
||||||
# Note that the Github Release™️ will be created before the packages,
|
# Note that the Github Release™ will be created before the artifacts,
|
||||||
# so there will be a few minutes where the release has no packages
|
# so there will be a few minutes where the release has no artifacts
|
||||||
# and then they will slowly trickle in, possibly failing. To make
|
# and then they will slowly trickle in, possibly failing. To make
|
||||||
# this more pleasant we mark the release as a "draft" until all
|
# this more pleasant we mark the release as a "draft" until all
|
||||||
# artifacts have been successfully uploaded. This allows you to
|
# artifacts have been successfully uploaded. This allows you to
|
||||||
@@ -17,114 +17,116 @@ name: Release
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
# This task will run whenever you push a git tag that looks like
|
# This task will run whenever you push a git tag that looks like a version
|
||||||
# a version number. We just look for `v` followed by at least one number
|
# like "v1", "v1.2.0", "v0.1.0-prerelease01", "my-app-v1.0.0", etc.
|
||||||
# and then whatever. so `v1`, `v1.0.0`, and `v1.0.0-prerelease` all work.
|
# 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 there's a prerelease-style suffix to the version then the Github Release™️
|
# If PACKAGE_NAME is specified, then we will create a Github Release™ for that
|
||||||
# will be marked as a prerelease (handled by taiki-e/create-gh-release-action).
|
# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
|
||||||
#
|
#
|
||||||
# Note that when generating links to uploaded artifacts, cargo-dist will currently
|
# If PACKAGE_NAME isn't specified, then we will create a Github Release™ for all
|
||||||
# assume that your git tag is always v{VERSION} where VERSION is the version in
|
# (cargo-dist-able) packages in the workspace with that version (this is mode is
|
||||||
# the published package's Cargo.toml (this is the default behaviour of cargo-release).
|
# intended for workspaces with only one dist-able package, or with all dist-able
|
||||||
# In the future this may be made more robust/configurable.
|
# 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:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- v[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 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:
|
outputs:
|
||||||
tag: ${{ steps.create-gh-release.outputs.computed-prefix }}${{ steps.create-gh-release.outputs.version }}
|
has-releases: ${{ steps.create-release.outputs.has-releases }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- id: create-gh-release
|
- name: Install Rust
|
||||||
uses: taiki-e/create-gh-release-action@v1
|
run: rustup update 1.71.0 --no-self-update && rustup default 1.71.0
|
||||||
with:
|
- name: Install cargo-dist
|
||||||
draft: true
|
run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.sh | sh
|
||||||
# (required) GitHub token for creating GitHub Releases.
|
- id: create-release
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
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
|
# Build and packages all the things
|
||||||
upload-artifacts:
|
upload-artifacts:
|
||||||
|
# Let the initial task tell us to not run (currently very blunt)
|
||||||
needs: create-release
|
needs: create-release
|
||||||
|
if: ${{ needs.create-release.outputs.has-releases == 'true' }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
# For these target platforms
|
# For these target platforms
|
||||||
include:
|
include:
|
||||||
- target: x86_64-unknown-linux-gnu
|
- os: macos-11
|
||||||
os: ubuntu-20.04
|
dist-args: --artifacts=local --target=aarch64-apple-darwin --target=x86_64-apple-darwin
|
||||||
install-dist: curl --proto '=https' --tlsv1.2 -L -sSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.2/installer.sh | sh
|
install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.sh | sh
|
||||||
- target: x86_64-apple-darwin
|
- os: ubuntu-20.04
|
||||||
os: macos-11
|
dist-args: --artifacts=local --target=x86_64-unknown-linux-gnu
|
||||||
install-dist: curl --proto '=https' --tlsv1.2 -L -sSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.2/installer.sh | sh
|
install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.sh | sh
|
||||||
- target: x86_64-pc-windows-msvc
|
- os: windows-2019
|
||||||
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.2/installer.ps1' | iex
|
install-dist: irm https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.ps1 | iex
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
run: rustup update stable && rustup default stable
|
run: rustup update 1.71.0 --no-self-update && rustup default 1.71.0
|
||||||
- name: Install cargo-dist
|
- name: Install cargo-dist
|
||||||
run: ${{ matrix.install-dist }}
|
run: ${{ matrix.install-dist }}
|
||||||
- name: Run cargo-dist
|
- name: Run cargo-dist
|
||||||
# This logic is a bit janky because it's trying to be a polyglot between
|
# 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!
|
# 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
|
# 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.
|
# do agree on 'cat' and '$()' so we use that to marshal values between commands.
|
||||||
run: |
|
run: |
|
||||||
# Actually do builds and make zips and whatnot
|
# Actually do builds and make zips and whatnot
|
||||||
cargo dist --target=${{ matrix.target }} --output-format=json > dist-manifest.json
|
cargo dist build --tag=${{ github.ref_name }} --output-format=json ${{ matrix.dist-args }} > dist-manifest.json
|
||||||
echo "dist ran successfully"
|
echo "dist ran successfully"
|
||||||
cat dist-manifest.json
|
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
|
# 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..."
|
echo "uploading..."
|
||||||
cat uploads.txt
|
cat uploads.txt
|
||||||
gh release upload ${{ needs.create-release.outputs.tag }} $(cat uploads.txt)
|
gh release upload ${{ github.ref_name }} $(cat uploads.txt)
|
||||||
echo "uploaded!"
|
echo "uploaded!"
|
||||||
|
|
||||||
# Compute and upload the manifest for everything
|
# Mark the Github Release™ as a non-draft now that everything has succeeded!
|
||||||
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:
|
publish-release:
|
||||||
needs: [create-release, upload-artifacts, upload-manifest]
|
# 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
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -132,5 +134,4 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: mark release as non-draft
|
- name: mark release as non-draft
|
||||||
run: |
|
run: |
|
||||||
gh release edit ${{ needs.create-release.outputs.tag }} --draft=false
|
gh release edit ${{ github.ref_name }} --draft=false
|
||||||
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
/target
|
/target
|
||||||
/Cargo.lock
|
/Cargo.lock
|
||||||
|
/.bacon-locations
|
||||||
|
/result-bin
|
||||||
|
|||||||
1065
Cargo.lock
generated
1065
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
23
Cargo.toml
23
Cargo.toml
@@ -1,8 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deduplicator"
|
name = "deduplicator"
|
||||||
version = "0.2.0"
|
version = "0.2.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "find,filter,delete Duplicates"
|
description = "find,filter,delete Duplicates"
|
||||||
|
repository = "https://github.com/sreedevk/deduplicator"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = [
|
authors = [
|
||||||
"Sreedev Kodichath <sreedevpadmakumar@gmail.com>",
|
"Sreedev Kodichath <sreedevpadmakumar@gmail.com>",
|
||||||
@@ -19,18 +20,32 @@ chrono = "0.4.23"
|
|||||||
clap = { version = "4.0.32", features = ["derive"] }
|
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"
|
|
||||||
globwalk = "0.8.1"
|
globwalk = "0.8.1"
|
||||||
|
gxhash = "3.4.1"
|
||||||
indicatif = { version = "0.17.2", features = ["rayon"] }
|
indicatif = { version = "0.17.2", features = ["rayon"] }
|
||||||
itertools = "0.10.5"
|
itertools = "0.10.5"
|
||||||
memmap2 = "0.5.8"
|
memmap2 = "0.5.8"
|
||||||
pathdiff = "0.2.1"
|
pathdiff = "0.2.1"
|
||||||
prettytable-rs = "0.10.0"
|
prettytable-rs = "0.10.0"
|
||||||
|
ratatui = "0.29.0"
|
||||||
rayon = "1.6.1"
|
rayon = "1.6.1"
|
||||||
|
serde = { version = "1.0.192", features = ["derive"] }
|
||||||
|
serde_json = "1.0.108"
|
||||||
|
tempfile = "3.20.0"
|
||||||
|
threadpool = "1.8.1"
|
||||||
unicode-segmentation = "1.10.0"
|
unicode-segmentation = "1.10.0"
|
||||||
|
uuid = { version = "1.17.0", features = ["v4"] }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
strip = true
|
||||||
|
|
||||||
# generated by 'cargo dist init'
|
# generated by 'cargo dist init'
|
||||||
[profile.dist]
|
[profile.dist]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
debug = true
|
lto = "thin"
|
||||||
split-debuginfo = "packed"
|
|
||||||
|
[workspace.metadata.dist]
|
||||||
|
rust-toolchain-version = "1.78.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"
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -21,6 +21,7 @@ Options:
|
|||||||
-f, --follow-links Follow links while scanning directories
|
-f, --follow-links Follow links while scanning directories
|
||||||
-h, --help Print help information
|
-h, --help Print help information
|
||||||
-V, --version Print version information
|
-V, --version Print version information
|
||||||
|
--json
|
||||||
```
|
```
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
@@ -47,10 +48,15 @@ deduplicator ~/Media --min-size 100mb
|
|||||||
|
|
||||||
#### Stable
|
#### Stable
|
||||||
|
|
||||||
|
> [!WARNING] Note from GxHash: GxHash relies on aes hardware acceleration, you must make sure the aes feature is enabled when building (otherwise it won't build). This can be done by setting the RUSTFLAGS environment variable to -C target-feature=+aes or -C target-cpu=native (the latter should work if your CPU is properly recognized by rustc, which is the case most of the time).
|
||||||
|
> please install version `0.2.1` if you are unable to install `0.2.2`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ cargo install deduplicator
|
$ RUSTFLAGS="-C target-cpu=native" cargo install deduplicator
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!]
|
||||||
|
|
||||||
#### Nightly
|
#### Nightly
|
||||||
|
|
||||||
if you'd like to install with nightly features, you can use
|
if you'd like to install with nightly features, you can use
|
||||||
@@ -121,3 +127,9 @@ Memory: 31731MiB (~32GiB)
|
|||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
- Tree format output for duplicate file listing
|
||||||
|
- GUI
|
||||||
|
- Packages for different operating system repositories (currently only installable via cargo)
|
||||||
|
- TUI: Improve Key Handling
|
||||||
|
|||||||
72
src/app.rs
Normal file
72
src/app.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use std::sync::mpsc::channel;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use threadpool::ThreadPool;
|
||||||
|
|
||||||
|
use crate::params::Params;
|
||||||
|
use crate::server::{Message, Server};
|
||||||
|
use crate::tui::Tui;
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
tpool: ThreadPool,
|
||||||
|
server: Arc<Server>,
|
||||||
|
app_opts: Arc<Params>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(app_opts: Arc<Params>) -> Self {
|
||||||
|
Self {
|
||||||
|
tpool: ThreadPool::new(8),
|
||||||
|
server: Arc::new(Server::new().expect("server init failed")),
|
||||||
|
app_opts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&self) -> Result<()> {
|
||||||
|
let (server_tx, server_rx) = channel::<Message>();
|
||||||
|
let (app_tx, app_rx) = channel::<Message>();
|
||||||
|
|
||||||
|
let server_ptr = self.server.clone();
|
||||||
|
let tui_server_ptr = self.server.clone();
|
||||||
|
let mut ui = Tui::new(app_tx.clone(), tui_server_ptr);
|
||||||
|
let root_dir = self
|
||||||
|
.app_opts
|
||||||
|
.get_directory()?
|
||||||
|
.into_os_string()
|
||||||
|
.into_string()
|
||||||
|
.map_err(|_| anyhow!("path to str conv failed"))?
|
||||||
|
.into_boxed_str();
|
||||||
|
|
||||||
|
self.tpool.execute(move || {
|
||||||
|
server_ptr.start(server_rx).expect("server init failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tpool.execute(move || {
|
||||||
|
ui.start().expect("ui init failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tpool.execute(move || loop {
|
||||||
|
match app_rx.try_recv() {
|
||||||
|
Ok(Message::Exit) => {
|
||||||
|
server_tx
|
||||||
|
.send(Message::Exit)
|
||||||
|
.expect("message passing to app from ui failed.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(Message::AddScanDirectory(dir)) => {
|
||||||
|
server_tx
|
||||||
|
.send(Message::AddScanDirectory(dir))
|
||||||
|
.expect("message passing to server failed.");
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app_tx.send(Message::AddScanDirectory(root_dir))?;
|
||||||
|
|
||||||
|
self.tpool.join();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/cli/mod.rs
Normal file
0
src/cli/mod.rs
Normal file
@@ -1,22 +1,25 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use gxhash::GxHasher;
|
||||||
use memmap2::Mmap;
|
use memmap2::Mmap;
|
||||||
|
use serde::Serialize;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::hash::Hasher;
|
use std::hash::Hasher;
|
||||||
use std::{fs::Metadata, path::PathBuf};
|
use std::{fs::Metadata, path::PathBuf};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct FileInfo {
|
pub struct FileInfo {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub filemeta: Metadata,
|
|
||||||
pub hash: Option<String>,
|
pub hash: Option<String>,
|
||||||
pub size: u64,
|
pub size: u64,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub filemeta: Metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileInfo {
|
impl FileInfo {
|
||||||
pub fn hash(&self) -> Result<Self> {
|
pub fn hash(&self) -> Result<Self> {
|
||||||
let file = fs::File::open(self.path.clone())?;
|
let file = fs::File::open(self.path.clone())?;
|
||||||
let mapper = unsafe { Mmap::map(&file)? };
|
let mapper = unsafe { Mmap::map(&file)? };
|
||||||
let mut primhasher = fxhash::FxHasher::default();
|
let mut primhasher = GxHasher::default();
|
||||||
|
|
||||||
mapper
|
mapper
|
||||||
.chunks(1_000_000)
|
.chunks(1_000_000)
|
||||||
|
|||||||
@@ -121,8 +121,13 @@ impl Formatter {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let output_table = Self::generate_table(raw, app_args)?;
|
if app_args.json {
|
||||||
output_table.printstd();
|
let output_json = serde_json::to_string_pretty(&raw)?;
|
||||||
|
println!("{}", output_json);
|
||||||
|
} else {
|
||||||
|
let output_table = Self::generate_table(raw, app_args)?;
|
||||||
|
output_table.printstd();
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/main.rs
33
src/main.rs
@@ -1,3 +1,4 @@
|
|||||||
|
mod app;
|
||||||
mod fileinfo;
|
mod fileinfo;
|
||||||
mod formatter;
|
mod formatter;
|
||||||
mod interactive;
|
mod interactive;
|
||||||
@@ -5,23 +6,35 @@ mod params;
|
|||||||
mod processor;
|
mod processor;
|
||||||
mod scanner;
|
mod scanner;
|
||||||
|
|
||||||
|
/* version 2.0 modules*/
|
||||||
|
mod cli;
|
||||||
|
mod server;
|
||||||
|
mod tui;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use self::app::App;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use formatter::Formatter;
|
|
||||||
use params::Params;
|
use params::Params;
|
||||||
use processor::Processor;
|
use std::sync::Arc;
|
||||||
use scanner::Scanner;
|
// use formatter::Formatter;
|
||||||
|
// use processor::Processor;
|
||||||
|
// use scanner::Scanner;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let app_args = Params::parse();
|
let app_args = Params::parse();
|
||||||
let scan_results = Scanner::build(&app_args)?.scan()?;
|
// let scan_results = Scanner::build(&app_args)?.scan()?;
|
||||||
let processor = Processor::new(scan_results);
|
// let processor = Processor::new(scan_results);
|
||||||
let results = processor.sizewise()?.hashwise()?;
|
// let results = processor.sizewise()?.hashwise()?;
|
||||||
|
|
||||||
match app_args.interactive {
|
// match app_args.interactive {
|
||||||
false => { Formatter::print(results.files, &app_args)?; }
|
// false => Formatter::print(results.files, &app_args)?,
|
||||||
true => { interactive::init(results.files, &app_args)?; }
|
// true => interactive::init(results.files, &app_args)?,
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
App::new(Arc::new(app_args))
|
||||||
|
.start()
|
||||||
|
.expect("app init failed.");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ pub struct Params {
|
|||||||
/// Follow links while scanning directories
|
/// Follow links while scanning directories
|
||||||
#[arg(long, short)]
|
#[arg(long, short)]
|
||||||
pub follow_links: bool,
|
pub follow_links: bool,
|
||||||
|
/// print json output
|
||||||
|
#[arg(long)]
|
||||||
|
pub json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Params {
|
impl Params {
|
||||||
|
|||||||
44
src/server/file.rs
Normal file
44
src/server/file.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
const PARTIAL_SIZE: u64 = 4096;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct FileMeta {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub path: Box<str>,
|
||||||
|
pub size: u64,
|
||||||
|
pub modtime: i64,
|
||||||
|
pub partial: Arc<[u8]>,
|
||||||
|
pub full_hash: Arc<[u8]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileMeta {
|
||||||
|
fn create_partial(path: &str) -> Result<Arc<[u8]>> {
|
||||||
|
let mut partial_take = File::open(path)?.take(PARTIAL_SIZE);
|
||||||
|
let mut partial_buffer = Vec::with_capacity(PARTIAL_SIZE as usize);
|
||||||
|
partial_take.read_to_end(&mut partial_buffer)?;
|
||||||
|
|
||||||
|
Ok(Arc::from(partial_buffer))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(path: Box<str>) -> Result<Self> {
|
||||||
|
let pstr = path.to_string();
|
||||||
|
let filemeta = std::fs::metadata(&pstr)?;
|
||||||
|
let partial = Self::create_partial(&pstr)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
size: filemeta.size(),
|
||||||
|
modtime: filemeta.mtime(),
|
||||||
|
full_hash: Arc::new([]),
|
||||||
|
path,
|
||||||
|
partial,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/server/mod.rs
Normal file
79
src/server/mod.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
pub mod file;
|
||||||
|
mod processor;
|
||||||
|
mod scanner;
|
||||||
|
mod store;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use threadpool::ThreadPool;
|
||||||
|
|
||||||
|
use self::processor::Processor;
|
||||||
|
use self::scanner::Scanner;
|
||||||
|
use self::store::Store;
|
||||||
|
|
||||||
|
pub type FileQueue = Arc<Mutex<Vec<Box<str>>>>;
|
||||||
|
|
||||||
|
pub enum Message {
|
||||||
|
AddScanDirectory(Box<str>),
|
||||||
|
Exit,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Server {
|
||||||
|
pub fq: FileQueue,
|
||||||
|
pub dupstore: Arc<Store>,
|
||||||
|
pub tpool: ThreadPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
fq: Arc::new(Mutex::new(vec![])),
|
||||||
|
dupstore: Arc::new(Store::new()),
|
||||||
|
tpool: ThreadPool::new(4),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&self, rx: mpsc::Receiver<Message>) -> Result<()> {
|
||||||
|
let processor_fq = self.fq.clone();
|
||||||
|
let processor_store = self.dupstore.clone();
|
||||||
|
let (processor_tx, processor_rx) = channel::<Message>();
|
||||||
|
|
||||||
|
self.tpool.execute(move || {
|
||||||
|
Processor::new(processor_fq, processor_store, processor_rx)
|
||||||
|
.process()
|
||||||
|
.expect("processer execution interrupted.");
|
||||||
|
});
|
||||||
|
|
||||||
|
let scanner_fq = self.fq.clone();
|
||||||
|
let (scanner_tx, scanner_rx) = channel::<Message>();
|
||||||
|
self.tpool.execute(move || {
|
||||||
|
Scanner::new(scanner_fq, scanner_rx)
|
||||||
|
.index()
|
||||||
|
.expect("scanner indexing interrupted.");
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tpool.execute(move || loop {
|
||||||
|
match rx.recv() {
|
||||||
|
Ok(Message::AddScanDirectory(path)) => {
|
||||||
|
scanner_tx
|
||||||
|
.send(Message::AddScanDirectory(path))
|
||||||
|
.expect("scanner tx message passing failed.");
|
||||||
|
}
|
||||||
|
Ok(Message::None) => {}
|
||||||
|
Ok(Message::Exit) | Err(_) => {
|
||||||
|
scanner_tx.send(Message::Exit).unwrap_or_default();
|
||||||
|
processor_tx.send(Message::Exit).unwrap_or_default();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tpool.join();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
146
src/server/processor.rs
Normal file
146
src/server/processor.rs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::sync::mpsc::{Receiver, TryRecvError};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::file::FileMeta;
|
||||||
|
use super::store::{Index, Store};
|
||||||
|
use super::{FileQueue, Message};
|
||||||
|
|
||||||
|
pub struct Processor {
|
||||||
|
files: FileQueue,
|
||||||
|
duplicates: Arc<Store>,
|
||||||
|
msg_rx: Receiver<Message>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Processor {
|
||||||
|
pub fn new(files: FileQueue, duplicates: Arc<Store>, msg_rx: Receiver<Message>) -> Self {
|
||||||
|
Self {
|
||||||
|
files,
|
||||||
|
duplicates,
|
||||||
|
msg_rx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process(&self) -> Result<()> {
|
||||||
|
loop {
|
||||||
|
match self.msg_rx.try_recv() {
|
||||||
|
Ok(Message::Exit) => break,
|
||||||
|
Err(TryRecvError::Empty) => {},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let next_file = {
|
||||||
|
let mut cfiles = self.files.lock().unwrap();
|
||||||
|
cfiles.pop()
|
||||||
|
};
|
||||||
|
|
||||||
|
match next_file {
|
||||||
|
None => continue,
|
||||||
|
Some(file_res) => match FileMeta::new(file_res) {
|
||||||
|
Err(_) => continue,
|
||||||
|
Ok(fm) => {
|
||||||
|
let fm_arc = Arc::new(fm);
|
||||||
|
self.duplicates
|
||||||
|
.add(Index::Size(fm_arc.size), fm_arc.clone());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::thread;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn processor_differentiates_files_with_different_sizes() -> Result<()> {
|
||||||
|
let file_queue = Arc::new(Mutex::new(vec![]));
|
||||||
|
let (tx, rx) = mpsc::channel::<Message>();
|
||||||
|
let root = TempDir::new()?;
|
||||||
|
let store = Arc::new(Store::new());
|
||||||
|
|
||||||
|
let fq_c = file_queue.clone();
|
||||||
|
let store_c = store.clone();
|
||||||
|
let proc_thread = thread::spawn(move || {
|
||||||
|
let processor = Processor::new(fq_c, store_c, rx);
|
||||||
|
processor.process().expect("processor failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
let files =
|
||||||
|
[("hello.txt", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat"),
|
||||||
|
("hello_dup.txt", "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum")];
|
||||||
|
|
||||||
|
for (filename, content) in files.into_iter() {
|
||||||
|
let fpath = root.path().join(filename);
|
||||||
|
let mut tf = File::create(fpath.clone())?;
|
||||||
|
tf.write_all(content.as_bytes())?;
|
||||||
|
|
||||||
|
let mut mfq = file_queue.lock().unwrap();
|
||||||
|
mfq.push(fpath.into_os_string().into_string().unwrap().into_boxed_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..10 {
|
||||||
|
if store.entries().len() < 2 {
|
||||||
|
thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.send(Message::Exit).expect("unable to send msg to processor");
|
||||||
|
proc_thread.join().expect("failed to join on thread!");
|
||||||
|
|
||||||
|
assert!(store.entries().len() == 2);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn processor_groups_files_with_same_size() -> Result<()> {
|
||||||
|
let file_queue = Arc::new(Mutex::new(vec![]));
|
||||||
|
let (tx, rx) = mpsc::channel::<Message>();
|
||||||
|
let root = TempDir::new()?;
|
||||||
|
let store = Arc::new(Store::new());
|
||||||
|
|
||||||
|
let fq_c = file_queue.clone();
|
||||||
|
let store_c = store.clone();
|
||||||
|
let proc_thread = thread::spawn(move || {
|
||||||
|
let processor = Processor::new(fq_c, store_c, rx);
|
||||||
|
processor.process().expect("processor failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
let files =
|
||||||
|
[("hello.txt", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum"),
|
||||||
|
("hello_dup.txt", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum")];
|
||||||
|
|
||||||
|
for (filename, content) in files.into_iter() {
|
||||||
|
let fpath = root.path().join(filename);
|
||||||
|
let mut tf = File::create(fpath.clone())?;
|
||||||
|
tf.write_all(content.as_bytes())?;
|
||||||
|
|
||||||
|
let mut mfq = file_queue.lock().unwrap();
|
||||||
|
mfq.push(fpath.into_os_string().into_string().unwrap().into_boxed_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..10 {
|
||||||
|
if store.entries().is_empty() {
|
||||||
|
thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.send(Message::Exit).expect("unable to send msg to processor");
|
||||||
|
proc_thread.join().expect("failed to join on thread!");
|
||||||
|
|
||||||
|
assert!(store.entries().len() == 1);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
128
src/server/scanner.rs
Normal file
128
src/server/scanner.rs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
use super::{FileQueue, Message};
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::fs;
|
||||||
|
use std::sync::mpsc::{self, Receiver};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Scanner {
|
||||||
|
files: FileQueue,
|
||||||
|
proc_queue: FileQueue,
|
||||||
|
msg_rx: Receiver<Message>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scanner {
|
||||||
|
pub fn new(fq: FileQueue, rx: Receiver<Message>) -> Self {
|
||||||
|
Self {
|
||||||
|
files: fq,
|
||||||
|
proc_queue: Arc::new(Mutex::new(vec![])),
|
||||||
|
msg_rx: rx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index(&self) -> Result<()> {
|
||||||
|
loop {
|
||||||
|
match self.msg_rx.try_recv() {
|
||||||
|
Ok(Message::AddScanDirectory(path)) => {
|
||||||
|
let mut mfq = self.proc_queue.lock().unwrap();
|
||||||
|
mfq.push(path);
|
||||||
|
}
|
||||||
|
Err(mpsc::TryRecvError::Empty) | Ok(Message::None) => {}
|
||||||
|
Ok(Message::Exit) | Err(_) => break,
|
||||||
|
}
|
||||||
|
|
||||||
|
let npath = {
|
||||||
|
match self.proc_queue.try_lock() {
|
||||||
|
Ok(mut q) => q.pop(),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match npath {
|
||||||
|
None => continue,
|
||||||
|
Some(path) => {
|
||||||
|
std::fs::read_dir(path.as_ref())?
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.for_each(|entry: fs::DirEntry| {
|
||||||
|
let mdata =
|
||||||
|
fs::metadata(entry.path()).expect("unable to read file metadata.");
|
||||||
|
|
||||||
|
let mpath = entry
|
||||||
|
.path()
|
||||||
|
.into_os_string()
|
||||||
|
.into_string()
|
||||||
|
.expect("invalid path conversion failed.")
|
||||||
|
.into_boxed_str();
|
||||||
|
|
||||||
|
match mdata.is_dir() {
|
||||||
|
true => {
|
||||||
|
let mut pq = self
|
||||||
|
.proc_queue
|
||||||
|
.lock()
|
||||||
|
.expect("proc queue lock acq failed.");
|
||||||
|
pq.push(mpath);
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
let mut fq = self
|
||||||
|
.files
|
||||||
|
.lock()
|
||||||
|
.expect("file queue lock acq failed.");
|
||||||
|
fq.push(mpath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scanner_scans_files() -> Result<()> {
|
||||||
|
let files =
|
||||||
|
[("hello.txt", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum"),
|
||||||
|
("hello_dup.txt", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum")];
|
||||||
|
|
||||||
|
let file_queue: FileQueue = Arc::new(Mutex::new(vec![]));
|
||||||
|
let (tx, rx) = channel::<Message>();
|
||||||
|
let root = TempDir::new()?;
|
||||||
|
|
||||||
|
for (filename, content) in files.into_iter() {
|
||||||
|
let mut tf = File::create(root.path().join(filename))?;
|
||||||
|
tf.write_all(content.as_bytes())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rpath = root.path().to_str().unwrap().to_string().into_boxed_str();
|
||||||
|
let fq_clone = file_queue.clone();
|
||||||
|
let scanner_thread = thread::spawn(move || {
|
||||||
|
let scanner = Scanner::new(fq_clone, rx);
|
||||||
|
scanner.index().unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
tx.send(Message::AddScanDirectory(rpath))?;
|
||||||
|
tx.send(Message::Exit)?;
|
||||||
|
scanner_thread.join().unwrap();
|
||||||
|
|
||||||
|
let fq_len = {
|
||||||
|
let v = file_queue.lock().unwrap();
|
||||||
|
v.len()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(files.len(), fq_len);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/server/store.rs
Normal file
35
src/server/store.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use super::file::FileMeta;
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||||
|
pub enum Index {
|
||||||
|
Size(u64),
|
||||||
|
Partial(Arc<[u8]>),
|
||||||
|
Full(Box<str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Store {
|
||||||
|
internal: Arc<DashMap<Index, Vec<Arc<FileMeta>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Store {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
internal: Arc::new(DashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn entries(&self) -> Arc<DashMap<Index, Vec<Arc<FileMeta>>>> {
|
||||||
|
self.internal.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&self, index: Index, file: Arc<FileMeta>) {
|
||||||
|
self.internal
|
||||||
|
.entry(index)
|
||||||
|
.and_modify(|fg| fg.push(file.clone()))
|
||||||
|
.or_insert(vec![file]);
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/tui/mod.rs
Normal file
99
src/tui/mod.rs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
use std::sync::mpsc::Sender;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use ratatui::crossterm::event::{self, Event, KeyCode};
|
||||||
|
use ratatui::layout::{Constraint, Direction, Layout};
|
||||||
|
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
|
||||||
|
use ratatui::{DefaultTerminal, Frame};
|
||||||
|
|
||||||
|
use crate::server::{Message, Server};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct Tui {
|
||||||
|
app_tx: Sender<Message>,
|
||||||
|
server: Arc<Server>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tui {
|
||||||
|
pub fn new(app_tx: Sender<Message>, server: Arc<Server>) -> Self {
|
||||||
|
Self { app_tx, server }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&mut self) -> Result<()> {
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
self.run(terminal).expect("ui loop failed.");
|
||||||
|
ratatui::restore();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_events() -> Result<Message> {
|
||||||
|
match event::poll(Duration::from_millis(100)).context("event polling failed.")? {
|
||||||
|
true => match event::read().context("event read failed.")? {
|
||||||
|
Event::Key(key) => match key.code {
|
||||||
|
KeyCode::Char('q') => Ok(Message::Exit),
|
||||||
|
_ => Ok(Message::None),
|
||||||
|
},
|
||||||
|
_ => Ok(Message::None),
|
||||||
|
},
|
||||||
|
false => Ok(Message::None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_events(&self) -> Result<Message> {
|
||||||
|
match Self::poll_events() {
|
||||||
|
Ok(Message::Exit) => {
|
||||||
|
self.app_tx
|
||||||
|
.send(Message::Exit)
|
||||||
|
.expect("app event send failed.");
|
||||||
|
Ok(Message::Exit)
|
||||||
|
}
|
||||||
|
_ => Ok(Message::None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self, frame: &mut Frame) {
|
||||||
|
let listitems = self
|
||||||
|
.server
|
||||||
|
.dupstore
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|val| {
|
||||||
|
let mut group = vec![ListItem::new(format!("{:?}", val.key()))];
|
||||||
|
val.value().iter().for_each(|f| {
|
||||||
|
group.push(ListItem::new(format!("|-{}", f.path)));
|
||||||
|
});
|
||||||
|
group
|
||||||
|
})
|
||||||
|
.collect::<Vec<ListItem>>();
|
||||||
|
|
||||||
|
let list = List::new(listitems.clone());
|
||||||
|
let layout_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
|
.split(frame.area());
|
||||||
|
|
||||||
|
frame.render_widget(
|
||||||
|
list.block(Block::new().borders(Borders::ALL)),
|
||||||
|
layout_chunks[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(String::new()).block(Block::new().borders(Borders::ALL)),
|
||||||
|
layout_chunks[1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
loop {
|
||||||
|
terminal.draw(|f| self.draw(f))?;
|
||||||
|
match self.handle_events() {
|
||||||
|
Ok(Message::Exit) => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user