Compare commits

..

32 Commits

Author SHA1 Message Date
andy.boot
9452049aff Increment version 2020-03-01 16:23:26 +00:00
andy.boot
09e3c27e70 Merge pull request #80 from bootandy/colours3
Rethink colors
2020-03-01 16:20:00 +00:00
andy.boot
b0bfe654c4 Rethink colors
Use LS_COLORS to print default color of each file or folder

Keep showing largest sub folder in red.
2020-03-01 16:10:22 +00:00
andy.boot
6bc44de495 Merge pull request #74 from rasmushalland/win-perf
Avoid opening all files for reading on windows
2020-03-01 14:49:47 +00:00
Rasmus Halland
efb455c739 Opening files on windows got a lot cheaper.
We avoid passing FILE_READ_DATA to CreateFile.
2020-03-01 14:41:21 +01:00
andy.boot
2c58041885 Clean up windows performance
Instead of generating random values for the drive and inode counter on
windows we return None instead
2020-03-01 14:41:21 +01:00
Rasmus Halland
c30f31c22c Avoid opening all files for reading on windows
It can be very expensive to do that, especially when it causes windows defender to read the files and scan them.
2020-03-01 14:41:21 +01:00
andy.boot
59f2cdfb84 Merge pull request #79 from NeelChotai/colours
use LS_COLORS for largest subdirectories
2020-03-01 00:59:34 +00:00
Neel Chotai
795d91237d update args descriptions 2020-03-01 00:51:36 +00:00
Neel Chotai
26ae050f16 use LS_COLORS for directories 2020-03-01 00:51:32 +00:00
Neel Chotai
58b395e7ee fix compiler warning 2020-03-01 00:49:10 +00:00
andy.boot
3beb2b5274 Merge pull request #78 from bootandy/ab-fix-build
Maybe fix windows 8 support
2020-02-29 23:06:57 +00:00
andy.boot
19d938b05e Maybe fix windows 8 support
enable_ansi_support must be run for windows 10 but is not required and
indeed fails if run on windows < 10.

Disable the expect so the code might run on windows 8

may fix:
https://github.com/bootandy/dust/issues/70
2020-02-29 11:13:13 +00:00
andy.boot
d4daa82297 Merge pull request #77 from bootandy/ab-fix-build
Ab fix build
2020-02-29 01:35:10 +00:00
andy.boot
21097578d9 Remove feature flag from build
The --features flag was empty which caused the build to fail. As the
features flag was not used it was removed.
2020-02-29 00:42:28 +00:00
andy.boot
069dc184a9 Merge pull request #73 from bootandy/readme
Update readme
2020-02-20 20:39:20 +00:00
andy.boot
02fa657128 Update readme 2020-02-20 20:36:35 +00:00
andy.boot
bc0e376c88 New Version 2020-02-19 20:22:54 +00:00
andy.boot
d7c602a2d2 Merge pull request #72 from bootandy/ab-redesign
Large redesign
2020-02-19 20:19:50 +00:00
andy.boot
603e6be7eb Large redesign
Use the whole width of the terminal assume width of 80 if none found

Show percentages in the final column. Show ASCII bars indicating usage
of the underlying directories in the space inbetween.

Display (height of terminal - 10) entries by default.

Reverse the output order so largest is at the bottom.

Break up tests. Change older tests to check real output of program.
2020-02-18 20:58:53 +00:00
andy.boot
efa469e12f Merge pull request #67 from bootandy/ab-simplify-inodes2
Simplify inodes & devices by removing Option
2020-02-10 22:11:35 +00:00
andy.boot
657858df16 Simplify inodes & devices by removing Option
This code now won't compile on none-(windows/unix family)
systems. [unix family includes mac]

Removing this method allows us to remove an Option and simplify the code
slightly
2020-02-09 14:33:46 +00:00
andy.boot
a8d700d530 Merge pull request #66 from bootandy/ab-refactor
Refactor code
2020-02-09 14:27:05 +00:00
andy.boot
c408d8887d Update docs for -X flag.
-X flag changed subtly with previous pull request. It now requires a
directory name and doesn't work on substrings.
2020-02-09 14:10:53 +00:00
andy.boot
be2250d241 Refactor: use if let instead of is_some 2020-02-09 14:10:53 +00:00
andy.boot
b6aa1378de Fix bug for devices and apparent size
If apparent_size was set and ignore files on other devices was set then
the latter flat would not work.

Fix this bug
2020-02-09 13:57:08 +00:00
andy.boot
2082141dfc Add tests for should_ignore_file function
function currently has a bug that is highlighted by second test
2020-02-09 13:57:08 +00:00
andy.boot
8a9b5e889d Refactor path depth calculation
Factor out duplicate code,
Add comment explaining why filter is necessary (thanks to rivy)
2020-02-09 13:57:08 +00:00
andy.boot
871b7e90d8 Merge pull request #64 from rivy/rf.path
Refactor ~ use Path/PathBuf instead of &str/String
2020-02-09 13:52:11 +00:00
andy.boot
edf300893c Increment version 2020-02-09 10:08:41 +00:00
Roy Ivy III
a3d8fc00e1 Refactor ~ use AsRef<Path> where possible 2020-02-03 16:56:50 -06:00
Roy Ivy III
9d4531d48b Refactor ~ use PathBuf instead of String 2020-02-03 16:56:47 -06:00
16 changed files with 988 additions and 789 deletions

View File

@@ -50,7 +50,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clippy
args: ${{ matrix.job.cargo-options }} --features "${{ matrix.job.features }}" -- -D warnings
args: ${{ matrix.job.cargo-options }} -- -D warnings
min_version:
name: MinSRV # Minimum supported rust version
@@ -193,13 +193,13 @@ jobs:
with:
use-cross: ${{ steps.vars.outputs.CARGO_USE_CROSS }}
command: build
args: --release --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} --features "${{ matrix.job.features }}"
args: --release --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }}
- name: Test
uses: actions-rs/cargo@v1
with:
use-cross: ${{ steps.vars.outputs.CARGO_USE_CROSS }}
command: test
args: --target=${{ matrix.job.target }} ${{ steps.vars.outputs.CARGO_TEST_OPTIONS}} ${{ matrix.job.cargo-options }} --features "${{ matrix.job.features }}"
args: --target=${{ matrix.job.target }} ${{ steps.vars.outputs.CARGO_TEST_OPTIONS}} ${{ matrix.job.cargo-options }}
- name: Archive executable artifacts
uses: actions/upload-artifact@master
with:

View File

@@ -1,83 +0,0 @@
# Based on the "trust" template v0.1.2
# https://github.com/japaric/trust/tree/v0.1.2
# ----------- To do a release ---------
# tag a commit and push:
# git tag v0.4.0.1
# git push origin v0.4.0.1
# Remember to do a cargo publish to put it in crates.io
dist: trusty
language: rust
services: docker
sudo: required
# TODO Rust builds on stable by default, this can be
# overridden on a case by case basis down below.
env:
global:
# TODO Update this to match the name of your project.
- CRATE_NAME=dust
matrix:
# TODO These are all the build jobs. Adjust as necessary. Comment out what you
# don't need
include:
# Linux
- env: TARGET=x86_64-unknown-linux-gnu
# OSX
- env: TARGET=x86_64-apple-darwin
os: osx
before_install:
- set -e
- rustup self update
install:
- sh ci/install.sh
- source ~/.cargo/env || true
script:
- bash ci/script.sh
after_script: set +e
before_deploy:
- sh ci/before_deploy.sh
deploy:
# TODO update `api_key.secure`
# - Create a `public_repo` GitHub token. Go to: https://github.com/settings/tokens/new
# - Encrypt it: `travis encrypt 0123456789012345678901234567890123456789
# - Paste the output down here
# api_key:
# secure: UlU73Td7Bkb2N88ws4YGLWR+4U0IMgiou9QQtMnmpouJFjeUNxtLSPMPODVXP7zq4sKt5HR5B3fX9MW4mKm351fvnQEoihETn06pKiXGnY//SlTPTt67MX9ZOYmd9ohJReMDOZDgqhnGLxfymycGtsLAmdjDZnAl+IMqgg0FMyVFj9Cl9aKxnn12lxQyX4zabHKk8TUKD3By8ZoEUnJMHt3gEtOmbDgS4brcTPeHCzqnYFw73LEnkqvz+JP0XwauJY7Cf8lminKm/klmjCkQji8T9SHI52v1g0Fxpx0ucp2o3vulQrLHXaHvZ6Fr7J0cSXXzaFF3rrGLt4t4jU/+9TZm1+n5k5XuPW4x4NTCC9NmIj/z0/z41t82E9qZhzhtm2Jdsg6H2tNk+C774TYqcmR6GCvfRadfjRp3cA5dh0UwDVjH2MJFxlHDVkl6la0mVVRsCGF3oBKZVk0BDl1womfnmI46o/uU+gLknHN6Ed6PHHPPYDViWd3VKdmHKT7XrkMMUF6HjZUtla689DWIOWZSiV++1dVPcl/1TV+6tTmN4bBtPcLuX7SHRuLp2PI2kATvRMECsa7gZRypW4jKpVn7b2yetX9TVI3i1zR5zkQJ3dPg8sATvYPL53aKH/WsqUg4rzoAlbk9so+++R4bQY69LhV3B511B7EAynoZFdM
api_key: $API_KEY
file_glob: true
file: $CRATE_NAME-$TRAVIS_TAG-$TARGET.*
on:
# TODO Here you can pick which targets will generate binary releases
# In this example, there are some targets that are tested using the stable
# and nightly channels. This condition makes sure there is only one release
# for such targets and that's generated using the stable channel
condition: $TRAVIS_RUST_VERSION = stable
tags: true
provider: releases
skip_cleanup: true
cache: cargo
before_cache:
# Travis can't cache files that are not readable by "others"
- chmod -R a+r $HOME/.cargo
branches:
only:
# release tags
- /^v\d+\.\d+\.\d+.*$/
- master
notifications:
email:
on_success: never

