diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000000000000000000000000000000000..f11c0f740bf6b37727e53c1297e7032a0b10cebe
--- /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 900662128b9e31de9a1cfcd594afd14c899aac0d..db38427607ec0b382995057c607becfd79047c10 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 af0ffabec76c7e2e8dca4a28234353f7b27796f9..0000000000000000000000000000000000000000
--- 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 0000000000000000000000000000000000000000..d9f8f39e0fbbee42fd30414f8523249f67e7ad5b
--- /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 0000000000000000000000000000000000000000..abd4dc25891214888245225b260400ffce83741a
--- /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 88da1d680cee424d0c4a37824667bea7e1cfb4bc..0000000000000000000000000000000000000000
--- 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 db8352385053e642d1d37203cae96b016d44e262..22642af27e747c9dcd7865e387c94e2ee306fee4 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 47e9bc59f486f722b53d7840730030f7f147b576..e198e0179f65dad82e25c216d82fb356410c96e6 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 9a994af677b0dfd41b4e3b76b3e7e604003d64e1..0000000000000000000000000000000000000000
--- 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 0000000000000000000000000000000000000000..0c762d684566f9dc7781b10e57c446783218164e
--- /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 deca5faa416db565c715052c3190bc52d43d39ab..0000000000000000000000000000000000000000
--- 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 2803992c78bea33a410623c489a3a813477af71c..0000000000000000000000000000000000000000
--- 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 442209d43fdad3f6225cbb18e6f8a03fc9297f3e..0000000000000000000000000000000000000000
--- 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 0000000000000000000000000000000000000000..47874a2040a200bac894ff0188105f5bf30863fb
--- /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 3067fa93ee3f76e893dd8944c587e2ce2777edb4..0000000000000000000000000000000000000000
--- 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 d00f6c37c0aabf213f882c634170031d80f00a8a..0000000000000000000000000000000000000000
--- 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 0000000000000000000000000000000000000000..dd3642730a10ba563cdaaf41dd56051c971f4835
--- /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 0000000000000000000000000000000000000000..6d22051982230eb46307b4b0db21e29248a32bbe
--- /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 0000000000000000000000000000000000000000..58eaf724965be0146eb030124155e5dd74997d4d
--- /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 0000000000000000000000000000000000000000..76823f14bf5d80e51453316feb74f201d7d0ae37
--- /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 0000000000000000000000000000000000000000..615ee313fbe63a52b55eef7339cc94520e5796d0
--- /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 0000000000000000000000000000000000000000..1787da477a139d6e1c83bb72b4089f9e7c043f63
--- /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(&current_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 0000000000000000000000000000000000000000..668268885df4855a2ba7b2d5c2fca17be23c318d
--- /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 0000000000000000000000000000000000000000..9816cb969095d8e075f7d089bb8585c9a46bd989
--- /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 0000000000000000000000000000000000000000..70a5e77d8ba1d0af6c44599bb9990eb641f964dd
--- /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 0000000000000000000000000000000000000000..f90069184f32680434762ec8a55d488b1984d503
--- /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 0000000000000000000000000000000000000000..86a76cf0662480fc72d21e1742b23f515c24121e
--- /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 0000000000000000000000000000000000000000..79a1cb11bc16a4a35d4c8e61693fa8c838815cad
--- /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)
+  };
+}
+
+// " -- &quot;  & -- &amp;  ' -- &#x27;  < -- &lt;  > -- &gt;  / -- &#x2f;
+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 0000000000000000000000000000000000000000..59f3a54fea5afe43d5bbb22f3d4127baff0c4f66
--- /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 0000000000000000000000000000000000000000..467f556b40926df4f47cd38ac891549a663389a7
--- /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 0000000000000000000000000000000000000000..958b56e81de1b156e42ce9da141bfc84b1566f00
--- /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 0000000000000000000000000000000000000000..c97f1f4806b80fb94296b013fb15cfd3743a66d7
--- /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 0000000000000000000000000000000000000000..5625c5dba4614f7e16f5e04c665cb5ccc6361c08
--- /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(&current_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 0000000000000000000000000000000000000000..e1bc12b22f0339e2fb00ecbbfe4db0a217ce5656
--- /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 0000000000000000000000000000000000000000..1d73db0587557d3e94cb1aa6ac28b7930521650f
--- /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 0000000000000000000000000000000000000000..5a5404cee876d16eddc9606c2b30ee0f8bb45021
--- /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 f2aa4aeda1d59fe3775e435f94799d4bbcab2084..0000000000000000000000000000000000000000
--- 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 fc538c1bebf491ab55d9fec36d5e617262615ccd..0000000000000000000000000000000000000000
--- 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 0000000000000000000000000000000000000000..ef8ff024553cf7d7c6cba31660555dabc915a519
--- /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 0000000000000000000000000000000000000000..ef8ff024553cf7d7c6cba31660555dabc915a519
--- /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
Binary files /dev/null and b/tests/test.png differ