diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 09637aa..a1cb45f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,15 +10,15 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - features: [--all-features, --features default, --features "service-rocket" --no-default-features] - os: [ubuntu-latest, windows-latest, macOS-latest] + os: [ubuntu-latest, windows-latest] + features: [--no-default-features, --features bundle-sqlite, --features ui] exclude: - os: windows-latest - features: --features "service-rocket" --no-default-features + features: --no-default-features steps: - name: Install libsqlite3-dev - if: contains(matrix.os, 'ubuntu') + if: contains(matrix.os, 'ubuntu') && !contains(matrix.features, 'bundle-sqlite') run: sudo apt-get update && sudo apt-get install libsqlite3-dev - uses: actions/checkout@v1 - uses: actions-rs/toolchain@v1 diff --git a/Cargo.lock b/Cargo.lock index 70cf37e..7553a58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,308 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "actix-codec" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78d1833b3838dbe990df0f1f87baf640cf6146e898166afe401839d1b001e570" +dependencies = [ + "bitflags", + "bytes 0.5.6", + "futures-core", + "futures-sink", + "log", + "pin-project 0.4.27", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-connect" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177837a10863f15ba8d3ae3ec12fac1099099529ed20083a27fdfe247381d0dc" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "derive_more", + "either", + "futures-util", + "http", + "log", + "trust-dns-proto", + "trust-dns-resolver", +] + +[[package]] +name = "actix-files" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d031468a7859f71674e5531bd05137e0ea5de05ec9a917314330b88c582e2e0a" +dependencies = [ + "actix-service", + "actix-web", + "bitflags", + "bytes 0.5.6", + "derive_more", + "futures-core", + "futures-util", + "log", + "mime", + "mime_guess", + "percent-encoding", + "v_htmlescape", +] + +[[package]] +name = "actix-http" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "452299e87817ae5673910e53c243484ca38be3828db819b6011736fc6982e874" +dependencies = [ + "actix-codec", + "actix-connect", + "actix-rt", + "actix-service", + "actix-threadpool", + "actix-utils", + "base64 0.13.0", + "bitflags", + "brotli2", + "bytes 0.5.6", + "cookie", + "copyless", + "derive_more", + "either", + "encoding_rs", + "flate2", + "futures-channel", + "futures-core", + "futures-util", + "fxhash", + "h2", + "http", + "httparse", + "indexmap", + "itoa", + "language-tags", + "lazy_static", + "log", + "mime", + "percent-encoding", + "pin-project 1.0.2", + "rand", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "sha-1 0.9.2", + "slab", + "time 0.2.23", +] + +[[package]] +name = "actix-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca8ce00b267af8ccebbd647de0d61e0674b6e61185cc7a592ff88772bed655" +dependencies = [ + "quote 1.0.7", + "syn 1.0.54", +] + +[[package]] +name = "actix-router" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd1f7dbda1645bf7da33554db60891755f6c01c1b2169e2f4c492098d30c235" +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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45407e6e672ca24784baa667c5d32ef109ccdd8d5e0b5ebb9ef8a67f4dfb708e" +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 0.4.27", +] + +[[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", + "threadpool", +] + +[[package]] +name = "actix-tls" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24789b7d7361cf5503a504ebe1c10806896f61e96eca9a7350e23001aca715fb" +dependencies = [ + "actix-codec", + "actix-service", + "actix-utils", + "futures-util", +] + +[[package]] +name = "actix-utils" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9022dec56632d1d7979e59af14f0597a28a830a9c1c7fec8b2327eb9f16b5a" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "bitflags", + "bytes 0.5.6", + "either", + "futures-channel", + "futures-sink", + "futures-util", + "log", + "pin-project 0.4.27", + "slab", +] + +[[package]] +name = "actix-web" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e641d4a172e7faa0862241a20ff4f1f5ab0ab7c279f00c2d4587b77483477b86" +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 0.5.6", + "derive_more", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "fxhash", + "log", + "mime", + "pin-project 1.0.2", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "socket2", + "time 0.2.23", + "tinyvec 1.1.0", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad26f77093333e0e7c6ffe54ebe3582d908a104e448723eec6d43d08b07143fb" +dependencies = [ + "proc-macro2", + "quote 1.0.7", + "syn 1.0.54", +] + +[[package]] +name = "actix-web-httpauth" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "536a75d767c5c2b3e64d3f569621f38ed7609359a0c82d149c88290a6ba41b22" +dependencies = [ + "actix-service", + "actix-web", + "base64 0.12.3", + "bytes 0.5.6", + "futures-util", +] + +[[package]] +name = "addr2line" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "0.2.3" @@ -12,61 +315,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" -[[package]] -name = "aead" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf01b9b56e767bb57b94ebf91a58b338002963785cdd7013e21c0d4679471e4" -dependencies = [ - "generic-array 0.12.3", -] - -[[package]] -name = "aes" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54eb1d8fe354e5fc611daf4f2ea97dd45a765f4f1e4512306ec183ae2e8f20c9" -dependencies = [ - "aes-soft", - "aesni", - "block-cipher-trait", -] - -[[package]] -name = "aes-gcm" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "834a6bda386024dbb7c8fc51322856c10ffe69559f972261c868485f5759c638" -dependencies = [ - "aead", - "aes", - "block-cipher-trait", - "ghash", - "subtle 2.3.0", - "zeroize", -] - -[[package]] -name = "aes-soft" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd7e7ae3f9a1fb5c03b389fc6bb9a51400d0c13053f0dca698c832bfd893a0d" -dependencies = [ - "block-cipher-trait", - "byteorder", - "opaque-debug 0.2.3", -] - -[[package]] -name = "aesni" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f70a6b5f971e473091ab7cfb5ffac6cde81666c4556751d8d5620ead8abf100" -dependencies = [ - "block-cipher-trait", - "opaque-debug 0.2.3", -] - [[package]] name = "aho-corasick" version = "0.7.15" @@ -92,14 +340,14 @@ dependencies = [ ] [[package]] -name = "atty" -version = "0.2.14" +name = "async-trait" +version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" dependencies = [ - "hermit-abi", - "libc", - "winapi 0.3.9", + "proc-macro2", + "quote 1.0.7", + "syn 1.0.54", ] [[package]] @@ -108,22 +356,50 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "awc" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b381e490e7b0cfc37ebc54079b0413d8093ef43d14a4e4747083f7fa47a9e691" +dependencies = [ + "actix-codec", + "actix-http", + "actix-rt", + "actix-service", + "base64 0.13.0", + "bytes 0.5.6", + "cfg-if 1.0.0", + "derive_more", + "futures-core", + "log", + "mime", + "percent-encoding", + "rand", + "serde", + "serde_json", + "serde_urlencoded", +] + +[[package]] +name = "backtrace" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" +dependencies = [ + "addr2line", + "cfg-if 1.0.0", + "libc", + "miniz_oxide 0.4.3", + "object", + "rustc-demangle", +] + [[package]] name = "base-x" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" -[[package]] -name = "base64" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" -dependencies = [ - "byteorder", - "safemem", -] - [[package]] name = "base64" version = "0.12.3" @@ -163,15 +439,6 @@ dependencies = [ "generic-array 0.14.4", ] -[[package]] -name = "block-cipher-trait" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774" -dependencies = [ - "generic-array 0.12.3", -] - [[package]] name = "block-padding" version = "0.1.5" @@ -181,6 +448,35 @@ dependencies = [ "byte-tools", ] +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881e704e61d0fb41d7c6c9ae2bd790eb8c13dc974ae102fb98c788b4fdea4349" +dependencies = [ + "bytes 0.6.0", +] + [[package]] name = "bumpalo" version = "3.4.0" @@ -211,6 +507,21 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" +[[package]] +name = "bytes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0dcbc35f504eb6fc275a6d20e4ebcda18cf50d40ba6fabff8c711fa16cb3b16" + +[[package]] +name = "bytestring" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7c05fa5172da78a62d9949d662d2ac89d4cc7355d7b49adee5163f1fb3f363" +dependencies = [ + "bytes 0.5.6", +] + [[package]] name = "cc" version = "1.0.66" @@ -260,29 +571,18 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c478836e029dcef17fb47c89023448c64f781a046e0300e257ad8225ae59afab" -[[package]] -name = "cookie" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5795cda0897252e34380a27baf884c53aa7ad9990329cdad96d4c5d027015d44" -dependencies = [ - "aes-gcm", - "base64 0.12.3", - "hkdf", - "hmac 0.7.1", - "percent-encoding 2.1.0", - "rand", - "sha2 0.8.2", - "time 0.1.44", -] - [[package]] name = "cookie" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f" dependencies = [ - "percent-encoding 2.1.0", + "base64 0.12.3", + "hkdf", + "hmac", + "percent-encoding", + "rand", + "sha2", "time 0.2.23", "version_check 0.9.2", ] @@ -293,16 +593,22 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3818dfca4b0cb5211a659bbcbb94225b7127407b2b135e650d717bfb78ab10d3" dependencies = [ - "cookie 0.14.3", - "idna 0.2.0", - "log 0.4.11", + "cookie", + "idna", + "log", "publicsuffix", "serde", "serde_json", "time 0.2.23", - "url 2.2.0", + "url", ] +[[package]] +name = "copyless" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" + [[package]] name = "cpuid-bool" version = "0.1.2" @@ -364,16 +670,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "crypto-mac" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" -dependencies = [ - "generic-array 0.12.3", - "subtle 1.0.0", -] - [[package]] name = "crypto-mac" version = "0.10.0" @@ -381,7 +677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" dependencies = [ "generic-array 0.14.4", - "subtle 2.3.0", + "subtle", ] [[package]] @@ -395,35 +691,14 @@ dependencies = [ ] [[package]] -name = "devise" -version = "0.2.0" +name = "derive_more" +version = "0.99.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74e04ba2d03c5fa0d954c061fc8c9c288badadffc272ebb87679a89846de3ed3" +checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" dependencies = [ - "devise_codegen", - "devise_core", -] - -[[package]] -name = "devise_codegen" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "066ceb7928ca93a9bedc6d0e612a8a0424048b0ab1f75971b203d01420c055d7" -dependencies = [ - "devise_core", - "quote 0.6.13", -] - -[[package]] -name = "devise_core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf41c59b22b5e3ec0ea55c7847e5f358d340f3a8d6d53a5cf4f1564967f96487" -dependencies = [ - "bitflags", - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", + "proc-macro2", + "quote 1.0.7", + "syn 1.0.54", ] [[package]] @@ -444,7 +719,7 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" dependencies = [ - "proc-macro2 1.0.24", + "proc-macro2", "quote 1.0.7", "syn 1.0.54", ] @@ -553,6 +828,27 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" +[[package]] +name = "encoding_rs" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801bbab217d7f79c0062f4f7205b5d4427c6d1a7bd7aafdd1475f7c59d62b283" +dependencies = [ + "cfg-if 1.0.0", +] + +[[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 1.0.7", + "syn 1.0.54", +] + [[package]] name = "error-chain" version = "0.12.4" @@ -568,18 +864,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" -[[package]] -name = "filetime" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c122a393ea57648015bf06fbd3d372378992e86b9ff5a7a497b076a28c79efe" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "redox_syscall", - "winapi 0.3.9", -] - [[package]] name = "flate2" version = "1.0.19" @@ -605,26 +889,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" dependencies = [ "matches", - "percent-encoding 2.1.0", -] - -[[package]] -name = "fsevent" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" -dependencies = [ - "bitflags", - "fsevent-sys", -] - -[[package]] -name = "fsevent-sys" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" -dependencies = [ - "libc", + "percent-encoding", ] [[package]] @@ -643,6 +908,98 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +[[package]] +name = "futures" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3b0c040a1fe6529d30b3c5944b280c7f0dcb2930d2c3062bca967b602583d0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7109687aa4e177ef6fe84553af6280ef2778bdb7783ba44c9dc3399110fe64" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748" + +[[package]] +name = "futures-io" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611834ce18aaa1bd13c4b374f5d653e1027cf99b6b502584ff8c9a64413b30bb" + +[[package]] +name = "futures-macro" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote 1.0.7", + "syn 1.0.54", +] + +[[package]] +name = "futures-sink" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f878195a49cee50e006b02b93cf7e0a95a38ac7b776b4c4d9cc1207cd20fcb3d" + +[[package]] +name = "futures-task" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c554eb5bf48b2426c4771ab68c6b14468b6e76cc90996f528c3338d761a4d0d" +dependencies = [ + "once_cell", +] + +[[package]] +name = "futures-util" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project 1.0.2", + "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.12.3" @@ -682,15 +1039,6 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1", ] -[[package]] -name = "ghash" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f0930ed19a7184089ea46d2fedead2f6dc2b674c5db4276b7da336c7cd83252" -dependencies = [ - "polyval", -] - [[package]] name = "gif" version = "0.11.1" @@ -702,10 +1050,30 @@ dependencies = [ ] [[package]] -name = "glob" -version = "0.3.0" +name = "gimli" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" + +[[package]] +name = "h2" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", + "tracing-futures", +] [[package]] name = "hashbrown" @@ -721,11 +1089,11 @@ checksum = "ed18eb2459bf1a09ad2d6b1547840c3e5e62882fa09b9a6a20b1de8e3228848f" dependencies = [ "base64 0.12.3", "bitflags", - "bytes", + "bytes 0.5.6", "headers-core", "http", - "mime 0.3.16", - "sha-1", + "mime", + "sha-1 0.8.2", "time 0.1.44", ] @@ -738,6 +1106,15 @@ dependencies = [ "http", ] +[[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.17" @@ -755,22 +1132,12 @@ checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" [[package]] name = "hkdf" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fa08a006102488bd9cd5b8013aabe84955cf5ae22e304c2caf655b633aefae3" +checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" dependencies = [ - "digest 0.8.1", - "hmac 0.7.1", -] - -[[package]] -name = "hmac" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695" -dependencies = [ - "crypto-mac 0.7.0", - "digest 0.8.1", + "digest 0.9.0", + "hmac", ] [[package]] @@ -779,17 +1146,28 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" dependencies = [ - "crypto-mac 0.10.0", + "crypto-mac", "digest 0.9.0", ] [[package]] -name = "http" -version = "0.2.1" +name = "hostname" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" dependencies = [ - "bytes", + "libc", + "match_cfg", + "winapi 0.3.9", +] + +[[package]] +name = "http" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84129d298a6d57d246960ff8eb831ca4af3f96d29e2e28848dae275408658e26" +dependencies = [ + "bytes 0.5.6", "fnv", "itoa", ] @@ -800,25 +1178,6 @@ version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" -[[package]] -name = "hyper" -version = "0.10.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a0652d9a2609a968c14be1a9ea00bf4b1d64e2e1f53a1b51b6fff3a6e829273" -dependencies = [ - "base64 0.9.3", - "httparse", - "language-tags", - "log 0.3.9", - "mime 0.2.6", - "num_cpus", - "time 0.1.44", - "traitobject", - "typeable", - "unicase", - "url 1.7.2", -] - [[package]] name = "id3" version = "0.5.1" @@ -832,17 +1191,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "idna" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "0.2.0" @@ -881,26 +1229,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "inotify" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" -dependencies = [ - "bitflags", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4563555856585ab3180a5bf0b2f9f8d301a728462afffc8195b3f5394229c55" -dependencies = [ - "libc", -] - [[package]] name = "instant" version = "0.1.9" @@ -919,6 +1247,18 @@ 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" @@ -965,12 +1305,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "lewton" version = "0.10.1" @@ -999,6 +1333,12 @@ dependencies = [ "vcpkg", ] +[[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.4.2" @@ -1008,15 +1348,6 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "log" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" -dependencies = [ - "log 0.4.11", -] - [[package]] name = "log" version = "0.4.11" @@ -1026,6 +1357,21 @@ dependencies = [ "cfg-if 0.1.10", ] +[[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" @@ -1061,7 +1407,7 @@ checksum = "4685bf0039a9d2919c2dbb281cba1c58d168dce58f519cbd70c468ce2c36a748" dependencies = [ "byteorder", "hex", - "log 0.4.11", + "log", ] [[package]] @@ -1080,26 +1426,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c" dependencies = [ "migrations_internals", - "proc-macro2 1.0.24", + "proc-macro2", "quote 1.0.7", "syn 1.0.54", ] -[[package]] -name = "mime" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" -dependencies = [ - "log 0.3.9", -] - [[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.3.7" @@ -1131,7 +1478,7 @@ dependencies = [ "iovec", "kernel32-sys", "libc", - "log 0.4.11", + "log", "miow", "net2", "slab", @@ -1139,15 +1486,14 @@ dependencies = [ ] [[package]] -name = "mio-extras" -version = "2.0.6" +name = "mio-uds" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" dependencies = [ - "lazycell", - "log 0.4.11", + "iovec", + "libc", "mio", - "slab", ] [[package]] @@ -1199,21 +1545,13 @@ dependencies = [ ] [[package]] -name = "notify" -version = "4.0.15" +name = "nom" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80ae4a7688d1fab81c5bf19c64fc8db920be8d519ce6336ed4e7efe024724dbd" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" dependencies = [ - "bitflags", - "filetime", - "fsevent", - "fsevent-sys", - "inotify", - "libc", - "mio", - "mio-extras", - "walkdir", - "winapi 0.3.9", + "memchr", + "version_check 0.1.5", ] [[package]] @@ -1267,6 +1605,12 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" + [[package]] name = "ogg" version = "0.7.1" @@ -1332,48 +1676,78 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3b8c0d71734018084da0c0354193a5edfb81b20d2d57a92c5b154aefc554a4a" dependencies = [ "base64 0.13.0", - "crypto-mac 0.10.0", - "hmac 0.10.1", + "crypto-mac", + "hmac", "rand", "rand_core", - "sha2 0.9.2", - "subtle 2.3.0", + "sha2", + "subtle", ] -[[package]] -name = "pear" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5320f212db967792b67cfe12bd469d08afd6318a249bd917d5c19bc92200ab8a" -dependencies = [ - "pear_codegen", -] - -[[package]] -name = "pear_codegen" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfc1c836fdc3d1ef87c348b237b5b5c4dff922156fb2d968f57734f9669768ca" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", - "version_check 0.9.2", - "yansi", -] - -[[package]] -name = "percent-encoding" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" - [[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.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffbc8e94b38ea3d2d8ba92aea2983b503cd75d0888d75b86bb37970b5698e15" +dependencies = [ + "pin-project-internal 0.4.27", +] + +[[package]] +name = "pin-project" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ccc2237c2c489783abd8c4c80e5450fc0e98644555b1364da68cc29aa151ca7" +dependencies = [ + "pin-project-internal 1.0.2", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" +dependencies = [ + "proc-macro2", + "quote 1.0.7", + "syn 1.0.54", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8e8d2bf0b23038a4424865103a4df472855692821aab4e4f5c3312d461d9e5f" +dependencies = [ + "proc-macro2", + "quote 1.0.7", + "syn 1.0.54", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" + +[[package]] +name = "pin-project-lite" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b063f57ec186e6140e2b8b6921e5f1bd89c7356dda5b33acc5401203ca6131c" + +[[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.19" @@ -1396,13 +1770,17 @@ dependencies = [ name = "polaris" version = "0.0.0" dependencies = [ + "actix-files", + "actix-web", + "actix-web-httpauth", "anyhow", "ape", "base64 0.13.0", - "cookie 0.14.3", + "cookie", "crossbeam-channel", "diesel", "diesel_migrations", + "futures-util", "getopts", "headers", "http", @@ -1410,19 +1788,17 @@ dependencies = [ "image", "lewton", "libsqlite3-sys", - "log 0.4.11", + "log", "metaflac", "mp3-duration", "mp4ameta", "num_cpus", "opus_headers", "pbkdf2", - "percent-encoding 2.1.0", + "percent-encoding", "rand", "rayon", "regex", - "rocket", - "rocket_contrib", "rustfm-scrobble", "sd-notify", "serde", @@ -1430,25 +1806,15 @@ dependencies = [ "serde_json", "simplelog", "thiserror", - "time 0.1.44", - "toml 0.5.7", + "time 0.2.23", + "toml", "unix-daemonize", "ureq", - "url 2.2.0", + "url", "uuid", "winapi 0.3.9", ] -[[package]] -name = "polyval" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec3341498978de3bfd12d1b22f1af1de22818f5473a11e8a6ef997989e3a212" -dependencies = [ - "cfg-if 0.1.10", - "universal-hash", -] - [[package]] name = "ppv-lite86" version = "0.2.10" @@ -1462,13 +1828,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] -name = "proc-macro2" -version = "0.4.30" +name = "proc-macro-nested" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" -dependencies = [ - "unicode-xid 0.1.0", -] +checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" [[package]] name = "proc-macro2" @@ -1486,10 +1849,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bbaa49075179162b49acac1c6aa45fb4dafb5f13cf6794276d77bc7fd95757b" dependencies = [ "error-chain", - "idna 0.2.0", + "idna", "lazy_static", "regex", - "url 2.2.0", + "url", ] [[package]] @@ -1498,31 +1861,28 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" dependencies = [ - "percent-encoding 2.1.0", + "percent-encoding", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" -[[package]] -name = "quote" -version = "0.6.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" -dependencies = [ - "proc-macro2 0.4.30", -] - [[package]] name = "quote" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" dependencies = [ - "proc-macro2 1.0.24", + "proc-macro2", ] [[package]] @@ -1531,7 +1891,7 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f" dependencies = [ - "log 0.4.11", + "log", "parking_lot", "scheduled-thread-pool", ] @@ -1626,6 +1986,16 @@ version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + [[package]] name = "ring" version = "0.16.19" @@ -1642,70 +2012,10 @@ dependencies = [ ] [[package]] -name = "rocket" -version = "0.4.6" +name = "rustc-demangle" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc7e5d6aaa32ace6893ae8a1875688ca7b07d6c2428ae88e704c3623c8866e9" -dependencies = [ - "atty", - "base64 0.12.3", - "log 0.4.11", - "memchr", - "num_cpus", - "pear", - "rocket_codegen", - "rocket_http", - "state", - "time 0.1.44", - "toml 0.4.10", - "version_check 0.9.2", - "yansi", -] - -[[package]] -name = "rocket_codegen" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519154b16df5fe552a8f9cd76a97793a9f5d58e34f186ab79c7b29ce1d009358" -dependencies = [ - "devise", - "glob", - "indexmap", - "quote 0.6.13", - "rocket_http", - "version_check 0.9.2", - "yansi", -] - -[[package]] -name = "rocket_contrib" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9465babd59cfb360669b60431db510f3dc3268d51ccd69fc9264e626681e596a" -dependencies = [ - "log 0.4.11", - "notify", - "rocket", - "serde", - "serde_json", -] - -[[package]] -name = "rocket_http" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d087de7203c7a60a0ed5cd3a135b552dbfbed9932c52d49d083e8629935257" -dependencies = [ - "cookie 0.11.3", - "hyper", - "indexmap", - "pear", - "percent-encoding 1.0.1", - "smallvec", - "state", - "time 0.1.44", - "unicode-xid 0.1.0", -] +checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" [[package]] name = "rustc_version" @@ -1736,7 +2046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b" dependencies = [ "base64 0.13.0", - "log 0.4.11", + "log", "ring", "sct", "webpki", @@ -1748,21 +2058,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "scheduled-thread-pool" version = "0.2.5" @@ -1824,7 +2119,7 @@ version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" dependencies = [ - "proc-macro2 1.0.24", + "proc-macro2", "quote 1.0.7", "syn 1.0.54", ] @@ -1840,6 +2135,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha-1" version = "0.8.2" @@ -1852,24 +2159,25 @@ dependencies = [ "opaque-debug 0.2.3", ] +[[package]] +name = "sha-1" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3cdf1b5e620a498ee6f2a171885ac7e22f0e12089ec4b3d22b84921792507c" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpuid-bool", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + [[package]] name = "sha1" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" -[[package]] -name = "sha2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - [[package]] name = "sha2" version = "0.9.2" @@ -1883,6 +2191,15 @@ dependencies = [ "opaque-debug 0.3.0", ] +[[package]] +name = "signal-hook-registry" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce32ea0c6c56d5eacaeb814fbed9960547021d3edd010ded1425f180536b20ab" +dependencies = [ + "libc", +] + [[package]] name = "simplelog" version = "0.8.0" @@ -1890,7 +2207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b2736f58087298a448859961d3f4a0850b832e72619d75adc69da7993c2cd3c" dependencies = [ "chrono", - "log 0.4.11", + "log", "termcolor", ] @@ -1906,6 +2223,18 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75" +[[package]] +name = "socket2" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "winapi 0.3.9", +] + [[package]] name = "spin" version = "0.5.2" @@ -1921,12 +2250,6 @@ dependencies = [ "version_check 0.9.2", ] -[[package]] -name = "state" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3015a7d0a5fd5105c91c3710d42f9ccf0abfb287d62206484dcc67f9569a6483" - [[package]] name = "stdweb" version = "0.4.20" @@ -1947,7 +2270,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" dependencies = [ - "proc-macro2 1.0.24", + "proc-macro2", "quote 1.0.7", "serde", "serde_derive", @@ -1961,7 +2284,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" dependencies = [ "base-x", - "proc-macro2 1.0.24", + "proc-macro2", "quote 1.0.7", "serde", "serde_derive", @@ -1976,12 +2299,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" -[[package]] -name = "subtle" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" - [[package]] name = "subtle" version = "2.3.0" @@ -1999,24 +2316,13 @@ dependencies = [ "unicode-xid 0.0.4", ] -[[package]] -name = "syn" -version = "0.15.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "unicode-xid 0.1.0", -] - [[package]] name = "syn" version = "1.0.54" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44" dependencies = [ - "proc-macro2 1.0.24", + "proc-macro2", "quote 1.0.7", "unicode-xid 0.2.1", ] @@ -2054,7 +2360,7 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56" dependencies = [ - "proc-macro2 1.0.24", + "proc-macro2", "quote 1.0.7", "syn 1.0.54", ] @@ -2068,6 +2374,15 @@ 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.44" @@ -2111,7 +2426,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" dependencies = [ "proc-macro-hack", - "proc-macro2 1.0.24", + "proc-macro2", "quote 1.0.7", "standback", "syn 1.0.54", @@ -2139,12 +2454,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] -name = "toml" -version = "0.4.10" +name = "tokio" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f" +checksum = "a6d7ad61edd59bfcc7e80dababf0f4aed2e6d5e0ba1659356ae889752dfc12ff" dependencies = [ - "serde", + "bytes 0.5.6", + "futures-core", + "iovec", + "lazy_static", + "libc", + "memchr", + "mio", + "mio-uds", + "pin-project-lite 0.1.11", + "signal-hook-registry", + "slab", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes 0.5.6", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.1.11", + "tokio", ] [[package]] @@ -2157,16 +2497,75 @@ dependencies = [ ] [[package]] -name = "traitobject" -version = "0.1.0" +name = "tracing" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" +checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3" +dependencies = [ + "cfg-if 1.0.0", + "log", + "pin-project-lite 0.2.0", + "tracing-core", +] [[package]] -name = "typeable" -version = "0.1.2" +name = "tracing-core" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-futures" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c" +dependencies = [ + "pin-project 0.4.27", + "tracing", +] + +[[package]] +name = "trust-dns-proto" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53861fcb288a166aae4c508ae558ed18b53838db728d4d310aad08270a7d4c2b" +dependencies = [ + "async-trait", + "backtrace", + "enum-as-inner", + "futures", + "idna", + "lazy_static", + "log", + "rand", + "smallvec", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6759e8efc40465547b0dfce9500d733c65f969a4cbbfbe3ccf68daaa46ef179e" +dependencies = [ + "backtrace", + "cfg-if 0.1.10", + "futures", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "trust-dns-proto", +] [[package]] name = "typenum" @@ -2176,11 +2575,11 @@ checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" [[package]] name = "unicase" -version = "1.4.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" dependencies = [ - "version_check 0.1.5", + "version_check 0.9.2", ] [[package]] @@ -2201,6 +2600,12 @@ dependencies = [ "tinyvec 1.1.0", ] +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + [[package]] name = "unicode-width" version = "0.1.8" @@ -2213,28 +2618,12 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" -[[package]] -name = "unicode-xid" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" - [[package]] name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" -[[package]] -name = "universal-hash" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0c900f2f9b4116803415878ff48b63da9edb268668e08cf9292d7503114a01" -dependencies = [ - "generic-array 0.12.3", - "subtle 2.3.0", -] - [[package]] name = "unix-daemonize" version = "0.1.2" @@ -2258,28 +2647,17 @@ checksum = "294b85ef5dbc3670a72e82a89971608a1fcc4ed5c7c5a2895230d31a95f0569b" dependencies = [ "base64 0.13.0", "chunked_transfer", - "cookie 0.14.3", + "cookie", "cookie_store", - "log 0.4.11", + "log", "once_cell", "qstring", "rustls", - "url 2.2.0", + "url", "webpki", "webpki-roots", ] -[[package]] -name = "url" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" -dependencies = [ - "idna 0.1.5", - "matches", - "percent-encoding 1.0.1", -] - [[package]] name = "url" version = "2.2.0" @@ -2287,9 +2665,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" dependencies = [ "form_urlencoded", - "idna 0.2.0", + "idna", "matches", - "percent-encoding 2.1.0", + "percent-encoding", ] [[package]] @@ -2298,6 +2676,38 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" +[[package]] +name = "v_escape" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccca9e73c678b882900cbaec16dae4d3662ace5a17774ac45af04e0f3988fafa" +dependencies = [ + "buf-min", + "v_escape_derive", +] + +[[package]] +name = "v_escape_derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c860ad1273f4eee7006cee05db20c9e60e5d24cba024a32e1094aa8e574f3668" +dependencies = [ + "nom", + "proc-macro2", + "quote 1.0.7", + "syn 1.0.54", +] + +[[package]] +name = "v_htmlescape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db00c903248abee8499af60bf20d242e7882335bbbffd2614915184cbb207402" +dependencies = [ + "cfg-if 1.0.0", + "v_escape", +] + [[package]] name = "vcpkg" version = "0.2.10" @@ -2316,17 +2726,6 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" -[[package]] -name = "walkdir" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" -dependencies = [ - "same-file", - "winapi 0.3.9", - "winapi-util", -] - [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -2357,8 +2756,8 @@ checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" dependencies = [ "bumpalo", "lazy_static", - "log 0.4.11", - "proc-macro2 1.0.24", + "log", + "proc-macro2", "quote 1.0.7", "syn 1.0.54", "wasm-bindgen-shared", @@ -2380,7 +2779,7 @@ version = "0.2.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" dependencies = [ - "proc-macro2 1.0.24", + "proc-macro2", "quote 1.0.7", "syn 1.0.54", "wasm-bindgen-backend", @@ -2428,6 +2827,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2bb9fc8309084dd7cd651336673844c1d47f8ef6d2091ec160b27f5c4aa277" +[[package]] +name = "widestring" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" + [[package]] name = "winapi" version = "0.2.8" @@ -2471,6 +2876,15 @@ 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 = "wrapped-vec" version = "0.2.1" @@ -2490,15 +2904,3 @@ dependencies = [ "winapi 0.2.8", "winapi-build", ] - -[[package]] -name = "yansi" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" - -[[package]] -name = "zeroize" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f33972566adbd2d3588b0491eb94b98b43695c4ef897903470ede4f3f5a28a" diff --git a/Cargo.toml b/Cargo.toml index f430772..c14ed3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,21 +5,25 @@ authors = ["Antoine Gersant "] edition = "2018" [features] -default = ["service-rocket", "bundle-sqlite"] -ui = ["uuid", "winapi"] -service-rocket = ["rocket", "rocket_contrib"] +default = ["bundle-sqlite"] bundle-sqlite = ["libsqlite3-sys"] +ui = ["uuid", "winapi"] [dependencies] +actix-files = { version = "0.4" } +actix-web = { version = "3" } +actix-web-httpauth = { version = "0.5.0" } anyhow = "1.0.35" ape = "0.3.0" base64 = "0.13" +cookie = { version = "0.14", features = ["signed", "key-expansion"] } crossbeam-channel = "0.5" diesel_migrations = { version = "1.4", features = ["sqlite"] } +futures-util = { version = "0.3" } getopts = "0.2.15" +http = "0.2.2" id3 = "0.5.1" libsqlite3-sys = { version = "0.18", features = ["bundled", "bundled-windows"], optional = true } -rustfm-scrobble = "1.1" lewton = "0.10.1" log = "0.4.5" metaflac = "0.2.3" @@ -27,17 +31,18 @@ mp3-duration = "0.1.9" mp4ameta = "0.7.1" num_cpus = "1.13.0" opus_headers = "0.1.2" +percent-encoding = "2.1" pbkdf2 = "0.6" rand = "0.7" rayon = "1.3" regex = "1.3.9" -rocket = { version = "0.4.5", optional = true } +rustfm-scrobble = "1.1" serde = { version = "1.0.111", features = ["derive"] } serde_derive = "1.0.111" serde_json = "1.0.53" simplelog = "0.8.0" thiserror = "1.0.19" -time = "0.1" +time = "0.2" toml = "0.5" ureq = "1.5" url = "2.1" @@ -52,12 +57,6 @@ version = "0.23.12" default_features = false features = ["bmp", "gif", "jpeg", "png"] -[dependencies.rocket_contrib] -version = "0.4.5" -default_features = false -features = ["json", "serve"] -optional = true - [target.'cfg(windows)'.dependencies] uuid = { version="0.8", optional = true } winapi = { version = "0.3.3", features = ["winuser", "libloaderapi", "shellapi", "errhandlingapi"], optional = true } @@ -67,7 +66,4 @@ sd-notify = "0.1.0" unix-daemonize = "0.1.2" [dev-dependencies] -percent-encoding = "2.1" -cookie = "0.14.0" -http = "0.2.1" headers = "0.3" diff --git a/rust-toolchain b/rust-toolchain index 2f9ebd3..870bbe4 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -nightly-2020-12-08 \ No newline at end of file +stable \ No newline at end of file diff --git a/src/app/lastfm/manager.rs b/src/app/lastfm/manager.rs index 901db0d..18a7a56 100644 --- a/src/app/lastfm/manager.rs +++ b/src/app/lastfm/manager.rs @@ -39,6 +39,7 @@ struct AuthResponse { pub session: AuthResponseSession, } +#[derive(Clone)] pub struct Manager { index: Index, user_manager: user::Manager, diff --git a/src/app/playlist/manager.rs b/src/app/playlist/manager.rs index 96790a2..e633809 100644 --- a/src/app/playlist/manager.rs +++ b/src/app/playlist/manager.rs @@ -11,6 +11,7 @@ use crate::app::index::Song; use crate::app::vfs; use crate::db::{playlist_songs, playlists, users, DB}; +#[derive(Clone)] pub struct Manager { db: DB, vfs_manager: vfs::Manager, diff --git a/src/app/thumbnail/manager.rs b/src/app/thumbnail/manager.rs index 6a19da9..a6cdf4d 100644 --- a/src/app/thumbnail/manager.rs +++ b/src/app/thumbnail/manager.rs @@ -7,6 +7,7 @@ use std::path::{Path, PathBuf}; use crate::app::thumbnail::*; +#[derive(Clone)] pub struct Manager { thumbnails_dir_path: PathBuf, } diff --git a/src/main.rs b/src/main.rs index 33c0b3a..875ba4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ #![recursion_limit = "256"] -#![feature(proc_macro_hygiene, decl_macro)] #[macro_use] extern crate diesel; diff --git a/src/service/actix/api.rs b/src/service/actix/api.rs new file mode 100644 index 0000000..6a7e1f3 --- /dev/null +++ b/src/service/actix/api.rs @@ -0,0 +1,690 @@ +use actix_files::NamedFile; +use actix_web::{ + client::HttpError, + delete, + dev::{MessageBody, Payload, Service, ServiceRequest, ServiceResponse}, + error::{BlockingError, ErrorForbidden, ErrorInternalServerError, ErrorUnauthorized}, + get, + http::StatusCode, + post, put, + web::{self, Data, Json, JsonConfig, ServiceConfig}, + FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError, +}; +use actix_web_httpauth::extractors::basic::BasicAuth; +use cookie::{self, *}; +use futures_util::future::{err, ok}; +use percent_encoding::percent_decode_str; +use std::future::Future; +use std::ops::Deref; +use std::path::Path; +use std::pin::Pin; +use std::str; +use time::Duration; + +use crate::app::{ + config, + index::{self, Index}, + lastfm, playlist, thumbnail, user, vfs, +}; +use crate::service::{dto, error::*}; + +pub fn make_config() -> impl FnOnce(&mut ServiceConfig) + Clone { + move |cfg: &mut ServiceConfig| { + let megabyte = 1024 * 1024; + cfg.app_data(JsonConfig::default().limit(4 * megabyte)) // 4MB + .service(version) + .service(initial_setup) + .service(get_settings) + .service(put_settings) + .service(get_preferences) + .service(put_preferences) + .service(trigger_index) + .service(login) + .service(browse_root) + .service(browse) + .service(flatten_root) + .service(flatten) + .service(random) + .service(recent) + .service(search_root) + .service(search) + .service(get_audio) + .service(get_thumbnail) + .service(list_playlists) + .service(save_playlist) + .service(read_playlist) + .service(delete_playlist) + .service(lastfm_now_playing) + .service(lastfm_scrobble) + .service(lastfm_link) + .service(lastfm_unlink); + } +} + +impl ResponseError for APIError { + fn status_code(&self) -> StatusCode { + match self { + APIError::IncorrectCredentials => StatusCode::UNAUTHORIZED, + APIError::OwnAdminPrivilegeRemoval => StatusCode::CONFLICT, + APIError::AudioFileIOError => StatusCode::NOT_FOUND, + APIError::ThumbnailFileIOError => StatusCode::NOT_FOUND, + APIError::LastFMAccountNotLinked => StatusCode::UNAUTHORIZED, + APIError::LastFMLinkContentBase64DecodeError => StatusCode::BAD_REQUEST, + APIError::LastFMLinkContentEncodingError => StatusCode::BAD_REQUEST, + APIError::UserNotFound => StatusCode::NOT_FOUND, + APIError::PlaylistNotFound => StatusCode::NOT_FOUND, + APIError::VFSPathNotFound => StatusCode::NOT_FOUND, + APIError::Unspecified => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +#[derive(Clone)] +struct Cookies { + jar: CookieJar, + key: Key, +} + +impl Cookies { + fn new(key: Key) -> Self { + let jar = CookieJar::new(); + Self { jar, key } + } + + fn add_original(&mut self, cookie: Cookie<'static>) { + self.jar.add_original(cookie); + } + + fn add(&mut self, cookie: Cookie<'static>) { + self.jar.add(cookie); + } + + fn add_signed(&mut self, cookie: Cookie<'static>) { + self.jar.signed(&self.key).add(cookie); + } + + #[allow(dead_code)] + fn get(&self, name: &str) -> Option<&Cookie> { + self.jar.get(name) + } + + fn get_signed(&mut self, name: &str) -> Option { + self.jar.signed(&self.key).get(name) + } +} + +impl FromRequest for Cookies { + type Error = actix_web::Error; + type Future = Pin>>>; + type Config = (); + + fn from_request(request: &HttpRequest, _payload: &mut Payload) -> Self::Future { + let request_cookies = match request.cookies() { + Ok(c) => c, + Err(_) => return Box::pin(err(ErrorInternalServerError(APIError::Unspecified))), + }; + + let key = match request.app_data::>() { + Some(k) => k.as_ref(), + None => return Box::pin(err(ErrorInternalServerError(APIError::Unspecified))), + }; + + let mut cookies = Cookies::new(key.clone()); + for cookie in request_cookies.deref() { + cookies.add_original(cookie.clone()); + } + + Box::pin(ok(cookies)) + } +} + +#[derive(Debug)] +enum AuthSource { + AuthorizationHeader, + Cookie, +} + +#[derive(Debug)] +struct Auth { + username: String, + source: AuthSource, +} + +impl FromRequest for Auth { + type Error = actix_web::Error; + type Future = Pin>>>; + type Config = (); + + fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future { + let user_manager = match request.app_data::>() { + Some(m) => m.clone(), + None => return Box::pin(err(ErrorInternalServerError(APIError::Unspecified))), + }; + + let cookies_future = Cookies::from_request(request, payload); + let http_auth_future = BasicAuth::from_request(request, payload); + + Box::pin(async move { + // Auth via session cookie + { + let mut cookies = cookies_future.await?; + if let Some(session_cookie) = cookies.get_signed(dto::COOKIE_SESSION) { + let username = session_cookie.value().to_string(); + let exists = block(move || user_manager.exists(&username)).await?; + if !exists { + return Err(ErrorUnauthorized(APIError::Unspecified)); + } + return Ok(Auth { + username: session_cookie.value().to_string(), + source: AuthSource::Cookie, + }); + } + } + + // Auth via HTTP header + { + let auth = http_auth_future.await?; + let username = auth.user_id().to_string(); + let password = auth + .password() + .map(|s| s.as_ref()) + .unwrap_or("") + .to_string(); + let auth_result = block(move || user_manager.auth(&username, &password)).await?; + if auth_result { + Ok(Auth { + username: auth.user_id().to_string(), + source: AuthSource::AuthorizationHeader, + }) + } else { + Err(ErrorUnauthorized(APIError::Unspecified)) + } + } + }) + } +} + +#[derive(Debug)] +struct AdminRights { + auth: Option, +} + +impl FromRequest for AdminRights { + type Error = actix_web::Error; + type Future = Pin>>>; + type Config = (); + + fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future { + let user_manager = match request.app_data::>() { + Some(m) => m.clone(), + None => return Box::pin(err(ErrorInternalServerError(APIError::Unspecified))), + }; + + let auth_future = Auth::from_request(request, payload); + + Box::pin(async move { + let user_manager_count = user_manager.clone(); + let user_count = block(move || user_manager_count.count()).await; + match user_count { + Err(_) => return Err(ErrorInternalServerError(APIError::Unspecified)), + Ok(0) => return Ok(AdminRights { auth: None }), + _ => (), + }; + + let auth = auth_future.await?; + let username = auth.username.clone(); + let is_admin = block(move || user_manager.is_admin(&username)).await?; + if is_admin { + Ok(AdminRights { auth: Some(auth) }) + } else { + Err(ErrorForbidden(APIError::Unspecified)) + } + }) + } +} + +pub fn http_auth_middleware< + B: MessageBody + 'static, + S: Service, Request = ServiceRequest, Error = actix_web::Error> + + 'static, +>( + request: ServiceRequest, + service: &mut S, +) -> Pin, actix_web::Error>>>> { + let user_manager = match request.app_data::>() { + Some(m) => m.clone(), + None => return Box::pin(err(ErrorInternalServerError(APIError::Unspecified))), + }; + + let (request, mut payload) = request.into_parts(); + let auth_future = Auth::from_request(&request, &mut payload); + let cookies_future = Cookies::from_request(&request, &mut payload); + let request = match ServiceRequest::from_parts(request, payload) { + Ok(s) => s, + Err(_) => return Box::pin(err(ErrorInternalServerError(APIError::Unspecified))), + }; + + let response_future = service.call(request); + Box::pin(async move { + let mut response = response_future.await?; + if let Ok(auth) = auth_future.await { + let set_cookies = match auth.source { + AuthSource::AuthorizationHeader => true, + AuthSource::Cookie => false, + }; + if set_cookies { + let cookies = cookies_future.await?; + let username = auth.username.clone(); + let is_admin = block(move || { + user_manager + .is_admin(&auth.username) + .map_err(|_| APIError::Unspecified) + }) + .await?; + add_auth_cookies(response.response_mut(), &cookies, &username, is_admin)?; + } + } + Ok(response) + }) +} + +fn add_auth_cookies( + response: &mut HttpResponse, + cookies: &Cookies, + username: &str, + is_admin: bool, +) -> Result<(), HttpError> { + let duration = Duration::days(1); + + let mut cookies = cookies.clone(); + + cookies.add_signed( + Cookie::build(dto::COOKIE_SESSION, username.to_owned()) + .same_site(cookie::SameSite::Lax) + .http_only(true) + .max_age(duration) + .finish(), + ); + + cookies.add( + Cookie::build(dto::COOKIE_USERNAME, username.to_owned()) + .same_site(cookie::SameSite::Lax) + .http_only(false) + .max_age(duration) + .path("/") + .finish(), + ); + + cookies.add( + Cookie::build(dto::COOKIE_ADMIN, format!("{}", is_admin)) + .same_site(cookie::SameSite::Lax) + .http_only(false) + .max_age(duration) + .path("/") + .finish(), + ); + + let headers = response.headers_mut(); + for cookie in cookies.jar.delta() { + http::HeaderValue::from_str(&cookie.to_string()).map(|c| { + headers.append(http::header::SET_COOKIE, c); + })?; + } + + Ok(()) +} + +async fn block(f: F) -> Result +where + F: FnOnce() -> Result + Send + 'static, + I: Send + 'static, + E: Send + std::fmt::Debug + 'static + Into, +{ + actix_web::web::block(f).await.map_err(|e| match e { + BlockingError::Error(e) => e.into(), + BlockingError::Canceled => APIError::Unspecified, + }) +} + +#[get("/version")] +async fn version() -> Json { + let current_version = dto::Version { + major: dto::API_MAJOR_VERSION, + minor: dto::API_MINOR_VERSION, + }; + Json(current_version) +} + +#[get("/initial_setup")] +async fn initial_setup( + user_manager: Data, +) -> Result, APIError> { + let initial_setup = block(move || -> Result { + let user_count = user_manager.count()?; + Ok(dto::InitialSetup { + has_any_users: user_count > 0, + }) + }) + .await?; + Ok(Json(initial_setup)) +} + +#[get("/settings")] +async fn get_settings( + config_manager: Data, + _admin_rights: AdminRights, +) -> Result, APIError> { + let config = block(move || config_manager.read()).await?; + Ok(Json(config)) +} + +#[put("/settings")] +async fn put_settings( + admin_rights: AdminRights, + config_manager: Data, + config: Json, +) -> Result { + // Do not let users remove their own admin rights + if let Some(auth) = &admin_rights.auth { + if let Some(users) = &config.users { + for user in users { + if auth.username == user.name && !user.admin { + return Err(APIError::OwnAdminPrivilegeRemoval); + } + } + } + } + + block(move || config_manager.amend(&config)).await?; + Ok(HttpResponse::new(StatusCode::OK)) +} + +#[get("/preferences")] +async fn get_preferences( + user_manager: Data, + auth: Auth, +) -> Result, APIError> { + let preferences = block(move || user_manager.read_preferences(&auth.username)).await?; + Ok(Json(preferences)) +} + +#[put("/preferences")] +async fn put_preferences( + user_manager: Data, + auth: Auth, + preferences: Json, +) -> Result { + block(move || user_manager.write_preferences(&auth.username, &preferences)).await?; + Ok(HttpResponse::new(StatusCode::OK)) +} + +#[post("/trigger_index")] +async fn trigger_index( + index: Data, + _admin_rights: AdminRights, +) -> Result { + index.trigger_reindex(); + Ok(HttpResponse::new(StatusCode::OK)) +} + +#[post("/auth")] +async fn login( + user_manager: Data, + credentials: Json, + cookies: Cookies, +) -> Result { + let username = credentials.username.clone(); + let is_admin = block(move || { + if !user_manager.auth(&credentials.username, &credentials.password)? { + return Err(APIError::IncorrectCredentials); + } + user_manager + .is_admin(&credentials.username) + .map_err(|_| APIError::Unspecified) + }) + .await?; + let mut response = HttpResponse::Ok().finish(); + add_auth_cookies(&mut response, &cookies, &username, is_admin) + .map_err(|_| APIError::Unspecified)?; + Ok(response) +} + +#[get("/browse")] +async fn browse_root( + index: Data, + _auth: Auth, +) -> Result>, APIError> { + let result = block(move || index.browse(Path::new(""))).await?; + Ok(Json(result)) +} + +#[get("/browse/{path:.*}")] +async fn browse( + index: Data, + _auth: Auth, + path: web::Path, +) -> Result>, APIError> { + let result = block(move || { + let path = percent_decode_str(&(path.0)).decode_utf8_lossy(); + index.browse(Path::new(path.as_ref())) + }) + .await?; + Ok(Json(result)) +} + +#[get("/flatten")] +async fn flatten_root(index: Data, _auth: Auth) -> Result>, APIError> { + let songs = block(move || index.flatten(Path::new(""))).await?; + Ok(Json(songs)) +} + +#[get("/flatten/{path:.*}")] +async fn flatten( + index: Data, + _auth: Auth, + path: web::Path, +) -> Result>, APIError> { + let songs = block(move || { + let path = percent_decode_str(&(path.0)).decode_utf8_lossy(); + index.flatten(Path::new(path.as_ref())) + }) + .await?; + Ok(Json(songs)) +} + +#[get("/random")] +async fn random(index: Data, _auth: Auth) -> Result>, APIError> { + let result = block(move || index.get_random_albums(20)).await?; + Ok(Json(result)) +} + +#[get("/recent")] +async fn recent(index: Data, _auth: Auth) -> Result>, APIError> { + let result = block(move || index.get_recent_albums(20)).await?; + Ok(Json(result)) +} + +#[get("/search")] +async fn search_root( + index: Data, + _auth: Auth, +) -> Result>, APIError> { + let result = block(move || index.search("")).await?; + Ok(Json(result)) +} + +#[get("/search/{query:.*}")] +async fn search( + index: Data, + _auth: Auth, + query: web::Path, +) -> Result>, APIError> { + let result = block(move || index.search(&query)).await?; + Ok(Json(result)) +} + +#[get("/audio/{path:.*}")] +async fn get_audio( + vfs_manager: Data, + _auth: Auth, + path: web::Path, +) -> Result { + let audio_path = block(move || { + let vfs = vfs_manager.get_vfs()?; + let path = percent_decode_str(&(path.0)).decode_utf8_lossy(); + vfs.virtual_to_real(Path::new(path.as_ref())) + .map_err(|_| APIError::VFSPathNotFound) + }) + .await?; + + let named_file = NamedFile::open(&audio_path).map_err(|_| APIError::AudioFileIOError)?; + Ok(named_file) +} + +#[get("/thumbnail/{path:.*}")] +async fn get_thumbnail( + vfs_manager: Data, + thumbnails_manager: Data, + _auth: Auth, + path: web::Path, + options_input: web::Query, +) -> Result { + let mut options = thumbnail::Options::default(); + options.pad_to_square = options_input.pad.unwrap_or(options.pad_to_square); + + let thumbnail_path = block(move || { + let vfs = vfs_manager.get_vfs()?; + let path = percent_decode_str(&(path.0)).decode_utf8_lossy(); + let image_path = vfs + .virtual_to_real(Path::new(path.as_ref())) + .map_err(|_| APIError::VFSPathNotFound)?; + thumbnails_manager + .get_thumbnail(&image_path, &options) + .map_err(|_| APIError::Unspecified) + }) + .await?; + + let named_file = + NamedFile::open(&thumbnail_path).map_err(|_| APIError::ThumbnailFileIOError)?; + + Ok(named_file) +} + +#[get("/playlists")] +async fn list_playlists( + playlist_manager: Data, + auth: Auth, +) -> Result>, APIError> { + let playlist_names = block(move || playlist_manager.list_playlists(&auth.username)).await?; + let playlists: Vec = playlist_names + .into_iter() + .map(|p| dto::ListPlaylistsEntry { name: p }) + .collect(); + + Ok(Json(playlists)) +} + +#[put("/playlist/{name}")] +async fn save_playlist( + playlist_manager: Data, + auth: Auth, + name: web::Path, + playlist: Json, +) -> Result { + block(move || playlist_manager.save_playlist(&name, &auth.username, &playlist.tracks)).await?; + Ok(HttpResponse::new(StatusCode::OK)) +} + +#[get("/playlist/{name}")] +async fn read_playlist( + playlist_manager: Data, + auth: Auth, + name: web::Path, +) -> Result>, APIError> { + let songs = block(move || playlist_manager.read_playlist(&name, &auth.username)).await?; + Ok(Json(songs)) +} + +#[delete("/playlist/{name}")] +async fn delete_playlist( + playlist_manager: Data, + auth: Auth, + name: web::Path, +) -> Result { + block(move || playlist_manager.delete_playlist(&name, &auth.username)).await?; + Ok(HttpResponse::new(StatusCode::OK)) +} + +#[put("/lastfm/now_playing/{path:.*}")] +async fn lastfm_now_playing( + lastfm_manager: Data, + user_manager: Data, + auth: Auth, + path: web::Path, +) -> Result { + block(move || -> Result<(), APIError> { + if !user_manager.is_lastfm_linked(&auth.username) { + return Err(APIError::LastFMAccountNotLinked); + } + let path = percent_decode_str(&(path.0)).decode_utf8_lossy(); + lastfm_manager.now_playing(&auth.username, Path::new(path.as_ref()))?; + Ok(()) + }) + .await?; + Ok(HttpResponse::new(StatusCode::OK)) +} + +#[post("/lastfm/scrobble/{path:.*}")] +async fn lastfm_scrobble( + lastfm_manager: Data, + user_manager: Data, + auth: Auth, + path: web::Path, +) -> Result { + block(move || -> Result<(), APIError> { + if !user_manager.is_lastfm_linked(&auth.username) { + return Err(APIError::LastFMAccountNotLinked); + } + let path = percent_decode_str(&(path.0)).decode_utf8_lossy(); + lastfm_manager.scrobble(&auth.username, Path::new(path.as_ref()))?; + Ok(()) + }) + .await?; + Ok(HttpResponse::new(StatusCode::OK)) +} + +#[get("/lastfm/link")] +async fn lastfm_link( + lastfm_manager: Data, + auth: Auth, + payload: web::Query, +) -> Result { + let popup_content_string = block(move || { + lastfm_manager.link(&auth.username, &payload.token)?; + // Percent decode + let base64_content = percent_decode_str(&payload.content).decode_utf8_lossy(); + + // Base64 decode + let popup_content = base64::decode(base64_content.as_bytes()) + .map_err(|_| APIError::LastFMLinkContentBase64DecodeError)?; + + // UTF-8 decode + str::from_utf8(&popup_content) + .map_err(|_| APIError::LastFMLinkContentEncodingError) + .map(|s| s.to_owned()) + }) + .await?; + + Ok(HttpResponse::build(StatusCode::OK) + .content_type("text/html; charset=utf-8") + .body(popup_content_string)) +} + +#[delete("/lastfm/link")] +async fn lastfm_unlink( + lastfm_manager: Data, + auth: Auth, +) -> Result { + block(move || lastfm_manager.unlink(&auth.username)).await?; + Ok(HttpResponse::new(StatusCode::OK)) +} diff --git a/src/service/actix/mod.rs b/src/service/actix/mod.rs new file mode 100644 index 0000000..3c1678b --- /dev/null +++ b/src/service/actix/mod.rs @@ -0,0 +1,58 @@ +use actix_web::{ + middleware::{normalize::TrailingSlash, Compress, Logger, NormalizePath}, + rt::System, + web::{self, ServiceConfig}, + App, HttpServer, +}; +use anyhow::*; +use log::error; + +use crate::service; + +mod api; + +#[cfg(test)] +pub mod test; + +pub fn make_config(context: service::Context) -> impl FnOnce(&mut ServiceConfig) + Clone { + move |cfg: &mut ServiceConfig| { + let encryption_key = cookie::Key::derive_from(&context.auth_secret[..]); + cfg.app_data(web::Data::new(context.index)) + .app_data(web::Data::new(context.config_manager)) + .app_data(web::Data::new(context.lastfm_manager)) + .app_data(web::Data::new(context.playlist_manager)) + .app_data(web::Data::new(context.thumbnail_manager)) + .app_data(web::Data::new(context.user_manager)) + .app_data(web::Data::new(context.vfs_manager)) + .app_data(web::Data::new(encryption_key)) + .service(web::scope(&context.api_url).configure(api::make_config())) + .service( + actix_files::Files::new(&context.swagger_url, context.swagger_dir_path) + .index_file("index.html"), + ) + .service( + actix_files::Files::new(&context.web_url, context.web_dir_path) + .index_file("index.html"), + ); + } +} + +pub fn run(context: service::Context) -> Result<()> { + System::run(move || { + let address = format!("0.0.0.0:{}", context.port); + HttpServer::new(move || { + App::new() + .wrap(Logger::default()) + .wrap(Compress::default()) + .wrap_fn(api::http_auth_middleware) + .wrap(NormalizePath::new(TrailingSlash::Trim)) + .configure(make_config(context.clone())) + }) + .disable_signals() + .bind(address) + .map(|server| server.run()) + .map_err(|e| error!("Error starting HTTP server: {:?}", e)) + .ok(); + })?; + Ok(()) +} diff --git a/src/service/actix/test.rs b/src/service/actix/test.rs new file mode 100644 index 0000000..9cddb4f --- /dev/null +++ b/src/service/actix/test.rs @@ -0,0 +1,155 @@ +use actix_web::{ + client::ClientResponse, + middleware::{normalize::TrailingSlash, Compress, Logger, NormalizePath}, + rt::{System, SystemRunner}, + test, + test::*, + web::Bytes, + App, +}; +use cookie::Cookie; +use http::{header, response::Builder, Method, Request, Response}; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::collections::HashMap; +use std::fs; +use std::ops::Deref; +use std::path::{Path, PathBuf}; + +use crate::service::actix::*; +use crate::service::test::TestService; + +pub struct ActixTestService { + system_runner: SystemRunner, + cookies: HashMap, + server: TestServer, +} + +pub type ServiceType = ActixTestService; + +impl ActixTestService { + fn update_cookies(&mut self, actix_response: &ClientResponse) { + let cookies = actix_response.headers().get_all(header::SET_COOKIE); + for raw_cookie in cookies { + let cookie = Cookie::parse(raw_cookie.to_str().unwrap()).unwrap(); + self.cookies + .insert(cookie.name().to_owned(), cookie.value().to_owned()); + } + } + + fn process_internal( + &mut self, + request: &Request, + ) -> (Builder, Option) { + let url = request.uri().to_string(); + let body = request.body().clone(); + + let mut actix_request = match *request.method() { + Method::GET => self.server.get(url), + Method::POST => self.server.post(url), + Method::PUT => self.server.put(url), + Method::DELETE => self.server.delete(url), + _ => unimplemented!(), + }; + + for (name, value) in request.headers() { + actix_request = actix_request.set_header(name, value.clone()); + } + + actix_request = { + let cookies_value = self + .cookies + .iter() + .map(|(name, value)| format!("{}={}", name, value)) + .collect::>() + .join("; "); + actix_request.set_header(header::COOKIE, cookies_value) + }; + + let mut actix_response = self + .system_runner + .block_on(async move { actix_request.send_json(&body).await.unwrap() }); + + self.update_cookies(&actix_response); + + let mut response_builder = Response::builder().status(actix_response.status()); + let headers = response_builder.headers_mut().unwrap(); + for (name, value) in actix_response.headers().iter() { + headers.append(name, value.clone()); + } + + let is_success = actix_response.status().is_success(); + let body = if is_success { + Some( + self.system_runner + .block_on(async move { actix_response.body().await.unwrap() }), + ) + } else { + None + }; + + (response_builder, body) + } +} + +impl TestService for ActixTestService { + fn new(test_name: &str) -> Self { + let mut db_path: PathBuf = ["test-output", test_name].iter().collect(); + fs::create_dir_all(&db_path).unwrap(); + db_path.push("db.sqlite"); + + if db_path.exists() { + fs::remove_file(&db_path).unwrap(); + } + + let context = service::ContextBuilder::new() + .port(5050) + .database_file_path(db_path) + .web_dir_path(Path::new("test-data/web").into()) + .swagger_dir_path(["docs", "swagger"].iter().collect()) + .cache_dir_path(["test-output", test_name].iter().collect()) + .build() + .unwrap(); + + let system_runner = System::new("test"); + let server = test::start(move || { + let config = make_config(context.clone()); + App::new() + .wrap(Logger::default()) + .wrap(Compress::default()) + .wrap_fn(api::http_auth_middleware) + .wrap(NormalizePath::new(TrailingSlash::Trim)) + .configure(config) + }); + + ActixTestService { + cookies: HashMap::new(), + system_runner, + server, + } + } + + fn fetch(&mut self, request: &Request) -> Response<()> { + let (response_builder, _body) = self.process_internal(request); + response_builder.body(()).unwrap() + } + + fn fetch_bytes( + &mut self, + request: &Request, + ) -> Response> { + let (response_builder, body) = self.process_internal(request); + response_builder + .body(body.unwrap().deref().to_owned()) + .unwrap() + } + + fn fetch_json( + &mut self, + request: &Request, + ) -> Response { + let (response_builder, body) = self.process_internal(request); + let body = serde_json::from_slice(&body.unwrap()).unwrap(); + response_builder.body(body).unwrap() + } +} diff --git a/src/service/dto.rs b/src/service/dto.rs index 598300e..ad046b3 100644 --- a/src/service/dto.rs +++ b/src/service/dto.rs @@ -17,20 +17,31 @@ pub struct InitialSetup { pub has_any_users: bool, } -#[derive(Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct AuthCredentials { pub username: String, pub password: String, } +#[derive(Serialize, Deserialize)] +pub struct ThumbnailOptions { + pub pad: Option, +} + #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct ListPlaylistsEntry { pub name: String, } -#[derive(Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct SavePlaylistInput { pub tracks: Vec, } +#[derive(Serialize, Deserialize)] +pub struct LastFMLink { + pub token: String, + pub content: String, +} + // TODO: Config, Preferences, CollectionFile, Song and Directory should have dto types diff --git a/src/service/error.rs b/src/service/error.rs index 1b58379..8bfb253 100644 --- a/src/service/error.rs +++ b/src/service/error.rs @@ -1,11 +1,24 @@ use thiserror::Error; +use crate::app::index::QueryError; +use crate::app::playlist; + #[derive(Error, Debug)] pub enum APIError { #[error("Incorrect Credentials")] IncorrectCredentials, #[error("Cannot remove own admin privilege")] OwnAdminPrivilegeRemoval, + #[error("Audio file could not be opened")] + AudioFileIOError, + #[error("Thumbnail file could not be opened")] + ThumbnailFileIOError, + #[error("No last.fm account has been linked")] + LastFMAccountNotLinked, + #[error("Could not decode content as base64 after linking last.fm account")] + LastFMLinkContentBase64DecodeError, + #[error("Could not decode content as UTF-8 after linking last.fm account")] + LastFMLinkContentEncodingError, #[error("Path not found in virtual filesystem")] VFSPathNotFound, #[error("User not found")] @@ -21,3 +34,22 @@ impl From for APIError { APIError::Unspecified } } + +impl From for APIError { + fn from(error: playlist::Error) -> APIError { + match error { + playlist::Error::PlaylistNotFound => APIError::PlaylistNotFound, + playlist::Error::UserNotFound => APIError::UserNotFound, + playlist::Error::Unspecified => APIError::Unspecified, + } + } +} + +impl From for APIError { + fn from(error: QueryError) -> APIError { + match error { + QueryError::VFSPathNotFound => APIError::VFSPathNotFound, + QueryError::Unspecified => APIError::Unspecified, + } + } +} diff --git a/src/service/mod.rs b/src/service/mod.rs index 75f7668..3f34e53 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -10,11 +10,10 @@ mod error; #[cfg(test)] mod test; -#[cfg(feature = "service-rocket")] -mod rocket; -#[cfg(feature = "service-rocket")] -pub use self::rocket::*; +mod actix; +pub use actix::*; +#[derive(Clone)] pub struct Context { pub port: u16, pub auth_secret: Vec, diff --git a/src/service/rocket/api.rs b/src/service/rocket/api.rs deleted file mode 100644 index 818cdf7..0000000 --- a/src/service/rocket/api.rs +++ /dev/null @@ -1,484 +0,0 @@ -use anyhow::*; -use rocket::http::{Cookie, Cookies, RawStr, Status}; -use rocket::request::{self, FromParam, FromRequest, Request}; -use rocket::response::content::Html; -use rocket::{delete, get, post, put, routes, Outcome, State}; -use rocket_contrib::json::Json; -use std::default::Default; -use std::fs::File; -use std::path::{Path, PathBuf}; -use std::str; -use std::str::FromStr; -use time::Duration; - -use super::serve; -use crate::app::index::{self, Index, QueryError}; -use crate::app::{config, lastfm, playlist, thumbnail, user, vfs}; -use crate::service::dto; -use crate::service::error::APIError; - -pub fn get_routes() -> Vec { - routes![ - version, - initial_setup, - get_settings, - put_settings, - get_preferences, - put_preferences, - trigger_index, - auth, - browse_root, - browse, - flatten_root, - flatten, - random, - recent, - search_root, - search, - audio, - thumbnail, - list_playlists, - save_playlist, - read_playlist, - delete_playlist, - lastfm_link, - lastfm_unlink, - lastfm_now_playing, - lastfm_scrobble, - ] -} - -impl<'r> rocket::response::Responder<'r> for APIError { - fn respond_to(self, _: &rocket::request::Request<'_>) -> rocket::response::Result<'r> { - let status = match self { - APIError::IncorrectCredentials => rocket::http::Status::Unauthorized, - APIError::OwnAdminPrivilegeRemoval => rocket::http::Status::Conflict, - APIError::VFSPathNotFound => rocket::http::Status::NotFound, - APIError::UserNotFound => rocket::http::Status::NotFound, - APIError::PlaylistNotFound => rocket::http::Status::NotFound, - APIError::Unspecified => rocket::http::Status::InternalServerError, - }; - rocket::response::Response::build().status(status).ok() - } -} - -impl From for APIError { - fn from(error: playlist::Error) -> APIError { - match error { - playlist::Error::PlaylistNotFound => APIError::PlaylistNotFound, - playlist::Error::UserNotFound => APIError::UserNotFound, - playlist::Error::Unspecified => APIError::Unspecified, - } - } -} - -impl From for APIError { - fn from(error: QueryError) -> APIError { - match error { - QueryError::VFSPathNotFound => APIError::VFSPathNotFound, - QueryError::Unspecified => APIError::Unspecified, - } - } -} - -struct Auth { - username: String, -} - -fn add_session_cookies(cookies: &mut Cookies, username: &str, is_admin: bool) -> () { - let duration = Duration::days(1); - - let session_cookie = Cookie::build(dto::COOKIE_SESSION, username.to_owned()) - .same_site(rocket::http::SameSite::Lax) - .http_only(true) - .max_age(duration) - .finish(); - - let username_cookie = Cookie::build(dto::COOKIE_USERNAME, username.to_owned()) - .same_site(rocket::http::SameSite::Lax) - .http_only(false) - .max_age(duration) - .path("/") - .finish(); - - let is_admin_cookie = Cookie::build(dto::COOKIE_ADMIN, format!("{}", is_admin)) - .same_site(rocket::http::SameSite::Lax) - .http_only(false) - .max_age(duration) - .path("/") - .finish(); - - cookies.add_private(session_cookie); - cookies.add(username_cookie); - cookies.add(is_admin_cookie); -} - -impl<'a, 'r> FromRequest<'a, 'r> for Auth { - type Error = (); - - fn from_request(request: &'a Request<'r>) -> request::Outcome { - let mut cookies = request.guard::>().unwrap(); - let user_manager = match request.guard::>() { - Outcome::Success(d) => d, - _ => return Outcome::Failure((Status::InternalServerError, ())), - }; - - if let Some(u) = cookies.get_private(dto::COOKIE_SESSION) { - let exists = match user_manager.exists(u.value()) { - Ok(e) => e, - Err(_) => return Outcome::Failure((Status::InternalServerError, ())), - }; - if !exists { - return Outcome::Failure((Status::Unauthorized, ())); - } - return Outcome::Success(Auth { - username: u.value().to_string(), - }); - } - - if let Some(auth_header_string) = request.headers().get_one("Authorization") { - use rocket::http::hyper::header::*; - if let Ok(Basic { - username, - password: Some(password), - }) = Basic::from_str(auth_header_string.trim_start_matches("Basic ")) - { - if user_manager.auth(&username, &password).unwrap_or(false) { - let is_admin = match user_manager.is_admin(&username) { - Ok(a) => a, - Err(_) => return Outcome::Failure((Status::InternalServerError, ())), - }; - add_session_cookies(&mut cookies, &username, is_admin); - return Outcome::Success(Auth { - username: username.to_string(), - }); - } - } - } - - Outcome::Failure((Status::Unauthorized, ())) - } -} - -struct AdminRights { - auth: Option, -} - -impl<'a, 'r> FromRequest<'a, 'r> for AdminRights { - type Error = (); - - fn from_request(request: &'a Request<'r>) -> request::Outcome { - let user_manager = request.guard::>()?; - - match user_manager.count() { - Err(_) => return Outcome::Failure((Status::InternalServerError, ())), - Ok(0) => return Outcome::Success(AdminRights { auth: None }), - _ => (), - }; - - let auth = request.guard::()?; - match user_manager.is_admin(&auth.username) { - Err(_) => Outcome::Failure((Status::InternalServerError, ())), - Ok(true) => Outcome::Success(AdminRights { auth: Some(auth) }), - Ok(false) => Outcome::Failure((Status::Forbidden, ())), - } - } -} - -struct VFSPathBuf { - path_buf: PathBuf, -} - -impl<'r> FromParam<'r> for VFSPathBuf { - type Error = &'r RawStr; - - fn from_param(param: &'r RawStr) -> Result { - let decoded_path = param.percent_decode_lossy(); - Ok(VFSPathBuf { - path_buf: PathBuf::from(decoded_path.into_owned()), - }) - } -} - -impl From for PathBuf { - fn from(vfs_path_buf: VFSPathBuf) -> Self { - vfs_path_buf.path_buf.clone() - } -} - -#[get("/version")] -fn version() -> Json { - let current_version = dto::Version { - major: dto::API_MAJOR_VERSION, - minor: dto::API_MINOR_VERSION, - }; - Json(current_version) -} - -#[get("/initial_setup")] -fn initial_setup(user_manager: State<'_, user::Manager>) -> Result> { - let initial_setup = dto::InitialSetup { - has_any_users: user_manager.count()? > 0, - }; - Ok(Json(initial_setup)) -} - -#[get("/settings")] -fn get_settings( - config_manager: State<'_, config::Manager>, - _admin_rights: AdminRights, -) -> Result> { - let config = config_manager.read()?; - Ok(Json(config)) -} - -#[put("/settings", data = "")] -fn put_settings( - config_manager: State<'_, config::Manager>, - admin_rights: AdminRights, - config: Json, -) -> Result<(), APIError> { - // Do not let users remove their own admin rights - if let Some(auth) = &admin_rights.auth { - if let Some(users) = &config.users { - for user in users { - if auth.username == user.name && !user.admin { - return Err(APIError::OwnAdminPrivilegeRemoval); - } - } - } - } - - config_manager.amend(&config)?; - Ok(()) -} - -#[get("/preferences")] -fn get_preferences( - user_manager: State<'_, user::Manager>, - auth: Auth, -) -> Result> { - let preferences = user_manager.read_preferences(&auth.username)?; - Ok(Json(preferences)) -} - -#[put("/preferences", data = "")] -fn put_preferences( - user_manager: State<'_, user::Manager>, - auth: Auth, - preferences: Json, -) -> Result<()> { - user_manager.write_preferences(&auth.username, &preferences)?; - Ok(()) -} - -#[post("/trigger_index")] -fn trigger_index(index: State<'_, Index>, _admin_rights: AdminRights) -> Result<()> { - index.trigger_reindex(); - Ok(()) -} - -#[post("/auth", data = "")] -fn auth( - user_manager: State<'_, user::Manager>, - credentials: Json, - mut cookies: Cookies<'_>, -) -> std::result::Result<(), APIError> { - if !user_manager.auth(&credentials.username, &credentials.password)? { - return Err(APIError::IncorrectCredentials); - } - let is_admin = user_manager.is_admin(&credentials.username)?; - add_session_cookies(&mut cookies, &credentials.username, is_admin); - Ok(()) -} - -#[get("/browse")] -fn browse_root(index: State<'_, Index>, _auth: Auth) -> Result>> { - let result = index.browse(&Path::new(""))?; - Ok(Json(result)) -} - -#[get("/browse/")] -fn browse( - index: State<'_, Index>, - _auth: Auth, - path: VFSPathBuf, -) -> Result>, APIError> { - let result = index.browse(&path.into() as &PathBuf)?; - Ok(Json(result)) -} - -#[get("/flatten")] -fn flatten_root(index: State<'_, Index>, _auth: Auth) -> Result>> { - let result = index.flatten(&PathBuf::new())?; - Ok(Json(result)) -} - -#[get("/flatten/")] -fn flatten( - index: State<'_, Index>, - _auth: Auth, - path: VFSPathBuf, -) -> Result>, APIError> { - let result = index.flatten(&path.into() as &PathBuf)?; - Ok(Json(result)) -} - -#[get("/random")] -fn random(index: State<'_, Index>, _auth: Auth) -> Result>> { - let result = index.get_random_albums(20)?; - Ok(Json(result)) -} - -#[get("/recent")] -fn recent(index: State<'_, Index>, _auth: Auth) -> Result>> { - let result = index.get_recent_albums(20)?; - Ok(Json(result)) -} - -#[get("/search")] -fn search_root(index: State<'_, Index>, _auth: Auth) -> Result>> { - let result = index.search("")?; - Ok(Json(result)) -} - -#[get("/search/")] -fn search( - index: State<'_, Index>, - _auth: Auth, - query: String, -) -> Result>> { - let result = index.search(&query)?; - Ok(Json(result)) -} - -#[get("/audio/")] -fn audio( - vfs_manager: State<'_, vfs::Manager>, - _auth: Auth, - path: VFSPathBuf, -) -> Result, APIError> { - let vfs = vfs_manager.get_vfs()?; - let real_path = vfs - .virtual_to_real(&path.into() as &PathBuf) - .map_err(|_| APIError::VFSPathNotFound)?; - let file = File::open(&real_path).map_err(|_| APIError::Unspecified)?; - Ok(serve::RangeResponder::new(file)) -} - -#[get("/thumbnail/?")] -fn thumbnail( - vfs_manager: State<'_, vfs::Manager>, - thumbnail_manager: State<'_, thumbnail::Manager>, - _auth: Auth, - path: VFSPathBuf, - pad: Option, -) -> Result { - let vfs = vfs_manager.get_vfs()?; - let image_path = vfs - .virtual_to_real(&path.into() as &PathBuf) - .map_err(|_| APIError::VFSPathNotFound)?; - let mut options = thumbnail::Options::default(); - options.pad_to_square = pad.unwrap_or(options.pad_to_square); - let thumbnail_path = thumbnail_manager.get_thumbnail(&image_path, &options)?; - let file = File::open(thumbnail_path).map_err(|_| APIError::Unspecified)?; - Ok(file) -} - -#[get("/playlists")] -fn list_playlists( - playlist_manager: State<'_, playlist::Manager>, - auth: Auth, -) -> Result>> { - let playlist_names = playlist_manager.list_playlists(&auth.username)?; - let playlists: Vec = playlist_names - .into_iter() - .map(|p| dto::ListPlaylistsEntry { name: p }) - .collect(); - - Ok(Json(playlists)) -} - -#[put("/playlist/", data = "")] -fn save_playlist( - playlist_manager: State<'_, playlist::Manager>, - auth: Auth, - name: String, - playlist: Json, -) -> Result<()> { - playlist_manager.save_playlist(&name, &auth.username, &playlist.tracks)?; - Ok(()) -} - -#[get("/playlist/")] -fn read_playlist( - playlist_manager: State<'_, playlist::Manager>, - auth: Auth, - name: String, -) -> Result>, APIError> { - let songs = playlist_manager.read_playlist(&name, &auth.username)?; - Ok(Json(songs)) -} - -#[delete("/playlist/")] -fn delete_playlist( - playlist_manager: State<'_, playlist::Manager>, - auth: Auth, - name: String, -) -> Result<(), APIError> { - playlist_manager.delete_playlist(&name, &auth.username)?; - Ok(()) -} - -#[put("/lastfm/now_playing/")] -fn lastfm_now_playing( - user_manager: State<'_, user::Manager>, - lastfm_manager: State<'_, lastfm::Manager>, - auth: Auth, - path: VFSPathBuf, -) -> Result<()> { - if user_manager.is_lastfm_linked(&auth.username) { - lastfm_manager.now_playing(&auth.username, &path.into() as &PathBuf)?; - } - Ok(()) -} - -#[post("/lastfm/scrobble/")] -fn lastfm_scrobble( - user_manager: State<'_, user::Manager>, - lastfm_manager: State<'_, lastfm::Manager>, - auth: Auth, - path: VFSPathBuf, -) -> Result<()> { - if user_manager.is_lastfm_linked(&auth.username) { - lastfm_manager.scrobble(&auth.username, &path.into() as &PathBuf)?; - } - Ok(()) -} - -#[get("/lastfm/link?&")] -fn lastfm_link( - lastfm_manager: State<'_, lastfm::Manager>, - auth: Auth, - token: String, - content: String, -) -> Result> { - lastfm_manager.link(&auth.username, &token)?; - - // Percent decode - let base64_content = RawStr::from_str(&content).percent_decode()?; - - // Base64 decode - let popup_content = base64::decode(base64_content.as_bytes())?; - - // UTF-8 decode - let popup_content_string = str::from_utf8(&popup_content)?; - - Ok(Html(popup_content_string.to_string())) -} - -#[delete("/lastfm/link")] -fn lastfm_unlink(lastfm_manager: State<'_, lastfm::Manager>, auth: Auth) -> Result<()> { - lastfm_manager.unlink(&auth.username)?; - Ok(()) -} diff --git a/src/service/rocket/mod.rs b/src/service/rocket/mod.rs deleted file mode 100644 index 9c1fe70..0000000 --- a/src/service/rocket/mod.rs +++ /dev/null @@ -1,53 +0,0 @@ -use anyhow::*; -use rocket; -use rocket::config::{Environment, LoggingLevel}; -use rocket_contrib::serve::{Options, StaticFiles}; - -use crate::service; - -mod api; -mod serve; - -#[cfg(test)] -pub mod test; - -pub fn get_server(context: service::Context) -> Result { - let mut config = rocket::Config::build(Environment::Production) - .log_level(LoggingLevel::Normal) - .port(context.port) - .keep_alive(0) - .finalize()?; - - let encoded = base64::encode(&context.auth_secret); - config.set_secret_key(encoded)?; - - let swagger_routes_rank = 0; - let web_routes_rank = swagger_routes_rank + 1; - let static_file_options = Options::Index | Options::NormalizeDirs; - - Ok(rocket::custom(config) - .manage(context.db) - .manage(context.index) - .manage(context.config_manager) - .manage(context.lastfm_manager) - .manage(context.playlist_manager) - .manage(context.thumbnail_manager) - .manage(context.user_manager) - .manage(context.vfs_manager) - .mount(&context.api_url, api::get_routes()) - .mount( - &context.swagger_url, - StaticFiles::new(context.swagger_dir_path, static_file_options) - .rank(swagger_routes_rank), - ) - .mount( - &context.web_url, - StaticFiles::new(context.web_dir_path, static_file_options).rank(web_routes_rank), - )) -} - -pub fn run(context: service::Context) -> Result<()> { - let server = get_server(context)?; - server.launch(); - Ok(()) -} diff --git a/src/service/rocket/serve.rs b/src/service/rocket/serve.rs deleted file mode 100644 index 2e14d03..0000000 --- a/src/service/rocket/serve.rs +++ /dev/null @@ -1,163 +0,0 @@ -use log::warn; -use rocket; -use rocket::http::hyper::header::*; -use rocket::http::Status; -use rocket::response::{self, Responder}; -use rocket::Response; -use std::cmp; -use std::convert::From; -use std::fs::File; -use std::io::{Read, Seek, SeekFrom}; -use std::str::FromStr; - -#[derive(Debug)] -pub enum PartialFileRange { - AllFrom(u64), - FromTo(u64, u64), - Last(u64), -} - -impl From for PartialFileRange { - fn from(b: ByteRangeSpec) -> PartialFileRange { - match b { - ByteRangeSpec::AllFrom(from) => PartialFileRange::AllFrom(from), - ByteRangeSpec::FromTo(from, to) => PartialFileRange::FromTo(from, to), - ByteRangeSpec::Last(last) => PartialFileRange::Last(last), - } - } -} - -impl From> for PartialFileRange { - fn from(v: Vec) -> PartialFileRange { - match v.into_iter().next() { - None => PartialFileRange::AllFrom(0), - Some(byte_range) => PartialFileRange::from(byte_range), - } - } -} - -pub struct RangeResponder { - original: R, -} - -impl<'r, R: Responder<'r>> RangeResponder { - pub fn new(original: R) -> RangeResponder { - RangeResponder { original } - } - - fn ignore_range( - self, - request: &rocket::request::Request<'_>, - file_length: Option, - ) -> response::Result<'r> { - let mut response = self.original.respond_to(request)?; - if let Some(content_length) = file_length { - response.set_header(ContentLength(content_length)); - } - response.set_status(Status::Ok); - Ok(response) - } - - fn reject_range(self, file_length: Option) -> response::Result<'r> { - let mut response = Response::build() - .status(Status::RangeNotSatisfiable) - .finalize(); - if file_length.is_some() { - let content_range = ContentRange(ContentRangeSpec::Bytes { - range: None, - instance_length: file_length, - }); - response.set_header(content_range); - } - response.set_status(Status::RangeNotSatisfiable); - Ok(response) - } -} - -fn truncate_range(range: &PartialFileRange, file_length: &Option) -> Option<(u64, u64)> { - use self::PartialFileRange::*; - - match (range, file_length) { - (FromTo(from, to), Some(file_length)) => { - if from <= to && from < file_length { - Some((*from, cmp::min(*to, file_length - 1))) - } else { - None - } - } - (AllFrom(from), Some(file_length)) => { - if from < file_length { - Some((*from, file_length - 1)) - } else { - None - } - } - (Last(last), Some(file_length)) => { - if last < file_length { - Some((file_length - last, file_length - 1)) - } else { - Some((0, file_length - 1)) - } - } - (_, None) => None, - } -} - -impl<'r> Responder<'r> for RangeResponder { - fn respond_to(mut self, request: &rocket::request::Request<'_>) -> response::Result<'r> { - let metadata: Option<_> = self.original.metadata().ok(); - let file_length: Option = metadata.map(|m| m.len()); - - let range_header = request.headers().get_one("Range"); - let range_header = match range_header { - None => return self.ignore_range(request, file_length), - Some(h) => h, - }; - - let vec_range = match Range::from_str(range_header) { - Ok(Range::Bytes(v)) => v, - _ => { - warn!( - "Ignoring range header that could not be parsed {:?}, file length is {:?}", - range_header, file_length - ); - return self.ignore_range(request, file_length); - } - }; - - let partial_file_range = match vec_range.into_iter().next() { - None => PartialFileRange::AllFrom(0), - Some(byte_range) => PartialFileRange::from(byte_range), - }; - - let range: Option<(u64, u64)> = truncate_range(&partial_file_range, &file_length); - - if let Some((from, to)) = range { - let content_range = ContentRange(ContentRangeSpec::Bytes { - range: range, - instance_length: file_length, - }); - let content_len = to - from + 1; - - match self.original.seek(SeekFrom::Start(from)) { - Ok(_) => (), - Err(_) => return Err(rocket::http::Status::InternalServerError), - } - let partial_original = self.original.take(content_len); - let response = Response::build() - .status(Status::PartialContent) - .header(ContentLength(content_len)) - .header(content_range) - .streamed_body(partial_original) - .finalize(); - - Ok(response) - } else { - warn!( - "Rejecting unsatisfiable range header {:?}, file length is {:?}", - &partial_file_range, &file_length - ); - self.reject_range(file_length) - } - } -} diff --git a/src/service/rocket/test.rs b/src/service/rocket/test.rs deleted file mode 100644 index 7310931..0000000 --- a/src/service/rocket/test.rs +++ /dev/null @@ -1,112 +0,0 @@ -use http::{header::HeaderName, method::Method, response::Builder, HeaderValue, Request, Response}; -use rocket; -use rocket::local::{Client, LocalResponse}; -use serde::de::DeserializeOwned; -use serde::Serialize; -use std::fs; -use std::path::{Path, PathBuf}; - -use crate::service; -use crate::service::test::{protocol, TestService}; - -pub struct RocketTestService { - client: Client, - request_builder: protocol::RequestBuilder, -} - -pub type ServiceType = RocketTestService; - -impl RocketTestService { - fn process_internal(&mut self, request: &Request) -> (LocalResponse, Builder) { - let rocket_response = { - let url = request.uri().to_string(); - let mut rocket_request = match *request.method() { - Method::GET => self.client.get(url), - Method::POST => self.client.post(url), - Method::PUT => self.client.put(url), - Method::DELETE => self.client.delete(url), - _ => unimplemented!(), - }; - - for (name, value) in request.headers() { - rocket_request.add_header(rocket::http::Header::new( - name.as_str().to_owned(), - value.to_str().unwrap().to_owned(), - )); - } - - let payload = request.body(); - let body = serde_json::to_string(payload).unwrap(); - rocket_request.set_body(body); - - let content_type = rocket::http::ContentType::JSON; - rocket_request.add_header(content_type); - - rocket_request.dispatch() - }; - - let mut builder = Response::builder().status(rocket_response.status().code); - let headers = builder.headers_mut().unwrap(); - for header in rocket_response.headers().iter() { - headers.append( - HeaderName::from_bytes(header.name.as_str().as_bytes()).unwrap(), - HeaderValue::from_str(header.value.as_ref()).unwrap(), - ); - } - - (rocket_response, builder) - } -} - -impl TestService for RocketTestService { - fn new(test_name: &str) -> Self { - let mut db_path: PathBuf = ["test-output", test_name].iter().collect(); - fs::create_dir_all(&db_path).unwrap(); - db_path.push("db.sqlite"); - - if db_path.exists() { - fs::remove_file(&db_path).unwrap(); - } - - let context = service::ContextBuilder::new() - .database_file_path(db_path) - .web_dir_path(Path::new("test-data/web").into()) - .swagger_dir_path(["docs", "swagger"].iter().collect()) - .cache_dir_path(["test-output", test_name].iter().collect()) - .build() - .unwrap(); - - let server = service::get_server(context).unwrap(); - let client = Client::new(server).unwrap(); - let request_builder = protocol::RequestBuilder::new(); - RocketTestService { - request_builder, - client, - } - } - - fn request_builder(&self) -> &protocol::RequestBuilder { - &self.request_builder - } - - fn fetch(&mut self, request: &Request) -> Response<()> { - let (_, builder) = self.process_internal(request); - builder.body(()).unwrap() - } - - fn fetch_bytes(&mut self, request: &Request) -> Response> { - let (mut rocket_response, builder) = self.process_internal(request); - let body = rocket_response.body().unwrap().into_bytes().unwrap(); - builder.body(body).unwrap() - } - - fn fetch_json( - &mut self, - request: &Request, - ) -> Response { - let (mut rocket_response, builder) = self.process_internal(request); - let body = rocket_response.body_string().unwrap(); - let body = serde_json::from_str(&body).unwrap(); - builder.body(body).unwrap() - } -} diff --git a/src/service/test/admin.rs b/src/service/test/admin.rs index 1cb39bd..cb023f2 100644 --- a/src/service/test/admin.rs +++ b/src/service/test/admin.rs @@ -2,13 +2,13 @@ use http::StatusCode; use crate::app::index; use crate::service::dto; -use crate::service::test::{ServiceType, TestService}; +use crate::service::test::{protocol, ServiceType, TestService}; use crate::test_name; #[test] fn test_returns_api_version() { let mut service = ServiceType::new(&test_name!()); - let request = service.request_builder().version(); + let request = protocol::version(); let response = service.fetch_json::<_, dto::Version>(&request); assert_eq!(response.status(), StatusCode::OK); } @@ -16,7 +16,7 @@ fn test_returns_api_version() { #[test] fn test_initial_setup_golden_path() { let mut service = ServiceType::new(&test_name!()); - let request = service.request_builder().initial_setup(); + let request = protocol::initial_setup(); { let response = service.fetch_json::<_, dto::InitialSetup>(&request); assert_eq!(response.status(), StatusCode::OK); @@ -48,7 +48,7 @@ fn test_trigger_index_golden_path() { service.complete_initial_setup(); service.login_admin(); - let request = service.request_builder().random(); + let request = protocol::random(); let response = service.fetch_json::<_, Vec>(&request); let entries = response.body(); @@ -65,7 +65,7 @@ fn test_trigger_index_golden_path() { fn test_trigger_index_requires_auth() { let mut service = ServiceType::new(&test_name!()); service.complete_initial_setup(); - let request = service.request_builder().trigger_index(); + let request = protocol::trigger_index(); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -75,7 +75,7 @@ fn test_trigger_index_requires_admin() { let mut service = ServiceType::new(&test_name!()); service.complete_initial_setup(); service.login(); - let request = service.request_builder().trigger_index(); + let request = protocol::trigger_index(); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::FORBIDDEN); } diff --git a/src/service/test/auth.rs b/src/service/test/auth.rs index 1927336..2597358 100644 --- a/src/service/test/auth.rs +++ b/src/service/test/auth.rs @@ -3,7 +3,7 @@ use headers::{self, HeaderMapExt}; use http::{Response, StatusCode}; use crate::service::dto; -use crate::service::test::{constants::*, ServiceType, TestService}; +use crate::service::test::{constants::*, protocol, ServiceType, TestService}; use crate::test_name; fn validate_cookies(response: &Response) { @@ -46,7 +46,7 @@ fn test_login_rejects_bad_username() { let mut service = ServiceType::new(&test_name!()); service.complete_initial_setup(); - let request = service.request_builder().login("garbage", TEST_PASSWORD); + let request = protocol::login("garbage", TEST_PASSWORD); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -56,7 +56,7 @@ fn test_login_rejects_bad_password() { let mut service = ServiceType::new(&test_name!()); service.complete_initial_setup(); - let request = service.request_builder().login(TEST_USERNAME, "garbage"); + let request = protocol::login(TEST_USERNAME, "garbage"); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -66,9 +66,7 @@ fn test_login_golden_path() { let mut service = ServiceType::new(&test_name!()); service.complete_initial_setup(); - let request = service - .request_builder() - .login(TEST_USERNAME, TEST_PASSWORD); + let request = protocol::login(TEST_USERNAME, TEST_PASSWORD); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::OK); @@ -81,7 +79,7 @@ fn test_requests_without_auth_header_do_not_set_cookies() { service.complete_initial_setup(); service.login(); - let request = service.request_builder().random(); + let request = protocol::random(); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::OK); @@ -93,7 +91,7 @@ fn test_authentication_via_http_header_rejects_bad_username() { let mut service = ServiceType::new(&test_name!()); service.complete_initial_setup(); - let mut request = service.request_builder().random(); + let mut request = protocol::random(); let basic = headers::Authorization::basic("garbage", TEST_PASSWORD); request.headers_mut().typed_insert(basic); @@ -106,7 +104,7 @@ fn test_authentication_via_http_header_rejects_bad_password() { let mut service = ServiceType::new(&test_name!()); service.complete_initial_setup(); - let mut request = service.request_builder().random(); + let mut request = protocol::random(); let basic = headers::Authorization::basic(TEST_PASSWORD, "garbage"); request.headers_mut().typed_insert(basic); @@ -119,7 +117,7 @@ fn test_authentication_via_http_header_golden_path() { let mut service = ServiceType::new(&test_name!()); service.complete_initial_setup(); - let mut request = service.request_builder().random(); + let mut request = protocol::random(); let basic = headers::Authorization::basic(TEST_USERNAME, TEST_PASSWORD); request.headers_mut().typed_insert(basic); diff --git a/src/service/test/collection.rs b/src/service/test/collection.rs index a758d65..564d51f 100644 --- a/src/service/test/collection.rs +++ b/src/service/test/collection.rs @@ -2,13 +2,13 @@ use http::StatusCode; use std::path::{Path, PathBuf}; use crate::app::index; -use crate::service::test::{add_trailing_slash, constants::*, ServiceType, TestService}; +use crate::service::test::{add_trailing_slash, constants::*, protocol, ServiceType, TestService}; use crate::test_name; #[test] fn test_browse_requires_auth() { let mut service = ServiceType::new(&test_name!()); - let request = service.request_builder().browse(&PathBuf::new()); + let request = protocol::browse(&PathBuf::new()); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -21,7 +21,7 @@ fn test_browse_root() { service.index(); service.login(); - let request = service.request_builder().browse(&PathBuf::new()); + let request = protocol::browse(&PathBuf::new()); let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); @@ -37,7 +37,7 @@ fn test_browse_directory() { service.login(); let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect(); - let request = service.request_builder().browse(&path); + let request = protocol::browse(&path); let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); @@ -51,7 +51,7 @@ fn test_browse_bad_directory() { service.login(); let path: PathBuf = ["not_my_collection"].iter().collect(); - let request = service.request_builder().browse(&path); + let request = protocol::browse(&path); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::NOT_FOUND); } @@ -59,7 +59,7 @@ fn test_browse_bad_directory() { #[test] fn test_flatten_requires_auth() { let mut service = ServiceType::new(&test_name!()); - let request = service.request_builder().flatten(&PathBuf::new()); + let request = protocol::flatten(&PathBuf::new()); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -72,7 +72,7 @@ fn test_flatten_root() { service.index(); service.login(); - let request = service.request_builder().flatten(&PathBuf::new()); + let request = protocol::flatten(&PathBuf::new()); let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); @@ -87,9 +87,7 @@ fn test_flatten_directory() { service.index(); service.login(); - let request = service - .request_builder() - .flatten(Path::new(TEST_MOUNT_NAME)); + let request = protocol::flatten(Path::new(TEST_MOUNT_NAME)); let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); @@ -103,7 +101,7 @@ fn test_flatten_bad_directory() { service.login(); let path: PathBuf = ["not_my_collection"].iter().collect(); - let request = service.request_builder().flatten(&path); + let request = protocol::flatten(&path); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::NOT_FOUND); } @@ -111,7 +109,7 @@ fn test_flatten_bad_directory() { #[test] fn test_random_requires_auth() { let mut service = ServiceType::new(&test_name!()); - let request = service.request_builder().random(); + let request = protocol::random(); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -124,7 +122,7 @@ fn test_random_golden_path() { service.index(); service.login(); - let request = service.request_builder().random(); + let request = protocol::random(); let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); @@ -139,7 +137,7 @@ fn test_random_with_trailing_slash() { service.index(); service.login(); - let mut request = service.request_builder().random(); + let mut request = protocol::random(); add_trailing_slash(&mut request); let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); @@ -150,7 +148,7 @@ fn test_random_with_trailing_slash() { #[test] fn test_recent_requires_auth() { let mut service = ServiceType::new(&test_name!()); - let request = service.request_builder().recent(); + let request = protocol::recent(); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -163,7 +161,7 @@ fn test_recent_golden_path() { service.index(); service.login(); - let request = service.request_builder().recent(); + let request = protocol::recent(); let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); @@ -178,7 +176,7 @@ fn test_recent_with_trailing_slash() { service.index(); service.login(); - let mut request = service.request_builder().recent(); + let mut request = protocol::recent(); add_trailing_slash(&mut request); let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); @@ -189,7 +187,7 @@ fn test_recent_with_trailing_slash() { #[test] fn test_search_requires_auth() { let mut service = ServiceType::new(&test_name!()); - let request = service.request_builder().search(""); + let request = protocol::search(""); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -200,7 +198,7 @@ fn test_search_without_query() { service.complete_initial_setup(); service.login(); - let request = service.request_builder().search(""); + let request = protocol::search(""); let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); } @@ -213,7 +211,7 @@ fn test_search_with_query() { service.index(); service.login(); - let request = service.request_builder().search("door"); + let request = protocol::search("door"); let response = service.fetch_json::<_, Vec>(&request); let results = response.body(); assert_eq!(results.len(), 1); diff --git a/src/service/test/lastfm.rs b/src/service/test/lastfm.rs new file mode 100644 index 0000000..db49398 --- /dev/null +++ b/src/service/test/lastfm.rs @@ -0,0 +1,39 @@ +use http::StatusCode; +use std::path::PathBuf; + +use crate::service::test::{constants::*, protocol, ServiceType, TestService}; +use crate::test_name; + +#[test] +fn test_lastfm_scrobble_rejects_unlinked_user() { + let mut service = ServiceType::new(&test_name!()); + service.complete_initial_setup(); + service.login_admin(); + service.index(); + service.login(); + + let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"] + .iter() + .collect(); + + let request = protocol::lastfm_scrobble(&path); + let response = service.fetch(&request); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); +} + +#[test] +fn test_lastfm_now_playing_rejects_unlinked_user() { + let mut service = ServiceType::new(&test_name!()); + service.complete_initial_setup(); + service.login_admin(); + service.index(); + service.login(); + + let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"] + .iter() + .collect(); + + let request = protocol::lastfm_now_playing(&path); + let response = service.fetch(&request); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); +} diff --git a/src/service/test/media.rs b/src/service/test/media.rs index 3ca3065..7c7ec95 100644 --- a/src/service/test/media.rs +++ b/src/service/test/media.rs @@ -1,7 +1,7 @@ use http::{header, HeaderValue, StatusCode}; use std::path::PathBuf; -use crate::service::test::{constants::*, ServiceType, TestService}; +use crate::service::test::{constants::*, protocol, ServiceType, TestService}; use crate::test_name; #[test] @@ -12,7 +12,7 @@ fn test_audio_requires_auth() { .iter() .collect(); - let request = service.request_builder().audio(&path); + let request = protocol::audio(&path); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -29,7 +29,7 @@ fn test_audio_golden_path() { .iter() .collect(); - let request = service.request_builder().audio(&path); + let request = protocol::audio(&path); let response = service.fetch_bytes(&request); assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.body().len(), 24_142); @@ -47,7 +47,7 @@ fn test_audio_partial_content() { .iter() .collect(); - let mut request = service.request_builder().audio(&path); + let mut request = protocol::audio(&path); let headers = request.headers_mut(); headers.append( header::RANGE, @@ -71,7 +71,7 @@ fn test_audio_bad_path_returns_not_found() { let path: PathBuf = ["not_my_collection"].iter().collect(); - let request = service.request_builder().audio(&path); + let request = protocol::audio(&path); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::NOT_FOUND); } @@ -85,7 +85,7 @@ fn test_thumbnail_requires_auth() { .collect(); let pad = None; - let request = service.request_builder().thumbnail(&path, pad); + let request = protocol::thumbnail(&path, pad); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -103,7 +103,7 @@ fn test_thumbnail_golden_path() { .collect(); let pad = None; - let request = service.request_builder().thumbnail(&path, pad); + let request = protocol::thumbnail(&path, pad); let response = service.fetch_bytes(&request); assert_eq!(response.status(), StatusCode::OK); } @@ -117,7 +117,7 @@ fn test_thumbnail_bad_path_returns_not_found() { let path: PathBuf = ["not_my_collection"].iter().collect(); let pad = None; - let request = service.request_builder().thumbnail(&path, pad); + let request = protocol::thumbnail(&path, pad); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::NOT_FOUND); } diff --git a/src/service/test/mod.rs b/src/service/test/mod.rs index 5fdc909..8ec7252 100644 --- a/src/service/test/mod.rs +++ b/src/service/test/mod.rs @@ -10,6 +10,7 @@ pub mod protocol; mod admin; mod auth; mod collection; +mod lastfm; mod media; mod playlist; mod preferences; @@ -20,15 +21,16 @@ mod web; use crate::app::{config, index, vfs}; use crate::service::test::constants::*; -#[cfg(feature = "service-rocket")] -pub use crate::service::rocket::test::ServiceType; +pub use crate::service::actix::test::ServiceType; pub trait TestService { fn new(test_name: &str) -> Self; - fn request_builder(&self) -> &protocol::RequestBuilder; - fn fetch(&mut self, request: &Request) -> Response<()>; - fn fetch_bytes(&mut self, request: &Request) -> Response>; - fn fetch_json( + fn fetch(&mut self, request: &Request) -> Response<()>; + fn fetch_bytes( + &mut self, + request: &Request, + ) -> Response>; + fn fetch_json( &mut self, request: &Request, ) -> Response; @@ -55,32 +57,30 @@ pub trait TestService { source: TEST_MOUNT_SOURCE.into(), }]), }; - let request = self.request_builder().put_settings(configuration); + let request = protocol::put_settings(configuration); let response = self.fetch(&request); assert_eq!(response.status(), StatusCode::OK); } fn login_admin(&mut self) { - let request = self - .request_builder() - .login(TEST_USERNAME_ADMIN, TEST_PASSWORD_ADMIN); + let request = protocol::login(TEST_USERNAME_ADMIN, TEST_PASSWORD_ADMIN); let response = self.fetch(&request); assert_eq!(response.status(), StatusCode::OK); } fn login(&mut self) { - let request = self.request_builder().login(TEST_USERNAME, TEST_PASSWORD); + let request = protocol::login(TEST_USERNAME, TEST_PASSWORD); let response = self.fetch(&request); assert_eq!(response.status(), StatusCode::OK); } fn index(&mut self) { - let request = self.request_builder().trigger_index(); + let request = protocol::trigger_index(); let response = self.fetch(&request); assert_eq!(response.status(), StatusCode::OK); loop { - let browse_request = self.request_builder().browse(Path::new("")); + let browse_request = protocol::browse(Path::new("")); let response = self.fetch_json::<(), Vec>(&browse_request); let entries = response.body(); if entries.len() > 0 { @@ -90,7 +90,7 @@ pub trait TestService { } loop { - let flatten_request = self.request_builder().flatten(Path::new("")); + let flatten_request = protocol::flatten(Path::new("")); let response = self.fetch_json::<_, Vec>(&flatten_request); let entries = response.body(); if entries.len() > 0 { diff --git a/src/service/test/playlist.rs b/src/service/test/playlist.rs index 0a83bd1..1b2e812 100644 --- a/src/service/test/playlist.rs +++ b/src/service/test/playlist.rs @@ -2,13 +2,13 @@ use http::StatusCode; use crate::app::index; use crate::service::dto; -use crate::service::test::{constants::*, ServiceType, TestService}; +use crate::service::test::{constants::*, protocol, ServiceType, TestService}; use crate::test_name; #[test] fn test_list_playlists_requires_auth() { let mut service = ServiceType::new(&test_name!()); - let request = service.request_builder().playlists(); + let request = protocol::playlists(); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -18,7 +18,7 @@ fn test_list_playlists_golden_path() { let mut service = ServiceType::new(&test_name!()); service.complete_initial_setup(); service.login(); - let request = service.request_builder().playlists(); + let request = protocol::playlists(); let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); } @@ -27,9 +27,7 @@ fn test_list_playlists_golden_path() { fn test_save_playlist_requires_auth() { let mut service = ServiceType::new(&test_name!()); let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() }; - let request = service - .request_builder() - .save_playlist(TEST_PLAYLIST_NAME, my_playlist); + let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -41,9 +39,22 @@ fn test_save_playlist_golden_path() { service.login(); let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() }; - let request = service - .request_builder() - .save_playlist(TEST_PLAYLIST_NAME, my_playlist); + let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist); + let response = service.fetch(&request); + assert_eq!(response.status(), StatusCode::OK); +} + +#[test] +fn test_save_playlist_large() { + let mut service = ServiceType::new(&test_name!()); + service.complete_initial_setup(); + service.login(); + + let tracks = (0..100_000) + .map(|_| "My Super Cool Song".to_string()) + .collect(); + let my_playlist = dto::SavePlaylistInput { tracks }; + let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::OK); } @@ -51,7 +62,7 @@ fn test_save_playlist_golden_path() { #[test] fn test_get_playlist_requires_auth() { let mut service = ServiceType::new(&test_name!()); - let request = service.request_builder().read_playlist(TEST_PLAYLIST_NAME); + let request = protocol::read_playlist(TEST_PLAYLIST_NAME); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -64,14 +75,12 @@ fn test_get_playlist_golden_path() { { let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() }; - let request = service - .request_builder() - .save_playlist(TEST_PLAYLIST_NAME, my_playlist); + let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::OK); } - let request = service.request_builder().read_playlist(TEST_PLAYLIST_NAME); + let request = protocol::read_playlist(TEST_PLAYLIST_NAME); let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); } @@ -82,7 +91,7 @@ fn test_get_playlist_bad_name_returns_not_found() { service.complete_initial_setup(); service.login(); - let request = service.request_builder().read_playlist(TEST_PLAYLIST_NAME); + let request = protocol::read_playlist(TEST_PLAYLIST_NAME); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::NOT_FOUND); } @@ -90,9 +99,7 @@ fn test_get_playlist_bad_name_returns_not_found() { #[test] fn test_delete_playlist_requires_auth() { let mut service = ServiceType::new(&test_name!()); - let request = service - .request_builder() - .delete_playlist(TEST_PLAYLIST_NAME); + let request = protocol::delete_playlist(TEST_PLAYLIST_NAME); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -105,16 +112,12 @@ fn test_delete_playlist_golden_path() { { let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() }; - let request = service - .request_builder() - .save_playlist(TEST_PLAYLIST_NAME, my_playlist); + let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::OK); } - let request = service - .request_builder() - .delete_playlist(TEST_PLAYLIST_NAME); + let request = protocol::delete_playlist(TEST_PLAYLIST_NAME); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::OK); } @@ -125,9 +128,7 @@ fn test_delete_playlist_bad_name_returns_not_found() { service.complete_initial_setup(); service.login(); - let request = service - .request_builder() - .delete_playlist(TEST_PLAYLIST_NAME); + let request = protocol::delete_playlist(TEST_PLAYLIST_NAME); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::NOT_FOUND); } diff --git a/src/service/test/preferences.rs b/src/service/test/preferences.rs index b5b624a..1634557 100644 --- a/src/service/test/preferences.rs +++ b/src/service/test/preferences.rs @@ -1,13 +1,13 @@ use http::StatusCode; use crate::app::user; -use crate::service::test::{ServiceType, TestService}; +use crate::service::test::{protocol, ServiceType, TestService}; use crate::test_name; #[test] fn test_get_preferences_requires_auth() { let mut service = ServiceType::new(&test_name!()); - let request = service.request_builder().get_preferences(); + let request = protocol::get_preferences(); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -18,7 +18,7 @@ fn test_get_preferences_golden_path() { service.complete_initial_setup(); service.login(); - let request = service.request_builder().get_preferences(); + let request = protocol::get_preferences(); let response = service.fetch_json::<_, user::Preferences>(&request); assert_eq!(response.status(), StatusCode::OK); } @@ -26,9 +26,7 @@ fn test_get_preferences_golden_path() { #[test] fn test_put_preferences_requires_auth() { let mut service = ServiceType::new(&test_name!()); - let request = service - .request_builder() - .put_preferences(user::Preferences::default()); + let request = protocol::put_preferences(user::Preferences::default()); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -39,9 +37,7 @@ fn test_put_preferences_golden_path() { service.complete_initial_setup(); service.login(); - let request = service - .request_builder() - .put_preferences(user::Preferences::default()); + let request = protocol::put_preferences(user::Preferences::default()); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::OK); } diff --git a/src/service/test/protocol.rs b/src/service/test/protocol.rs index b8db27c..8ed8f00 100644 --- a/src/service/test/protocol.rs +++ b/src/service/test/protocol.rs @@ -5,205 +5,216 @@ use std::path::Path; use crate::app::{config, user}; use crate::service::dto; -pub struct RequestBuilder {} +pub fn web_index() -> Request<()> { + Request::builder() + .method(Method::GET) + .uri("/") + .body(()) + .unwrap() +} -impl RequestBuilder { - pub fn new() -> Self { - Self {} - } +pub fn swagger_index() -> Request<()> { + Request::builder() + .method(Method::GET) + .uri("/swagger") + .body(()) + .unwrap() +} - pub fn web_index(&self) -> Request<()> { - Request::builder() - .method(Method::GET) - .uri("/") - .body(()) - .unwrap() - } +pub fn version() -> Request<()> { + Request::builder() + .method(Method::GET) + .uri("/api/version") + .body(()) + .unwrap() +} - pub fn swagger_index(&self) -> Request<()> { - Request::builder() - .method(Method::GET) - .uri("/swagger") - .body(()) - .unwrap() - } +pub fn initial_setup() -> Request<()> { + Request::builder() + .method(Method::GET) + .uri("/api/initial_setup") + .body(()) + .unwrap() +} - pub fn version(&self) -> Request<()> { - Request::builder() - .method(Method::GET) - .uri("/api/version") - .body(()) - .unwrap() - } +pub fn login(username: &str, password: &str) -> Request { + let credentials = dto::AuthCredentials { + username: username.into(), + password: password.into(), + }; + Request::builder() + .method(Method::POST) + .uri("/api/auth") + .body(credentials) + .unwrap() +} - pub fn initial_setup(&self) -> Request<()> { - Request::builder() - .method(Method::GET) - .uri("/api/initial_setup") - .body(()) - .unwrap() - } +pub fn get_settings() -> Request<()> { + Request::builder() + .method(Method::GET) + .uri("/api/settings") + .body(()) + .unwrap() +} - pub fn login(&self, username: &str, password: &str) -> Request { - let credentials = dto::AuthCredentials { - username: username.into(), - password: password.into(), - }; - Request::builder() - .method(Method::POST) - .uri("/api/auth") - .body(credentials) - .unwrap() - } +pub fn put_settings(configuration: config::Config) -> Request { + Request::builder() + .method(Method::PUT) + .uri("/api/settings") + .body(configuration) + .unwrap() +} - pub fn get_settings(&self) -> Request<()> { - Request::builder() - .method(Method::GET) - .uri("/api/settings") - .body(()) - .unwrap() - } +pub fn get_preferences() -> Request<()> { + Request::builder() + .method(Method::GET) + .uri("/api/preferences") + .body(()) + .unwrap() +} - pub fn put_settings(&self, configuration: config::Config) -> Request { - Request::builder() - .method(Method::PUT) - .uri("/api/settings") - .body(configuration) - .unwrap() - } +pub fn put_preferences(preferences: user::Preferences) -> Request { + Request::builder() + .method(Method::PUT) + .uri("/api/preferences") + .body(preferences) + .unwrap() +} - pub fn get_preferences(&self) -> Request<()> { - Request::builder() - .method(Method::GET) - .uri("/api/preferences") - .body(()) - .unwrap() - } +pub fn trigger_index() -> Request<()> { + Request::builder() + .method(Method::POST) + .uri("/api/trigger_index") + .body(()) + .unwrap() +} - pub fn put_preferences(&self, preferences: user::Preferences) -> Request { - Request::builder() - .method(Method::PUT) - .uri("/api/preferences") - .body(preferences) - .unwrap() - } +pub fn browse(path: &Path) -> Request<()> { + let path = path.to_string_lossy(); + let endpoint = format!("/api/browse/{}", url_encode(path.as_ref())); + Request::builder() + .method(Method::GET) + .uri(&endpoint) + .body(()) + .unwrap() +} - pub fn trigger_index(&self) -> Request<()> { - Request::builder() - .method(Method::POST) - .uri("/api/trigger_index") - .body(()) - .unwrap() - } +pub fn flatten(path: &Path) -> Request<()> { + let path = path.to_string_lossy(); + let endpoint = format!("/api/flatten/{}", url_encode(path.as_ref())); + Request::builder() + .method(Method::GET) + .uri(&endpoint) + .body(()) + .unwrap() +} - pub fn browse(&self, path: &Path) -> Request<()> { - let path = path.to_string_lossy(); - let uri = format!("/api/browse/{}", url_encode(path.as_ref())); - Request::builder() - .method(Method::GET) - .uri(uri) - .body(()) - .unwrap() - } +pub fn random() -> Request<()> { + Request::builder() + .method(Method::GET) + .uri("/api/random") + .body(()) + .unwrap() +} - pub fn flatten(&self, path: &Path) -> Request<()> { - let path = path.to_string_lossy(); - let uri = format!("/api/flatten/{}", url_encode(path.as_ref())); - Request::builder() - .method(Method::GET) - .uri(uri) - .body(()) - .unwrap() - } +pub fn recent() -> Request<()> { + Request::builder() + .method(Method::GET) + .uri("/api/recent") + .body(()) + .unwrap() +} - pub fn random(&self) -> Request<()> { - Request::builder() - .method(Method::GET) - .uri("/api/random") - .body(()) - .unwrap() - } +pub fn search(query: &str) -> Request<()> { + let endpoint = format!("/api/search/{}", url_encode(query)); + Request::builder() + .method(Method::GET) + .uri(&endpoint) + .body(()) + .unwrap() +} - pub fn recent(&self) -> Request<()> { - Request::builder() - .method(Method::GET) - .uri("/api/recent") - .body(()) - .unwrap() - } +pub fn audio(path: &Path) -> Request<()> { + let path = path.to_string_lossy(); + let endpoint = format!("/api/audio/{}", url_encode(path.as_ref())); + Request::builder() + .method(Method::GET) + .uri(&endpoint) + .body(()) + .unwrap() +} - pub fn search(&self, query: &str) -> Request<()> { - let uri = format!("/api/search/{}", url_encode(query)); - Request::builder() - .method(Method::GET) - .uri(uri) - .body(()) - .unwrap() - } +pub fn thumbnail(path: &Path, pad: Option) -> Request<()> { + let path = path.to_string_lossy(); + let mut endpoint = format!("/api/thumbnail/{}", url_encode(path.as_ref())); + match pad { + Some(true) => endpoint.push_str("?pad=true"), + Some(false) => endpoint.push_str("?pad=false"), + None => (), + }; + Request::builder() + .method(Method::GET) + .uri(&endpoint) + .body(()) + .unwrap() +} - pub fn audio(&self, path: &Path) -> Request<()> { - let path = path.to_string_lossy(); - let uri = format!("/api/audio/{}", url_encode(path.as_ref())); - Request::builder() - .method(Method::GET) - .uri(uri) - .body(()) - .unwrap() - } +pub fn playlists() -> Request<()> { + Request::builder() + .method(Method::GET) + .uri("/api/playlists") + .body(()) + .unwrap() +} - pub fn thumbnail(&self, path: &Path, pad: Option) -> Request<()> { - let path = path.to_string_lossy(); - let mut uri = format!("/api/thumbnail/{}", url_encode(path.as_ref())); - match pad { - Some(true) => uri.push_str("?pad=true"), - Some(false) => uri.push_str("?pad=false"), - None => (), - }; - Request::builder() - .method(Method::GET) - .uri(uri) - .body(()) - .unwrap() - } +pub fn save_playlist( + name: &str, + playlist: dto::SavePlaylistInput, +) -> Request { + let endpoint = format!("/api/playlist/{}", url_encode(name)); + Request::builder() + .method(Method::PUT) + .uri(&endpoint) + .body(playlist) + .unwrap() +} - pub fn playlists(&self) -> Request<()> { - Request::builder() - .method(Method::GET) - .uri("/api/playlists") - .body(()) - .unwrap() - } +pub fn read_playlist(name: &str) -> Request<()> { + let endpoint = format!("/api/playlist/{}", url_encode(name)); + Request::builder() + .method(Method::GET) + .uri(&endpoint) + .body(()) + .unwrap() +} - pub fn save_playlist( - &self, - name: &str, - playlist: dto::SavePlaylistInput, - ) -> Request { - let uri = format!("/api/playlist/{}", url_encode(name)); - Request::builder() - .method(Method::PUT) - .uri(uri) - .body(playlist) - .unwrap() - } +pub fn delete_playlist(name: &str) -> Request<()> { + let endpoint = format!("/api/playlist/{}", url_encode(name)); + Request::builder() + .method(Method::DELETE) + .uri(&endpoint) + .body(()) + .unwrap() +} - pub fn read_playlist(&self, name: &str) -> Request<()> { - let uri = format!("/api/playlist/{}", url_encode(name)); - Request::builder() - .method(Method::GET) - .uri(uri) - .body(()) - .unwrap() - } +pub fn lastfm_now_playing(path: &Path) -> Request<()> { + let path = path.to_string_lossy(); + let endpoint = format!("/api/lastfm/now_playing/{}", url_encode(path.as_ref())); + Request::builder() + .method(Method::PUT) + .uri(&endpoint) + .body(()) + .unwrap() +} - pub fn delete_playlist(&self, name: &str) -> Request<()> { - let uri = format!("/api/playlist/{}", url_encode(name)); - Request::builder() - .method(Method::DELETE) - .uri(uri) - .body(()) - .unwrap() - } +pub fn lastfm_scrobble(path: &Path) -> Request<()> { + let path = path.to_string_lossy(); + let endpoint = format!("/api/lastfm/scrobble/{}", url_encode(path.as_ref())); + Request::builder() + .method(Method::POST) + .uri(&endpoint) + .body(()) + .unwrap() } fn url_encode(input: &str) -> String { diff --git a/src/service/test/settings.rs b/src/service/test/settings.rs index f177e48..d07d28f 100644 --- a/src/service/test/settings.rs +++ b/src/service/test/settings.rs @@ -1,7 +1,7 @@ use http::StatusCode; use crate::app::config; -use crate::service::test::{constants::*, ServiceType, TestService}; +use crate::service::test::{constants::*, protocol, ServiceType, TestService}; use crate::test_name; #[test] @@ -9,7 +9,7 @@ fn test_get_settings_requires_auth() { let mut service = ServiceType::new(&test_name!()); service.complete_initial_setup(); - let request = service.request_builder().get_settings(); + let request = protocol::get_settings(); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -20,7 +20,7 @@ fn test_get_settings_requires_admin() { service.complete_initial_setup(); service.login(); - let request = service.request_builder().get_settings(); + let request = protocol::get_settings(); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::FORBIDDEN); } @@ -31,7 +31,7 @@ fn test_get_settings_golden_path() { service.complete_initial_setup(); service.login_admin(); - let request = service.request_builder().get_settings(); + let request = protocol::get_settings(); let response = service.fetch_json::<_, config::Config>(&request); assert_eq!(response.status(), StatusCode::OK); } @@ -40,9 +40,7 @@ fn test_get_settings_golden_path() { fn test_put_settings_requires_auth() { let mut service = ServiceType::new(&test_name!()); service.complete_initial_setup(); - let request = service - .request_builder() - .put_settings(config::Config::default()); + let request = protocol::put_settings(config::Config::default()); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -52,9 +50,7 @@ fn test_put_settings_requires_admin() { let mut service = ServiceType::new(&test_name!()); service.complete_initial_setup(); service.login(); - let request = service - .request_builder() - .put_settings(config::Config::default()); + let request = protocol::put_settings(config::Config::default()); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::FORBIDDEN); } @@ -65,9 +61,7 @@ fn test_put_settings_golden_path() { service.complete_initial_setup(); service.login_admin(); - let request = service - .request_builder() - .put_settings(config::Config::default()); + let request = protocol::put_settings(config::Config::default()); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::OK); } @@ -84,7 +78,7 @@ fn test_put_settings_cannot_unadmin_self() { password: "".into(), admin: false, }]); - let request = service.request_builder().put_settings(configuration); + let request = protocol::put_settings(configuration); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::CONFLICT); } diff --git a/src/service/test/swagger.rs b/src/service/test/swagger.rs index e10ed13..558ac40 100644 --- a/src/service/test/swagger.rs +++ b/src/service/test/swagger.rs @@ -1,12 +1,12 @@ use http::StatusCode; -use crate::service::test::{add_trailing_slash, ServiceType, TestService}; +use crate::service::test::{add_trailing_slash, protocol, ServiceType, TestService}; use crate::test_name; #[test] fn test_swagger_can_get_index() { let mut service = ServiceType::new(&test_name!()); - let request = service.request_builder().swagger_index(); + let request = protocol::swagger_index(); let response = service.fetch(&request); let status = response.status(); assert!(status == StatusCode::OK || status == StatusCode::PERMANENT_REDIRECT); @@ -15,7 +15,7 @@ fn test_swagger_can_get_index() { #[test] fn test_swagger_can_get_index_with_trailing_slash() { let mut service = ServiceType::new(&test_name!()); - let mut request = service.request_builder().swagger_index(); + let mut request = protocol::swagger_index(); add_trailing_slash(&mut request); let response = service.fetch(&request); assert_eq!(response.status(), StatusCode::OK); diff --git a/src/service/test/web.rs b/src/service/test/web.rs index 7e53dba..f688dd7 100644 --- a/src/service/test/web.rs +++ b/src/service/test/web.rs @@ -1,12 +1,12 @@ use http::StatusCode; -use crate::service::test::{ServiceType, TestService}; +use crate::service::test::{protocol, ServiceType, TestService}; use crate::test_name; #[test] fn test_serves_web_client() { let mut service = ServiceType::new(&test_name!()); - let request = service.request_builder().web_index(); + let request = protocol::web_index(); let response = service.fetch_bytes(&request); assert_eq!(response.status(), StatusCode::OK); }