├── .github └── workflows │ └── CICD.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── ci └── .gitattributes ├── doc └── diskus.1 ├── src ├── filesize.rs ├── lib.rs ├── main.rs ├── unique_id.rs └── walk.rs └── tests └── walk.rs /.github/workflows/CICD.yml: -------------------------------------------------------------------------------- 1 | name: CICD 2 | 3 | env: 4 | MIN_SUPPORTED_RUST_VERSION: "1.76.0" 5 | CICD_INTERMEDIATES_DIR: "_cicd-intermediates" 6 | 7 | on: 8 | workflow_dispatch: 9 | pull_request: 10 | push: 11 | branches: 12 | - master 13 | tags: 14 | - '*' 15 | 16 | jobs: 17 | min_version: 18 | name: Minimum supported rust version 19 | runs-on: ubuntu-20.04 20 | steps: 21 | - name: Checkout source code 22 | uses: actions/checkout@v2 23 | 24 | - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }}) 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: ${{ env.MIN_SUPPORTED_RUST_VERSION }} 28 | default: true 29 | profile: minimal # minimal component installation (ie, no documentation) 30 | components: clippy, rustfmt 31 | - name: Ensure `cargo fmt` has been run 32 | uses: actions-rs/cargo@v1 33 | with: 34 | command: fmt 35 | args: -- --check 36 | - name: Run clippy (on minimum supported rust version to prevent warnings we can't fix) 37 | uses: actions-rs/cargo@v1 38 | with: 39 | command: clippy 40 | args: --locked --all-targets --all-features 41 | - name: Run tests 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: test 45 | args: --locked 46 | 47 | build: 48 | name: ${{ matrix.job.os }} (${{ matrix.job.target }}) 49 | runs-on: ${{ matrix.job.os }} 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | job: 54 | - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } 55 | - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } 56 | - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } 57 | - { target: i686-pc-windows-msvc , os: windows-2019 } 58 | - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } 59 | - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } 60 | - { target: x86_64-apple-darwin , os: macos-13 } 61 | - { target: aarch64-apple-darwin , os: macos-15 } 62 | - { target: x86_64-pc-windows-gnu , os: windows-2019 } 63 | - { target: x86_64-pc-windows-msvc , os: windows-2019 } 64 | - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } 65 | - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } 66 | steps: 67 | - name: Checkout source code 68 | uses: actions/checkout@v2 69 | 70 | - name: Install prerequisites 71 | shell: bash 72 | run: | 73 | case ${{ matrix.job.target }} in 74 | arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; 75 | aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; 76 | esac 77 | 78 | - name: Extract crate information 79 | shell: bash 80 | run: | 81 | echo "PROJECT_NAME=diskus" >> $GITHUB_ENV 82 | echo "PROJECT_VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)" >> $GITHUB_ENV 83 | echo "PROJECT_MAINTAINER=$(sed -n 's/^authors = \["\(.*\)"\]/\1/p' Cargo.toml)" >> $GITHUB_ENV 84 | echo "PROJECT_HOMEPAGE=$(sed -n 's/^homepage = "\(.*\)"/\1/p' Cargo.toml)" >> $GITHUB_ENV 85 | 86 | - name: Install Rust toolchain 87 | uses: actions-rs/toolchain@v1 88 | with: 89 | toolchain: stable 90 | target: ${{ matrix.job.target }} 91 | override: true 92 | profile: minimal # minimal component installation (ie, no documentation) 93 | 94 | - name: Show version information (Rust, cargo, GCC) 95 | shell: bash 96 | run: | 97 | gcc --version || true 98 | rustup -V 99 | rustup toolchain list 100 | rustup default 101 | cargo -V 102 | rustc -V 103 | 104 | - name: Build 105 | uses: actions-rs/cargo@v1 106 | with: 107 | use-cross: ${{ matrix.job.use-cross }} 108 | command: build 109 | args: --locked --release --target=${{ matrix.job.target }} 110 | 111 | - name: Strip debug information from executable 112 | id: strip 113 | shell: bash 114 | run: | 115 | # Figure out suffix of binary 116 | EXE_suffix="" 117 | case ${{ matrix.job.target }} in 118 | *-pc-windows-*) EXE_suffix=".exe" ;; 119 | esac; 120 | 121 | # Figure out what strip tool to use if any 122 | STRIP="strip" 123 | case ${{ matrix.job.target }} in 124 | arm-unknown-linux-*) STRIP="arm-linux-gnueabihf-strip" ;; 125 | aarch64-unknown-linux-gnu) STRIP="aarch64-linux-gnu-strip" ;; 126 | *-pc-windows-msvc) STRIP="" ;; 127 | esac; 128 | 129 | # Setup paths 130 | BIN_DIR="${{ env.CICD_INTERMEDIATES_DIR }}/stripped-release-bin/" 131 | mkdir -p "${BIN_DIR}" 132 | BIN_NAME="${{ env.PROJECT_NAME }}${EXE_suffix}" 133 | BIN_PATH="${BIN_DIR}/${BIN_NAME}" 134 | 135 | # Copy the release build binary to the result location 136 | cp "target/${{ matrix.job.target }}/release/${BIN_NAME}" "${BIN_DIR}" 137 | 138 | # Also strip if possible 139 | if [ -n "${STRIP}" ]; then 140 | "${STRIP}" "${BIN_PATH}" 141 | fi 142 | 143 | # Let subsequent steps know where to find the (stripped) bin 144 | echo "BIN_PATH=${BIN_PATH}" >> $GITHUB_OUTPUT 145 | echo "BIN_NAME=${BIN_NAME}" >> $GITHUB_OUTPUT 146 | 147 | - name: Set testing options 148 | id: test-options 149 | shell: bash 150 | run: | 151 | # test only library unit tests and binary for arm-type targets 152 | unset CARGO_TEST_OPTIONS 153 | unset CARGO_TEST_OPTIONS ; case ${{ matrix.job.target }} in arm-* | aarch64-*) CARGO_TEST_OPTIONS="--lib --bin ${PROJECT_NAME}" ;; esac; 154 | echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT 155 | 156 | - name: Run tests 157 | uses: actions-rs/cargo@v1 158 | with: 159 | use-cross: ${{ matrix.job.use-cross }} 160 | command: test 161 | args: --locked --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}} 162 | 163 | - name: Create tarball 164 | id: package 165 | shell: bash 166 | run: | 167 | PKG_suffix=".tar.gz" ; case ${{ matrix.job.target }} in *-pc-windows-*) PKG_suffix=".zip" ;; esac; 168 | PKG_BASENAME=${PROJECT_NAME}-v${PROJECT_VERSION}-${{ matrix.job.target }} 169 | PKG_NAME=${PKG_BASENAME}${PKG_suffix} 170 | echo "PKG_NAME=${PKG_NAME}" >> $GITHUB_OUTPUT 171 | 172 | PKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/package" 173 | ARCHIVE_DIR="${PKG_STAGING}/${PKG_BASENAME}/" 174 | mkdir -p "${ARCHIVE_DIR}" 175 | 176 | # Binary 177 | cp "${{ steps.strip.outputs.BIN_PATH }}" "$ARCHIVE_DIR" 178 | 179 | # Man page 180 | cp 'doc/${{ env.PROJECT_NAME }}.1' "$ARCHIVE_DIR" 181 | 182 | # README, LICENSE and CHANGELOG files 183 | cp "README.md" "LICENSE-MIT" "LICENSE-APACHE" "CHANGELOG.md" "$ARCHIVE_DIR" 184 | 185 | # base compressed package 186 | pushd "${PKG_STAGING}/" >/dev/null 187 | case ${{ matrix.job.target }} in 188 | *-pc-windows-*) 7z -y a "${PKG_NAME}" "${PKG_BASENAME}"/* | tail -2 ;; 189 | *) tar czf "${PKG_NAME}" "${PKG_BASENAME}"/* ;; 190 | esac; 191 | popd >/dev/null 192 | 193 | # Let subsequent steps know where to find the compressed package 194 | echo "PKG_PATH="${PKG_STAGING}/${PKG_NAME}"" >> $GITHUB_OUTPUT 195 | 196 | - name: Create Debian package 197 | id: debian-package 198 | shell: bash 199 | if: startsWith(matrix.job.os, 'ubuntu') 200 | run: | 201 | COPYRIGHT_YEARS="2018 - "$(date "+%Y") 202 | DPKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/debian-package" 203 | DPKG_DIR="${DPKG_STAGING}/dpkg" 204 | mkdir -p "${DPKG_DIR}" 205 | 206 | DPKG_BASENAME=${PROJECT_NAME} 207 | DPKG_CONFLICTS=${PROJECT_NAME}-musl 208 | case ${{ matrix.job.target }} in *-musl*) DPKG_BASENAME=${PROJECT_NAME}-musl ; DPKG_CONFLICTS=${PROJECT_NAME} ;; esac; 209 | DPKG_VERSION=${PROJECT_VERSION} 210 | 211 | unset DPKG_ARCH 212 | case ${{ matrix.job.target }} in 213 | aarch64-*-linux-*) DPKG_ARCH=arm64 ;; 214 | arm-*-linux-*hf) DPKG_ARCH=armhf ;; 215 | i686-*-linux-*) DPKG_ARCH=i686 ;; 216 | x86_64-*-linux-*) DPKG_ARCH=amd64 ;; 217 | *) DPKG_ARCH=notset ;; 218 | esac; 219 | 220 | DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb" 221 | echo "DPKG_NAME=${DPKG_NAME}" >> $GITHUB_OUTPUT 222 | 223 | # Binary 224 | install -Dm755 "${{ steps.strip.outputs.BIN_PATH }}" "${DPKG_DIR}/usr/bin/${{ steps.strip.outputs.BIN_NAME }}" 225 | 226 | # Man page 227 | install -Dm644 'doc/${{ env.PROJECT_NAME }}.1' "${DPKG_DIR}/usr/share/man/man1/${{ env.PROJECT_NAME }}.1" 228 | gzip -n --best "${DPKG_DIR}/usr/share/man/man1/${{ env.PROJECT_NAME }}.1" 229 | 230 | # README and LICENSE 231 | install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md" 232 | install -Dm644 "LICENSE-MIT" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE-MIT" 233 | install -Dm644 "LICENSE-APACHE" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE-APACHE" 234 | install -Dm644 "CHANGELOG.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog" 235 | gzip -n --best "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog" 236 | 237 | cat > "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright" < "${DPKG_DIR}/DEBIAN/control" <> $GITHUB_OUTPUT 295 | 296 | # build dpkg 297 | fakeroot dpkg-deb --build "${DPKG_DIR}" "${DPKG_PATH}" 298 | 299 | - name: "Artifact upload: tarball" 300 | uses: actions/upload-artifact@master 301 | with: 302 | name: ${{ steps.package.outputs.PKG_NAME }} 303 | path: ${{ steps.package.outputs.PKG_PATH }} 304 | 305 | - name: "Artifact upload: Debian package" 306 | uses: actions/upload-artifact@master 307 | if: steps.debian-package.outputs.DPKG_NAME 308 | with: 309 | name: ${{ steps.debian-package.outputs.DPKG_NAME }} 310 | path: ${{ steps.debian-package.outputs.DPKG_PATH }} 311 | 312 | - name: Check for release 313 | id: is-release 314 | shell: bash 315 | run: | 316 | unset IS_RELEASE ; if [[ $GITHUB_REF =~ ^refs/tags/v[0-9].* ]]; then IS_RELEASE='true' ; fi 317 | echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT 318 | 319 | - name: Publish archives and packages 320 | uses: softprops/action-gh-release@v1 321 | if: steps.is-release.outputs.IS_RELEASE 322 | with: 323 | files: | 324 | ${{ steps.package.outputs.PKG_PATH }} 325 | ${{ steps.debian-package.outputs.DPKG_PATH }} 326 | env: 327 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 328 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # unreleased 2 | 3 | ## Changes 4 | 5 | 6 | ## Features 7 | 8 | 9 | ## Bugfixes 10 | 11 | 12 | ## Other 13 | 14 | 15 | ## Packaging 16 | 17 | # v0.6.0 18 | 19 | - Updated dependencies 20 | 21 | # v0.6.0 22 | 23 | ## Changes 24 | 25 | There is an important change in default behavior: `diskus` will now report "disk usage" instead of "apparent file size", in analogy to what `du -sh` does. 26 | 27 | At the same time however, we introduce a new `-b`/`--apparent-size` option which can be used to switch back to apparent file size (in analogy to what `du -sbh` does). 28 | 29 | see #25 30 | 31 | ## Features 32 | 33 | - `diskus` is now available for Windows, see #32 (@fawick) 34 | - Error messages are now hidden by default and can be re-enabled via `--verbose`, see #34 (@wngr) 35 | - Added a new `--size-format ` option which can be used to switch from decimal to binary exponents (MiB instead of MB). 36 | - `diskus` changes its output format when the output is piped to a file or to another program. It will simply print the number of bytes, see #35 37 | - Added a new `-b`/`--apparent-size` option which can be used to switch from "disk usage" to "apparent size" (not available on Windows) 38 | 39 | ## Other 40 | 41 | - diskus is now in the official Arch repositories, see #24 (@polyzen) 42 | - diskus is now available on NixOS, see #26 (@fuerbringer) 43 | - diskus is now available on Homebrew and MacPorts, see #33 (@heimskr) 44 | - Added a man page 45 | 46 | # v0.5.0 47 | 48 | - Expose diskus internals as a library, see #21 (@amilajack) 49 | 50 | # v0.4.0 51 | 52 | - More performance improvements by using a custom parallel directory-walker, see #15 53 | 54 | # v0.3.1 55 | 56 | # v0.3.0 57 | 58 | - Renamed the project to diskus 59 | 60 | # v0.2.0 61 | 62 | - Fine-tuned number of threads (makes is even faster) 63 | 64 | # v0.1.0 65 | 66 | Initial release 67 | 68 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ansi_term" 7 | version = "0.12.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 10 | dependencies = [ 11 | "winapi", 12 | ] 13 | 14 | [[package]] 15 | name = "arrayvec" 16 | version = "0.7.6" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 19 | 20 | [[package]] 21 | name = "atty" 22 | version = "0.2.14" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 25 | dependencies = [ 26 | "hermit-abi 0.1.19", 27 | "libc", 28 | "winapi", 29 | ] 30 | 31 | [[package]] 32 | name = "bitflags" 33 | version = "1.3.2" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 36 | 37 | [[package]] 38 | name = "clap" 39 | version = "2.34.0" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 42 | dependencies = [ 43 | "ansi_term", 44 | "atty", 45 | "bitflags", 46 | "strsim", 47 | "term_size", 48 | "textwrap", 49 | "unicode-width", 50 | "vec_map", 51 | ] 52 | 53 | [[package]] 54 | name = "crossbeam-channel" 55 | version = "0.5.14" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" 58 | dependencies = [ 59 | "crossbeam-utils", 60 | ] 61 | 62 | [[package]] 63 | name = "crossbeam-deque" 64 | version = "0.8.6" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 67 | dependencies = [ 68 | "crossbeam-epoch", 69 | "crossbeam-utils", 70 | ] 71 | 72 | [[package]] 73 | name = "crossbeam-epoch" 74 | version = "0.9.18" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 77 | dependencies = [ 78 | "crossbeam-utils", 79 | ] 80 | 81 | [[package]] 82 | name = "crossbeam-utils" 83 | version = "0.8.21" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 86 | 87 | [[package]] 88 | name = "diskus" 89 | version = "0.8.0" 90 | dependencies = [ 91 | "atty", 92 | "clap", 93 | "crossbeam-channel", 94 | "humansize", 95 | "num-format", 96 | "num_cpus", 97 | "rayon", 98 | "tempdir", 99 | ] 100 | 101 | [[package]] 102 | name = "either" 103 | version = "1.13.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 106 | 107 | [[package]] 108 | name = "fuchsia-cprng" 109 | version = "0.1.1" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 112 | 113 | [[package]] 114 | name = "hermit-abi" 115 | version = "0.1.19" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 118 | dependencies = [ 119 | "libc", 120 | ] 121 | 122 | [[package]] 123 | name = "hermit-abi" 124 | version = "0.3.9" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 127 | 128 | [[package]] 129 | name = "humansize" 130 | version = "1.1.1" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" 133 | 134 | [[package]] 135 | name = "itoa" 136 | version = "1.0.14" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 139 | 140 | [[package]] 141 | name = "libc" 142 | version = "0.2.169" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 145 | 146 | [[package]] 147 | name = "num-format" 148 | version = "0.4.4" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" 151 | dependencies = [ 152 | "arrayvec", 153 | "itoa", 154 | ] 155 | 156 | [[package]] 157 | name = "num_cpus" 158 | version = "1.16.0" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 161 | dependencies = [ 162 | "hermit-abi 0.3.9", 163 | "libc", 164 | ] 165 | 166 | [[package]] 167 | name = "rand" 168 | version = "0.4.6" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 171 | dependencies = [ 172 | "fuchsia-cprng", 173 | "libc", 174 | "rand_core 0.3.1", 175 | "rdrand", 176 | "winapi", 177 | ] 178 | 179 | [[package]] 180 | name = "rand_core" 181 | version = "0.3.1" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 184 | dependencies = [ 185 | "rand_core 0.4.2", 186 | ] 187 | 188 | [[package]] 189 | name = "rand_core" 190 | version = "0.4.2" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 193 | 194 | [[package]] 195 | name = "rayon" 196 | version = "1.10.0" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 199 | dependencies = [ 200 | "either", 201 | "rayon-core", 202 | ] 203 | 204 | [[package]] 205 | name = "rayon-core" 206 | version = "1.12.1" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 209 | dependencies = [ 210 | "crossbeam-deque", 211 | "crossbeam-utils", 212 | ] 213 | 214 | [[package]] 215 | name = "rdrand" 216 | version = "0.4.0" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 219 | dependencies = [ 220 | "rand_core 0.3.1", 221 | ] 222 | 223 | [[package]] 224 | name = "remove_dir_all" 225 | version = "0.5.3" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 228 | dependencies = [ 229 | "winapi", 230 | ] 231 | 232 | [[package]] 233 | name = "strsim" 234 | version = "0.8.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 237 | 238 | [[package]] 239 | name = "tempdir" 240 | version = "0.3.7" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" 243 | dependencies = [ 244 | "rand", 245 | "remove_dir_all", 246 | ] 247 | 248 | [[package]] 249 | name = "term_size" 250 | version = "0.3.2" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" 253 | dependencies = [ 254 | "libc", 255 | "winapi", 256 | ] 257 | 258 | [[package]] 259 | name = "textwrap" 260 | version = "0.11.0" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 263 | dependencies = [ 264 | "term_size", 265 | "unicode-width", 266 | ] 267 | 268 | [[package]] 269 | name = "unicode-width" 270 | version = "0.1.14" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 273 | 274 | [[package]] 275 | name = "vec_map" 276 | version = "0.8.2" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 279 | 280 | [[package]] 281 | name = "winapi" 282 | version = "0.3.9" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 285 | dependencies = [ 286 | "winapi-i686-pc-windows-gnu", 287 | "winapi-x86_64-pc-windows-gnu", 288 | ] 289 | 290 | [[package]] 291 | name = "winapi-i686-pc-windows-gnu" 292 | version = "0.4.0" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 295 | 296 | [[package]] 297 | name = "winapi-x86_64-pc-windows-gnu" 298 | version = "0.4.0" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 301 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["David Peter "] 3 | categories = ["command-line-utilities"] 4 | description = "A minimal, fast alternative to 'du -sh'." 5 | homepage = "https://github.com/sharkdp/diskus" 6 | license = "MIT/Apache-2.0" 7 | name = "diskus" 8 | readme = "README.md" 9 | repository = "https://github.com/sharkdp/diskus" 10 | version = "0.8.0" 11 | edition = "2021" 12 | rust-version = "1.76" 13 | 14 | [dependencies] 15 | num_cpus = "1.0" 16 | humansize = "1.1" 17 | num-format = "0.4" 18 | rayon = "1.0" 19 | crossbeam-channel = "0.5" 20 | atty = "0.2" 21 | 22 | [dependencies.clap] 23 | version = "2" 24 | features = ["suggestions", "color", "wrap_help"] 25 | 26 | [dev-dependencies] 27 | tempdir = "0.3" 28 | 29 | [[bin]] 30 | name = "diskus" 31 | path = "src/main.rs" 32 | 33 | [profile.release] 34 | lto = true 35 | codegen-units = 1 36 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diskus 2 | 3 | [![CICD](https://github.com/sharkdp/diskus/actions/workflows/CICD.yml/badge.svg)](https://github.com/sharkdp/diskus/actions/workflows/CICD.yml) 4 | 5 | *A minimal, fast alternative to `du -sh`.* 6 | 7 | `diskus` is a very simple program that computes the total size of the current directory. It is a 8 | parallelized version of `du -sh`. On my 8-core laptop, it is about ten times faster than `du` with 9 | a cold disk cache and more than three times faster with a warm disk cache. 10 | 11 | ``` bash 12 | > diskus 13 | 9.59 GB (9,587,408,896 bytes) 14 | ``` 15 | 16 | ## Benchmark 17 | 18 | The following benchmarks have been performed with [hyperfine](https://github.com/sharkdp/hyperfine) on 19 | a moderately large folder (15GB, 100k directories, 400k files). Smaller folders are not really of any 20 | interest since all programs would finish in a reasonable time that would not interrupt your workflow. 21 | 22 | In addition to `du` and `diskus`, we also add [tin-summer](https://github.com/vmchale/tin-summer) (`sn`) and 23 | [`dust`](https://github.com/bootandy/dust) in our comparison. Both are also written in Rust and provide 24 | much more features than `diskus` (check them out!). The optimal number of threads for `sn` (`-j` option) was 25 | determined via `hyperfine --parameter-scan`. 26 | 27 | ### Cold disk cache 28 | 29 | ```bash 30 | sudo -v 31 | hyperfine --prepare 'sync; echo 3 | sudo tee /proc/sys/vm/drop_caches' \ 32 | 'diskus' 'du -sh' 'sn p -d0 -j8' 'dust -d0' 33 | ``` 34 | (the `sudo`/`sync`/`drop_caches` commands are a way to 35 | [clear the filesystem caches between benchmarking runs](https://github.com/sharkdp/hyperfine#io-heavy-programs)) 36 | 37 | | Command | Mean [s] | Min [s] | Max [s] | Relative | 38 | |:---|---:|---:|---:|---:| 39 | | `diskus` | 1.746 ± 0.017 | 1.728 | 1.770 | 1.00 | 40 | | `du -sh` | 17.776 ± 0.549 | 17.139 | 18.413 | 10.18 | 41 | | `sn p -d0 -j8` | 18.094 ± 0.566 | 17.482 | 18.579 | 10.36 | 42 | | `dust -d0` | 21.357 ± 0.328 | 20.974 | 21.759 | 12.23 | 43 | 44 | 45 | ### Warm disk cache 46 | 47 | On a warm disk cache, the differences are smaller: 48 | ```bash 49 | hyperfine --warmup 5 'diskus' 'du -sh' 'sn p -d0 -j8' 'dust -d0' 50 | ``` 51 | 52 | | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | 53 | |:---|---:|---:|---:|---:| 54 | | `diskus` | 500.3 ± 17.3 | 472.9 | 530.6 | 1.00 | 55 | | `du -sh` | 1098.3 ± 10.0 | 1087.8 | 1122.4 | 2.20 | 56 | | `sn p -d0 -j8` | 1122.2 ± 18.2 | 1107.3 | 1170.1 | 2.24 | 57 | | `dust -d0` | 3532.1 ± 26.4 | 3490.0 | 3563.1 | 7.06 | 58 | 59 | 60 | ## Installation 61 | 62 | ### On Debian-based systems 63 | 64 | You can download the latest Debian package from the 65 | [release page](https://github.com/sharkdp/diskus/releases) and install it via `dpkg`: 66 | 67 | ``` bash 68 | wget "https://github.com/sharkdp/diskus/releases/download/v0.8.0/diskus_0.8.0_amd64.deb" 69 | sudo dpkg -i diskus_0.8.0_amd64.deb 70 | ``` 71 | 72 | ### On Arch-based systems 73 | 74 | ``` bash 75 | pacman -S diskus 76 | ``` 77 | 78 | Or download [diskus-bin](https://aur.archlinux.org/packages/diskus-bin/) from the AUR. 79 | 80 | ### On Void-based systems 81 | 82 | ``` bash 83 | xbps-install diskus 84 | ``` 85 | 86 | ### On macOS 87 | 88 | You can install `diskus` with [Homebrew](https://formulae.brew.sh/formula/diskus): 89 | ``` 90 | brew install diskus 91 | ``` 92 | 93 | Or with [MacPorts](https://ports.macports.org/port/diskus/summary): 94 | ``` 95 | sudo port install diskus 96 | ``` 97 | 98 | ### On Haiku 99 | 100 | ``` bash 101 | pkgman install diskus 102 | ``` 103 | 104 | ### On NixOS 105 | 106 | ``` 107 | nix-env -iA nixos.diskus 108 | ``` 109 | 110 | Or add it to `environment.systemPackages` in your `configuration.nix`. 111 | 112 | ### On other systems 113 | 114 | Check out the [release page](https://github.com/sharkdp/diskus/releases) for binary builds. 115 | 116 | ### Via cargo 117 | 118 | If you have Rust 1.76 or higher, you can install `diskus` from source via `cargo`: 119 | ``` 120 | cargo install diskus 121 | ``` 122 | 123 | ## Windows caveats 124 | 125 | Windows-internal tools such as Powershell, Explorer or `dir` are not respecting hardlinks or 126 | junction points when determining the size of a directory. `diskus` does the same and counts 127 | such entries multiple times (on Unix systems, multiple hardlinks to a single file are counted 128 | just once). 129 | 130 | ## License 131 | 132 | Licensed under either of 133 | 134 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 135 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 136 | 137 | at your option. 138 | -------------------------------------------------------------------------------- /ci/.gitattributes: -------------------------------------------------------------------------------- 1 | *.bash linguist-vendored 2 | -------------------------------------------------------------------------------- /doc/diskus.1: -------------------------------------------------------------------------------- 1 | .TH DISKUS "1" 2 | .SH NAME 3 | diskus - Compute disk usage for the given filesystem entries 4 | .SH SYNOPSIS 5 | .B diskus 6 | .RB [OPTIONS] 7 | .RB [path...] 8 | .SH OPTIONS 9 | .TP 10 | \fB\-j\fR, \fB\-\-threads\fR 11 | Set the number of threads (default: 3 x num cores) 12 | .TP 13 | \fB\-\-size\-format\fR 14 | Output format for file sizes (decimal: MB, binary: MiB) [default: decimal] 15 | [possible values: decimal, binary] 16 | .TP 17 | \fB\-v\fR, \fB\-\-verbose\fR 18 | Do not hide filesystem errors 19 | .TP 20 | \fB\-b\fR, \fB\-\-apparent\-size\fR 21 | Compute apparent size instead of disk usage 22 | .TP 23 | \fB\-h\fR, \fB\-\-help\fR 24 | Prints help information 25 | .TP 26 | \fB\-V\fR, \fB\-\-version\fR 27 | Prints version information 28 | .SH ARGUMENTS 29 | .TP 30 | ... 31 | List of filesystem paths 32 | -------------------------------------------------------------------------------- /src/filesize.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy)] 2 | pub enum FilesizeType { 3 | DiskUsage, 4 | ApparentSize, 5 | } 6 | 7 | impl FilesizeType { 8 | #[cfg(not(windows))] 9 | pub fn size(self, metadata: &std::fs::Metadata) -> u64 { 10 | use std::os::unix::fs::MetadataExt; 11 | 12 | match self { 13 | FilesizeType::ApparentSize => metadata.len(), 14 | // block size is always 512 byte, see stat(2) manpage 15 | FilesizeType::DiskUsage => metadata.blocks() * 512, 16 | } 17 | } 18 | 19 | #[cfg(windows)] 20 | pub fn size(self, metadata: &std::fs::Metadata) -> u64 { 21 | metadata.len() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Basic usage 2 | //! 3 | //! ``` 4 | //! use std::path::PathBuf; 5 | //! use diskus::{Walk, FilesizeType}; 6 | //! 7 | //! let num_threads = 4; 8 | //! let root_directories = &[PathBuf::from(".")]; 9 | //! let walk = Walk::new(root_directories, num_threads, FilesizeType::DiskUsage); 10 | //! let (size_in_bytes, errors) = walk.run(); 11 | //! ``` 12 | 13 | mod filesize; 14 | mod unique_id; 15 | pub mod walk; 16 | 17 | pub use crate::filesize::FilesizeType; 18 | pub use crate::walk::{Error, Walk}; 19 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{crate_name, crate_version, App, AppSettings, Arg}; 4 | use humansize::file_size_opts::{self, FileSizeOpts}; 5 | use humansize::FileSize; 6 | use num_format::{Locale, ToFormattedString}; 7 | 8 | use diskus::{Error, FilesizeType, Walk}; 9 | 10 | fn print_result(size: u64, errors: &[Error], size_format: &FileSizeOpts, verbose: bool) { 11 | if verbose { 12 | for err in errors { 13 | match err { 14 | Error::NoMetadataForPath(path) => { 15 | eprintln!( 16 | "diskus: could not retrieve metadata for path '{}'", 17 | path.to_string_lossy() 18 | ); 19 | } 20 | Error::CouldNotReadDir(path) => { 21 | eprintln!( 22 | "diskus: could not read contents of directory '{}'", 23 | path.to_string_lossy() 24 | ); 25 | } 26 | } 27 | } 28 | } else if !errors.is_empty() { 29 | eprintln!( 30 | "[diskus warning] the results may be tainted. Re-run with -v/--verbose to print all errors." 31 | ); 32 | } 33 | 34 | if atty::is(atty::Stream::Stdout) { 35 | println!( 36 | "{} ({:} bytes)", 37 | size.file_size(size_format).unwrap(), 38 | size.to_formatted_string(&Locale::en) 39 | ); 40 | } else { 41 | println!("{}", size); 42 | } 43 | } 44 | 45 | fn main() { 46 | let app = App::new(crate_name!()) 47 | .setting(AppSettings::ColorAuto) 48 | .setting(AppSettings::ColoredHelp) 49 | .setting(AppSettings::DeriveDisplayOrder) 50 | .setting(AppSettings::UnifiedHelpMessage) 51 | .version(crate_version!()) 52 | .about("Compute disk usage for the given filesystem entries") 53 | .arg( 54 | Arg::with_name("path") 55 | .multiple(true) 56 | .help("List of filesystem paths"), 57 | ) 58 | .arg( 59 | Arg::with_name("threads") 60 | .long("threads") 61 | .short("j") 62 | .value_name("N") 63 | .takes_value(true) 64 | .help("Set the number of threads (default: 3 x num cores)"), 65 | ) 66 | .arg( 67 | Arg::with_name("size-format") 68 | .long("size-format") 69 | .takes_value(true) 70 | .value_name("type") 71 | .possible_values(&["decimal", "binary"]) 72 | .default_value("decimal") 73 | .help("Output format for file sizes (decimal: MB, binary: MiB)"), 74 | ) 75 | .arg( 76 | Arg::with_name("verbose") 77 | .long("verbose") 78 | .short("v") 79 | .takes_value(false) 80 | .help("Do not hide filesystem errors"), 81 | ); 82 | 83 | #[cfg(not(windows))] 84 | let app = app.arg( 85 | Arg::with_name("apparent-size") 86 | .long("apparent-size") 87 | .short("b") 88 | .help("Compute apparent size instead of disk usage"), 89 | ); 90 | 91 | let matches = app.get_matches(); 92 | 93 | // Setting the number of threads to 3x the number of cores is a good tradeoff between 94 | // cold-cache and warm-cache runs. For a cold disk cache, we are limited by disk IO and 95 | // therefore want the number of threads to be rather large in order for the IO scheduler to 96 | // plan ahead. On the other hand, the number of threads shouldn't be too high for warm disk 97 | // caches where we would otherwise pay a higher synchronization overhead. 98 | let num_threads = matches 99 | .value_of("threads") 100 | .and_then(|t| t.parse().ok()) 101 | .unwrap_or(3 * num_cpus::get()); 102 | 103 | let paths: Vec = matches 104 | .values_of("path") 105 | .map(|paths| paths.map(PathBuf::from).collect()) 106 | .unwrap_or_else(|| vec![PathBuf::from(".")]); 107 | 108 | let filesize_type = if matches.is_present("apparent-size") { 109 | FilesizeType::ApparentSize 110 | } else { 111 | FilesizeType::DiskUsage 112 | }; 113 | 114 | let size_format = match matches.value_of("size-format") { 115 | Some("decimal") => file_size_opts::DECIMAL, 116 | _ => file_size_opts::BINARY, 117 | }; 118 | 119 | let verbose = matches.is_present("verbose"); 120 | 121 | let walk = Walk::new(&paths, num_threads, filesize_type); 122 | let (size, errors) = walk.run(); 123 | print_result(size, &errors, &size_format, verbose); 124 | } 125 | -------------------------------------------------------------------------------- /src/unique_id.rs: -------------------------------------------------------------------------------- 1 | #[derive(Eq, PartialEq, Hash)] 2 | pub struct UniqueID { 3 | device: u64, 4 | inode: u64, 5 | } 6 | 7 | #[cfg(not(windows))] 8 | pub fn generate_unique_id(metadata: &std::fs::Metadata) -> Option { 9 | use std::os::unix::fs::MetadataExt; 10 | // If the entry has more than one hard link, generate 11 | // a unique ID consisting of device and inode in order 12 | // not to count this entry twice. 13 | if metadata.is_file() && metadata.nlink() > 1 { 14 | Some(UniqueID { 15 | device: metadata.dev(), 16 | inode: metadata.ino(), 17 | }) 18 | } else { 19 | None 20 | } 21 | } 22 | 23 | #[cfg(windows)] 24 | pub fn generate_unique_id(_metadata: &std::fs::Metadata) -> Option { 25 | // Windows-internal tools such as Powershell, Explorer or `dir` are not respecting hardlinks 26 | // or junction points when determining the size of a directory. `diskus` does the same and 27 | // counts such entries multiple times (on Unix systems, multiple hardlinks to a single file are 28 | // counted just once). 29 | // 30 | // See: https://github.com/sharkdp/diskus/issues/32 31 | None 32 | } 33 | -------------------------------------------------------------------------------- /src/walk.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::fs; 3 | use std::path::PathBuf; 4 | use std::thread; 5 | 6 | use crossbeam_channel as channel; 7 | 8 | use rayon::{self, prelude::*}; 9 | 10 | use crate::filesize::FilesizeType; 11 | use crate::unique_id::{generate_unique_id, UniqueID}; 12 | 13 | pub enum Error { 14 | NoMetadataForPath(PathBuf), 15 | CouldNotReadDir(PathBuf), 16 | } 17 | 18 | enum Message { 19 | SizeEntry(Option, u64), 20 | Error { error: Error }, 21 | } 22 | 23 | fn walk(tx: channel::Sender, entries: &[PathBuf], filesize_type: FilesizeType) { 24 | entries.into_par_iter().for_each_with(tx, |tx_ref, entry| { 25 | if let Ok(metadata) = entry.symlink_metadata() { 26 | let unique_id = generate_unique_id(&metadata); 27 | 28 | let size = filesize_type.size(&metadata); 29 | 30 | tx_ref.send(Message::SizeEntry(unique_id, size)).unwrap(); 31 | 32 | if metadata.is_dir() { 33 | let mut children = vec![]; 34 | match fs::read_dir(entry) { 35 | Ok(child_entries) => { 36 | for child_entry in child_entries.flatten() { 37 | children.push(child_entry.path()); 38 | } 39 | } 40 | Err(_) => { 41 | tx_ref 42 | .send(Message::Error { 43 | error: Error::CouldNotReadDir(entry.clone()), 44 | }) 45 | .unwrap(); 46 | } 47 | } 48 | 49 | walk(tx_ref.clone(), &children[..], filesize_type); 50 | }; 51 | } else { 52 | tx_ref 53 | .send(Message::Error { 54 | error: Error::NoMetadataForPath(entry.clone()), 55 | }) 56 | .unwrap(); 57 | }; 58 | }); 59 | } 60 | 61 | pub struct Walk<'a> { 62 | root_directories: &'a [PathBuf], 63 | num_threads: usize, 64 | filesize_type: FilesizeType, 65 | } 66 | 67 | impl Walk<'_> { 68 | pub fn new( 69 | root_directories: &[PathBuf], 70 | num_threads: usize, 71 | filesize_type: FilesizeType, 72 | ) -> Walk { 73 | Walk { 74 | root_directories, 75 | num_threads, 76 | filesize_type, 77 | } 78 | } 79 | 80 | pub fn run(&self) -> (u64, Vec) { 81 | let (tx, rx) = channel::unbounded(); 82 | 83 | let receiver_thread = thread::spawn(move || { 84 | let mut total = 0; 85 | let mut ids = HashSet::new(); 86 | let mut error_messages: Vec = Vec::new(); 87 | for msg in rx { 88 | match msg { 89 | Message::SizeEntry(unique_id, size) => { 90 | if let Some(unique_id) = unique_id { 91 | // Only count this entry if the ID has not been seen 92 | if ids.insert(unique_id) { 93 | total += size; 94 | } 95 | } else { 96 | total += size; 97 | } 98 | } 99 | Message::Error { error } => { 100 | error_messages.push(error); 101 | } 102 | } 103 | } 104 | (total, error_messages) 105 | }); 106 | 107 | let pool = rayon::ThreadPoolBuilder::new() 108 | .num_threads(self.num_threads) 109 | .build() 110 | .unwrap(); 111 | pool.install(|| walk(tx, self.root_directories, self.filesize_type)); 112 | 113 | receiver_thread.join().unwrap() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/walk.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fs::File; 3 | use std::io::Write; 4 | 5 | use tempdir::TempDir; 6 | 7 | use diskus::{FilesizeType, Walk}; 8 | 9 | #[test] 10 | fn size_of_single_file() -> Result<(), Box> { 11 | let tmp_dir = TempDir::new("diskus-tests")?; 12 | 13 | let file_path = tmp_dir.path().join("file-100-byte"); 14 | File::create(&file_path)?.write_all(&[0u8; 100])?; 15 | 16 | let num_threads = 1; 17 | let root_directories = &[file_path]; 18 | let walk = Walk::new(root_directories, num_threads, FilesizeType::ApparentSize); 19 | let (size_in_bytes, errors) = walk.run(); 20 | 21 | assert!(errors.is_empty()); 22 | assert_eq!(size_in_bytes, 100); 23 | 24 | Ok(()) 25 | } 26 | --------------------------------------------------------------------------------