actix-files-0.6.10/.cargo_vcs_info.json0000644000000001511046102023000133470ustar { "git": { "sha1": "bc27fd2724fb507292f38de556ce38e94efb10b1" }, "path_in_vcs": "actix-files" }actix-files-0.6.10/CHANGES.md000064400000000000000000000166311046102023000135210ustar 00000000000000# Changes ## Unreleased ## 0.6.10 ### Security Notice We addressed 2 vulnerabilities in this release: - Do not panic with empty Range header. - Avoid serving CWD on invalid `Files::new` inputs. We encourage updating your `actix-files` version as soon as possible. ### Other changes - Minimum supported Rust version (MSRV) is now 1.88. - `PathBufWrap` & `UriSegmentError` made public. [#3694] [#3694]: https://github.com/actix/actix-web/pull/3694 ## 0.6.9 - Correct `derive_more` dependency feature requirements. ## 0.6.8 - Add `Files::with_permanent_redirect()` method. - Change default redirect status code to 307 Temporary Redirect. ## 0.6.7 - Add `{Files, NamedFile}::read_mode_threshold()` methods to allow faster synchronous reads of small files. - Minimum supported Rust version (MSRV) is now 1.75. ## 0.6.6 - Update `tokio-uring` dependency to `0.4`. - Minimum supported Rust version (MSRV) is now 1.72. ## 0.6.5 - Fix handling of special characters in filenames. ## 0.6.4 - Fix handling of newlines in filenames. - Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency. ## 0.6.3 - XHTML files now use `Content-Disposition: inline` instead of `attachment`. [#2903] - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. - Update `tokio-uring` dependency to `0.4`. [#2903]: https://github.com/actix/actix-web/pull/2903 ## 0.6.2 - Allow partial range responses for video content to start streaming sooner. [#2817] - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. [#2817]: https://github.com/actix/actix-web/pull/2817 ## 0.6.1 - Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021] - Update `tokio-uring` dependency to `0.3`. - Audio files now use `Content-Disposition: inline` instead of `attachment`. [#2645] - Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. [#2021]: https://github.com/actix/actix-web/pull/2021 [#2645]: https://github.com/actix/actix-web/pull/2645 ## 0.6.0 - No significant changes since `0.6.0-beta.16`. ## 0.6.0-beta.16 - No significant changes since `0.6.0-beta.15`. ## 0.6.0-beta.15 - No significant changes since `0.6.0-beta.14`. ## 0.6.0-beta.14 - The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583] [#2583]: https://github.com/actix/actix-web/pull/2583 ## 0.6.0-beta.13 - The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398] - The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398] - Minimum supported Rust version (MSRV) is now 1.54. [#2398]: https://github.com/actix/actix-web/pull/2398 ## 0.6.0-beta.12 - No significant changes since `0.6.0-beta.11`. ## 0.6.0-beta.11 - No significant changes since `0.6.0-beta.10`. ## 0.6.0-beta.10 - No significant changes since `0.6.0-beta.9`. ## 0.6.0-beta.9 - Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408] - Add `NamedFile::open_async`. [#2408] - Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453] - The `Responder` impl for `NamedFile` now has a boxed future associated type. [#2408] - The `Service` impl for `NamedFileService` now has a boxed future associated type. [#2408] - Add `impl Clone` for `FilesService`. [#2408] [#2408]: https://github.com/actix/actix-web/pull/2408 [#2453]: https://github.com/actix/actix-web/pull/2453 ## 0.6.0-beta.8 - Minimum supported Rust version (MSRV) is now 1.52. ## 0.6.0-beta.7 - Minimum supported Rust version (MSRV) is now 1.51. ## 0.6.0-beta.6 - Added `Files::path_filter()`. [#2274] - `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228] [#2274]: https://github.com/actix/actix-web/pull/2274 [#2228]: https://github.com/actix/actix-web/pull/2228 ## 0.6.0-beta.5 - `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135] - For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156] - `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225] - `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257] [#2135]: https://github.com/actix/actix-web/pull/2135 [#2156]: https://github.com/actix/actix-web/pull/2156 [#2225]: https://github.com/actix/actix-web/pull/2225 [#2257]: https://github.com/actix/actix-web/pull/2257 ## 0.6.0-beta.4 - Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046] [#2046]: https://github.com/actix/actix-web/pull/2046 ## 0.6.0-beta.3 - No notable changes. ## 0.6.0-beta.2 - Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887] - Replace `v_htmlescape` with `askama_escape`. [#1953] [#1887]: https://github.com/actix/actix-web/pull/1887 [#1953]: https://github.com/actix/actix-web/pull/1953 ## 0.6.0-beta.1 - `HttpRange::parse` now has its own error type. - Update `bytes` to `1.0`. [#1813] [#1813]: https://github.com/actix/actix-web/pull/1813 ## 0.5.0 - Optionally support hidden files/directories. [#1811] [#1811]: https://github.com/actix/actix-web/pull/1811 ## 0.4.1 - Clarify order of parameters in `Files::new` and improve docs. ## 0.4.0 - Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714] [#1714]: https://github.com/actix/actix-web/pull/1714 ## 0.3.0 - No significant changes from 0.3.0-beta.1. ## 0.3.0-beta.1 - Update `v_htmlescape` to 0.10 - Update `actix-web` and `actix-http` dependencies to beta.1 ## 0.3.0-alpha.1 - Update `actix-web` and `actix-http` dependencies to alpha - Fix some typos in the docs - Bump minimum supported Rust version to 1.40 - Support sending Content-Length when Content-Range is specified [#1384] [#1384]: https://github.com/actix/actix-web/pull/1384 ## 0.2.1 - Use the same format for file URLs regardless of platforms ## 0.2.0 - Fix BodyEncoding trait import #1220 ## 0.2.0-alpha.1 - Migrate to `std::future` ## 0.1.7 - Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151) ## 0.1.6 - Add option to redirect to a slash-ended path `Files` #1132 ## 0.1.5 - Bump up `mime_guess` crate version to 2.0.1 - Bump up `percent-encoding` crate version to 2.1 - Allow user defined request guards for `Files` #1113 ## 0.1.4 - Allow to disable `Content-Disposition` header #686 ## 0.1.3 - Do not set `Content-Length` header, let actix-http set it #930 ## 0.1.2 - Content-Length is 0 for NamedFile HEAD request #914 - Fix ring dependency from actix-web default features for #741 ## 0.1.1 - Static files are incorrectly served as both chunked and with length #812 ## 0.1.0 - NamedFile last-modified check always fails due to nano-seconds in file modified date #820 ## 0.1.0-beta.4 - Update actix-web to beta.4 ## 0.1.0-beta.1 - Update actix-web to beta.1 ## 0.1.0-alpha.6 - Update actix-web to alpha6 ## 0.1.0-alpha.4 - Update actix-web to alpha4 ## 0.1.0-alpha.2 - Add default handler support ## 0.1.0-alpha.1 - Initial impl actix-files-0.6.10/Cargo.lock0000644000001430101046102023000113240ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "actix-codec" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ "bitflags 2.10.0", "bytes", "futures-core", "futures-sink", "memchr", "pin-project-lite", "tokio", "tokio-util", "tracing", ] [[package]] name = "actix-files" version = "0.6.10" dependencies = [ "actix-http", "actix-rt", "actix-server", "actix-service", "actix-test", "actix-utils", "actix-web", "bitflags 2.10.0", "bytes", "derive_more", "env_logger", "futures-core", "http-range", "log", "mime", "mime_guess", "percent-encoding", "pin-project-lite", "tempfile", "tokio-uring", "v_htmlescape", ] [[package]] name = "actix-http" version = "3.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49" dependencies = [ "actix-codec", "actix-rt", "actix-service", "actix-utils", "base64", "bitflags 2.10.0", "brotli", "bytes", "bytestring", "derive_more", "encoding_rs", "flate2", "foldhash", "futures-core", "h2", "http 0.2.12", "httparse", "httpdate", "itoa", "language-tags", "local-channel", "mime", "percent-encoding", "pin-project-lite", "rand", "sha1", "smallvec", "tokio", "tokio-util", "tracing", "zstd", ] [[package]] name = "actix-http-test" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "061d27c2a6fea968fdaca0961ff429d23a4ec878c4f68f5d08626663ade69c80" dependencies = [ "actix-codec", "actix-rt", "actix-server", "actix-service", "actix-tls", "actix-utils", "awc", "bytes", "futures-core", "http 0.2.12", "log", "serde", "serde_json", "serde_urlencoded", "slab", "socket2 0.5.10", "tokio", ] [[package]] name = "actix-macros" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", "syn", ] [[package]] name = "actix-router" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" dependencies = [ "bytestring", "cfg-if", "http 0.2.12", "regex", "regex-lite", "serde", "tracing", ] [[package]] name = "actix-rt" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" dependencies = [ "actix-macros", "futures-core", "tokio", "tokio-uring", ] [[package]] name = "actix-server" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" dependencies = [ "actix-rt", "actix-service", "actix-utils", "futures-core", "futures-util", "mio", "socket2 0.5.10", "tokio", "tokio-uring", "tracing", ] [[package]] name = "actix-service" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" dependencies = [ "futures-core", "pin-project-lite", ] [[package]] name = "actix-test" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439022b5a7b5dac10798465029a9566e8e0cca7a6014541ed277b695691fac5f" dependencies = [ "actix-codec", "actix-http", "actix-http-test", "actix-rt", "actix-service", "actix-utils", "actix-web", "awc", "futures-core", "futures-util", "log", "serde", "serde_json", "serde_urlencoded", "tokio", ] [[package]] name = "actix-tls" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6176099de3f58fbddac916a7f8c6db297e021d706e7a6b99947785fee14abe9f" dependencies = [ "actix-rt", "actix-service", "actix-utils", "futures-core", "http 0.2.12", "http 1.4.0", "impl-more", "pin-project-lite", "tokio", "tokio-util", "tracing", ] [[package]] name = "actix-utils" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" dependencies = [ "local-waker", "pin-project-lite", ] [[package]] name = "actix-web" version = "4.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1654a77ba142e37f049637a3e5685f864514af11fcbc51cb51eb6596afe5b8d6" dependencies = [ "actix-codec", "actix-http", "actix-macros", "actix-router", "actix-rt", "actix-server", "actix-service", "actix-utils", "actix-web-codegen", "bytes", "bytestring", "cfg-if", "cookie", "derive_more", "encoding_rs", "foldhash", "futures-core", "futures-util", "impl-more", "itoa", "language-tags", "log", "mime", "once_cell", "pin-project-lite", "regex", "regex-lite", "serde", "serde_json", "serde_urlencoded", "smallvec", "socket2 0.6.2", "time", "tracing", "url", ] [[package]] name = "actix-web-codegen" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" dependencies = [ "actix-router", "proc-macro2", "quote", "syn", ] [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "alloc-no-stdlib" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" dependencies = [ "alloc-no-stdlib", ] [[package]] name = "anstream" version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys 0.61.2", ] [[package]] name = "awc" version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c170039c11c7f6c0a28f7b3bd4fb0c674cbfa317fabc1560022ad3ec2d69e7c" dependencies = [ "actix-codec", "actix-http", "actix-rt", "actix-service", "actix-tls", "actix-utils", "base64", "bytes", "cfg-if", "cookie", "derive_more", "futures-core", "futures-util", "h2", "http 0.2.12", "itoa", "log", "mime", "percent-encoding", "pin-project-lite", "rand", "serde", "serde_json", "serde_urlencoded", "tokio", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "brotli" version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", "brotli-decompressor", ] [[package]] name = "brotli-decompressor" version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytestring" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" dependencies = [ "bytes", ] [[package]] name = "cc" version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "jobserver", "libc", "shlex", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "convert_case" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ "unicode-segmentation", ] [[package]] name = "cookie" version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ "percent-encoding", "time", "version_check", ] [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", ] [[package]] name = "deranged" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] [[package]] name = "derive_more" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", "syn", "unicode-xid", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "env_filter" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", "jiff", "log", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.61.2", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-sink", "futures-task", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", ] [[package]] name = "h2" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", "http 0.2.12", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "http" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", "itoa", ] [[package]] name = "http-range" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "icu_collections" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_normalizer" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", "writeable", "yoke", "zerofrom", "zerotrie", "zerovec", ] [[package]] name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "impl-more" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "indexmap" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "io-uring" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "595a0399f411a508feb2ec1e970a4a30c249351e30208960d58298de8660b0e5" dependencies = [ "bitflags 1.3.2", "libc", ] [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", "serde_core", ] [[package]] name = "jiff-static" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "jobserver" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom", "libc", ] [[package]] name = "language-tags" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "libc" version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "local-channel" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" dependencies = [ "futures-core", "futures-sink", "local-waker", ] [[package]] name = "local-waker" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", ] [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", ] [[package]] name = "mio" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", "wasi", "windows-sys 0.61.2", ] [[package]] name = "num-conv" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-link", ] [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags 2.10.0", ] [[package]] name = "regex" version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-lite" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.2", ] [[package]] name = "ryu" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", "serde", "serde_core", "zmij", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ "errno", "libc", ] [[package]] name = "simd-adler32" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", ] [[package]] name = "socket2" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "socket2" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", ] [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tempfile" version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom", "once_cell", "rustix", "windows-sys 0.61.2", ] [[package]] name = "time" version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", ] [[package]] name = "tinystr" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tokio" version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2 0.6.2", "windows-sys 0.61.2", ] [[package]] name = "tokio-uring" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "748482e3e13584a34664a710168ad5068e8cb1d968aa4ffa887e83ca6dd27967" dependencies = [ "bytes", "futures-util", "io-uring", "libc", "slab", "socket2 0.4.10", "tokio", ] [[package]] name = "tokio-util" version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "tracing" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicase" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "url" version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", ] [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "v_htmlescape" version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets 0.53.5", ] [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", "windows_i686_gnullvm 0.53.1", "windows_i686_msvc 0.53.1", "windows_x86_64_gnu 0.53.1", "windows_x86_64_gnullvm 0.53.1", "windows_x86_64_msvc 0.53.1", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerocopy" version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerotrie" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", "zerofrom", ] [[package]] name = "zerovec" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zmij" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" [[package]] name = "zstd" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", ] actix-files-0.6.10/Cargo.toml0000644000000057431046102023000113610ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "actix-files" version = "0.6.10" authors = [ "Nikolay Kim ", "Rob Ede ", ] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Static file serving for Actix Web" homepage = "https://actix.rs" readme = "README.md" keywords = [ "actix", "http", "async", "futures", ] categories = [ "asynchronous", "web-programming::http-server", ] license = "MIT OR Apache-2.0" repository = "https://github.com/actix/actix-web" [package.metadata.cargo_check_external_types] allowed_external_types = [ "actix_http::*", "actix_service::*", "actix_web::*", "http::*", "mime::*", ] [features] experimental-io-uring = [ "actix-web/experimental-io-uring", "tokio-uring", ] [lib] name = "actix_files" path = "src/lib.rs" [[example]] name = "guarded-listing" path = "examples/guarded-listing.rs" [[test]] name = "encoding" path = "tests/encoding.rs" [[test]] name = "guard" path = "tests/guard.rs" [[test]] name = "traversal" path = "tests/traversal.rs" [dependencies.actix-http] version = "3" [dependencies.actix-service] version = "2" [dependencies.actix-utils] version = "3" [dependencies.actix-web] version = "4" default-features = false [dependencies.bitflags] version = "2" [dependencies.bytes] version = "1" [dependencies.derive_more] version = "2" features = [ "deref", "deref_mut", "display", "error", "from", ] [dependencies.futures-core] version = "0.3.17" features = ["alloc"] default-features = false [dependencies.http-range] version = "0.1.4" [dependencies.log] version = "0.4" [dependencies.mime] version = "0.3.9" [dependencies.mime_guess] version = "2.0.1" [dependencies.percent-encoding] version = "2.1" [dependencies.pin-project-lite] version = "0.2.7" [dependencies.v_htmlescape] version = "0.15.5" [dev-dependencies.actix-rt] version = "2.7" [dev-dependencies.actix-test] version = "0.1" [dev-dependencies.actix-web] version = "4" [dev-dependencies.env_logger] version = "0.11" [dev-dependencies.tempfile] version = "3.2" [target.'cfg(target_os = "linux")'.dependencies.actix-server] version = "2.4" optional = true [target.'cfg(target_os = "linux")'.dependencies.tokio-uring] version = "0.5" features = ["bytes"] optional = true [lints.clippy] [lints.rust.future_incompatible] level = "deny" priority = 0 [lints.rust.nonstandard_style] level = "deny" priority = 0 [lints.rust.rust_2018_idioms] level = "deny" priority = 0 actix-files-0.6.10/Cargo.toml.orig000064400000000000000000000027431046102023000150150ustar 00000000000000[package] name = "actix-files" version = "0.6.10" authors = ["Nikolay Kim ", "Rob Ede "] description = "Static file serving for Actix Web" keywords = ["actix", "http", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web" categories = ["asynchronous", "web-programming::http-server"] license = "MIT OR Apache-2.0" edition = "2021" [package.metadata.cargo_check_external_types] allowed_external_types = ["actix_http::*", "actix_service::*", "actix_web::*", "http::*", "mime::*"] [features] experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] [dependencies] actix-http = "3" actix-service = "2" actix-utils = "3" actix-web = { version = "4", default-features = false } bitflags = "2" bytes = "1" derive_more = { version = "2", features = ["deref", "deref_mut", "display", "error", "from"] } futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } http-range = "0.1.4" log = "0.4" mime = "0.3.9" mime_guess = "2.0.1" percent-encoding = "2.1" pin-project-lite = "0.2.7" v_htmlescape = "0.15.5" # experimental-io-uring [target.'cfg(target_os = "linux")'.dependencies] tokio-uring = { version = "0.5", optional = true, features = ["bytes"] } actix-server = { version = "2.4", optional = true } # ensure matching tokio-uring versions [dev-dependencies] actix-rt = "2.7" actix-test = "0.1" actix-web = "4" env_logger = "0.11" tempfile = "3.2" [lints] workspace = true actix-files-0.6.10/LICENSE-APACHE000064400000000000000000000261201046102023000140450ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2017-NOW Actix Team Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. actix-files-0.6.10/LICENSE-MIT000064400000000000000000000020421046102023000135520ustar 00000000000000Copyright (c) 2017-NOW Actix Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. actix-files-0.6.10/README.md000064400000000000000000000020711046102023000133770ustar 00000000000000# `actix-files` [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.9)](https://docs.rs/actix-files/0.6.9) ![Version](https://img.shields.io/badge/rustc-1.88+-ab6000.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
[![dependency status](https://deps.rs/crate/actix-files/0.6.9/status.svg)](https://deps.rs/crate/actix-files/0.6.9) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) Static file serving for Actix Web. Provides a non-blocking service for serving static files from disk. ## Examples ```rust use actix_web::App; use actix_files::Files; let app = App::new() .service(Files::new("/static", ".").prefer_utf8(true)); ``` actix-files-0.6.10/examples/guarded-listing.rs000064400000000000000000000016731046102023000173750ustar 00000000000000use actix_files::Files; use actix_web::{get, guard, middleware, App, HttpServer, Responder}; const EXAMPLES_DIR: &str = concat![env!("CARGO_MANIFEST_DIR"), "/examples"]; #[get("/")] async fn index() -> impl Responder { "Hello world!" } #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); log::info!("starting HTTP server at http://localhost:8080"); HttpServer::new(|| { App::new() .service(index) .service( Files::new("/assets", EXAMPLES_DIR) .show_files_listing() .guard(guard::Header("show-listing", "?1")), ) .service(Files::new("/assets", EXAMPLES_DIR)) .wrap(middleware::Compress::default()) .wrap(middleware::Logger::default()) }) .bind(("127.0.0.1", 8080))? .workers(2) .run() .await } actix-files-0.6.10/src/chunked.rs000064400000000000000000000155521046102023000147060ustar 00000000000000use std::{ cmp, fmt, future::Future, io, pin::Pin, task::{Context, Poll}, }; use actix_web::{error::Error, web::Bytes}; #[cfg(feature = "experimental-io-uring")] use bytes::BytesMut; use futures_core::{ready, Stream}; use pin_project_lite::pin_project; use super::named::File; #[derive(Debug, Clone, Copy)] pub(crate) enum ReadMode { Sync, Async, } pin_project! { /// Adapter to read a `std::file::File` in chunks. #[doc(hidden)] pub struct ChunkedReadFile { size: u64, offset: u64, #[pin] state: ChunkedReadFileState, counter: u64, callback: F, read_mode: ReadMode, } } #[cfg(not(feature = "experimental-io-uring"))] pin_project! { #[project = ChunkedReadFileStateProj] #[project_replace = ChunkedReadFileStateProjReplace] enum ChunkedReadFileState { File { file: Option, }, Future { #[pin] fut: Fut }, } } #[cfg(feature = "experimental-io-uring")] pin_project! { #[project = ChunkedReadFileStateProj] #[project_replace = ChunkedReadFileStateProjReplace] enum ChunkedReadFileState { File { file: Option<(File, BytesMut)> }, Future { #[pin] fut: Fut }, } } impl fmt::Debug for ChunkedReadFile { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("ChunkedReadFile") } } pub(crate) fn new_chunked_read( size: u64, offset: u64, file: File, read_mode_threshold: u64, ) -> impl Stream> { ChunkedReadFile { size, offset, #[cfg(not(feature = "experimental-io-uring"))] state: ChunkedReadFileState::File { file: Some(file) }, #[cfg(feature = "experimental-io-uring")] state: ChunkedReadFileState::File { file: Some((file, BytesMut::new())), }, counter: 0, callback: chunked_read_file_callback, read_mode: if size < read_mode_threshold { ReadMode::Sync } else { ReadMode::Async }, } } #[cfg(not(feature = "experimental-io-uring"))] fn chunked_read_file_callback_sync( mut file: File, offset: u64, max_bytes: usize, ) -> Result<(File, Bytes), io::Error> { use io::{Read as _, Seek as _}; let mut buf = Vec::with_capacity(max_bytes); file.seek(io::SeekFrom::Start(offset))?; let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; if n_bytes == 0 { Err(io::Error::from(io::ErrorKind::UnexpectedEof)) } else { Ok((file, Bytes::from(buf))) } } #[cfg(not(feature = "experimental-io-uring"))] #[inline] async fn chunked_read_file_callback( file: File, offset: u64, max_bytes: usize, read_mode: ReadMode, ) -> Result<(File, Bytes), Error> { let res = match read_mode { ReadMode::Sync => chunked_read_file_callback_sync(file, offset, max_bytes)?, ReadMode::Async => { actix_web::web::block(move || chunked_read_file_callback_sync(file, offset, max_bytes)) .await?? } }; Ok(res) } #[cfg(feature = "experimental-io-uring")] async fn chunked_read_file_callback( file: File, offset: u64, max_bytes: usize, mut bytes_mut: BytesMut, ) -> io::Result<(File, Bytes, BytesMut)> { bytes_mut.reserve(max_bytes); let (res, mut bytes_mut) = file.read_at(bytes_mut, offset).await; let n_bytes = res?; if n_bytes == 0 { return Err(io::ErrorKind::UnexpectedEof.into()); } let bytes = bytes_mut.split_to(n_bytes).freeze(); Ok((file, bytes, bytes_mut)) } #[cfg(feature = "experimental-io-uring")] impl Stream for ChunkedReadFile where F: Fn(File, u64, usize, BytesMut) -> Fut, Fut: Future>, { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let mut this = self.as_mut().project(); match this.state.as_mut().project() { ChunkedReadFileStateProj::File { file } => { let size = *this.size; let offset = *this.offset; let counter = *this.counter; if size == counter { Poll::Ready(None) } else { let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; let (file, bytes_mut) = file .take() .expect("ChunkedReadFile polled after completion"); let fut = (this.callback)(file, offset, max_bytes, bytes_mut); this.state .project_replace(ChunkedReadFileState::Future { fut }); self.poll_next(cx) } } ChunkedReadFileStateProj::Future { fut } => { let (file, bytes, bytes_mut) = ready!(fut.poll(cx))?; this.state.project_replace(ChunkedReadFileState::File { file: Some((file, bytes_mut)), }); *this.offset += bytes.len() as u64; *this.counter += bytes.len() as u64; Poll::Ready(Some(Ok(bytes))) } } } } #[cfg(not(feature = "experimental-io-uring"))] impl Stream for ChunkedReadFile where F: Fn(File, u64, usize, ReadMode) -> Fut, Fut: Future>, { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let mut this = self.as_mut().project(); match this.state.as_mut().project() { ChunkedReadFileStateProj::File { file } => { let size = *this.size; let offset = *this.offset; let counter = *this.counter; if size == counter { Poll::Ready(None) } else { let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; let file = file .take() .expect("ChunkedReadFile polled after completion"); let fut = (this.callback)(file, offset, max_bytes, *this.read_mode); this.state .project_replace(ChunkedReadFileState::Future { fut }); self.poll_next(cx) } } ChunkedReadFileStateProj::Future { fut } => { let (file, bytes) = ready!(fut.poll(cx))?; this.state .project_replace(ChunkedReadFileState::File { file: Some(file) }); *this.offset += bytes.len() as u64; *this.counter += bytes.len() as u64; Poll::Ready(Some(Ok(bytes))) } } } } actix-files-0.6.10/src/directory.rs000064400000000000000000000066761046102023000153000ustar 00000000000000use std::{ fmt::Write, fs::DirEntry, io, path::{Path, PathBuf}, }; use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse}; use percent_encoding::{utf8_percent_encode, CONTROLS}; use v_htmlescape::escape as escape_html_entity; /// A directory; responds with the generated directory listing. #[derive(Debug)] pub struct Directory { /// Base directory. pub base: PathBuf, /// Path of subdirectory to generate listing for. pub path: PathBuf, } impl Directory { /// Create a new directory pub fn new(base: PathBuf, path: PathBuf) -> Directory { Directory { base, path } } /// Is this entry visible from this directory? pub fn is_visible(&self, entry: &io::Result) -> bool { if let Ok(ref entry) = *entry { if let Some(name) = entry.file_name().to_str() { if name.starts_with('.') { return false; } } if let Ok(ref md) = entry.metadata() { let ft = md.file_type(); return ft.is_dir() || ft.is_file() || ft.is_symlink(); } } false } } pub(crate) type DirectoryRenderer = dyn Fn(&Directory, &HttpRequest) -> Result; /// Returns percent encoded file URL path. macro_rules! encode_file_url { ($path:ident) => { utf8_percent_encode(&$path, CONTROLS) }; } /// Returns HTML entity encoded formatter. /// /// ```plain /// " => " /// & => & /// ' => ' /// < => < /// > => > /// / => / /// ``` macro_rules! encode_file_name { ($entry:ident) => { escape_html_entity(&$entry.file_name().to_string_lossy()) }; } pub(crate) fn directory_listing( dir: &Directory, req: &HttpRequest, ) -> Result { let index_of = format!("Index of {}", req.path()); let mut body = String::new(); let base = Path::new(req.path()); for entry in dir.path.read_dir()? { if dir.is_visible(&entry) { let entry = entry.unwrap(); let p = match entry.path().strip_prefix(&dir.path) { Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace('\\', "/"), Ok(p) => base.join(p).to_string_lossy().into_owned(), Err(_) => continue, }; // if file is a directory, add '/' to the end of the name if let Ok(metadata) = entry.metadata() { if metadata.is_dir() { let _ = write!( body, "
  • {}/
  • ", encode_file_url!(p), encode_file_name!(entry), ); } else { let _ = write!( body, "
  • {}
  • ", encode_file_url!(p), encode_file_name!(entry), ); } } else { continue; } } } let html = format!( "\ {}\

    {}

    \
      \ {}\
    \n", index_of, index_of, body ); Ok(ServiceResponse::new( req.clone(), HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(html), )) } actix-files-0.6.10/src/encoding.rs000064400000000000000000000022751046102023000150510ustar 00000000000000use mime::Mime; /// Transforms MIME `text/*` types into their UTF-8 equivalent, if supported. /// /// MIME types that are converted /// - application/javascript /// - text/html /// - text/css /// - text/plain /// - text/csv /// - text/tab-separated-values pub(crate) fn equiv_utf8_text(ct: Mime) -> Mime { // use (roughly) order of file-type popularity for a web server if ct == mime::APPLICATION_JAVASCRIPT { return mime::APPLICATION_JAVASCRIPT_UTF_8; } if ct == mime::TEXT_HTML { return mime::TEXT_HTML_UTF_8; } if ct == mime::TEXT_CSS { return mime::TEXT_CSS_UTF_8; } if ct == mime::TEXT_PLAIN { return mime::TEXT_PLAIN_UTF_8; } if ct == mime::TEXT_CSV { return mime::TEXT_CSV_UTF_8; } if ct == mime::TEXT_TAB_SEPARATED_VALUES { return mime::TEXT_TAB_SEPARATED_VALUES_UTF_8; } ct } #[cfg(test)] mod tests { use super::*; #[test] fn test_equiv_utf8_text() { assert_eq!(equiv_utf8_text(mime::TEXT_PLAIN), mime::TEXT_PLAIN_UTF_8); assert_eq!(equiv_utf8_text(mime::TEXT_XML), mime::TEXT_XML); assert_eq!(equiv_utf8_text(mime::IMAGE_PNG), mime::IMAGE_PNG); } } actix-files-0.6.10/src/error.rs000064400000000000000000000027521046102023000144140ustar 00000000000000use actix_web::{http::StatusCode, ResponseError}; use derive_more::Display; /// Errors which can occur when serving static files. #[derive(Debug, PartialEq, Eq, Display)] pub enum FilesError { /// Path is not a directory. #[allow(dead_code)] #[display("path is not a directory. Unable to serve static files")] IsNotDirectory, /// Cannot render directory. #[display("unable to render directory without index file")] IsDirectory, } impl ResponseError for FilesError { /// Returns `404 Not Found`. fn status_code(&self) -> StatusCode { StatusCode::NOT_FOUND } } /// Error which can occur with parsing/validating a request-uri path #[derive(Debug, PartialEq, Eq, Display)] #[non_exhaustive] pub enum UriSegmentError { /// Segment started with the wrapped invalid character. #[display("segment started with invalid character: ('{_0}')")] BadStart(char), /// Segment contained the wrapped invalid character. #[display("segment contained invalid character ('{_0}')")] BadChar(char), /// Segment ended with the wrapped invalid character. #[display("segment ended with invalid character: ('{_0}')")] BadEnd(char), /// Path is not a valid UTF-8 string after percent-decoding. #[display("path is not a valid UTF-8 string after percent-decoding")] NotValidUtf8, } impl ResponseError for UriSegmentError { /// Returns `400 Bad Request`. fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } } actix-files-0.6.10/src/files.rs000064400000000000000000000370621046102023000143670ustar 00000000000000use std::{ cell::RefCell, fmt, io, path::{Path, PathBuf}, rc::Rc, }; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; use actix_web::{ dev::{ AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, ServiceResponse, }, error::Error, guard::Guard, http::header::DispositionType, HttpRequest, }; use futures_core::future::LocalBoxFuture; use crate::{ directory_listing, named, service::{FilesService, FilesServiceInner}, Directory, DirectoryRenderer, HttpNewService, MimeOverride, PathFilter, }; /// Static files handling service. /// /// `Files` service must be registered with `App::service()` method. /// /// # Examples /// ``` /// use actix_web::App; /// use actix_files::Files; /// /// let app = App::new() /// .service(Files::new("/static", ".")); /// ``` pub struct Files { mount_path: String, directory: PathBuf, index: Option, show_index: bool, redirect_to_slash: bool, with_permanent_redirect: bool, default: Rc>>>, renderer: Rc, mime_override: Option>, path_filter: Option>, file_flags: named::Flags, use_guards: Option>, guards: Vec>, hidden_files: bool, read_mode_threshold: u64, } impl fmt::Debug for Files { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("Files") } } impl Clone for Files { fn clone(&self) -> Self { Self { directory: self.directory.clone(), index: self.index.clone(), show_index: self.show_index, redirect_to_slash: self.redirect_to_slash, with_permanent_redirect: self.with_permanent_redirect, default: self.default.clone(), renderer: self.renderer.clone(), file_flags: self.file_flags, mount_path: self.mount_path.clone(), mime_override: self.mime_override.clone(), path_filter: self.path_filter.clone(), use_guards: self.use_guards.clone(), guards: self.guards.clone(), hidden_files: self.hidden_files, read_mode_threshold: self.read_mode_threshold, } } } impl Files { /// Create new `Files` instance for a specified base directory. /// /// # Argument Order /// The first argument (`mount_path`) is the root URL at which the static files are served. /// For example, `/assets` will serve files at `example.com/assets/...`. /// /// The second argument (`serve_from`) is the location on disk at which files are loaded. /// This can be a relative path. For example, `./` would serve files from the current /// working directory. /// /// # Implementation Notes /// If the mount path is set as the root path `/`, services registered after this one will /// be inaccessible. Register more specific handlers and services first. /// /// If `serve_from` cannot be canonicalized at startup, an error is logged and the original /// path is preserved. Requests will return `404 Not Found` until the path exists. /// /// `Files` utilizes the existing Tokio thread-pool for blocking filesystem operations. /// The number of running threads is adjusted over time as needed, up to a maximum of 512 times /// the number of server [workers](actix_web::HttpServer::workers), by default. pub fn new>(mount_path: &str, serve_from: T) -> Files { let orig_dir = serve_from.into(); let dir = match orig_dir.canonicalize() { Ok(canon_dir) => canon_dir, Err(_) => { log::error!("Specified path is not a directory: {:?}", orig_dir); // Preserve original path so requests don't fall back to CWD. orig_dir } }; Files { mount_path: mount_path.trim_end_matches('/').to_owned(), directory: dir, index: None, show_index: false, redirect_to_slash: false, with_permanent_redirect: false, default: Rc::new(RefCell::new(None)), renderer: Rc::new(directory_listing), mime_override: None, path_filter: None, file_flags: named::Flags::default(), use_guards: None, guards: Vec::new(), hidden_files: false, read_mode_threshold: 0, } } /// Show files listing for directories. /// /// By default show files listing is disabled. /// /// When used with [`Files::index_file()`], files listing is shown as a fallback /// when the index file is not found. pub fn show_files_listing(mut self) -> Self { self.show_index = true; self } /// Redirects to a slash-ended path when browsing a directory. /// /// By default never redirect. pub fn redirect_to_slash_directory(mut self) -> Self { self.redirect_to_slash = true; self } /// Redirect with permanent redirect status code (308). /// /// By default redirect with temporary redirect status code (307). pub fn with_permanent_redirect(mut self) -> Self { self.with_permanent_redirect = true; self } /// Set custom directory renderer. pub fn files_listing_renderer(mut self, f: F) -> Self where for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result + 'static, { self.renderer = Rc::new(f); self } /// Specifies MIME override callback. pub fn mime_override(mut self, f: F) -> Self where F: Fn(&mime::Name<'_>) -> DispositionType + 'static, { self.mime_override = Some(Rc::new(f)); self } /// Sets path filtering closure. /// /// The path provided to the closure is relative to `serve_from` path. /// You can safely join this path with the `serve_from` path to get the real path. /// However, the real path may not exist since the filter is called before checking path existence. /// /// When a path doesn't pass the filter, [`Files::default_handler`] is called if set, otherwise, /// `404 Not Found` is returned. /// /// # Examples /// ``` /// use std::path::Path; /// use actix_files::Files; /// /// // prevent searching subdirectories and following symlinks /// let files_service = Files::new("/", "./static").path_filter(|path, _| { /// path.components().count() == 1 /// && Path::new("./static") /// .join(path) /// .symlink_metadata() /// .map(|m| !m.file_type().is_symlink()) /// .unwrap_or(false) /// }); /// ``` pub fn path_filter(mut self, f: F) -> Self where F: Fn(&Path, &RequestHead) -> bool + 'static, { self.path_filter = Some(Rc::new(f)); self } /// Set index file /// /// Shows specific index file for directories instead of /// showing files listing. /// /// If the index file is not found, files listing is shown as a fallback if /// [`Files::show_files_listing()`] is set. pub fn index_file>(mut self, index: T) -> Self { self.index = Some(index.into()); self } /// Sets the size threshold that determines file read mode (sync/async). /// /// When a file is smaller than the threshold (bytes), the reader will use synchronous /// (blocking) file reads. For larger files, it switches to async reads to avoid blocking the /// main thread. /// /// Tweaking this value according to your expected usage may lead to significant performance /// gains (or losses in other handlers, if `size` is too high). /// /// When the `experimental-io-uring` crate feature is enabled, file reads are always async. /// /// Default is 0, meaning all files are read asynchronously. pub fn read_mode_threshold(mut self, size: u64) -> Self { self.read_mode_threshold = size; self } /// Specifies whether to use ETag or not. /// /// Default is true. pub fn use_etag(mut self, value: bool) -> Self { self.file_flags.set(named::Flags::ETAG, value); self } /// Specifies whether to use Last-Modified or not. /// /// Default is true. pub fn use_last_modified(mut self, value: bool) -> Self { self.file_flags.set(named::Flags::LAST_MD, value); self } /// Specifies whether text responses should signal a UTF-8 encoding. /// /// Default is false (but will default to true in a future version). pub fn prefer_utf8(mut self, value: bool) -> Self { self.file_flags.set(named::Flags::PREFER_UTF8, value); self } /// Adds a routing guard. /// /// Use this to allow multiple chained file services that respond to strictly different /// properties of a request. Due to the way routing works, if a guard check returns true and the /// request starts being handled by the file service, it will not be able to back-out and try /// the next service, you will simply get a 404 (or 405) error response. /// /// To allow `POST` requests to retrieve files, see [`Files::method_guard()`]. /// /// # Examples /// ``` /// use actix_web::{guard::Header, App}; /// use actix_files::Files; /// /// App::new().service( /// Files::new("/","/my/site/files") /// .guard(Header("Host", "example.com")) /// ); /// ``` pub fn guard(mut self, guard: G) -> Self { self.guards.push(Rc::new(guard)); self } /// Specifies guard to check before fetching directory listings or files. /// /// Note that this guard has no effect on routing; it's main use is to guard on the request's /// method just before serving the file, only allowing `GET` and `HEAD` requests by default. /// See [`Files::guard`] for routing guards. pub fn method_guard(mut self, guard: G) -> Self { self.use_guards = Some(Rc::new(guard)); self } /// See [`Files::method_guard`]. #[doc(hidden)] #[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")] pub fn use_guards(self, guard: G) -> Self { self.method_guard(guard) } /// Disable `Content-Disposition` header. /// /// By default Content-Disposition` header is enabled. pub fn disable_content_disposition(mut self) -> Self { self.file_flags.remove(named::Flags::CONTENT_DISPOSITION); self } /// Sets default handler which is used when no matched file could be found. /// /// # Examples /// Setting a fallback static file handler: /// ``` /// use actix_files::{Files, NamedFile}; /// use actix_web::dev::{ServiceRequest, ServiceResponse, fn_service}; /// /// # fn run() -> Result<(), actix_web::Error> { /// let files = Files::new("/", "./static") /// .index_file("index.html") /// .default_handler(fn_service(|req: ServiceRequest| async { /// let (req, _) = req.into_parts(); /// let file = NamedFile::open_async("./static/404.html").await?; /// let res = file.into_response(&req); /// Ok(ServiceResponse::new(req, res)) /// })); /// # Ok(()) /// # } /// ``` pub fn default_handler(mut self, f: F) -> Self where F: IntoServiceFactory, U: ServiceFactory + 'static, { // create and configure default resource self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory( f.into_factory().map_init_err(|_| ()), ))))); self } /// Enables serving hidden files and directories, allowing a leading dots in url fragments. pub fn use_hidden_files(mut self) -> Self { self.hidden_files = true; self } } impl HttpServiceFactory for Files { fn register(mut self, config: &mut AppService) { let guards = if self.guards.is_empty() { None } else { let guards = std::mem::take(&mut self.guards); Some( guards .into_iter() .map(|guard| -> Box { Box::new(guard) }) .collect::>(), ) }; if self.default.borrow().is_none() { *self.default.borrow_mut() = Some(config.default_service()); } let rdef = if config.is_root() { ResourceDef::root_prefix(&self.mount_path) } else { ResourceDef::prefix(&self.mount_path) }; config.register_service(rdef, guards, self, None) } } impl ServiceFactory for Files { type Response = ServiceResponse; type Error = Error; type Config = (); type Service = FilesService; type InitError = (); type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { let mut inner = FilesServiceInner { directory: self.directory.clone(), index: self.index.clone(), show_index: self.show_index, redirect_to_slash: self.redirect_to_slash, default: None, renderer: self.renderer.clone(), mime_override: self.mime_override.clone(), path_filter: self.path_filter.clone(), file_flags: self.file_flags, guards: self.use_guards.clone(), hidden_files: self.hidden_files, size_threshold: self.read_mode_threshold, with_permanent_redirect: self.with_permanent_redirect, }; if let Some(ref default) = *self.default.borrow() { let fut = default.new_service(()); Box::pin(async { match fut.await { Ok(default) => { inner.default = Some(default); Ok(FilesService(Rc::new(inner))) } Err(_) => Err(()), } }) } else { Box::pin(async move { Ok(FilesService(Rc::new(inner))) }) } } } #[cfg(test)] mod tests { use actix_web::{ http::StatusCode, test::{self, TestRequest}, App, HttpResponse, }; use super::*; #[actix_web::test] async fn custom_files_listing_renderer() { let srv = test::init_service( App::new().service( Files::new("/", "./tests") .show_files_listing() .files_listing_renderer(|dir, req| { Ok(ServiceResponse::new( req.clone(), HttpResponse::Ok().body(dir.path.to_str().unwrap().to_owned()), )) }), ), ) .await; let req = TestRequest::with_uri("/").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); let body = test::read_body(res).await; let body_str = std::str::from_utf8(&body).unwrap(); let actual_path = Path::new(&body_str); let expected_path = Path::new("actix-files/tests"); assert!( actual_path.ends_with(expected_path), "body {:?} does not end with {:?}", actual_path, expected_path ); } } actix-files-0.6.10/src/lib.rs000064400000000000000000001105751046102023000140340ustar 00000000000000//! Static file serving for Actix Web. //! //! Provides a non-blocking service for serving static files from disk. //! //! # Examples //! ``` //! use actix_web::App; //! use actix_files::Files; //! //! let app = App::new() //! .service(Files::new("/static", ".").prefer_utf8(true)); //! ``` #![warn(missing_docs, missing_debug_implementations)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_cfg))] use std::path::Path; use actix_service::boxed::{BoxService, BoxServiceFactory}; use actix_web::{ dev::{RequestHead, ServiceRequest, ServiceResponse}, error::Error, http::header::DispositionType, }; use mime_guess::from_ext; mod chunked; mod directory; mod encoding; mod error; mod files; mod named; mod path_buf; mod range; mod service; pub use self::{ chunked::ChunkedReadFile, directory::Directory, error::UriSegmentError, files::Files, named::NamedFile, path_buf::PathBufWrap, range::HttpRange, service::FilesService, }; use self::{ directory::{directory_listing, DirectoryRenderer}, error::FilesError, }; type HttpService = BoxService; type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; /// Return the MIME type associated with a filename extension (case-insensitive). /// If `ext` is empty or no associated type for the extension was found, returns /// the type `application/octet-stream`. #[inline] pub fn file_extension_to_mime(ext: &str) -> mime::Mime { from_ext(ext).first_or_octet_stream() } type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType; type PathFilter = dyn Fn(&Path, &RequestHead) -> bool; #[cfg(test)] mod tests { use std::{ fmt::Write as _, fs::{self}, ops::Add, time::{Duration, SystemTime}, }; use actix_web::{ dev::ServiceFactory, guard, http::{ header::{self, ContentDisposition, DispositionParam}, Method, StatusCode, }, middleware::Compress, test::{self, TestRequest}, web::{self, Bytes}, App, HttpResponse, Responder, }; use super::*; use crate::named::File; #[actix_web::test] async fn test_file_extension_to_mime() { let m = file_extension_to_mime(""); assert_eq!(m, mime::APPLICATION_OCTET_STREAM); let m = file_extension_to_mime("jpg"); assert_eq!(m, mime::IMAGE_JPEG); let m = file_extension_to_mime("invalid extension!!"); assert_eq!(m, mime::APPLICATION_OCTET_STREAM); let m = file_extension_to_mime(""); assert_eq!(m, mime::APPLICATION_OCTET_STREAM); } #[actix_rt::test] async fn test_if_modified_since_without_if_none_match() { let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let req = TestRequest::default() .insert_header((header::IF_MODIFIED_SINCE, since)) .to_http_request(); let resp = file.respond_to(&req); assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); } #[actix_rt::test] async fn test_if_modified_since_without_if_none_match_same() { let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let since = file.last_modified().unwrap(); let req = TestRequest::default() .insert_header((header::IF_MODIFIED_SINCE, since)) .to_http_request(); let resp = file.respond_to(&req); assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); } #[actix_rt::test] async fn test_if_modified_since_with_if_none_match() { let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let req = TestRequest::default() .insert_header((header::IF_NONE_MATCH, "miss_etag")) .insert_header((header::IF_MODIFIED_SINCE, since)) .to_http_request(); let resp = file.respond_to(&req); assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); } #[actix_rt::test] async fn test_if_unmodified_since() { let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let since = file.last_modified().unwrap(); let req = TestRequest::default() .insert_header((header::IF_UNMODIFIED_SINCE, since)) .to_http_request(); let resp = file.respond_to(&req); assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_if_unmodified_since_failed() { let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let since = header::HttpDate::from(SystemTime::UNIX_EPOCH); let req = TestRequest::default() .insert_header((header::IF_UNMODIFIED_SINCE, since)) .to_http_request(); let resp = file.respond_to(&req); assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED); } #[actix_rt::test] async fn test_named_file_text() { assert!(NamedFile::open_async("test--").await.is_err()); let mut file = NamedFile::open_async("Cargo.toml").await.unwrap(); { file.file(); let _f: &File = &file; } { let _f: &mut File = &mut file; } let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/x-toml" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "inline; filename=\"Cargo.toml\"" ); } #[actix_rt::test] async fn test_named_file_content_disposition() { assert!(NamedFile::open_async("test--").await.is_err()); let mut file = NamedFile::open_async("Cargo.toml").await.unwrap(); { file.file(); let _f: &File = &file; } { let _f: &mut File = &mut file; } let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "inline; filename=\"Cargo.toml\"" ); let file = NamedFile::open_async("Cargo.toml") .await .unwrap() .disable_content_disposition(); let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req); assert!(resp.headers().get(header::CONTENT_DISPOSITION).is_none()); } #[actix_rt::test] async fn test_named_file_non_ascii_file_name() { let file = { #[cfg(feature = "experimental-io-uring")] { crate::named::File::open("Cargo.toml").await.unwrap() } #[cfg(not(feature = "experimental-io-uring"))] { crate::named::File::open("Cargo.toml").unwrap() } }; let mut file = NamedFile::from_file(file, "貨物.toml").unwrap(); { file.file(); let _f: &File = &file; } { let _f: &mut File = &mut file; } let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/x-toml" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "inline; filename=\"貨物.toml\"; filename*=UTF-8''%E8%B2%A8%E7%89%A9.toml" ); } #[actix_rt::test] async fn test_named_file_set_content_type() { let mut file = NamedFile::open_async("Cargo.toml") .await .unwrap() .set_content_type(mime::TEXT_XML); { file.file(); let _f: &File = &file; } { let _f: &mut File = &mut file; } let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/xml" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "inline; filename=\"Cargo.toml\"" ); } #[actix_rt::test] async fn test_named_file_image() { let mut file = NamedFile::open_async("tests/test.png").await.unwrap(); { file.file(); let _f: &File = &file; } { let _f: &mut File = &mut file; } let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "image/png" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "inline; filename=\"test.png\"" ); } #[actix_rt::test] async fn test_named_file_javascript() { let file = NamedFile::open_async("tests/test.js").await.unwrap(); let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/javascript", ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "inline; filename=\"test.js\"", ); } #[actix_rt::test] async fn test_named_file_image_attachment() { let cd = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::Filename(String::from("test.png"))], }; let mut file = NamedFile::open_async("tests/test.png") .await .unwrap() .set_content_disposition(cd); { file.file(); let _f: &File = &file; } { let _f: &mut File = &mut file; } let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "image/png" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "attachment; filename=\"test.png\"" ); } #[actix_rt::test] async fn test_named_file_binary() { let mut file = NamedFile::open_async("tests/test.binary").await.unwrap(); { file.file(); let _f: &File = &file; } { let _f: &mut File = &mut file; } let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "application/octet-stream" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "attachment; filename=\"test.binary\"" ); } #[allow(deprecated)] #[actix_rt::test] async fn status_code_customize_same_output() { let file1 = NamedFile::open_async("Cargo.toml") .await .unwrap() .set_status_code(StatusCode::NOT_FOUND); let file2 = NamedFile::open_async("Cargo.toml") .await .unwrap() .customize() .with_status(StatusCode::NOT_FOUND); let req = TestRequest::default().to_http_request(); let res1 = file1.respond_to(&req); let res2 = file2.respond_to(&req); assert_eq!(res1.status(), StatusCode::NOT_FOUND); assert_eq!(res2.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_named_file_status_code_text() { let mut file = NamedFile::open_async("Cargo.toml").await.unwrap(); { file.file(); let _f: &File = &file; } { let _f: &mut File = &mut file; } let file = file.customize().with_status(StatusCode::NOT_FOUND); let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/x-toml" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "inline; filename=\"Cargo.toml\"" ); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_mime_override() { fn all_attachment(_: &mime::Name<'_>) -> DispositionType { DispositionType::Attachment } let srv = test::init_service( App::new().service( Files::new("/", ".") .mime_override(all_attachment) .index_file("Cargo.toml"), ), ) .await; let request = TestRequest::get().uri("/").to_request(); let response = test::call_service(&srv, request).await; assert_eq!(response.status(), StatusCode::OK); let content_disposition = response .headers() .get(header::CONTENT_DISPOSITION) .expect("To have CONTENT_DISPOSITION"); let content_disposition = content_disposition .to_str() .expect("Convert CONTENT_DISPOSITION to str"); assert_eq!(content_disposition, "attachment; filename=\"Cargo.toml\""); } #[actix_rt::test] async fn test_named_file_ranges_status_code() { let srv = test::init_service( App::new().service(Files::new("/test", ".").index_file("Cargo.toml")), ) .await; // Valid range header let request = TestRequest::get() .uri("/t%65st/Cargo.toml") .insert_header((header::RANGE, "bytes=10-20")) .to_request(); let response = test::call_service(&srv, request).await; assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); // Invalid range header let request = TestRequest::get() .uri("/t%65st/Cargo.toml") .insert_header((header::RANGE, "bytes=1-0")) .to_request(); let response = test::call_service(&srv, request).await; assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); } #[actix_rt::test] async fn test_named_file_empty_range_headers() { let srv = actix_test::start(|| App::new().service(Files::new("/", "."))); for range in ["", "bytes="] { let response = srv .get("/tests/test.binary") .insert_header((header::RANGE, range)) .send() .await .unwrap(); assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); let content_range = response.headers().get(header::CONTENT_RANGE).unwrap(); assert_eq!(content_range.to_str().unwrap(), "bytes */100"); } } #[actix_rt::test] async fn test_named_file_content_range_headers() { let srv = actix_test::start(|| App::new().service(Files::new("/", "."))); // Valid range header let response = srv .get("/tests/test.binary") .insert_header((header::RANGE, "bytes=10-20")) .send() .await .unwrap(); let content_range = response.headers().get(header::CONTENT_RANGE).unwrap(); assert_eq!(content_range.to_str().unwrap(), "bytes 10-20/100"); // Invalid range header let response = srv .get("/tests/test.binary") .insert_header((header::RANGE, "bytes=10-5")) .send() .await .unwrap(); let content_range = response.headers().get(header::CONTENT_RANGE).unwrap(); assert_eq!(content_range.to_str().unwrap(), "bytes */100"); } #[actix_rt::test] async fn test_named_file_content_length_headers() { let srv = actix_test::start(|| App::new().service(Files::new("/", "."))); // Valid range header let response = srv .get("/tests/test.binary") .insert_header((header::RANGE, "bytes=10-20")) .send() .await .unwrap(); let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap(); assert_eq!(content_length.to_str().unwrap(), "11"); // Valid range header, starting from 0 let response = srv .get("/tests/test.binary") .insert_header((header::RANGE, "bytes=0-20")) .send() .await .unwrap(); let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap(); assert_eq!(content_length.to_str().unwrap(), "21"); // Without range header let mut response = srv.get("/tests/test.binary").send().await.unwrap(); let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap(); assert_eq!(content_length.to_str().unwrap(), "100"); // Should be no transfer-encoding let transfer_encoding = response.headers().get(header::TRANSFER_ENCODING); assert!(transfer_encoding.is_none()); // Check file contents let bytes = response.body().await.unwrap(); let data = web::Bytes::from(fs::read("tests/test.binary").unwrap()); assert_eq!(bytes, data); } #[actix_rt::test] async fn test_head_content_length_headers() { let srv = actix_test::start(|| App::new().service(Files::new("/", "."))); let response = srv.head("/tests/test.binary").send().await.unwrap(); let content_length = response .headers() .get(header::CONTENT_LENGTH) .unwrap() .to_str() .unwrap(); assert_eq!(content_length, "100"); } #[actix_rt::test] async fn test_static_files_with_spaces() { let srv = test::init_service(App::new().service(Files::new("/", ".").index_file("Cargo.toml"))) .await; let request = TestRequest::get() .uri("/tests/test%20space.binary") .to_request(); let response = test::call_service(&srv, request).await; assert_eq!(response.status(), StatusCode::OK); let bytes = test::read_body(response).await; let data = web::Bytes::from(fs::read("tests/test space.binary").unwrap()); assert_eq!(bytes, data); } #[cfg(not(target_os = "windows"))] #[actix_rt::test] async fn test_static_files_with_special_characters() { // Create the file we want to test against ad-hoc. We can't check it in as otherwise // Windows can't even checkout this repository. let temp_dir = tempfile::tempdir().unwrap(); let file_with_newlines = temp_dir.path().join("test\n\x0B\x0C\rnewline.text"); fs::write(&file_with_newlines, "Look at my newlines").unwrap(); let srv = test::init_service( App::new().service(Files::new("/", temp_dir.path()).index_file("Cargo.toml")), ) .await; let request = TestRequest::get() .uri("/test%0A%0B%0C%0Dnewline.text") .to_request(); let response = test::call_service(&srv, request).await; assert_eq!(response.status(), StatusCode::OK); let bytes = test::read_body(response).await; let data = web::Bytes::from(fs::read(file_with_newlines).unwrap()); assert_eq!(bytes, data); } #[actix_rt::test] async fn test_files_not_allowed() { let srv = test::init_service(App::new().service(Files::new("/", "."))).await; let req = TestRequest::default() .uri("/Cargo.toml") .method(Method::POST) .to_request(); let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); let srv = test::init_service(App::new().service(Files::new("/", "."))).await; let req = TestRequest::default() .method(Method::PUT) .uri("/Cargo.toml") .to_request(); let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } #[actix_rt::test] async fn test_files_guards() { let srv = test::init_service( App::new().service(Files::new("/", ".").method_guard(guard::Post())), ) .await; let req = TestRequest::default() .uri("/Cargo.toml") .method(Method::POST) .to_request(); let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_named_file_content_encoding() { let srv = test::init_service(App::new().wrap(Compress::default()).service( web::resource("/").to(|| async { NamedFile::open_async("Cargo.toml") .await .unwrap() .set_content_encoding(header::ContentEncoding::Identity) }), )) .await; let request = TestRequest::get() .uri("/") .insert_header((header::ACCEPT_ENCODING, "gzip")) .to_request(); let res = test::call_service(&srv, request).await; assert_eq!(res.status(), StatusCode::OK); assert!(res.headers().contains_key(header::CONTENT_ENCODING)); assert!(!test::read_body(res).await.is_empty()); } #[actix_rt::test] async fn test_named_file_content_encoding_gzip() { let srv = test::init_service(App::new().wrap(Compress::default()).service( web::resource("/").to(|| async { NamedFile::open_async("Cargo.toml") .await .unwrap() .set_content_encoding(header::ContentEncoding::Gzip) }), )) .await; let request = TestRequest::get() .uri("/") .insert_header((header::ACCEPT_ENCODING, "gzip")) .to_request(); let res = test::call_service(&srv, request).await; assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers() .get(header::CONTENT_ENCODING) .unwrap() .to_str() .unwrap(), "gzip" ); } #[actix_rt::test] async fn test_named_file_allowed_method() { let req = TestRequest::default().method(Method::GET).to_http_request(); let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let resp = file.respond_to(&req); assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_static_files() { let srv = test::init_service(App::new().service(Files::new("/", ".").show_files_listing())).await; let req = TestRequest::with_uri("/missing").to_request(); let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); let srv = test::init_service(App::new().service(Files::new("/", "."))).await; let req = TestRequest::default().to_request(); let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); let srv = test::init_service(App::new().service(Files::new("/", ".").show_files_listing())).await; let req = TestRequest::with_uri("/tests").to_request(); let resp = test::call_service(&srv, req).await; assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/html; charset=utf-8" ); let bytes = test::read_body(resp).await; assert!(format!("{:?}", bytes).contains("/tests/test.png")); } #[actix_rt::test] async fn test_redirect_to_slash_directory() { // should not redirect if no index and files listing is disabled let srv = test::init_service( App::new().service(Files::new("/", ".").redirect_to_slash_directory()), ) .await; let req = TestRequest::with_uri("/tests").to_request(); let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); // should redirect if index present let srv = test::init_service( App::new().service( Files::new("/", ".") .index_file("test.png") .redirect_to_slash_directory(), ), ) .await; let req = TestRequest::with_uri("/tests").to_request(); let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT); // should redirect if index present with permanent redirect let srv = test::init_service( App::new().service( Files::new("/", ".") .index_file("test.png") .redirect_to_slash_directory() .with_permanent_redirect(), ), ) .await; let req = TestRequest::with_uri("/tests").to_request(); let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::PERMANENT_REDIRECT); // should redirect if files listing is enabled let srv = test::init_service( App::new().service( Files::new("/", ".") .show_files_listing() .redirect_to_slash_directory(), ), ) .await; let req = TestRequest::with_uri("/tests").to_request(); let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT); // should not redirect if the path is wrong let req = TestRequest::with_uri("/not_existing").to_request(); let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_static_files_bad_directory() { let service = Files::new("/", "./missing").new_service(()).await.unwrap(); let req = TestRequest::with_uri("/").to_srv_request(); let resp = test::call_service(&service, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_static_files_bad_directory_does_not_serve_cwd_files() { let service = Files::new("/", "./missing").new_service(()).await.unwrap(); let req = TestRequest::with_uri("/Cargo.toml").to_srv_request(); let resp = test::call_service(&service, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_default_handler_file_missing() { let st = Files::new("/", ".") .default_handler(|req: ServiceRequest| async { Ok(req.into_response(HttpResponse::Ok().body("default content"))) }) .new_service(()) .await .unwrap(); let req = TestRequest::with_uri("/missing").to_srv_request(); let resp = test::call_service(&st, req).await; assert_eq!(resp.status(), StatusCode::OK); let bytes = test::read_body(resp).await; assert_eq!(bytes, web::Bytes::from_static(b"default content")); } #[actix_rt::test] async fn test_serve_index_nested() { let service = Files::new(".", ".") .index_file("lib.rs") .new_service(()) .await .unwrap(); let req = TestRequest::default().uri("/src").to_srv_request(); let resp = test::call_service(&service, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/x-rust" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), "inline; filename=\"lib.rs\"" ); } #[actix_rt::test] async fn integration_serve_index() { let srv = test::init_service( App::new().service(Files::new("test", ".").index_file("Cargo.toml")), ) .await; let req = TestRequest::get().uri("/test").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); let bytes = test::read_body(res).await; let data = Bytes::from(fs::read("Cargo.toml").unwrap()); assert_eq!(bytes, data); let req = TestRequest::get().uri("/test/").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); let bytes = test::read_body(res).await; let data = Bytes::from(fs::read("Cargo.toml").unwrap()); assert_eq!(bytes, data); // nonexistent index file let req = TestRequest::get().uri("/test/unknown").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::NOT_FOUND); let req = TestRequest::get().uri("/test/unknown/").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn integration_percent_encoded() { let srv = test::init_service( App::new().service(Files::new("test", ".").index_file("Cargo.toml")), ) .await; let req = TestRequest::get().uri("/test/%43argo.toml").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); // `%2F` == `/` let req = TestRequest::get().uri("/test%2Ftest.binary").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::NOT_FOUND); let req = TestRequest::get().uri("/test/Cargo.toml%00").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_percent_encoding_2() { let temp_dir = tempfile::tempdir().unwrap(); let filename = match cfg!(unix) { true => "ض:?#[]{}<>()@!$&'`|*+,;= %20\n.test", false => "ض#[]{}()@!$&'`+,;= %20.test", }; let filename_encoded = filename .as_bytes() .iter() .fold(String::new(), |mut buf, c| { write!(&mut buf, "%{:02X}", c).unwrap(); buf }); std::fs::File::create(temp_dir.path().join(filename)).unwrap(); let srv = test::init_service(App::new().service(Files::new("/", temp_dir.path()))).await; let req = TestRequest::get() .uri(&format!("/{}", filename_encoded)) .to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); } #[actix_rt::test] async fn test_serve_named_file() { let factory = NamedFile::open_async("Cargo.toml").await.unwrap(); let srv = test::init_service(App::new().service(factory)).await; let req = TestRequest::get().uri("/Cargo.toml").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); let bytes = test::read_body(res).await; let data = Bytes::from(fs::read("Cargo.toml").unwrap()); assert_eq!(bytes, data); let req = TestRequest::get().uri("/test/unknown").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_serve_named_file_prefix() { let factory = NamedFile::open_async("Cargo.toml").await.unwrap(); let srv = test::init_service(App::new().service(web::scope("/test").service(factory))).await; let req = TestRequest::get().uri("/test/Cargo.toml").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); let bytes = test::read_body(res).await; let data = Bytes::from(fs::read("Cargo.toml").unwrap()); assert_eq!(bytes, data); let req = TestRequest::get().uri("/Cargo.toml").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_named_file_default_service() { let factory = NamedFile::open_async("Cargo.toml").await.unwrap(); let srv = test::init_service(App::new().default_service(factory)).await; for route in ["/foobar", "/baz", "/"].iter() { let req = TestRequest::get().uri(route).to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); let bytes = test::read_body(res).await; let data = Bytes::from(fs::read("Cargo.toml").unwrap()); assert_eq!(bytes, data); } } #[actix_rt::test] async fn test_default_handler_named_file() { let factory = NamedFile::open_async("Cargo.toml").await.unwrap(); let st = Files::new("/", ".") .default_handler(factory) .new_service(()) .await .unwrap(); let req = TestRequest::with_uri("/missing").to_srv_request(); let resp = test::call_service(&st, req).await; assert_eq!(resp.status(), StatusCode::OK); let bytes = test::read_body(resp).await; let data = Bytes::from(fs::read("Cargo.toml").unwrap()); assert_eq!(bytes, data); } #[actix_rt::test] async fn test_symlinks() { let srv = test::init_service(App::new().service(Files::new("test", "."))).await; let req = TestRequest::get() .uri("/test/tests/symlink-test.png") .to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(header::CONTENT_DISPOSITION).unwrap(), "inline; filename=\"symlink-test.png\"" ); } #[actix_rt::test] async fn test_index_with_show_files_listing() { let service = Files::new(".", ".") .index_file("lib.rs") .show_files_listing() .new_service(()) .await .unwrap(); // Serve the index if exists let req = TestRequest::default().uri("/src").to_srv_request(); let resp = test::call_service(&service, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/x-rust" ); // Show files listing, otherwise. let req = TestRequest::default().uri("/tests").to_srv_request(); let resp = test::call_service(&service, req).await; assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/html; charset=utf-8" ); let bytes = test::read_body(resp).await; assert!(format!("{:?}", bytes).contains("/tests/test.png")); } #[actix_rt::test] async fn test_path_filter() { // prevent searching subdirectories let st = Files::new("/", ".") .path_filter(|path, _| path.components().count() == 1) .new_service(()) .await .unwrap(); let req = TestRequest::with_uri("/Cargo.toml").to_srv_request(); let resp = test::call_service(&st, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/src/lib.rs").to_srv_request(); let resp = test::call_service(&st, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_default_handler_filter() { let st = Files::new("/", ".") .default_handler(|req: ServiceRequest| async { Ok(req.into_response(HttpResponse::Ok().body("default content"))) }) .path_filter(|path, _| path.extension() == Some("png".as_ref())) .new_service(()) .await .unwrap(); let req = TestRequest::with_uri("/Cargo.toml").to_srv_request(); let resp = test::call_service(&st, req).await; assert_eq!(resp.status(), StatusCode::OK); let bytes = test::read_body(resp).await; assert_eq!(bytes, web::Bytes::from_static(b"default content")); } } actix-files-0.6.10/src/named.rs000064400000000000000000000551271046102023000143530ustar 00000000000000use std::{ fs::Metadata, io, path::{Path, PathBuf}, time::{SystemTime, UNIX_EPOCH}, }; use actix_web::{ body::{self, BoxBody, SizedStream}, dev::{ self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest, ServiceResponse, }, http::{ header::{ self, Charset, ContentDisposition, ContentEncoding, DispositionParam, DispositionType, ExtendedValue, HeaderValue, }, StatusCode, }, Error, HttpMessage, HttpRequest, HttpResponse, Responder, }; use bitflags::bitflags; use derive_more::{Deref, DerefMut}; use futures_core::future::LocalBoxFuture; use mime::Mime; use crate::{encoding::equiv_utf8_text, range::HttpRange}; bitflags! { #[derive(Debug, Clone, Copy)] pub(crate) struct Flags: u8 { const ETAG = 0b0000_0001; const LAST_MD = 0b0000_0010; const CONTENT_DISPOSITION = 0b0000_0100; const PREFER_UTF8 = 0b0000_1000; } } impl Default for Flags { fn default() -> Self { Flags::from_bits_truncate(0b0000_1111) } } /// A file with an associated name. /// /// `NamedFile` can be registered as services: /// ``` /// use actix_web::App; /// use actix_files::NamedFile; /// /// # async fn run() -> Result<(), Box> { /// let file = NamedFile::open_async("./static/index.html").await?; /// let app = App::new().service(file); /// # Ok(()) /// # } /// ``` /// /// They can also be returned from handlers: /// ``` /// use actix_web::{Responder, get}; /// use actix_files::NamedFile; /// /// #[get("/")] /// async fn index() -> impl Responder { /// NamedFile::open_async("./static/index.html").await /// } /// ``` #[derive(Debug, Deref, DerefMut)] pub struct NamedFile { #[deref] #[deref_mut] file: File, path: PathBuf, modified: Option, pub(crate) md: Metadata, pub(crate) flags: Flags, pub(crate) status_code: StatusCode, pub(crate) content_type: Mime, pub(crate) content_disposition: ContentDisposition, pub(crate) encoding: Option, pub(crate) read_mode_threshold: u64, } #[cfg(not(feature = "experimental-io-uring"))] pub(crate) use std::fs::File; #[cfg(feature = "experimental-io-uring")] pub(crate) use tokio_uring::fs::File; use super::chunked; impl NamedFile { /// Creates an instance from a previously opened file. /// /// The given `path` need not exist and is only used to determine the `ContentType` and /// `ContentDisposition` headers. /// /// # Examples /// ```ignore /// use std::{ /// io::{self, Write as _}, /// env, /// fs::File /// }; /// use actix_files::NamedFile; /// /// let mut file = File::create("foo.txt")?; /// file.write_all(b"Hello, world!")?; /// let named_file = NamedFile::from_file(file, "bar.txt")?; /// # std::fs::remove_file("foo.txt"); /// Ok(()) /// ``` pub fn from_file>(file: File, path: P) -> io::Result { let path = path.as_ref().to_path_buf(); // Get the name of the file and use it to construct default Content-Type // and Content-Disposition values let (content_type, content_disposition) = { let filename = match path.file_name() { Some(name) => name.to_string_lossy(), None => { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Provided path has no filename", )); } }; let ct = mime_guess::from_path(&path).first_or_octet_stream(); let disposition = match ct.type_() { mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline, mime::APPLICATION => match ct.subtype() { mime::JAVASCRIPT | mime::JSON => DispositionType::Inline, name if name == "wasm" || name == "xhtml" => DispositionType::Inline, _ => DispositionType::Attachment, }, _ => DispositionType::Attachment, }; // replace special characters in filenames which could occur on some filesystems let filename_s = filename .replace('\n', "%0A") // \n line break .replace('\x0B', "%0B") // \v vertical tab .replace('\x0C', "%0C") // \f form feed .replace('\r', "%0D"); // \r carriage return let mut parameters = vec![DispositionParam::Filename(filename_s)]; if !filename.is_ascii() { parameters.push(DispositionParam::FilenameExt(ExtendedValue { charset: Charset::Ext(String::from("UTF-8")), language_tag: None, value: filename.into_owned().into_bytes(), })) } let cd = ContentDisposition { disposition, parameters, }; (ct, cd) }; let md = { #[cfg(not(feature = "experimental-io-uring"))] { file.metadata()? } #[cfg(feature = "experimental-io-uring")] { use std::os::unix::prelude::{AsRawFd, FromRawFd}; let fd = file.as_raw_fd(); // SAFETY: fd is borrowed and lives longer than the unsafe block unsafe { let file = std::fs::File::from_raw_fd(fd); let md = file.metadata(); // SAFETY: forget the fd before exiting block in success or error case but don't // run destructor (that would close file handle) std::mem::forget(file); md? } } }; let modified = md.modified().ok(); let encoding = None; Ok(NamedFile { path, file, content_type, content_disposition, md, modified, encoding, status_code: StatusCode::OK, flags: Flags::default(), read_mode_threshold: 0, }) } /// Attempts to open a file in read-only mode. /// /// # Examples /// ``` /// use actix_files::NamedFile; /// let file = NamedFile::open("foo.txt"); /// ``` #[cfg(not(feature = "experimental-io-uring"))] pub fn open>(path: P) -> io::Result { let file = File::open(&path)?; Self::from_file(file, path) } /// Attempts to open a file asynchronously in read-only mode. /// /// When the `experimental-io-uring` crate feature is enabled, this will be async. Otherwise, it /// will behave just like `open`. /// /// # Examples /// ``` /// use actix_files::NamedFile; /// # async fn open() { /// let file = NamedFile::open_async("foo.txt").await.unwrap(); /// # } /// ``` pub async fn open_async>(path: P) -> io::Result { let file = { #[cfg(not(feature = "experimental-io-uring"))] { File::open(&path)? } #[cfg(feature = "experimental-io-uring")] { File::open(&path).await? } }; Self::from_file(file, path) } /// Returns reference to the underlying file object. #[inline] pub fn file(&self) -> &File { &self.file } /// Returns the filesystem path to this file. /// /// # Examples /// ``` /// # use std::io; /// use actix_files::NamedFile; /// /// # async fn path() -> io::Result<()> { /// let file = NamedFile::open_async("test.txt").await?; /// assert_eq!(file.path().as_os_str(), "foo.txt"); /// # Ok(()) /// # } /// ``` #[inline] pub fn path(&self) -> &Path { self.path.as_path() } /// Returns the time the file was last modified. /// /// Returns `None` only on unsupported platforms; see [`std::fs::Metadata::modified()`]. /// Therefore, it is usually safe to unwrap this. #[inline] pub fn modified(&self) -> Option { self.modified } /// Returns the filesystem metadata associated with this file. #[inline] pub fn metadata(&self) -> &Metadata { &self.md } /// Returns the `Content-Type` header that will be used when serving this file. #[inline] pub fn content_type(&self) -> &Mime { &self.content_type } /// Returns the `Content-Disposition` that will be used when serving this file. #[inline] pub fn content_disposition(&self) -> &ContentDisposition { &self.content_disposition } /// Returns the `Content-Encoding` that will be used when serving this file. /// /// A return value of `None` indicates that the content is not already using a compressed /// representation and may be subject to compression downstream. #[inline] pub fn content_encoding(&self) -> Option { self.encoding } /// Set response status code. #[deprecated(since = "0.7.0", note = "Prefer `Responder::customize()`.")] pub fn set_status_code(mut self, status: StatusCode) -> Self { self.status_code = status; self } /// Sets the `Content-Type` header that will be used when serving this file. By default the /// `Content-Type` is inferred from the filename extension. #[inline] pub fn set_content_type(mut self, mime_type: Mime) -> Self { self.content_type = mime_type; self } /// Set the Content-Disposition for serving this file. This allows changing the /// `inline/attachment` disposition as well as the filename sent to the peer. /// /// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and /// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise, and the /// filename is taken from the path provided in the `open` method after converting it to UTF-8 /// (using `to_string_lossy`). #[inline] pub fn set_content_disposition(mut self, cd: ContentDisposition) -> Self { self.content_disposition = cd; self.flags.insert(Flags::CONTENT_DISPOSITION); self } /// Disables `Content-Disposition` header. /// /// By default, the `Content-Disposition` header is sent. #[inline] pub fn disable_content_disposition(mut self) -> Self { self.flags.remove(Flags::CONTENT_DISPOSITION); self } /// Sets content encoding for this file. /// /// This prevents the `Compress` middleware from modifying the file contents and signals to /// browsers/clients how to decode it. For example, if serving a compressed HTML file (e.g., /// `index.html.gz`) then use `.set_content_encoding(ContentEncoding::Gzip)`. #[inline] pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { self.encoding = Some(enc); self } /// Sets the size threshold that determines file read mode (sync/async). /// /// When a file is smaller than the threshold (bytes), the reader will use synchronous /// (blocking) file reads. For larger files, it switches to async reads to avoid blocking the /// main thread. /// /// Tweaking this value according to your expected usage may lead to significant performance /// gains (or losses in other handlers, if `size` is too high). /// /// When the `experimental-io-uring` crate feature is enabled, file reads are always async. /// /// Default is 0, meaning all files are read asynchronously. pub fn read_mode_threshold(mut self, size: u64) -> Self { self.read_mode_threshold = size; self } /// Specifies whether to return `ETag` header in response. /// /// Default is true. #[inline] pub fn use_etag(mut self, value: bool) -> Self { self.flags.set(Flags::ETAG, value); self } /// Specifies whether to return `Last-Modified` header in response. /// /// Default is true. #[inline] pub fn use_last_modified(mut self, value: bool) -> Self { self.flags.set(Flags::LAST_MD, value); self } /// Specifies whether text responses should signal a UTF-8 encoding. /// /// Default is false (but will default to true in a future version). #[inline] pub fn prefer_utf8(mut self, value: bool) -> Self { self.flags.set(Flags::PREFER_UTF8, value); self } /// Creates an `ETag` in a format is similar to Apache's. pub(crate) fn etag(&self) -> Option { self.modified.as_ref().map(|mtime| { let ino = { #[cfg(unix)] { #[cfg(unix)] use std::os::unix::fs::MetadataExt as _; self.md.ino() } #[cfg(not(unix))] { 0 } }; let dur = mtime .duration_since(UNIX_EPOCH) .expect("modification time must be after epoch"); header::EntityTag::new_strong(format!( "{:x}:{:x}:{:x}:{:x}", ino, self.md.len(), dur.as_secs(), dur.subsec_nanos() )) }) } pub(crate) fn last_modified(&self) -> Option { self.modified.map(|mtime| mtime.into()) } /// Creates an `HttpResponse` with file as a streaming body. pub fn into_response(self, req: &HttpRequest) -> HttpResponse { if self.status_code != StatusCode::OK { let mut res = HttpResponse::build(self.status_code); let ct = if self.flags.contains(Flags::PREFER_UTF8) { equiv_utf8_text(self.content_type.clone()) } else { self.content_type }; res.insert_header((header::CONTENT_TYPE, ct.to_string())); if self.flags.contains(Flags::CONTENT_DISPOSITION) { res.insert_header(( header::CONTENT_DISPOSITION, self.content_disposition.to_string(), )); } if let Some(current_encoding) = self.encoding { res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str())); } let reader = chunked::new_chunked_read(self.md.len(), 0, self.file, self.read_mode_threshold); return res.streaming(reader); } let etag = if self.flags.contains(Flags::ETAG) { self.etag() } else { None }; let last_modified = if self.flags.contains(Flags::LAST_MD) { self.last_modified() } else { None }; // check preconditions let precondition_failed = if !any_match(etag.as_ref(), req) { true } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = (last_modified, req.get_header()) { let t1: SystemTime = (*m).into(); let t2: SystemTime = (*since).into(); match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { (Ok(t1), Ok(t2)) => t1.as_secs() > t2.as_secs(), _ => false, } } else { false }; // check last modified let not_modified = if !none_match(etag.as_ref(), req) { true } else if req.headers().contains_key(header::IF_NONE_MATCH) { false } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = (last_modified, req.get_header()) { let t1: SystemTime = (*m).into(); let t2: SystemTime = (*since).into(); match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { (Ok(t1), Ok(t2)) => t1.as_secs() <= t2.as_secs(), _ => false, } } else { false }; let mut res = HttpResponse::build(self.status_code); let ct = if self.flags.contains(Flags::PREFER_UTF8) { equiv_utf8_text(self.content_type.clone()) } else { self.content_type }; res.insert_header((header::CONTENT_TYPE, ct.to_string())); if self.flags.contains(Flags::CONTENT_DISPOSITION) { res.insert_header(( header::CONTENT_DISPOSITION, self.content_disposition.to_string(), )); } if let Some(current_encoding) = self.encoding { res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str())); } if let Some(lm) = last_modified { res.insert_header((header::LAST_MODIFIED, lm.to_string())); } if let Some(etag) = etag { res.insert_header((header::ETAG, etag.to_string())); } res.insert_header((header::ACCEPT_RANGES, "bytes")); let mut length = self.md.len(); let mut offset = 0; // check for range header if let Some(ranges) = req.headers().get(header::RANGE) { if let Ok(ranges_header) = ranges.to_str() { if let Some(range) = HttpRange::parse(ranges_header, length) .ok() .and_then(|ranges| ranges.first().copied()) { length = range.length; offset = range.start; // When a Content-Encoding header is present in a 206 partial content response // for video content, it prevents browser video players from starting playback // before loading the whole video and also prevents seeking. // // See: https://github.com/actix/actix-web/issues/2815 // // The assumption of this fix is that the video player knows to not send an // Accept-Encoding header for this request and that downstream middleware will // not attempt compression for requests without it. // // TODO: Solve question around what to do if self.encoding is set and partial // range is requested. Reject request? Ignoring self.encoding seems wrong, too. // In practice, it should not come up. if req.headers().contains_key(&header::ACCEPT_ENCODING) { // don't allow compression middleware to modify partial content res.insert_header(( header::CONTENT_ENCODING, HeaderValue::from_static("identity"), )); } res.insert_header(( header::CONTENT_RANGE, format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()), )); } else { res.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length))); return res.status(StatusCode::RANGE_NOT_SATISFIABLE).finish(); }; } else { return res.status(StatusCode::BAD_REQUEST).finish(); }; }; if precondition_failed { return res.status(StatusCode::PRECONDITION_FAILED).finish(); } else if not_modified { return res .status(StatusCode::NOT_MODIFIED) .body(body::None::new()) .map_into_boxed_body(); } let reader = chunked::new_chunked_read(length, offset, self.file, self.read_mode_threshold); if offset != 0 || length != self.md.len() { res.status(StatusCode::PARTIAL_CONTENT); } res.body(SizedStream::new(length, reader)) } } /// Returns true if `req` has no `If-Match` header or one which matches `etag`. fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { match req.get_header::() { None | Some(header::IfMatch::Any) => true, Some(header::IfMatch::Items(ref items)) => { if let Some(some_etag) = etag { for item in items { if item.strong_eq(some_etag) { return true; } } } false } } } /// Returns true if `req` doesn't have an `If-None-Match` header matching `req`. fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { match req.get_header::() { Some(header::IfNoneMatch::Any) => false, Some(header::IfNoneMatch::Items(ref items)) => { if let Some(some_etag) = etag { for item in items { if item.weak_eq(some_etag) { return false; } } } true } None => true, } } impl Responder for NamedFile { type Body = BoxBody; fn respond_to(self, req: &HttpRequest) -> HttpResponse { self.into_response(req) } } impl ServiceFactory for NamedFile { type Response = ServiceResponse; type Error = Error; type Config = (); type Service = NamedFileService; type InitError = (); type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { let service = NamedFileService { path: self.path.clone(), }; Box::pin(async move { Ok(service) }) } } #[doc(hidden)] #[derive(Debug)] pub struct NamedFileService { path: PathBuf, } impl Service for NamedFileService { type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; dev::always_ready!(); fn call(&self, req: ServiceRequest) -> Self::Future { let (req, _) = req.into_parts(); let path = self.path.clone(); Box::pin(async move { let file = NamedFile::open_async(path).await?; let res = file.into_response(&req); Ok(ServiceResponse::new(req, res)) }) } } impl HttpServiceFactory for NamedFile { fn register(self, config: &mut AppService) { config.register_service( ResourceDef::root_prefix(self.path.to_string_lossy().as_ref()), None, self, None, ) } } actix-files-0.6.10/src/path_buf.rs000064400000000000000000000164261046102023000150560ustar 00000000000000use std::{ path::{Component, Path, PathBuf}, str::FromStr, }; use actix_utils::future::{ready, Ready}; use actix_web::{dev::Payload, FromRequest, HttpRequest}; use crate::error::UriSegmentError; /// Secure Path Traversal Guard /// /// This struct parses a request-uri [`PathBuf`](std::path::PathBuf) #[derive(Debug, PartialEq, Eq)] pub struct PathBufWrap(PathBuf); impl FromStr for PathBufWrap { type Err = UriSegmentError; fn from_str(path: &str) -> Result { Self::parse_path(path, false) } } impl PathBufWrap { /// Parse a safe path from the unprocessed tail of a supplied /// [`HttpRequest`](actix_web::HttpRequest), given the choice of allowing hidden files to be /// considered valid segments. /// /// This uses [`HttpRequest::match_info`](actix_web::HttpRequest::match_info) and /// [`Path::unprocessed`](actix_web::dev::Path::unprocessed), which returns the part of the /// path not matched by route patterns. This is useful for mounted services (eg. `Files`), /// where only the tail should be parsed. /// /// Path traversal is guarded by this method. #[inline] pub fn parse_unprocessed_req( req: &HttpRequest, hidden_files: bool, ) -> Result { Self::parse_path(req.match_info().unprocessed(), hidden_files) } /// Parse a safe path from the full request path of a supplied /// [`HttpRequest`](actix_web::HttpRequest), given the choice of allowing hidden files to be /// considered valid segments. /// /// This uses [`HttpRequest::path`](actix_web::HttpRequest::path), and is more appropriate /// for non-mounted handlers that want the entire request path. /// /// Path traversal is guarded by this method. #[inline] pub fn parse_req_path(req: &HttpRequest, hidden_files: bool) -> Result { Self::parse_path(req.path(), hidden_files) } /// Parse a path, giving the choice of allowing hidden files to be considered valid segments. /// /// Path traversal is guarded by this method. pub fn parse_path(path: &str, hidden_files: bool) -> Result { let mut buf = PathBuf::new(); // equivalent to `path.split('/').count()` let mut segment_count = path.matches('/').count() + 1; // we can decode the whole path here (instead of per-segment decoding) // because we will reject `%2F` in paths using `segment_count`. let path = percent_encoding::percent_decode_str(path) .decode_utf8() .map_err(|_| UriSegmentError::NotValidUtf8)?; // disallow decoding `%2F` into `/` if segment_count != path.matches('/').count() + 1 { return Err(UriSegmentError::BadChar('/')); } for segment in path.split('/') { if segment == ".." { segment_count -= 1; buf.pop(); } else if !hidden_files && segment.starts_with('.') { return Err(UriSegmentError::BadStart('.')); } else if segment.starts_with('*') { return Err(UriSegmentError::BadStart('*')); } else if segment.ends_with(':') { return Err(UriSegmentError::BadEnd(':')); } else if segment.ends_with('>') { return Err(UriSegmentError::BadEnd('>')); } else if segment.ends_with('<') { return Err(UriSegmentError::BadEnd('<')); } else if segment.is_empty() { segment_count -= 1; continue; } else if cfg!(windows) && segment.contains('\\') { return Err(UriSegmentError::BadChar('\\')); } else if cfg!(windows) && segment.contains(':') { return Err(UriSegmentError::BadChar(':')); } else { buf.push(segment) } } // make sure we agree with stdlib parser for (i, component) in buf.components().enumerate() { assert!( matches!(component, Component::Normal(_)), "component `{:?}` is not normal", component ); assert!(i < segment_count); } Ok(PathBufWrap(buf)) } } impl AsRef for PathBufWrap { fn as_ref(&self) -> &Path { self.0.as_ref() } } impl FromRequest for PathBufWrap { type Error = UriSegmentError; type Future = Ready>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { // Uses the unprocessed tail of the request path and disallows hidden files. ready(req.match_info().unprocessed().parse()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_path_buf() { assert_eq!( PathBufWrap::from_str("/test/.tt").map(|t| t.0), Err(UriSegmentError::BadStart('.')) ); assert_eq!( PathBufWrap::from_str("/test/*tt").map(|t| t.0), Err(UriSegmentError::BadStart('*')) ); assert_eq!( PathBufWrap::from_str("/test/tt:").map(|t| t.0), Err(UriSegmentError::BadEnd(':')) ); assert_eq!( PathBufWrap::from_str("/test/tt<").map(|t| t.0), Err(UriSegmentError::BadEnd('<')) ); assert_eq!( PathBufWrap::from_str("/test/tt>").map(|t| t.0), Err(UriSegmentError::BadEnd('>')) ); assert_eq!( PathBufWrap::from_str("/seg1/seg2/").unwrap().0, PathBuf::from_iter(vec!["seg1", "seg2"]) ); assert_eq!( PathBufWrap::from_str("/seg1/../seg2/").unwrap().0, PathBuf::from_iter(vec!["seg2"]) ); } #[test] fn test_parse_path() { assert_eq!( PathBufWrap::parse_path("/test/.tt", false).map(|t| t.0), Err(UriSegmentError::BadStart('.')) ); assert_eq!( PathBufWrap::parse_path("/test/.tt", true).unwrap().0, PathBuf::from_iter(vec!["test", ".tt"]) ); } #[test] fn path_traversal() { assert_eq!( PathBufWrap::parse_path("/../README.md", false).unwrap().0, PathBuf::from_iter(vec!["README.md"]) ); assert_eq!( PathBufWrap::parse_path("/../README.md", true).unwrap().0, PathBuf::from_iter(vec!["README.md"]) ); assert_eq!( PathBufWrap::parse_path("/../../../../../../../../../../etc/passwd", false) .unwrap() .0, PathBuf::from_iter(vec!["etc/passwd"]) ); } #[test] #[cfg_attr(windows, should_panic)] fn windows_drive_traversal() { // detect issues in windows that could lead to path traversal // see for HttpRangeParseError { fn from(err: http_range::HttpRangeParseError) -> Self { match err { http_range::HttpRangeParseError::InvalidRange => Self::InvalidRange, http_range::HttpRangeParseError::NoOverlap => Self::NoOverlap, } } } #[derive(Debug, Clone, Error)] #[non_exhaustive] pub struct ParseRangeErr(#[error(not(source))] HttpRangeParseError); impl fmt::Display for ParseRangeErr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("invalid Range header: ")?; f.write_str(match self.0 { HttpRangeParseError::InvalidRange => "invalid syntax", HttpRangeParseError::NoOverlap => "range starts after end of content", }) } } /// HTTP Range header representation. #[derive(Debug, Clone, Copy)] pub struct HttpRange { /// Start of range. pub start: u64, /// Length of range. pub length: u64, } impl HttpRange { /// Parses Range HTTP header string as per RFC 2616. /// /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). /// `size` is full size of response (file). pub fn parse(header: &str, size: u64) -> Result, ParseRangeErr> { let ranges = http_range::HttpRange::parse(header, size).map_err(|err| ParseRangeErr(err.into()))?; Ok(ranges .iter() .map(|range| HttpRange { start: range.start, length: range.length, }) .collect()) } } #[cfg(test)] mod tests { use super::*; struct T(&'static str, u64, Vec); #[test] fn test_parse() { let tests = vec![ T("", 0, vec![]), T("", 1000, vec![]), T("foo", 0, vec![]), T("bytes=", 0, vec![]), T("bytes=7", 10, vec![]), T("bytes= 7 ", 10, vec![]), T("bytes=1-", 0, vec![]), T("bytes=5-4", 10, vec![]), T("bytes=0-2,5-4", 10, vec![]), T("bytes=2-5,4-3", 10, vec![]), T("bytes=--5,4--3", 10, vec![]), T("bytes=A-", 10, vec![]), T("bytes=A- ", 10, vec![]), T("bytes=A-Z", 10, vec![]), T("bytes= -Z", 10, vec![]), T("bytes=5-Z", 10, vec![]), T("bytes=Ran-dom, garbage", 10, vec![]), T("bytes=0x01-0x02", 10, vec![]), T("bytes= ", 10, vec![]), T("bytes= , , , ", 10, vec![]), T( "bytes=0-9", 10, vec![HttpRange { start: 0, length: 10, }], ), T( "bytes=0-", 10, vec![HttpRange { start: 0, length: 10, }], ), T( "bytes=5-", 10, vec![HttpRange { start: 5, length: 5, }], ), T( "bytes=0-20", 10, vec![HttpRange { start: 0, length: 10, }], ), T( "bytes=15-,0-5", 10, vec![HttpRange { start: 0, length: 6, }], ), T( "bytes=1-2,5-", 10, vec![ HttpRange { start: 1, length: 2, }, HttpRange { start: 5, length: 5, }, ], ), T( "bytes=-2 , 7-", 11, vec![ HttpRange { start: 9, length: 2, }, HttpRange { start: 7, length: 4, }, ], ), T( "bytes=0-0 ,2-2, 7-", 11, vec![ HttpRange { start: 0, length: 1, }, HttpRange { start: 2, length: 1, }, HttpRange { start: 7, length: 4, }, ], ), T( "bytes=-5", 10, vec![HttpRange { start: 5, length: 5, }], ), T( "bytes=-15", 10, vec![HttpRange { start: 0, length: 10, }], ), T( "bytes=0-499", 10000, vec![HttpRange { start: 0, length: 500, }], ), T( "bytes=500-999", 10000, vec![HttpRange { start: 500, length: 500, }], ), T( "bytes=-500", 10000, vec![HttpRange { start: 9500, length: 500, }], ), T( "bytes=9500-", 10000, vec![HttpRange { start: 9500, length: 500, }], ), T( "bytes=0-0,-1", 10000, vec![ HttpRange { start: 0, length: 1, }, HttpRange { start: 9999, length: 1, }, ], ), T( "bytes=500-600,601-999", 10000, vec![ HttpRange { start: 500, length: 101, }, HttpRange { start: 601, length: 399, }, ], ), T( "bytes=500-700,601-999", 10000, vec![ HttpRange { start: 500, length: 201, }, HttpRange { start: 601, length: 399, }, ], ), // Match Apache laxity: T( "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", 11, vec![ HttpRange { start: 1, length: 2, }, HttpRange { start: 4, length: 2, }, HttpRange { start: 7, length: 2, }, ], ), ]; for t in tests { let header = t.0; let size = t.1; let expected = t.2; let res = HttpRange::parse(header, size); if let Err(err) = res { if expected.is_empty() { continue; } else { panic!("parse({header}, {size}) returned error {err:?}"); } } let got = res.unwrap(); if got.len() != expected.len() { panic!( "len(parseRange({}, {})) = {}, want {}", header, size, got.len(), expected.len() ); } for i in 0..expected.len() { if got[i].start != expected[i].start { panic!( "parseRange({}, {})[{}].start = {}, want {}", header, size, i, got[i].start, expected[i].start ) } if got[i].length != expected[i].length { panic!( "parseRange({}, {})[{}].length = {}, want {}", header, size, i, got[i].length, expected[i].length ) } } } } } actix-files-0.6.10/src/service.rs000064400000000000000000000143511046102023000147210ustar 00000000000000use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc}; use actix_web::{ body::BoxBody, dev::{self, Service, ServiceRequest, ServiceResponse}, error::Error, guard::Guard, http::{header, Method}, HttpResponse, }; use futures_core::future::LocalBoxFuture; use crate::{ named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile, PathBufWrap, PathFilter, }; /// Assembled file serving service. #[derive(Clone)] pub struct FilesService(pub(crate) Rc); impl Deref for FilesService { type Target = FilesServiceInner; fn deref(&self) -> &Self::Target { &self.0 } } pub struct FilesServiceInner { pub(crate) directory: PathBuf, pub(crate) index: Option, pub(crate) show_index: bool, pub(crate) redirect_to_slash: bool, pub(crate) default: Option, pub(crate) renderer: Rc, pub(crate) mime_override: Option>, pub(crate) path_filter: Option>, pub(crate) file_flags: named::Flags, pub(crate) guards: Option>, pub(crate) hidden_files: bool, pub(crate) size_threshold: u64, pub(crate) with_permanent_redirect: bool, } impl fmt::Debug for FilesServiceInner { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("FilesServiceInner") } } impl FilesService { async fn handle_err( &self, err: io::Error, req: ServiceRequest, ) -> Result { log::debug!("error handling {}: {}", req.path(), err); if let Some(ref default) = self.default { default.call(req).await } else { Ok(req.error_response(err)) } } fn serve_named_file(&self, req: ServiceRequest, mut named_file: NamedFile) -> ServiceResponse { if let Some(ref mime_override) = self.mime_override { let new_disposition = mime_override(&named_file.content_type.type_()); named_file.content_disposition.disposition = new_disposition; } named_file.flags = self.file_flags; let (req, _) = req.into_parts(); let res = named_file .read_mode_threshold(self.size_threshold) .into_response(&req); ServiceResponse::new(req, res) } fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse { let dir = Directory::new(self.directory.clone(), path); let (req, _) = req.into_parts(); (self.renderer)(&dir, &req).unwrap_or_else(|err| ServiceResponse::from_err(err, req)) } } impl fmt::Debug for FilesService { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("FilesService") } } impl Service for FilesService { type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; dev::always_ready!(); fn call(&self, req: ServiceRequest) -> Self::Future { let is_method_valid = if let Some(guard) = &self.guards { // execute user defined guards (**guard).check(&req.guard_ctx()) } else { // default behavior matches!(*req.method(), Method::HEAD | Method::GET) }; let this = self.clone(); Box::pin(async move { if !is_method_valid { return Ok(req.into_response( HttpResponse::MethodNotAllowed() .insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8)) .body("Request did not meet this resource's requirements."), )); } let path_on_disk = match PathBufWrap::parse_path(req.match_info().unprocessed(), this.hidden_files) { Ok(item) => item, Err(err) => return Ok(req.error_response(err)), }; if let Some(filter) = &this.path_filter { if !filter(path_on_disk.as_ref(), req.head()) { if let Some(ref default) = this.default { return default.call(req).await; } else { return Ok(req.into_response(HttpResponse::NotFound().finish())); } } } // full file path let path = this.directory.join(&path_on_disk); if let Err(err) = path.canonicalize() { return this.handle_err(err, req).await; } if path.is_dir() { if this.redirect_to_slash && !req.path().ends_with('/') && (this.index.is_some() || this.show_index) { let redirect_to = format!("{}/", req.path()); let response = if this.with_permanent_redirect { HttpResponse::PermanentRedirect() } else { HttpResponse::TemporaryRedirect() } .insert_header((header::LOCATION, redirect_to)) .finish(); return Ok(req.into_response(response)); } match this.index { Some(ref index) => { let named_path = path.join(index); match NamedFile::open_async(named_path).await { Ok(named_file) => Ok(this.serve_named_file(req, named_file)), Err(_) if this.show_index => Ok(this.show_index(req, path)), Err(err) => this.handle_err(err, req).await, } } None if this.show_index => Ok(this.show_index(req, path)), None => Ok(ServiceResponse::from_err( FilesError::IsDirectory, req.into_parts().0, )), } } else { match NamedFile::open_async(&path).await { Ok(named_file) => Ok(this.serve_named_file(req, named_file)), Err(err) => this.handle_err(err, req).await, } } }) } } actix-files-0.6.10/tests/encoding.rs000064400000000000000000000042151046102023000154200ustar 00000000000000use actix_files::{Files, NamedFile}; use actix_web::{ http::{ header::{self, HeaderValue}, StatusCode, }, test::{self, TestRequest}, web, App, }; #[actix_web::test] async fn test_utf8_file_contents() { // use default ISO-8859-1 encoding let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await; let req = TestRequest::with_uri("/utf8.txt").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(header::CONTENT_TYPE), Some(&HeaderValue::from_static("text/plain; charset=utf-8")), ); // disable UTF-8 attribute let srv = test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(false))).await; let req = TestRequest::with_uri("/utf8.txt").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(header::CONTENT_TYPE), Some(&HeaderValue::from_static("text/plain")), ); } #[actix_web::test] async fn partial_range_response_encoding() { let srv = test::init_service(App::new().default_service(web::to(|| async { NamedFile::open_async("./tests/test.binary").await.unwrap() }))) .await; // range request without accept-encoding returns no content-encoding header let req = TestRequest::with_uri("/") .append_header((header::RANGE, "bytes=10-20")) .to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT); assert!(!res.headers().contains_key(header::CONTENT_ENCODING)); // range request with accept-encoding returns a content-encoding header let req = TestRequest::with_uri("/") .append_header((header::RANGE, "bytes=10-20")) .append_header((header::ACCEPT_ENCODING, "identity")) .to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT); assert_eq!( res.headers().get(header::CONTENT_ENCODING).unwrap(), "identity" ); } actix-files-0.6.10/tests/fixtures/guards/first/index.txt000064400000000000000000000000051046102023000214120ustar 00000000000000firstactix-files-0.6.10/tests/fixtures/guards/second/index.txt000064400000000000000000000000061046102023000215370ustar 00000000000000secondactix-files-0.6.10/tests/guard.rs000064400000000000000000000020061046102023000147300ustar 00000000000000use actix_files::Files; use actix_web::{ guard::Host, http::StatusCode, test::{self, TestRequest}, App, }; use bytes::Bytes; #[actix_web::test] async fn test_guard_filter() { let srv = test::init_service( App::new() .service(Files::new("/", "./tests/fixtures/guards/first").guard(Host("first.com"))) .service(Files::new("/", "./tests/fixtures/guards/second").guard(Host("second.com"))), ) .await; let req = TestRequest::with_uri("/index.txt") .append_header(("Host", "first.com")) .to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); assert_eq!(test::read_body(res).await, Bytes::from("first")); let req = TestRequest::with_uri("/index.txt") .append_header(("Host", "second.com")) .to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); assert_eq!(test::read_body(res).await, Bytes::from("second")); } actix-files-0.6.10/tests/symlink-test.png000064400000000000000000000002501046102023000164300ustar 00000000000000PNG  IHDRZ pHYs  tIME .7DziiTXtCommentCreated with GIMPd.eIDAT8c?0yTQ͔k%!h[IENDB`actix-files-0.6.10/tests/test space.binary000064400000000000000000000001441046102023000165220ustar 00000000000000TǑɂV2vI\R˙evD:藽RVYp;Gp!2C. pA !ߦx j+UcXc%;"yAIactix-files-0.6.10/tests/test.binary000064400000000000000000000001441046102023000154460ustar 00000000000000TǑɂV2vI\R˙evD:藽RVYp;Gp!2C. pA !ߦx j+UcXc%;"yAIactix-files-0.6.10/tests/test.js000064400000000000000000000000271046102023000145760ustar 00000000000000// this file is empty. actix-files-0.6.10/tests/test.png000064400000000000000000000002501046102023000147440ustar 00000000000000PNG  IHDRZ pHYs  tIME .7DziiTXtCommentCreated with GIMPd.eIDAT8c?0yTQ͔k%!h[IENDB`actix-files-0.6.10/tests/traversal.rs000064400000000000000000000016101046102023000156310ustar 00000000000000use actix_files::Files; use actix_web::{ http::StatusCode, test::{self, TestRequest}, App, }; #[actix_rt::test] async fn test_directory_traversal_prevention() { let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await; let req = TestRequest::with_uri("/../../../../../../../../../../../etc/passwd").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::NOT_FOUND); let req = TestRequest::with_uri( "/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd", ) .to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::NOT_FOUND); let req = TestRequest::with_uri("/%00/etc/passwd%00").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::NOT_FOUND); } actix-files-0.6.10/tests/utf8.txt000064400000000000000000000000541046102023000147100ustar 00000000000000中文内容显示正确。 English is OK.