├── .github ├── cover.png ├── sfz.svg └── workflows │ ├── cd.yaml │ └── ci.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── HomebrewFormula ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── pkg └── brew │ └── sfz.rb ├── src ├── cli │ ├── app.rs │ ├── args.rs │ └── mod.rs ├── extensions.rs ├── http │ ├── conditional_requests.rs │ ├── content_encoding.rs │ ├── mod.rs │ └── range_requests.rs ├── main.rs ├── server │ ├── index.html │ ├── mod.rs │ ├── res.rs │ ├── send.rs │ ├── serve.rs │ └── style.css └── test_utils.rs └── tests ├── .gitignore ├── .hidden.html ├── .hidden └── nested.html ├── dir └── ignore_pattern ├── dir_with_sub_dirs ├── file.txt └── sub_dir │ └── file.txt ├── file.txt ├── ignore_pattern ├── symlink_dir └── symlink_file.txt /.github/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/sfz/8b04beff55fd1b59a00a0c02c50e35be16e1db15/.github/cover.png -------------------------------------------------------------------------------- /.github/sfz.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | name: CD 2 | on: 3 | push: 4 | tags: 5 | - '[vV][0-9]+.[0-9]+.[0-9]+*' 6 | env: 7 | CRATE_NAME: sfz 8 | jobs: 9 | release: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | - os: ubuntu-latest 16 | target: x86_64-unknown-linux-gnu 17 | cratesio: true 18 | - os: ubuntu-latest 19 | target: x86_64-unknown-linux-musl 20 | - os: macos-latest 21 | target: x86_64-apple-darwin 22 | - os: windows-latest 23 | target: x86_64-pc-windows-msvc 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions-rs/toolchain@v1 27 | with: 28 | toolchain: stable 29 | target: ${{ matrix.target }} 30 | override: true 31 | profile: minimal 32 | - name: Preflight check 33 | uses: actions-rs/cargo@v1 34 | with: 35 | command: test 36 | args: --release --target=${{ matrix.target }} 37 | - name: Initialize workflow variables 38 | id: vars 39 | shell: bash 40 | run: | 41 | unset REF_TAG ; case ${GITHUB_REF} in refs/tags/*) REF_TAG=${GITHUB_REF#refs/tags/} ;; esac; 42 | EXE_SUFFIX= 43 | PKG_SUFFIX=".tar.gz" ; 44 | case ${{ matrix.os }} in windows-*) EXE_SUFFIX=".exe" PKG_SUFFIX=".zip" ;; esac; 45 | unset PKG_NAME ; PKG_NAME="${{ env.CRATE_NAME }}-${REF_TAG}-${{ matrix.target }}${PKG_SUFFIX}" 46 | 47 | echo ::set-output name=ARCHIVE_DIR::__archive__ 48 | echo ::set-output name=EXE_SUFFIX::${EXE_SUFFIX} 49 | echo ::set-output name=PKG_NAME::${PKG_NAME} 50 | - name: Build 51 | uses: actions-rs/cargo@v1 52 | with: 53 | command: build 54 | args: --release --target=${{ matrix.target }} 55 | - name: Package 56 | shell: bash 57 | run: | 58 | # Copy build artifacts 59 | mkdir -p "${{ steps.vars.outputs.ARCHIVE_DIR }}" 60 | cp "target/${{ matrix.target }}/release/${{ env.CRATE_NAME }}${{ steps.vars.outputs.EXE_SUFFIX }}" "${{ steps.vars.outputs.ARCHIVE_DIR }}/" 61 | 62 | # Strip binary 63 | case ${{ matrix.os }} in 64 | windows-*) ;; 65 | *) strip "${{ steps.vars.outputs.ARCHIVE_DIR }}/${{ env.CRATE_NAME }}" ;; 66 | esac; 67 | 68 | # Package binary 69 | pushd "${{ steps.vars.outputs.ARCHIVE_DIR }}/" > /dev/null 70 | case ${{ matrix.os }} in 71 | windows-*) 7z -y a '${{ steps.vars.outputs.PKG_NAME }}' * | tail -2 ;; 72 | *) tar czf '${{ steps.vars.outputs.PKG_NAME }}' * ;; 73 | esac; 74 | popd > /dev/null 75 | - name: Publish to GitHub Release 76 | uses: softprops/action-gh-release@v1 77 | with: 78 | files: | 79 | ${{ steps.vars.outputs.ARCHIVE_DIR }}/${{ steps.vars.outputs.PKG_NAME }} 80 | env: 81 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 82 | - name: Clean up working directory 83 | shell: bash 84 | run: | 85 | # Clean up working directory 86 | git clean -df 87 | - name: Publish to crates.io 88 | if: ${{ matrix.cratesio }} 89 | uses: actions-rs/cargo@v1 90 | with: 91 | command: publish 92 | env: 93 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 94 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | env: 8 | RUST_BACKTRACE: 1 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: stable 17 | components: rustfmt 18 | override: true 19 | profile: minimal 20 | - uses: actions-rs/cargo@v1 21 | with: 22 | command: fmt 23 | args: -- --check 24 | test: 25 | runs-on: ${{ matrix.os }} 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | include: 30 | - os: ubuntu-latest 31 | target: x86_64-unknown-linux-gnu 32 | toolchain: stable 33 | - os: ubuntu-latest 34 | target: x86_64-unknown-linux-musl 35 | toolchain: stable 36 | - os: ubuntu-latest 37 | target: x86_64-unknown-linux-gnu 38 | toolchain: nightly 39 | - os: macos-latest 40 | target: x86_64-apple-darwin 41 | toolchain: stable 42 | - os: windows-latest 43 | target: x86_64-pc-windows-msvc 44 | toolchain: stable 45 | steps: 46 | - uses: actions/checkout@v2 47 | - uses: actions-rs/toolchain@v1 48 | with: 49 | toolchain: ${{ matrix.toolchain }} 50 | target: ${{ matrix.target }} 51 | override: true 52 | profile: minimal 53 | - name: Test 54 | uses: actions-rs/cargo@v1 55 | with: 56 | command: test 57 | args: --target=${{ matrix.target }} 58 | coverage: 59 | if: github.ref == 'refs/heads/master' 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v2 63 | - uses: actions-rs/toolchain@v1 64 | with: 65 | toolchain: nightly 66 | override: true 67 | - uses: actions-rs/cargo@v1 68 | with: 69 | command: clean 70 | - uses: actions-rs/cargo@v1 71 | with: 72 | command: test 73 | args: --no-fail-fast 74 | env: 75 | CARGO_INCREMENTAL: '0' 76 | RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort' 77 | RUSTDOCFLAGS: '-Cpanic=abort' # Though we are not using rustdoc... 78 | - uses: actions-rs/grcov@v0.1 79 | id: coverage 80 | - uses: codecov/codecov-action@v1 81 | with: 82 | file: ${{ steps.coverage.outputs.report }} 83 | fail_ci_if_error: false 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/*.rs.bk 3 | 4 | .vscode 5 | .idea 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | Every release, along with the migration instructions, is documented on this file and Github [Releases](https://github.com/weihanglo/sfz/releases) page. 5 | 6 | ## [Unreleased](https://github.com/weihanglo/sfz/compare/v0.7.1...HEAD) 7 | 8 | ## [v0.7.1] - 2022-09-20 9 | 10 | - fix: Sfz now uses Stream to send your files :tada: ([#97][], [#99][], kudos to [@henry40408][]!) 11 | - chore: fix lint errors and deprecations ([#86][], kudos to [@henry40408][]!) 12 | - chore: dependency updates ([#88][], kudos to [@henry40408][]!) 13 | 14 | [v0.7.1]: https://github.com/weihanglo/sfz/releases/tag/v0.7.1 15 | [v0.7.1-changes]: https://github.com/weihanglo/sfz/compare/v0.7.0...v0.7.1 16 | [#86]: https://github.com/weihanglo/sfz/pull/86 17 | [#88]: https://github.com/weihanglo/sfz/pull/88 18 | [#97]: https://github.com/weihanglo/sfz/pull/97 19 | [#99]: https://github.com/weihanglo/sfz/pull/99 20 | 21 | ## [v0.7.0] - 2022-01-21 22 | 23 | - feat: add title attribute to provide a full length file name tooltip ([#78][], kudos to [@mu-arch][]!) 24 | - feat: add arg coi for cross-origin isolation ([#84][], thanks [@HKalbasi][]!) 25 | - fix: guess charset naively ([#77][]) 26 | - chore: ugprade to Rust Edition 2021 :tada: 27 | - chore: bump clap to v3 :tada: ([#82][]) 28 | - chore: cargo update ([#81][], kudos to [@henry40408][]!) 29 | 30 | [@mu-arch]: https://github.com/mu-arch 31 | [@henry40408]: https://github.com/henry40408 32 | [@HKalbasi]: https://github.com/HKalbasi 33 | [v0.7.0]: https://github.com/weihanglo/sfz/releases/tag/v0.7.0 34 | [v0.7.0-changes]: https://github.com/weihanglo/sfz/compare/v0.6.2...v0.7.0 35 | [#78]: https://github.com/weihanglo/sfz/pull/78 36 | [#81]: https://github.com/weihanglo/sfz/pull/81 37 | [#82]: https://github.com/weihanglo/sfz/pull/82 38 | [#84]: https://github.com/weihanglo/sfz/pull/84 39 | 40 | ## [v0.6.2] - 2021-10-10 41 | 42 | - fix: guess charset naively ([#77][]) 43 | 44 | [v0.6.2]: https://github.com/weihanglo/sfz/releases/tag/v0.6.2 45 | [v0.6.2-changes]: https://github.com/weihanglo/sfz/compare/v0.6.1...v0.6.2 46 | [#77]: https://github.com/weihanglo/sfz/pull/77 47 | 48 | ## [v0.6.1] - 2021-07-10 49 | 50 | - chore: bump hyper to 0.14.10 (two CVEs) ([#71][]) 51 | - fix: content-type defaults to charset=utf-8 ([#68][]) 52 | 53 | [v0.6.1]: https://github.com/weihanglo/sfz/releases/tag/v0.6.1 54 | [v0.6.1-changes]: https://github.com/weihanglo/sfz/compare/v0.6.0...v0.6.1 55 | [#68]: https://github.com/weihanglo/sfz/pull/68 56 | [#71]: https://github.com/weihanglo/sfz/pull/71 57 | 58 | ## [v0.6.0] - 2021-04-23 59 | 60 | [Changes][v0.6.0-changes] 61 | 62 | - fix: revert graceful shutdown support in order to support background job controls ([#66][]) 63 | 64 | [v0.6.0]: https://github.com/weihanglo/sfz/releases/tag/v0.6.0 65 | [v0.6.0-changes]: https://github.com/weihanglo/sfz/compare/v0.5.0...v0.6.0 66 | [#66]: https://github.com/weihanglo/sfz/pull/66 67 | 68 | ## [v0.5.0] - 2021-04-19 69 | 70 | [Changes][v0.5.0-changes] 71 | 72 | - feat: support graceful shutdown via Ctrl-D ([#63][], thanks [@sayanarijit][]!) 73 | 74 | [@sayanarijit]: https://github.com/sayanarijit 75 | [v0.5.0]: https://github.com/weihanglo/sfz/releases/tag/v0.5.0 76 | [v0.5.0-changes]: https://github.com/weihanglo/sfz/compare/v0.4.0...v0.5.0 77 | [#63]: https://github.com/weihanglo/sfz/pull/63 78 | 79 | ## [v0.4.0] - 2021-03-17 80 | 81 | [Changes][v0.4.0-changes] 82 | 83 | - Bugfix: Handle paths on Windows properly ([#53][], kudos to [@lunar-mycroft][]!) 84 | - Internal: Upgraded to Tokio v1 and Hyper v0.14 85 | 86 | [@lunar-mycroft]: https://github.com/lunar-mycroft 87 | [v0.4.0]: https://github.com/weihanglo/sfz/releases/tag/v0.4.0 88 | [v0.4.0-changes]: https://github.com/weihanglo/sfz/compare/v0.3.0...v0.4.0 89 | [#53]: https://github.com/weihanglo/sfz/pull/53 90 | 91 | ## [v0.3.0] - 2020-10-24 92 | 93 | [Changes][v0.3.0-changes] 94 | 95 | - New feature: Download directory as zip ([#50][], kudos to [@whizsid][]!) 96 | 97 | [@whizsid]: https://github.com/whizsid 98 | [v0.3.0]: https://github.com/weihanglo/sfz/releases/tag/v0.3.0 99 | [v0.3.0-changes]: https://github.com/weihanglo/sfz/compare/v0.2.1...v0.3.0 100 | [#50]: https://github.com/weihanglo/sfz/pull/50 101 | 102 | ## [v0.2.1] - 2020-09-04 103 | 104 | [Changes][v0.2.1-changes] 105 | 106 | - **Breaking**: Default adress from 0.0.0.0 to 127.0.0.1 107 | - Bugfix: Fixed missing prefix slash for path-prefix ([#48][]) 108 | - Internal: Refactored `cli` module ([#47][]) 109 | - Internal: Splited `send::send_dir` function 110 | - Internal: Added lots of unit tests 111 | 112 | [v0.2.1]: https://github.com/weihanglo/sfz/releases/tag/v0.2.1 113 | [v0.2.1-changes]: https://github.com/weihanglo/sfz/compare/v0.2.0...v0.2.1 114 | [#47]: https://github.com/weihanglo/sfz/pull/47 115 | [#48]: https://github.com/weihanglo/sfz/pull/48 116 | 117 | ## [v0.2.0] - 2020-08-31 118 | 119 | [Changes][v0.2.0-changes] 120 | 121 | - Internal: Renamed `PathExt::is_hidden` to `PathExt::is_relatively_hidden` and now would check if any parent path component is prefixed with a dot. ([#46][]) 122 | - Internal: Switched CI provider to GitHub Action 123 | - Internal: Upgraded lots of dependencies ([#41][]), including significant refactor on hyper 0.11 to 0.13 ([#42][]) 124 | 125 | [v0.2.0]: https://github.com/weihanglo/sfz/releases/tag/v0.2.0 126 | [v0.2.0-changes]: https://github.com/weihanglo/sfz/compare/v0.1.2...v0.2.0 127 | [#41]: https://github.com/weihanglo/sfz/pull/41 128 | [#42]: https://github.com/weihanglo/sfz/pull/42 129 | [#46]: https://github.com/weihanglo/sfz/pull/46 130 | 131 | ## [v0.1.2] - 2020-08-28 132 | 133 | [Changes][v0.1.2-changes] 134 | 135 | - Fixed range header off-by-one error ([#39](https://github.com/weihanglo/sfz/issues/39)) 136 | 137 | [v0.1.2]: https://github.com/weihanglo/sfz/releases/tag/v0.1.2 138 | [v0.1.2-changes]: https://github.com/weihanglo/sfz/compare/0.1.1...v0.1.2 139 | 140 | ## [0.1.1] - 2020-06-04 141 | 142 | [Changes][0.1.1-changes] 143 | 144 | - Fixed duplicated prefix slash regression issue ([#31](https://github.com/weihanglo/sfz/issues/31)) 145 | 146 | [0.1.1]: https://github.com/weihanglo/sfz/releases/tag/0.1.1 147 | [0.1.1-changes]: https://github.com/weihanglo/sfz/compare/0.1.0...0.1.1 148 | 149 | ## [0.1.0] - 2020-05-01 150 | 151 | [Changes][0.1.0-changes] 152 | 153 | - Added new flag `--path-prefix` to customize path prefix when serving content (credit to [@jxs](https://github.com/jxs)) 154 | 155 | [0.1.0]: https://github.com/weihanglo/sfz/releases/tag/0.1.0 156 | [0.1.0-changes]: https://github.com/weihanglo/sfz/compare/0.0.4...0.1.0 157 | 158 | ## [0.0.4] - 2019-09-07 159 | 160 | [Changes][0.0.4-changes] 161 | 162 | - Added new feature: logs request/response by default. 163 | - Added new option flag `--no-log` to disable request/response logging. 164 | - Updated to Rust 2018 edition. 165 | - Upgraded dependency `mime_guess` from 2.0.0-alpha to 2.0. 166 | - Upgraded dependency `percent-encoding` from 1.0 to 2.1. 167 | - Upgraded dependency `brotli` from 1.1 to 3. 168 | - Upgraded dependency `unicase` from 2.1 to 2.5. 169 | 170 | [0.0.4]: https://github.com/weihanglo/sfz/releases/tag/0.0.4 171 | [0.0.4-changes]: https://github.com/weihanglo/sfz/compare/0.0.3...0.0.4 172 | 173 | ## [0.0.3] - 2018-03-07 174 | 175 | [Changes][0.0.3-changes] 176 | 177 | - Handled error with some human-readable format. 178 | - Added new command arg `--render--index` to automatically render index file such as `index.html`. 179 | - Updated some command args' short names, default values and descriptions. 180 | 181 | [0.0.3]: https://github.com/weihanglo/sfz/releases/tag/0.0.3 182 | [0.0.3-changes]: https://github.com/weihanglo/sfz/compare/0.0.2...0.0.3 183 | 184 | ## [0.0.2] - 2018-03-03 185 | 186 | First release version on [Crates.io][crate-sfz]! 187 | 188 | [Changes][0.0.2-changes] 189 | 190 | - Hombrew formula for sfz! You can now donwload sfz via homebrew from GitHub. 191 | - Fixed missing `ETag` and `Last-Modified` header fields. 192 | - Fixed unsecure symlink following. 193 | 194 | [0.0.2]: https://github.com/weihanglo/sfz/releases/tag/0.0.2 195 | [0.0.2-changes]: https://github.com/weihanglo/sfz/compare/0.0.1-beta.1...0.0.2 196 | 197 | ## [0.0.1-beta.1] - 2018-03-02 198 | 199 | Beta release. 200 | 201 | [0.0.1-beta.1]: https://github.com/weihanglo/sfz/releases/tag/0.0.1-beta.1 202 | 203 | [crate-sfz]: https://crates.io/crates/sfz 204 | -------------------------------------------------------------------------------- /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 = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "0.7.19" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "alloc-no-stdlib" 22 | version = "2.0.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3" 25 | 26 | [[package]] 27 | name = "alloc-stdlib" 28 | version = "0.2.1" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" 31 | dependencies = [ 32 | "alloc-no-stdlib", 33 | ] 34 | 35 | [[package]] 36 | name = "android_system_properties" 37 | version = "0.1.5" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 40 | dependencies = [ 41 | "libc", 42 | ] 43 | 44 | [[package]] 45 | name = "async-compression" 46 | version = "0.3.14" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "345fd392ab01f746c717b1357165b76f0b67a60192007b234058c9045fdcf695" 49 | dependencies = [ 50 | "brotli", 51 | "flate2", 52 | "futures-core", 53 | "memchr", 54 | "pin-project-lite", 55 | "tokio", 56 | ] 57 | 58 | [[package]] 59 | name = "autocfg" 60 | version = "1.1.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 63 | 64 | [[package]] 65 | name = "base64" 66 | version = "0.13.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 69 | 70 | [[package]] 71 | name = "bitflags" 72 | version = "1.3.2" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 75 | 76 | [[package]] 77 | name = "block-buffer" 78 | version = "0.10.3" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" 81 | dependencies = [ 82 | "generic-array", 83 | ] 84 | 85 | [[package]] 86 | name = "brotli" 87 | version = "3.3.4" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" 90 | dependencies = [ 91 | "alloc-no-stdlib", 92 | "alloc-stdlib", 93 | "brotli-decompressor", 94 | ] 95 | 96 | [[package]] 97 | name = "brotli-decompressor" 98 | version = "2.3.2" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" 101 | dependencies = [ 102 | "alloc-no-stdlib", 103 | "alloc-stdlib", 104 | ] 105 | 106 | [[package]] 107 | name = "bstr" 108 | version = "0.2.17" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" 111 | dependencies = [ 112 | "memchr", 113 | ] 114 | 115 | [[package]] 116 | name = "bumpalo" 117 | version = "3.11.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" 120 | 121 | [[package]] 122 | name = "byteorder" 123 | version = "1.4.3" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 126 | 127 | [[package]] 128 | name = "bytes" 129 | version = "1.2.1" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" 132 | 133 | [[package]] 134 | name = "cfg-if" 135 | version = "1.0.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 138 | 139 | [[package]] 140 | name = "chrono" 141 | version = "0.4.22" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" 144 | dependencies = [ 145 | "iana-time-zone", 146 | "js-sys", 147 | "num-integer", 148 | "num-traits", 149 | "time", 150 | "wasm-bindgen", 151 | "winapi", 152 | ] 153 | 154 | [[package]] 155 | name = "chrono-tz" 156 | version = "0.6.3" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "29c39203181991a7dd4343b8005bd804e7a9a37afb8ac070e43771e8c820bbde" 159 | dependencies = [ 160 | "chrono", 161 | "chrono-tz-build", 162 | "phf", 163 | ] 164 | 165 | [[package]] 166 | name = "chrono-tz-build" 167 | version = "0.0.3" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "6f509c3a87b33437b05e2458750a0700e5bdd6956176773e6c7d6dd15a283a0c" 170 | dependencies = [ 171 | "parse-zoneinfo", 172 | "phf", 173 | "phf_codegen", 174 | ] 175 | 176 | [[package]] 177 | name = "clap" 178 | version = "3.2.20" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd" 181 | dependencies = [ 182 | "bitflags", 183 | "clap_lex", 184 | "indexmap", 185 | "once_cell", 186 | "textwrap", 187 | ] 188 | 189 | [[package]] 190 | name = "clap_lex" 191 | version = "0.2.4" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 194 | dependencies = [ 195 | "os_str_bytes", 196 | ] 197 | 198 | [[package]] 199 | name = "core-foundation-sys" 200 | version = "0.8.3" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 203 | 204 | [[package]] 205 | name = "cpufeatures" 206 | version = "0.2.5" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" 209 | dependencies = [ 210 | "libc", 211 | ] 212 | 213 | [[package]] 214 | name = "crc32fast" 215 | version = "1.3.2" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 218 | dependencies = [ 219 | "cfg-if", 220 | ] 221 | 222 | [[package]] 223 | name = "crossbeam-utils" 224 | version = "0.8.11" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" 227 | dependencies = [ 228 | "cfg-if", 229 | "once_cell", 230 | ] 231 | 232 | [[package]] 233 | name = "crypto-common" 234 | version = "0.1.6" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 237 | dependencies = [ 238 | "generic-array", 239 | "typenum", 240 | ] 241 | 242 | [[package]] 243 | name = "deunicode" 244 | version = "0.4.3" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690" 247 | 248 | [[package]] 249 | name = "digest" 250 | version = "0.10.3" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" 253 | dependencies = [ 254 | "block-buffer", 255 | "crypto-common", 256 | ] 257 | 258 | [[package]] 259 | name = "fastrand" 260 | version = "1.8.0" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" 263 | dependencies = [ 264 | "instant", 265 | ] 266 | 267 | [[package]] 268 | name = "flate2" 269 | version = "1.0.24" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" 272 | dependencies = [ 273 | "crc32fast", 274 | "miniz_oxide", 275 | ] 276 | 277 | [[package]] 278 | name = "fnv" 279 | version = "1.0.7" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 282 | 283 | [[package]] 284 | name = "futures" 285 | version = "0.3.24" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" 288 | dependencies = [ 289 | "futures-channel", 290 | "futures-core", 291 | "futures-executor", 292 | "futures-io", 293 | "futures-sink", 294 | "futures-task", 295 | "futures-util", 296 | ] 297 | 298 | [[package]] 299 | name = "futures-channel" 300 | version = "0.3.24" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" 303 | dependencies = [ 304 | "futures-core", 305 | "futures-sink", 306 | ] 307 | 308 | [[package]] 309 | name = "futures-core" 310 | version = "0.3.24" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" 313 | 314 | [[package]] 315 | name = "futures-executor" 316 | version = "0.3.24" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" 319 | dependencies = [ 320 | "futures-core", 321 | "futures-task", 322 | "futures-util", 323 | ] 324 | 325 | [[package]] 326 | name = "futures-io" 327 | version = "0.3.24" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" 330 | 331 | [[package]] 332 | name = "futures-macro" 333 | version = "0.3.24" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" 336 | dependencies = [ 337 | "proc-macro2", 338 | "quote", 339 | "syn", 340 | ] 341 | 342 | [[package]] 343 | name = "futures-sink" 344 | version = "0.3.24" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" 347 | 348 | [[package]] 349 | name = "futures-task" 350 | version = "0.3.24" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" 353 | 354 | [[package]] 355 | name = "futures-util" 356 | version = "0.3.24" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" 359 | dependencies = [ 360 | "futures-channel", 361 | "futures-core", 362 | "futures-io", 363 | "futures-macro", 364 | "futures-sink", 365 | "futures-task", 366 | "memchr", 367 | "pin-project-lite", 368 | "pin-utils", 369 | "slab", 370 | ] 371 | 372 | [[package]] 373 | name = "generic-array" 374 | version = "0.14.6" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" 377 | dependencies = [ 378 | "typenum", 379 | "version_check", 380 | ] 381 | 382 | [[package]] 383 | name = "getrandom" 384 | version = "0.2.7" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 387 | dependencies = [ 388 | "cfg-if", 389 | "libc", 390 | "wasi 0.11.0+wasi-snapshot-preview1", 391 | ] 392 | 393 | [[package]] 394 | name = "globset" 395 | version = "0.4.9" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" 398 | dependencies = [ 399 | "aho-corasick", 400 | "bstr", 401 | "fnv", 402 | "log", 403 | "regex", 404 | ] 405 | 406 | [[package]] 407 | name = "globwalk" 408 | version = "0.8.1" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" 411 | dependencies = [ 412 | "bitflags", 413 | "ignore", 414 | "walkdir", 415 | ] 416 | 417 | [[package]] 418 | name = "hashbrown" 419 | version = "0.12.3" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 422 | 423 | [[package]] 424 | name = "headers" 425 | version = "0.3.8" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" 428 | dependencies = [ 429 | "base64", 430 | "bitflags", 431 | "bytes", 432 | "headers-core", 433 | "http", 434 | "httpdate", 435 | "mime", 436 | "sha1", 437 | ] 438 | 439 | [[package]] 440 | name = "headers-core" 441 | version = "0.2.0" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" 444 | dependencies = [ 445 | "http", 446 | ] 447 | 448 | [[package]] 449 | name = "hermit-abi" 450 | version = "0.1.19" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 453 | dependencies = [ 454 | "libc", 455 | ] 456 | 457 | [[package]] 458 | name = "http" 459 | version = "0.2.8" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" 462 | dependencies = [ 463 | "bytes", 464 | "fnv", 465 | "itoa", 466 | ] 467 | 468 | [[package]] 469 | name = "http-body" 470 | version = "0.4.5" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 473 | dependencies = [ 474 | "bytes", 475 | "http", 476 | "pin-project-lite", 477 | ] 478 | 479 | [[package]] 480 | name = "httparse" 481 | version = "1.8.0" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 484 | 485 | [[package]] 486 | name = "httpdate" 487 | version = "1.0.2" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 490 | 491 | [[package]] 492 | name = "humansize" 493 | version = "1.1.1" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" 496 | 497 | [[package]] 498 | name = "hyper" 499 | version = "0.14.20" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" 502 | dependencies = [ 503 | "bytes", 504 | "futures-channel", 505 | "futures-core", 506 | "futures-util", 507 | "http", 508 | "http-body", 509 | "httparse", 510 | "httpdate", 511 | "itoa", 512 | "pin-project-lite", 513 | "socket2", 514 | "tokio", 515 | "tower-service", 516 | "tracing", 517 | "want", 518 | ] 519 | 520 | [[package]] 521 | name = "iana-time-zone" 522 | version = "0.1.47" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "4c495f162af0bf17656d0014a0eded5f3cd2f365fdd204548c2869db89359dc7" 525 | dependencies = [ 526 | "android_system_properties", 527 | "core-foundation-sys", 528 | "js-sys", 529 | "once_cell", 530 | "wasm-bindgen", 531 | "winapi", 532 | ] 533 | 534 | [[package]] 535 | name = "ignore" 536 | version = "0.4.18" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" 539 | dependencies = [ 540 | "crossbeam-utils", 541 | "globset", 542 | "lazy_static", 543 | "log", 544 | "memchr", 545 | "regex", 546 | "same-file", 547 | "thread_local", 548 | "walkdir", 549 | "winapi-util", 550 | ] 551 | 552 | [[package]] 553 | name = "indexmap" 554 | version = "1.9.1" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" 557 | dependencies = [ 558 | "autocfg", 559 | "hashbrown", 560 | ] 561 | 562 | [[package]] 563 | name = "instant" 564 | version = "0.1.12" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 567 | dependencies = [ 568 | "cfg-if", 569 | ] 570 | 571 | [[package]] 572 | name = "itoa" 573 | version = "1.0.3" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" 576 | 577 | [[package]] 578 | name = "js-sys" 579 | version = "0.3.59" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" 582 | dependencies = [ 583 | "wasm-bindgen", 584 | ] 585 | 586 | [[package]] 587 | name = "lazy_static" 588 | version = "1.4.0" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 591 | 592 | [[package]] 593 | name = "libc" 594 | version = "0.2.132" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" 597 | 598 | [[package]] 599 | name = "log" 600 | version = "0.4.17" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 603 | dependencies = [ 604 | "cfg-if", 605 | ] 606 | 607 | [[package]] 608 | name = "memchr" 609 | version = "2.5.0" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 612 | 613 | [[package]] 614 | name = "mime" 615 | version = "0.3.16" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 618 | 619 | [[package]] 620 | name = "mime_guess" 621 | version = "2.0.4" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" 624 | dependencies = [ 625 | "mime", 626 | "unicase", 627 | ] 628 | 629 | [[package]] 630 | name = "miniz_oxide" 631 | version = "0.5.4" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" 634 | dependencies = [ 635 | "adler", 636 | ] 637 | 638 | [[package]] 639 | name = "mio" 640 | version = "0.8.4" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" 643 | dependencies = [ 644 | "libc", 645 | "log", 646 | "wasi 0.11.0+wasi-snapshot-preview1", 647 | "windows-sys", 648 | ] 649 | 650 | [[package]] 651 | name = "num-integer" 652 | version = "0.1.45" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 655 | dependencies = [ 656 | "autocfg", 657 | "num-traits", 658 | ] 659 | 660 | [[package]] 661 | name = "num-traits" 662 | version = "0.2.15" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 665 | dependencies = [ 666 | "autocfg", 667 | ] 668 | 669 | [[package]] 670 | name = "num_cpus" 671 | version = "1.13.1" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 674 | dependencies = [ 675 | "hermit-abi", 676 | "libc", 677 | ] 678 | 679 | [[package]] 680 | name = "once_cell" 681 | version = "1.14.0" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" 684 | 685 | [[package]] 686 | name = "os_str_bytes" 687 | version = "6.3.0" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" 690 | 691 | [[package]] 692 | name = "parse-zoneinfo" 693 | version = "0.3.0" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" 696 | dependencies = [ 697 | "regex", 698 | ] 699 | 700 | [[package]] 701 | name = "percent-encoding" 702 | version = "2.1.0" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 705 | 706 | [[package]] 707 | name = "pest" 708 | version = "2.3.0" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "4b0560d531d1febc25a3c9398a62a71256c0178f2e3443baedd9ad4bb8c9deb4" 711 | dependencies = [ 712 | "thiserror", 713 | "ucd-trie", 714 | ] 715 | 716 | [[package]] 717 | name = "pest_derive" 718 | version = "2.3.0" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "905708f7f674518498c1f8d644481440f476d39ca6ecae83319bba7c6c12da91" 721 | dependencies = [ 722 | "pest", 723 | "pest_generator", 724 | ] 725 | 726 | [[package]] 727 | name = "pest_generator" 728 | version = "2.3.0" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "5803d8284a629cc999094ecd630f55e91b561a1d1ba75e233b00ae13b91a69ad" 731 | dependencies = [ 732 | "pest", 733 | "pest_meta", 734 | "proc-macro2", 735 | "quote", 736 | "syn", 737 | ] 738 | 739 | [[package]] 740 | name = "pest_meta" 741 | version = "2.3.0" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "1538eb784f07615c6d9a8ab061089c6c54a344c5b4301db51990ca1c241e8c04" 744 | dependencies = [ 745 | "once_cell", 746 | "pest", 747 | "sha-1", 748 | ] 749 | 750 | [[package]] 751 | name = "phf" 752 | version = "0.11.1" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" 755 | dependencies = [ 756 | "phf_shared", 757 | ] 758 | 759 | [[package]] 760 | name = "phf_codegen" 761 | version = "0.11.1" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" 764 | dependencies = [ 765 | "phf_generator", 766 | "phf_shared", 767 | ] 768 | 769 | [[package]] 770 | name = "phf_generator" 771 | version = "0.11.1" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" 774 | dependencies = [ 775 | "phf_shared", 776 | "rand", 777 | ] 778 | 779 | [[package]] 780 | name = "phf_shared" 781 | version = "0.11.1" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" 784 | dependencies = [ 785 | "siphasher", 786 | "uncased", 787 | ] 788 | 789 | [[package]] 790 | name = "pin-project-lite" 791 | version = "0.2.9" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 794 | 795 | [[package]] 796 | name = "pin-utils" 797 | version = "0.1.0" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 800 | 801 | [[package]] 802 | name = "ppv-lite86" 803 | version = "0.2.16" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 806 | 807 | [[package]] 808 | name = "proc-macro2" 809 | version = "1.0.43" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" 812 | dependencies = [ 813 | "unicode-ident", 814 | ] 815 | 816 | [[package]] 817 | name = "qstring" 818 | version = "0.7.2" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" 821 | dependencies = [ 822 | "percent-encoding", 823 | ] 824 | 825 | [[package]] 826 | name = "quote" 827 | version = "1.0.21" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 830 | dependencies = [ 831 | "proc-macro2", 832 | ] 833 | 834 | [[package]] 835 | name = "rand" 836 | version = "0.8.5" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 839 | dependencies = [ 840 | "libc", 841 | "rand_chacha", 842 | "rand_core", 843 | ] 844 | 845 | [[package]] 846 | name = "rand_chacha" 847 | version = "0.3.1" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 850 | dependencies = [ 851 | "ppv-lite86", 852 | "rand_core", 853 | ] 854 | 855 | [[package]] 856 | name = "rand_core" 857 | version = "0.6.3" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 860 | dependencies = [ 861 | "getrandom", 862 | ] 863 | 864 | [[package]] 865 | name = "redox_syscall" 866 | version = "0.2.16" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 869 | dependencies = [ 870 | "bitflags", 871 | ] 872 | 873 | [[package]] 874 | name = "regex" 875 | version = "1.6.0" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" 878 | dependencies = [ 879 | "aho-corasick", 880 | "memchr", 881 | "regex-syntax", 882 | ] 883 | 884 | [[package]] 885 | name = "regex-syntax" 886 | version = "0.6.27" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 889 | 890 | [[package]] 891 | name = "remove_dir_all" 892 | version = "0.5.3" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 895 | dependencies = [ 896 | "winapi", 897 | ] 898 | 899 | [[package]] 900 | name = "ryu" 901 | version = "1.0.11" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" 904 | 905 | [[package]] 906 | name = "same-file" 907 | version = "1.0.6" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 910 | dependencies = [ 911 | "winapi-util", 912 | ] 913 | 914 | [[package]] 915 | name = "serde" 916 | version = "1.0.144" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" 919 | dependencies = [ 920 | "serde_derive", 921 | ] 922 | 923 | [[package]] 924 | name = "serde_derive" 925 | version = "1.0.144" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" 928 | dependencies = [ 929 | "proc-macro2", 930 | "quote", 931 | "syn", 932 | ] 933 | 934 | [[package]] 935 | name = "serde_json" 936 | version = "1.0.85" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" 939 | dependencies = [ 940 | "itoa", 941 | "ryu", 942 | "serde", 943 | ] 944 | 945 | [[package]] 946 | name = "sfz" 947 | version = "0.7.1" 948 | dependencies = [ 949 | "async-compression", 950 | "bytes", 951 | "chrono", 952 | "clap", 953 | "futures", 954 | "headers", 955 | "hyper", 956 | "ignore", 957 | "mime_guess", 958 | "once_cell", 959 | "percent-encoding", 960 | "qstring", 961 | "serde", 962 | "tempfile", 963 | "tera", 964 | "tokio", 965 | "tokio-util", 966 | "zip", 967 | ] 968 | 969 | [[package]] 970 | name = "sha-1" 971 | version = "0.10.0" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" 974 | dependencies = [ 975 | "cfg-if", 976 | "cpufeatures", 977 | "digest", 978 | ] 979 | 980 | [[package]] 981 | name = "sha1" 982 | version = "0.10.4" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "006769ba83e921b3085caa8334186b00cf92b4cb1a6cf4632fbccc8eff5c7549" 985 | dependencies = [ 986 | "cfg-if", 987 | "cpufeatures", 988 | "digest", 989 | ] 990 | 991 | [[package]] 992 | name = "siphasher" 993 | version = "0.3.10" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" 996 | 997 | [[package]] 998 | name = "slab" 999 | version = "0.4.7" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" 1002 | dependencies = [ 1003 | "autocfg", 1004 | ] 1005 | 1006 | [[package]] 1007 | name = "slug" 1008 | version = "0.1.4" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" 1011 | dependencies = [ 1012 | "deunicode", 1013 | ] 1014 | 1015 | [[package]] 1016 | name = "socket2" 1017 | version = "0.4.7" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" 1020 | dependencies = [ 1021 | "libc", 1022 | "winapi", 1023 | ] 1024 | 1025 | [[package]] 1026 | name = "syn" 1027 | version = "1.0.99" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" 1030 | dependencies = [ 1031 | "proc-macro2", 1032 | "quote", 1033 | "unicode-ident", 1034 | ] 1035 | 1036 | [[package]] 1037 | name = "tempfile" 1038 | version = "3.3.0" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 1041 | dependencies = [ 1042 | "cfg-if", 1043 | "fastrand", 1044 | "libc", 1045 | "redox_syscall", 1046 | "remove_dir_all", 1047 | "winapi", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "tera" 1052 | version = "1.17.0" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "1d4685e72cb35f0eb74319c8fe2d3b61e93da5609841cde2cb87fcc3bea56d20" 1055 | dependencies = [ 1056 | "chrono", 1057 | "chrono-tz", 1058 | "globwalk", 1059 | "humansize", 1060 | "lazy_static", 1061 | "percent-encoding", 1062 | "pest", 1063 | "pest_derive", 1064 | "rand", 1065 | "regex", 1066 | "serde", 1067 | "serde_json", 1068 | "slug", 1069 | "unic-segment", 1070 | ] 1071 | 1072 | [[package]] 1073 | name = "textwrap" 1074 | version = "0.15.0" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" 1077 | 1078 | [[package]] 1079 | name = "thiserror" 1080 | version = "1.0.34" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252" 1083 | dependencies = [ 1084 | "thiserror-impl", 1085 | ] 1086 | 1087 | [[package]] 1088 | name = "thiserror-impl" 1089 | version = "1.0.34" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487" 1092 | dependencies = [ 1093 | "proc-macro2", 1094 | "quote", 1095 | "syn", 1096 | ] 1097 | 1098 | [[package]] 1099 | name = "thread_local" 1100 | version = "1.1.4" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 1103 | dependencies = [ 1104 | "once_cell", 1105 | ] 1106 | 1107 | [[package]] 1108 | name = "time" 1109 | version = "0.1.44" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 1112 | dependencies = [ 1113 | "libc", 1114 | "wasi 0.10.0+wasi-snapshot-preview1", 1115 | "winapi", 1116 | ] 1117 | 1118 | [[package]] 1119 | name = "tokio" 1120 | version = "1.21.0" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "89797afd69d206ccd11fb0ea560a44bbb87731d020670e79416d442919257d42" 1123 | dependencies = [ 1124 | "autocfg", 1125 | "libc", 1126 | "mio", 1127 | "num_cpus", 1128 | "once_cell", 1129 | "pin-project-lite", 1130 | "socket2", 1131 | "tokio-macros", 1132 | "winapi", 1133 | ] 1134 | 1135 | [[package]] 1136 | name = "tokio-macros" 1137 | version = "1.8.0" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" 1140 | dependencies = [ 1141 | "proc-macro2", 1142 | "quote", 1143 | "syn", 1144 | ] 1145 | 1146 | [[package]] 1147 | name = "tokio-util" 1148 | version = "0.7.4" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" 1151 | dependencies = [ 1152 | "bytes", 1153 | "futures-core", 1154 | "futures-sink", 1155 | "pin-project-lite", 1156 | "tokio", 1157 | ] 1158 | 1159 | [[package]] 1160 | name = "tower-service" 1161 | version = "0.3.2" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1164 | 1165 | [[package]] 1166 | name = "tracing" 1167 | version = "0.1.36" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" 1170 | dependencies = [ 1171 | "cfg-if", 1172 | "pin-project-lite", 1173 | "tracing-core", 1174 | ] 1175 | 1176 | [[package]] 1177 | name = "tracing-core" 1178 | version = "0.1.29" 1179 | source = "registry+https://github.com/rust-lang/crates.io-index" 1180 | checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" 1181 | dependencies = [ 1182 | "once_cell", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "try-lock" 1187 | version = "0.2.3" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 1190 | 1191 | [[package]] 1192 | name = "typenum" 1193 | version = "1.15.0" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" 1196 | 1197 | [[package]] 1198 | name = "ucd-trie" 1199 | version = "0.1.5" 1200 | source = "registry+https://github.com/rust-lang/crates.io-index" 1201 | checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" 1202 | 1203 | [[package]] 1204 | name = "uncased" 1205 | version = "0.9.7" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622" 1208 | dependencies = [ 1209 | "version_check", 1210 | ] 1211 | 1212 | [[package]] 1213 | name = "unic-char-property" 1214 | version = "0.9.0" 1215 | source = "registry+https://github.com/rust-lang/crates.io-index" 1216 | checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" 1217 | dependencies = [ 1218 | "unic-char-range", 1219 | ] 1220 | 1221 | [[package]] 1222 | name = "unic-char-range" 1223 | version = "0.9.0" 1224 | source = "registry+https://github.com/rust-lang/crates.io-index" 1225 | checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" 1226 | 1227 | [[package]] 1228 | name = "unic-common" 1229 | version = "0.9.0" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" 1232 | 1233 | [[package]] 1234 | name = "unic-segment" 1235 | version = "0.9.0" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" 1238 | dependencies = [ 1239 | "unic-ucd-segment", 1240 | ] 1241 | 1242 | [[package]] 1243 | name = "unic-ucd-segment" 1244 | version = "0.9.0" 1245 | source = "registry+https://github.com/rust-lang/crates.io-index" 1246 | checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" 1247 | dependencies = [ 1248 | "unic-char-property", 1249 | "unic-char-range", 1250 | "unic-ucd-version", 1251 | ] 1252 | 1253 | [[package]] 1254 | name = "unic-ucd-version" 1255 | version = "0.9.0" 1256 | source = "registry+https://github.com/rust-lang/crates.io-index" 1257 | checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" 1258 | dependencies = [ 1259 | "unic-common", 1260 | ] 1261 | 1262 | [[package]] 1263 | name = "unicase" 1264 | version = "2.6.0" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 1267 | dependencies = [ 1268 | "version_check", 1269 | ] 1270 | 1271 | [[package]] 1272 | name = "unicode-ident" 1273 | version = "1.0.3" 1274 | source = "registry+https://github.com/rust-lang/crates.io-index" 1275 | checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" 1276 | 1277 | [[package]] 1278 | name = "version_check" 1279 | version = "0.9.4" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1282 | 1283 | [[package]] 1284 | name = "walkdir" 1285 | version = "2.3.2" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 1288 | dependencies = [ 1289 | "same-file", 1290 | "winapi", 1291 | "winapi-util", 1292 | ] 1293 | 1294 | [[package]] 1295 | name = "want" 1296 | version = "0.3.0" 1297 | source = "registry+https://github.com/rust-lang/crates.io-index" 1298 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1299 | dependencies = [ 1300 | "log", 1301 | "try-lock", 1302 | ] 1303 | 1304 | [[package]] 1305 | name = "wasi" 1306 | version = "0.10.0+wasi-snapshot-preview1" 1307 | source = "registry+https://github.com/rust-lang/crates.io-index" 1308 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 1309 | 1310 | [[package]] 1311 | name = "wasi" 1312 | version = "0.11.0+wasi-snapshot-preview1" 1313 | source = "registry+https://github.com/rust-lang/crates.io-index" 1314 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1315 | 1316 | [[package]] 1317 | name = "wasm-bindgen" 1318 | version = "0.2.82" 1319 | source = "registry+https://github.com/rust-lang/crates.io-index" 1320 | checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" 1321 | dependencies = [ 1322 | "cfg-if", 1323 | "wasm-bindgen-macro", 1324 | ] 1325 | 1326 | [[package]] 1327 | name = "wasm-bindgen-backend" 1328 | version = "0.2.82" 1329 | source = "registry+https://github.com/rust-lang/crates.io-index" 1330 | checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" 1331 | dependencies = [ 1332 | "bumpalo", 1333 | "log", 1334 | "once_cell", 1335 | "proc-macro2", 1336 | "quote", 1337 | "syn", 1338 | "wasm-bindgen-shared", 1339 | ] 1340 | 1341 | [[package]] 1342 | name = "wasm-bindgen-macro" 1343 | version = "0.2.82" 1344 | source = "registry+https://github.com/rust-lang/crates.io-index" 1345 | checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" 1346 | dependencies = [ 1347 | "quote", 1348 | "wasm-bindgen-macro-support", 1349 | ] 1350 | 1351 | [[package]] 1352 | name = "wasm-bindgen-macro-support" 1353 | version = "0.2.82" 1354 | source = "registry+https://github.com/rust-lang/crates.io-index" 1355 | checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" 1356 | dependencies = [ 1357 | "proc-macro2", 1358 | "quote", 1359 | "syn", 1360 | "wasm-bindgen-backend", 1361 | "wasm-bindgen-shared", 1362 | ] 1363 | 1364 | [[package]] 1365 | name = "wasm-bindgen-shared" 1366 | version = "0.2.82" 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" 1368 | checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" 1369 | 1370 | [[package]] 1371 | name = "winapi" 1372 | version = "0.3.9" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1375 | dependencies = [ 1376 | "winapi-i686-pc-windows-gnu", 1377 | "winapi-x86_64-pc-windows-gnu", 1378 | ] 1379 | 1380 | [[package]] 1381 | name = "winapi-i686-pc-windows-gnu" 1382 | version = "0.4.0" 1383 | source = "registry+https://github.com/rust-lang/crates.io-index" 1384 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1385 | 1386 | [[package]] 1387 | name = "winapi-util" 1388 | version = "0.1.5" 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" 1390 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1391 | dependencies = [ 1392 | "winapi", 1393 | ] 1394 | 1395 | [[package]] 1396 | name = "winapi-x86_64-pc-windows-gnu" 1397 | version = "0.4.0" 1398 | source = "registry+https://github.com/rust-lang/crates.io-index" 1399 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1400 | 1401 | [[package]] 1402 | name = "windows-sys" 1403 | version = "0.36.1" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 1406 | dependencies = [ 1407 | "windows_aarch64_msvc", 1408 | "windows_i686_gnu", 1409 | "windows_i686_msvc", 1410 | "windows_x86_64_gnu", 1411 | "windows_x86_64_msvc", 1412 | ] 1413 | 1414 | [[package]] 1415 | name = "windows_aarch64_msvc" 1416 | version = "0.36.1" 1417 | source = "registry+https://github.com/rust-lang/crates.io-index" 1418 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 1419 | 1420 | [[package]] 1421 | name = "windows_i686_gnu" 1422 | version = "0.36.1" 1423 | source = "registry+https://github.com/rust-lang/crates.io-index" 1424 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 1425 | 1426 | [[package]] 1427 | name = "windows_i686_msvc" 1428 | version = "0.36.1" 1429 | source = "registry+https://github.com/rust-lang/crates.io-index" 1430 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 1431 | 1432 | [[package]] 1433 | name = "windows_x86_64_gnu" 1434 | version = "0.36.1" 1435 | source = "registry+https://github.com/rust-lang/crates.io-index" 1436 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 1437 | 1438 | [[package]] 1439 | name = "windows_x86_64_msvc" 1440 | version = "0.36.1" 1441 | source = "registry+https://github.com/rust-lang/crates.io-index" 1442 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 1443 | 1444 | [[package]] 1445 | name = "zip" 1446 | version = "0.6.2" 1447 | source = "registry+https://github.com/rust-lang/crates.io-index" 1448 | checksum = "bf225bcf73bb52cbb496e70475c7bd7a3f769df699c0020f6c7bd9a96dcf0b8d" 1449 | dependencies = [ 1450 | "byteorder", 1451 | "crc32fast", 1452 | "crossbeam-utils", 1453 | "flate2", 1454 | ] 1455 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sfz" 3 | version = "0.7.1" 4 | authors = ["Weihang Lo "] 5 | description = "A simple static file serving command-line tool." 6 | documentation = "https://github.com/weihanglo/sfz" 7 | homepage = "https://github.com/weihanglo/sfz" 8 | repository = "https://github.com/weihanglo/sfz" 9 | readme = "README.md" 10 | keywords = ["static", "file", "server", "http", "cli"] 11 | categories = ["command-line-utilities", "web-programming::http-server"] 12 | exclude = ["HomebrewFormula"] 13 | license = "MIT/Apache-2.0" 14 | edition = "2021" 15 | 16 | [dependencies] 17 | # Command-line 18 | clap = { version = "3", default-features = false, features = ["std", "cargo"] } 19 | # Server 20 | tokio = { version = "1", features = ["rt-multi-thread", "macros"] } 21 | tokio-util = { version = "0.7", features = ["io"] } 22 | hyper = { version = "0.14.20", features = ["http1", "server", "tcp", "stream"] } 23 | headers = "0.3" 24 | mime_guess = "2.0" 25 | percent-encoding = "2.1" 26 | # Compression 27 | async-compression = { version = "0.3.7", features = [ 28 | "brotli", 29 | "deflate", 30 | "gzip", 31 | "tokio", 32 | ] } 33 | # Rendering 34 | tera = "1" 35 | serde = { version = "1.0", features = [ 36 | "derive", 37 | ] } # For tera serializing variables to template. 38 | ignore = "0.4" # Respect to .gitignore while listing directories. 39 | # Logging 40 | chrono = "0.4" 41 | # Directory Download 42 | qstring = "0.7" 43 | zip = { version = "0.6", default-features = false, features = ["deflate"] } 44 | futures = "0.3" 45 | tempfile = "3" 46 | bytes = "1" 47 | 48 | [dev-dependencies] 49 | tempfile = "3" 50 | once_cell = "1" 51 | 52 | [profile.dev] 53 | codegen-units = 2 54 | incremental = true 55 | 56 | [profile.release] 57 | lto = true 58 | panic = "abort" 59 | -------------------------------------------------------------------------------- /HomebrewFormula: -------------------------------------------------------------------------------- 1 | pkg/brew/ -------------------------------------------------------------------------------- /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 | MIT License 2 | 3 | Copyright (c) 2018 Weihang Lo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Thank you for using this tool so far. Unfortunately I have no time 2 | > maintaining it, but I believe people can find a better alternative 3 | > out there on [crates.io](https://crates.io). 4 | > 5 | > If you are still interested in supporting Rust and its community, please 6 | > consider **sponsoring Rust developers you like**. You can find a few from 7 | > the official [Rust teams](https://www.rust-lang.org/governance/), 8 | > or just [buy me a coffee](https://github.com/sponsors/weihanglo) ❤️. 9 | 10 | # [][sfz] 11 | 12 | [](https://crates.io/crates/sfz) 13 | [](https://github.com/weihanglo/sfz/actions?query=workflow%3ACI) 14 | [](https://codecov.io/gh/weihanglo/sfz) 15 | [][sfz] 16 | [](https://deps.rs/repo/github/weihanglo/sfz) 17 | 18 | sfz, or **S**tatic **F**ile **Z**erver, is a simple command-line tool serving static files for you. 19 | 20 |  21 | 22 | The name **sfz** is derived from an accented note [Sforzando][sforzando] in music, which means “suddenly with force.” 23 | 24 | [sfz]: https://github.com/weihanglo/sfz 25 | [sforzando]: https://en.wikipedia.org/wiki/Dynamics_(music)#Sudden_changes_and_accented_notes 26 | 27 | ## Features 28 | 29 | - Directory listing 30 | - Partial responses (range requests) 31 | - Conditional requests with cache validations 32 | - Cross-origin resource sharing 33 | - Automatic HTTP compression (Brotli, Gzip, Deflate) 34 | - Automatic rendering `index.html` 35 | - Respect `.gitignore` file 36 | - Customize path prefix 37 | 38 | ## Installation 39 | 40 | ### Automatic 41 | 42 | #### macOS 43 | 44 | If you are a **macOS Homebrew** user, you can install sfz from a custom tap: 45 | 46 | ```shell 47 | brew tap weihanglo/sfz https://github.com/weihanglo/sfz.git 48 | brew install sfz 49 | ``` 50 | 51 | > Disclaimer: Formula on **Linuxbrew** did not fully tested. 52 | 53 | #### Cargo 54 | 55 | If you are a **Rust programmer**, sfz are available on [crates.io][crates.io] via [Cargo][cargo]. 56 | 57 | ```shell 58 | cargo install sfz 59 | ``` 60 | 61 | You can also install the latest version (or a specific commit) of sfz directly from GitHub. 62 | 63 | ```shell 64 | cargo install --git https://github.com/weihanglo/sfz.git 65 | ``` 66 | 67 | [crates.io]: https://crates.io 68 | [cargo]: https://doc.rust-lang.org/cargo/ 69 | 70 | ### Manual 71 | 72 | #### Prebuilt binaries 73 | 74 | Archives of prebuilt binaries are available on [GitHub Release][gh-release] for Linux, maxOS and Windows. Download a compatible binary for your system. For convenience, make sure you place sfz under $PATH if you want access it from the command line. 75 | 76 | [gh-release]: https://github.com/weihanglo/sfz/releases 77 | 78 | #### Build from source 79 | 80 | sfz is written in Rust. You need to [install Rust][install-rust] in order to compile it. 81 | 82 | ```shell 83 | $ git clone https://github.com/weihanglo/sfz.git 84 | $ cd sfz 85 | $ cargo build --release 86 | $ ./target/release/sfz --version 87 | 0.7.1 88 | ``` 89 | 90 | [install-rust]: https://www.rust-lang.org/install.html 91 | 92 | ## Usage 93 | 94 | The simplest way to start serving files is to run this command: 95 | 96 | ```shell 97 | sfz [FLAGS] [OPTIONS] [path] 98 | ``` 99 | 100 | The command above will start serving your current working directory on `127.0.0.1:5000` by default. 101 | 102 | If you want to serve another directory, pass `[path]` positional argument in with either absolute or relaitve path. 103 | 104 | ```shell 105 | sfz /usr/local 106 | 107 | # Serve files under `/usr/local` directory. 108 | # 109 | # You can press ctrl-c to exit immediately. 110 | ``` 111 | 112 | ### Flags and Options 113 | 114 | sfz aims to be simple but configurable. Here is a list of available options: 115 | 116 | ``` 117 | USAGE: 118 | sfz [OPTIONS] [path] 119 | 120 | ARGS: 121 | Path to a directory for serving files [default: .] 122 | 123 | OPTIONS: 124 | -a, --all Serve hidden and dot (.) files 125 | -b, --bind Specify bind address [default: 127.0.0.1] 126 | -c, --cache Specify max-age of HTTP caching in seconds [default: 0] 127 | -C, --cors Enable Cross-Origin Resource Sharing from any origin (*) 128 | --coi Enable Cross-Origin isolation 129 | -h, --help Print help information 130 | -I, --no-ignore Don't respect gitignore file 131 | -L, --follow-links Follow symlinks outside current serving base path 132 | --no-log Don't log any request/response information. 133 | -p, --port Specify port to listen on [default: 5000] 134 | --path-prefix Specify an url path prefix, helpful when running behing a reverse 135 | proxy 136 | -r, --render-index Render existing index.html when requesting a directory. 137 | -V, --version Print version information 138 | -Z, --unzipped Disable HTTP compression 139 | ``` 140 | 141 | ## Contributing 142 | 143 | Contributions are highly appreciated! Feel free to open issues or send pull requests directly. 144 | 145 | ## Credits 146 | 147 | sfz was originally inspired by another static serving tool [serve][serve], and sfz's directory-listing UI is mainly borrowed from [GitHub][github]. 148 | 149 | sfz is built on the top of awesome Rust community. Thanks for all Rust and crates contributors. 150 | 151 | [serve]: https://github.com/zeit/serve 152 | [github]: https://github.com/ 153 | 154 | ## License 155 | 156 | This project is licensed under either of 157 | 158 | - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 159 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 160 | 161 | at your option. 162 | 163 | ### Contribution 164 | 165 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in sfz by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 166 | -------------------------------------------------------------------------------- /pkg/brew/sfz.rb: -------------------------------------------------------------------------------- 1 | class Sfz < Formula 2 | version '0.7.1' 3 | desc "A simple static file serving command-line tool." 4 | homepage "https://github.com/weihanglo/sfz" 5 | head "https://github.com/weihanglo/sfz.git" 6 | 7 | if OS.mac? 8 | url "https://github.com/weihanglo/sfz/releases/download/v#{version}/sfz-v#{version}-x86_64-apple-darwin.tar.gz" 9 | sha256 "4ece429bb3cb6953c1a6f0b07b50b6220ef219f88a66fb403d820ee933eb0f43" 10 | elsif OS.linux? 11 | url "https://github.com/weihanglo/sfz/releases/download/v#{version}/sfz-v#{version}-x86_64-unknown-linux-musl.tar.gz" 12 | sha256 "bbbfa1fafb1fc481d78b292c671c28318108d51f4f5ae54b99a843f52bbcafd1" 13 | end 14 | 15 | def install 16 | bin.install "sfz" 17 | end 18 | 19 | test do 20 | system "#{bin}/sfz", "--help" 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /src/cli/app.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Weihang Lo 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use clap::crate_description; 10 | use clap::{Arg, ArgMatches}; 11 | 12 | const ABOUT: &str = concat!("\n", crate_description!()); // Add extra newline. 13 | 14 | fn app() -> clap::Command<'static> { 15 | let arg_port = Arg::new("port") 16 | .short('p') 17 | .long("port") 18 | .default_value("5000") 19 | .help("Specify port to listen on") 20 | .value_name("port"); 21 | 22 | let arg_address = Arg::new("address") 23 | .short('b') 24 | .long("bind") 25 | .default_value("127.0.0.1") 26 | .help("Specify bind address") 27 | .value_name("address"); 28 | 29 | let arg_cors = Arg::new("cors") 30 | .short('C') 31 | .long("cors") 32 | .help("Enable Cross-Origin Resource Sharing from any origin (*)"); 33 | 34 | let arg_coi = Arg::new("coi") 35 | .long("coi") 36 | .help("Enable Cross-Origin isolation"); 37 | 38 | let arg_cache = Arg::new("cache") 39 | .short('c') 40 | .long("cache") 41 | .default_value("0") 42 | .help("Specify max-age of HTTP caching in seconds") 43 | .value_name("seconds"); 44 | 45 | let arg_path = Arg::new("path") 46 | .default_value(".") 47 | .allow_invalid_utf8(true) 48 | .help("Path to a directory for serving files"); 49 | 50 | let arg_unzipped = Arg::new("unzipped") 51 | .short('Z') 52 | .long("unzipped") 53 | .help("Disable HTTP compression"); 54 | 55 | let arg_all = Arg::new("all") 56 | .short('a') 57 | .long("all") 58 | .help("Serve hidden and dot (.) files"); 59 | 60 | let arg_no_ignore = Arg::new("no-ignore") 61 | .short('I') 62 | .long("no-ignore") 63 | .help("Don't respect gitignore file"); 64 | 65 | let arg_no_log = Arg::new("no-log") 66 | .long("--no-log") 67 | .help("Don't log any request/response information."); 68 | 69 | let arg_follow_links = Arg::new("follow-links") 70 | .short('L') 71 | .long("--follow-links") 72 | .help("Follow symlinks outside current serving base path"); 73 | 74 | let arg_render_index = Arg::new("render-index") 75 | .short('r') 76 | .long("--render-index") 77 | .help("Render existing index.html when requesting a directory."); 78 | 79 | let arg_path_prefix = Arg::new("path-prefix") 80 | .long("path-prefix") 81 | .help("Specify an url path prefix, helpful when running behing a reverse proxy") 82 | .value_name("path"); 83 | 84 | clap::command!() 85 | .about(ABOUT) 86 | .arg(arg_address) 87 | .arg(arg_port) 88 | .arg(arg_cache) 89 | .arg(arg_cors) 90 | .arg(arg_coi) 91 | .arg(arg_path) 92 | .arg(arg_unzipped) 93 | .arg(arg_all) 94 | .arg(arg_no_ignore) 95 | .arg(arg_no_log) 96 | .arg(arg_follow_links) 97 | .arg(arg_render_index) 98 | .arg(arg_path_prefix) 99 | } 100 | 101 | pub fn matches() -> ArgMatches { 102 | app().get_matches() 103 | } 104 | 105 | #[cfg(test)] 106 | mod t { 107 | use super::*; 108 | 109 | #[test] 110 | fn verify_app() { 111 | app().debug_assert(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/cli/args.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Weihang Lo 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use std::env; 10 | use std::fs::canonicalize; 11 | use std::net::SocketAddr; 12 | use std::path::{Path, PathBuf}; 13 | 14 | use clap::ArgMatches; 15 | 16 | use crate::BoxResult; 17 | 18 | #[derive(Debug, Clone, Eq, PartialEq)] 19 | pub struct Args { 20 | pub address: String, 21 | pub port: u16, 22 | pub cache: u64, 23 | pub cors: bool, 24 | pub coi: bool, 25 | pub compress: bool, 26 | pub path: PathBuf, 27 | pub all: bool, 28 | pub ignore: bool, 29 | pub follow_links: bool, 30 | pub render_index: bool, 31 | pub log: bool, 32 | pub path_prefix: Option, 33 | } 34 | 35 | impl Args { 36 | /// Parse command-line arguments. 37 | /// 38 | /// If a parsing error ocurred, exit the process and print out informative 39 | /// error message to user. 40 | pub fn parse(matches: ArgMatches) -> BoxResult { 41 | let address = matches.value_of("address").unwrap_or_default().to_owned(); 42 | let port = matches.value_of_t::("port")?; 43 | let cache = matches.value_of_t::("cache")?; 44 | let cors = matches.is_present("cors"); 45 | let coi = matches.is_present("coi"); 46 | let path = matches.value_of_os("path").unwrap_or_default(); 47 | let path = Args::parse_path(path)?; 48 | 49 | let compress = !matches.is_present("unzipped"); 50 | let all = matches.is_present("all"); 51 | let ignore = !matches.is_present("no-ignore"); 52 | let follow_links = matches.is_present("follow-links"); 53 | let render_index = matches.is_present("render-index"); 54 | let log = !matches.is_present("no-log"); 55 | let path_prefix = matches 56 | .value_of("path-prefix") 57 | .map(|s| format!("/{}", s.trim_start_matches('/'))); 58 | 59 | Ok(Args { 60 | address, 61 | port, 62 | cache, 63 | cors, 64 | coi, 65 | path, 66 | compress, 67 | all, 68 | ignore, 69 | follow_links, 70 | render_index, 71 | log, 72 | path_prefix, 73 | }) 74 | } 75 | 76 | /// Parse path. 77 | fn parse_path>(path: P) -> BoxResult { 78 | let path = path.as_ref(); 79 | if !path.exists() { 80 | bail!("error: path \"{}\" doesn't exist", path.display()); 81 | } 82 | 83 | env::current_dir() 84 | .and_then(|mut p| { 85 | p.push(path); // If path is absolute, it replaces the current path. 86 | canonicalize(p) 87 | }) 88 | .or_else(|err| { 89 | bail!( 90 | "error: failed to access path \"{}\": {}", 91 | path.display(), 92 | err, 93 | ) 94 | }) 95 | } 96 | 97 | /// Construct socket address from arguments. 98 | pub fn address(&self) -> BoxResult { 99 | format!("{}:{}", self.address, self.port) 100 | .parse() 101 | .or_else(|err| { 102 | bail!( 103 | "error: invalid address {}:{} : {}", 104 | self.address, 105 | self.port, 106 | err, 107 | ) 108 | }) 109 | } 110 | } 111 | 112 | #[cfg(test)] 113 | mod t { 114 | use super::*; 115 | use crate::matches; 116 | use crate::test_utils::with_current_dir; 117 | use std::fs::File; 118 | use tempfile::Builder; 119 | 120 | impl Default for Args { 121 | /// Just for convenience. We do not need a default at this time. 122 | fn default() -> Self { 123 | Self { 124 | address: "127.0.0.1".to_owned(), 125 | port: 5000, 126 | cache: 0, 127 | cors: true, 128 | coi: true, 129 | compress: true, 130 | path: ".".into(), 131 | all: true, 132 | ignore: true, 133 | follow_links: true, 134 | render_index: true, 135 | log: true, 136 | path_prefix: None, 137 | } 138 | } 139 | } 140 | 141 | const fn temp_name() -> &'static str { 142 | concat!(env!("CARGO_PKG_NAME"), "-", env!("CARGO_PKG_VERSION")) 143 | } 144 | 145 | #[test] 146 | fn parse_default() { 147 | let current_dir = env!("CARGO_MANIFEST_DIR"); 148 | with_current_dir(current_dir, || { 149 | let args = Args::parse(matches()).unwrap(); 150 | 151 | // See following link to figure out why we need canonicalize here. 152 | // Thought this leaks some internal implementations.... 153 | // 154 | // - https://stackoverflow.com/a/41233992/8851735 155 | // - https://stackoverflow.com/q/50322817/8851735 156 | let path = PathBuf::from(current_dir).canonicalize().unwrap(); 157 | 158 | assert_eq!( 159 | args, 160 | Args { 161 | address: "127.0.0.1".to_string(), 162 | all: false, 163 | cache: 0, 164 | compress: true, 165 | cors: false, 166 | coi: false, 167 | follow_links: false, 168 | ignore: true, 169 | log: true, 170 | path, 171 | path_prefix: None, 172 | render_index: false, 173 | port: 5000 174 | } 175 | ); 176 | }); 177 | } 178 | 179 | #[test] 180 | fn parse_absolute_path() { 181 | let tmp_dir = Builder::new().prefix(temp_name()).tempdir().unwrap(); 182 | let path = tmp_dir.path().join("temp.txt"); 183 | assert!(path.is_absolute()); 184 | // error: No exists 185 | assert!(Args::parse_path(&path).is_err()); 186 | // create file 187 | File::create(&path).unwrap(); 188 | assert_eq!( 189 | Args::parse_path(&path).unwrap(), 190 | path.canonicalize().unwrap(), 191 | ); 192 | } 193 | 194 | #[test] 195 | fn parse_relative_path() { 196 | let tmp_dir = Builder::new().prefix(temp_name()).tempdir().unwrap(); 197 | with_current_dir(tmp_dir.path(), || { 198 | let relative_path: &Path = "temp.txt".as_ref(); 199 | File::create(relative_path).unwrap(); 200 | 201 | assert!(relative_path.is_relative()); 202 | assert_eq!( 203 | Args::parse_path(relative_path).unwrap(), 204 | tmp_dir.path().join(relative_path).canonicalize().unwrap(), 205 | ); 206 | }); 207 | } 208 | 209 | #[test] 210 | fn parse_addresses() { 211 | // IPv4 212 | let args = Args::default(); 213 | assert!(args.address().is_ok()); 214 | 215 | // IPv6 216 | let args = Args { 217 | address: "[::1]".to_string(), 218 | ..Default::default() 219 | }; 220 | assert!(args.address().is_ok()); 221 | 222 | // Invalid 223 | let args = Args { 224 | address: "".to_string(), 225 | ..Default::default() 226 | }; 227 | assert!(args.address().is_err()); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Weihang Lo 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | mod app; 10 | mod args; 11 | 12 | pub use self::app::matches; 13 | pub use self::args::Args; 14 | -------------------------------------------------------------------------------- /src/extensions.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Weihang Lo 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use std::path::{Component, Path}; 10 | use std::time::SystemTime; 11 | 12 | use mime_guess::{mime, Mime}; 13 | 14 | use crate::server::PathType; 15 | 16 | pub trait PathExt { 17 | fn mime(&self) -> Option; 18 | fn is_relatively_hidden(&self) -> bool; 19 | fn mtime(&self) -> SystemTime; 20 | fn filename_str(&self) -> &str; 21 | fn size(&self) -> u64; 22 | fn type_(&self) -> PathType; 23 | } 24 | 25 | impl PathExt for Path { 26 | /// Guess MIME type from a path. 27 | fn mime(&self) -> Option { 28 | mime_guess::from_path(&self).first() 29 | } 30 | 31 | /// Check if a path is relatively hidden. 32 | /// 33 | /// A path is "relatively hidden" means that if any component of the path 34 | /// is hidden, no matter whether the path's basename is prefixed with `.` 35 | /// or not, it is considered as hidden. 36 | fn is_relatively_hidden(&self) -> bool { 37 | self.components() 38 | .filter_map(|c| match c { 39 | Component::Normal(os_str) => os_str.to_str(), 40 | _ => None, 41 | }) 42 | .any(|s| s.starts_with('.')) 43 | } 44 | 45 | /// Get modified time from a path. 46 | fn mtime(&self) -> SystemTime { 47 | self.metadata().and_then(|meta| meta.modified()).unwrap() 48 | } 49 | 50 | /// Get file size, in bytes, from a path. 51 | fn size(&self) -> u64 { 52 | self.metadata().map(|meta| meta.len()).unwrap_or_default() 53 | } 54 | 55 | /// Get a filename `&str` from a path. 56 | fn filename_str(&self) -> &str { 57 | self.file_name() 58 | .and_then(|s| s.to_str()) 59 | .unwrap_or_default() 60 | } 61 | 62 | /// Determine given path is a normal file/directory or a symlink. 63 | fn type_(&self) -> PathType { 64 | self.symlink_metadata() 65 | .map(|meta| { 66 | let is_symlink = meta.file_type().is_symlink(); 67 | let is_dir = self.is_dir(); 68 | match (is_symlink, is_dir) { 69 | (true, true) => PathType::SymlinkDir, 70 | (false, true) => PathType::Dir, 71 | (true, false) => PathType::SymlinkFile, 72 | (false, false) => PathType::File, 73 | } 74 | }) 75 | .unwrap_or(PathType::File) 76 | } 77 | } 78 | 79 | pub trait SystemTimeExt { 80 | fn timestamp(&self) -> u64; 81 | } 82 | 83 | impl SystemTimeExt for SystemTime { 84 | /// Convert `SystemTime` to timestamp in seconds. 85 | fn timestamp(&self) -> u64 { 86 | self.duration_since(std::time::UNIX_EPOCH) 87 | .unwrap_or_default() 88 | .as_secs() 89 | } 90 | } 91 | 92 | pub trait MimeExt { 93 | fn is_compressed_format(&self) -> bool; 94 | fn guess_charset(&self) -> Option; 95 | } 96 | 97 | impl MimeExt for Mime { 98 | /// Detect if MIME type is 99 | /// 100 | /// - `video/*` 101 | /// - `audio/*` 102 | /// - `*/gif` 103 | /// - `*/jpeg` 104 | /// - `*/png` 105 | /// - `*/bmp` 106 | /// - `*/avif` 107 | /// - `*/webp` 108 | /// - `*/tiff` 109 | fn is_compressed_format(&self) -> bool { 110 | let subtype = self.subtype(); 111 | #[allow(clippy::match_like_matches_macro)] 112 | match (self.type_(), subtype, subtype.as_str()) { 113 | (mime::VIDEO | mime::AUDIO, _, _) => true, 114 | (_, mime::GIF | mime::JPEG | mime::PNG | mime::BMP, _) => true, 115 | (_, _, "avif" | "webp" | "tiff") => true, 116 | _ => false, 117 | } 118 | } 119 | 120 | /// Detect possible charset. 121 | /// 122 | /// In order not to perform any I/O. We only inspect the type of mime to 123 | /// determine the charset. 124 | /// 125 | /// - `text/*`, `*/xml`, `*/javascript`, `*/json` -> UTF-8 126 | /// - `*/*+xml`, `*/*+json` -> UTF-8 127 | /// - others -> leave it as is 128 | fn guess_charset(&self) -> Option { 129 | match (self.type_(), self.subtype(), self.suffix()) { 130 | (mime::TEXT, _, _) 131 | | (_, mime::XML | mime::JAVASCRIPT | mime::JSON, _) 132 | | (_, _, Some(mime::XML | mime::JSON)) => Some(mime::UTF_8), 133 | _ => None, 134 | } 135 | } 136 | } 137 | 138 | #[cfg(test)] 139 | mod t_extensions { 140 | use super::*; 141 | use std::path::PathBuf; 142 | 143 | fn file_txt_path() -> PathBuf { 144 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 145 | path.push("./tests/file.txt"); 146 | path 147 | } 148 | 149 | fn hidden_html_path() -> PathBuf { 150 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 151 | path.push("./tests/.hidden.html"); 152 | path 153 | } 154 | 155 | #[test] 156 | fn path_mime() { 157 | assert_eq!(file_txt_path().mime(), Some(mime::TEXT_PLAIN)); 158 | assert_eq!(hidden_html_path().mime(), Some(mime::TEXT_HTML)); 159 | } 160 | 161 | #[test] 162 | fn path_is_relatively_hidden() { 163 | assert!(hidden_html_path().is_relatively_hidden()); 164 | 165 | let path = "./.hidden/visible.html"; 166 | assert!(PathBuf::from(path).is_relatively_hidden()); 167 | } 168 | 169 | #[test] 170 | fn path_is_not_relatively_hidden() { 171 | let path = "./visible/visible.html"; 172 | assert!(!PathBuf::from(path).is_relatively_hidden()); 173 | } 174 | 175 | #[ignore] 176 | #[test] 177 | fn path_mtime() {} 178 | 179 | #[test] 180 | fn path_size() { 181 | assert_eq!(file_txt_path().size(), 8); 182 | assert_eq!(hidden_html_path().size(), 0); 183 | } 184 | 185 | #[test] 186 | fn path_filename_str() { 187 | assert_eq!(file_txt_path().filename_str(), "file.txt"); 188 | assert_eq!(hidden_html_path().filename_str(), ".hidden.html"); 189 | } 190 | 191 | #[test] 192 | fn path_type_() { 193 | let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 194 | 195 | let mut dir_path = path.clone(); 196 | dir_path.push("./tests/dir"); 197 | assert_eq!(dir_path.type_(), PathType::Dir); 198 | 199 | let mut symlink_dir_path = path.clone(); 200 | symlink_dir_path.push("./tests/symlink_dir"); 201 | assert_eq!(symlink_dir_path.type_(), PathType::SymlinkDir); 202 | 203 | assert_eq!(file_txt_path().type_(), PathType::File); 204 | 205 | let mut symlink_file_txt_path = path.clone(); 206 | symlink_file_txt_path.push("./tests/symlink_file.txt"); 207 | assert_eq!(symlink_file_txt_path.type_(), PathType::SymlinkFile); 208 | } 209 | 210 | #[test] 211 | fn system_time_to_timestamp() { 212 | use std::time::Duration; 213 | let secs = 1000; 214 | let tm = SystemTime::UNIX_EPOCH + Duration::from_secs(secs); 215 | assert_eq!(tm.timestamp(), secs); 216 | } 217 | 218 | #[test] 219 | fn mime_is_compressed() { 220 | let cases = [ 221 | "video/*", "audio/*", "*/gif", "*/jpeg", "*/png", "*/bmp", "*/avif", "*/webp", "*/tiff", 222 | ]; 223 | for mime in cases { 224 | assert!(mime.parse::().unwrap().is_compressed_format()); 225 | } 226 | 227 | assert_eq!( 228 | "text/*" 229 | .parse::() 230 | .unwrap() 231 | .is_compressed_format(), 232 | false 233 | ); 234 | } 235 | 236 | #[test] 237 | fn guess_charset() { 238 | let cases = [ 239 | "application/json", 240 | "application/xml", 241 | "application/javascript", 242 | "application/atmo+xml", 243 | "application/geo+json", 244 | "text/plain", 245 | "text/x-yaml", 246 | ]; 247 | for mime in cases { 248 | assert_eq!( 249 | mime.parse::().unwrap().guess_charset(), 250 | Some(mime::UTF_8) 251 | ); 252 | } 253 | 254 | assert!("application/octet-stream" 255 | .parse::() 256 | .unwrap() 257 | .guess_charset() 258 | .is_none()); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/http/conditional_requests.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Weihang Lo 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use std::time::SystemTime; 10 | 11 | use headers::{ETag, HeaderMapExt, IfMatch, IfModifiedSince, IfNoneMatch, IfUnmodifiedSince}; 12 | use hyper::Method; 13 | 14 | use crate::server::Request; 15 | 16 | /// Indicates that conditions given in the request header evaluted to false. 17 | /// Return true if any preconditions fail. 18 | /// 19 | /// Note that this method is only implemented partial precedence of 20 | /// conditions defined in [RFC7232][1] which is only related to precondition 21 | /// (Status Code 412) but not caching response (Status Code 304). Caller must 22 | /// handle caching responses by themselves. 23 | /// 24 | /// [1]: https://tools.ietf.org/html/rfc7232#section-6 25 | pub fn is_precondition_failed(req: &Request, etag: &ETag, last_modified: SystemTime) -> bool { 26 | // 3. Evaluate If-None-Match 27 | let eval_if_none_match = || { 28 | req.headers().typed_get::().is_some() 29 | && req.method() != Method::GET 30 | && req.method() != Method::HEAD 31 | }; 32 | 33 | // 1. Evaluate If-Match 34 | let eval_if_match = req 35 | .headers() 36 | .typed_get::() 37 | .map(|if_match| !if_match.precondition_passes(etag) || eval_if_none_match()); 38 | 39 | // 2. Evaluate If-Unmodified-Since 40 | let eval_if_unmodified_since = || { 41 | req.headers() 42 | .typed_get::() 43 | .map(|if_unmodified_since| { 44 | !if_unmodified_since.precondition_passes(last_modified) || eval_if_none_match() 45 | }) 46 | }; 47 | 48 | eval_if_match 49 | .or_else(eval_if_unmodified_since) 50 | .or_else(|| Some(eval_if_none_match())) 51 | .unwrap_or_default() 52 | } 53 | 54 | /// Determine freshness of requested resource by validate `If-None-Match` 55 | /// and `If-Modified-Sinlsece` precondition header fields containing validators. 56 | /// 57 | /// See more on [RFC7234, 4.3.2. Handling a Received Validation Request][1]. 58 | /// 59 | /// [1]: https://tools.ietf.org/html/rfc7234#section-4.3.2 60 | pub fn is_fresh(req: &Request, etag: &ETag, last_modified: SystemTime) -> bool { 61 | // `If-None-Match` takes presedence over `If-Modified-Since`. 62 | if let Some(if_none_match) = req.headers().typed_get::() { 63 | !if_none_match.precondition_passes(etag) 64 | } else if let Some(if_modified_since) = req.headers().typed_get::() { 65 | !if_modified_since.is_modified(last_modified) 66 | } else { 67 | false 68 | } 69 | } 70 | 71 | #[cfg(test)] 72 | fn init_request() -> (Request, ETag, SystemTime) { 73 | ( 74 | Request::default(), 75 | "\"hello\"".to_string().parse::().unwrap(), 76 | SystemTime::now(), 77 | ) 78 | } 79 | 80 | #[cfg(test)] 81 | mod t_precondition { 82 | use super::*; 83 | use std::time::Duration; 84 | 85 | #[test] 86 | fn ok_without_any_precondition() { 87 | let (req, etag, date) = init_request(); 88 | assert!(!is_precondition_failed(&req, &etag, date)); 89 | } 90 | 91 | #[test] 92 | fn failed_with_if_match_not_passes() { 93 | let (mut req, etag, date) = init_request(); 94 | let if_match = IfMatch::from("\"\"".to_string().parse::().unwrap()); 95 | req.headers_mut().typed_insert(if_match); 96 | assert!(is_precondition_failed(&req, &etag, date)); 97 | } 98 | 99 | #[test] 100 | fn with_if_match_passes() { 101 | let (mut req, etag, date) = init_request(); 102 | let if_match = IfMatch::from("\"hello\"".to_string().parse::().unwrap()); 103 | let if_none_match = IfNoneMatch::from("\"world\"".to_string().parse::().unwrap()); 104 | req.headers_mut().typed_insert(if_match); 105 | req.headers_mut().typed_insert(if_none_match); 106 | // OK with GET HEAD methods 107 | assert!(!is_precondition_failed(&req, &etag, date)); 108 | // Failed with method other than GET HEAD 109 | *req.method_mut() = Method::PUT; 110 | assert!(is_precondition_failed(&req, &etag, date)); 111 | } 112 | 113 | #[test] 114 | fn failed_with_if_unmodified_since_not_passes() { 115 | let (mut req, etag, date) = init_request(); 116 | let past = date - Duration::from_secs(1); 117 | let if_unmodified_since = IfUnmodifiedSince::from(past); 118 | req.headers_mut().typed_insert(if_unmodified_since); 119 | assert!(is_precondition_failed(&req, &etag, date)); 120 | } 121 | 122 | #[test] 123 | fn with_if_unmodified_since_passes() { 124 | let (mut req, etag, date) = init_request(); 125 | let if_unmodified_since = IfUnmodifiedSince::from(date); 126 | let if_none_match = IfNoneMatch::from("\"nonematch\"".to_string().parse::().unwrap()); 127 | req.headers_mut().typed_insert(if_unmodified_since); 128 | req.headers_mut().typed_insert(if_none_match); 129 | // OK with GET HEAD methods 130 | assert!(!is_precondition_failed(&req, &etag, date)); 131 | // Failed with method other than GET HEAD 132 | *req.method_mut() = Method::PUT; 133 | assert!(is_precondition_failed(&req, &etag, date)); 134 | } 135 | } 136 | 137 | #[cfg(test)] 138 | mod t_fresh { 139 | use super::*; 140 | use std::time::Duration; 141 | 142 | #[test] 143 | fn no_precondition_header_fields() { 144 | let (req, etag, date) = init_request(); 145 | assert!(!is_fresh(&req, &etag, date)); 146 | } 147 | 148 | #[test] 149 | fn if_none_match_precedes_if_modified_since() { 150 | let (mut req, etag, date) = init_request(); 151 | let if_none_match = IfNoneMatch::from(etag.clone()); 152 | let future = date + Duration::from_secs(1); 153 | let if_modified_since = IfModifiedSince::from(future); 154 | req.headers_mut().typed_insert(if_none_match); 155 | req.headers_mut().typed_insert(if_modified_since); 156 | assert!(is_fresh(&req, &etag, date)); 157 | } 158 | 159 | #[test] 160 | fn only_if_modified_since() { 161 | let (mut req, etag, date) = init_request(); 162 | let future = date + Duration::from_secs(1); 163 | let if_modified_since = IfModifiedSince::from(future); 164 | req.headers_mut().typed_insert(if_modified_since); 165 | assert!(is_fresh(&req, &etag, date)); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/http/content_encoding.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Weihang Lo 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use std::cmp::Ordering; 10 | use std::io; 11 | 12 | use async_compression::{ 13 | tokio::bufread::{BrotliEncoder, DeflateEncoder, GzipEncoder}, 14 | Level, 15 | }; 16 | use bytes::Bytes; 17 | use futures::Stream; 18 | use hyper::header::HeaderValue; 19 | use hyper::Body; 20 | use tokio_util::io::{ReaderStream, StreamReader}; 21 | 22 | pub const IDENTITY: &str = "identity"; 23 | pub const DEFLATE: &str = "deflate"; 24 | pub const GZIP: &str = "gzip"; 25 | pub const BR: &str = "br"; 26 | 27 | /// Inner helper type to store quality values. 28 | /// 29 | /// - 0: content enconding 30 | /// - 1: weight from 0 to 1000 31 | #[derive(Debug, PartialEq)] 32 | struct QualityValue<'a>(&'a str, u32); 33 | 34 | /// Inner helper type for comparsion by intrinsic enum variant order. 35 | #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] 36 | enum Encoding { 37 | Identity, 38 | Deflate, 39 | Gzip, 40 | Brotli, 41 | } 42 | 43 | impl From<&str> for Encoding { 44 | fn from(s: &str) -> Self { 45 | match s { 46 | DEFLATE => Self::Deflate, 47 | GZIP => Self::Gzip, 48 | BR => Self::Brotli, 49 | _ => Self::Identity, 50 | } 51 | } 52 | } 53 | 54 | /// This match expression is necessary to return a `&'static str`. 55 | pub fn encoding_to_static_str<'a>(encoding: &'a str) -> &'static str { 56 | match encoding { 57 | DEFLATE => DEFLATE, 58 | GZIP => GZIP, 59 | BR => BR, 60 | _ => IDENTITY, 61 | } 62 | } 63 | 64 | /// Sorting encodings according to the weight of quality values and then the 65 | /// intrinsic rank of `Encoding` enum varaint. 66 | /// 67 | /// The function only accecpt Brotli, Gzip and Deflate encodings, passing other 68 | /// encodings in may lead to a unexpected result. 69 | fn sort_encoding(a: &QualityValue, b: &QualityValue) -> Ordering { 70 | a.1.cmp(&b.1) 71 | .then_with(|| Encoding::from(a.0).cmp(&Encoding::from(b.0))) 72 | } 73 | 74 | /// According to RFC7231, a [Quality Values][1] is defined as follow grammar: 75 | /// 76 | /// ```text 77 | /// weight = OWS ";" OWS "q=" qvalue 78 | /// qvalue = ( "0" [ "." 0*3DIGIT ] ) 79 | /// / ( "1" [ "." 0*3("0") ] ) 80 | /// ``` 81 | /// 82 | /// Note that: 83 | /// 84 | /// - Quality value of 0 means unacceptable. 85 | /// - The weight ranges from 0 to 1 in real number with three digit at most. 86 | /// - Weight defaults to 1 if not present. 87 | /// - We define unrecognized qvalue as zero. 88 | /// 89 | /// [1]: https://tools.ietf.org/html/rfc7231#section-5.3.1 90 | fn parse_qvalue(q: &str) -> Option { 91 | let mut iter = q.trim().split_terminator(';').take(2); 92 | let content = iter.next().map(str::trim_end)?; 93 | let weight = match iter.next() { 94 | Some(s) => s 95 | .trim_start() 96 | .trim_start_matches("q=") 97 | .parse::() 98 | .ok() 99 | .map(|num| (num * 1000.0) as u32) 100 | .filter(|v| *v <= 1000) 101 | .unwrap_or_default(), 102 | None => 1000, 103 | }; 104 | Some(QualityValue(content, weight)) 105 | } 106 | 107 | /// Get prior encoding from `Accept-Encoding` header field. 108 | /// 109 | /// Note that: 110 | /// 111 | /// - Only accept `br` / `gzip` / `deflate` 112 | /// - Highest non-zero qvalue is preferred. 113 | pub fn get_prior_encoding<'a>(accept_encoding: &'a HeaderValue) -> &'static str { 114 | accept_encoding 115 | .to_str() 116 | .ok() 117 | .and_then(|accept_encoding| { 118 | let mut quality_values = accept_encoding 119 | .split(',') 120 | .filter_map(parse_qvalue) 121 | .collect::>(); 122 | // Sort by quality value, than by encoding type. 123 | quality_values.sort_unstable_by(sort_encoding); 124 | // Get the last encoding (highest priority). 125 | quality_values.last().map(|q| encoding_to_static_str(q.0)) 126 | }) 127 | // Default using identity encoding, which means no content encoding. 128 | .unwrap_or(IDENTITY) 129 | } 130 | 131 | /// Compress data stream. 132 | /// 133 | /// # Parameters 134 | /// 135 | /// * `input` - [`futures::stream::Stream`] to be compressed, e.g. [`hyper::body::Body`]. 136 | /// * `encoding` - Only support `br`, `deflate`, `gzip` and `identity`. 137 | pub fn compress_stream( 138 | input: impl Stream> + Send + 'static, 139 | encoding: &str, 140 | ) -> io::Result { 141 | match encoding { 142 | BR => Ok(Body::wrap_stream(ReaderStream::new( 143 | BrotliEncoder::with_quality(StreamReader::new(input), Level::Fastest), 144 | ))), 145 | DEFLATE => Ok(Body::wrap_stream(ReaderStream::new(DeflateEncoder::new( 146 | StreamReader::new(input), 147 | )))), 148 | GZIP => Ok(Body::wrap_stream(ReaderStream::new(GzipEncoder::new( 149 | StreamReader::new(input), 150 | )))), 151 | _ => Err(io::Error::new(io::ErrorKind::Other, "Unsupported Encoding")), 152 | } 153 | } 154 | 155 | pub fn should_compress(enc: &str) -> bool { 156 | IDENTITY != enc 157 | } 158 | 159 | #[cfg(test)] 160 | mod t_parse_qvalue { 161 | use super::*; 162 | 163 | #[test] 164 | fn parse_successfully() { 165 | let cases = vec![ 166 | (Some(QualityValue(BR, 1000)), "br;q=1"), 167 | (Some(QualityValue(BR, 0)), "br;q=0"), 168 | (Some(QualityValue(BR, 1000)), "br;q=1.000"), 169 | (Some(QualityValue(BR, 0)), "br;q=0.000"), 170 | (Some(QualityValue(BR, 1000)), "br"), 171 | (Some(QualityValue(BR, 1000)), "br;"), 172 | (Some(QualityValue(BR, 0)), "br;1234asd"), 173 | (Some(QualityValue(BR, 500)), " br ; q=0.5 "), 174 | (Some(QualityValue("*", 1000)), "*"), 175 | (Some(QualityValue("*", 300)), "*;q=0.3"), 176 | (Some(QualityValue("q=123", 1000)), "q=123"), 177 | (None, ""), 178 | ]; 179 | for case in cases { 180 | let res = parse_qvalue(case.1); 181 | assert_eq!(res, case.0, "failed on case: {:?}", case); 182 | } 183 | } 184 | } 185 | 186 | #[cfg(test)] 187 | mod t_sort { 188 | use super::*; 189 | 190 | #[test] 191 | fn same_qualities() { 192 | let brotli = &QualityValue(BR, 1000); 193 | let gzip = &QualityValue(GZIP, 1000); 194 | let deflate = &QualityValue(DEFLATE, 1000); 195 | assert_eq!(sort_encoding(brotli, gzip), Ordering::Greater); 196 | assert_eq!(sort_encoding(brotli, deflate), Ordering::Greater); 197 | assert_eq!(sort_encoding(gzip, deflate), Ordering::Greater); 198 | assert_eq!(sort_encoding(gzip, brotli), Ordering::Less); 199 | assert_eq!(sort_encoding(deflate, brotli), Ordering::Less); 200 | } 201 | 202 | #[test] 203 | fn second_item_with_greater_quality() { 204 | let a = &QualityValue(BR, 500); 205 | let b = &QualityValue(DEFLATE, 1000); 206 | assert_eq!(sort_encoding(a, b), Ordering::Less); 207 | } 208 | } 209 | 210 | #[cfg(test)] 211 | mod t_prior { 212 | use super::*; 213 | use hyper::header::HeaderValue; 214 | 215 | #[test] 216 | fn with_unsupported_encoding() { 217 | // Empty encoding 218 | let accept_encoding = HeaderValue::from_static(""); 219 | let encoding = get_prior_encoding(&accept_encoding); 220 | assert_eq!(encoding, IDENTITY); 221 | 222 | // Deprecated encoding. 223 | let accept_encoding = HeaderValue::from_static("compress"); 224 | let encoding = get_prior_encoding(&accept_encoding); 225 | assert_eq!(encoding, IDENTITY); 226 | } 227 | 228 | #[test] 229 | fn pick_highest_priority() { 230 | let cases = vec![ 231 | (BR, "br,gzip,deflate"), 232 | (BR, "gzip,br,deflate"), 233 | (BR, "deflate,gzip,br"), 234 | (BR, "br;q=0.8,gzip;q=0.5,deflate;q=0.2"), 235 | (GZIP, "br;q=0.5,gzip,deflate;q=0.8"), 236 | ]; 237 | for case in cases { 238 | let accept_encoding = HeaderValue::from_static(case.1); 239 | let encoding = get_prior_encoding(&accept_encoding); 240 | assert_eq!(encoding, case.0, "failed on case: {:?}", case); 241 | } 242 | } 243 | 244 | #[test] 245 | fn filter_out_zero_quality() { 246 | let accept_encoding = HeaderValue::from_static("brotli;q=0,gzip;q=0,deflate"); 247 | let encoding = get_prior_encoding(&accept_encoding); 248 | assert_eq!(encoding, DEFLATE); 249 | } 250 | } 251 | 252 | #[cfg(test)] 253 | mod t_compress { 254 | use super::*; 255 | 256 | #[test] 257 | fn failed() { 258 | let s = futures::stream::iter(vec![Ok::<_, io::Error>(Bytes::from_static(b"hello"))]); 259 | let error = compress_stream(s, "unrecognized").unwrap_err(); 260 | assert_eq!(error.kind(), io::ErrorKind::Other); 261 | } 262 | 263 | #[tokio::test] 264 | async fn compressed() { 265 | let s = futures::stream::iter(vec![Ok::<_, io::Error>(Bytes::from_static(b"xxxxx"))]); 266 | let body = compress_stream(s, BR).unwrap(); 267 | assert_eq!(hyper::body::to_bytes(body).await.unwrap().len(), 9); 268 | 269 | let s = futures::stream::iter(vec![Ok::<_, io::Error>(Bytes::from_static(b"xxxxx"))]); 270 | let body = compress_stream(s, DEFLATE).unwrap(); 271 | assert_eq!(hyper::body::to_bytes(body).await.unwrap().len(), 5); 272 | 273 | let s = futures::stream::iter(vec![Ok::<_, io::Error>(Bytes::from_static(b"xxxxx"))]); 274 | let body = compress_stream(s, GZIP).unwrap(); 275 | assert_eq!(hyper::body::to_bytes(body).await.unwrap().len(), 23); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/http/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Weihang Lo 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | pub mod conditional_requests; 10 | pub mod content_encoding; 11 | pub mod range_requests; 12 | -------------------------------------------------------------------------------- /src/http/range_requests.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Weihang Lo 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use headers::{ContentRange, ETag, HeaderMapExt, IfRange, LastModified, Range}; 10 | 11 | use crate::server::Request; 12 | 13 | /// Check if given value from `If-Range` header field is fresh. 14 | /// 15 | /// According to RFC7232, to validate `If-Range` header, the implementation 16 | /// must use a strong comparison. 17 | pub fn is_range_fresh(req: &Request, etag: &ETag, last_modified: &LastModified) -> bool { 18 | // Ignore `If-Range` if `Range` header is not present. 19 | if req.headers().typed_get::().is_none() { 20 | return false; 21 | } 22 | 23 | req.headers() 24 | .typed_get::() 25 | .map(|if_range| !if_range.is_modified(Some(etag), Some(last_modified))) 26 | // Always be fresh if there is no validators 27 | .unwrap_or(true) 28 | } 29 | 30 | /// Convert `Range` header field in incoming request to `Content-Range` header 31 | /// field for response. 32 | /// 33 | /// Here are all situations mapped to returning `Option`: 34 | /// 35 | /// - None byte-range -> None 36 | /// - One satisfiable byte-range -> Some 37 | /// - One not satisfiable byte-range -> None 38 | /// - Two or more byte-ranges -> None 39 | /// - bytes-units are not in "bytes" -> None 40 | /// 41 | /// A satisfiable byte range must conform to following criteria: 42 | /// 43 | /// - Invalid if the last-byte-pos is present and less than the first-byte-pos. 44 | /// - First-byte-pos must be less than complete length of the representation. 45 | /// - If suffix-byte-range-spec is present, it must not be zero. 46 | pub fn is_satisfiable_range(range: &Range, complete_length: u64) -> Option { 47 | use core::ops::Bound::{Included, Unbounded}; 48 | let mut iter = range.iter(); 49 | let bounds = iter.next(); 50 | 51 | if iter.next().is_some() { 52 | // Found multiple byte-range-spec. Drop. 53 | return None; 54 | } 55 | 56 | bounds.and_then(|b| match b { 57 | (Included(start), Included(end)) if start <= end && start < complete_length => { 58 | ContentRange::bytes( 59 | start..=end.min(complete_length.saturating_sub(1)), 60 | complete_length, 61 | ) 62 | .ok() 63 | } 64 | (Included(start), Unbounded) if start < complete_length => { 65 | ContentRange::bytes(start.., complete_length).ok() 66 | } 67 | (Unbounded, Included(end)) if end > 0 => { 68 | ContentRange::bytes(complete_length.saturating_sub(end).., complete_length).ok() 69 | } 70 | _ => None, 71 | }) 72 | } 73 | 74 | #[cfg(test)] 75 | mod t_range { 76 | use super::*; 77 | use std::time::{Duration, SystemTime}; 78 | 79 | #[test] 80 | fn no_range_header() { 81 | // Ignore range freshness validation. Return ture. 82 | let req = &mut Request::default(); 83 | let last_modified = &LastModified::from(SystemTime::now()); 84 | let etag = &"\"strong\"".to_string().parse::().unwrap(); 85 | let if_range = IfRange::etag(etag.clone()); 86 | req.headers_mut().typed_insert(if_range); 87 | assert!(!is_range_fresh(req, etag, last_modified)); 88 | } 89 | 90 | #[test] 91 | fn no_if_range_header() { 92 | // Ignore if-range freshness validation. Return ture. 93 | let req = &mut Request::default(); 94 | req.headers_mut().typed_insert(Range::bytes(0..).unwrap()); 95 | let last_modified = &LastModified::from(SystemTime::now()); 96 | let etag = &"\"strong\"".to_string().parse::().unwrap(); 97 | // Always be fresh if there is no validators 98 | assert!(is_range_fresh(req, etag, last_modified)); 99 | } 100 | 101 | #[test] 102 | fn weak_validator_as_falsy() { 103 | let req = &mut Request::default(); 104 | req.headers_mut().typed_insert(Range::bytes(0..).unwrap()); 105 | 106 | let last_modified = &LastModified::from(SystemTime::now()); 107 | let etag = &"W/\"weak\"".to_string().parse::().unwrap(); 108 | let if_range = IfRange::etag(etag.clone()); 109 | req.headers_mut().typed_insert(if_range); 110 | assert!(!is_range_fresh(req, etag, last_modified)); 111 | } 112 | 113 | #[test] 114 | fn only_accept_exact_match_mtime() { 115 | let req = &mut Request::default(); 116 | let etag = &"\"strong\"".to_string().parse::().unwrap(); 117 | let date = SystemTime::now(); 118 | let last_modified = &LastModified::from(date); 119 | req.headers_mut().typed_insert(Range::bytes(0..).unwrap()); 120 | 121 | // Same date. 122 | req.headers_mut().typed_insert(IfRange::date(date)); 123 | assert!(is_range_fresh(req, etag, last_modified)); 124 | 125 | // Before 10 sec. 126 | let past = date - Duration::from_secs(10); 127 | req.headers_mut().typed_insert(IfRange::date(past)); 128 | assert!(!is_range_fresh(req, etag, last_modified)); 129 | 130 | // After 10 sec. 131 | // 132 | // TODO: Uncomment the assertion after someone fixes the issue. 133 | // 134 | // [RFC7233: 3.2. If-Range][1] describe that `If-Range` validation must 135 | // comparison by exact match. However, the [current implementation][2] 136 | // is doing it wrong! 137 | // 138 | // [1]: https://tools.ietf.org/html/rfc7233#section-3.2 139 | // [2]: https://github.com/hyperium/headers/blob/2e8c12b/src/common/if_range.rs#L66 140 | let future = date + Duration::from_secs(10); 141 | req.headers_mut().typed_insert(IfRange::date(future)); 142 | // assert!(!is_range_fresh(req, etag, last_modified)); 143 | } 144 | 145 | #[test] 146 | fn strong_validator() { 147 | let req = &mut Request::default(); 148 | req.headers_mut().typed_insert(Range::bytes(0..).unwrap()); 149 | 150 | let last_modified = &LastModified::from(SystemTime::now()); 151 | let etag = &"\"strong\"".to_string().parse::().unwrap(); 152 | let if_range = IfRange::etag(etag.clone()); 153 | req.headers_mut().typed_insert(if_range); 154 | assert!(is_range_fresh(req, etag, last_modified)); 155 | } 156 | } 157 | 158 | #[cfg(test)] 159 | mod t_satisfiable { 160 | use super::*; 161 | 162 | #[test] 163 | fn zero_byte_range() { 164 | let range = &Range::bytes(1..1).unwrap(); 165 | assert!(is_satisfiable_range(range, 10).is_none()); 166 | } 167 | 168 | #[test] 169 | fn one_satisfiable_byte_range() { 170 | let range = &Range::bytes(4..=6).unwrap(); 171 | let complete_length = 10; 172 | let content_range = is_satisfiable_range(range, complete_length); 173 | assert_eq!( 174 | content_range, 175 | ContentRange::bytes(4..7, complete_length).ok() 176 | ); 177 | 178 | // only first-byte-pos and retrieve to the end 179 | let range = &Range::bytes(3..).unwrap(); 180 | let complete_length = 10; 181 | let content_range = is_satisfiable_range(range, complete_length); 182 | assert_eq!( 183 | content_range, 184 | ContentRange::bytes(3..10, complete_length).ok() 185 | ); 186 | 187 | // last-byte-pos exceeds complete length 188 | let range = &Range::bytes(7..20).unwrap(); 189 | let complete_length = 10; 190 | let content_range = is_satisfiable_range(range, complete_length); 191 | assert_eq!( 192 | content_range, 193 | ContentRange::bytes(7..10, complete_length).ok() 194 | ); 195 | 196 | // suffix-byte-range-spec 197 | let range = &Range::bytes(..=3).unwrap(); 198 | let complete_length = 10; 199 | let content_range = is_satisfiable_range(range, complete_length); 200 | assert_eq!( 201 | content_range, 202 | ContentRange::bytes(7..10, complete_length).ok() 203 | ); 204 | 205 | // suffix-byte-range-spec greater than complete length 206 | let range = &Range::bytes(..20).unwrap(); 207 | let complete_length = 10; 208 | let content_range = is_satisfiable_range(range, complete_length); 209 | assert_eq!( 210 | content_range, 211 | ContentRange::bytes(0..10, complete_length).ok() 212 | ); 213 | } 214 | 215 | #[test] 216 | fn one_unsatisfiable_byte_range() { 217 | // First-byte-pos is greater than complete length. 218 | let range = &Range::bytes(20..).unwrap(); 219 | assert!(is_satisfiable_range(range, 10).is_none()); 220 | 221 | // Last-bypte-pos is less than first-byte-pos 222 | let range = &Range::bytes(5..3).unwrap(); 223 | assert!(is_satisfiable_range(range, 10).is_none()); 224 | 225 | // suffix-byte-range-spec must be non-zero 226 | let mut headers = headers::HeaderMap::new(); 227 | headers.insert( 228 | hyper::header::RANGE, 229 | headers::HeaderValue::from_static("bytes=-0"), 230 | ); 231 | let range = &headers.typed_get::().unwrap(); 232 | assert!(is_satisfiable_range(range, 10).is_none()); 233 | } 234 | 235 | #[test] 236 | fn multiple_byte_ranges() { 237 | let mut headers = headers::HeaderMap::new(); 238 | headers.insert( 239 | hyper::header::RANGE, 240 | headers::HeaderValue::from_static("bytes=0-1,30-40"), 241 | ); 242 | let range = &headers.typed_get::().unwrap(); 243 | assert!(is_satisfiable_range(range, 10).is_none()); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Weihang Lo 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | macro_rules! bail { 10 | ($($tt:tt)*) => { 11 | return Err(From::from(format!($($tt)*))) 12 | } 13 | } 14 | 15 | mod cli; 16 | mod extensions; 17 | mod http; 18 | mod server; 19 | #[cfg(test)] 20 | pub mod test_utils; 21 | 22 | use crate::cli::{matches, Args}; 23 | use crate::server::serve; 24 | 25 | pub type BoxResult = Result>; 26 | 27 | #[tokio::main] 28 | async fn main() { 29 | Args::parse(matches()) 30 | .map(serve) 31 | .unwrap_or_else(handle_err) 32 | .await 33 | .unwrap_or_else(handle_err); 34 | } 35 | 36 | fn handle_err(err: Box) -> T { 37 | eprintln!("Server error: {}", err); 38 | std::process::exit(1); 39 | } 40 | -------------------------------------------------------------------------------- /src/server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Files in {{ dir_name }}/ 7 | 8 | 9 | 10 | 11 | {% for breadcrumb in breadcrumbs %} 12 | {% if loop.last %} 13 | {{ breadcrumb.name }} 14 | {% elif loop.first and not loop.last %} 15 | {{ breadcrumb.name }} 16 | {% else %} 17 | {{ breadcrumb.name }} 18 | {% endif %} 19 | / 20 | {% endfor %} 21 | 22 | 23 | 24 | 25 | 26 | {% for file in files %} 27 | 28 | 29 | {% if file.path_type == "Dir" %} 30 | 31 | {% elif file.path_type == "File" %} 32 | 33 | {% elif file.path_type == "SymlinkDir" %} 34 | 35 | {% else %} 36 | 37 | {% endif %} 38 | 39 | {{ file.name }} 40 | 41 | {% endfor %} 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/server/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Weihang Lo 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | mod res; 10 | mod send; 11 | mod serve; 12 | 13 | pub type Request = hyper::Request; 14 | pub type Response = hyper::Response; 15 | 16 | pub use self::serve::{serve, PathType}; 17 | -------------------------------------------------------------------------------- /src/server/res.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Weihang Lo 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | //! Response factory functions. 10 | //! 11 | 12 | use headers::{ContentLength, HeaderMapExt}; 13 | use hyper::StatusCode; 14 | 15 | use crate::server::Response; 16 | 17 | /// Generate 304 NotModified response. 18 | pub fn not_modified(mut res: Response) -> Response { 19 | *res.status_mut() = StatusCode::NOT_MODIFIED; 20 | res 21 | } 22 | 23 | /// Generate 403 Forbidden response. 24 | pub fn forbidden(res: Response) -> Response { 25 | prepare_response(res, StatusCode::FORBIDDEN, "403 Forbidden") 26 | } 27 | 28 | /// Generate 404 NotFound response. 29 | pub fn not_found(res: Response) -> Response { 30 | prepare_response(res, StatusCode::NOT_FOUND, "404 Not Found") 31 | } 32 | 33 | /// Generate 412 PreconditionFailed response. 34 | pub fn precondition_failed(res: Response) -> Response { 35 | prepare_response( 36 | res, 37 | StatusCode::PRECONDITION_FAILED, 38 | "412 Precondition Failed", 39 | ) 40 | } 41 | 42 | /// Generate 500 InternalServerError response. 43 | pub fn internal_server_error(res: Response) -> Response { 44 | prepare_response( 45 | res, 46 | StatusCode::INTERNAL_SERVER_ERROR, 47 | "500 Internal Server Error", 48 | ) 49 | } 50 | 51 | fn prepare_response(mut res: Response, code: StatusCode, body: &'static str) -> Response { 52 | *res.status_mut() = code; 53 | *res.body_mut() = body.into(); 54 | res.headers_mut() 55 | .typed_insert(ContentLength(body.len() as u64)); 56 | res 57 | } 58 | 59 | #[cfg(test)] 60 | mod t { 61 | use super::*; 62 | 63 | #[test] 64 | fn response_304() { 65 | let res = not_modified(Response::default()); 66 | assert_eq!(res.status(), StatusCode::NOT_MODIFIED); 67 | } 68 | 69 | #[test] 70 | fn response_403() { 71 | let res = forbidden(Response::default()); 72 | assert_eq!(res.status(), StatusCode::FORBIDDEN); 73 | } 74 | 75 | #[test] 76 | fn response_404() { 77 | let res = not_found(Response::default()); 78 | assert_eq!(res.status(), StatusCode::NOT_FOUND); 79 | } 80 | 81 | #[test] 82 | fn response_412() { 83 | let res = precondition_failed(Response::default()); 84 | assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED); 85 | } 86 | 87 | #[test] 88 | fn response_500() { 89 | let res = internal_server_error(Response::default()); 90 | assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/server/send.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Weihang Lo 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use std::convert::AsRef; 10 | use std::fs::File; 11 | use std::io::{self, BufReader, Read, Seek, SeekFrom}; 12 | use std::path::Path; 13 | use std::pin::Pin; 14 | use std::sync::Mutex; 15 | use std::task::Poll; 16 | 17 | use bytes::BytesMut; 18 | use futures::Stream; 19 | use ignore::WalkBuilder; 20 | use serde::Serialize; 21 | use tera::{Context, Tera}; 22 | use zip::ZipWriter; 23 | 24 | use crate::extensions::PathExt; 25 | use crate::server::PathType; 26 | 27 | /// Serializable `Item` that would be passed to Tera for template rendering. 28 | /// The order of struct fields is deremined to ensure sorting precedence. 29 | #[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)] 30 | struct Item { 31 | path_type: PathType, 32 | name: String, 33 | path: String, 34 | } 35 | 36 | /// Breadcrumb represents a directory name and a path. 37 | #[derive(Debug, Serialize)] 38 | struct Breadcrumb<'a> { 39 | name: &'a str, 40 | path: String, 41 | } 42 | 43 | /// Walking inside a directory recursively 44 | fn get_dir_contents>( 45 | dir_path: P, 46 | with_ignore: bool, 47 | show_all: bool, 48 | depth: Option, 49 | ) -> ignore::Walk { 50 | WalkBuilder::new(dir_path) 51 | .standard_filters(false) // Disable all standard filters. 52 | .git_ignore(with_ignore) 53 | .hidden(!show_all) // Filter out hidden entries on demand. 54 | .max_depth(depth) // Do not traverse subpaths. 55 | .build() 56 | } 57 | 58 | /// Send a HTML page of all files under the path. 59 | /// 60 | /// # Parameters 61 | /// 62 | /// * `dir_path` - Directory to be listed files. 63 | /// * `base_path` - The base path resolving all filepaths under `dir_path`. 64 | /// * `show_all` - Whether to show hidden and 'dot' files. 65 | /// * `with_ignore` - Whether to respet gitignore files. 66 | /// * `path_prefix` - The url path prefix optionally defined 67 | pub fn send_dir, P2: AsRef>( 68 | dir_path: P1, 69 | base_path: P2, 70 | show_all: bool, 71 | with_ignore: bool, 72 | path_prefix: Option<&str>, 73 | ) -> io::Result<(Vec, usize)> { 74 | let base_path = base_path.as_ref(); 75 | let dir_path = dir_path.as_ref(); 76 | // Prepare dirname of current dir relative to base path. 77 | let prefix = path_prefix.unwrap_or(""); 78 | 79 | // Breadcrumbs for navigation. 80 | let breadcrumbs = create_breadcrumbs(dir_path, base_path, prefix); 81 | 82 | // Collect filename and there links. 83 | let files_iter = get_dir_contents(dir_path, with_ignore, show_all, Some(1)) 84 | .filter_map(|entry| entry.ok()) 85 | .filter(|entry| dir_path != entry.path()) // Exclude `.` 86 | .map(|entry| { 87 | let abs_path = entry.path(); 88 | // Get relative path. 89 | let rel_path = abs_path.strip_prefix(base_path).unwrap(); 90 | let rel_path_ref = rel_path.to_str().unwrap_or_default(); 91 | 92 | Item { 93 | path_type: abs_path.type_(), 94 | name: rel_path.filename_str().to_owned(), 95 | path: format!( 96 | "{}/{}", 97 | prefix, 98 | if cfg!(windows) { 99 | rel_path_ref.replace("\\", "/") 100 | } else { 101 | rel_path_ref.to_string() 102 | } 103 | ), 104 | } 105 | }); 106 | 107 | let mut files = if base_path == dir_path { 108 | // CWD == base dir 109 | files_iter.collect::>() 110 | } else { 111 | // CWD == sub dir of base dir 112 | // Append an item for popping back to parent directory. 113 | 114 | let path = format!( 115 | "{}/{}", 116 | prefix, 117 | dir_path 118 | .parent() 119 | .unwrap() 120 | .strip_prefix(base_path) 121 | .unwrap() 122 | .to_str() 123 | .unwrap() 124 | ); 125 | 126 | vec![Item { 127 | name: "..".to_owned(), 128 | path, 129 | path_type: PathType::Dir, 130 | }] 131 | .into_iter() 132 | .chain(files_iter) 133 | .collect::>() 134 | }; 135 | // Sort files (dir-first and lexicographic ordering). 136 | files.sort_unstable(); 137 | 138 | let content = render(dir_path.filename_str(), &files, &breadcrumbs).into_bytes(); 139 | let size = content.len(); 140 | Ok((content, size)) 141 | } 142 | 143 | #[derive(Debug)] 144 | pub struct FileStream { 145 | reader: Mutex, 146 | } 147 | 148 | impl Stream for FileStream { 149 | type Item = io::Result; 150 | 151 | fn poll_next(self: Pin<&mut Self>, _: &mut std::task::Context<'_>) -> Poll> { 152 | let mut r = match self.reader.lock() { 153 | Ok(r) => r, 154 | Err(e) => { 155 | eprintln!("{e:?}"); 156 | let e = io::Error::new(io::ErrorKind::Other, "Failed to read file"); 157 | return Poll::Ready(Some(Err(e))); 158 | } 159 | }; 160 | let mut buf = BytesMut::zeroed(4_096); 161 | match r.read(&mut buf[..]) { 162 | Ok(bytes) => { 163 | if bytes == 0 { 164 | Poll::Ready(None) 165 | } else { 166 | buf.truncate(bytes); 167 | Poll::Ready(Some(Ok(buf.freeze()))) 168 | } 169 | } 170 | Err(e) => Poll::Ready(Some(Err(e))), 171 | } 172 | } 173 | } 174 | 175 | /// Send a stream of file to client. 176 | pub fn send_file>(file_path: P) -> io::Result<(FileStream>, u64)> { 177 | let file = File::open(file_path)?; 178 | let size = file.metadata()?.len(); 179 | let reader = Mutex::new(BufReader::new(file)); 180 | Ok((FileStream { reader }, size)) 181 | } 182 | 183 | /// Sending a directory as zip buffer 184 | pub fn send_dir_as_zip>( 185 | dir_path: P, 186 | show_all: bool, 187 | with_ignore: bool, 188 | ) -> io::Result<(FileStream>, u64)> { 189 | let dir_path = dir_path.as_ref(); 190 | 191 | // Creating a temporary file to make zip file 192 | let zip_file = tempfile::tempfile()?; 193 | let mut zip_writer = ZipWriter::new(zip_file); 194 | 195 | let zip_options = zip::write::FileOptions::default() 196 | .compression_method(zip::CompressionMethod::Stored) 197 | .unix_permissions(0o755); 198 | 199 | // Recursively finding files and directories 200 | let files_iter = get_dir_contents(dir_path, with_ignore, show_all, None) 201 | .filter_map(|entry| entry.ok()) 202 | .filter(|entry| entry.path() != dir_path); 203 | 204 | for dir_entry in files_iter { 205 | let file_path = dir_entry.path(); 206 | let name = file_path.strip_prefix(dir_path).unwrap().to_str().unwrap(); 207 | 208 | if file_path.is_dir() { 209 | zip_writer 210 | .add_directory(name, zip_options) 211 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; 212 | } else { 213 | zip_writer 214 | .start_file(name, zip_options) 215 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; 216 | let mut file = File::open(file_path)?; 217 | 218 | std::io::copy(&mut file, &mut zip_writer)?; 219 | } 220 | } 221 | 222 | let mut zip = zip_writer 223 | .finish() 224 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; 225 | 226 | zip.seek(SeekFrom::Start(0))?; 227 | 228 | let size = zip.metadata()?.len(); 229 | let reader = Mutex::new(BufReader::new(zip)); 230 | Ok((FileStream { reader }, size)) 231 | } 232 | 233 | /// Send a stream with specific range. 234 | /// 235 | /// # Parameters 236 | /// 237 | /// * `file_path` - Path to the file that is going to send. 238 | /// * `range` - Tuple of `(start, end)` range (inclusive). 239 | pub fn send_file_with_range>( 240 | file_path: P, 241 | range: (u64, u64), 242 | ) -> io::Result<(FileStream>>, u64)> { 243 | let (start, end) = range; // TODO: should return HTTP 416 244 | if end < start { 245 | return Err(io::Error::from(io::ErrorKind::InvalidInput)); 246 | } 247 | 248 | let mut f = File::open(file_path)?; 249 | let max_end = f.metadata()?.len() - 1; 250 | f.seek(SeekFrom::Start(start))?; 251 | 252 | let reader = Mutex::new(BufReader::new(f).take(end - start + 1)); 253 | let size = if start > max_end { 254 | 0 255 | } else { 256 | std::cmp::min(end, max_end) - start + 1 257 | }; 258 | Ok((FileStream { reader }, size)) 259 | } 260 | 261 | /// Create breadcrumbs for navigation. 262 | fn create_breadcrumbs<'a>( 263 | dir_path: &'a Path, 264 | base_path: &'a Path, 265 | prefix: &str, 266 | ) -> Vec> { 267 | let base_breadcrumb = Breadcrumb { 268 | name: base_path.filename_str(), 269 | path: format!("{}/", prefix), 270 | }; 271 | vec![base_breadcrumb] 272 | .into_iter() 273 | .chain( 274 | dir_path 275 | .strip_prefix(base_path) 276 | .unwrap() 277 | .iter() 278 | .map(|s| s.to_str().unwrap()) 279 | .scan(prefix.to_string(), |path, name| { 280 | path.push('/'); 281 | path.push_str(name); 282 | Some(Breadcrumb { 283 | name, 284 | path: path.clone(), 285 | }) 286 | }), 287 | ) 288 | .collect::>() 289 | } 290 | 291 | /// Render page with Tera template engine. 292 | fn render(dir_name: &str, files: &[Item], breadcrumbs: &[Breadcrumb]) -> String { 293 | let mut ctx = Context::new(); 294 | ctx.insert("dir_name", dir_name); 295 | ctx.insert("files", files); 296 | ctx.insert("breadcrumbs", breadcrumbs); 297 | ctx.insert("style", include_str!("style.css")); 298 | Tera::one_off(include_str!("index.html"), &ctx, true) 299 | .unwrap_or_else(|e| format!("500 Internal server error: {}", e)) 300 | } 301 | 302 | #[cfg(test)] 303 | mod t { 304 | use super::*; 305 | 306 | #[test] 307 | fn render_successfully() { 308 | let page = render("", &vec![], &vec![]); 309 | assert!(page.starts_with("")) 310 | } 311 | #[test] 312 | fn breadcrumbs() { 313 | // Only one level 314 | let base_path = Path::new("/a"); 315 | let dir_path = Path::new("/a"); 316 | let breadcrumbs = create_breadcrumbs(dir_path, base_path, ""); 317 | assert_eq!(breadcrumbs.len(), 1); 318 | assert_eq!(breadcrumbs[0].name, "a"); 319 | assert_eq!(breadcrumbs[0].path, "/"); 320 | 321 | // Nested two levels 322 | let base_path = Path::new("/a"); 323 | let dir_path = Path::new("/a/b"); 324 | let breadcrumbs = create_breadcrumbs(dir_path, base_path, ""); 325 | assert_eq!(breadcrumbs.len(), 2); 326 | assert_eq!(breadcrumbs[0].name, "a"); 327 | assert_eq!(breadcrumbs[0].path, "/"); 328 | assert_eq!(breadcrumbs[1].name, "b"); 329 | assert_eq!(breadcrumbs[1].path, "/b"); 330 | 331 | // Nested four levels 332 | let base_path = Path::new("/a"); 333 | let dir_path = Path::new("/a/b/c/d"); 334 | let breadcrumbs = create_breadcrumbs(dir_path, base_path, ""); 335 | assert_eq!(breadcrumbs.len(), 4); 336 | assert_eq!(breadcrumbs[0].name, "a"); 337 | assert_eq!(breadcrumbs[0].path, "/"); 338 | assert_eq!(breadcrumbs[1].name, "b"); 339 | assert_eq!(breadcrumbs[1].path, "/b"); 340 | assert_eq!(breadcrumbs[2].name, "c"); 341 | assert_eq!(breadcrumbs[2].path, "/b/c"); 342 | assert_eq!(breadcrumbs[3].name, "d"); 343 | assert_eq!(breadcrumbs[3].path, "/b/c/d"); 344 | } 345 | 346 | #[test] 347 | fn breadcrumbs_with_slashes() { 348 | let base_path = Path::new("////a/b"); 349 | let dir_path = Path::new("////////a//////b///c////////////"); 350 | let breadcrumbs = create_breadcrumbs(dir_path, base_path, ""); 351 | assert_eq!(breadcrumbs.len(), 2); 352 | assert_eq!(breadcrumbs[0].name, "b"); 353 | assert_eq!(breadcrumbs[0].path, "/"); 354 | assert_eq!(breadcrumbs[1].name, "c"); 355 | assert_eq!(breadcrumbs[1].path, "/c"); 356 | } 357 | 358 | #[test] 359 | fn prefixed_breadcrumbs() { 360 | let base_path = Path::new("/a"); 361 | let dir_path = Path::new("/a/b/c"); 362 | let breadcrumbs = create_breadcrumbs(dir_path, base_path, "/xdd~帥//"); 363 | assert_eq!(breadcrumbs.len(), 3); 364 | assert_eq!(breadcrumbs[0].name, "a"); 365 | assert_eq!(breadcrumbs[0].path, "/xdd~帥///"); 366 | assert_eq!(breadcrumbs[1].name, "b"); 367 | assert_eq!(breadcrumbs[1].path, "/xdd~帥///b"); 368 | assert_eq!(breadcrumbs[2].name, "c"); 369 | assert_eq!(breadcrumbs[2].path, "/xdd~帥///b/c"); 370 | } 371 | 372 | #[test] 373 | fn breadcrumbs_from_root() { 374 | let base_path = Path::new("/"); 375 | let dir_path = Path::new("/a/b"); 376 | let breadcrumbs = create_breadcrumbs(dir_path, base_path, ""); 377 | assert_eq!(breadcrumbs.len(), 3); 378 | assert_eq!(breadcrumbs[0].name, ""); 379 | assert_eq!(breadcrumbs[0].path, "/"); 380 | assert_eq!(breadcrumbs[1].name, "a"); 381 | assert_eq!(breadcrumbs[1].path, "/a"); 382 | assert_eq!(breadcrumbs[2].name, "b"); 383 | assert_eq!(breadcrumbs[2].path, "/a/b"); 384 | } 385 | } 386 | 387 | #[cfg(test)] 388 | mod t_send { 389 | use futures::StreamExt; 390 | 391 | use super::*; 392 | 393 | fn file_txt_path() -> std::path::PathBuf { 394 | let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); 395 | path.push("./tests/file.txt"); 396 | path 397 | } 398 | 399 | fn dir_with_sub_dir_path() -> std::path::PathBuf { 400 | let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); 401 | path.push("./tests/dir_with_sub_dirs/"); 402 | path 403 | } 404 | 405 | fn missing_file_path() -> std::path::PathBuf { 406 | let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); 407 | path.push("./missing/file"); 408 | path 409 | } 410 | 411 | #[ignore] 412 | #[test] 413 | fn t_send_dir() {} 414 | 415 | async fn stream_to_vec(mut s: FileStream) -> Vec { 416 | let mut buf = vec![]; 417 | while let Some(r) = s.next().await { 418 | if let Ok(b) = r { 419 | buf.extend_from_slice(&b); 420 | } 421 | } 422 | buf 423 | } 424 | 425 | #[tokio::test] 426 | async fn t_send_file_success() { 427 | let (s, size) = send_file(file_txt_path()).unwrap(); 428 | assert!(size > 0); 429 | 430 | let buf = stream_to_vec(s).await; 431 | assert_eq!(&buf, b"01234567"); 432 | } 433 | 434 | #[test] 435 | fn t_send_file_not_found() { 436 | let buf = send_file(missing_file_path()); 437 | assert_eq!(buf.unwrap_err().kind(), std::io::ErrorKind::NotFound); 438 | } 439 | 440 | #[tokio::test] 441 | async fn t_send_file_with_range_one_byte() { 442 | for i in 0..=7 { 443 | let (s, size) = send_file_with_range(file_txt_path(), (i, i)).unwrap(); 444 | let buf = stream_to_vec(s).await; 445 | assert_eq!(buf, i.to_string().as_bytes()); 446 | assert_eq!(size, 1); 447 | } 448 | } 449 | 450 | #[tokio::test] 451 | async fn t_send_file_with_range_multiple_bytes() { 452 | let (s, size) = send_file_with_range(file_txt_path(), (0, 1)).unwrap(); 453 | let buf = stream_to_vec(s).await; 454 | assert_eq!(buf, b"01"); 455 | assert_eq!(size, 2); 456 | let (s, size) = send_file_with_range(file_txt_path(), (1, 2)).unwrap(); 457 | let buf = stream_to_vec(s).await; 458 | assert_eq!(buf, b"12"); 459 | assert_eq!(size, 2); 460 | let (s, size) = send_file_with_range(file_txt_path(), (1, 4)).unwrap(); 461 | let buf = stream_to_vec(s).await; 462 | assert_eq!(buf, b"1234"); 463 | assert_eq!(size, 4); 464 | let (s, size) = send_file_with_range(file_txt_path(), (7, 65535)).unwrap(); 465 | let buf = stream_to_vec(s).await; 466 | assert_eq!(buf, b"7"); 467 | assert_eq!(size, 1); 468 | let (s, size) = send_file_with_range(file_txt_path(), (8, 8)).unwrap(); 469 | let buf = stream_to_vec(s).await; 470 | assert_eq!(buf, b""); 471 | assert_eq!(size, 0); 472 | } 473 | 474 | #[test] 475 | fn t_send_file_with_range_not_found() { 476 | let buf = send_file_with_range(missing_file_path(), (0, 0)); 477 | assert_eq!(buf.unwrap_err().kind(), std::io::ErrorKind::NotFound); 478 | } 479 | 480 | #[test] 481 | fn t_send_file_with_range_invalid_range() { 482 | // TODO: HTTP code 416 483 | let buf = send_file_with_range(file_txt_path(), (1, 0)); 484 | assert_eq!(buf.unwrap_err().kind(), std::io::ErrorKind::InvalidInput); 485 | } 486 | 487 | #[tokio::test] 488 | async fn t_send_dir_as_zip() { 489 | let s = send_dir_as_zip(dir_with_sub_dir_path(), true, false); 490 | assert!(s.is_ok()); 491 | 492 | let (s, size) = s.unwrap(); 493 | assert!(size > 0); 494 | 495 | let v = stream_to_vec(s).await; 496 | assert!(v.len() > 0); 497 | 498 | // https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html#localheader 499 | assert_eq!(&v[0..4], &[0x50, 0x4b, 0x03, 0x04]); 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /src/server/serve.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Weihang Lo 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use std::convert::{AsRef, Infallible}; 10 | use std::io; 11 | use std::path::{Path, PathBuf}; 12 | use std::str::Utf8Error; 13 | use std::sync::Arc; 14 | use std::time::Duration; 15 | 16 | use chrono::Local; 17 | use futures::TryStreamExt as _; 18 | use headers::{ 19 | AcceptRanges, AccessControlAllowHeaders, AccessControlAllowOrigin, CacheControl, ContentLength, 20 | ContentType, ETag, HeaderMapExt, LastModified, Range, Server, 21 | }; 22 | // Can not use headers::ContentDisposition. Because of https://github.com/hyperium/headers/issues/8 23 | use hyper::header::{HeaderValue, CONTENT_DISPOSITION}; 24 | use hyper::service::{make_service_fn, service_fn}; 25 | use hyper::{Body, StatusCode}; 26 | use ignore::gitignore::Gitignore; 27 | use mime_guess::mime; 28 | use percent_encoding::percent_decode; 29 | use qstring::QString; 30 | use serde::Serialize; 31 | 32 | use crate::cli::Args; 33 | use crate::extensions::{MimeExt, PathExt, SystemTimeExt}; 34 | use crate::http::conditional_requests::{is_fresh, is_precondition_failed}; 35 | use crate::http::content_encoding::{compress_stream, get_prior_encoding, should_compress}; 36 | use crate::http::range_requests::{is_range_fresh, is_satisfiable_range}; 37 | 38 | use crate::server::send::{send_dir, send_dir_as_zip, send_file, send_file_with_range}; 39 | use crate::server::{res, Request, Response}; 40 | use crate::BoxResult; 41 | 42 | const SERVER_VERSION: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); 43 | const CROSS_ORIGIN_EMBEDDER_POLICY: &str = "Cross-Origin-Embedder-Policy"; 44 | const CROSS_ORIGIN_OPENER_POLICY: &str = "Cross-Origin-Opener-Policy"; 45 | 46 | /// Indicate that a path is a normal file/dir or a symlink to another path/dir. 47 | /// 48 | /// This enum is serializable in order to rendering with Tera template engine. 49 | /// And the order of enum variants is deremined to ensure sorting precedence. 50 | #[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)] 51 | pub enum PathType { 52 | Dir, 53 | SymlinkDir, 54 | File, 55 | SymlinkFile, 56 | } 57 | 58 | /// Run the server. 59 | pub async fn serve(args: Args) -> BoxResult<()> { 60 | let address = args.address()?; 61 | let path_prefix = args.path_prefix.clone().unwrap_or_default(); 62 | 63 | let inner = Arc::new(InnerService::new(args)); 64 | let make_svc = make_service_fn(move |_| { 65 | let inner = inner.clone(); 66 | async { 67 | Ok::<_, Infallible>(service_fn(move |req| { 68 | let inner = inner.clone(); 69 | inner.call(req) 70 | })) 71 | } 72 | }); 73 | let server = hyper::Server::try_bind(&address)?.serve(make_svc); 74 | let address = server.local_addr(); 75 | eprintln!("Files served on http://{address}{path_prefix}"); 76 | server.await?; 77 | 78 | Ok(()) 79 | } 80 | 81 | /// File and folder actions 82 | enum Action { 83 | DownloadZip, 84 | ListDir, 85 | DownloadFile, 86 | } 87 | 88 | struct InnerService { 89 | args: Args, 90 | gitignore: Gitignore, 91 | } 92 | 93 | impl InnerService { 94 | pub fn new(args: Args) -> Self { 95 | let gitignore = Gitignore::new(args.path.join(".gitignore")).0; 96 | Self { args, gitignore } 97 | } 98 | 99 | pub async fn call(self: Arc, req: Request) -> Result { 100 | let res = self 101 | .handle_request(&req) 102 | .await 103 | .unwrap_or_else(|_| res::internal_server_error(Response::default())); 104 | // Logging 105 | // TODO: use proper logging crate 106 | if self.args.log { 107 | println!( 108 | r#"[{}] "{} {}" - {}"#, 109 | Local::now().format("%d/%b/%Y %H:%M:%S"), 110 | req.method(), 111 | req.uri(), 112 | res.status(), 113 | ); 114 | } 115 | // Returning response 116 | Ok(res) 117 | } 118 | 119 | /// Construct file path from request path. 120 | /// 121 | /// 1. Remove leading slash. 122 | /// 2. Strip path prefix if defined 123 | /// 3. URI percent decode. 124 | /// 4. If on windows, switch slashes 125 | /// 5. Concatenate base path and requested path. 126 | fn file_path_from_path(&self, path: &str) -> Result, Utf8Error> { 127 | let decoded = percent_decode(path[1..].as_bytes()).decode_utf8()?; 128 | let slashes_switched = if cfg!(windows) { 129 | decoded.replace("/", "\\") 130 | } else { 131 | decoded.into_owned() 132 | }; 133 | let stripped_path = match self.strip_path_prefix(&slashes_switched) { 134 | Some(path) => path, 135 | None => return Ok(None), 136 | }; 137 | let mut path = self.args.path.join(stripped_path); 138 | if self.args.render_index && path.is_dir() { 139 | path.push("index.html") 140 | } 141 | 142 | Ok(Some(path)) 143 | } 144 | 145 | /// Enable HTTP cache control (current always enable with max-age=0) 146 | fn enable_cache_control(&self, res: &mut Response) { 147 | let header = CacheControl::new() 148 | .with_public() 149 | .with_max_age(Duration::from_secs(self.args.cache)); 150 | res.headers_mut().typed_insert(header); 151 | } 152 | 153 | /// Enable cross-origin resource sharing for given response. 154 | fn enable_cors(&self, res: &mut Response) { 155 | if self.args.cors { 156 | res.headers_mut() 157 | .typed_insert(AccessControlAllowOrigin::ANY); 158 | res.headers_mut().typed_insert( 159 | vec![ 160 | hyper::header::RANGE, 161 | hyper::header::CONTENT_TYPE, 162 | hyper::header::ACCEPT, 163 | hyper::header::ORIGIN, 164 | ] 165 | .into_iter() 166 | .collect::(), 167 | ); 168 | } 169 | } 170 | 171 | /// Enable cross-origin isolation for given response. 172 | fn enable_coi(&self, res: &mut Response) { 173 | if self.args.coi { 174 | res.headers_mut().insert( 175 | CROSS_ORIGIN_EMBEDDER_POLICY, 176 | HeaderValue::from_str("require-corp").unwrap(), 177 | ); 178 | res.headers_mut().insert( 179 | CROSS_ORIGIN_OPENER_POLICY, 180 | HeaderValue::from_str("same-origin").unwrap(), 181 | ); 182 | } 183 | } 184 | 185 | /// Determine if payload should be compressed. 186 | /// 187 | /// Enable compression when all criteria are met: 188 | /// 189 | /// - `compress` arg is true 190 | /// - is not partial responses 191 | /// - is not media contents 192 | /// 193 | /// # Parameters 194 | /// 195 | /// * `status` - Current status code prepared to respond. 196 | /// * `mime` - MIME type of the payload. 197 | fn can_compress(&self, status: StatusCode, mime: &mime::Mime) -> bool { 198 | self.args.compress && status != StatusCode::PARTIAL_CONTENT && !mime.is_compressed_format() 199 | } 200 | 201 | /// Determine critera if given path exists or not. 202 | /// 203 | /// A path exists if matches all rules below: 204 | /// 205 | /// 1. exists 206 | /// 2. is not hidden 207 | /// 3. is not ignored 208 | fn path_exists>(&self, path: P) -> bool { 209 | let path = path.as_ref(); 210 | path.exists() && !self.path_is_hidden(path) && !self.path_is_ignored(path) 211 | } 212 | 213 | /// Determine if given path is hidden. 214 | /// 215 | /// A path is considered as hidden if matches all rules below: 216 | /// 217 | /// 1. `all` arg is false 218 | /// 2. any component of the path is hidden (prefixed with dot `.`) 219 | fn path_is_hidden>(&self, path: P) -> bool { 220 | !self.args.all && path.as_ref().is_relatively_hidden() 221 | } 222 | 223 | /// Determine if given path is ignored. 224 | /// 225 | /// A path is considered as ignored if matches all rules below: 226 | /// 227 | /// 1. `ignore` arg is true 228 | /// 2. matches any rules in .gitignore 229 | fn path_is_ignored>(&self, path: P) -> bool { 230 | let path = path.as_ref(); 231 | self.args.ignore && self.gitignore.matched(path, path.is_dir()).is_ignore() 232 | } 233 | 234 | /// Check if requested resource is under directory of basepath. 235 | /// 236 | /// The given path must be resolved (canonicalized) to eliminate 237 | /// incorrect path reported by symlink path. 238 | fn path_is_under_basepath>(&self, path: P) -> bool { 239 | let path = path.as_ref(); 240 | match path.canonicalize() { 241 | Ok(path) => path.starts_with(&self.args.path), 242 | Err(_) => false, 243 | } 244 | } 245 | 246 | /// Strip the path prefix of the request path. 247 | /// 248 | /// If there is a path prefix defined and `strip_prefix` returns `None`, 249 | /// return None. Otherwise return the path with the prefix stripped. 250 | fn strip_path_prefix<'a, P: AsRef>(&self, path: &'a P) -> Option<&'a Path> { 251 | let path = path.as_ref(); 252 | match self.args.path_prefix.as_deref() { 253 | Some(prefix) => { 254 | let prefix = prefix.trim_start_matches('/'); 255 | path.strip_prefix(prefix).ok() 256 | } 257 | None => Some(path), 258 | } 259 | } 260 | 261 | fn get_content_encoding<'a>( 262 | &'a self, 263 | accept_encoding: Option<&'a HeaderValue>, 264 | status: StatusCode, 265 | mime_type: &'a mime::Mime, 266 | ) -> Option<&'static str> { 267 | if !self.can_compress(status, mime_type) { 268 | return None; 269 | } 270 | let encoding = match accept_encoding { 271 | Some(e) => e, 272 | None => return None, 273 | }; 274 | let content_encoding = get_prior_encoding(encoding); 275 | if !should_compress(content_encoding) { 276 | return None; 277 | } 278 | Some(content_encoding) 279 | } 280 | 281 | /// Request handler for `MyService`. 282 | async fn handle_request(&self, req: &Request) -> BoxResult { 283 | // Construct response. 284 | let mut res = Response::default(); 285 | res.headers_mut() 286 | .typed_insert(Server::from_static(SERVER_VERSION)); 287 | 288 | let path = match self.file_path_from_path(req.uri().path())? { 289 | Some(path) => path, 290 | None => return Ok(res::not_found(res)), 291 | }; 292 | 293 | let default_action = if path.is_dir() { 294 | Action::ListDir 295 | } else { 296 | Action::DownloadFile 297 | }; 298 | 299 | let action = match req.uri().query() { 300 | Some(query) => { 301 | let query = QString::from(query); 302 | 303 | match query.get("action") { 304 | Some(action_str) => match action_str { 305 | "zip" => { 306 | if path.is_dir() { 307 | Action::DownloadZip 308 | } else { 309 | bail!("error: invalid action"); 310 | } 311 | } 312 | _ => bail!("error: invalid action"), 313 | }, 314 | None => default_action, 315 | } 316 | } 317 | None => default_action, 318 | }; 319 | 320 | // CORS headers 321 | self.enable_cors(&mut res); 322 | 323 | // COOP and COEP headers 324 | self.enable_coi(&mut res); 325 | 326 | // Check critera if the path should be ignore (404 NotFound). 327 | if !self.path_exists(&path) { 328 | return Ok(res::not_found(res)); 329 | } 330 | 331 | // Unless `follow_links` arg is on, any resource laid outside 332 | // current directory of basepath are forbidden. 333 | if !self.args.follow_links && !self.path_is_under_basepath(&path) { 334 | return Ok(res::forbidden(res)); 335 | } 336 | 337 | // Prepare response body. 338 | // Being mutable for further modifications. 339 | let mut body = Body::empty(); 340 | let mut content_length = None; 341 | 342 | // Extra process for serving files. 343 | match action { 344 | Action::ListDir => { 345 | let (content, size) = send_dir( 346 | &path, 347 | &self.args.path, 348 | self.args.all, 349 | self.args.ignore, 350 | self.args.path_prefix.as_deref(), 351 | )?; 352 | body = Body::from(content); 353 | content_length = Some(size as u64); 354 | } 355 | Action::DownloadFile => { 356 | // Cache-Control. 357 | self.enable_cache_control(&mut res); 358 | 359 | // Last-Modified-Time from file metadata _mtime_. 360 | let (mtime, size) = (path.mtime(), path.size()); 361 | let last_modified = LastModified::from(mtime); 362 | // Concatenate _modified time_ and _file size_ to 363 | // form a (nearly) strong validator. 364 | let etag = format!(r#""{}-{}""#, mtime.timestamp(), size) 365 | .parse::() 366 | .unwrap(); 367 | 368 | // Validate preconditions of conditional requests. 369 | if is_precondition_failed(req, &etag, mtime) { 370 | return Ok(res::precondition_failed(res)); 371 | } 372 | 373 | // Validate cache freshness. 374 | if is_fresh(req, &etag, mtime) { 375 | res.headers_mut().typed_insert(last_modified); 376 | res.headers_mut().typed_insert(etag); 377 | return Ok(res::not_modified(res)); 378 | } 379 | 380 | // Range Request support. 381 | if let Some(range) = req.headers().typed_get::() { 382 | #[allow(clippy::single_match)] 383 | match ( 384 | is_range_fresh(req, &etag, &last_modified), 385 | is_satisfiable_range(&range, size as u64), 386 | ) { 387 | (true, Some(content_range)) => { 388 | // 206 Partial Content. 389 | if let Some(range) = content_range.bytes_range() { 390 | let (stream, size) = send_file_with_range(&path, range)?; 391 | body = Body::wrap_stream(stream); 392 | content_length = Some(size); 393 | } 394 | res.headers_mut().typed_insert(content_range); 395 | *res.status_mut() = StatusCode::PARTIAL_CONTENT; 396 | } 397 | // Respond entire entity if Range header contains 398 | // unsatisfiable range. 399 | _ => (), 400 | } 401 | } 402 | 403 | if res.status() != StatusCode::PARTIAL_CONTENT { 404 | let (stream, size) = send_file(&path)?; 405 | body = Body::wrap_stream(stream); 406 | content_length = Some(size); 407 | } 408 | res.headers_mut().typed_insert(last_modified); 409 | res.headers_mut().typed_insert(etag); 410 | } 411 | Action::DownloadZip => { 412 | let (stream, size) = send_dir_as_zip(&path, self.args.all, self.args.ignore)?; 413 | body = Body::wrap_stream(stream); 414 | content_length = Some(size); 415 | 416 | // Changing the filename 417 | res.headers_mut().insert( 418 | CONTENT_DISPOSITION, 419 | HeaderValue::from_str(&format!( 420 | "attachment; filename=\"{}.zip\"", 421 | path.file_name().unwrap().to_str().unwrap() 422 | )) 423 | .unwrap(), 424 | ); 425 | } 426 | } 427 | 428 | let accept_encoding = req.headers().get(hyper::header::ACCEPT_ENCODING); 429 | let mime_type = InnerService::guess_path_mime(&path, action); 430 | if let Some(content_encoding) = 431 | self.get_content_encoding(accept_encoding, res.status(), &mime_type) 432 | { 433 | body = compress_stream( 434 | body.map_err(|e| io::Error::new(io::ErrorKind::Other, e)), 435 | content_encoding.as_ref(), 436 | )?; 437 | content_length = None; 438 | res.headers_mut().insert( 439 | hyper::header::CONTENT_ENCODING, 440 | hyper::header::HeaderValue::from_static(content_encoding), 441 | ); 442 | // Representation varies, so responds with a `Vary` header. 443 | res.headers_mut().insert( 444 | hyper::header::VARY, 445 | hyper::header::HeaderValue::from_name(hyper::header::ACCEPT_ENCODING), 446 | ); 447 | } 448 | 449 | // Common headers 450 | res.headers_mut().typed_insert(AcceptRanges::bytes()); 451 | res.headers_mut().typed_insert(ContentType::from(mime_type)); 452 | 453 | // Set Content-Length only when body is not compressed, 454 | // otherwise the client will get confused 455 | // e.g. curl: (18) transfer closed with N bytes remaining to read 456 | if let Some(content_length) = content_length { 457 | res.headers_mut() 458 | .typed_insert(ContentLength(content_length)); 459 | } 460 | 461 | *res.body_mut() = body; 462 | Ok(res) 463 | } 464 | 465 | fn guess_path_mime>(path: P, action: Action) -> mime::Mime { 466 | let path = path.as_ref(); 467 | path.mime() 468 | .map(|x| match x.get_param(mime::CHARSET) { 469 | Some(_) => x, 470 | None => x 471 | .guess_charset() 472 | .and_then(|c| format!("{}; charset={}", x, c).parse().ok()) 473 | .unwrap_or(x), 474 | }) 475 | .unwrap_or_else(|| match action { 476 | Action::ListDir => mime::TEXT_HTML_UTF_8, 477 | Action::DownloadFile => mime::TEXT_PLAIN_UTF_8, 478 | Action::DownloadZip => mime::APPLICATION_OCTET_STREAM, 479 | }) 480 | } 481 | } 482 | 483 | #[cfg(test)] 484 | mod t_server { 485 | use super::*; 486 | use crate::test_utils::{get_tests_dir, with_current_dir}; 487 | use std::fs::File; 488 | use tempfile::Builder; 489 | 490 | fn bootstrap(args: Args) -> (InnerService, Response) { 491 | (InnerService::new(args), Response::default()) 492 | } 493 | 494 | const fn temp_name() -> &'static str { 495 | concat!(env!("CARGO_PKG_NAME"), "-", env!("CARGO_PKG_VERSION")) 496 | } 497 | 498 | #[test] 499 | fn file_path_from_path() { 500 | let args = Args { 501 | render_index: false, 502 | path: Path::new("/storage").to_owned(), 503 | ..Default::default() 504 | }; 505 | let (service, _) = bootstrap(args); 506 | let path = "/%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C"; 507 | assert_eq!( 508 | service.file_path_from_path(path).unwrap(), 509 | Some(PathBuf::from("/storage/你好世界")) 510 | ); 511 | 512 | // Return index.html if `--render-index` flag is on. 513 | let dir = Builder::new().prefix(temp_name()).tempdir().unwrap(); 514 | let args = Args { 515 | path: dir.path().to_owned(), 516 | ..Default::default() 517 | }; 518 | let (service, _) = bootstrap(args); 519 | assert_eq!( 520 | service.file_path_from_path(".").unwrap(), 521 | Some(dir.path().join("index.html")), 522 | ); 523 | } 524 | 525 | #[test] 526 | fn guess_path_mime() { 527 | let mime_type = 528 | InnerService::guess_path_mime("file-wthout-extension", Action::DownloadFile); 529 | assert_eq!(mime_type, mime::TEXT_PLAIN_UTF_8); 530 | 531 | let mime_type = InnerService::guess_path_mime("file.json", Action::DownloadFile); 532 | let json_utf8 = "application/json; charset=utf-8" 533 | .parse::() 534 | .unwrap(); 535 | assert_eq!(mime_type, json_utf8); 536 | assert_eq!(mime_type.get_param(mime::CHARSET), Some(mime::UTF_8)); 537 | 538 | let mime_type = InnerService::guess_path_mime("lib.wasm", Action::DownloadFile); 539 | let wasm = "application/wasm".parse::().unwrap(); 540 | assert_eq!(mime_type, wasm); 541 | assert_eq!(mime_type.get_param(mime::CHARSET), None); 542 | 543 | let dir_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 544 | let mime_type = InnerService::guess_path_mime(dir_path, Action::ListDir); 545 | assert_eq!(mime_type, mime::TEXT_HTML_UTF_8); 546 | 547 | let dir_path = PathBuf::from("./tests"); 548 | let mime_type = InnerService::guess_path_mime(dir_path, Action::DownloadZip); 549 | assert_eq!(mime_type, mime::APPLICATION_OCTET_STREAM); 550 | } 551 | 552 | #[test] 553 | fn enable_cors() { 554 | let args = Args::default(); 555 | let (service, mut res) = bootstrap(args); 556 | service.enable_cors(&mut res); 557 | assert_eq!( 558 | res.headers() 559 | .typed_get::() 560 | .unwrap(), 561 | AccessControlAllowOrigin::ANY, 562 | ); 563 | assert_eq!( 564 | res.headers() 565 | .typed_get::() 566 | .unwrap(), 567 | vec![ 568 | hyper::header::RANGE, 569 | hyper::header::CONTENT_TYPE, 570 | hyper::header::ACCEPT, 571 | hyper::header::ORIGIN, 572 | ] 573 | .into_iter() 574 | .collect::(), 575 | ); 576 | } 577 | 578 | #[test] 579 | fn enable_coi() { 580 | let args = Args::default(); 581 | let (service, mut res) = bootstrap(args); 582 | service.enable_coi(&mut res); 583 | assert_eq!( 584 | res.headers().get(CROSS_ORIGIN_OPENER_POLICY).unwrap(), 585 | "same-origin", 586 | ); 587 | assert_eq!( 588 | res.headers().get(CROSS_ORIGIN_EMBEDDER_POLICY).unwrap(), 589 | "require-corp", 590 | ); 591 | } 592 | 593 | #[test] 594 | fn disable_cors() { 595 | let args = Args { 596 | cors: false, 597 | ..Default::default() 598 | }; 599 | let (service, mut res) = bootstrap(args); 600 | service.enable_cors(&mut res); 601 | assert!(res 602 | .headers() 603 | .typed_get::() 604 | .is_none()); 605 | } 606 | 607 | #[test] 608 | fn enable_cache_control() { 609 | let args = Args::default(); 610 | let (service, mut res) = bootstrap(args); 611 | service.enable_cache_control(&mut res); 612 | assert_eq!( 613 | res.headers().typed_get::().unwrap(), 614 | CacheControl::new() 615 | .with_public() 616 | .with_max_age(Duration::default()), 617 | ); 618 | 619 | let cache = 3600; 620 | let args = Args { 621 | cache, 622 | ..Default::default() 623 | }; 624 | let (service, mut res) = bootstrap(args); 625 | service.enable_cache_control(&mut res); 626 | assert_eq!( 627 | res.headers().typed_get::().unwrap(), 628 | CacheControl::new() 629 | .with_public() 630 | .with_max_age(Duration::from_secs(3600)), 631 | ); 632 | } 633 | 634 | #[test] 635 | fn can_compress() { 636 | let args = Args::default(); 637 | let (service, _) = bootstrap(args); 638 | assert!(service.can_compress(StatusCode::OK, &mime::TEXT_PLAIN)); 639 | } 640 | 641 | #[test] 642 | fn cannot_compress() { 643 | let args = Args { 644 | compress: false, 645 | ..Default::default() 646 | }; 647 | let (service, _) = bootstrap(args); 648 | assert!(!service.can_compress(StatusCode::OK, &mime::STAR_STAR)); 649 | assert!(!service.can_compress(StatusCode::OK, &mime::TEXT_PLAIN)); 650 | assert!(!service.can_compress(StatusCode::OK, &mime::IMAGE_JPEG)); 651 | 652 | let args = Args::default(); 653 | let (service, _) = bootstrap(args); 654 | assert!(!service.can_compress(StatusCode::PARTIAL_CONTENT, &mime::STAR_STAR)); 655 | assert!(!service.can_compress(StatusCode::PARTIAL_CONTENT, &mime::TEXT_PLAIN)); 656 | assert!(!service.can_compress(StatusCode::PARTIAL_CONTENT, &mime::IMAGE_JPEG)); 657 | assert!(!service.can_compress(StatusCode::OK, &"video/*".parse::().unwrap())); 658 | assert!(!service.can_compress(StatusCode::OK, &"audio/*".parse::().unwrap())); 659 | } 660 | 661 | #[test] 662 | fn path_exists() { 663 | with_current_dir(get_tests_dir(), || { 664 | let args = Args::default(); 665 | let (service, _) = bootstrap(args); 666 | // Exists but not hidden nor ignored 667 | assert!(service.path_exists("file.txt")); 668 | }); 669 | } 670 | 671 | #[test] 672 | fn path_does_not_exists() { 673 | with_current_dir(get_tests_dir(), || { 674 | let args = Args { 675 | all: false, 676 | ..Default::default() 677 | }; 678 | let (service, _) = bootstrap(args); 679 | 680 | // Not exists 681 | let path = "NOT_EXISTS_README.md"; 682 | assert!(!PathBuf::from(path).exists()); 683 | assert!(!service.path_exists(path)); 684 | 685 | // Exists but hidden 686 | let path = ".hidden.html"; 687 | assert!(PathBuf::from(path).exists()); 688 | assert!(service.path_is_hidden(path)); 689 | assert!(!service.path_exists(path)); 690 | 691 | // Exists but the file's parent is hidden 692 | let path = ".hidden/nested.html"; 693 | assert!(PathBuf::from(path).exists()); 694 | assert!(service.path_is_hidden(path)); 695 | assert!(!service.path_exists(path)); 696 | 697 | // Exists and not hidden but ignored 698 | let path = "ignore_pattern"; 699 | assert!(PathBuf::from(path).exists()); 700 | assert!(!service.path_is_hidden(path)); 701 | assert!(!service.path_exists(path)); 702 | }); 703 | } 704 | 705 | #[test] 706 | fn path_is_hidden() { 707 | // A file prefixed with `.` is considered as hidden. 708 | let args = Args { 709 | all: false, 710 | ..Default::default() 711 | }; 712 | let (service, _) = bootstrap(args); 713 | assert!(service.path_is_hidden(".a-hidden-file")); 714 | } 715 | 716 | #[test] 717 | fn path_is_not_hidden() { 718 | // `--all` flag is on 719 | let args = Args::default(); 720 | let (service, _) = bootstrap(args); 721 | assert!(!service.path_is_hidden(".a-hidden-file")); 722 | assert!(!service.path_is_hidden("a-public-file")); 723 | 724 | // `--all` flag is off and the file is not prefixed with `.` 725 | let args = Args { 726 | all: false, 727 | ..Default::default() 728 | }; 729 | let (service, _) = bootstrap(args); 730 | assert!(!service.path_is_hidden("a-public-file")); 731 | } 732 | 733 | #[test] 734 | fn path_is_ignored() { 735 | with_current_dir(get_tests_dir(), || { 736 | let args = Args::default(); 737 | let (service, _) = bootstrap(args); 738 | assert!(service.path_is_ignored("ignore_pattern")); 739 | assert!(service.path_is_ignored("dir/ignore_pattern")); 740 | }); 741 | } 742 | 743 | #[test] 744 | fn path_is_not_ignored() { 745 | with_current_dir(get_tests_dir(), || { 746 | // `--no-ignore` flag is on 747 | let args = Args { 748 | ignore: false, 749 | ..Default::default() 750 | }; 751 | let (service, _) = bootstrap(args); 752 | assert!(!service.path_is_ignored("ignore_pattern")); 753 | assert!(!service.path_is_ignored("dir/ignore_pattern")); 754 | 755 | // file.txt and .hidden.html is not ignored. 756 | let args = Args::default(); 757 | let (service, _) = bootstrap(args); 758 | assert!(!service.path_is_ignored("file.txt")); 759 | assert!(!service.path_is_ignored(".hidden.html")); 760 | }); 761 | } 762 | 763 | #[test] 764 | fn path_is_under_basepath() { 765 | #[cfg(unix)] 766 | use std::os::unix::fs::symlink as symlink_file; 767 | #[cfg(windows)] 768 | use std::os::windows::fs::symlink_file; 769 | 770 | let src_dir = Builder::new().prefix(temp_name()).tempdir().unwrap(); 771 | let src_dir = src_dir.path().canonicalize().unwrap(); 772 | let src_path = src_dir.join("src_file.txt"); 773 | let _ = File::create(&src_path); 774 | 775 | // Is under service's base path 776 | let symlink_path = src_dir.join("symlink"); 777 | let args = Args { 778 | path: src_dir, 779 | ..Default::default() 780 | }; 781 | let (service, _) = bootstrap(args); 782 | symlink_file(&src_path, &symlink_path).unwrap(); 783 | assert!(service.path_is_under_basepath(&symlink_path)); 784 | 785 | // Not under base path. 786 | let args = Args::default(); 787 | let (service, _) = bootstrap(args); 788 | assert!(!service.path_is_under_basepath(&symlink_path)); 789 | } 790 | 791 | #[test] 792 | fn strips_path_prefix() { 793 | let args = Args { 794 | path_prefix: Some("/foo".into()), 795 | ..Default::default() 796 | }; 797 | let (service, _) = bootstrap(args); 798 | 799 | assert_eq!( 800 | service.strip_path_prefix(&Path::new("foo/dir/to/bar.txt")), 801 | Some(Path::new("dir/to/bar.txt")) 802 | ); 803 | 804 | assert_eq!( 805 | service.strip_path_prefix(&Path::new("dir/to/bar.txt")), 806 | None 807 | ); 808 | 809 | let args = Args::default(); 810 | let (service, _) = bootstrap(args); 811 | 812 | assert_eq!( 813 | service.strip_path_prefix(&Path::new("foo/dir/to/bar.txt")), 814 | Some(Path::new("foo/dir/to/bar.txt")) 815 | ); 816 | } 817 | 818 | #[ignore] 819 | #[test] 820 | fn handle_request() {} 821 | 822 | #[test] 823 | fn get_gzip_content_encoding() { 824 | let args = Args::default(); 825 | let (service, _) = bootstrap(args); 826 | 827 | let accept_encoding = &HeaderValue::from_str("gzip").unwrap(); 828 | let accept_encoding = Some(accept_encoding); 829 | 830 | let status = StatusCode::OK; 831 | let mime_type = &mime::TEXT_PLAIN; 832 | let content_encoding = service.get_content_encoding(accept_encoding, status, mime_type); 833 | assert_eq!(Some("gzip"), content_encoding); 834 | } 835 | 836 | #[test] 837 | fn get_identity_content_encoding() { 838 | let args = Args::default(); 839 | let (service, _) = bootstrap(args); 840 | 841 | let accept_encoding = None; 842 | 843 | let status = StatusCode::OK; 844 | let mime_type = &mime::TEXT_PLAIN; 845 | let content_encoding = service.get_content_encoding(accept_encoding, status, mime_type); 846 | assert_eq!(None, content_encoding); 847 | } 848 | } 849 | -------------------------------------------------------------------------------- /src/server/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: -apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif; 3 | line-height: 1.5; 4 | color: #24292e; 5 | } 6 | 7 | .breadcrumbs { 8 | font-size: 1.25em; 9 | padding: 2.5em; 10 | padding-bottom: 0; 11 | } 12 | 13 | .breadcrumbs > a { 14 | color: #0366d6; 15 | text-decoration: none; 16 | } 17 | 18 | .breadcrumbs > a:hover { 19 | text-decoration: underline; 20 | } 21 | 22 | /* final breadcrumb */ 23 | .breadcrumbs > b { 24 | color: #24292e; 25 | } 26 | 27 | .breadcrumbs > .separator { 28 | color: #586069; 29 | } 30 | 31 | ul { 32 | font-size: 16; 33 | padding: 0 2.5em; 34 | max-width: 1000px; 35 | display: flex; 36 | flex-wrap: wrap; 37 | } 38 | 39 | li { 40 | display: flex; 41 | list-style: none; 42 | width: 200px; 43 | padding: 1em; 44 | } 45 | 46 | li div { 47 | width: 1.5em; 48 | } 49 | 50 | li svg, .breadcrumbs svg { 51 | height: 100%; 52 | fill: rgba(3,47,98,0.5); 53 | padding-right: 0.5em; 54 | } 55 | 56 | .breadcrumbs svg { 57 | padding-left: 0.5em; 58 | } 59 | 60 | li a { 61 | color: #0366d6; 62 | text-overflow: ellipsis; 63 | white-space: nowrap; 64 | overflow: hidden; 65 | display: block; 66 | text-decoration: none; 67 | } 68 | 69 | li a:hover { 70 | text-decoration: underline; 71 | } 72 | -------------------------------------------------------------------------------- /src/test_utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Weihang Lo 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use std::convert::AsRef; 10 | use std::env; 11 | use std::ops::FnOnce; 12 | use std::panic; 13 | use std::path::{Path, PathBuf}; 14 | use std::sync::Mutex; 15 | 16 | use once_cell::sync::Lazy; 17 | 18 | static LOCK: Lazy> = Lazy::new(|| Mutex::new(())); 19 | 20 | static TESTS_DIR: Lazy = Lazy::new(|| { 21 | let path: &Path = env!("CARGO_MANIFEST_DIR").as_ref(); 22 | path.join("tests") 23 | }); 24 | 25 | pub fn get_tests_dir() -> impl AsRef { 26 | TESTS_DIR.as_path() 27 | } 28 | 29 | /// Ensure only one thread can access `std::env::set_current_dir` at the same 30 | /// time. Also reset current working directory after dropping. 31 | pub fn with_current_dir(current_dir: P, f: F) 32 | where 33 | P: AsRef, 34 | F: FnOnce() + panic::UnwindSafe, 35 | { 36 | let _lock = LOCK.lock().unwrap(); 37 | 38 | let old_cwd = env::current_dir().expect("store current working directory"); 39 | env::set_current_dir(current_dir).expect("set current working directory"); 40 | 41 | let result = panic::catch_unwind(f); 42 | 43 | env::set_current_dir(old_cwd).expect("restore current working directory"); 44 | 45 | if let Err(e) = result { 46 | panic::resume_unwind(e) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | ignore_pattern 2 | -------------------------------------------------------------------------------- /tests/.hidden.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/sfz/8b04beff55fd1b59a00a0c02c50e35be16e1db15/tests/.hidden.html -------------------------------------------------------------------------------- /tests/.hidden/nested.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/sfz/8b04beff55fd1b59a00a0c02c50e35be16e1db15/tests/.hidden/nested.html -------------------------------------------------------------------------------- /tests/dir/ignore_pattern: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/sfz/8b04beff55fd1b59a00a0c02c50e35be16e1db15/tests/dir/ignore_pattern -------------------------------------------------------------------------------- /tests/dir_with_sub_dirs/file.txt: -------------------------------------------------------------------------------- 1 | Test1 2 | -------------------------------------------------------------------------------- /tests/dir_with_sub_dirs/sub_dir/file.txt: -------------------------------------------------------------------------------- 1 | Test2 2 | -------------------------------------------------------------------------------- /tests/file.txt: -------------------------------------------------------------------------------- 1 | 01234567 -------------------------------------------------------------------------------- /tests/ignore_pattern: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/sfz/8b04beff55fd1b59a00a0c02c50e35be16e1db15/tests/ignore_pattern -------------------------------------------------------------------------------- /tests/symlink_dir: -------------------------------------------------------------------------------- 1 | dir -------------------------------------------------------------------------------- /tests/symlink_file.txt: -------------------------------------------------------------------------------- 1 | file.txt --------------------------------------------------------------------------------
(current_dir: P, f: F) 32 | where 33 | P: AsRef, 34 | F: FnOnce() + panic::UnwindSafe, 35 | { 36 | let _lock = LOCK.lock().unwrap(); 37 | 38 | let old_cwd = env::current_dir().expect("store current working directory"); 39 | env::set_current_dir(current_dir).expect("set current working directory"); 40 | 41 | let result = panic::catch_unwind(f); 42 | 43 | env::set_current_dir(old_cwd).expect("restore current working directory"); 44 | 45 | if let Err(e) = result { 46 | panic::resume_unwind(e) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | ignore_pattern 2 | -------------------------------------------------------------------------------- /tests/.hidden.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/sfz/8b04beff55fd1b59a00a0c02c50e35be16e1db15/tests/.hidden.html -------------------------------------------------------------------------------- /tests/.hidden/nested.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/sfz/8b04beff55fd1b59a00a0c02c50e35be16e1db15/tests/.hidden/nested.html -------------------------------------------------------------------------------- /tests/dir/ignore_pattern: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/sfz/8b04beff55fd1b59a00a0c02c50e35be16e1db15/tests/dir/ignore_pattern -------------------------------------------------------------------------------- /tests/dir_with_sub_dirs/file.txt: -------------------------------------------------------------------------------- 1 | Test1 2 | -------------------------------------------------------------------------------- /tests/dir_with_sub_dirs/sub_dir/file.txt: -------------------------------------------------------------------------------- 1 | Test2 2 | -------------------------------------------------------------------------------- /tests/file.txt: -------------------------------------------------------------------------------- 1 | 01234567 -------------------------------------------------------------------------------- /tests/ignore_pattern: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weihanglo/sfz/8b04beff55fd1b59a00a0c02c50e35be16e1db15/tests/ignore_pattern -------------------------------------------------------------------------------- /tests/symlink_dir: -------------------------------------------------------------------------------- 1 | dir -------------------------------------------------------------------------------- /tests/symlink_file.txt: -------------------------------------------------------------------------------- 1 | file.txt --------------------------------------------------------------------------------