From b7965493e15b35b2ff319207049186ef2e3f9886 Mon Sep 17 00:00:00 2001 From: Eduardo Trujillo <ed@chromabits.com> Date: Tue, 8 Sep 2020 00:34:31 -0700 Subject: [PATCH] Initial commit for Rust rewrite --- .editorconfig | 5 + .gitignore | 5 +- .gitlab-ci.yml | 16 - Cargo.lock | 2963 ++++++++++++++++++++++++++++++++ Cargo.toml | 40 + Dockerfile | 22 - LICENSE | 4 +- README.md | 52 +- Setup.hs | 2 - config.sample.toml | 14 + config.sample.yaml | 145 -- espresso.cabal | 50 - package.yaml | 39 - rustfmt.toml | 1 + src/Espresso/Config.hs | 68 - src/Main.hs | 294 ---- src/bundle/mod.rs | 215 +++ src/bundle/poller/local_dir.rs | 41 + src/bundle/poller/mod.rs | 39 + src/bundle/poller/s3.rs | 159 ++ src/bundle/rundir.rs | 270 +++ src/config.rs | 113 ++ src/files/LICENSE | 25 + src/files/README.md | 4 + src/files/chunked.rs | 98 ++ src/files/error.rs | 27 + src/files/mime_ext.rs | 26 + src/files/mod.rs | 1214 +++++++++++++ src/files/named.rs | 437 +++++ src/files/pathbuf.rs | 113 ++ src/lib.rs | 9 + src/main.rs | 157 ++ src/monitor.rs | 140 ++ src/server.rs | 69 + src/stats.rs | 76 + src/thread.rs | 165 ++ stack.yaml | 70 - stack.yaml.lock | 12 - tests/test space.binary | 1 + tests/test.binary | 1 + tests/test.png | Bin 0 -> 67390 bytes 41 files changed, 6448 insertions(+), 753 deletions(-) create mode 100644 .editorconfig delete mode 100644 .gitlab-ci.yml create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 Dockerfile delete mode 100644 Setup.hs create mode 100644 config.sample.toml delete mode 100644 config.sample.yaml delete mode 100644 espresso.cabal delete mode 100644 package.yaml create mode 100644 rustfmt.toml delete mode 100644 src/Espresso/Config.hs delete mode 100644 src/Main.hs create mode 100644 src/bundle/mod.rs create mode 100644 src/bundle/poller/local_dir.rs create mode 100644 src/bundle/poller/mod.rs create mode 100644 src/bundle/poller/s3.rs create mode 100644 src/bundle/rundir.rs create mode 100644 src/config.rs create mode 100644 src/files/LICENSE create mode 100644 src/files/README.md create mode 100644 src/files/chunked.rs create mode 100644 src/files/error.rs create mode 100644 src/files/mime_ext.rs create mode 100644 src/files/mod.rs create mode 100644 src/files/named.rs create mode 100644 src/files/pathbuf.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/monitor.rs create mode 100644 src/server.rs create mode 100644 src/stats.rs create mode 100644 src/thread.rs delete mode 100644 stack.yaml delete mode 100644 stack.yaml.lock create mode 100644 tests/test space.binary create mode 100644 tests/test.binary create mode 100644 tests/test.png diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f11c0f7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*.rs] +indent_style = space +indent_size = 4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9006621..db38427 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -.stack-work -config.yaml -run +/target +config.toml \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index af0ffab..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,16 +0,0 @@ -image: fpco/stack-build-small:lts-14.17 - -stages: - - build - -variables: - STACK_ROOT: "${CI_PROJECT_DIR}/.stack-root" - -build: - cache: - key: "$CI_JOB_NAME" - paths: - - .stack-work/ - - .stack-root/ - script: - - stack --no-terminal build diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d9f8f39 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2963 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "actix" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4af87564ff659dee8f9981540cac9418c45e910c8072fdedd643a262a38fcaf" +dependencies = [ + "actix-http", + "actix-rt", + "actix_derive", + "bitflags", + "bytes", + "crossbeam-channel", + "derive_more", + "futures", + "lazy_static", + "log", + "parking_lot 0.10.2", + "pin-project", + "smallvec", + "tokio", + "tokio-util 0.2.0", + "trust-dns-proto", + "trust-dns-resolver", +] + +[[package]] +name = "actix-codec" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "log", + "tokio", + "tokio-util 0.2.0", +] + +[[package]] +name = "actix-connect" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c95cc9569221e9802bf4c377f6c18b90ef10227d787611decf79fd47d2a8e76c" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "derive_more", + "either", + "futures", + "http", + "log", + "trust-dns-proto", + "trust-dns-resolver", +] + +[[package]] +name = "actix-files" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193b22cb1f7b4ff12a4eb2415d6d19e47e44ea93e05930b30d05375ea29d3529" +dependencies = [ + "actix-http", + "actix-service", + "actix-web", + "bitflags", + "bytes", + "derive_more", + "futures-core", + "futures-util", + "log", + "mime", + "mime_guess", + "percent-encoding", + "v_htmlescape 0.4.5", +] + +[[package]] +name = "actix-http" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c16664cc4fdea8030837ad5a845eb231fb93fc3c5c171edfefb52fad92ce9019" +dependencies = [ + "actix-codec", + "actix-connect", + "actix-rt", + "actix-service", + "actix-threadpool", + "actix-utils", + "base64 0.11.0", + "bitflags", + "brotli2", + "bytes", + "chrono", + "copyless", + "derive_more", + "either", + "encoding_rs", + "failure", + "flate2", + "futures-channel", + "futures-core", + "futures-util", + "fxhash", + "h2", + "http", + "httparse", + "indexmap", + "language-tags", + "lazy_static", + "log", + "mime", + "percent-encoding", + "pin-project", + "rand", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "sha1", + "slab", + "time 0.1.43", +] + +[[package]] +name = "actix-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a60f9ba7c4e6df97f3aacb14bb5c0cd7d98a49dcbaed0d7f292912ad9a6a3ed2" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d7a10ca4d94e8c8e7a87c5173aba1b97ba9a6563ca02b0e1cd23531093d3ec8" +dependencies = [ + "bytestring", + "http", + "log", + "regex", + "serde", +] + +[[package]] +name = "actix-rt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143fcc2912e0d1de2bcf4e2f720d2a60c28652ab4179685a1ee159e0fb3db227" +dependencies = [ + "actix-macros", + "actix-threadpool", + "copyless", + "futures-channel", + "futures-util", + "smallvec", + "tokio", +] + +[[package]] +name = "actix-server" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d74b464215a473c973a2d7d03a69cc10f4ce1f4b38a7659c5193dc5c675630" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "futures-channel", + "futures-util", + "log", + "mio", + "mio-uds", + "num_cpus", + "slab", + "socket2", +] + +[[package]] +name = "actix-service" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0052435d581b5be835d11f4eb3bce417c8af18d87ddf8ace99f8e67e595882bb" +dependencies = [ + "futures-util", + "pin-project", +] + +[[package]] +name = "actix-testing" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47239ca38799ab74ee6a8a94d1ce857014b2ac36f242f70f3f75a66f691e791c" +dependencies = [ + "actix-macros", + "actix-rt", + "actix-server", + "actix-service", + "log", + "socket2", +] + +[[package]] +name = "actix-threadpool" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d209f04d002854b9afd3743032a27b066158817965bf5d036824d19ac2cc0e30" +dependencies = [ + "derive_more", + "futures-channel", + "lazy_static", + "log", + "num_cpus", + "parking_lot 0.11.0", + "threadpool", +] + +[[package]] +name = "actix-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e5b4faaf105e9a6d389c606c298dcdb033061b00d532af9df56ff3a54995a8" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "derive_more", + "either", + "futures", + "log", +] + +[[package]] +name = "actix-utils" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf8f5631bf01adec2267808f00e228b761c60c0584cc9fa0b5364f41d147f4e" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "bitflags", + "bytes", + "either", + "futures", + "log", + "pin-project", + "slab", +] + +[[package]] +name = "actix-web" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3158e822461040822f0dbf1735b9c2ce1f95f93b651d7a7aded00b1efbb1f635" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-testing", + "actix-threadpool", + "actix-tls", + "actix-utils", + "actix-web-codegen", + "awc", + "bytes", + "derive_more", + "encoding_rs", + "futures", + "fxhash", + "log", + "mime", + "net2", + "pin-project", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "time 0.1.43", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a71bf475cbe07281d0b3696abb48212db118e7e23219f13596ce865235ff5766" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "actix_derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95aceadaf327f18f0df5962fedc1bde2f870566a0b9f65c89508a3b1f79334c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "addr2line" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" + +[[package]] +name = "aho-corasick" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" +dependencies = [ + "memchr", +] + +[[package]] +name = "arc-swap" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" + +[[package]] +name = "async-channel" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21279cfaa4f47df10b1816007e738ca3747ef2ee53ffc51cdbf57a8bb266fee3" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-compat" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316ce79a7185ddb5cbb692bc5e992e3bbdb68a00382fa0b0ee248f05c16ecd7" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-executor" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f47c78ea98277cb1f5e6f60ba4fc762f5eafe9f6511bc2f7dfd8b75c225650" +dependencies = [ + "async-io", + "futures-lite", + "multitask", + "parking 1.0.6", + "scoped-tls", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae22a338d28c75b53702b66f77979062cb29675db376d99e451af4fa79dedb3" +dependencies = [ + "cfg-if", + "concurrent-queue", + "futures-lite", + "libc", + "once_cell", + "parking 2.0.0", + "polling", + "socket2", + "vec-arena", + "wepoll-sys-stjepang", + "winapi 0.3.9", +] + +[[package]] +name = "async-mutex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66941c2577c4fa351e4ce5fdde8f86c69b88d623f3b955be1bc7362a23434632" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-std" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c8da367da62b8ff2313c406c9ac091c1b31d67a165becdd2de380d846260f7" +dependencies = [ + "async-executor", + "async-io", + "async-mutex", + "async-task", + "blocking", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "futures-timer", + "kv-log-macro", + "log", + "memchr", + "num_cpus", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-tar" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb619eae01ab289095debb1ff7c02710d5124c20edde1b2eca926572a34c3998" +dependencies = [ + "async-std", + "filetime", + "libc", + "pin-project", + "redox_syscall", + "xattr", +] + +[[package]] +name = "async-task" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17772156ef2829aadc587461c7753af20b7e8db1529bc66855add962a3b35d3" + +[[package]] +name = "async-trait" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687c230d85c0a52504709705fc8a53e4a692b83a2184f03dae73e38e1e93a783" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "awc" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7601d4d1d7ef2335d6597a41b5fe069f6ab799b85f53565ab390e7b7065aac5" +dependencies = [ + "actix-codec", + "actix-http", + "actix-rt", + "actix-service", + "base64 0.11.0", + "bytes", + "derive_more", + "futures-core", + "log", + "mime", + "percent-encoding", + "rand", + "serde", + "serde_json", + "serde_urlencoded", +] + +[[package]] +name = "backtrace" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base-x" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b20b618342cf9891c292c4f5ac2cde7287cc5c87e87e9c769d617793607dec1" + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "blake2b_simd" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5800d29218fea137b0880387e5948694a23c93fcdde157006966693a865c7c" +dependencies = [ + "async-channel", + "atomic-waker", + "futures-lite", + "once_cell", + "waker-fn", +] + +[[package]] +name = "brotli-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "brotli2" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e" +dependencies = [ + "brotli-sys", + "libc", +] + +[[package]] +name = "buf-min" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6ae7069aad07c7cdefe6a22a671f00650728bd2331a4cc62e1e5d0becdf9ca4" +dependencies = [ + "bytes", +] + +[[package]] +name = "bumpalo" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytestring" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7c05fa5172da78a62d9949d662d2ac89d4cc7355d7b49adee5163f1fb3f363" +dependencies = [ + "bytes", +] + +[[package]] +name = "cache-padded" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" + +[[package]] +name = "cc" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "chrono" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942f72db697d8767c22d46a598e01f2d3b475501ea43d0db4f16d90259182d0b" +dependencies = [ + "num-integer", + "num-traits", + "serde", + "time 0.1.43", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "cloudabi" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" +dependencies = [ + "bitflags", +] + +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "copyless" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" + +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + +[[package]] +name = "crc32fast" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ee0cc8804d5393478d743b035099520087a5186f3b93fa58cec08fa62407b6" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "derive_more" +version = "0.99.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298998b1cf6b5b2c8a7b023dfd45821825ce3ba8a8af55c921a0e734e4653f76" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if", + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "dtoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" + +[[package]] +name = "either" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f" + +[[package]] +name = "encoding_rs" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ac63f94732332f44fe654443c46f6375d1939684c17b0afb6cb56b0456e171" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-as-inner" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "espresso" +version = "0.1.0" +dependencies = [ + "actix", + "actix-files", + "actix-http", + "actix-rt", + "actix-service", + "actix-web", + "async-compat", + "async-tar", + "async-trait", + "bitflags", + "bytes", + "futures-core", + "futures-util", + "lazy_static", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pretty_env_logger", + "rusoto_core", + "rusoto_credential", + "rusoto_s3", + "serde", + "serde_derive", + "snafu", + "tokio", + "toml", + "v_htmlescape 0.10.0", +] + +[[package]] +name = "event-listener" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cd41440ae7e4734bbd42302f63eaba892afc93a3912dad84006247f0dedb0e" + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "fastrand" +version = "1.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c85295147490b8fcf2ea3d104080a105a8b2c63f9c319e82c02d8e952388919" + +[[package]] +name = "filetime" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed85775dcc68644b5c950ac06a2b23768d3bc9390464151aaf27136998dcf9e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi 0.3.9", +] + +[[package]] +name = "flate2" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c90b0fc46cf89d227cc78b40e494ff81287a92dd07631e5af0d06fe3cf885e" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" + +[[package]] +name = "futures-executor" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" + +[[package]] +name = "futures-lite" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97999970129b808f0ccba93211201d431fcc12d7e1ffae03a61b5cedd1a7ced2" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking 2.0.0", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" + +[[package]] +name = "futures-task" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" +dependencies = [ + "once_cell", +] + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +dependencies = [ + "gloo-timers", + "send_wrapper", +] + +[[package]] +name = "futures-util" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check 0.9.2", +] + +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724" + +[[package]] +name = "gloo-timers" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "h2" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993f9e0baeed60001cf565546b0d3dbe6a6ad23f2bd31644a133c641eccf6d53" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util 0.3.1", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" +dependencies = [ + "autocfg", +] + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac", + "digest", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi 0.3.9", +] + +[[package]] +name = "http" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "httparse" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "hyper" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e68a8dd9716185d9e64ea473ea6ef63529252e3e27623295a0378a19665d5eb" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project", + "socket2", + "time 0.1.43", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-tls", +] + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b45e59b16c76b11bf9738fd5d38879d3bd28ad292d7b313608becb17ae2df9" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b141fdc7836c525d4d594027d318c84161ca17aaf8113ab1f81ab93ae897485" + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipconfig" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" +dependencies = [ + "socket2", + "widestring", + "winapi 0.3.9", + "winreg", +] + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + +[[package]] +name = "js-sys" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a7e2c92a4804dd459b86c339278d0fe87cf93757fae222c3fa3ae75458bc73" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "language-tags" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2f02823cf78b754822df5f7f268fb59822e7296276d3e069d8e8cb26a14bd10" + +[[package]] +name = "linked-hash-map" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" + +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "lock_api" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0f75932c1f6cfae3c04000e40114adf955636e19040f9c0a2c380702aa1c7f" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" +dependencies = [ + "cfg-if", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow 0.2.1", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-named-pipes" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" +dependencies = [ + "log", + "mio", + "miow 0.3.5", + "winapi 0.3.9", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio", +] + +[[package]] +name = "miow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "miow" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b88fb9795d4d36d62a012dfbf49a8f5cf12751f36d31a9dbe66d528e58979e" +dependencies = [ + "socket2", + "winapi 0.3.9", +] + +[[package]] +name = "multitask" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c09c35271e7dcdb5f709779111f2c8e8ab8e06c1b587c1c6a9e179d865aaa5b4" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", +] + +[[package]] +name = "native-tls" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "net2" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ba7c918ac76704fb42afcbbb43891e72731f3dcca3bef2a19786297baf14af7" +dependencies = [ + "cfg-if", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + +[[package]] +name = "num-integer" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5" + +[[package]] +name = "once_cell" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "lazy_static", + "libc", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" + +[[package]] +name = "openssl-sys" +version = "0.9.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb300f271742d4a2a66c01b6b2fa0c83dfebd2e0bf11addb879a3547b4ed87c" + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.7.2", +] + +[[package]] +name = "parking_lot" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733" +dependencies = [ + "instant", + "lock_api 0.4.1", + "parking_lot_core 0.8.0", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +dependencies = [ + "cfg-if", + "cloudabi 0.0.3", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" +dependencies = [ + "cfg-if", + "cloudabi 0.1.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" + +[[package]] +name = "polling" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fffa183f6bd5f1a8a3e1f60ce2f8d5621e350eed84a62d6daaa5b9d1aaf6fbd" +dependencies = [ + "cfg-if", + "libc", + "log", + "wepoll-sys-stjepang", + "winapi 0.3.9", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" + +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598" + +[[package]] +name = "proc-macro-nested" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" + +[[package]] +name = "proc-macro2" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom", + "redox_syscall", + "rust-argon2", +] + +[[package]] +name = "regex" +version = "1.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "resolv-conf" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11834e137f3b14e309437a8276714eed3a80d1ef894869e510f2c0c0b98b9f4a" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "rusoto_core" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e977941ee0658df96fca7291ecc6fc9a754600b21ad84b959eb1dbbc9d5abcc7" +dependencies = [ + "async-trait", + "base64 0.12.3", + "bytes", + "crc32fast", + "futures", + "http", + "hyper", + "hyper-tls", + "lazy_static", + "log", + "md5", + "percent-encoding", + "pin-project", + "rusoto_credential", + "rusoto_signature", + "rustc_version", + "serde", + "serde_json", + "tokio", + "xml-rs", +] + +[[package]] +name = "rusoto_credential" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac05563f83489b19b4d413607a30821ab08bbd9007d14fa05618da3ef09d8b" +dependencies = [ + "async-trait", + "chrono", + "dirs", + "futures", + "hyper", + "pin-project", + "regex", + "serde", + "serde_json", + "shlex", + "tokio", + "zeroize", +] + +[[package]] +name = "rusoto_s3" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1146e37a7c1df56471ea67825fe09bbbd37984b5f6e201d8b2e0be4ee15643d8" +dependencies = [ + "async-trait", + "bytes", + "futures", + "rusoto_core", + "xml-rs", +] + +[[package]] +name = "rusoto_signature" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a740a88dde8ded81b6f2cff9cd5e054a5a2e38a38397260f7acdd2c85d17dd" +dependencies = [ + "base64 0.12.3", + "bytes", + "futures", + "hex", + "hmac", + "http", + "hyper", + "log", + "md5", + "percent-encoding", + "pin-project", + "rusoto_credential", + "rustc_version", + "serde", + "sha2", + "time 0.2.16", + "tokio", +] + +[[package]] +name = "rust-argon2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" +dependencies = [ + "base64 0.12.3", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + +[[package]] +name = "serde" +version = "1.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" +dependencies = [ + "dtoa", + "itoa", + "serde", + "url", +] + +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + +[[package]] +name = "sha2" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2933378ddfeda7ea26f48c555bdad8bb446bf8a3d17832dc83e380d444cfb8c1" +dependencies = [ + "block-buffer", + "cfg-if", + "cpuid-bool", + "digest", + "opaque-debug", +] + +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + +[[package]] +name = "signal-hook-registry" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e12110bc539e657a646068aaf5eb5b63af9d0c1f7b29c97113fad80e15f035" +dependencies = [ + "arc-swap", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "smallvec" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" + +[[package]] +name = "snafu" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5aed652511f5c9123cf2afbe9c244c29db6effa2abb05c866e965c82405ce" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebf8f7d5720104a9df0f7076a8682024e958bba0fe9848767bb44f251f3648e9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "socket2" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi 0.3.9", +] + +[[package]] +name = "standback" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33a71ea1ea5f8747d1af1979bfb7e65c3a025a70609f04ceb78425bc5adad8e6" +dependencies = [ + "version_check 0.9.2", +] + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "subtle" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502d53007c02d7605a05df1c1a73ee436952781653da5d0bf57ad608f66932c1" + +[[package]] +name = "syn" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e69abc24912995b3038597a7a593be5053eb0fb44f3cc5beec0deb421790c1f4" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.9", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "time" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a51cadc5b1eec673a685ff7c33192ff7b7603d0b75446fb354939ee615acb15" +dependencies = [ + "cfg-if", + "libc", + "standback", + "stdweb", + "time-macros", + "version_check 0.9.2", + "winapi 0.3.9", +] + +[[package]] +name = "time-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9b6e9f095bc105e183e3cd493d72579be3181ad4004fceb01adbe9eecab2d" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn", +] + +[[package]] +name = "tinyvec" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed" + +[[package]] +name = "tokio" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d34ca54d84bf2b5b4d7d31e901a8464f7b60ac145a284fba25ceb801f2ddccd" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "iovec", + "lazy_static", + "libc", + "memchr", + "mio", + "mio-named-pipes", + "mio-uds", + "pin-project-lite", + "signal-hook-registry", + "slab", + "tokio-macros", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-macros" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" + +[[package]] +name = "tracing" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d79ca061b032d6ce30c660fded31189ca0b9922bf483cd70759f13a2d86786c" +dependencies = [ + "cfg-if", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db63662723c316b43ca36d833707cc93dff82a02ba3d7e354f342682cc8b3545" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "trust-dns-proto" +version = "0.18.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a7f3a2ab8a919f5eca52a468866a67ed7d3efa265d48a652a9a3452272b413f" +dependencies = [ + "async-trait", + "enum-as-inner", + "failure", + "futures", + "idna", + "lazy_static", + "log", + "rand", + "smallvec", + "socket2", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.18.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f90b1502b226f8b2514c6d5b37bafa8c200d7ca4102d57dc36ee0f3b7a04a2f" +dependencies = [ + "cfg-if", + "failure", + "futures", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "resolv-conf", + "smallvec", + "tokio", + "trust-dns-proto", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check 0.9.2", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "url" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" +dependencies = [ + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "v_escape" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "660b101c07b5d0863deb9e7fb3138777e858d6d2a79f9e6049a27d1cc77c6da6" +dependencies = [ + "v_escape_derive 0.5.6", +] + +[[package]] +name = "v_escape" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2d5ca56f0412d5ad5e642202e5c8fb61b61ad39435a53ed501fbd45380e8d3" +dependencies = [ + "buf-min", + "v_escape_derive 0.8.1", +] + +[[package]] +name = "v_escape_derive" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ca2a14bc3fc5b64d188b087a7d3a927df87b152e941ccfbc66672e20c467ae" +dependencies = [ + "nom", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "v_escape_derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cae7cffca0b1f9af9b20610f6fdeee9ffcce61417b5ad186a5d482dc904e24cd" +dependencies = [ + "nom", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "v_htmlescape" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33e939c0d8cf047514fb6ba7d5aac78bc56677a6938b2ee67000b91f2e97e41" +dependencies = [ + "cfg-if", + "v_escape 0.7.4", +] + +[[package]] +name = "v_htmlescape" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5fd25529cb2f78527b5ee507bcfb357b26d057b5e480853c26d49a4ead5c629" +dependencies = [ + "cfg-if", + "v_escape 0.12.1", +] + +[[package]] +name = "vcpkg" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" + +[[package]] +name = "vec-arena" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb18268690309760d59ee1a9b21132c126ba384f374c59a94db4bc03adeb561" + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasm-bindgen" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0563a9a4b071746dd5aedbc3a28c6fe9be4586fb3fbadb67c400d4f53c6b16c" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc71e4c5efa60fb9e74160e89b93353bc24059999c0ae0fb03affc39770310b0" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95f8d235a77f880bcef268d379810ea6c0af2eacfa90b1ad5af731776e0c4699" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c57cefa5fa80e2ba15641578b44d36e7a64279bc5ed43c6dbaf329457a2ed2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841a6d1c35c6f596ccea1f82504a192a60378f64b3bb0261904ad8f2f5657556" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93b162580e34310e5931c4b792560108b10fd14d64915d7fff8ff00180e70092" + +[[package]] +name = "web-sys" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda38f4e5ca63eda02c059d243aa25b5f35ab98451e518c51612cd0f1bd19a47" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wepoll-sys-stjepang" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd319e971980166b53e17b1026812ad66c6b54063be879eb182342b55284694" +dependencies = [ + "cc", +] + +[[package]] +name = "widestring" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a763e303c0e0f23b0da40888724762e802a8ffefbc22de4127ef42493c2ea68c" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[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-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[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-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + +[[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 = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "xattr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +dependencies = [ + "libc", +] + +[[package]] +name = "xml-rs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a" + +[[package]] +name = "zeroize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..abd4dc2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "espresso" +version = "0.1.0" +authors = ["Eduardo Trujillo <ed@chromabits.com>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix = "0.9.0" +actix-web = "2.0.0" +actix-rt = "1.0" +actix-files = "0.2.2" +bitflags = "1.2.1" +actix-http = "1.0.1" +mime_guess = "2.0.3" +mime = "0.3.16" +futures-util = "0.3.5" +actix-service = "1.0.6" +percent-encoding = "2.1.0" +futures-core = "0.3.5" +v_htmlescape = "0.10.0" +bytes = "0.5.6" +log = "0.4" +snafu = "0.6.8" +serde = "1.0.115" +serde_derive = "1.0.115" +toml = "0.5" +pretty_env_logger = "0.4.0" +rusoto_core = "0.45.0" +rusoto_s3 = "0.45.0" +rusoto_credential = "0.45.0" +async-trait = "0.1.40" +async-tar = "0.3" +async-compat = "0.1.3" +lazy_static = "1.4.0" + +[dependencies.tokio] +version = "0.2" +features = ["stream", "signal", "macros"] diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 88da1d6..0000000 --- a/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -# Build -FROM fpco/stack-build:lts-14.17 as build - -RUN mkdir /opt/build -COPY . /opt/build - -RUN cd /opt/build && stack install --system-ghc - -# Runtime -FROM ubuntu:20.04 - -RUN mkdir -p /opt/espresso -ARG BINARY_PATH -WORKDIR /opt/espresso - -RUN apt-get update && apt-get install -y \ - ca-certificates \ - libgmp-dev - -COPY --from=build /root/.local/bin/espresso . - -CMD ["/opt/espresso/espresso"] \ No newline at end of file diff --git a/LICENSE b/LICENSE index db83523..22642af 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright Eduardo Trujillo (c) 2019 +Copyright Eduardo Trujillo (c) 2019-2020 All rights reserved. @@ -27,4 +27,4 @@ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 47e9bc5..e198e01 100644 --- a/README.md +++ b/README.md @@ -21,41 +21,33 @@ named `bundle.tar.gz` at the root of the bucket. ## Configuration -Espresso supports the following environment variables: - -- **`SITE_STAGE` (Optional):** One of `production`, `staging`, or `development` - (Defaults to `development`). This specifies which set of configurations to - use as part of the `kawaii` package. -- **`SITE_DOMAIN`**: Domain the site will be served from. This will be used to - redirect the user if a different domain is used (Defaults to - `chromabits.com`). -- **`SITE_404_ROUTE`:** Route to redirect a user to if the path requested is - not found (Defaults to `/404`). -- **`MINIO_ACCESS_KEY`:** Access key for the S3-compatible storage backend. -- **`MINIO_SECRET_KEY`:** Secret key for the S3-compatible storage backend. -- **`OBJECT_STORAGE_ENDPOINT`**: Endpoint to the S3-compatible storage backend. -- **`BUCKET_NAME`:** The name of the bucket where the site bundle will be - stored in. - -### Stages - -- **Development:** Listens on port 9090 and has a minimal set of configurations - and middleware enabled. -- **Staging:** Listens on port 8080 and has a set of configuration closer to - the production stage. Enables security middlewares (CSP, Force HTTPS), - as well as, GZIP compression and index-less URLs. -- **Production:** Listens on port 80. Same configuration as the staging stage, - but with the addition of HSTS. +Espresso can be configured via a TOML configuration file. Simply create a +`config.toml` file and place it on the working directory. + +```toml +[server] +address = "127.0.0.1:8088" +run_dir = "run" +auto_cleanup = true + +[stats] +address = "127.0.0.1:8089" + +[bundle] +type = "LocalBundle" +dir = "/tmp/" + +[unbundler] +poll_seconds = 10 +``` ## Customization While Espresso was built specifically for chromabits.com, it is able to serve other static sites as long as their requirements are simple. -One of the main limitations you may encounter is the default set of CSP -policies. These are not configurable over environment variables and require the -server to be recompiled. - ## Development -`stack build --copy-bins` to build the server. +`RUST_LOG=info cargo run` to build and run the server. + +`RUST_LOG=info cargo test` to run all tests. diff --git a/Setup.hs b/Setup.hs deleted file mode 100644 index 9a994af..0000000 --- a/Setup.hs +++ /dev/null @@ -1,2 +0,0 @@ -import Distribution.Simple -main = defaultMain diff --git a/config.sample.toml b/config.sample.toml new file mode 100644 index 0000000..0c762d6 --- /dev/null +++ b/config.sample.toml @@ -0,0 +1,14 @@ +[server] +address = "127.0.0.1:8088" +run_dir = "run" +# auto_cleanup = true + +[stats] +address = "127.0.0.1:8089" + +[bundle] +type = "LocalBundle" +dir = "/tmp/" + +[unbundler] +poll_seconds = 10 \ No newline at end of file diff --git a/config.sample.yaml b/config.sample.yaml deleted file mode 100644 index deca5fa..0000000 --- a/config.sample.yaml +++ /dev/null @@ -1,145 +0,0 @@ -server: - stage: Development - dev: - middleware: - - tag: LoggerMiddleware - - tag: SecurityHeadersMiddleware - - tag: DeindexifyMiddleware - - tag: GzipMiddleware - tlsConfiguration: null - port: 9090 - notFoundHandler: - tag: NotFoundPath - contents: "/404" - staging: - middleware: - - tag: LoggerMiddleware - - tag: ContentSecurityPolicyMiddleware - contents: &csp - - tag: DefaultSrc - contents: - - tag: KeywordSource - contents: Self - # Allowed scripts sources - - tag: ScriptSrc - contents: - - tag: KeywordSource - contents: Self - - tag: KeywordSource - contents: UnsafeInline - - tag: HostSource - scheme: "https" - host: - tag: DomainHostPart - wildcard: false - domain: "use.typekit.net" - - tag: HostSource - scheme: "https" - host: - tag: DomainHostPart - wildcard: false - domain: "gist.github.com" - # Allowed image sources - - tag: ImgSrc - contents: - - tag: KeywordSource - contents: Self - - tag: SchemeSource - contents: "https" - - tag: SchemeSource - contents: "data" - # Allowed font sources - - tag: FontSrc - contents: - - tag: KeywordSource - contents: Self - - tag: SchemeSource - contents: "data" - - tag: HostSource - scheme: "https" - host: - tag: DomainHostPart - wildcard: false - domain: "use.typekit.net" - - tag: HostSource - scheme: "https" - host: - tag: DomainHostPart - wildcard: false - domain: "fonts.typekit.net" - # Allowed style sources - - tag: StyleSrc - contents: - - tag: KeywordSource - contents: Self - - tag: KeywordSource - contents: UnsafeInline - - tag: HostSource - scheme: "https" - host: - tag: DomainHostPart - wildcard: false - domain: "use.typekit.net" - - tag: HostSource - scheme: "https" - host: - tag: DomainHostPart - wildcard: false - domain: "assets-cdn.github.com" - # Allowed frame sources - - tag: FrameSrc - contents: - - tag: HostSource - scheme: "https" - host: - tag: DomainHostPart - wildcard: false - domain: "www.youtube.com" - - tag: SecurityHeadersMiddleware - - tag: DomainMiddleware - contents: "chromabits.com" - - tag: ForceSSLMiddleware - - tag: DeindexifyMiddleware - - tag: GzipMiddleware - tlsConfiguration: null - port: 8080 - notFoundHandler: - tag: NotFoundPath - contents: "/404" - prod: - middleware: - - tag: LoggerMiddleware - - tag: ContentSecurityPolicyMiddleware - contents: *csp - - tag: SecurityHeadersMiddleware - - tag: DomainMiddleware - contents: "chromabits.com" - - tag: ForceSSLMiddleware - - tag: DeindexifyMiddleware - - tag: GzipMiddleware - - tag: StrictTransportSecurityMiddleware - contents: - maxAge: 31536000 - includeSubdomains: true - preload: false - tlsConfiguration: null - port: 80 - notFoundHandler: - tag: NotFoundPath - contents: "/404" - cacheControl: - tag: CacheSeconds - contents: 604801 -# bundle: -# tag: S3Bundle -# contents: -# accessKey: EXAMPLE -# secretKey: EXAMPLE -# endpoint: https://minio -# bucket: example -# pollSeconds: 30 -# objectName: bundle.tar.gz -bundle: - tag: LocalBundle - contents: - path: run/serve diff --git a/espresso.cabal b/espresso.cabal deleted file mode 100644 index 2803992..0000000 --- a/espresso.cabal +++ /dev/null @@ -1,50 +0,0 @@ -cabal-version: 1.12 - --- This file has been generated from package.yaml by hpack version 0.31.2. --- --- see: https://github.com/sol/hpack --- --- hash: 88f6a790ccefae14d57575e829a82a2df917f04afacc2c41ad729913f1e467f1 - -name: espresso -version: 0.1.0.0 -description: A web server for chromabits.com -category: Web -homepage: https://github.com/etcinit/espresso#readme -author: Eduardo Trujillo -maintainer: ed@chromabits.com -copyright: Copyright 2019 Eduardo Trujillo -license: BSD3 -license-file: LICENSE -build-type: Simple -extra-source-files: - README.md - -executable espresso - main-is: Main.hs - other-modules: - Espresso.Config - Paths_espresso - hs-source-dirs: - src - build-depends: - aeson - , base >=4.7 && <5 - , conduit - , conduit-extra - , data-default >=0.7.1.1 - , directory - , filepath - , generic-lens - , kawaii >=0.0.3.0 - , lens >=4.0.0 - , lifted-async - , lifted-base - , minio-hs - , monad-control - , monad-logger - , tar - , text - , transformers-base - , yaml - default-language: Haskell2010 diff --git a/package.yaml b/package.yaml deleted file mode 100644 index 442209d..0000000 --- a/package.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: espresso -version: 0.1.0.0 -#synopsis: -description: A web server for chromabits.com -homepage: https://github.com/etcinit/espresso#readme -license: BSD3 -author: Eduardo Trujillo -maintainer: ed@chromabits.com -copyright: Copyright 2019 Eduardo Trujillo -category: Web -extra-source-files: - - README.md - -dependencies: - - base >= 4.7 && < 5 - - kawaii >= 0.0.3.0 - - lens >= 4.0.0 - - data-default >= 0.7.1.1 - - minio-hs - - conduit - - conduit-extra - - directory - - filepath - - tar - - monad-logger - - text - - lifted-base - - lifted-async - - monad-control - - transformers-base - - aeson - - directory - - generic-lens - - yaml - -executables: - espresso: - source-dirs: src - main: Main.hs diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..47874a2 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +tab_spaces=2 \ No newline at end of file diff --git a/src/Espresso/Config.hs b/src/Espresso/Config.hs deleted file mode 100644 index 3067fa9..0000000 --- a/src/Espresso/Config.hs +++ /dev/null @@ -1,68 +0,0 @@ -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DuplicateRecordFields #-} - -module Espresso.Config where - -import Control.Monad.IO.Class ( MonadIO - , liftIO - ) -import GHC.Generics ( Generic ) - -import Control.Exception.Lifted ( Exception - , throwIO - ) -import Control.Monad.Base ( MonadBase ) -import System.Directory ( getCurrentDirectory ) -import Data.Aeson ( FromJSON ) -import Data.Yaml ( decodeFileEither - , prettyPrintParseException - ) -import Network.Wai.Serve.Types ( StageConfiguration ) -import Data.Text ( Text ) -import qualified Data.Text as T - -data ConfigException = ConfigParseException Text deriving (Show) - -instance Exception ConfigException - -data Config = Config - { server :: StageConfiguration - , bundle :: BundleConfig - } deriving (Generic) - -instance FromJSON Config - -data BundleConfig - = S3Bundle S3BundleConfig - | LocalBundle LocalBundleConfig - deriving (Generic, Show) - -instance FromJSON BundleConfig - -data S3BundleConfig = S3BundleConfig - { accessKey :: Text - , secretKey :: Text - , endpoint :: Text - , bucket :: Text - , objectName :: Text - , pollSeconds :: Int - } deriving (Generic, Show) - -instance FromJSON S3BundleConfig - -data LocalBundleConfig = LocalBundleConfig - { path :: Text - } deriving (Generic, Show) - -instance FromJSON LocalBundleConfig - -loadConfig :: (MonadIO m, MonadBase IO m) => m Config -loadConfig = do - configFile <- (<> "/config.yaml") <$> liftIO getCurrentDirectory - parseResult <- liftIO $ decodeFileEither configFile - - case parseResult of - Left parserError -> throwIO - $ ConfigParseException (T.pack $ prettyPrintParseException parserError) - Right decodedConfig -> pure decodedConfig diff --git a/src/Main.hs b/src/Main.hs deleted file mode 100644 index d00f6c3..0000000 --- a/src/Main.hs +++ /dev/null @@ -1,294 +0,0 @@ -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE OverloadedLabels #-} -{-# LANGUAGE TemplateHaskell #-} - -import qualified Codec.Archive.Tar as Tar - -import Control.Concurrent ( forkIO ) -import Control.Concurrent.Async.Lifted - ( async - , cancel - ) -import Control.Concurrent.Chan.Lifted ( Chan - , newChan - , readChan - , writeChan - ) -import Control.Concurrent.Lifted ( threadDelay ) -import Control.Concurrent.MVar.Lifted ( MVar - , newEmptyMVar - , putMVar - , readMVar - , takeMVar - ) -import Control.Lens -import Control.Monad ( forever ) -import Control.Monad.Base ( MonadBase ) -import Control.Monad.IO.Class ( MonadIO - , liftIO - ) -import Control.Monad.Logger ( MonadLogger - , logError - , logInfo - , runChanLoggingT - , runStdoutLoggingT - , unChanLoggingT - ) -import Control.Monad.Trans.Control ( MonadBaseControl ) -import Data.List.NonEmpty ( NonEmpty((:|)) ) - -import qualified Data.Conduit as C -import qualified Data.Conduit.Binary as CB -import Data.Default ( def ) -import Data.Generics.Labels ( ) -import Data.Maybe ( fromMaybe ) -import Data.Monoid ( (<>) ) -import Data.Text ( Text ) -import qualified Data.Text as T -import Data.String ( fromString ) - -import Network.Minio ( Bucket - , Credentials(..) - , ConnectInfo - , GetObjectResponse - , MinioErr - , ObjectInfo(oiETag) - , defaultGetObjectOptions - , fromAWSEnv - , fromMinioEnv - , getObject - , gorObjectInfo - , gorObjectStream - , runMinio - , setCreds - , statObject - ) -import Network.Minio.S3API ( ETag ) -import Network.Http.Csp ( DirectiveList - , Keyword(..) - , SourceExpression(..) - , Directive(..) - , SchemePart(..) - , HostPart(..) - ) -import Network.Wai.Serve.Applications ( redirect ) -import Network.Wai.Serve.Main ( serve' ) -import Network.Wai.Serve.Middleware ( (<#>) - , cspHeadersMiddleware - , deindexifyMiddleware - , domainMiddleware - , forceSSLMiddleware - , gzipMiddleware - , loggerMiddleware - , securityHeadersMiddleware - , stsHeadersMiddleware - ) -import Network.Wai.Serve.Types ( Stage(..) - , TLSConfiguration(..) - , NotFoundHandlerConfig(..) - , MiddlewareConfig(..) - ) - -import System.Directory ( createDirectoryIfMissing - , doesDirectoryExist - , getCurrentDirectory - , removeDirectoryRecursive - , makeAbsolute - ) -import System.Environment ( lookupEnv ) -import System.FilePath.Posix ( (</>) ) - -import Espresso.Config ( loadConfig - , BundleConfig(..) - , Config - ) - -lookupEnvOrDefault :: (Read a, MonadIO m) => String -> a -> m a -lookupEnvOrDefault name def = do - lookup <- liftIO $ lookupEnv name - - pure $ maybe def read lookup - --- | The entry point of the server application. -main :: IO () -main = do - config <- loadConfig - - loggerChan <- newChan - restartChan <- newChan - etagMVar <- newEmptyMVar - - -- Start a logger thread - forkIO $ runStdoutLoggingT $ unChanLoggingT loggerChan - - runChanLoggingT loggerChan $ case (config ^. #bundle) of - LocalBundle bundleConfig -> do - path_ <- liftIO $ makeAbsolute (T.unpack $ bundleConfig ^. #path) - - $(logInfo) - $ "Bundle: Using local path backend (" - <> (T.pack path_) - <> ")." - - liftIO $ runServer config path_ - S3Bundle bundleConfig -> do - $(logInfo) "Bundle: Using object storage backend." - - cwd <- liftIO getCurrentDirectory - - let runDir = cwd </> "run/" - let serveDir = runDir </> "serve/" - let bundlePath = runDir </> (T.unpack bundleObjectName) - - liftIO $ createDirectoryIfMissing True runDir - - $(logInfo) ("Bucket: " <> bucket) - - minioResult <- downloadBundle cb3CI bucket bundlePath bundleObjectName - - case minioResult of - Left e -> $(logError) $ "getObject failed." <> T.pack (show e) - Right response -> do - $(logInfo) "Sucessfully downloaded bundle." - - let bundleInfo = gorObjectInfo response - $(logInfo) $ "ETag: " <> oiETag bundleInfo - - putMVar etagMVar (oiETag bundleInfo) - - async $ foreverServer restartChan config serveDir bundlePath - - forever $ updater cb3CI - bucket - etagMVar - restartChan - bundlePath - bundleObjectName - (bundleConfig ^. #pollSeconds) - - where - bucket = bundleConfig ^. #bucket - bundleObjectName = bundleConfig ^. #objectName - endpointUrl = T.unpack $ bundleConfig ^. #endpoint - cb3CI = setCreds - (Credentials (bundleConfig ^. #accessKey) (bundleConfig ^. #secretKey)) - (fromString endpointUrl) - -downloadBundle - :: (MonadIO a, MonadLogger a) - => ConnectInfo - -> Bucket - -> FilePath - -> Text - -> a (Either MinioErr GetObjectResponse) -downloadBundle connectInfo bucket bundlePath bundleObjectName = do - $(logInfo) "Downloading latest bundle..." - - liftIO $ runMinio connectInfo $ do - response <- getObject bucket bundleObjectName defaultGetObjectOptions - - C.connect (gorObjectStream response) $ CB.sinkFileCautious bundlePath - - pure response - -updater - :: (MonadIO m, MonadLogger m, MonadBase IO m) - => ConnectInfo - -> Bucket - -> MVar ETag - -> Chan Bool - -> FilePath - -> Text - -> Int - -> m () -updater connectInfo bucket etagMVar restartChan bundlePath bundleObjectName pollSeconds - = do - threadDelay (1000000 * pollSeconds) - - $(logInfo) "Checking for bundle updates..." - - minioResult <- liftIO $ runMinio connectInfo $ statObject - bucket - bundleObjectName - defaultGetObjectOptions - - case minioResult of - Left e -> $(logError) $ "statObject failed." <> T.pack (show e) - Right response -> do - currentETag <- readMVar etagMVar - let newETag = oiETag response - - if newETag == currentETag - then $(logInfo) "No changes" - else do - $(logInfo) $ "Got new ETag: " <> oiETag response - - minioResult_ <- downloadBundle connectInfo - bucket - bundlePath - bundleObjectName - - case minioResult_ of - Left e -> $(logError) $ "getObject failed." <> T.pack (show e) - Right response -> do - $(logInfo) "Sucessfully downloaded new bundle." - - -- Update eTag. - _ <- takeMVar etagMVar - putMVar etagMVar newETag - - -- Schedule restart of web server. - writeChan restartChan True - -foreverServer - :: (MonadIO m, MonadLogger m, MonadBaseControl IO m) - => Chan Bool - -> Config - -> FilePath - -> FilePath - -> m () -foreverServer restartChan config serveDir bundlePath = forever $ do - serverId <- async $ prepareAndRunServer config serveDir bundlePath - - _ <- readChan restartChan - - $(logInfo) "Stopping server thread..." - cancel serverId - -prepareAndRunServer - :: (MonadIO a, MonadLogger a) => Config -> FilePath -> FilePath -> a () -prepareAndRunServer config serveDir bundlePath = do - prepareServeDir serveDir - - $(logInfo) "Extracting bundle..." - liftIO $ Tar.extract serveDir bundlePath - - liftIO $ runServer config serveDir - -prepareServeDir :: (MonadIO a, MonadLogger a) => FilePath -> a () -prepareServeDir serveDir = do - serveDirExists <- liftIO $ doesDirectoryExist serveDir - - if serveDirExists - then do - $(logInfo) "Removing existing serve directory..." - - liftIO $ removeDirectoryRecursive serveDir - else pure () - - $(logInfo) "Recreating serve directory..." - liftIO $ createDirectoryIfMissing True serveDir - -runServer :: Config -> FilePath -> IO () -runServer config serveDir = do - let serverConfig = config ^. #server - - stage_ <- lookupEnvOrDefault "SITE_STAGE" (serverConfig ^. #stage) - - let serverEnvironmentConfig = case stage_ of - Development -> serverConfig ^. #dev - Staging -> serverConfig ^. #staging - Production -> serverConfig ^. #prod - - serve' $ (serverEnvironmentConfig & #path .~ (Just serveDir)) diff --git a/src/bundle/mod.rs b/src/bundle/mod.rs new file mode 100644 index 0000000..dd36427 --- /dev/null +++ b/src/bundle/mod.rs @@ -0,0 +1,215 @@ +use crate::config::Config; +use rundir::RunDir; +use snafu::{ResultExt, Snafu}; +use std::{ + path::PathBuf, + sync::{Arc, RwLock}, +}; +use tokio::time::{interval, Duration}; + +mod poller; +pub mod rundir; + +#[derive(Snafu, Debug)] +pub enum Error { + InitRunDir { source: rundir::Error }, + DeinitRunDir { source: rundir::Error }, + LockRead, + LockWrite, + AttachSignalHook { source: std::io::Error }, + MissingETag, + PollError { source: poller::Error }, + SubDirError { source: rundir::Error }, +} + +type Result<T, E = Error> = std::result::Result<T, E>; + +#[derive(Copy, Clone, Debug)] +pub enum UnbundlerStatus { + INIT = 0, + IDLE = 1, +} + +#[derive(Clone)] +pub struct Bundle { + etag: Option<String>, +} + +struct UnbundlerState { + rundir: RunDir, + status: UnbundlerStatus, + active_bundle: Option<Bundle>, + staging_bundle: Option<Bundle>, + temp_dir: Option<PathBuf>, +} + +pub struct Unbundler { + config: Arc<Config>, + serve_dir: Arc<RwLock<Option<PathBuf>>>, + state: RwLock<UnbundlerState>, + poller: Box<dyn poller::BundlePoller + Sync + Send>, +} + +impl Unbundler { + pub fn new(config: Arc<Config>, serve_dir: Arc<RwLock<Option<PathBuf>>>) -> Unbundler { + let mut rundir = RunDir::new(config.server.run_dir.clone()); + + if let Some(allow_cleaning) = config.server.auto_cleanup { + rundir = rundir.allow_cleaning(allow_cleaning); + } + + let poller: Box<dyn poller::BundlePoller + Sync + Send> = match &config.bundle { + crate::config::BundleConfig::S3Bundle { + access_key, + secret_key, + endpoint, + bucket, + region, + object_name, + } => Box::new(poller::S3BundlePoller::new( + access_key.clone(), + secret_key.clone(), + endpoint.clone(), + bucket.clone(), + region.clone(), + object_name.clone(), + )), + crate::config::BundleConfig::LocalBundle { dir } => { + Box::new(poller::LocalBundlePoller::new(dir.clone())) + } + }; + + Unbundler { + config: config.clone(), + state: RwLock::new(UnbundlerState { + rundir, + status: UnbundlerStatus::INIT, + active_bundle: None, + staging_bundle: None, + temp_dir: None, + }), + poller, + serve_dir, + } + } + + pub fn get_status(&self) -> Result<UnbundlerStatus> { + let state = self.state.read().map_err(|_| Error::LockRead)?; + + Ok(state.status.clone()) + } + + pub fn get_serve_dir(&self) -> Result<Option<PathBuf>> { + let serve_dir = self.serve_dir.read().map_err(|_| Error::LockRead)?; + + Ok(serve_dir.clone()) + } + + pub async fn enter(&self) -> Result<()> { + self.init()?; + + let mut interval = interval(Duration::from_secs(self.config.unbundler.poll_seconds)); + + loop { + let me = self; + + tokio::select! { + _ = tokio::signal::ctrl_c() => { + break; + } + _ = interval.tick() => { + me.poll().await?; + } + } + } + + self.deinit() + } + + fn init(&self) -> Result<()> { + info!("Unbundler: Initializing..."); + + let mut state = self.state.write().map_err(|_| Error::LockWrite)?; + + state.rundir.initialize().context(InitRunDir)?; + state.temp_dir = Some(state.rundir.create_subdir("temp").context(InitRunDir)?); + + state.status = UnbundlerStatus::IDLE; + + Ok(()) + } + + async fn poll(&self) -> Result<()> { + info!("Unbundler: Checking for updates..."); + + let mut state = self.state.write().map_err(|_| Error::LockWrite)?; + + let result = self + .poller + .poll(&state.active_bundle) + .await + .context(PollError)?; + + match result { + poller::PollResult::Skip => { + info!("Unbundler: No updates from poller."); + + Ok(()) + } + poller::PollResult::StaticUpdateReady { etag, path } => { + // Replacing active bundle. + state.active_bundle = Some(Bundle { etag }); + state.staging_bundle = None; + + let mut serve_dir = self.serve_dir.write().map_err(|_| Error::LockWrite)?; + + serve_dir.replace(path); + // TODO: Update path. + + Ok(()) + } + poller::PollResult::UpdateReady { etag } => { + if state.rundir.subdir_exists(&etag).context(SubDirError)? { + warn!("Unbundler: Skipping update. Subdir already exists."); + + return Ok(()); + } + + state.staging_bundle = Some(Bundle { + etag: Some(etag.clone()), + }); + + let newdir = state.rundir.create_subdir(&etag).context(SubDirError)?; + + let result = self.poller.retrieve(&etag, newdir.clone()).await; + + if result.is_err() { + warn!("Unbundler: Poller failed to retrieve new bundle. Rolling back."); + + state.staging_bundle = None; + + state.rundir.remove_subdir_all(&etag).context(SubDirError)?; + } + + // Replacing active bundle. + state.active_bundle = state.staging_bundle.clone(); + state.staging_bundle = None; + + let mut serve_dir = self.serve_dir.write().map_err(|_| Error::LockWrite)?; + + serve_dir.replace(newdir); + // TODO: Update path. + + Ok(()) + } + } + } + + fn deinit(&self) -> Result<()> { + let state = self.state.write().map_err(|_| Error::LockWrite)?; + + state.rundir.cleanup().context(DeinitRunDir)?; + + Ok(()) + } +} diff --git a/src/bundle/poller/local_dir.rs b/src/bundle/poller/local_dir.rs new file mode 100644 index 0000000..6d22051 --- /dev/null +++ b/src/bundle/poller/local_dir.rs @@ -0,0 +1,41 @@ +use super::{BundlePoller, PollResult, Result}; +use crate::bundle::Bundle; +use async_trait::async_trait; +use std::path::PathBuf; + +pub struct LocalBundlePoller { + dir: PathBuf, +} + +impl LocalBundlePoller { + pub fn new(dir: PathBuf) -> LocalBundlePoller { + LocalBundlePoller { dir } + } +} + +#[async_trait] +impl BundlePoller for LocalBundlePoller { + async fn poll(&self, active_bundle: &Option<Bundle>) -> Result<PollResult> { + if let None = active_bundle { + info!( + "LocalBundlePoller: Updating bundle (Local dir: {}).", + self.dir.display() + ); + + return Ok(PollResult::StaticUpdateReady { + etag: None, + path: self.dir.clone(), + }); + } + + info!("LocalBundlePoller: Local dir. No update needed."); + + Ok(PollResult::Skip) + } + + async fn retrieve(&self, _bundle_id: &str, _path: PathBuf) -> Result<()> { + warn!("LocalBundlePoller: retrieve is not supported. Ignoring."); + + Ok(()) + } +} diff --git a/src/bundle/poller/mod.rs b/src/bundle/poller/mod.rs new file mode 100644 index 0000000..58eaf72 --- /dev/null +++ b/src/bundle/poller/mod.rs @@ -0,0 +1,39 @@ +use super::Bundle; +use async_trait::async_trait; +pub use local_dir::LocalBundlePoller; +pub use s3::S3BundlePoller; +use snafu::Snafu; +use std::path::PathBuf; + +mod local_dir; +mod s3; + +#[derive(Snafu, Debug)] +pub enum Error { + MissingBundleID, + InternalPollerError { + source: Box<dyn std::error::Error>, + }, + MismatchedBundleID { + original_id: String, + current_id: String, + }, + UnpackError { + source: std::io::Error, + }, +} + +pub type Result<T, E = Error> = std::result::Result<T, E>; + +pub enum PollResult { + Skip, + UpdateReady { etag: String }, + StaticUpdateReady { etag: Option<String>, path: PathBuf }, +} + +#[async_trait] +pub trait BundlePoller { + async fn poll(&self, active_bundle: &Option<Bundle>) -> Result<PollResult>; + + async fn retrieve(&self, bundle_id: &str, path: PathBuf) -> Result<()>; +} diff --git a/src/bundle/poller/s3.rs b/src/bundle/poller/s3.rs new file mode 100644 index 0000000..76823f1 --- /dev/null +++ b/src/bundle/poller/s3.rs @@ -0,0 +1,159 @@ +use super::{BundlePoller, Error, PollResult, Result}; +use crate::bundle::Bundle; +use async_compat::CompatExt; +use async_tar::Archive; +use async_trait::async_trait; +use rusoto_core::RusotoError; +use rusoto_s3::{ + GetObjectError, GetObjectRequest, HeadObjectError, HeadObjectRequest, S3Client, S3, +}; +use snafu::{ResultExt, Snafu}; +use std::path::PathBuf; + +#[derive(Snafu, Debug)] +pub enum InternalError { + S3HeadObjectError { + source: RusotoError<HeadObjectError>, + }, + S3GetObjectError { + source: RusotoError<GetObjectError>, + }, + S3MissingBody, + S3MissingETag, +} + +impl From<InternalError> for Error { + fn from(err: InternalError) -> Self { + Error::InternalPollerError { + source: Box::new(err), + } + } +} + +pub struct S3BundlePoller { + client: S3Client, + bucket: String, + object_name: String, +} + +impl S3BundlePoller { + pub fn new( + access_key: String, + secret_key: String, + endpoint: String, + bucket: String, + region: String, + object_name: String, + ) -> S3BundlePoller { + let region = rusoto_core::Region::Custom { + name: region.clone(), + endpoint: endpoint.clone(), + }; + let credentials = + rusoto_credential::StaticProvider::new_minimal(access_key.clone(), secret_key.clone()); + let dispatcher = rusoto_core::HttpClient::new().unwrap(); + let client = S3Client::new_with(dispatcher, credentials, region); + + S3BundlePoller { + client, + bucket, + object_name, + } + } +} + +#[async_trait] +impl BundlePoller for S3BundlePoller { + async fn poll(&self, active_bundle: &Option<Bundle>) -> Result<PollResult> { + if let Some(active_bundle) = active_bundle { + if let Some(_) = &active_bundle.etag { + let mut head_request: HeadObjectRequest = Default::default(); + + head_request.bucket = self.bucket.clone(); + head_request.key = self.object_name.clone(); + + let head_response = self + .client + .head_object(head_request) + .await + .context(S3HeadObjectError)?; + + if head_response.e_tag == active_bundle.etag { + info!( + "S3BundlePoller: No updates found for object: {}", + self.object_name + ); + + // Skip update download + return Ok(PollResult::Skip); + } else { + info!( + "S3BundlePoller: Object {} has changed. Starting bundle update.", + self.object_name + ); + } + } else { + error!("S3BundlePoller: Expected the active bundle to have a valid ETag."); + + return Err(Error::MissingBundleID); + } + } + + // Head object + let mut head_request: HeadObjectRequest = Default::default(); + + head_request.bucket = self.bucket.clone(); + head_request.key = self.object_name.clone(); + + let head_response = self + .client + .head_object(head_request) + .await + .context(S3HeadObjectError)?; + + Ok(PollResult::UpdateReady { + etag: head_response.e_tag.ok_or(InternalError::S3MissingETag)?, + }) + } + + async fn retrieve(&self, bundle_id: &str, path: PathBuf) -> Result<()> { + info!("S3BundlePoller: Starting bundle download..."); + + let mut get_object_request: GetObjectRequest = Default::default(); + + get_object_request.bucket = self.bucket.clone(); + get_object_request.key = self.object_name.clone(); + + let get_object_response = self + .client + .get_object(get_object_request) + .await + .context(S3GetObjectError)?; + + let current_bundle_id = get_object_response + .e_tag + .ok_or(InternalError::S3MissingETag)?; + + if current_bundle_id != bundle_id { + return Err(Error::MismatchedBundleID { + original_id: String::from(bundle_id), + current_id: current_bundle_id, + }); + } + + info!("S3BundlePoller: Unpacking bundle..."); + + // TODO: Write temp file to disk. + let archive = Archive::new( + get_object_response + .body + .ok_or(InternalError::S3MissingBody)? + .into_async_read() + .compat(), + ); + + archive.unpack(path).await.context(super::UnpackError)?; + + Ok(()) + } +} diff --git a/src/bundle/rundir.rs b/src/bundle/rundir.rs new file mode 100644 index 0000000..615ee31 --- /dev/null +++ b/src/bundle/rundir.rs @@ -0,0 +1,270 @@ +use snafu::{ResultExt, Snafu}; +use std::path::{Path, PathBuf}; + +#[derive(Snafu, Debug)] +pub enum Error { + Initialize { + path: PathBuf, + }, + PathIsNotDir { + path: PathBuf, + }, + DirIsNotEmpty { + path: PathBuf, + child_path: PathBuf, + }, + CreateDir { + path: PathBuf, + source: std::io::Error, + }, + ScanDir { + path: PathBuf, + source: std::io::Error, + }, + RecreateDir { + path: PathBuf, + source: std::io::Error, + }, + RemoveSubDir { + path: PathBuf, + source: std::io::Error, + }, + InvalidSubDirName { + name: String, + inner_error: Option<std::io::Error>, + }, +} + +type Result<T, E = Error> = std::result::Result<T, E>; + +pub struct RunDir { + path: PathBuf, + allow_cleaning: bool, +} + +impl RunDir { + pub fn new<T: Into<PathBuf>>(path: T) -> RunDir { + RunDir { + path: path.into(), + allow_cleaning: false, + } + } + + /// Set whether the directory can perform cleanup operations. + /// + /// Use with caution. This will clear existing directories on initialization + /// and cleanup. + pub fn allow_cleaning(mut self, allow_cleaning: bool) -> RunDir { + self.allow_cleaning = allow_cleaning; + + self + } + + /// Creates the initial RunDir. + /// + /// # Examples + /// + /// ``` + /// use espresso::bundle::rundir::RunDir; + /// + /// let rundir = RunDir::new("tests/rundir").allow_cleaning(true); + /// + /// rundir.initialize().unwrap(); + /// + /// rundir.cleanup().unwrap(); + /// ``` + pub fn initialize(&self) -> Result<()> { + if Path::exists(&self.path) { + info!("RunDir already exists: {}", self.path.display()); + + if !Path::is_dir(&self.path) { + error!("RunDir is not a directory: {}", self.path.display()); + + return Err(Error::PathIsNotDir { + path: self.path.clone(), + }); + } else { + // Scan dir + let dir_iterator = std::fs::read_dir(&self.path).context(ScanDir { + path: self.path.clone(), + })?; + + let mut existing_child_path = None; + + for entry in dir_iterator { + let entry = entry.context(ScanDir { + path: self.path.clone(), + })?; + + existing_child_path = Some(entry.path()); + + break; + } + + if let Some(child_path) = existing_child_path { + if self.allow_cleaning { + info!("Recreating RunDir."); + + std::fs::remove_dir_all(&self.path).context(RecreateDir { + path: self.path.clone(), + })?; + + std::fs::create_dir_all(&self.path).context(RecreateDir { + path: self.path.clone(), + })?; + } else { + return Err(Error::DirIsNotEmpty { + path: self.path.clone(), + child_path, + }); + } + } + } + } else { + info!("Creating new RunDir: {}", self.path.display()); + + std::fs::create_dir_all(&self.path).context(CreateDir { + path: self.path.clone(), + })? + } + + Ok(()) + } + + pub fn cleanup(&self) -> Result<()> { + if self.allow_cleaning { + std::fs::remove_dir_all(&self.path).context(RecreateDir { + path: self.path.clone(), + })?; + + info!("Cleaned up RunDir: {}", self.path.display()); + } else { + warn!("Leaving RunDir unmodified. Manual cleanup may be needed."); + } + + Ok(()) + } + + /// Creates a subdir within the RunDir. + pub fn create_subdir(&self, name: &str) -> Result<PathBuf> { + let pathbuf = self.validate_subdir_name(name)?; + + std::fs::create_dir(&pathbuf).context(CreateDir { + path: pathbuf.clone(), + })?; + + Ok(pathbuf) + } + + /// Removes a subdir and all its contents from the RunDir. + pub fn remove_subdir_all(&self, name: &str) -> Result<()> { + let pathbuf = self.validate_subdir_name(name)?; + + std::fs::remove_dir_all(&pathbuf).context(RemoveSubDir { path: pathbuf })?; + + Ok(()) + } + + /// Checks if a subdir exists within the RunDir. + pub fn subdir_exists(&self, name: &str) -> Result<bool> { + let pathbuf = self.validate_subdir_name(name)?; + + Ok(pathbuf.exists()) + } + + fn validate_subdir_name(&self, name: &str) -> Result<PathBuf> { + // Check that the name results in a dir exactly one level below the current one. + let mut pathbuf = self.path.clone(); + + pathbuf.push(name); + + if let Some(parent) = pathbuf.parent() { + if PathBuf::from(parent) != PathBuf::from(&self.path) { + return Err(Error::InvalidSubDirName { + name: String::from(name), + inner_error: None, + }); + } + } else { + return Err(Error::InvalidSubDirName { + name: String::from(name), + inner_error: None, + }); + } + + Ok(pathbuf) + } +} + +#[cfg(test)] +mod tests { + use super::{Error, RunDir}; + use std::path::PathBuf; + + #[test] + fn test_initialize() -> () { + // Create dir for the first time. + let result = RunDir::new("tests/rundir").initialize(); + + assert!(result.is_ok()); + + // Use existing empty dir. + let result = RunDir::new("tests/rundir").initialize(); + + assert!(result.is_ok()); + + // Fail when dir is not empty and allow_cleaning is not set. + std::fs::write("tests/rundir/hello.world", "test").unwrap(); + let result = RunDir::new("tests/rundir").initialize(); + + match result { + Err(Error::DirIsNotEmpty { path, child_path }) => { + assert_eq!(path, PathBuf::from("tests/rundir")); + assert_eq!(child_path, PathBuf::from("tests/rundir/hello.world")); + } + _ => panic!("Expected an error."), + } + + // Clean existing dir. + let result = RunDir::new("tests/rundir") + .allow_cleaning(true) + .initialize(); + + assert!(result.is_ok()); + + std::fs::remove_dir("tests/rundir").unwrap(); + + // Fail when dir is not a directory. + std::fs::write("tests/rundir", "hello").unwrap(); + let result = RunDir::new("tests/rundir").initialize(); + + match result { + Err(Error::PathIsNotDir { path }) => { + assert_eq!(path, PathBuf::from("tests/rundir")); + } + _ => panic!("Expected an error."), + } + + std::fs::remove_file("tests/rundir").unwrap(); + } + + #[test] + fn test_subdirs() -> () { + let rundir = RunDir::new("tests/rundir2").allow_cleaning(true); + + rundir.initialize().unwrap(); + + assert_eq!(PathBuf::from("tests/rundir2/subtest").exists(), false); + assert_eq!(rundir.subdir_exists("subtest").unwrap(), false); + + rundir.create_subdir("subtest").unwrap(); + assert_eq!(PathBuf::from("tests/rundir2/subtest").exists(), true); + assert_eq!(rundir.subdir_exists("subtest").unwrap(), true); + + rundir.remove_subdir_all("subtest").unwrap(); + assert_eq!(PathBuf::from("tests/rundir2/subtest").exists(), false); + assert_eq!(rundir.subdir_exists("subtest").unwrap(), false); + + rundir.cleanup().unwrap(); + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..1787da4 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,113 @@ +use serde_derive::{Deserialize, Serialize}; +use snafu::{ResultExt, Snafu}; +use std::{ + env, fs, + net::SocketAddr, + path::{Path, PathBuf}, +}; + +#[derive(Snafu, Debug)] +pub enum Error { + /// The configuration file could not be found or read. + #[snafu(display("Could not open config from {}: {}", path.display(), source))] + OpenConfig { + path: PathBuf, + source: std::io::Error, + }, + /// The configuration file could not be parsed or deserialized. + #[snafu(display("Could not deserialize config from {}: {}", path.display(), source))] + DeserializeConfig { + path: PathBuf, + source: toml::de::Error, + }, + /// The current directory could not be determined. + GetCurrentDir { source: std::io::Error }, +} + +type Result<T, E = Error> = std::result::Result<T, E>; + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Config { + pub bundle: BundleConfig, + pub unbundler: UnbundlerConfig, + pub server: ServerConfig, + pub stats: Option<StatsConfig>, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct ServerConfig { + pub address: SocketAddr, + pub run_dir: PathBuf, + pub auto_cleanup: Option<bool>, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct StatsConfig { + pub address: SocketAddr, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct UnbundlerConfig { + pub poll_seconds: u64, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +#[serde(tag = "type")] +pub enum BundleConfig { + S3Bundle { + access_key: String, + secret_key: String, + endpoint: String, + bucket: String, + region: String, + object_name: String, + }, + LocalBundle { + dir: PathBuf, + }, +} + +pub fn from_file(path: &Path) -> Result<Config> { + info!("Reading config file from {}", path.display()); + + let contents = fs::read_to_string(path).context(OpenConfig { path })?; + + let config = toml::from_str(&contents); + + config.context(DeserializeConfig { path }) +} + +pub fn from_current_dir() -> Result<Config> { + let mut current_path = env::current_dir().context(GetCurrentDir)?; + + current_path.push("config.toml"); + + from_file(¤t_path) +} + +#[cfg(test)] +mod tests { + use super::{from_file, BundleConfig, Config, ServerConfig, StatsConfig, UnbundlerConfig}; + use std::path::{Path, PathBuf}; + + #[test] + fn test_from_file() { + assert_eq!( + from_file(&Path::new("config.sample.toml")).unwrap(), + Config { + stats: Some(StatsConfig { + address: "127.0.0.1:8089".parse().unwrap(), + }), + bundle: BundleConfig::LocalBundle { + dir: PathBuf::from("/tmp/") + }, + server: ServerConfig { + address: "127.0.0.1:8088".parse().unwrap(), + run_dir: PathBuf::from("run"), + auto_cleanup: None, + }, + unbundler: UnbundlerConfig { poll_seconds: 10 } + } + ) + } +} diff --git a/src/files/LICENSE b/src/files/LICENSE new file mode 100644 index 0000000..6682688 --- /dev/null +++ b/src/files/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2017 Nikolay Kim + +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. \ No newline at end of file diff --git a/src/files/README.md b/src/files/README.md new file mode 100644 index 0000000..9816cb9 --- /dev/null +++ b/src/files/README.md @@ -0,0 +1,4 @@ +# files + +Fork of https://github.com/actix/actix-web/tree/master/actix-files +Modified to better support the espresso use-case. diff --git a/src/files/chunked.rs b/src/files/chunked.rs new file mode 100644 index 0000000..70a5e77 --- /dev/null +++ b/src/files/chunked.rs @@ -0,0 +1,98 @@ +use actix_web::{ + error::{BlockingError, Error, ErrorInternalServerError}, + web, +}; +use bytes::Bytes; +use futures_core::Stream; +use futures_util::future::{FutureExt, LocalBoxFuture}; +use io::Seek; +use std::{ + cmp, + fs::File, + future::Future, + io, + io::Read, + pin::Pin, + task::{Context, Poll}, +}; + +fn handle_error(err: BlockingError<io::Error>) -> Error { + match err { + BlockingError::Error(err) => err.into(), + BlockingError::Canceled => ErrorInternalServerError("Unexpected error"), + } +} + +#[doc(hidden)] +/// A helper created from a `std::fs::File` which reads the file +/// chunk-by-chunk on a `ThreadPool`. +pub struct ChunkedReadFile { + size: u64, + offset: u64, + file: Option<File>, + fut: Option<LocalBoxFuture<'static, Result<(File, Bytes), BlockingError<io::Error>>>>, + counter: u64, +} + +impl ChunkedReadFile { + pub fn new( + size: u64, + offset: u64, + file: Option<File>, + fut: Option<LocalBoxFuture<'static, Result<(File, Bytes), BlockingError<io::Error>>>>, + counter: u64, + ) -> ChunkedReadFile { + ChunkedReadFile { + size, + offset, + file, + fut, + counter, + } + } +} + +impl Stream for ChunkedReadFile { + type Item = Result<Bytes, Error>; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> { + if let Some(ref mut fut) = self.fut { + return match Pin::new(fut).poll(cx) { + Poll::Ready(Ok((file, bytes))) => { + self.fut.take(); + self.file = Some(file); + self.offset += bytes.len() as u64; + self.counter += bytes.len() as u64; + Poll::Ready(Some(Ok(bytes))) + } + Poll::Ready(Err(e)) => Poll::Ready(Some(Err(handle_error(e)))), + Poll::Pending => Poll::Pending, + }; + } + + let size = self.size; + let offset = self.offset; + let counter = self.counter; + + if size == counter { + Poll::Ready(None) + } else { + let mut file = self.file.take().expect("Use after completion"); + self.fut = Some( + web::block(move || { + let max_bytes: usize; + max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; + let mut buf = Vec::with_capacity(max_bytes); + file.seek(io::SeekFrom::Start(offset))?; + let nbytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; + if nbytes == 0 { + return Err(io::ErrorKind::UnexpectedEof.into()); + } + Ok((file, Bytes::from(buf))) + }) + .boxed_local(), + ); + self.poll_next(cx) + } + } +} diff --git a/src/files/error.rs b/src/files/error.rs new file mode 100644 index 0000000..f900691 --- /dev/null +++ b/src/files/error.rs @@ -0,0 +1,27 @@ +use actix_web::{http::StatusCode, HttpResponse, ResponseError}; +use snafu::Snafu; + +/// Errors which can occur when serving static files. +#[derive(Snafu, Debug, PartialEq)] +pub enum FilesError { + /// Path is not a directory + #[snafu(display("Path is not a directory. Unable to serve static files"))] + IsNotDirectory, + + /// Cannot render directory + #[snafu(display("Unable to render directory without index file"))] + IsDirectory, + + #[snafu(display("Serve directory is not ready."))] + ServeDirNotReady, + + #[snafu(display("Unable to obtain read lock for serve dir."))] + ServerDirReadLockFail, +} + +/// Return `NotFound` for `FilesError` +impl ResponseError for FilesError { + fn error_response(&self) -> HttpResponse { + HttpResponse::new(StatusCode::NOT_FOUND) + } +} diff --git a/src/files/mime_ext.rs b/src/files/mime_ext.rs new file mode 100644 index 0000000..86a76cf --- /dev/null +++ b/src/files/mime_ext.rs @@ -0,0 +1,26 @@ +use mime_guess::from_ext; + +/// 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() +} + +#[cfg(test)] +mod tests { + use super::file_extension_to_mime; + + #[actix_rt::test] + async fn test_file_extension_to_mime() { + 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); + } +} diff --git a/src/files/mod.rs b/src/files/mod.rs new file mode 100644 index 0000000..79a1cb1 --- /dev/null +++ b/src/files/mod.rs @@ -0,0 +1,1214 @@ +#![allow(clippy::borrow_interior_mutable_const, clippy::type_complexity)] + +//! Static files support +use std::cell::RefCell; +use std::fmt::Write; +use std::fs::DirEntry; +use std::io; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::{ + sync::{Arc, RwLock}, + task::{Context, Poll}, +}; + +use actix_service::boxed::{self, BoxService, BoxServiceFactory}; +use actix_service::{IntoServiceFactory, Service, ServiceFactory}; +use actix_web::dev::{ + AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse, +}; +use actix_web::error::Error as ActixError; +use actix_web::guard::Guard; +use actix_web::http::header::{self, DispositionType}; +use actix_web::http::Method; +use actix_web::{HttpRequest, HttpResponse}; +use error::FilesError as Error; +use futures_util::future::{ok, Either, FutureExt, LocalBoxFuture, Ready}; +use percent_encoding::{utf8_percent_encode, CONTROLS}; +use v_htmlescape::escape as escape_html_entity; + +use named::NamedFile; +use pathbuf::UriPathBuf; + +mod chunked; +mod error; +mod mime_ext; +mod named; +mod pathbuf; + +type HttpService = BoxService<ServiceRequest, ServiceResponse, ActixError>; +type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, ActixError, ()>; + +type DirectoryRenderer = dyn Fn(&Directory, &HttpRequest) -> Result<ServiceResponse, io::Error>; + +/// 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<DirEntry>) -> 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 + } +} + +// show file url as relative to static path +macro_rules! encode_file_url { + ($path:ident) => { + utf8_percent_encode(&$path, CONTROLS) + }; +} + +// " -- " & -- & ' -- ' < -- < > -- > / -- / +macro_rules! encode_file_name { + ($entry:ident) => { + escape_html_entity(&$entry.file_name().to_string_lossy()) + }; +} + +fn directory_listing(dir: &Directory, req: &HttpRequest) -> Result<ServiceResponse, io::Error> { + 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, + "<li><a href=\"{}\">{}/</a></li>", + encode_file_url!(p), + encode_file_name!(entry), + ); + } else { + let _ = write!( + body, + "<li><a href=\"{}\">{}</a></li>", + encode_file_url!(p), + encode_file_name!(entry), + ); + } + } else { + continue; + } + } + } + + let html = format!( + "<html>\ + <head><title>{}</title></head>\ + <body><h1>{}</h1>\ + <ul>\ + {}\ + </ul></body>\n</html>", + index_of, index_of, body + ); + Ok(ServiceResponse::new( + req.clone(), + HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(html), + )) +} + +type MimeOverride = dyn Fn(&mime::Name) -> DispositionType; + +/// Static files handling +/// +/// `Files` service must be registered with `App::service()` method. +/// +/// ```rust +/// use actix_web::App; +/// use actix_files as fs; +/// +/// fn main() { +/// let app = App::new() +/// .service(fs::Files::new("/static", ".")); +/// } +/// ``` +pub struct Files { + path: String, + directory: Arc<RwLock<Option<PathBuf>>>, + index: Option<String>, + show_index: bool, + redirect_to_slash: bool, + default: Rc<RefCell<Option<Rc<HttpNewService>>>>, + renderer: Rc<DirectoryRenderer>, + mime_override: Option<Rc<MimeOverride>>, + file_flags: named::Flags, + // FIXME: Should re-visit later. + #[allow(clippy::redundant_allocation)] + guards: Option<Rc<Box<dyn Guard>>>, +} + +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, + default: self.default.clone(), + renderer: self.renderer.clone(), + file_flags: self.file_flags, + path: self.path.clone(), + mime_override: self.mime_override.clone(), + guards: self.guards.clone(), + } + } +} + +impl Files { + /// Create new `Files` instance for specified base directory. + /// + /// `File` uses `ThreadPool` for blocking filesystem operations. + /// By default pool with 5x threads of available cpus is used. + /// Pool size can be changed by setting ACTIX_THREADPOOL environment variable. + pub fn new(path: &str, directory: Arc<RwLock<Option<PathBuf>>>) -> Files { + // let orig_dir = dir.into(); + // let dir = match orig_dir.canonicalize() { + // Ok(canon_dir) => canon_dir, + // Err(_) => { + // log::error!("Specified path is not a directory: {:?}", orig_dir); + // PathBuf::new() + // } + // }; + + Files { + path: path.to_string(), + directory, + index: None, + show_index: false, + redirect_to_slash: false, + default: Rc::new(RefCell::new(None)), + renderer: Rc::new(directory_listing), + mime_override: None, + file_flags: named::Flags::default(), + guards: None, + } + } + + /// Show files listing for directories. + /// + /// By default show files listing is disabled. + 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 + } + + /// Set custom directory renderer + pub fn files_listing_renderer<F>(mut self, f: F) -> Self + where + for<'r, 's> F: + Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error> + 'static, + { + self.renderer = Rc::new(f); + self + } + + /// Specifies mime override callback + pub fn mime_override<F>(mut self, f: F) -> Self + where + F: Fn(&mime::Name) -> DispositionType + 'static, + { + self.mime_override = Some(Rc::new(f)); + self + } + + /// Set index file + /// + /// Shows specific index file for directory "/" instead of + /// showing files listing. + pub fn index_file<T: Into<String>>(mut self, index: T) -> Self { + self.index = Some(index.into()); + self + } + + #[inline] + /// 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 + } + + #[inline] + /// 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 custom guards to use for directory listings and files. + /// + /// Default behaviour allows GET and HEAD. + #[inline] + pub fn use_guards<G: Guard + 'static>(mut self, guards: G) -> Self { + self.guards = Some(Rc::new(Box::new(guards))); + self + } + + /// Disable `Content-Disposition` header. + /// + /// By default Content-Disposition` header is enabled. + #[inline] + 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. + pub fn default_handler<F, U>(mut self, f: F) -> Self + where + F: IntoServiceFactory<U>, + U: ServiceFactory< + Config = (), + Request = ServiceRequest, + Response = ServiceResponse, + Error = ActixError, + > + 'static, + { + // create and configure default resource + self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory( + f.into_factory().map_init_err(|_| ()), + ))))); + + self + } +} + +impl HttpServiceFactory for Files { + fn register(self, config: &mut AppService) { + if self.default.borrow().is_none() { + *self.default.borrow_mut() = Some(config.default_service()); + } + let rdef = if config.is_root() { + ResourceDef::root_prefix(&self.path) + } else { + ResourceDef::prefix(&self.path) + }; + config.register_service(rdef, None, self, None) + } +} + +impl ServiceFactory for Files { + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = ActixError; + type Config = (); + type Service = FilesService; + type InitError = (); + type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>; + + fn new_service(&self, _: ()) -> Self::Future { + let mut srv = FilesService { + 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(), + file_flags: self.file_flags, + guards: self.guards.clone(), + }; + + if let Some(ref default) = *self.default.borrow() { + default + .new_service(()) + .map(move |result| match result { + Ok(default) => { + srv.default = Some(default); + Ok(srv) + } + Err(_) => Err(()), + }) + .boxed_local() + } else { + ok(srv).boxed_local() + } + } +} + +pub struct FilesService { + directory: Arc<RwLock<Option<PathBuf>>>, + index: Option<String>, + show_index: bool, + redirect_to_slash: bool, + default: Option<HttpService>, + renderer: Rc<DirectoryRenderer>, + mime_override: Option<Rc<MimeOverride>>, + file_flags: named::Flags, + // FIXME: Should re-visit later. + #[allow(clippy::redundant_allocation)] + guards: Option<Rc<Box<dyn Guard>>>, +} + +impl FilesService { + fn handle_err( + &mut self, + e: io::Error, + req: ServiceRequest, + ) -> Either< + Ready<Result<ServiceResponse, ActixError>>, + LocalBoxFuture<'static, Result<ServiceResponse, ActixError>>, + > { + log::debug!("Files: Failed to handle {}: {}", req.path(), e); + if let Some(ref mut default) = self.default { + Either::Right(default.call(req)) + } else { + Either::Left(ok(req.error_response(e))) + } + } +} + +impl Service for FilesService { + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = ActixError; + type Future = Either< + Ready<Result<Self::Response, Self::Error>>, + LocalBoxFuture<'static, Result<Self::Response, Self::Error>>, + >; + + fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: ServiceRequest) -> Self::Future { + let is_method_valid = if let Some(guard) = &self.guards { + // execute user defined guards + (**guard).check(req.head()) + } else { + // default behavior + matches!(*req.method(), Method::HEAD | Method::GET) + }; + + if !is_method_valid { + return Either::Left(ok( + req.into_response( + actix_web::HttpResponse::MethodNotAllowed() + .header(header::CONTENT_TYPE, "text/plain") + .body("Request did not meet this resource's requirements."), + ), + )); + } + + let real_path = match UriPathBuf::new(req.match_info().path()) { + Ok(item) => item, + Err(e) => return Either::Left(ok(req.error_response(e))), + }; + + let path_ref: PathBuf = real_path.into(); + + let maybe_dir = match self.directory.read().map(|dir| dir.clone()) { + Ok(maybe_dir) => maybe_dir, + Err(_) => return Either::Left(ok(req.error_response(Error::ServerDirReadLockFail))), + }; + + let dir = match maybe_dir { + Some(dir) => dir, + None => return Either::Left(ok(req.error_response(Error::ServeDirNotReady))), + }; + + // full file path + let path = match dir.join(&path_ref).canonicalize() { + Ok(path) => path, + Err(e) => return self.handle_err(e, req), + }; + + if path.is_dir() { + if let Some(ref redir_index) = self.index { + if self.redirect_to_slash && !req.path().ends_with('/') { + let redirect_to = format!("{}/", req.path()); + return Either::Left(ok( + req.into_response( + HttpResponse::Found() + .header(header::LOCATION, redirect_to) + .body("") + .into_body(), + ), + )); + } + + let path = path.join(redir_index); + + match NamedFile::open(path) { + Ok(mut named_file) => { + 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(); + Either::Left(ok(match named_file.into_response(&req) { + Ok(item) => ServiceResponse::new(req, item), + Err(e) => ServiceResponse::from_err(e, req), + })) + } + Err(e) => self.handle_err(e, req), + } + } else if self.show_index { + let dir = Directory::new(dir.clone(), path); + let (req, _) = req.into_parts(); + let x = (self.renderer)(&dir, &req); + match x { + Ok(resp) => Either::Left(ok(resp)), + Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), + } + } else { + Either::Left(ok(ServiceResponse::from_err( + Error::IsDirectory, + req.into_parts().0, + ))) + } + } else { + match NamedFile::open(path) { + Ok(mut named_file) => { + 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(); + match named_file.into_response(&req) { + Ok(item) => Either::Left(ok(ServiceResponse::new(req.clone(), item))), + Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), + } + } + Err(e) => self.handle_err(e, req), + } + } + } +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::ops::Add; + use std::time::{Duration, SystemTime}; + + use super::*; + use actix_web::guard; + use actix_web::http::header::{self, ContentDisposition, DispositionParam, DispositionType}; + use actix_web::http::{Method, StatusCode}; + use actix_web::middleware::Compress; + use actix_web::test::{self, TestRequest}; + use actix_web::{web, App, Responder}; + use bytes::Bytes; + use fs::File; + use named::NamedFile; + + fn serve_dir<T: Into<PathBuf>>(path: T) -> Arc<RwLock<Option<PathBuf>>> { + Arc::new(RwLock::new(Some(path.into()))) + } + + #[actix_rt::test] + async fn test_if_modified_since_without_if_none_match() { + let file = NamedFile::open("Cargo.toml").unwrap(); + let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + + let req = TestRequest::default() + .header(header::IF_MODIFIED_SINCE, since) + .to_http_request(); + let resp = file.respond_to(&req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); + } + + #[actix_rt::test] + async fn test_if_modified_since_with_if_none_match() { + let file = NamedFile::open("Cargo.toml").unwrap(); + let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + + let req = TestRequest::default() + .header(header::IF_NONE_MATCH, "miss_etag") + .header(header::IF_MODIFIED_SINCE, since) + .to_http_request(); + let resp = file.respond_to(&req).await.unwrap(); + assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); + } + + #[actix_rt::test] + async fn test_named_file_text() { + assert!(NamedFile::open("test--").is_err()); + let mut file = NamedFile::open("Cargo.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).await.unwrap(); + 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("test--").is_err()); + let mut file = NamedFile::open("Cargo.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).await.unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"Cargo.toml\"" + ); + + let file = NamedFile::open("Cargo.toml") + .unwrap() + .disable_content_disposition(); + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).await.unwrap(); + assert!(resp.headers().get(header::CONTENT_DISPOSITION).is_none()); + } + + #[actix_rt::test] + async fn test_named_file_non_ascii_file_name() { + let mut file = NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.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).await.unwrap(); + 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("Cargo.toml") + .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).await.unwrap(); + 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("tests/test.png").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).await.unwrap(); + 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_image_attachment() { + let cd = ContentDisposition { + disposition: DispositionType::Attachment, + parameters: vec![DispositionParam::Filename(String::from("test.png"))], + }; + let mut file = NamedFile::open("tests/test.png") + .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).await.unwrap(); + 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("tests/test.binary").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).await.unwrap(); + 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\"" + ); + } + + #[actix_rt::test] + async fn test_named_file_status_code_text() { + let mut file = NamedFile::open("Cargo.toml") + .unwrap() + .set_status_code(StatusCode::NOT_FOUND); + { + 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).await.unwrap(); + 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 mut srv = test::init_service( + App::new().service( + Files::new("/", serve_dir(".")) + .mime_override(all_attachment) + .index_file("Cargo.toml"), + ), + ) + .await; + + let request = TestRequest::get().uri("/").to_request(); + let response = test::call_service(&mut 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 mut srv = test::init_service( + App::new().service(Files::new("/test", serve_dir(".")).index_file("Cargo.toml")), + ) + .await; + + // Valid range header + let request = TestRequest::get() + .uri("/t%65st/Cargo.toml") + .header(header::RANGE, "bytes=10-20") + .to_request(); + let response = test::call_service(&mut srv, request).await; + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + + // Invalid range header + let request = TestRequest::get() + .uri("/t%65st/Cargo.toml") + .header(header::RANGE, "bytes=1-0") + .to_request(); + let response = test::call_service(&mut srv, request).await; + + assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); + } + + #[actix_rt::test] + async fn test_named_file_content_range_headers() { + let srv = test::start(|| App::new().service(Files::new("/", serve_dir(".")))); + + // Valid range header + let response = srv + .get("/tests/test.binary") + .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") + .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 = test::start(|| App::new().service(Files::new("/", serve_dir(".")))); + + // Valid range header + let response = srv + .get("/tests/test.binary") + .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") + .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 = Bytes::from(fs::read("tests/test.binary").unwrap()); + assert_eq!(bytes, data); + } + + #[actix_rt::test] + async fn test_head_content_length_headers() { + let srv = test::start(|| App::new().service(Files::new("/", serve_dir(".")))); + + 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 mut srv = test::init_service( + App::new().service(Files::new("/", serve_dir(".")).index_file("Cargo.toml")), + ) + .await; + let request = TestRequest::get() + .uri("/tests/test%20space.binary") + .to_request(); + let response = test::call_service(&mut srv, request).await; + assert_eq!(response.status(), StatusCode::OK); + + let bytes = test::read_body(response).await; + let data = Bytes::from(fs::read("tests/test space.binary").unwrap()); + assert_eq!(bytes, data); + } + + #[actix_rt::test] + async fn test_files_not_allowed() { + let mut srv = test::init_service(App::new().service(Files::new("/", serve_dir(".")))).await; + + let req = TestRequest::default() + .uri("/Cargo.toml") + .method(Method::POST) + .to_request(); + + let resp = test::call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + + let mut srv = test::init_service(App::new().service(Files::new("/", serve_dir(".")))).await; + let req = TestRequest::default() + .method(Method::PUT) + .uri("/Cargo.toml") + .to_request(); + let resp = test::call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + } + + #[actix_rt::test] + async fn test_files_guards() { + let mut srv = test::init_service( + App::new().service(Files::new("/", serve_dir(".")).use_guards(guard::Post())), + ) + .await; + + let req = TestRequest::default() + .uri("/Cargo.toml") + .method(Method::POST) + .to_request(); + + let resp = test::call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + } + + #[actix_rt::test] + async fn test_named_file_content_encoding() { + let mut srv = test::init_service(App::new().wrap(Compress::default()).service( + web::resource("/").to(|| async { + NamedFile::open("Cargo.toml") + .unwrap() + .set_content_encoding(header::ContentEncoding::Identity) + }), + )) + .await; + + let request = TestRequest::get() + .uri("/") + .header(header::ACCEPT_ENCODING, "gzip") + .to_request(); + let res = test::call_service(&mut srv, request).await; + assert_eq!(res.status(), StatusCode::OK); + assert!(!res.headers().contains_key(header::CONTENT_ENCODING)); + } + + #[actix_rt::test] + async fn test_named_file_content_encoding_gzip() { + let mut srv = test::init_service(App::new().wrap(Compress::default()).service( + web::resource("/").to(|| async { + NamedFile::open("Cargo.toml") + .unwrap() + .set_content_encoding(header::ContentEncoding::Gzip) + }), + )) + .await; + + let request = TestRequest::get() + .uri("/") + .header(header::ACCEPT_ENCODING, "gzip") + .to_request(); + let res = test::call_service(&mut 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("Cargo.toml").unwrap(); + let resp = file.respond_to(&req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[actix_rt::test] + async fn test_static_files() { + let mut srv = + test::init_service(App::new().service(Files::new("/", serve_dir(".")).show_files_listing())) + .await; + let req = TestRequest::with_uri("/missing").to_request(); + + let resp = test::call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let mut srv = test::init_service(App::new().service(Files::new("/", serve_dir(".")))).await; + + let req = TestRequest::default().to_request(); + let resp = test::call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let mut srv = + test::init_service(App::new().service(Files::new("/", serve_dir(".")).show_files_listing())) + .await; + let req = TestRequest::with_uri("/tests").to_request(); + let resp = test::call_service(&mut 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 + let mut srv = test::init_service( + App::new().service(Files::new("/", serve_dir(".")).redirect_to_slash_directory()), + ) + .await; + let req = TestRequest::with_uri("/tests").to_request(); + let resp = test::call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + // should redirect if index present + let mut srv = test::init_service( + App::new().service( + Files::new("/", serve_dir(".")) + .index_file("test.png") + .redirect_to_slash_directory(), + ), + ) + .await; + let req = TestRequest::with_uri("/tests").to_request(); + let resp = test::call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::FOUND); + + // should not redirect if the path is wrong + let req = TestRequest::with_uri("/not_existing").to_request(); + let resp = test::call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[actix_rt::test] + async fn test_static_files_bad_directory() { + let _st: Files = Files::new("/", serve_dir("missing")); + let _st: Files = Files::new("/", serve_dir("Cargo.toml")); + } + + #[actix_rt::test] + async fn test_default_handler_file_missing() { + let mut st = Files::new("/", serve_dir(".")) + .default_handler(|req: ServiceRequest| { + 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(&mut st, req).await; + assert_eq!(resp.status(), StatusCode::OK); + let bytes = test::read_body(resp).await; + assert_eq!(bytes, Bytes::from_static(b"default content")); + } + + // #[actix_rt::test] + // async fn test_serve_index() { + // let st = Files::new(".").index_file("test.binary"); + // let req = TestRequest::default().uri("/tests").finish(); + + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::OK); + // assert_eq!( + // resp.headers() + // .get(header::CONTENT_TYPE) + // .expect("content type"), + // "application/octet-stream" + // ); + // assert_eq!( + // resp.headers() + // .get(header::CONTENT_DISPOSITION) + // .expect("content disposition"), + // "attachment; filename=\"test.binary\"" + // ); + + // let req = TestRequest::default().uri("/tests/").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::OK); + // 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\"" + // ); + + // // nonexistent index file + // let req = TestRequest::default().uri("/tests/unknown").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + // let req = TestRequest::default().uri("/tests/unknown/").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::NOT_FOUND); + // } + + // #[actix_rt::test] + // async fn test_serve_index_nested() { + // let st = Files::new(".").index_file("mod.rs"); + // let req = TestRequest::default().uri("/src/client").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // 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=\"mod.rs\"" + // ); + // } + + // #[actix_rt::test] + // fn integration_serve_index() { + // let mut srv = test::TestServer::with_factory(|| { + // App::new().handler( + // "test", + // Files::new(".").index_file("Cargo.toml"), + // ) + // }); + + // let request = srv.get().uri(srv.url("/test")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::OK); + // let bytes = srv.execute(response.body()).unwrap(); + // let data = Bytes::from(fs::read("Cargo.toml").unwrap()); + // assert_eq!(bytes, data); + + // let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::OK); + // let bytes = srv.execute(response.body()).unwrap(); + // let data = Bytes::from(fs::read("Cargo.toml").unwrap()); + // assert_eq!(bytes, data); + + // // nonexistent index file + // let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::NOT_FOUND); + + // let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::NOT_FOUND); + // } + + // #[actix_rt::test] + // fn integration_percent_encoded() { + // let mut srv = test::TestServer::with_factory(|| { + // App::new().handler( + // "test", + // Files::new(".").index_file("Cargo.toml"), + // ) + // }); + + // let request = srv + // .get() + // .uri(srv.url("/test/%43argo.toml")) + // .finish() + // .unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::OK); + // } +} diff --git a/src/files/named.rs b/src/files/named.rs new file mode 100644 index 0000000..59f3a54 --- /dev/null +++ b/src/files/named.rs @@ -0,0 +1,437 @@ +use std::fs::{File, Metadata}; +use std::io; +use std::ops::{Deref, DerefMut}; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[cfg(unix)] +use std::os::unix::fs::MetadataExt; + +use bitflags::bitflags; +use mime_guess::from_path; + +use actix_http::body::SizedStream; +use actix_web::dev::BodyEncoding; +use actix_web::http::header::{ + self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue, +}; +use actix_web::http::{ContentEncoding, StatusCode}; +use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder}; +use futures_util::future::{ready, Ready}; + +use super::chunked::ChunkedReadFile; +use actix_files::HttpRange; + +bitflags! { + pub(crate) struct Flags: u8 { + const ETAG = 0b0000_0001; + const LAST_MD = 0b0000_0010; + const CONTENT_DISPOSITION = 0b0000_0100; + } +} + +impl Default for Flags { + fn default() -> Self { + Flags::all() + } +} + +/// A file with an associated name. +#[derive(Debug)] +pub struct NamedFile { + path: PathBuf, + file: File, + modified: Option<SystemTime>, + pub(crate) md: Metadata, + pub(crate) flags: Flags, + pub(crate) status_code: StatusCode, + pub(crate) content_type: mime::Mime, + pub(crate) content_disposition: header::ContentDisposition, + pub(crate) encoding: Option<ContentEncoding>, +} + +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 + /// + /// ```rust + /// use actix_files::NamedFile; + /// use std::io::{self, Write}; + /// use std::env; + /// use std::fs::File; + /// + /// fn main() -> io::Result<()> { + /// 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<P: AsRef<Path>>(file: File, path: P) -> io::Result<NamedFile> { + 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 = from_path(&path).first_or_octet_stream(); + let disposition = match ct.type_() { + mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, + _ => DispositionType::Attachment, + }; + let mut parameters = vec![DispositionParam::Filename(String::from(filename.as_ref()))]; + 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 = file.metadata()?; + 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(), + }) + } + + /// Attempts to open a file in read-only mode. + /// + /// # Examples + /// + /// ```rust + /// use actix_files::NamedFile; + /// + /// let file = NamedFile::open("foo.txt"); + /// ``` + pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> { + Self::from_file(File::open(&path)?, path) + } + + /// Returns reference to the underlying `File` object. + #[inline] + pub fn file(&self) -> &File { + &self.file + } + + /// Retrieve the path of this file. + /// + /// # Examples + /// + /// ```rust + /// # use std::io; + /// use actix_files::NamedFile; + /// + /// # fn path() -> io::Result<()> { + /// let file = NamedFile::open("test.txt")?; + /// assert_eq!(file.path().as_os_str(), "foo.txt"); + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn path(&self) -> &Path { + self.path.as_path() + } + + /// Set response **Status Code** + pub fn set_status_code(mut self, status: StatusCode) -> Self { + self.status_code = status; + self + } + + /// Set the MIME Content-Type for 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::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, and video content 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](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy). + #[inline] + pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self { + self.content_disposition = cd; + self.flags.insert(Flags::CONTENT_DISPOSITION); + self + } + + /// Disable `Content-Disposition` header. + /// + /// By default Content-Disposition` header is enabled. + #[inline] + pub fn disable_content_disposition(mut self) -> Self { + self.flags.remove(Flags::CONTENT_DISPOSITION); + self + } + + /// Set content encoding for serving this file + #[inline] + pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { + self.encoding = Some(enc); + self + } + + #[inline] + ///Specifies whether to use ETag or not. + /// + ///Default is true. + pub fn use_etag(mut self, value: bool) -> Self { + self.flags.set(Flags::ETAG, value); + self + } + + #[inline] + ///Specifies whether to use Last-Modified or not. + /// + ///Default is true. + pub fn use_last_modified(mut self, value: bool) -> Self { + self.flags.set(Flags::LAST_MD, value); + self + } + + pub(crate) fn etag(&self) -> Option<header::EntityTag> { + // This etag format is similar to Apache's. + self.modified.as_ref().map(|mtime| { + let ino = { + #[cfg(unix)] + { + self.md.ino() + } + #[cfg(not(unix))] + { + 0 + } + }; + + let dur = mtime + .duration_since(UNIX_EPOCH) + .expect("modification time must be after epoch"); + header::EntityTag::strong(format!( + "{:x}:{:x}:{:x}:{:x}", + ino, + self.md.len(), + dur.as_secs(), + dur.subsec_nanos() + )) + }) + } + + pub(crate) fn last_modified(&self) -> Option<header::HttpDate> { + self.modified.map(|mtime| mtime.into()) + } + + pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Error> { + if self.status_code != StatusCode::OK { + let mut resp = HttpResponse::build(self.status_code); + resp.set(header::ContentType(self.content_type.clone())) + .if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| { + res.header( + header::CONTENT_DISPOSITION, + self.content_disposition.to_string(), + ); + }); + if let Some(current_encoding) = self.encoding { + resp.encoding(current_encoding); + } + let reader = ChunkedReadFile::new(self.md.len(), 0, Some(self.file), None, 0); + return Ok(resp.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.clone().into(); + let t2: SystemTime = since.clone().into(); + match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { + (Ok(t1), Ok(t2)) => t1 > t2, + _ => 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.clone().into(); + let t2: SystemTime = since.clone().into(); + match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { + (Ok(t1), Ok(t2)) => t1 <= t2, + _ => false, + } + } else { + false + }; + + let mut resp = HttpResponse::build(self.status_code); + resp.set(header::ContentType(self.content_type.clone())) + .if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| { + res.header( + header::CONTENT_DISPOSITION, + self.content_disposition.to_string(), + ); + }); + // default compressing + if let Some(current_encoding) = self.encoding { + resp.encoding(current_encoding); + } + + resp.if_some(last_modified, |lm, resp| { + resp.set(header::LastModified(lm)); + }) + .if_some(etag, |etag, resp| { + resp.set(header::ETag(etag)); + }); + + resp.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(rangesheader) = ranges.to_str() { + if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) { + length = rangesvec[0].length; + offset = rangesvec[0].start; + resp.encoding(ContentEncoding::Identity); + resp.header( + header::CONTENT_RANGE, + format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()), + ); + } else { + resp.header(header::CONTENT_RANGE, format!("bytes */{}", length)); + return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish()); + }; + } else { + return Ok(resp.status(StatusCode::BAD_REQUEST).finish()); + }; + }; + + if precondition_failed { + return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish()); + } else if not_modified { + return Ok(resp.status(StatusCode::NOT_MODIFIED).finish()); + } + + let reader = ChunkedReadFile::new(length, offset, Some(self.file), None, 0); + + if offset != 0 || length != self.md.len() { + resp.status(StatusCode::PARTIAL_CONTENT); + } + + Ok(resp.body(SizedStream::new(length, reader))) + } +} + +impl Deref for NamedFile { + type Target = File; + + fn deref(&self) -> &File { + &self.file + } +} + +impl DerefMut for NamedFile { + fn deref_mut(&mut self) -> &mut File { + &mut self.file + } +} + +/// 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::<header::IfMatch>() { + 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::<header::IfNoneMatch>() { + 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 Error = Error; + type Future = Ready<Result<HttpResponse, Error>>; + + fn respond_to(self, req: &HttpRequest) -> Self::Future { + ready(self.into_response(req)) + } +} diff --git a/src/files/pathbuf.rs b/src/files/pathbuf.rs new file mode 100644 index 0000000..467f556 --- /dev/null +++ b/src/files/pathbuf.rs @@ -0,0 +1,113 @@ +use actix_web::{dev::Payload, http::StatusCode, FromRequest, HttpRequest, ResponseError}; +use futures_util::future::{ready, Ready}; +use snafu::Snafu; +use std::path::PathBuf; + +#[derive(Snafu, Debug, PartialEq)] +pub enum Error { + /// The segment started with the wrapped invalid character. + #[snafu(display("The segment started with the wrapped invalid character."))] + BadStart { char: char }, + /// The segment contained the wrapped invalid character. + #[snafu(display("The segment contained the wrapped invalid character."))] + BadChar { char: char }, + /// The segment ended with the wrapped invalid character. + #[snafu(display("The segment ended with the wrapped invalid character."))] + BadEnd { char: char }, +} + +/// Return `BadRequest` for `Error`. +impl ResponseError for Error { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +type Result<T, E = Error> = std::result::Result<T, E>; + +#[derive(Debug)] +pub struct UriPathBuf(PathBuf); + +impl UriPathBuf { + pub fn new(path: &str) -> Result<Self> { + let mut buf = PathBuf::new(); + + for segment in path.split('/') { + if segment == ".." { + buf.pop(); + } else if segment.starts_with('.') { + return Err(Error::BadStart { char: '.' }); + } else if segment.starts_with('*') { + return Err(Error::BadStart { char: '*' }); + } else if segment.ends_with(':') { + return Err(Error::BadEnd { char: ':' }); + } else if segment.ends_with('>') { + return Err(Error::BadEnd { char: '>' }); + } else if segment.ends_with('<') { + return Err(Error::BadEnd { char: '<' }); + } else if segment.is_empty() { + continue; + } else if cfg!(windows) && segment.contains('\\') { + return Err(Error::BadChar { char: '\\' }); + } else { + buf.push(segment) + } + } + + Ok(UriPathBuf(buf)) + } +} + +impl FromRequest for UriPathBuf { + type Error = Error; + type Future = Ready<Result<Self, Self::Error>>; + type Config = (); + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ready(UriPathBuf::new(req.match_info().path())) + } +} + +impl From<UriPathBuf> for PathBuf { + fn from(buf: UriPathBuf) -> PathBuf { + buf.0 + } +} + +#[cfg(test)] +mod tests { + use super::{Error, UriPathBuf}; + use std::{iter::FromIterator, path::PathBuf}; + + #[actix_rt::test] + async fn test_path_buf() { + assert_eq!( + UriPathBuf::new("/test/.tt").map(|t| t.0), + Err(Error::BadStart { char: '.' }) + ); + assert_eq!( + UriPathBuf::new("/test/*tt").map(|t| t.0), + Err(Error::BadStart { char: '*' }) + ); + assert_eq!( + UriPathBuf::new("/test/tt:").map(|t| t.0), + Err(Error::BadEnd { char: ':' }) + ); + assert_eq!( + UriPathBuf::new("/test/tt<").map(|t| t.0), + Err(Error::BadEnd { char: '<' }) + ); + assert_eq!( + UriPathBuf::new("/test/tt>").map(|t| t.0), + Err(Error::BadEnd { char: '>' }) + ); + assert_eq!( + UriPathBuf::new("/seg1/seg2/").unwrap().0, + PathBuf::from_iter(vec!["seg1", "seg2"]) + ); + assert_eq!( + UriPathBuf::new("/seg1/../seg2/").unwrap().0, + PathBuf::from_iter(vec!["seg2"]) + ); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..958b56e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +#[macro_use] +extern crate log; + +pub mod bundle; +mod config; +pub mod files; +mod monitor; +mod stats; +pub mod thread; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c97f1f4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,157 @@ +#[macro_use] +extern crate log; + +use actix::prelude::*; +use lazy_static::lazy_static; +use monitor::Monitor; +use server::Server; +use snafu::{ResultExt, Snafu}; +use stats::StatsServer; +use std::sync::mpsc; +use std::{ + collections::HashSet, + net::SocketAddr, + sync::{Arc, RwLock}, +}; + +mod bundle; +mod config; +mod files; +mod monitor; +mod server; +mod stats; +mod thread; + +lazy_static! { + static ref MONITOR: Monitor = Monitor::new(); +} + +#[derive(Snafu, Debug)] +pub enum Error { + OpenConfig { + source: config::Error, + }, + Bind { + address: SocketAddr, + source: std::io::Error, + }, + Unbundle { + source: bundle::Error, + }, + Serve { + source: server::Error, + }, + ServeStats { + source: stats::Error, + }, + RecvNotify, +} + +type Result<T, E = Error> = std::result::Result<T, E>; + +#[actix_rt::main] +async fn main() -> Result<()> { + inner_main().await +} + +async fn inner_main() -> Result<()> { + MONITOR.init(); + + pretty_env_logger::init(); + + // Set up a channel for receiving thread notifications. + let (monitor_tx, monitor_rx) = mpsc::channel(); + + // Keep track of what threads have been started. + let mut server_thread_ids = HashSet::new(); + let mut server_thread_handles = vec![]; + + // Load config from current dir. + let config = Arc::new(config::from_current_dir().context(OpenConfig)?); + + // Set up unbundler. + let serve_dir = Arc::new(RwLock::new(None)); + let unbundler = Arc::new(bundle::Unbundler::new(config.clone(), serve_dir.clone())); + + // Set up main server. + let server = Server::new(config.server.clone(), serve_dir); + + let (server_handle, server_join_handle, server_thread_handle) = + server.spawn(monitor_tx.clone()).await.context(Serve)?; + + server_thread_ids.insert(server_join_handle.thread().id()); + server_thread_handles.push(server_thread_handle); + + // Set up optional stats server. + let mut maybe_stats_server_handle = None; + + match &config.stats { + Some(stats_config) => { + let stats_server = StatsServer::new(stats_config.clone(), unbundler.clone()); + + let (stats_server_handle, stats_join_handle, stats_thread_handle) = stats_server + .spawn(monitor_tx.clone()) + .await + .context(ServeStats)?; + + maybe_stats_server_handle = Some(stats_server_handle); + server_thread_ids.insert(stats_join_handle.thread().id()); + server_thread_handles.push(stats_thread_handle); + } + None => {} + } + + let (unbundler_join_handle, unbundler_thread_handle) = + thread::spawn(monitor_tx.clone(), move || { + let test = unbundler.clone(); + let mut sys = System::new("unbundler"); + + let result = sys + .block_on(async move { test.enter().await }) + .context(Unbundle); + + match result { + Err(e) => error!("Unbundler failed: {:?}", e), + Ok(_) => {} + } + }); + + let (_, monitor_thread_handle) = thread::spawn(monitor_tx.clone(), move || { + let mut watched_thread_ids = HashSet::new(); + + watched_thread_ids.insert(unbundler_join_handle.thread().id()); + + for server_thread_id in server_thread_ids { + watched_thread_ids.insert(server_thread_id); + } + + match MONITOR.watch(watched_thread_ids) { + Err(_) => error!("Failed to watch threads for panics."), + Ok(_) => {} + } + }); + + // Wait for a thread to finish. + loop { + monitor_rx.recv().map_err(|_| Error::RecvNotify)?; + + if Ok(true) == monitor_thread_handle.has_ended() { + info!("Stopping servers due to a panic."); + + break; + } else if Ok(true) == unbundler_thread_handle.has_ended() { + info!("Stopping servers due to unbundler shutdown."); + + break; + } + } + + // Stop server threads. + server_handle.stop(true).await; + + if let Some(stats_server_handle) = maybe_stats_server_handle { + stats_server_handle.stop(true).await; + } + + Ok(()) +} diff --git a/src/monitor.rs b/src/monitor.rs new file mode 100644 index 0000000..5625c5d --- /dev/null +++ b/src/monitor.rs @@ -0,0 +1,140 @@ +use snafu::Snafu; +use std::{ + collections::{HashMap, HashSet}, + panic, + sync::{Condvar, Mutex}, + thread::{self, Thread, ThreadId}, +}; + +#[derive(Debug, Snafu)] +pub enum Error { + WatchStateLock, + #[snafu(display("There should only be one active call to watch()."))] + MultipleWatches, +} + +pub type Result<T, E = Error> = std::result::Result<T, E>; + +pub struct State { + panicked: HashMap<ThreadId, Thread>, + watched: Option<HashSet<ThreadId>>, +} + +pub struct Monitor { + condvar: Condvar, + state: Mutex<State>, +} + +impl Monitor { + pub fn new() -> Self { + Monitor { + condvar: Condvar::new(), + state: Mutex::new(State { + panicked: HashMap::new(), + watched: None, + }), + } + } + + pub fn init(&'static self) -> () { + let hook = panic::take_hook(); + + panic::set_hook(Box::new(move |panic_info| { + match self.state.lock() { + Ok(mut state) => { + match &state.watched { + Some(watched) => { + let current_thread = thread::current(); + + // Only notify if the thread ID is being watched. + if watched.contains(¤t_thread.id()) { + state.panicked.insert(current_thread.id(), current_thread); + + self.condvar.notify_all(); + } + } + None => {} + } + } + Err(_) => error!("Unable to update map of panicked threads."), + } + + hook(panic_info) + })) + } + + pub fn watch(&self, thread_ids: HashSet<ThreadId>) -> Result<Vec<Thread>> { + let mut watched_panicked = vec![]; + let mut state = self.state.lock().map_err(|_| Error::WatchStateLock)?; + + if let Some(_) = state.watched { + return Err(Error::MultipleWatches); + } else if thread_ids.is_empty() { + return Ok(vec![]); + } + + state.panicked = HashMap::new(); + state.watched = Some(thread_ids.clone()); + + loop { + for thread_id in &thread_ids { + if let Some(thread) = state.panicked.get(&thread_id) { + watched_panicked.push(thread.clone().clone()); + } + } + + if !watched_panicked.is_empty() { + return Ok(watched_panicked); + } + + state = self + .condvar + .wait(state) + .map_err(|_| Error::WatchStateLock)?; + } + } +} + +#[cfg(test)] +mod tests { + use super::Monitor; + use lazy_static::lazy_static; + use std::{collections::HashSet, sync::mpsc, thread, time::Duration}; + + lazy_static! { + static ref MONITOR: Monitor = Monitor::new(); + } + + #[test] + pub fn test_watch() { + MONITOR.init(); + + MONITOR.watch(HashSet::new()).unwrap(); + + let (tx, rx) = mpsc::channel(); + + let handle = thread::spawn(move || { + rx.recv().unwrap(); + + panic!("Oh no"); + }); + + let mut thread_ids = HashSet::new(); + thread_ids.insert(handle.thread().id()); + + thread::sleep(Duration::from_millis(10)); + + let test_handle = thread::spawn(move || { + let watch_result = MONITOR.watch(thread_ids).unwrap(); + + assert_eq!(watch_result.is_empty(), false); + assert_eq!(watch_result[0].id(), handle.thread().id()); + }); + + thread::sleep(Duration::from_millis(10)); + + tx.send(true).unwrap(); + + test_handle.join().unwrap(); + } +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..e1bc12b --- /dev/null +++ b/src/server.rs @@ -0,0 +1,69 @@ +use crate::{config::ServerConfig, files::Files, thread, thread::ThreadHandle}; +use actix::System; +use actix_web::{ + dev::Server as ActixServer, middleware::Logger, App, HttpResponse, HttpServer, Responder, +}; +use snafu::{ResultExt, Snafu}; +use std::{ + path::PathBuf, + sync::{ + mpsc::{self, RecvError, SendError, Sender}, + Arc, RwLock, + }, + thread::{JoinHandle, Thread}, +}; + +#[derive(Debug, Snafu)] +pub enum Error { + ChannelSend { source: SendError<Server> }, + ChannelReceive { source: RecvError }, + Bind { source: std::io::Error }, + SystemRun { source: std::io::Error }, +} + +type Result<T, E = Error> = std::result::Result<T, E>; + +pub struct Server { + config: ServerConfig, + serve_dir: Arc<RwLock<Option<PathBuf>>>, +} + +impl Server { + pub fn new(config: ServerConfig, serve_dir: Arc<RwLock<Option<PathBuf>>>) -> Self { + Server { config, serve_dir } + } + + pub async fn spawn( + self, + notify_sender: Sender<Thread>, + ) -> Result<(ActixServer, JoinHandle<Result<()>>, ThreadHandle)> { + let (tx, rx) = mpsc::channel(); + + let server_address = self.config.address.clone(); + let serve_dir = Arc::clone(&self.serve_dir); + + let (join_handle, thread_handle) = thread::spawn(notify_sender, move || { + let sys = System::new("http-server"); + + let srv = HttpServer::new(move || { + App::new() + .wrap(Logger::default()) + .service(Files::new("/", Arc::clone(&serve_dir))) + }) + .bind(server_address) + .context(Bind)? + .shutdown_timeout(60) + .run(); + + let _ = tx.send(srv); + + sys.run().context(SystemRun) + }); + + Ok(( + rx.recv().context(ChannelReceive)?, + join_handle, + thread_handle, + )) + } +} diff --git a/src/stats.rs b/src/stats.rs new file mode 100644 index 0000000..1d73db0 --- /dev/null +++ b/src/stats.rs @@ -0,0 +1,76 @@ +use crate::thread::{self, ThreadHandle}; +use crate::{bundle::Unbundler, config::StatsConfig}; +use actix::System; +use actix_web::{dev::Server, middleware::Logger, App, HttpResponse, HttpServer, Responder}; +use mpsc::{RecvError, SendError, Sender}; +use snafu::{ResultExt, Snafu}; +use std::{ + sync::{mpsc, Arc}, + thread::{JoinHandle, Thread}, +}; + +#[derive(Debug, Snafu)] +pub enum Error { + ChannelSend { source: SendError<Server> }, + ChannelReceive { source: RecvError }, + Bind { source: std::io::Error }, + SystemRun { source: std::io::Error }, +} + +type Result<T, E = Error> = std::result::Result<T, E>; + +struct State { + unbundler: Arc<Unbundler>, +} + +pub struct StatsServer { + config: StatsConfig, + unbundler: Arc<Unbundler>, +} + +impl StatsServer { + pub fn new(config: StatsConfig, unbundler: Arc<Unbundler>) -> Self { + StatsServer { config, unbundler } + } + + pub async fn spawn( + self, + notify_sender: Sender<Thread>, + ) -> Result<(Server, JoinHandle<Result<()>>, ThreadHandle)> { + let (tx, rx) = mpsc::channel(); + + let (join_handle, thread_handle) = thread::spawn(notify_sender, move || { + let sys = System::new("stats-http-server"); + + let unbundler = self.unbundler.clone(); + let server_address = self.config.address; + + let srv = HttpServer::new(move || { + App::new() + .wrap(Logger::default()) + .route("/status", actix_web::web::get().to(get_status)) + .data(State { + unbundler: Arc::clone(&unbundler), + }) + }) + .bind(server_address) + .context(Bind)? + .shutdown_timeout(60) + .run(); + + tx.send(srv).context(ChannelSend)?; + + sys.run().context(SystemRun) + }); + + Ok(( + rx.recv().context(ChannelReceive)?, + join_handle, + thread_handle, + )) + } +} + +async fn get_status(data: actix_web::web::Data<State>) -> impl Responder { + HttpResponse::Ok().body(format!("Test {:?}", data.unbundler.get_status().unwrap())) +} diff --git a/src/thread.rs b/src/thread.rs new file mode 100644 index 0000000..5a5404c --- /dev/null +++ b/src/thread.rs @@ -0,0 +1,165 @@ +use mpsc::Sender; +use snafu::Snafu; +use std::{ + sync::{mpsc, Arc, RwLock}, + thread::JoinHandle, +}; + +#[derive(Snafu, Debug, PartialEq)] +pub enum Error { + // Unable to obtain a read lock to check the status of a thread. + LockRead, +} + +pub type Result<T, E = Error> = std::result::Result<T, E>; + +/// A lightweight abstration over a regular thread that provides an API for +/// determining if a thread has terminated. +pub struct ThreadHandle { + ended: Arc<RwLock<bool>>, +} + +impl ThreadHandle { + /// Attempts to check if the thread has ended. + /// + /// An error may be returned if the underlying channel is disconnected. + pub fn has_ended(&self) -> Result<bool> { + let result = self.ended.read().map_err(|_| Error::LockRead)?; + + Ok(result.clone()) + } +} + +/// Like `std::thread::spawn`, but returns a `ThreadHandle` instead. +/// +/// # Examples +/// +/// Create ten threads and wait for all threads to finish. +/// +/// ``` +/// use espresso::thread::spawn; +/// use std::{ +/// collections::HashMap, +/// sync::{mpsc, Arc, Barrier}, +/// }; +/// +/// let (monitor_tx, monitor_rx) = mpsc::channel(); +/// let barrier = Arc::new(Barrier::new(10)); +/// +/// let mut handles = HashMap::new(); +/// +/// for _ in 0..10 { +/// let bc = barrier.clone(); +/// +/// let (join_handle, thread_handle) = spawn(monitor_tx.clone(), move || { +/// /// Sync all threads. +/// bc.wait(); +/// }); +/// +/// handles.insert(join_handle.thread().id(), thread_handle); +/// } +/// +/// // Loop until we have been notified of every thread ending. +/// loop { +/// let thread = monitor_rx.recv().unwrap(); +/// +/// handles.remove(&thread.id()); +/// +/// if handles.is_empty() { +/// break; +/// } +/// } +/// ``` +pub fn spawn<F, T>( + notify_sender: Sender<std::thread::Thread>, + f: F, +) -> (JoinHandle<T>, ThreadHandle) +where + F: FnOnce() -> T, + F: Send + 'static, + T: Send + 'static, +{ + let ended = Arc::new(RwLock::new(false)); + let ended_for_spawn = ended.clone(); + + let join_handle = std::thread::spawn(move || { + let ended = ended_for_spawn.clone(); + + let result = f(); + + let mut ended = ended.write().unwrap(); + *ended = true; + notify_sender.send(std::thread::current()).unwrap(); + + result + }); + + (join_handle, ThreadHandle { ended }) +} + +#[cfg(test)] +mod tests { + use super::spawn; + use std::{ + collections::HashMap, + sync::{mpsc, Arc, Barrier}, + }; + + #[test] + fn test_spawn() { + let (monitor_tx, monitor_rx) = mpsc::channel(); + let (ready_tx, ready_rx) = mpsc::channel(); + let (end_tx, end_rx) = mpsc::channel(); + + let (join_handle, handle) = spawn(monitor_tx, move || { + ready_tx.send(()).unwrap(); + + end_rx.recv().unwrap(); + }); + + ready_rx.recv().unwrap(); + + assert_eq!((&handle).has_ended(), Ok(false)); + + end_tx.send(()).unwrap(); + + monitor_rx.recv().unwrap(); + join_handle.join().unwrap(); + + assert_eq!(handle.has_ended(), Ok(true)); + } + + #[test] + fn test_multiple() { + let (monitor_tx, monitor_rx) = mpsc::channel(); + let barrier = Arc::new(Barrier::new(11)); + + let mut handles = HashMap::new(); + + for _ in 0..10 { + let bc = barrier.clone(); + + let (join_handle, thread_handle) = spawn(monitor_tx.clone(), move || { + bc.wait(); + }); + + handles.insert(join_handle.thread().id(), thread_handle); + } + + for (_, handle) in &handles { + assert_eq!(handle.has_ended(), Ok(false)); + } + + barrier.wait(); + + loop { + let thread = monitor_rx.recv().unwrap(); + + handles.remove(&thread.id()); + + if handles.is_empty() { + break; + } + } + } +} diff --git a/stack.yaml b/stack.yaml deleted file mode 100644 index f2aa4ae..0000000 --- a/stack.yaml +++ /dev/null @@ -1,70 +0,0 @@ -# This file was automatically generated by 'stack init' -# -# Some commonly used options have been documented as comments in this file. -# For advanced use and comprehensive documentation of the format, please see: -# https://docs.haskellstack.org/en/stable/yaml_configuration/ - -# Resolver to choose a 'specific' stackage snapshot or a compiler version. -# A snapshot resolver dictates the compiler version and the set of packages -# to be used for project dependencies. For example: -# -# resolver: lts-3.5 -# resolver: nightly-2015-09-21 -# resolver: ghc-7.10.2 -# -# The location of a snapshot can be provided as a file or url. Stack assumes -# a snapshot provided as a file might change, whereas a url resource does not. -# -# resolver: ./custom-snapshot.yaml -# resolver: https://example.com/snapshots/2018-01-01.yaml -resolver: lts-14.17 - -# User packages to be built. -# Various formats can be used as shown in the example below. -# -# packages: -# - some-directory -# - https://example.com/foo/bar/baz-0.0.2.tar.gz -# subdirs: -# - auto-update -# - wai -packages: - - . - - ../kawaii -# Dependency packages to be pulled from upstream that are not in the resolver. -# These entries can reference officially published versions as well as -# forks / in-progress versions pinned to a git hash. For example: -# -# extra-deps: -# - acme-missiles-0.3 -# - git: https://github.com/commercialhaskell/stack.git -# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a -# -extra-deps: - [] - # - git: https://gitlab.chromabits.com/etcinit/kawaii.git - # commit: e44766f543b71895e3cbe1737fa99c9e5ddfc2ba -# -# Override default flag values for local packages and extra-deps -# flags: {} - -# Extra package databases containing global packages -# extra-package-dbs: [] - -# Control whether we use the GHC we find on the path -# system-ghc: true -# -# Require a specific version of stack, using version ranges -# require-stack-version: -any # Default -# require-stack-version: ">=2.1" -# -# Override the architecture used by stack, especially useful on Windows -# arch: i386 -# arch: x86_64 -# -# Extra directories used by stack for building -# extra-include-dirs: [/path/to/dir] -# extra-lib-dirs: [/path/to/dir] -# -# Allow a newer minor version of GHC than the snapshot specifies -# compiler-check: newer-minor diff --git a/stack.yaml.lock b/stack.yaml.lock deleted file mode 100644 index fc538c1..0000000 --- a/stack.yaml.lock +++ /dev/null @@ -1,12 +0,0 @@ -# This file was autogenerated by Stack. -# You should not edit this file by hand. -# For more information, please see the documentation at: -# https://docs.haskellstack.org/en/stable/lock_files - -packages: [] -snapshots: -- completed: - size: 524799 - url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/14/17.yaml - sha256: 1d72b33c0fc048e23f4f18fd76a6ad79dd1d8a3c054644098a71a09855e40c7c - original: lts-14.17 diff --git a/tests/test space.binary b/tests/test space.binary new file mode 100644 index 0000000..ef8ff02 --- /dev/null +++ b/tests/test space.binary @@ -0,0 +1 @@ +ÂTǑɂVù2þvI ª–\ÇRË™–ˆæeÞvDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ.–û®õpA!ûߦÙx j+Uc÷±©X”c%Û;ï"yìAI \ No newline at end of file diff --git a/tests/test.binary b/tests/test.binary new file mode 100644 index 0000000..ef8ff02 --- /dev/null +++ b/tests/test.binary @@ -0,0 +1 @@ +ÂTǑɂVù2þvI ª–\ÇRË™–ˆæeÞvDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ.–û®õpA!ûߦÙx j+Uc÷±©X”c%Û;ï"yìAI \ No newline at end of file diff --git a/tests/test.png b/tests/test.png new file mode 100644 index 0000000000000000000000000000000000000000..2ab19f24f851de6ff5b21662e718f56d27302930 GIT binary patch literal 67390 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 literal 0 HcmV?d00001 -- GitLab