50
Cargo.lock generated
View File

@@ -26,7 +26,7 @@ dependencies = [
"environment 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.47 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -46,7 +46,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "backtrace"
version = "0.3.42"
version = "0.3.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -180,14 +180,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "du-dust"
version = "0.4.4"
version = "0.5.1"
dependencies = [
"ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)",
"assert_cli 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
"jwalk 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lscolors 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"terminal_size 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -206,7 +209,7 @@ name = "failure"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)",
"backtrace 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)",
"failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -217,7 +220,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)",
"synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -241,7 +244,7 @@ dependencies = [
[[package]]
name = "itoa"
version = "0.4.4"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -263,6 +266,14 @@ name = "libc"
version = "0.2.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "lscolors"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "memoffset"
version = "0.5.3"
@@ -416,10 +427,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde_json"
version = "1.0.44"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
"ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -431,7 +442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "syn"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -446,7 +457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -463,6 +474,15 @@ dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "terminal_size"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "textwrap"
version = "0.11.0"
@@ -524,7 +544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum assert_cli 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a29ab7c0ed62970beb0534d637a8688842506d0ff9157de83286dacd065c8149"
"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
"checksum backtrace 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b4b1549d804b6c73f4817df2ba073709e96e426f12987127c48e6745568c350b"
"checksum backtrace 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)" = "e4036b9bf40f3cf16aba72a3d65e8a520fc4bafcdc7079aea8f848c58c5b5536"
"checksum backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491"
"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
"checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb"
@@ -545,10 +565,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0bc225b78e0391e4b8683440bf2e63c2deeeb2ce5189eab46e2b68c6d3725d08"
"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
"checksum hermit-abi 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772"
"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
"checksum jwalk 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2b3dbf0a8f61baee43a2918ff50ac6a2d3b2c105bc08ed53bc298779f1263409"
"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
"checksum lscolors 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ea3b3414b2d015c4fd689815f2551797f3c2296bb241dd709c7da233ec7cba4b"
"checksum memoffset 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9"
"checksum num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6"
"checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
@@ -569,11 +590,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
"checksum serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449"
"checksum serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)" = "48c575e0cc52bdd09b47f330f646cf59afc586e9c4e3ccd6fc1f625b8ea1dad7"
"checksum serde_json 1.0.47 (registry+https://github.com/rust-lang/crates.io-index)" = "15913895b61e0be854afd32fd4163fcd2a3df34142cf2cb961b310ce694cbf90"
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
"checksum syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1e4ff033220a41d1a57d8125eab57bf5263783dfdcc18688b1dacc6ce9651ef8"
"checksum syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "af6f3550d8dff9ef7dc34d384ac6f107e5d31c8f57d9f28e0081503f547ac8f5"
"checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
"checksum terminal_size 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "e25a60e3024df9029a414be05f46318a77c22538861a22170077d0388c0e926e"
"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"

View File

@@ -1,7 +1,7 @@
[package]
name = "du-dust"
description = "A more intuitive version of du"
version = "0.4.4"
version = "0.5.1"
authors = ["bootandy <bootandy@gmail.com>", "nebkor <code@ardent.nebcorp.com>"]
edition = "2018"
@@ -24,7 +24,10 @@ path = "src/main.rs"
ansi_term = "=0.12"
clap = "=2.33"
jwalk = "0.4.0"
lscolors = "0.6.0"
num_cpus = "1.12"
terminal_size = "0.1.10"
unicode-width = "0.1.7"
[target.'cfg(windows)'.dependencies]
winapi-util = "0.1"
@@ -32,3 +35,8 @@ winapi-util = "0.1"
[dev-dependencies]
assert_cli = "=0.6"
tempfile = "=3"
[[test]]
name = "integration"
path = "tests/tests.rs"

View File

@@ -3,31 +3,32 @@
# Dust
du + rust = dust. Like du but more intuitive
du + rust = dust. Like du but more intuitive.
# Why
Because I want an easy way to see where my disk is being used.
# Demo
![Example](media/snap.png)
## Install
#### Cargo Install
#### Cargo
* cargo install du-dust
* `cargo install du-dust`
#### Download Install
#### Download
* Download linux / mac binary from [Releases](https://github.com/bootandy/dust/releases)
* unzip file: tar -xvf _downloaded_file.tar.gz_
* move file to executable path: sudo mv dust /usr/local/bin/
* Download Linux/Mac binary from [Releases](https://github.com/bootandy/dust/releases)
* unzip file: `tar -xvf _downloaded_file.tar.gz`
* move file to executable path: `sudo mv dust /usr/local/bin/`
## Overview
Dust is meant to give you an instant overview of which directories are using disk space without requiring sort or head. Dust will print a maximum of 1 'Did not have permissions message'.
Dust is meant to give you an instant overview of which directories are using disk space without requiring sort or head. Dust will print a maximum of one 'Did not have permissions message'.
Dust will list the 20 biggest sub directories or files and will smartly recurse down the tree to find the larger ones. There is no need for a '-d' flag or a '-h' flag. The largest sub directory will have its size shown in *red*
## Why?
du has a number of ways of showing you what it finds, in terms of disk consumption, but really, there are only one or two ways you invoke it: with -h for “human readable” units, like 100G or 89k, or with -b for “bytes”. The former is generally used for a quick survey of a directory with a small number of things in it, and the latter for when you have a bunch and need to sort the output numerically, and youre obligated to either further pass it into something like awk to turn bytes into the appropriate human-friendly unit like mega or gigabytes, or pipe thru sort and head while remembering the '-h' flag. Then once you have the top offenders, you recurse down into the largest one and repeat the process until youve found your cruft or gems and can move on.
Dust assumes thats what you wanted to do in the first place, and takes care of tracking the largest offenders in terms of actual size, and showing them to you with human-friendly units and in-context within the filetree.
Dust will list a slightly-less-than-the-terminal-height number of the biggest subdirectories or files and will smartly recurse down the tree to find the larger ones. There is no need for a '-d' flag or a '-h' flag. The largest subdirectories will be colored.
## Usage
@@ -37,31 +38,12 @@ Usage: dust <dir>
Usage: dust <dir> <another_dir> <and_more>
Usage: dust -p <dir> (full-path - does not shorten the path of the subdirectories)
Usage: dust -s <dir> (apparent-size - shows the length of the file as opposed to the amount of disk space it uses)
Usage: dust -n 30 <dir> (Shows 30 directories not 20)
Usage: dust -d 3 <dir> (Shows 3 levels of subdirectories)
Usage: dust -r <dir> (Reverse order of output, with root at the lowest)
Usage: dust -x <dir> (Only show directories on same filesystem)
Usage: dust -X ignore <dir> (Ignore all files and directories containing the string 'ignore')
```
```
djin:git/dust> dust
1.2G target
622M ├─┬ debug
445M │ ├── deps
70M │ ├── incremental
56M │ └── build
262M ├─┬ rls
262M │ └─┬ debug
203M │ ├── deps
56M │ └── build
165M ├─┬ package
165M │ └─┬ du-dust-0.2.4
165M │ └─┬ target
165M │ └─┬ debug
131M │ └── deps
165M └─┬ release
124M └── deps
Usage: dust -n 30 <dir> (shows 30 directories instead of the default)
Usage: dust -d 3 <dir> (shows 3 levels of subdirectories)
Usage: dust -r <dir> (reverse order of output, with root at the lowest)
Usage: dust -x <dir> (only show directories on the same filesystem)
Usage: dust -X ignore <dir> (ignore all files and directories with the name 'ignore')
Usage: dust -b <dir> (do not show percentages or draw ASCII bars)
```

6
ci/how2publish.txt Normal file
View File

@@ -0,0 +1,6 @@
# ----------- To do a release ---------
# tag a commit and push:
# git tag v0.4.5
# git push origin v0.4.5
# cargo publish to put it in crates.io

BIN
media/snap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -1,35 +1,38 @@
extern crate ansi_term;
use self::ansi_term::Colour::Fixed;
use self::ansi_term::Style;
use crate::utils::Node;
use self::ansi_term::Colour::Fixed;
use lscolors::{LsColors, Style};
use terminal_size::{terminal_size, Height, Width};
use unicode_width::UnicodeWidthStr;
use std::cmp::max;
use std::cmp::min;
use std::fs;
use std::iter::repeat;
use std::path::Path;
static UNITS: [char; 4] = ['T', 'G', 'M', 'K'];
static BLOCKS: [char; 5] = ['█', '▓', '▒', '░', ' '];
static DEFAULT_TERMINAL_WIDTH: u16 = 80;
pub struct DisplayData {
pub short_paths: bool,
pub is_reversed: bool,
pub colors_on: bool,
pub base_size: u64,
pub longest_string_length: usize,
pub ls_colors: LsColors,
}
impl DisplayData {
fn get_first_chars(&self) -> &str {
if self.is_reversed {
"─┴"
} else {
"─┬"
}
}
#[allow(clippy::collapsible_if)]
fn get_tree_chars(
&self,
num_siblings: u64,
max_siblings: u64,
has_children: bool,
) -> &'static str {
fn get_tree_chars(&self, was_i_last: bool, has_children: bool) -> &'static str {
if self.is_reversed {
if num_siblings == max_siblings - 1 {
if was_i_last {
if has_children {
"┌─┴"
} else {
@@ -41,7 +44,7 @@ impl DisplayData {
"├──"
}
} else {
if num_siblings == 0 {
if was_i_last {
if has_children {
"└─┬"
} else {
@@ -55,22 +58,82 @@ impl DisplayData {
}
}
fn is_biggest(&self, num_siblings: u64, max_siblings: u64) -> bool {
fn is_biggest(&self, num_siblings: usize, max_siblings: u64) -> bool {
if self.is_reversed {
num_siblings == 0
num_siblings == (max_siblings - 1) as usize
} else {
num_siblings == max_siblings - 1
num_siblings == 0
}
}
fn get_children_from_node(&self, node: Node) -> impl Iterator<Item = Node> {
fn is_last(&self, num_siblings: usize, max_siblings: u64) -> bool {
if self.is_reversed {
let n: Vec<Node> = node.children.into_iter().rev().map(|a| a).collect();
n.into_iter()
num_siblings == 0
} else {
node.children.into_iter()
num_siblings == (max_siblings - 1) as usize
}
}
fn percent_size(&self, node: &Node) -> f32 {
let result = node.size as f32 / self.base_size as f32;
if result.is_normal() {
result
} else {
0.0
}
}
}
fn get_children_from_node(node: Node, is_reversed: bool) -> impl Iterator<Item = Node> {
if is_reversed {
let n: Vec<Node> = node.children.into_iter().rev().map(|a| a).collect();
n.into_iter()
} else {
node.children.into_iter()
}
}
struct DrawData<'a> {
indent: String,
percent_bar: String,
display_data: &'a DisplayData,
}
impl DrawData<'_> {
fn get_new_indent(&self, has_children: bool, was_i_last: bool) -> String {
let chars = self.display_data.get_tree_chars(was_i_last, has_children);
self.indent.to_string() + chars
}
fn generate_bar(&self, node: &Node, level: usize) -> String {
let chars_in_bar = self.percent_bar.chars().count();
let num_bars = chars_in_bar as f32 * self.display_data.percent_size(node);
let mut num_not_my_bar = (chars_in_bar as i32) - num_bars as i32;
let mut new_bar = "".to_string();
let idx = 5 - min(5, max(1, level));
for c in self.percent_bar.chars() {
num_not_my_bar -= 1;
if num_not_my_bar <= 0 {
new_bar.push(BLOCKS[0]);
} else if c == BLOCKS[0] {
new_bar.push(BLOCKS[idx]);
} else {
new_bar.push(c);
}
}
new_bar
}
}
fn get_width_of_terminal() -> u16 {
// Windows CI runners detect a very low terminal width
if let Some((Width(w), Height(_h))) = terminal_size() {
max(w, DEFAULT_TERMINAL_WIDTH)
} else {
DEFAULT_TERMINAL_WIDTH
}
}
pub fn draw_it(
@@ -78,44 +141,93 @@ pub fn draw_it(
use_full_path: bool,
is_reversed: bool,
no_colors: bool,
no_percents: bool,
root_node: Node,
) {
if !permissions {
eprintln!("Did not have permissions for all directories");
}
let display_data = DisplayData {
short_paths: !use_full_path,
is_reversed,
colors_on: !no_colors,
let longest_string_length = root_node
.children
.iter()
.map(|c| find_longest_dir_name(&c, " ", !use_full_path))
.fold(0, max);
let terminal_width = get_width_of_terminal() - 16;
let max_bar_length = if no_percents || longest_string_length >= terminal_width as usize {
0
} else {
terminal_width as usize - longest_string_length
};
for c in display_data.get_children_from_node(root_node) {
let first_tree_chars = display_data.get_first_chars();
display_node(c, true, first_tree_chars, &display_data)
// handle usize error also add do not show fancy output option
let bar_text = repeat(BLOCKS[0]).take(max_bar_length).collect::<String>();
for c in get_children_from_node(root_node, is_reversed) {
let display_data = DisplayData {
short_paths: !use_full_path,
is_reversed,
colors_on: !no_colors,
base_size: c.size,
longest_string_length,
ls_colors: LsColors::from_env().unwrap_or_default(),
};
let draw_data = DrawData {
indent: "".to_string(),
percent_bar: bar_text.clone(),
display_data: &display_data,
};
display_node(c, &draw_data, true, true);
}
}
fn display_node(node: Node, is_biggest: bool, indent: &str, display_data: &DisplayData) {
let mut num_siblings = node.children.len() as u64;
let max_sibling = num_siblings;
let new_indent = clean_indentation_string(indent);
let name = node.name.clone();
let size = node.size;
// We can probably pass depth instead of indent here.
// It is ugly to feed in ' ' instead of the actual tree characters but we don't need them yet.
fn find_longest_dir_name(node: &Node, indent: &str, long_paths: bool) -> usize {
let printable_name = get_printable_name(&node.name, long_paths);
let longest = get_unicode_width_of_indent_and_name(indent, &printable_name);
if !display_data.is_reversed {
print_this_node(&*name, size, is_biggest, display_data, indent);
// each none root tree drawing is 2 chars
let full_indent: String = indent.to_string() + " ";
node.children
.iter()
.map(|c| find_longest_dir_name(c, &*full_indent, long_paths))
.fold(longest, max)
}
fn display_node(node: Node, draw_data: &DrawData, is_biggest: bool, is_last: bool) {
let indent2 = draw_data.get_new_indent(!node.children.is_empty(), is_last);
// hacky way of working out how deep we are in the tree
let level = ((indent2.chars().count() - 1) / 2) - 1;
let bar_text = draw_data.generate_bar(&node, level);
let to_print = format_string(
&node,
&*indent2,
&*bar_text,
is_biggest,
draw_data.display_data,
);
if !draw_data.display_data.is_reversed {
println!("{}", to_print)
}
for c in display_data.get_children_from_node(node) {
num_siblings -= 1;
let chars = display_data.get_tree_chars(num_siblings, max_sibling, !c.children.is_empty());
let is_biggest = display_data.is_biggest(num_siblings, max_sibling);
let full_indent = new_indent.clone() + chars;
display_node(c, is_biggest, &*full_indent, display_data)
let dd = DrawData {
indent: clean_indentation_string(&*indent2),
percent_bar: bar_text,
display_data: draw_data.display_data,
};
let num_siblings = node.children.len() as u64;
for (count, c) in get_children_from_node(node, draw_data.display_data.is_reversed).enumerate() {
let is_biggest = dd.display_data.is_biggest(count, num_siblings);
let was_i_last = dd.display_data.is_last(count, num_siblings);
display_node(c, &dd, is_biggest, was_i_last);
}
if display_data.is_reversed {
print_this_node(&*name, size, is_biggest, display_data, indent);
if draw_data.display_data.is_reversed {
println!("{}", to_print)
}
}
@@ -136,47 +248,74 @@ fn clean_indentation_string(s: &str) -> String {
is
}
fn print_this_node(
name: &str,
size: u64,
is_biggest: bool,
display_data: &DisplayData,
indentation: &str,
) {
let pretty_size = format!("{:>5}", human_readable_number(size),);
println!(
"{}",
format_string(name, is_biggest, display_data, &*pretty_size, indentation)
)
}
pub fn format_string(
dir_name: &str,
is_biggest: bool,
display_data: &DisplayData,
size: &str,
indentation: &str,
) -> String {
fn get_printable_name<P: AsRef<Path>>(dir_name: &P, long_paths: bool) -> String {
let dir_name = dir_name.as_ref();
let printable_name = {
if display_data.short_paths {
dir_name
.split(std::path::is_separator)
.last()
.unwrap_or(dir_name)
if long_paths {
match dir_name.parent() {
Some(prefix) => match dir_name.strip_prefix(prefix) {
Ok(base) => base,
Err(_) => dir_name,
},
None => dir_name,
}
} else {
dir_name
}
};
format!(
"{} {} {}",
if is_biggest && display_data.colors_on {
Fixed(196).paint(size)
} else {
Style::new().paint(size)
},
indentation,
printable_name,
)
printable_name.display().to_string()
}
fn get_unicode_width_of_indent_and_name(indent: &str, name: &str) -> usize {
let indent_and_name = format!("{} {}", indent, name);
UnicodeWidthStr::width(&*indent_and_name)
}
pub fn format_string(
node: &Node,
indent: &str,
percent_bar: &str,
is_biggest: bool,
display_data: &DisplayData,
) -> String {
let pretty_size = format!("{:>5}", human_readable_number(node.size));
let percent_size_str = format!("{:.0}%", display_data.percent_size(node) * 100.0);
let name = get_printable_name(&node.name, display_data.short_paths);
let width = get_unicode_width_of_indent_and_name(indent, &name);
let name_and_padding = name
+ &(repeat(" ")
.take(display_data.longest_string_length - width)
.collect::<String>());
let percents = if percent_bar != "" {
format!("{}{:>4}", percent_bar, percent_size_str)
} else {
"".into()
};
let pretty_size = if is_biggest && display_data.colors_on {
format!("{}", Fixed(196).paint(pretty_size))
} else {
pretty_size
};
let pretty_name = if display_data.colors_on {
let meta_result = fs::metadata(node.name.clone());
let directory_color = display_data
.ls_colors
.style_for_path_with_metadata(node.name.clone(), meta_result.as_ref().ok());
let ansi_style = directory_color
.map(Style::to_ansi_term_style)
.unwrap_or_default();
format!("{}", ansi_style.paint(name_and_padding))
} else {
name_and_padding
};
format!("{} {} {}{}", pretty_size, indent, pretty_name, percents)
}
fn human_readable_number(size: u64) -> String {

View File

@@ -1,27 +1,53 @@
#[macro_use]
extern crate clap;
extern crate unicode_width;
use self::display::draw_it;
use crate::utils::is_a_parent_of;
use clap::{App, AppSettings, Arg};
use std::cmp::max;
use std::path::PathBuf;
use terminal_size::{terminal_size, Height, Width};
use utils::{find_big_ones, get_dir_tree, simplify_dir_names, sort, trim_deep_ones, Node};
mod display;
mod utils;
static DEFAULT_NUMBER_OF_LINES: usize = 20;
static DEFAULT_NUMBER_OF_LINES: usize = 30;
#[cfg(windows)]
fn init_color() {
ansi_term::enable_ansi_support().expect("Couldn't enable color support");
fn init_color(no_color: bool) -> bool {
// Required for windows 10
// Fails to resolve for windows 8 so disable color
match ansi_term::enable_ansi_support() {
Ok(_) => no_color,
Err(_) => {
eprintln!(
"This version of Windows does not support ANSI colors, setting no_color flag"
);
true
}
}
}
#[cfg(not(windows))]
fn init_color() {}
fn init_color(no_color: bool) -> bool {
no_color
}
fn get_height_of_terminal() -> usize {
// Windows CI runners detect a terminal height of 0
if let Some((Width(_w), Height(h))) = terminal_size() {
max(h as usize, DEFAULT_NUMBER_OF_LINES) - 10
} else {
DEFAULT_NUMBER_OF_LINES - 10
}
}
fn main() {
init_color();
let def_num_str = DEFAULT_NUMBER_OF_LINES.to_string();
let default_height = get_height_of_terminal();
let def_num_str = default_height.to_string();
let options = App::new("Dust")
.about("Like du but more intuitive")
.version(crate_version!())
@@ -52,7 +78,7 @@ fn main() {
Arg::with_name("display_full_paths")
.short("p")
.long("full-paths")
.help("If set sub directories will not have their path shortened"),
.help("Subdirectories will not have their path shortened"),
)
.arg(
Arg::with_name("ignore_directory")
@@ -61,31 +87,37 @@ fn main() {
.takes_value(true)
.number_of_values(1)
.multiple(true)
.help("Exclude any file or directory with contains this substring."),
.help("Exclude any file or directory with this name"),
)
.arg(
Arg::with_name("limit_filesystem")
.short("x")
.long("limit-filesystem")
.help("Only count the files and directories in the same filesystem as the supplied directory"),
.help("Only count the files and directories on the same filesystem as the supplied directory"),
)
.arg(
Arg::with_name("display_apparent_size")
.short("s")
.long("apparent-size")
.help("If set will use file length. Otherwise we use blocks"),
.help("Use file length instead of blocks"),
)
.arg(
Arg::with_name("reverse")
.short("r")
.long("reverse")
.help("If applied tree will be printed upside down (biggest lowest)"),
.help("Print tree upside down (biggest highest)"),
)
.arg(
Arg::with_name("no_colors")
.short("c")
.long("no_colors")
.help("If applied no colors will be printed (normally largest directories are marked in red"),
.long("no-colors")
.help("No colors will be printed (normally largest directories are colored)"),
)
.arg(
Arg::with_name("no_bars")
.short("b")
.long("no-percent-bars")
.help("No percent bars or percentages will be displayed"),
)
.arg(Arg::with_name("inputs").multiple(true))
.get_matches();
@@ -101,7 +133,7 @@ fn main() {
Ok(v) => v,
Err(_) => {
eprintln!("Ignoring bad value for number_of_lines");
DEFAULT_NUMBER_OF_LINES
default_height
}
};
@@ -130,19 +162,23 @@ fn main() {
.map_err(|_| eprintln!("Ignoring bad value for depth"))
.ok()
});
if options.is_present("depth") && number_of_lines != DEFAULT_NUMBER_OF_LINES {
if options.is_present("depth") && number_of_lines != default_height {
eprintln!("Use either -n or -d. Not both");
return;
}
let no_colors = init_color(options.is_present("no_colors"));
let use_apparent_size = options.is_present("display_apparent_size");
let limit_filesystem = options.is_present("limit_filesystem");
let ignore_directories = options.values_of("ignore_directory").map(|r| r.collect());
let ignore_directories = match options.values_of("ignore_directory") {
Some(i) => Some(i.map(PathBuf::from).collect()),
None => None,
};
let simplified_dirs = simplify_dir_names(target_dirs);
let (permissions, nodes) = get_dir_tree(
&simplified_dirs,
ignore_directories,
&ignore_directories,
use_apparent_size,
limit_filesystem,
threads,
@@ -159,13 +195,14 @@ fn main() {
draw_it(
permissions,
options.is_present("display_full_paths"),
options.is_present("reverse"),
options.is_present("no_colors"),
!options.is_present("reverse"),
no_colors,
options.is_present("no_bars"),
tree,
);
}
fn build_tree(biggest_ones: Vec<(String, u64)>, depth: Option<u64>) -> Node {
fn build_tree(biggest_ones: Vec<(PathBuf, u64)>, depth: Option<u64>) -> Node {
let mut top_parent = Node::default();
// assume sorted order
@@ -196,6 +233,3 @@ fn recursively_build_tree(parent_node: &mut Node, new_node: Node, depth: Option<
parent_node.children.push(new_node);
}
}
#[cfg(test)]
mod tests;

View File

View File

@@ -1,412 +0,0 @@
use super::*;
use crate::display::DisplayData;
use display::format_string;
use std::fs::File;
use std::io::Write;
use std::panic;
use std::path::PathBuf;
use std::process::Command;
use tempfile::Builder;
use tempfile::TempDir;
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_main() {
assert_cli::Assert::main_binary()
.with_args(&["src/test_dir"])
.stdout()
.is(main_output(true).as_str())
.unwrap();
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_main_long_paths() {
assert_cli::Assert::main_binary()
.with_args(&["-p", "src/test_dir"])
.stdout()
.is(main_output(false).as_str())
.unwrap();
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_main_multi_arg() {
assert_cli::Assert::main_binary()
.with_args(&["src/test_dir/many/", "src/test_dir/", "src/test_dir"])
.stdout()
.is(main_output(true).as_str())
.unwrap();
}
#[cfg(target_os = "macos")]
fn main_output(short_paths: bool) -> String {
let d = DisplayData {
short_paths,
is_reversed: false,
colors_on: true,
};
format!(
"{}
{}
{}
{}",
format_string("src/test_dir", true, &d, " 4.0K", "─┬"),
format_string("src/test_dir/many", true, &d, " 4.0K", " └─┬",),
format_string("src/test_dir/many/hello_file", true, &d, " 4.0K", " ├──",),
format_string("src/test_dir/many/a_file", false, &d, " 0B", " └──",),
)
}
#[cfg(target_os = "linux")]
fn main_output(short_paths: bool) -> String {
let d = DisplayData {
short_paths,
is_reversed: false,
colors_on: true,
};
format!(
"{}
{}
{}
{}",
format_string("src/test_dir", true, &d, " 12K", "─┬"),
format_string("src/test_dir/many", true, &d, " 8.0K", " └─┬",),
format_string("src/test_dir/many/hello_file", true, &d, " 4.0K", " ├──",),
format_string("src/test_dir/many/a_file", false, &d, " 0B", " └──",),
)
}
#[cfg(target_os = "windows")]
fn main_output(short_paths: bool) -> String {
let d = DisplayData {
short_paths,
is_reversed: false,
colors_on: true,
};
format!(
"{}
{}
{}
{}",
format_string("src/test_dir", true, &d, " 6B", "─┬"),
format_string("src/test_dir\\many", true, &d, " 6B", " └─┬",),
format_string(
"src/test_dir\\many\\hello_file",
true,
&d,
" 6B",
" ├──",
),
format_string("src/test_dir\\many\\a_file", false, &d, " 0B", " └──",),
)
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_no_color_flag() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "src/test_dir/"])
.stdout()
.is(no_color_flag_output().as_str())
.unwrap();
}
#[cfg(target_os = "macos")]
fn no_color_flag_output() -> String {
"
4.0K ─┬ test_dir
4.0K └─┬ many
4.0K ├── hello_file
0B └── a_file
"
.to_string()
}
#[cfg(target_os = "linux")]
fn no_color_flag_output() -> String {
"
12K ─┬ test_dir
8.0K └─┬ many
4.0K ├── hello_file
0B └── a_file
"
.to_string()
}
#[cfg(target_os = "windows")]
fn no_color_flag_output() -> String {
"
6B ─┬ test_dir
6B └─┬ many
6B ├── hello_file
0B └── a_file
"
.to_string()
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_apparent_size() {
let d = DisplayData {
short_paths: true,
is_reversed: false,
colors_on: true,
};
let r = format!(
"{}",
format_string("src/test_dir/many/hello_file", true, &d, " 6B", " ├──",),
);
assert_cli::Assert::main_binary()
.with_args(&["-s", "src/test_dir"])
.stdout()
.contains(r.as_str())
.unwrap();
}
#[test]
pub fn test_reverse_flag() {
// variable names the same length make the output easier to read
let a = " ┌── a_file";
let b = " ├── hello_file";
let c = " ┌─┴ many";
let d = " ─┴ test_dir";
assert_cli::Assert::main_binary()
.with_args(&["-r", "src/test_dir"])
.stdout()
.contains(a)
.stdout()
.contains(b)
.stdout()
.contains(c)
.stdout()
.contains(d)
.unwrap();
}
#[test]
pub fn test_d_flag_works() {
// We should see the top level directory but not the sub dirs / files:
assert_cli::Assert::main_binary()
.with_args(&["-d", "1", "-s", "src/test_dir"])
.stdout()
.doesnt_contain("hello_file")
.unwrap();
}
fn build_temp_file(dir: &TempDir) -> PathBuf {
let file_path = dir.path().join("notes.txt");
let mut file = File::create(&file_path).unwrap();
writeln!(file, "I am a temp file").unwrap();
file_path
}
// fix! [rivy; 2020-01-22] possible on "windows"?; `ln` is not usually an available command; creation of symbolic links requires special enhanced permissions
// ... ref: <https://superuser.com/questions/343074/directory-junction-vs-directory-symbolic-link> @@ <https://archive.is/gpTLE>
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_soft_sym_link() {
let dir = Builder::new().tempdir().unwrap();
let file = build_temp_file(&dir);
let dir_s = dir.path().to_str().unwrap();
let file_path_s = file.to_str().unwrap();
let link_name = dir.path().join("the_link");
let link_name_s = link_name.to_str().unwrap();
let c = Command::new("ln")
.arg("-s")
.arg(file_path_s)
.arg(link_name_s)
.output();
assert!(c.is_ok());
let a = format!(" ─┬ {}", dir_s);
let b = format!(" ├── {}", file_path_s);
let c = format!(" └── {}", link_name_s);
assert_cli::Assert::main_binary()
.with_args(&["-p", &dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b.as_str())
.stdout()
.contains(c.as_str())
.unwrap();
}
// Hard links are ignored as the inode is the same as the file
// fix! [rivy; 2020-01-22] may fail on "usual" windows hosts as `ln` is not usually an available command
#[test]
pub fn test_hard_sym_link() {
let dir = Builder::new().tempdir().unwrap();
let file = build_temp_file(&dir);
let dir_s = dir.path().to_str().unwrap();
let file_path_s = file.to_str().unwrap();
let link_name = dir.path().join("the_link");
let link_name_s = link_name.to_str().unwrap();
let c = Command::new("ln")
.arg(file_path_s)
.arg(link_name_s)
.output();
assert!(c.is_ok());
let a = format!(" ─┬ {}", dir_s);
let b = format!(" └── {}", link_name_s);
let b2 = format!(" └── {}", file_path_s);
// Because this is a hard link the file and hard link look identical. Therefore
// we cannot guarantee which version will appear first.
let result = panic::catch_unwind(|| {
assert_cli::Assert::main_binary()
.with_args(&["-p", dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b.as_str())
.unwrap();
});
if result.is_err() {
assert_cli::Assert::main_binary()
.with_args(&["-p", dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b2.as_str())
.unwrap();
}
}
// Check we don't recurse down an infinite symlink tree
// fix! [rivy; 2020-01-22] possible on "windows"?; `ln` is not usually an available command; creation of symbolic links requires special enhanced permissions
// ... ref: <https://superuser.com/questions/343074/directory-junction-vs-directory-symbolic-link> @@ <https://archive.is/gpTLE>
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_recursive_sym_link() {
let dir = Builder::new().tempdir().unwrap();
let dir_s = dir.path().to_str().unwrap();
let link_name = dir.path().join("the_link");
let link_name_s = link_name.to_str().unwrap();
let c = Command::new("ln")
.arg("-s")
.arg(dir_s)
.arg(link_name_s)
.output();
assert!(c.is_ok());
let a = format!(" ─┬ {}", dir_s);
let b = format!(" └── {}", link_name_s);
assert_cli::Assert::main_binary()
.with_args(&["-p", dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b.as_str())
.unwrap();
}
// Check against directories and files whos names are substrings of each other
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_substring_of_names() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "src/test_dir2"])
.stdout()
.is(no_substring_of_names_output().as_str())
.unwrap();
}
#[cfg(target_os = "linux")]
fn no_substring_of_names_output() -> String {
"
24K ─┬ test_dir2
8.0K ├─┬ dir
4.0K │ └── hello
8.0K ├─┬ dir_substring
4.0K │ └── hello
4.0K └── dir_name_clash
"
.into()
}
#[cfg(target_os = "macos")]
fn no_substring_of_names_output() -> String {
"
12K ─┬ test_dir2
4.0K ├─┬ dir
4.0K │ └── hello
4.0K ├── dir_name_clash
4.0K └─┬ dir_substring
4.0K └── hello
"
.into()
}
#[cfg(target_os = "windows")]
fn no_substring_of_names_output() -> String {
"
16B ─┬ test_dir2
6B ├─┬ dir_substring
6B │ └── hello
5B ├─┬ dir
5B │ └── hello
5B └── dir_name_clash
"
.into()
}
// Check against directories and files whos names are substrings of each other
#[test]
pub fn test_ignore_dir() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "-X", "dir_substring", "src/test_dir2"])
.stdout()
.is(ignore_dir_output().as_str())
.unwrap();
}
#[cfg(target_os = "linux")]
fn ignore_dir_output() -> String {
"
16K ─┬ test_dir2
8.0K ├─┬ dir
4.0K │ └── hello
4.0K └── dir_name_clash
"
.into()
}
#[cfg(target_os = "macos")]
fn ignore_dir_output() -> String {
"
8.0K ─┬ test_dir2
4.0K ├─┬ dir
4.0K │ └── hello
4.0K └── dir_name_clash
"
.into()
}
#[cfg(target_os = "windows")]
fn ignore_dir_output() -> String {
"
10B ─┬ test_dir2
5B ├─┬ dir
5B │ └── hello
5B └── dir_name_clash
"
.into()
}

View File

@@ -2,6 +2,7 @@ use jwalk::DirEntry;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use jwalk::WalkDir;
@@ -10,7 +11,7 @@ use self::platform::*;
#[derive(Debug, Default, Eq)]
pub struct Node {
pub name: String,
pub name: PathBuf,
pub size: u64,
pub children: Vec<Node>,
}
@@ -37,15 +38,15 @@ impl PartialEq for Node {
}
}
pub fn is_a_parent_of(parent: &str, child: &str) -> bool {
let path_parent = std::path::Path::new(parent);
let path_child = std::path::Path::new(child);
(path_child.starts_with(path_parent) && !path_parent.starts_with(path_child))
pub fn is_a_parent_of<P: AsRef<Path>>(parent: P, child: P) -> bool {
let parent = parent.as_ref();
let child = child.as_ref();
child.starts_with(parent) && !parent.starts_with(child)
}
pub fn simplify_dir_names(filenames: Vec<&str>) -> HashSet<String> {
let mut top_level_names: HashSet<String> = HashSet::with_capacity(filenames.len());
let mut to_remove: Vec<String> = Vec::with_capacity(filenames.len());
pub fn simplify_dir_names<P: AsRef<Path>>(filenames: Vec<P>) -> HashSet<PathBuf> {
let mut top_level_names: HashSet<PathBuf> = HashSet::with_capacity(filenames.len());
let mut to_remove: Vec<PathBuf> = Vec::with_capacity(filenames.len());
for t in filenames {
let top_level_name = normalize_path(t);
@@ -53,7 +54,7 @@ pub fn simplify_dir_names(filenames: Vec<&str>) -> HashSet<String> {
for tt in top_level_names.iter() {
if is_a_parent_of(&top_level_name, tt) {
to_remove.push(tt.to_string());
to_remove.push(tt.to_path_buf());
} else if is_a_parent_of(tt, &top_level_name) {
can_add = false;
}
@@ -62,43 +63,46 @@ pub fn simplify_dir_names(filenames: Vec<&str>) -> HashSet<String> {
top_level_names.retain(|tr| to_remove.binary_search(tr).is_err());
to_remove.clear();
if can_add {
top_level_names.insert(normalize_path(t).to_owned());
top_level_names.insert(top_level_name);
}
}
top_level_names
}
pub fn get_dir_tree(
top_level_names: &HashSet<String>,
ignore_directories: Option<Vec<&str>>,
pub fn get_dir_tree<P: AsRef<Path>>(
top_level_names: &HashSet<P>,
ignore_directories: &Option<Vec<PathBuf>>,
apparent_size: bool,
limit_filesystem: bool,
threads: Option<usize>,
) -> (bool, HashMap<String, u64>) {
) -> (bool, HashMap<PathBuf, u64>) {
let mut permissions = 0;
let mut data: HashMap<String, u64> = HashMap::new();
let mut data: HashMap<PathBuf, u64> = HashMap::new();
let restricted_filesystems = if limit_filesystem {
get_allowed_filesystems(top_level_names)
} else {
None
};
let mut examine_dir_args = ExamineDirMutArsg {
data: &mut data,
file_count_no_permission: &mut permissions,
};
for b in top_level_names.iter() {
examine_dir(
&b,
b,
apparent_size,
&restricted_filesystems,
&ignore_directories,
&mut data,
&mut permissions,
ignore_directories,
threads,
&mut examine_dir_args,
);
}
(permissions == 0, data)
}
fn get_allowed_filesystems(top_level_names: &HashSet<String>) -> Option<HashSet<u64>> {
fn get_allowed_filesystems<P: AsRef<Path>>(top_level_names: &HashSet<P>) -> Option<HashSet<u64>> {
let mut limit_filesystems: HashSet<u64> = HashSet::new();
for file_name in top_level_names.iter() {
if let Ok(a) = get_filesystem(file_name) {
@@ -108,29 +112,30 @@ fn get_allowed_filesystems(top_level_names: &HashSet<String>) -> Option<HashSet<
Some(limit_filesystems)
}
pub fn normalize_path<P: AsRef<std::path::Path>>(path: P) -> std::string::String {
pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
// normalize path ...
// 1. removing repeated separators
// 2. removing interior '.' ("current directory") path segments
// 3. removing trailing extra separators and '.' ("current directory") path segments
// * `Path.components()` does all the above work; ref: <https://doc.rust-lang.org/std/path/struct.Path.html#method.components>
// 4. changing to os preferred separator (automatically done by recollecting components back into a PathBuf)
path.as_ref()
.components()
.collect::<std::path::PathBuf>()
.to_string_lossy()
.to_string()
path.as_ref().components().collect::<PathBuf>()
}
fn examine_dir(
top_dir: &str,
struct ExamineDirMutArsg<'a> {
data: &'a mut HashMap<PathBuf, u64>,
file_count_no_permission: &'a mut u64,
}
fn examine_dir<P: AsRef<Path>>(
top_dir: P,
apparent_size: bool,
filesystems: &Option<HashSet<u64>>,
ignore_directories: &Option<Vec<&str>>,
data: &mut HashMap<String, u64>,
file_count_no_permission: &mut u64,
ignore_directories: &Option<Vec<PathBuf>>,
threads: Option<usize>,
mut_args: &mut ExamineDirMutArsg,
) {
let top_dir = top_dir.as_ref();
let mut inodes: HashSet<(u64, u64)> = HashSet::new();
let mut iter = WalkDir::new(top_dir)
.preload_metadata(true)
@@ -138,27 +143,36 @@ fn examine_dir(
if let Some(threads_to_start) = threads {
iter = iter.num_threads(threads_to_start);
}
'entry: for entry in iter {
if let Ok(e) = entry {
let maybe_size_and_inode = get_metadata(&e, apparent_size);
if let Some(d) = ignore_directories {
for s in d {
if e.path().to_string_lossy().contains(*s) {
if let Some(dirs) = ignore_directories {
let path = e.path();
let parts = path.components().collect::<Vec<std::path::Component>>();
for d in dirs {
let seq = d.components().collect::<Vec<std::path::Component>>();
if parts
.windows(seq.len())
.any(|window| window.iter().collect::<PathBuf>() == *d)
{
continue 'entry;
}
}
}
match maybe_size_and_inode {
Some((size, maybe_inode)) => {
if !should_ignore_file(apparent_size, filesystems, &mut inodes, maybe_inode) {
process_file_with_size_and_inode(top_dir, data, e, size)
Some(data) => {
let (size, inode_device) = data;
if !should_ignore_file(apparent_size, filesystems, &mut inodes, inode_device) {
process_file_with_size_and_inode(top_dir, mut_args.data, e, size)
}
}
None => *file_count_no_permission += 1,
None => *mut_args.file_count_no_permission += 1,
}
} else {
*file_count_no_permission += 1
*mut_args.file_count_no_permission += 1
}
}
}
@@ -167,52 +181,54 @@ fn should_ignore_file(
apparent_size: bool,
restricted_filesystems: &Option<HashSet<u64>>,
inodes: &mut HashSet<(u64, u64)>,
maybe_inode: Option<(u64, u64)>,
maybe_inode_device: Option<(u64, u64)>,
) -> bool {
if !apparent_size {
if let Some(inode_dev_pair) = maybe_inode {
match maybe_inode_device {
None => false,
Some(data) => {
let (inode, device) = data;
// Ignore files on different devices (if flag applied)
if restricted_filesystems.is_some()
&& !restricted_filesystems
.as_ref()
.unwrap()
.contains(&inode_dev_pair.1)
{
return true;
if let Some(rs) = restricted_filesystems {
if !rs.contains(&device) {
return true;
}
}
// Ignore files already visited or symlinked
if inodes.contains(&inode_dev_pair) {
return true;
if !apparent_size {
// Ignore files already visited or symlinked
if inodes.contains(&(inode, device)) {
return true;
}
inodes.insert((inode, device));
}
inodes.insert(inode_dev_pair);
false
}
}
false
}
fn process_file_with_size_and_inode(
top_dir: &str,
data: &mut HashMap<String, u64>,
fn process_file_with_size_and_inode<P: AsRef<Path>>(
top_dir: P,
data: &mut HashMap<PathBuf, u64>,
e: DirEntry,
size: u64,
) {
let top_dir = top_dir.as_ref();
// This path and all its parent paths have their counter incremented
for path_name in e.path().ancestors() {
for path in e.path().ancestors() {
// This is required due to bug in Jwalk that adds '/' to all sub dir lists
// see: https://github.com/jessegrosjean/jwalk/issues/13
if path_name.to_string_lossy() == "/" && top_dir != "/" {
if path.to_string_lossy() == "/" && top_dir.to_string_lossy() != "/" {
continue;
}
let path_name = path_name.to_string_lossy();
let s = data.entry(path_name.to_string()).or_insert(0);
let s = data.entry(normalize_path(path)).or_insert(0);
*s += size;
if path_name == top_dir {
if path.starts_with(top_dir) && top_dir.starts_with(path) {
break;
}
}
}
pub fn sort_by_size_first_name_second(a: &(String, u64), b: &(String, u64)) -> Ordering {
pub fn sort_by_size_first_name_second(a: &(PathBuf, u64), b: &(PathBuf, u64)) -> Ordering {
let result = b.1.cmp(&a.1);
if result == Ordering::Equal {
a.0.cmp(&b.0)
@@ -221,13 +237,13 @@ pub fn sort_by_size_first_name_second(a: &(String, u64), b: &(String, u64)) -> O
}
}
pub fn sort(data: HashMap<String, u64>) -> Vec<(String, u64)> {
let mut new_l: Vec<(String, u64)> = data.iter().map(|(a, b)| (a.clone(), *b)).collect();
pub fn sort(data: HashMap<PathBuf, u64>) -> Vec<(PathBuf, u64)> {
let mut new_l: Vec<(PathBuf, u64)> = data.iter().map(|(a, b)| (a.clone(), *b)).collect();
new_l.sort_unstable_by(sort_by_size_first_name_second);
new_l
}
pub fn find_big_ones(new_l: Vec<(String, u64)>, max_to_show: usize) -> Vec<(String, u64)> {
pub fn find_big_ones(new_l: Vec<(PathBuf, u64)>, max_to_show: usize) -> Vec<(PathBuf, u64)> {
if max_to_show > 0 && new_l.len() > max_to_show {
new_l[0..max_to_show].to_vec()
} else {
@@ -235,20 +251,29 @@ pub fn find_big_ones(new_l: Vec<(String, u64)>, max_to_show: usize) -> Vec<(Stri
}
}
fn depth_of_path(name: &PathBuf) -> usize {
// Filter required as paths can have some odd preliminary
// ("Prefix") bits (for example, from windows, "\\?\" or "\\UNC\")
name.components()
.filter(|&c| match c {
std::path::Component::Prefix(_) => false,
_ => true,
})
.count()
}
pub fn trim_deep_ones(
input: Vec<(String, u64)>,
input: Vec<(PathBuf, u64)>,
max_depth: u64,
top_level_names: &HashSet<String>,
) -> Vec<(String, u64)> {
let mut result: Vec<(String, u64)> = Vec::with_capacity(input.len() * top_level_names.len());
top_level_names: &HashSet<PathBuf>,
) -> Vec<(PathBuf, u64)> {
let mut result: Vec<(PathBuf, u64)> = Vec::with_capacity(input.len() * top_level_names.len());
for name in top_level_names {
let my_max_depth = name.matches(std::path::is_separator).count() + max_depth as usize;
let name_ref: &str = name.as_ref();
let my_max_depth = depth_of_path(name) + max_depth as usize;
for &(ref k, ref v) in input.iter() {
if k.starts_with(name_ref) && k.matches(std::path::is_separator).count() <= my_max_depth
{
if k.starts_with(name) && depth_of_path(k) <= my_max_depth {
result.push((k.clone(), *v));
}
}
@@ -263,34 +288,22 @@ mod tests {
#[test]
fn test_simplify_dir() {
let mut correct = HashSet::new();
correct.insert("a".to_string());
correct.insert(PathBuf::from("a"));
assert_eq!(simplify_dir_names(vec!["a"]), correct);
}
#[test]
fn test_simplify_dir_rm_subdir() {
let mut correct = HashSet::new();
correct.insert(
["a", "b"]
.iter()
.collect::<std::path::PathBuf>()
.to_string_lossy()
.to_string(),
);
correct.insert(["a", "b"].iter().collect::<PathBuf>());
assert_eq!(simplify_dir_names(vec!["a/b", "a/b/c", "a/b/d/f"]), correct);
}
#[test]
fn test_simplify_dir_duplicates() {
let mut correct = HashSet::new();
correct.insert(
["a", "b"]
.iter()
.collect::<std::path::PathBuf>()
.to_string_lossy()
.to_string(),
);
correct.insert("c".to_string());
correct.insert(["a", "b"].iter().collect::<PathBuf>());
correct.insert(PathBuf::from("c"));
assert_eq!(
simplify_dir_names(vec![
"a/b",
@@ -308,36 +321,24 @@ mod tests {
#[test]
fn test_simplify_dir_rm_subdir_and_not_substrings() {
let mut correct = HashSet::new();
correct.insert("b".to_string());
correct.insert(
["c", "a", "b"]
.iter()
.collect::<std::path::PathBuf>()
.to_string_lossy()
.to_string(),
);
correct.insert(
["a", "b"]
.iter()
.collect::<std::path::PathBuf>()
.to_string_lossy()
.to_string(),
);
correct.insert(PathBuf::from("b"));
correct.insert(["c", "a", "b"].iter().collect::<PathBuf>());
correct.insert(["a", "b"].iter().collect::<PathBuf>());
assert_eq!(simplify_dir_names(vec!["a/b", "c/a/b/", "b"]), correct);
}
#[test]
fn test_simplify_dir_dots() {
let mut correct = HashSet::new();
correct.insert("src".to_string());
correct.insert(PathBuf::from("src"));
assert_eq!(simplify_dir_names(vec!["src/."]), correct);
}
#[test]
fn test_simplify_dir_substring_names() {
let mut correct = HashSet::new();
correct.insert("src".to_string());
correct.insert("src_v2".to_string());
correct.insert(PathBuf::from("src"));
correct.insert(PathBuf::from("src_v2"));
assert_eq!(simplify_dir_names(vec!["src/", "src_v2"]), correct);
}
@@ -360,4 +361,43 @@ mod tests {
assert!(is_a_parent_of("/", "/usr"));
assert!(!is_a_parent_of("/", "/"));
}
#[test]
fn test_should_ignore_file() {
let mut files = HashSet::new();
files.insert((10, 20));
assert!(!should_ignore_file(true, &None, &mut files, Some((0, 0))));
// New file is not known it will be inserted to the hashmp and should not be ignored
assert!(!should_ignore_file(
false,
&None,
&mut files,
Some((11, 12))
));
assert!(files.contains(&(11, 12)));
// The same file will be ignored the second time
assert!(should_ignore_file(false, &None, &mut files, Some((11, 12))));
}
#[test]
fn test_should_ignore_file_on_different_device() {
let mut files = HashSet::new();
files.insert((10, 20));
let mut devices = HashSet::new();
devices.insert(99);
let od = Some(devices);
// If we are looking at a different device (disk) and the device flag is set
// then apparent_size is irrelevant - we ignore files on other devices
assert!(should_ignore_file(false, &od, &mut files, Some((11, 12))));
assert!(should_ignore_file(true, &od, &mut files, Some((11, 12))));
// We do not ignore files on the same device
assert!(!should_ignore_file(false, &od, &mut files, Some((2, 99))));
assert!(!should_ignore_file(true, &od, &mut files, Some((2, 99))));
}
}

View File

@@ -2,6 +2,7 @@ use jwalk::DirEntry;
#[allow(unused_imports)]
use std::fs;
use std::io;
use std::path::Path;
#[cfg(target_family = "unix")]
fn get_block_size() -> u64 {
@@ -14,48 +15,124 @@ fn get_block_size() -> u64 {
pub fn get_metadata(d: &DirEntry, use_apparent_size: bool) -> Option<(u64, Option<(u64, u64)>)> {
use std::os::unix::fs::MetadataExt;
d.metadata.as_ref().unwrap().as_ref().ok().map(|md| {
let inode = Some((md.ino(), md.dev()));
if use_apparent_size {
(md.len(), inode)
(md.len(), Some((md.ino(), md.dev())))
} else {
(md.blocks() * get_block_size(), inode)
(md.blocks() * get_block_size(), Some((md.ino(), md.dev())))
}
})
}
#[cfg(target_family = "windows")]
pub fn get_metadata(d: &DirEntry, _use_apparent_size: bool) -> Option<(u64, Option<(u64, u64)>)> {
use winapi_util::file::information;
// On windows opening the file to get size, file ID and volume can be very
// expensive because 1) it causes a few system calls, and more importantly 2) it can cause
// windows defender to scan the file.
// Therefore we try to avoid doing that for common cases, mainly those of
// plain files:
// The idea is to make do with the file size that we get from the OS for
// free as part of iterating a folder. Therefore we want to make sure that
// it makes sense to use that free size information:
// Volume boundaries:
// The user can ask us not to cross volume boundaries. If the DirEntry is a
// plain file and not a reparse point or other non-trivial stuff, we assume
// that the file is located on the same volume as the directory that
// contains it.
// File ID:
// This optimization does deprive us of access to a file ID. As a
// workaround, we just make one up that hopefully does not collide with real
// file IDs.
// Hard links: Unresolved. We don't get inode/file index, so hard links
// count once for each link. Hopefully they are not too commonly in use on
// windows.
// Size:
// We assume (naively?) that for the common cases the free size info is the
// same as one would get by doing the expensive thing. Sparse, encrypted and
// compressed files are not included in the common cases, as one can image
// there being more than view on their size.
// Savings in orders of magnitude in terms of time, io and cpu have been
// observed on hdd, windows 10, some 100Ks files taking up some hundreds of
// GBs:
// Consistently opening the file: 30 minutes.
// With this optimization: 8 sec.
use winapi_util::Handle;
fn handle_from_path_limited<P: AsRef<Path>>(path: P) -> io::Result<Handle> {
use std::fs::OpenOptions;
use std::os::windows::fs::OpenOptionsExt;
const FILE_READ_ATTRIBUTES: u32 = 0x0080;
let h = Handle::from_path_any(d.path()).ok()?;
let info = information(&h).ok()?;
// So, it seems that it does does have to be that expensive to open
// files to get their info: Avoiding opening the file with the full
// GENERIC_READ is key:
Some((
info.file_size(),
Some((info.file_index(), info.volume_serial_number())),
))
}
// https://docs.microsoft.com/en-us/windows/win32/secauthz/generic-access-rights:
// "For example, a Windows file object maps the GENERIC_READ bit to the
// READ_CONTROL and SYNCHRONIZE standard access rights and to the
// FILE_READ_DATA, FILE_READ_EA, and FILE_READ_ATTRIBUTES
// object-specific access rights"
#[cfg(all(not(target_family = "windows"), not(target_family = "unix")))]
pub fn get_metadata(d: &DirEntry, _apparent: bool) -> Option<(u64, Option<(u64, u64)>)> {
d.metadata
.as_ref()
.unwrap()
.as_ref()
.ok()
.map(|md| (md.len(), None))
// The flag FILE_READ_DATA seems to be the expensive one, so we'll avoid
// that, and a most of the other ones. Simply because it seems that we
// don't need them.
let file = OpenOptions::new()
.access_mode(FILE_READ_ATTRIBUTES)
.open(path)?;
Ok(Handle::from_file(file))
}
fn get_metadata_expensive(d: &DirEntry) -> Option<(u64, Option<(u64, u64)>)> {
use winapi_util::file::information;
let h = handle_from_path_limited(d.path()).ok()?;
let info = information(&h).ok()?;
Some((
info.file_size(),
Some((info.file_index(), info.volume_serial_number())),
))
}
match d.metadata {
Some(Ok(ref md)) => {
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x20u32;
const FILE_ATTRIBUTE_READONLY: u32 = 0x1u32;
const FILE_ATTRIBUTE_HIDDEN: u32 = 0x2u32;
const FILE_ATTRIBUTE_SYSTEM: u32 = 0x4u32;
const FILE_ATTRIBUTE_NORMAL: u32 = 0x80u32;
const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10u32;
let attr_filtered = md.file_attributes()
& !(FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_SYSTEM);
if attr_filtered == FILE_ATTRIBUTE_ARCHIVE
|| attr_filtered == FILE_ATTRIBUTE_DIRECTORY
|| md.file_attributes() == FILE_ATTRIBUTE_NORMAL
{
Some((md.len(), None))
} else {
get_metadata_expensive(&d)
}
}
_ => get_metadata_expensive(&d),
}
}
#[cfg(target_family = "unix")]
pub fn get_filesystem(file_path: &str) -> Result<u64, io::Error> {
pub fn get_filesystem<P: AsRef<Path>>(file_path: P) -> Result<u64, io::Error> {
use std::os::unix::fs::MetadataExt;
let metadata = fs::metadata(file_path)?;
Ok(metadata.dev())
}
#[cfg(target_family = "windows")]
pub fn get_filesystem(file_path: &str) -> Result<u64, io::Error> {
pub fn get_filesystem<P: AsRef<Path>>(file_path: P) -> Result<u64, io::Error> {
use winapi_util::file::information;
use winapi_util::Handle;

267
tests/tests.rs Normal file
View File

@@ -0,0 +1,267 @@
mod tests_symlinks;
// File sizes differ on both platform and on the format of the disk.
// We can at least test the file names are there
#[test]
pub fn test_basic_output() {
assert_cli::Assert::main_binary()
.with_args(&["src/test_dir/"])
.stdout()
.contains(" ┌─┴ ")
.stdout()
.contains("test_dir ")
.stdout()
.contains(" ┌─┴ ")
.stdout()
.contains("many ")
.stdout()
.contains(" ├── ")
.stdout()
.contains("hello_file")
.stdout()
.contains(" ┌── ")
.stdout()
.contains("a_file ")
.unwrap();
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_main_basic() {
// -c is no color mode - This makes testing much simpler
assert_cli::Assert::main_binary()
.with_args(&["-c", "src/test_dir"])
.stdout()
.is(main_output().as_str())
.unwrap();
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_main_multi_arg() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "src/test_dir/many/", "src/test_dir/", "src/test_dir"])
.stdout()
.is(main_output().as_str())
.unwrap();
}
#[cfg(target_os = "macos")]
fn main_output() -> String {
r#"
0B ┌── a_file │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█ │ 0%
4.0K ├── hello_file│██████████████████████████████████████████████ │ 100%
4.0K ┌─┴ many │██████████████████████████████████████████████ │ 100%
4.0K ┌─┴ test_dir │██████████████████████████████████████████████ │ 100%
"#
.to_string()
}
#[cfg(target_os = "linux")]
fn main_output() -> String {
r#"
0B ┌── a_file │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█ │ 0%
4.0K ├── hello_file│ ░░░░░░░░░░░░░░░████████████████ │ 33%
8.0K ┌─┴ many │ ███████████████████████████████ │ 67%
12K ┌─┴ test_dir │██████████████████████████████████████████████ │ 100%
"#
.to_string()
}
#[cfg(target_os = "windows")]
fn main_output() -> String {
"PRs welcome".to_string()
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_main_long_paths() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "-p", "src/test_dir"])
.stdout()
.is(main_output_long_paths().as_str())
.unwrap();
}
#[cfg(target_os = "macos")]
fn main_output_long_paths() -> String {
r#"
0B ┌── src/test_dir/many/a_file │░░░░░░░░░░░░░░░░░░░░░░░░░░░█ │ 0%
4.0K ├── src/test_dir/many/hello_file│████████████████████████████ │ 100%
4.0K ┌─┴ src/test_dir/many │████████████████████████████ │ 100%
4.0K ┌─┴ src/test_dir │████████████████████████████ │ 100%
"#
.to_string()
}
#[cfg(target_os = "linux")]
fn main_output_long_paths() -> String {
r#"
0B ┌── src/test_dir/many/a_file │ ░░░░░░░░░░░░░░░░░░█ │ 0%
4.0K ├── src/test_dir/many/hello_file│ ░░░░░░░░░██████████ │ 33%
8.0K ┌─┴ src/test_dir/many │ ███████████████████ │ 67%
12K ┌─┴ src/test_dir │████████████████████████████ │ 100%
"#
.to_string()
}
#[cfg(target_os = "windows")]
fn main_output_long_paths() -> String {
"PRs welcome".to_string()
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_apparent_size() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "-s", "src/test_dir"])
.stdout()
.is(output_apparent_size().as_str())
.unwrap();
}
#[cfg(target_os = "linux")]
fn output_apparent_size() -> String {
r#"
0B ┌── a_file │ ░░░░░░░░░░░░░░░░░░░░░░░█ │ 0%
6B ├── hello_file│ ░░░░░░░░░░░░░░░░░░░░░░░█ │ 0%
4.0K ┌─┴ many │ ████████████████████████ │ 50%
8.0K ┌─┴ test_dir │██████████████████████████████████████████████ │ 100%
"#
.to_string()
}
#[cfg(target_os = "macos")]
fn output_apparent_size() -> String {
r#"
0B ┌── a_file │ ░░░░░░░░░░░░░░░░░░░░░░░░░░█ │ 0%
6B ├── hello_file│ ░░░░░░░░░░░░░░░░░░░░░░░░░██ │ 3%
134B ┌─┴ many │ ███████████████████████████ │ 58%
230B ┌─┴ test_dir │██████████████████████████████████████████████ │ 100%
"#
.to_string()
}
#[cfg(target_os = "windows")]
fn output_apparent_size() -> String {
"".to_string()
}
#[test]
pub fn test_reverse_flag() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "-r", "src/test_dir/"])
.stdout()
.contains(" └─┬ test_dir ")
.stdout()
.contains(" └─┬ many ")
.stdout()
.contains(" ├── hello_file")
.stdout()
.contains(" └── a_file ")
.unwrap();
}
#[test]
pub fn test_d_flag_works() {
// We should see the top level directory but not the sub dirs / files:
assert_cli::Assert::main_binary()
.with_args(&["-d", "1", "-s", "src/test_dir"])
.stdout()
.doesnt_contain("hello_file")
.unwrap();
}
// Check against directories and files whos names are substrings of each other
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_substring_of_names() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "src/test_dir2"])
.stdout()
.is(no_substring_of_names_output().as_str())
.unwrap();
}
#[cfg(target_os = "linux")]
fn no_substring_of_names_output() -> String {
"
4.0K ┌── dir_name_clash│ ████████ │ 17%
4.0K │ ┌── hello │ ░░░░░░░████████ │ 17%
8.0K ├─┴ dir_substring │ ███████████████ │ 33%
4.0K │ ┌── hello │ ░░░░░░░████████ │ 17%
8.0K ├─┴ dir │ ███████████████ │ 33%
24K ┌─┴ test_dir2 │████████████████████████████████████████████ │ 100%
"
.into()
}
#[cfg(target_os = "macos")]
fn no_substring_of_names_output() -> String {
"
4.0K ┌── hello │ ███████████████ │ 33%
4.0K ┌─┴ dir_substring │ ███████████████ │ 33%
4.0K ├── dir_name_clash│ ███████████████ │ 33%
4.0K │ ┌── hello │ ███████████████ │ 33%
4.0K ├─┴ dir │ ███████████████ │ 33%
12K ┌─┴ test_dir2 │████████████████████████████████████████████ │ 100%
"
.into()
}
#[cfg(target_os = "windows")]
fn no_substring_of_names_output() -> String {
"PRs".into()
}
// fix! [rivy; 2020-22-01] "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_unicode_directories() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "src/test_dir3"])
.stdout()
.is(unicode_dir().as_str())
.unwrap();
}
#[cfg(target_os = "linux")]
fn unicode_dir() -> String {
// The way unicode & asian characters are rendered on the terminal should make this line up
"
0B ┌── 👩.unicode │ █ │ 0%
0B ├── ラウトは難しいです!.japan│ █ │ 0%
4.0K ┌─┴ test_dir3 │████████████████████████████████ │ 100%
"
.into()
}
#[cfg(target_os = "macos")]
fn unicode_dir() -> String {
"
0B ┌── 👩.unicode │ █ │ 0%
0B ├── ラウトは難しいです!.japan│ █ │ 0%
0B ┌─┴ test_dir3 │ █ │ 0%
"
.into()
}
#[cfg(target_os = "windows")]
fn unicode_dir() -> String {
"".into()
}
// Check against directories and files whos names are substrings of each other
#[test]
pub fn test_ignore_dir() {
assert_cli::Assert::main_binary()
.with_args(&["-c", "-X", "dir_substring", "src/test_dir2"])
.stdout()
.doesnt_contain("dir_substring")
.unwrap();
}

119
tests/tests_symlinks.rs Normal file
View File

@@ -0,0 +1,119 @@
use std::fs::File;
use std::io::Write;
use std::panic;
use std::path::PathBuf;
use std::process::Command;
use tempfile::Builder;
use tempfile::TempDir;
// File sizes differ on both platform and on the format of the disk.
// Windows: `ln` is not usually an available command; creation of symbolic links requires special enhanced permissions
fn build_temp_file(dir: &TempDir) -> PathBuf {
let file_path = dir.path().join("notes.txt");
let mut file = File::create(&file_path).unwrap();
writeln!(file, "I am a temp file").unwrap();
file_path
}
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_soft_sym_link() {
let dir = Builder::new().tempdir().unwrap();
let file = build_temp_file(&dir);
let dir_s = dir.path().to_str().unwrap();
let file_path_s = file.to_str().unwrap();
let link_name = dir.path().join("the_link");
let link_name_s = link_name.to_str().unwrap();
let c = Command::new("ln")
.arg("-s")
.arg(file_path_s)
.arg(link_name_s)
.output();
assert!(c.is_ok());
let c = format!(" ┌── {}", link_name_s);
let b = format!(" ├── {}", file_path_s);
let a = format!("─┴ {}", dir_s);
assert_cli::Assert::main_binary()
.with_args(&["-p", "-c", &dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b.as_str())
.stdout()
.contains(c.as_str())
.unwrap();
}
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_hard_sym_link() {
let dir = Builder::new().tempdir().unwrap();
let file = build_temp_file(&dir);
let dir_s = dir.path().to_str().unwrap();
let file_path_s = file.to_str().unwrap();
let link_name = dir.path().join("the_link");
let link_name_s = link_name.to_str().unwrap();
let c = Command::new("ln")
.arg(file_path_s)
.arg(link_name_s)
.output();
assert!(c.is_ok());
let a = format!("─┴ {}", dir_s);
let b = format!(" ┌── {}", link_name_s);
let b2 = format!(" ┌── {}", file_path_s);
// Because this is a hard link the file and hard link look identical. Therefore
// we cannot guarantee which version will appear first.
let result = panic::catch_unwind(|| {
assert_cli::Assert::main_binary()
.with_args(&["-p", "-c", dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b.as_str())
.unwrap();
});
if result.is_err() {
assert_cli::Assert::main_binary()
.with_args(&["-p", "-c", dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b2.as_str())
.unwrap();
}
}
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_recursive_sym_link() {
let dir = Builder::new().tempdir().unwrap();
let dir_s = dir.path().to_str().unwrap();
let link_name = dir.path().join("the_link");
let link_name_s = link_name.to_str().unwrap();
let c = Command::new("ln")
.arg("-s")
.arg(dir_s)
.arg(link_name_s)
.output();
assert!(c.is_ok());
let a = format!("─┬ {}", dir_s);
let b = format!(" └── {}", link_name_s);
assert_cli::Assert::main_binary()
.with_args(&["-c", "-r", "-p", dir_s])
.stdout()
.contains(a.as_str())
.stdout()
.contains(b.as_str())
.unwrap();
}