diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 572c9bd..b821126 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - features: [--all-features, --no-default-features] + features: [--all-features, --features default] os: [ubuntu-latest, windows-latest, macOS-latest] steps: @@ -24,10 +24,10 @@ jobs: submodules: true - uses: actions-rs/toolchain@v1 with: - toolchain: nightly - override: true + toolchain: nightly-2020-01-14 + profile: minimal + default: true - uses: actions-rs/cargo@v1 with: command: test - toolchain: nightly args: --release ${{ matrix.features }} diff --git a/Cargo.lock b/Cargo.lock index a3c6229..9d4d4a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,6 +157,11 @@ dependencies = [ "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "bytes" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "c2-chacha" version = "0.2.3" @@ -429,6 +434,7 @@ dependencies = [ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "diesel_derives 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "libsqlite3-sys 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", + "r2d2 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -684,6 +690,24 @@ name = "fuchsia-zircon-sys" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "function_name" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "function_name-proc-macro 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "function_name-proc-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro-crate 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "futures" version = "0.1.29" @@ -787,6 +811,16 @@ dependencies = [ "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "http" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "http-body" version = "0.1.0" @@ -1383,6 +1417,15 @@ dependencies = [ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "parking_lot" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lock_api 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot_core 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "parking_lot_core" version = "0.6.2" @@ -1397,6 +1440,19 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "parking_lot_core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", + "smallvec 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "pbkdf2" version = "0.3.0" @@ -1465,11 +1521,14 @@ dependencies = [ "ape 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "app_dirs 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cookie 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "diesel 1.4.3 (registry+https://github.com/rust-lang/crates.io-index)", "diesel_migrations 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "flame 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "flamer 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "function_name 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "id3 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "image 0.22.3 (registry+https://github.com/rust-lang/crates.io-index)", "lewton 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1478,6 +1537,7 @@ dependencies = [ "metaflac 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "mp3-duration 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "pbkdf2 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1494,6 +1554,7 @@ dependencies = [ "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", "unix-daemonize 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1503,6 +1564,14 @@ name = "ppv-lite86" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "proc-macro-crate" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "toml 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "proc-macro2" version = "0.4.30" @@ -1552,6 +1621,16 @@ dependencies = [ "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "r2d2" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "scheduled-thread-pool 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rand" version = "0.3.23" @@ -1973,6 +2052,14 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "parking_lot 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "scoped_threadpool" version = "0.1.9" @@ -2663,6 +2750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" "checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" "checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +"checksum bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "10004c15deb332055f7a4a208190aed362cf9a7c2f6ab70a305fba50e1105f38" "checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" "checksum cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)" = "f52a465a666ca3d838ebbf08b241383421412fe7ebb463527bba275526d89f76" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" @@ -2724,6 +2812,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +"checksum function_name 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "88b2afa9b514dc3a75af6cf24d1914e1c7eb6f1b86de849147563548d5c0a0cd" +"checksum function_name-proc-macro 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6790a8d356d2f65d7972181e866b92a50a87c27d6a48cbe9dbb8be13ca784c7d" "checksum futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)" = "1b980f2816d6ee8673b6517b52cb0e808a180efc92e5c19d02cdda79066703ef" "checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" "checksum gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)" = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" @@ -2736,6 +2826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "023b39be39e3a2da62a94feb433e91e8bcd37676fbc8bea371daf52b7a769a3e" "checksum hmac 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695" "checksum http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" +"checksum http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b708cc7f06493459026f53b9a61a7a121a5d1ec6238dee58ea4941132b30156b" "checksum http-body 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" "checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" "checksum hyper 0.10.16 (registry+https://github.com/rust-lang/crates.io-index)" = "0a0652d9a2609a968c14be1a9ea00bf4b1d64e2e1f53a1b51b6fff3a6e829273" @@ -2798,8 +2889,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum openssl 0.10.26 (registry+https://github.com/rust-lang/crates.io-index)" = "3a3cc5799d98e1088141b8e01ff760112bbd9f19d850c124500566ca6901a585" "checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" "checksum openssl-sys 0.9.53 (registry+https://github.com/rust-lang/crates.io-index)" = "465d16ae7fc0e313318f7de5cecf57b2fbe7511fd213978b457e1c96ff46736f" +"checksum parking_lot 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "92e98c49ab0b7ce5b222f2cc9193fc4efe11c6d0bd4f648e374684a6857b1cfc" "checksum parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" "checksum parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" +"checksum parking_lot_core 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7582838484df45743c8434fbff785e8edf260c28748353d44bc0da32e0ceabf1" "checksum pbkdf2 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "006c038a43a45995a9670da19e67600114740e8511d4333bf97a56e66a7542d9" "checksum pear 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c26d2b92e47063ffce70d3e3b1bd097af121a9e0db07ca38a6cc1cf0cc85ff25" "checksum pear_codegen 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "336db4a192cc7f54efeb0c4e11a9245394824cc3bcbd37ba3ff51240c35d7a6e" @@ -2808,12 +2901,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" "checksum png 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)" = "247cb804bd7fc86d0c2b153d1374265e67945875720136ca8fe451f11c6aed52" "checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" +"checksum proc-macro-crate 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e10d4b51f154c8a7fb96fd6dad097cb74b863943ec010ac94b9fd1be8861fe1e" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" "checksum proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27" "checksum publicsuffix 1.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3bbaa49075179162b49acac1c6aa45fb4dafb5f13cf6794276d77bc7fd95757b" "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" "checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" +"checksum r2d2 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1497e40855348e4a8a40767d8e55174bce1e445a3ac9254ad44ad468ee0485af" "checksum rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)" = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" "checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" "checksum rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" @@ -2855,6 +2950,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum safemem 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" "checksum same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "585e8ddcedc187886a30fa705c47985c3fa88d06624095856b36ca0b82ff4421" "checksum schannel 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "87f550b06b6cba9c8b8be3ee73f391990116bf527450d2556e9b9ce263b9a021" +"checksum scheduled-thread-pool 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f5de7bc31f28f8e6c28df5e1bf3d10610f5fdc14cc95f272853512c70a2bd779" "checksum scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" "checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" "checksum sd-notify 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aef40838bbb143707f8309b1e92e6ba3225287592968ba6f6e3b6de4a9816486" diff --git a/Cargo.toml b/Cargo.toml index 2ac3737..d4afb38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,19 +5,24 @@ authors = ["Antoine Gersant "] edition = "2018" [features] +default = ["service-rocket"] ui = [] profile-index = ["flame", "flamer"] +service-rocket = ["rocket", "rocket_contrib"] [dependencies] anyhow = "1.0" ape = "0.2.0" app_dirs = "1.1.1" base64 = "0.11.0" -diesel = { version = "1.4", features = ["sqlite"] } +cookie = "0.12.0" +diesel = { version = "1.4", features = ["sqlite", "r2d2"] } diesel_migrations = { version = "1.4", features = ["sqlite"] } flame = { version = "0.2.2", optional = true } flamer = { version = "0.4", optional = true } +function_name = "0.2.0" getopts = "0.2.15" +http = "0.2" id3 = "0.3" image = "0.22" libsqlite3-sys = { version = "0.16", features = ["bundled-windows"] } @@ -30,7 +35,7 @@ pbkdf2 = "0.3" rand = "0.7" regex = "1.2" reqwest = "0.9.2" -rocket = "0.4.2" +rocket = { version = "0.4.2", optional = true } rust-crypto = "0.2.36" serde = { version = "1.0", features = ["derive"] } serde_derive = "1.0" @@ -39,11 +44,13 @@ simplelog = "0.7" thiserror = "1.0" time = "0.1" toml = "0.5" +url = "2.1" [dependencies.rocket_contrib] version = "0.4.2" default_features = false features = ["json", "serve"] +optional = true [target.'cfg(windows)'.dependencies] uuid = "0.8" @@ -55,3 +62,6 @@ features = ["winuser", "libloaderapi", "shellapi", "errhandlingapi"] [target.'cfg(unix)'.dependencies] sd-notify = "0.1.0" unix-daemonize = "0.1.2" + +[dev-dependencies] +percent-encoding = "2.1" \ No newline at end of file diff --git a/src/api_tests.rs b/src/api_tests.rs deleted file mode 100644 index f39f6a1..0000000 --- a/src/api_tests.rs +++ /dev/null @@ -1,526 +0,0 @@ -use rocket::http::hyper::header::*; -use rocket::http::uri::Uri; -use rocket::http::Status; -use rocket::local::Client; -use std::{thread, time}; - -use crate::api; -use crate::config; -use crate::ddns; -use crate::index; -use crate::vfs; - -use crate::test::get_test_environment; - -const TEST_USERNAME: &str = "test_user"; -const TEST_PASSWORD: &str = "test_password"; -const TEST_MOUNT_NAME: &str = "collection"; -const TEST_MOUNT_SOURCE: &str = "test/collection"; - -fn complete_initial_setup(client: &Client) { - let configuration = config::Config { - album_art_pattern: None, - prefix_url: None, - reindex_every_n_seconds: None, - ydns: None, - users: Some(vec![config::ConfigUser { - name: TEST_USERNAME.into(), - password: TEST_PASSWORD.into(), - admin: true, - }]), - mount_dirs: Some(vec![vfs::MountPoint { - name: TEST_MOUNT_NAME.into(), - source: TEST_MOUNT_SOURCE.into(), - }]), - }; - let body = serde_json::to_string(&configuration).unwrap(); - let response = client.put("/api/settings").body(&body).dispatch(); - assert_eq!(response.status(), Status::Ok); -} - -fn do_auth(client: &Client) { - let credentials = api::AuthCredentials { - username: TEST_USERNAME.into(), - password: TEST_PASSWORD.into(), - }; - let body = serde_json::to_string(&credentials).unwrap(); - let response = client.post("/api/auth").body(body).dispatch(); - assert_eq!(response.status(), Status::Ok); -} - -#[test] -fn version() { - let env = get_test_environment("api_version.sqlite"); - let client = &env.client; - let mut response = client.get("/api/version").dispatch(); - - assert_eq!(response.status(), Status::Ok); - - let response_body = response.body_string().unwrap(); - let response_json: api::Version = serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json, api::Version { major: 4, minor: 0 }); -} - -#[test] -fn initial_setup() { - let env = get_test_environment("api_initial_setup.sqlite"); - let client = &env.client; - - { - let mut response = client.get("/api/initial_setup").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: api::InitialSetup = serde_json::from_str(&response_body).unwrap(); - assert_eq!( - response_json, - api::InitialSetup { - has_any_users: false - } - ); - } - - complete_initial_setup(client); - - { - let mut response = client.get("/api/initial_setup").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: api::InitialSetup = serde_json::from_str(&response_body).unwrap(); - assert_eq!( - response_json, - api::InitialSetup { - has_any_users: true - } - ); - } -} - -#[test] -fn settings() { - let env = get_test_environment("api_settings.sqlite"); - let client = &env.client; - complete_initial_setup(client); - - { - let response = client.get("/api/settings").dispatch(); - assert_eq!(response.status(), Status::Unauthorized); - } - - do_auth(client); - - { - let mut response = client.get("/api/settings").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: config::Config = serde_json::from_str(&response_body).unwrap(); - assert_eq!( - response_json, - config::Config { - album_art_pattern: Some("Folder.(jpg|png)".to_string()), - reindex_every_n_seconds: Some(1800), - mount_dirs: Some(vec![vfs::MountPoint { - name: TEST_MOUNT_NAME.into(), - source: TEST_MOUNT_SOURCE.into() - }]), - prefix_url: None, - users: Some(vec![config::ConfigUser { - name: TEST_USERNAME.into(), - password: "".into(), - admin: true - }]), - ydns: Some(ddns::DDNSConfig { - host: "".into(), - username: "".into(), - password: "".into() - }), - } - ); - } - - let mut configuration = config::Config { - album_art_pattern: Some("my_pattern".to_owned()), - reindex_every_n_seconds: Some(3600), - mount_dirs: Some(vec![ - vfs::MountPoint { - name: TEST_MOUNT_NAME.into(), - source: TEST_MOUNT_SOURCE.into(), - }, - vfs::MountPoint { - name: "more_music".into(), - source: "test/collection".into(), - }, - ]), - prefix_url: Some("my_prefix".to_owned()), - users: Some(vec![ - config::ConfigUser { - name: "test_user".into(), - password: "some_password".into(), - admin: true, - }, - config::ConfigUser { - name: "other_user".into(), - password: "some_other_password".into(), - admin: false, - }, - ]), - ydns: Some(ddns::DDNSConfig { - host: "my_host".into(), - username: "my_username".into(), - password: "my_password".into(), - }), - }; - - let body = serde_json::to_string(&configuration).unwrap(); - - configuration.users = Some(vec![ - config::ConfigUser { - name: "test_user".into(), - password: "".into(), - admin: true, - }, - config::ConfigUser { - name: "other_user".into(), - password: "".into(), - admin: false, - }, - ]); - - client.put("/api/settings").body(body).dispatch(); - - { - let mut response = client.get("/api/settings").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: config::Config = serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json, configuration); - } -} - -#[test] -fn preferences() { - // TODO -} - -#[test] -fn trigger_index() { - let env = get_test_environment("api_trigger_index.sqlite"); - let client = &env.client; - complete_initial_setup(client); - do_auth(client); - - { - let mut response = client.get("/api/random").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: Vec = serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json.len(), 0); - } - - { - let response = client.post("/api/trigger_index").dispatch(); - assert_eq!(response.status(), Status::Ok); - } - - let timeout = time::Duration::from_secs(5); - thread::sleep(timeout); - - { - let mut response = client.get("/api/random").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: Vec = serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json.len(), 2); - } -} - -#[test] -fn auth() { - let env = get_test_environment("api_auth.sqlite"); - let client = &env.client; - complete_initial_setup(client); - - { - let credentials = api::AuthCredentials { - username: "garbage".into(), - password: "garbage".into(), - }; - let response = client - .post("/api/auth") - .body(serde_json::to_string(&credentials).unwrap()) - .dispatch(); - assert_eq!(response.status(), Status::Unauthorized); - } - { - let credentials = api::AuthCredentials { - username: TEST_USERNAME.into(), - password: "garbage".into(), - }; - let response = client - .post("/api/auth") - .body(serde_json::to_string(&credentials).unwrap()) - .dispatch(); - assert_eq!(response.status(), Status::Unauthorized); - } - { - let credentials = api::AuthCredentials { - username: TEST_USERNAME.into(), - password: TEST_PASSWORD.into(), - }; - let response = client - .post("/api/auth") - .body(serde_json::to_string(&credentials).unwrap()) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(response - .cookies() - .iter() - .any(|cookie| cookie.name() == "username")); - assert!(response - .cookies() - .iter() - .any(|cookie| cookie.name() == "admin")); - assert!(response - .cookies() - .iter() - .any(|cookie| cookie.name() == "session")); - } -} - -#[test] -fn browse() { - let env = get_test_environment("api_browse.sqlite"); - let client = &env.client; - complete_initial_setup(client); - do_auth(client); - env.update_index(); - - { - let mut response = client.get("/api/browse").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: Vec = - serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json.len(), 1); - } - - let mut next; - { - let mut response = client.get("/api/browse/collection").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: Vec = - serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json.len(), 2); - - match response_json[0] { - index::CollectionFile::Directory(ref d) => { - next = d.path.clone(); - } - _ => panic!(), - } - } - - // /api/browse/collection/Khemmis - { - let url = format!("/api/browse/{}", Uri::percent_encode(&next)); - let mut response = client.get(url).dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: Vec = - serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json.len(), 1); - match response_json[0] { - index::CollectionFile::Directory(ref d) => { - next = d.path.clone(); - } - _ => panic!(), - } - } - - // /api/browse/collection/Khemmis/Hunted - { - let url = format!("/api/browse/{}", Uri::percent_encode(&next)); - let mut response = client.get(url).dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: Vec = - serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json.len(), 5); - } -} - -#[test] -fn flatten() { - let env = get_test_environment("api_flatten.sqlite"); - let client = &env.client; - complete_initial_setup(client); - do_auth(client); - env.update_index(); - - { - let mut response = client.get("/api/flatten").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: Vec = serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json.len(), 12); - } - - { - let mut response = client.get("/api/flatten/collection").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: Vec = serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json.len(), 12); - } -} - -#[test] -fn random() { - let env = get_test_environment("api_random.sqlite"); - let client = &env.client; - complete_initial_setup(client); - do_auth(client); - env.update_index(); - - let mut response = client.get("/api/random").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: Vec = serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json.len(), 2); -} - -#[test] -fn recent() { - let env = get_test_environment("api_recent.sqlite"); - let client = &env.client; - complete_initial_setup(client); - do_auth(client); - env.update_index(); - - let mut response = client.get("/api/recent").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: Vec = serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json.len(), 2); -} - -#[test] -fn search() { - let env = get_test_environment("api_search.sqlite"); - let client = &env.client; - complete_initial_setup(client); - do_auth(client); - env.update_index(); - - let mut response = client.get("/api/search/door").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: Vec = serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json.len(), 1); - match response_json[0] { - index::CollectionFile::Song(ref s) => assert_eq!(s.title, Some("Beyond The Door".into())), - _ => panic!(), - } -} - -#[test] -fn serve() { - let env = get_test_environment("api_serve.sqlite"); - let client = &env.client; - complete_initial_setup(client); - do_auth(client); - env.update_index(); - - { - let mut response = client - .get("/api/serve/collection%2FKhemmis%2FHunted%2F02%20-%20Candlelight.mp3") - .dispatch(); - assert_eq!(response.status(), Status::Ok); - let body = response.body().unwrap(); - let body = body.into_bytes().unwrap(); - assert_eq!(body.len(), 24_142); - } - - { - let mut response = client - .get("/api/serve/collection%2FKhemmis%2FHunted%2F02%20-%20Candlelight.mp3") - .header(Range::bytes(100, 299)) - .dispatch(); - assert_eq!(response.status(), Status::PartialContent); - let body = response.body().unwrap(); - let body = body.into_bytes().unwrap(); - assert_eq!(body.len(), 200); - assert_eq!(response.headers().get_one("Content-Length").unwrap(), "200"); - } -} - -#[test] -fn playlists() { - let env = get_test_environment("api_playlists.sqlite"); - let client = &env.client; - complete_initial_setup(client); - do_auth(client); - env.update_index(); - - { - let mut response = client.get("/api/playlists").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: Vec = - serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json.len(), 0); - } - - { - let songs: Vec; - { - let mut response = client.get("/api/flatten").dispatch(); - let response_body = response.body_string().unwrap(); - songs = serde_json::from_str(&response_body).unwrap(); - } - let my_playlist = api::SavePlaylistInput { - tracks: songs[2..6].into_iter().map(|s| s.path.clone()).collect(), - }; - let response = client - .put("/api/playlist/my_playlist") - .body(serde_json::to_string(&my_playlist).unwrap()) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - } - - { - let mut response = client.get("/api/playlists").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: Vec = - serde_json::from_str(&response_body).unwrap(); - assert_eq!( - response_json, - vec![api::ListPlaylistsEntry { - name: "my_playlist".into() - }] - ); - } - - { - let mut response = client.get("/api/playlist/my_playlist").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: Vec = serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json.len(), 4); - } - - { - let response = client.delete("/api/playlist/my_playlist").dispatch(); - assert_eq!(response.status(), Status::Ok); - } - - { - let mut response = client.get("/api/playlists").dispatch(); - assert_eq!(response.status(), Status::Ok); - let response_body = response.body_string().unwrap(); - let response_json: Vec = - serde_json::from_str(&response_body).unwrap(); - assert_eq!(response_json.len(), 0); - } -} diff --git a/src/config.rs b/src/config.rs index de141b5..6af5520 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,8 +10,7 @@ use std::io::Read; use std::path; use toml; -use crate::db::ConnectionSource; -use crate::db::{ddns_config, misc_settings, mount_points, users}; +use crate::db::{ddns_config, misc_settings, mount_points, users, DB}; use crate::ddns::DDNSConfig; use crate::user::*; use crate::vfs::MountPoint; @@ -73,14 +72,11 @@ pub fn parse_toml_file(path: &path::Path) -> Result { Ok(config) } -pub fn read(db: &T) -> Result -where - T: ConnectionSource, -{ +pub fn read(db: &DB) -> Result { use self::ddns_config::dsl::*; use self::misc_settings::dsl::*; - let connection = db.get_connection(); + let connection = db.connect()?; let mut config = Config { album_art_pattern: None, @@ -97,7 +93,7 @@ where index_sleep_duration_seconds, prefix_url, )) - .get_result(connection.deref())?; + .get_result(&connection)?; config.album_art_pattern = Some(art_pattern); config.reindex_every_n_seconds = Some(sleep_duration); @@ -108,13 +104,13 @@ where use self::mount_points::dsl::*; mount_dirs = mount_points .select((source, name)) - .get_results(connection.deref())?; + .get_results(&connection)?; config.mount_dirs = Some(mount_dirs); } let found_users: Vec<(String, i32)> = users::table .select((users::columns::name, users::columns::admin)) - .get_results(connection.deref())?; + .get_results(&connection)?; config.users = Some( found_users .into_iter() @@ -128,46 +124,39 @@ where let ydns = ddns_config .select((host, username, password)) - .get_result(connection.deref())?; + .get_result(&connection)?; config.ydns = Some(ydns); Ok(config) } #[cfg(test)] -pub fn reset(db: &T) -> Result<()> -where - T: ConnectionSource, -{ +pub fn reset(db: &DB) -> Result<()> { use self::ddns_config::dsl::*; - let connection = db.get_connection(); + let connection = db.connect()?; - diesel::delete(mount_points::table).execute(connection.deref())?; - diesel::delete(users::table).execute(connection.deref())?; + diesel::delete(mount_points::table).execute(&connection)?; + diesel::delete(users::table).execute(&connection)?; diesel::update(ddns_config) .set((host.eq(""), username.eq(""), password.eq(""))) - .execute(connection.deref())?; + .execute(&connection)?; Ok(()) } -pub fn amend(db: &T, new_config: &Config) -> Result<()> -where - T: ConnectionSource, -{ - let connection = db.get_connection(); +pub fn amend(db: &DB, new_config: &Config) -> Result<()> { + let connection = db.connect()?; if let Some(ref mount_dirs) = new_config.mount_dirs { - diesel::delete(mount_points::table).execute(connection.deref())?; + diesel::delete(mount_points::table).execute(&connection)?; diesel::insert_into(mount_points::table) .values(mount_dirs) - .execute(connection.deref())?; + .execute(&*connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822 } if let Some(ref config_users) = new_config.users { - let old_usernames: Vec = users::table - .select(users::name) - .get_results(connection.deref())?; + let old_usernames: Vec = + users::table.select(users::name).get_results(&connection)?; // Delete users that are not in new list let delete_usernames: Vec = old_usernames @@ -176,7 +165,7 @@ where .filter(|old_name| config_users.iter().find(|u| &u.name == old_name).is_none()) .collect::<_>(); diesel::delete(users::table.filter(users::name.eq_any(&delete_usernames))) - .execute(connection.deref())?; + .execute(&connection)?; // Insert new users let insert_users: Vec<&ConfigUser> = config_users @@ -194,7 +183,7 @@ where let new_user = User::new(&config_user.name, &config_user.password)?; diesel::insert_into(users::table) .values(&new_user) - .execute(connection.deref())?; + .execute(&connection)?; } // Update users @@ -204,26 +193,26 @@ where let hash = hash_password(&user.password)?; diesel::update(users::table.filter(users::name.eq(&user.name))) .set(users::password_hash.eq(hash)) - .execute(connection.deref())?; + .execute(&connection)?; } // Update admin rights diesel::update(users::table.filter(users::name.eq(&user.name))) .set(users::admin.eq(user.admin as i32)) - .execute(connection.deref())?; + .execute(&connection)?; } } if let Some(sleep_duration) = new_config.reindex_every_n_seconds { diesel::update(misc_settings::table) .set(misc_settings::index_sleep_duration_seconds.eq(sleep_duration as i32)) - .execute(connection.deref())?; + .execute(&connection)?; } if let Some(ref album_art_pattern) = new_config.album_art_pattern { diesel::update(misc_settings::table) .set(misc_settings::index_album_art_pattern.eq(album_art_pattern)) - .execute(connection.deref())?; + .execute(&connection)?; } if let Some(ref ydns) = new_config.ydns { @@ -234,28 +223,25 @@ where username.eq(ydns.username.clone()), password.eq(ydns.password.clone()), )) - .execute(connection.deref())?; + .execute(&connection)?; } if let Some(ref prefix_url) = new_config.prefix_url { diesel::update(misc_settings::table) .set(misc_settings::prefix_url.eq(prefix_url)) - .execute(connection.deref())?; + .execute(&connection)?; } Ok(()) } -pub fn read_preferences(db: &T, username: &str) -> Result -where - T: ConnectionSource, -{ +pub fn read_preferences(db: &DB, username: &str) -> Result { use self::users::dsl::*; - let connection = db.get_connection(); + let connection = db.connect()?; let (theme_base, theme_accent, read_lastfm_username) = users .select((web_theme_base, web_theme_accent, lastfm_username)) .filter(name.eq(username)) - .get_result(connection.deref())?; + .get_result(&connection)?; Ok(Preferences { web_theme_base: theme_base, web_theme_accent: theme_accent, @@ -263,33 +249,24 @@ where }) } -pub fn write_preferences(db: &T, username: &str, preferences: &Preferences) -> Result<()> -where - T: ConnectionSource, -{ +pub fn write_preferences(db: &DB, username: &str, preferences: &Preferences) -> Result<()> { use crate::db::users::dsl::*; - let connection = db.get_connection(); + let connection = db.connect()?; diesel::update(users.filter(name.eq(username))) .set(( web_theme_base.eq(&preferences.web_theme_base), web_theme_accent.eq(&preferences.web_theme_accent), )) - .execute(connection.deref())?; + .execute(&connection)?; Ok(()) } -pub fn get_auth_secret(db: &T) -> Result> -where - T: ConnectionSource, -{ +pub fn get_auth_secret(db: &DB) -> Result> { use self::misc_settings::dsl::*; - let connection = db.get_connection(); + let connection = db.connect()?; - match misc_settings - .select(auth_secret) - .get_result(connection.deref()) - { + match misc_settings.select(auth_secret).get_result(&connection) { Err(diesel::result::Error::NotFound) => bail!("Cannot find authentication secret"), Ok(secret) => Ok(secret), Err(e) => Err(e.into()), @@ -391,11 +368,11 @@ fn test_amend_preserve_password_hashes() { amend(&db, &initial_config).unwrap(); { - let connection = db.get_connection(); + let connection = db.connect().unwrap(); initial_hash = users .select(password_hash) .filter(name.eq("Teddy🐻")) - .get_result(connection.deref()) + .get_result(&connection) .unwrap(); } @@ -421,11 +398,11 @@ fn test_amend_preserve_password_hashes() { amend(&db, &new_config).unwrap(); { - let connection = db.get_connection(); + let connection = db.connect().unwrap(); new_hash = users .select(password_hash) .filter(name.eq("Teddy🐻")) - .get_result(connection.deref()) + .get_result(&connection) .unwrap(); } @@ -453,8 +430,8 @@ fn test_amend_ignore_blank_users() { }; amend(&db, &config).unwrap(); - let connection = db.get_connection(); - let user_count: i64 = users.count().get_result(connection.deref()).unwrap(); + let connection = db.connect().unwrap(); + let user_count: i64 = users.count().get_result(&connection).unwrap(); assert_eq!(user_count, 0); } @@ -473,8 +450,8 @@ fn test_amend_ignore_blank_users() { }; amend(&db, &config).unwrap(); - let connection = db.get_connection(); - let user_count: i64 = users.count().get_result(connection.deref()).unwrap(); + let connection = db.connect().unwrap(); + let user_count: i64 = users.count().get_result(&connection).unwrap(); assert_eq!(user_count, 0); } } @@ -500,8 +477,8 @@ fn test_toggle_admin() { amend(&db, &initial_config).unwrap(); { - let connection = db.get_connection(); - let is_admin: i32 = users.select(admin).get_result(connection.deref()).unwrap(); + let connection = db.connect().unwrap(); + let is_admin: i32 = users.select(admin).get_result(&connection).unwrap(); assert_eq!(is_admin, 1); } @@ -520,8 +497,8 @@ fn test_toggle_admin() { amend(&db, &new_config).unwrap(); { - let connection = db.get_connection(); - let is_admin: i32 = users.select(admin).get_result(connection.deref()).unwrap(); + let connection = db.connect().unwrap(); + let is_admin: i32 = users.select(admin).get_result(&connection).unwrap(); assert_eq!(is_admin, 0); } } diff --git a/src/db/mod.rs b/src/db/mod.rs index 17e5ea3..e59695d 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,11 +1,10 @@ use anyhow::*; -use core::ops::Deref; -use diesel::prelude::*; +use diesel::r2d2::{self, ConnectionManager, PooledConnection}; use diesel::sqlite::SqliteConnection; +use diesel::RunQueryDsl; use diesel_migrations; use log::info; use std::path::Path; -use std::sync::{Arc, Mutex, MutexGuard}; mod schema; @@ -15,44 +14,49 @@ pub use self::schema::*; const DB_MIGRATIONS_PATH: &str = "migrations"; embed_migrations!("migrations"); -pub trait ConnectionSource { - fn get_connection(&self) -> MutexGuard<'_, SqliteConnection>; - fn get_connection_mutex(&self) -> Arc>; +#[derive(Clone)] +pub struct DB { + pool: r2d2::Pool>, } -pub struct DB { - connection: Arc>, +#[derive(Debug)] +struct ConnectionCustomizer {} +impl diesel::r2d2::CustomizeConnection for ConnectionCustomizer { + fn on_acquire(&self, connection: &mut SqliteConnection) -> Result<(), diesel::r2d2::Error> { + let query = diesel::sql_query(r#" + PRAGMA busy_timeout = 60000; + PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + PRAGMA foreign_keys = ON; + "#); + query.execute(connection) + .map_err(|e| diesel::r2d2::Error::QueryError(e))?; + Ok(()) + } } impl DB { pub fn new(path: &Path) -> Result { info!("Database file path: {}", path.to_string_lossy()); - let connection = Arc::new(Mutex::new(SqliteConnection::establish( - &path.to_string_lossy(), - )?)); - let db = DB { - connection: connection.clone(), - }; - db.init()?; + let manager = ConnectionManager::::new(path.to_string_lossy()); + let pool = diesel::r2d2::Pool::builder() + .connection_customizer(Box::new(ConnectionCustomizer {})) + .build(manager)?; + let db = DB { pool: pool }; + db.migrate_up()?; Ok(db) } - fn init(&self) -> Result<()> { - { - let connection = self.connection.lock().unwrap(); - connection.execute("PRAGMA synchronous = NORMAL")?; - } - self.migrate_up()?; - Ok(()) + pub fn connect(&self) -> Result>> { + self.pool.get().map_err(Error::new) } #[allow(dead_code)] fn migrate_down(&self) -> Result<()> { - let connection = self.connection.lock().unwrap(); - let connection = connection.deref(); + let connection = self.connect().unwrap(); loop { match diesel_migrations::revert_latest_migration_in_directory( - connection, + &connection, Path::new(DB_MIGRATIONS_PATH), ) { Ok(_) => (), @@ -66,23 +70,12 @@ impl DB { } fn migrate_up(&self) -> Result<()> { - let connection = self.connection.lock().unwrap(); - let connection = connection.deref(); - embedded_migrations::run(connection)?; + let connection = self.connect().unwrap(); + embedded_migrations::run(&connection)?; Ok(()) } } -impl ConnectionSource for DB { - fn get_connection(&self) -> MutexGuard<'_, SqliteConnection> { - self.connection.lock().unwrap() - } - - fn get_connection_mutex(&self) -> Arc> { - self.connection.clone() - } -} - #[cfg(test)] pub fn get_test_db(name: &str) -> DB { use crate::config; diff --git a/src/ddns.rs b/src/ddns.rs index c5f5e4e..2dcf6ea 100644 --- a/src/ddns.rs +++ b/src/ddns.rs @@ -1,5 +1,4 @@ use anyhow::*; -use core::ops::Deref; use diesel::prelude::*; use log::{error, info}; use reqwest; @@ -8,7 +7,7 @@ use std::thread; use std::time; use crate::db::ddns_config; -use crate::db::{ConnectionSource, DB}; +use crate::db::DB; #[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Queryable, Serialize)] #[table_name = "ddns_config"] @@ -25,19 +24,16 @@ pub trait DDNSConfigSource { impl DDNSConfigSource for DB { fn get_ddns_config(&self) -> Result { use self::ddns_config::dsl::*; - let connection = self.get_connection(); + let connection = self.connect()?; Ok(ddns_config .select((host, username, password)) - .get_result(connection.deref())?) + .get_result(&connection)?) } } const DDNS_UPDATE_URL: &str = "https://ydns.io/api/v1/update/"; -fn update_my_ip(config_source: &T) -> Result<()> -where - T: DDNSConfigSource, -{ +fn update_my_ip(config_source: &DB) -> Result<()> { let config = config_source.get_ddns_config()?; if config.host.is_empty() || config.username.is_empty() { info!("Skipping DDNS update because credentials are missing"); @@ -59,10 +55,7 @@ where Ok(()) } -pub fn run(config_source: &T) -where - T: DDNSConfigSource, -{ +pub fn run(config_source: &DB) { loop { if let Err(e) = update_my_ip(config_source) { error!("Dynamic DNS update error: {:?}", e); diff --git a/src/index.rs b/src/index.rs index d41cc98..ee31a62 100644 --- a/src/index.rs +++ b/src/index.rs @@ -4,7 +4,6 @@ use diesel; use diesel::dsl::sql; use diesel::prelude::*; use diesel::sql_types; -use diesel::sqlite::SqliteConnection; #[cfg(feature = "profile-index")] use flame; use log::{error, info}; @@ -22,8 +21,7 @@ use std::time; use crate::config::MiscSettings; #[cfg(test)] use crate::db; -use crate::db::{directories, misc_settings, songs}; -use crate::db::{ConnectionSource, DB}; +use crate::db::{directories, misc_settings, songs, DB}; use crate::metadata; use crate::vfs::{VFSSource, VFS}; @@ -80,16 +78,14 @@ impl CommandSender { } } -pub fn init(db: Arc) -> Arc { +pub fn init(db: DB) -> Arc { let (index_sender, index_receiver) = channel(); let command_sender = Arc::new(CommandSender::new(index_sender)); let command_receiver = CommandReceiver::new(index_receiver); // Start update loop - let db_ref = db.clone(); std::thread::spawn(move || { - let db = db_ref.deref(); - update_loop(db, &command_receiver); + update_loop(&db, &command_receiver); }); command_sender @@ -162,19 +158,16 @@ struct NewDirectory { date_added: i32, } -struct IndexBuilder<'conn> { +struct IndexBuilder { new_songs: Vec, new_directories: Vec, - connection: &'conn Mutex, + db: DB, album_art_pattern: Regex, } -impl<'conn> IndexBuilder<'conn> { +impl IndexBuilder { #[cfg_attr(feature = "profile-index", flame)] - fn new( - connection: &Mutex, - album_art_pattern: Regex, - ) -> Result> { + fn new(db: DB, album_art_pattern: Regex) -> Result { let mut new_songs = Vec::new(); let mut new_directories = Vec::new(); new_songs.reserve_exact(INDEX_BUILDING_INSERT_BUFFER_SIZE); @@ -182,35 +175,27 @@ impl<'conn> IndexBuilder<'conn> { Ok(IndexBuilder { new_songs, new_directories, - connection, + db, album_art_pattern, }) } #[cfg_attr(feature = "profile-index", flame)] fn flush_songs(&mut self) -> Result<()> { - let connection = self.connection.lock().unwrap(); - let connection = connection.deref(); - connection.transaction::<_, anyhow::Error, _>(|| { - diesel::insert_into(songs::table) - .values(&self.new_songs) - .execute(connection)?; - Ok(()) - })?; + let connection = self.db.connect()?; + diesel::insert_into(songs::table) + .values(&self.new_songs) + .execute(&*connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822 self.new_songs.clear(); Ok(()) } #[cfg_attr(feature = "profile-index", flame)] fn flush_directories(&mut self) -> Result<()> { - let connection = self.connection.lock().unwrap(); - let connection = connection.deref(); - connection.transaction::<_, anyhow::Error, _>(|| { - diesel::insert_into(directories::table) - .values(&self.new_directories) - .execute(connection)?; - Ok(()) - })?; + let connection = self.db.connect()?; + diesel::insert_into(directories::table) + .values(&self.new_directories) + .execute(&*connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822 self.new_directories.clear(); Ok(()) } @@ -364,17 +349,14 @@ impl<'conn> IndexBuilder<'conn> { } #[cfg_attr(feature = "profile-index", flame)] -fn clean(db: &T) -> Result<()> -where - T: ConnectionSource + VFSSource, -{ +fn clean(db: &DB) -> Result<()> { let vfs = db.get_vfs()?; { let all_songs: Vec; { - let connection = db.get_connection(); - all_songs = songs::table.select(songs::path).load(connection.deref())?; + let connection = db.connect()?; + all_songs = songs::table.select(songs::path).load(&connection)?; } let missing_songs = all_songs @@ -386,10 +368,10 @@ where .collect::>(); { - let connection = db.get_connection(); + let connection = db.connect()?; for chunk in missing_songs[..].chunks(INDEX_BUILDING_CLEAN_BUFFER_SIZE) { diesel::delete(songs::table.filter(songs::path.eq_any(chunk))) - .execute(connection.deref())?; + .execute(&connection)?; } } } @@ -397,10 +379,10 @@ where { let all_directories: Vec; { - let connection = db.get_connection(); + let connection = db.connect()?; all_directories = directories::table .select(directories::path) - .load(connection.deref())?; + .load(&connection)?; } let missing_directories = all_directories @@ -412,10 +394,10 @@ where .collect::>(); { - let connection = db.get_connection(); + let connection = db.connect()?; for chunk in missing_directories[..].chunks(INDEX_BUILDING_CLEAN_BUFFER_SIZE) { diesel::delete(directories::table.filter(directories::path.eq_any(chunk))) - .execute(connection.deref())?; + .execute(&connection)?; } } } @@ -424,22 +406,18 @@ where } #[cfg_attr(feature = "profile-index", flame)] -fn populate(db: &T) -> Result<()> -where - T: ConnectionSource + VFSSource, -{ +fn populate(db: &DB) -> Result<()> { let vfs = db.get_vfs()?; let mount_points = vfs.get_mount_points(); let album_art_pattern; { - let connection = db.get_connection(); - let settings: MiscSettings = misc_settings::table.get_result(connection.deref())?; + let connection = db.connect()?; + let settings: MiscSettings = misc_settings::table.get_result(&connection)?; album_art_pattern = Regex::new(&settings.index_album_art_pattern)?; } - let connection_mutex = db.get_connection_mutex(); - let mut builder = IndexBuilder::new(connection_mutex.deref(), album_art_pattern)?; + let mut builder = IndexBuilder::new(db.clone(), album_art_pattern)?; for target in mount_points.values() { builder.populate_directory(None, target.as_path())?; } @@ -448,10 +426,7 @@ where Ok(()) } -pub fn update(db: &T) -> Result<()> -where - T: ConnectionSource + VFSSource, -{ +pub fn update(db: &DB) -> Result<()> { let start = time::Instant::now(); info!("Beginning library index update"); clean(db)?; @@ -465,10 +440,7 @@ where Ok(()) } -fn update_loop(db: &T, command_buffer: &CommandReceiver) -where - T: ConnectionSource + VFSSource, -{ +fn update_loop(db: &DB, command_buffer: &CommandReceiver) { loop { // Wait for a command if command_buffer.receiver.recv().is_err() { @@ -492,10 +464,7 @@ where } } -pub fn self_trigger(db: &T, command_buffer: &Arc) -where - T: ConnectionSource, -{ +pub fn self_trigger(db: &DB, command_buffer: &Arc) { loop { { let command_buffer = command_buffer.deref(); @@ -504,19 +473,20 @@ where return; } } - let sleep_duration; - { - let connection = db.get_connection(); - let settings: Result = misc_settings::table - .get_result(connection.deref()) - .map_err(|e| e.into()); - if let Err(ref e) = settings { - error!("Could not retrieve index sleep duration: {}", e); - } - sleep_duration = settings - .map(|s| s.index_sleep_duration_seconds) - .unwrap_or(1800); - } + let sleep_duration = { + let connection = db.connect(); + connection + .and_then(|c| { + misc_settings::table + .get_result(&c) + .map_err(|e| Error::new(e)) + }) + .map(|s: MiscSettings| s.index_sleep_duration_seconds) + .unwrap_or_else(|e| { + error!("Could not retrieve index sleep duration: {}", e); + 1800 + }) + }; thread::sleep(time::Duration::from_secs(sleep_duration as u64)); } } @@ -551,20 +521,19 @@ fn virtualize_directory(vfs: &VFS, mut directory: Directory) -> Option(db: &T, virtual_path: P) -> Result> +pub fn browse

(db: &DB, virtual_path: P) -> Result> where - T: ConnectionSource + VFSSource, P: AsRef, { let mut output = Vec::new(); let vfs = db.get_vfs()?; - let connection = db.get_connection(); + let connection = db.connect()?; if virtual_path.as_ref().components().count() == 0 { // Browse top-level let real_directories: Vec = directories::table .filter(directories::parent.is_null()) - .load(connection.deref())?; + .load(&connection)?; let virtual_directories = real_directories .into_iter() .filter_map(|s| virtualize_directory(&vfs, s)); @@ -577,7 +546,7 @@ where let real_directories: Vec = directories::table .filter(directories::parent.eq(&real_path_string)) .order(sql::("path COLLATE NOCASE ASC")) - .load(connection.deref())?; + .load(&connection)?; let virtual_directories = real_directories .into_iter() .filter_map(|s| virtualize_directory(&vfs, s)); @@ -586,7 +555,7 @@ where let real_songs: Vec = songs::table .filter(songs::parent.eq(&real_path_string)) .order(sql::("path COLLATE NOCASE ASC")) - .load(connection.deref())?; + .load(&connection)?; let virtual_songs = real_songs .into_iter() .filter_map(|s| virtualize_song(&vfs, s)); @@ -596,14 +565,13 @@ where Ok(output) } -pub fn flatten(db: &T, virtual_path: P) -> Result> +pub fn flatten

(db: &DB, virtual_path: P) -> Result> where - T: ConnectionSource + VFSSource, P: AsRef, { use self::songs::dsl::*; let vfs = db.get_vfs()?; - let connection = db.get_connection(); + let connection = db.connect()?; let real_songs: Vec = if virtual_path.as_ref().parent() != None { let real_path = vfs.virtual_to_real(virtual_path)?; @@ -611,9 +579,9 @@ where songs .filter(path.like(&like_path)) .order(path) - .load(connection.deref())? + .load(&connection)? } else { - songs.order(path).load(connection.deref())? + songs.order(path).load(&connection)? }; let virtual_songs = real_songs @@ -622,48 +590,39 @@ where Ok(virtual_songs.collect::>()) } -pub fn get_random_albums(db: &T, count: i64) -> Result> -where - T: ConnectionSource + VFSSource, -{ +pub fn get_random_albums(db: &DB, count: i64) -> Result> { use self::directories::dsl::*; let vfs = db.get_vfs()?; - let connection = db.get_connection(); + let connection = db.connect()?; let real_directories = directories .filter(album.is_not_null()) .limit(count) .order(random) - .load(connection.deref())?; + .load(&connection)?; let virtual_directories = real_directories .into_iter() .filter_map(|s| virtualize_directory(&vfs, s)); Ok(virtual_directories.collect::>()) } -pub fn get_recent_albums(db: &T, count: i64) -> Result> -where - T: ConnectionSource + VFSSource, -{ +pub fn get_recent_albums(db: &DB, count: i64) -> Result> { use self::directories::dsl::*; let vfs = db.get_vfs()?; - let connection = db.get_connection(); + let connection = db.connect()?; let real_directories: Vec = directories .filter(album.is_not_null()) .order(date_added.desc()) .limit(count) - .load(connection.deref())?; + .load(&connection)?; let virtual_directories = real_directories .into_iter() .filter_map(|s| virtualize_directory(&vfs, s)); Ok(virtual_directories.collect::>()) } -pub fn search(db: &T, query: &str) -> Result> -where - T: ConnectionSource + VFSSource, -{ +pub fn search(db: &DB, query: &str) -> Result> { let vfs = db.get_vfs()?; - let connection = db.get_connection(); + let connection = db.connect()?; let like_test = format!("%{}%", query); let mut output = Vec::new(); @@ -673,7 +632,7 @@ where let real_directories: Vec = directories .filter(path.like(&like_test)) .filter(parent.not_like(&like_test)) - .load(connection.deref())?; + .load(&connection)?; let virtual_directories = real_directories .into_iter() @@ -694,7 +653,7 @@ where .or(album_artist.like(&like_test)), ) .filter(parent.not_like(&like_test)) - .load(connection.deref())?; + .load(&connection)?; let virtual_songs = real_songs .into_iter() @@ -706,19 +665,16 @@ where Ok(output) } -pub fn get_song(db: &T, virtual_path: &Path) -> Result -where - T: ConnectionSource + VFSSource, -{ +pub fn get_song(db: &DB, virtual_path: &Path) -> Result { let vfs = db.get_vfs()?; - let connection = db.get_connection(); + let connection = db.connect()?; let real_path = vfs.virtual_to_real(virtual_path)?; let real_path_string = real_path.as_path().to_string_lossy(); use self::songs::dsl::*; let real_song: Song = songs .filter(path.eq(real_path_string)) - .get_result(connection.deref())?; + .get_result(&connection)?; match virtualize_song(&vfs, real_song) { Some(s) => Ok(s), @@ -732,9 +688,9 @@ fn test_populate() { update(&db).unwrap(); update(&db).unwrap(); // Check that subsequent updates don't run into conflicts - let connection = db.get_connection(); - let all_directories: Vec = directories::table.load(connection.deref()).unwrap(); - let all_songs: Vec = songs::table.load(connection.deref()).unwrap(); + let connection = db.connect().unwrap(); + let all_directories: Vec = directories::table.load(&connection).unwrap(); + let all_songs: Vec = songs::table.load(&connection).unwrap(); assert_eq!(all_directories.len(), 5); assert_eq!(all_songs.len(), 12); } @@ -756,10 +712,10 @@ fn test_metadata() { let db = db::get_test_db("metadata.sqlite"); update(&db).unwrap(); - let connection = db.get_connection(); + let connection = db.connect().unwrap(); let songs: Vec = songs::table .filter(songs::title.eq("シャーベット (Sherbet)")) - .load(connection.deref()) + .load(&connection) .unwrap(); assert_eq!(songs.len(), 1); diff --git a/src/lastfm.rs b/src/lastfm.rs index 843e8a6..5045f80 100644 --- a/src/lastfm.rs +++ b/src/lastfm.rs @@ -3,10 +3,9 @@ use rustfm_scrobble::{Scrobble, Scrobbler}; use serde::Deserialize; use std::path::Path; -use crate::db::ConnectionSource; +use crate::db::DB; use crate::index; use crate::user; -use crate::vfs::VFSSource; const LASTFM_API_KEY: &str = "02b96c939a2b451c31dfd67add1f696e"; const LASTFM_API_SECRET: &str = "0f25a80ceef4b470b5cb97d99d4b3420"; @@ -42,10 +41,7 @@ struct AuthResponse { pub session: AuthResponseSession, } -fn scrobble_from_path(db: &T, track: &Path) -> Result -where - T: ConnectionSource + VFSSource, -{ +fn scrobble_from_path(db: &DB, track: &Path) -> Result { let song = index::get_song(db, track)?; Ok(Scrobble::new( song.artist.unwrap_or_else(|| "".into()), @@ -54,27 +50,18 @@ where )) } -pub fn link(db: &T, username: &str, token: &str) -> Result<()> -where - T: ConnectionSource + VFSSource, -{ +pub fn link(db: &DB, username: &str, token: &str) -> Result<()> { let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into()); let auth_response = scrobbler.authenticate_with_token(token.to_string())?; user::lastfm_link(db, username, &auth_response.name, &auth_response.key) } -pub fn unlink(db: &T, username: &str) -> Result<()> -where - T: ConnectionSource + VFSSource, -{ +pub fn unlink(db: &DB, username: &str) -> Result<()> { user::lastfm_unlink(db, username) } -pub fn scrobble(db: &T, username: &str, track: &Path) -> Result<()> -where - T: ConnectionSource + VFSSource, -{ +pub fn scrobble(db: &DB, username: &str, track: &Path) -> Result<()> { let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into()); let scrobble = scrobble_from_path(db, track)?; let auth_token = user::get_lastfm_session_key(db, username)?; @@ -83,10 +70,7 @@ where Ok(()) } -pub fn now_playing(db: &T, username: &str, track: &Path) -> Result<()> -where - T: ConnectionSource + VFSSource, -{ +pub fn now_playing(db: &DB, username: &str, track: &Path) -> Result<()> { let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into()); let scrobble = scrobble_from_path(db, track)?; let auth_token = user::get_lastfm_session_key(db, username)?; diff --git a/src/main.rs b/src/main.rs index 0060d97..3208f81 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,16 +21,11 @@ use std::io::prelude::*; use unix_daemonize::{daemonize_redirect, ChdirMode}; use anyhow::*; -use core::ops::Deref; use getopts::Options; use log::info; use simplelog::{LevelFilter, SimpleLogger, TermLogger, TerminalMode}; use std::path::Path; -use std::sync::Arc; -mod api; -#[cfg(test)] -mod api_tests; mod config; mod db; mod ddns; @@ -38,17 +33,13 @@ mod index; mod lastfm; mod metadata; mod playlist; -mod serve; -mod server; -mod swagger; -#[cfg(test)] -mod test; +mod service; + mod thumbnails; mod ui; mod user; mod utils; mod vfs; -mod web; fn log_config() -> simplelog::Config { simplelog::ConfigBuilder::new() @@ -167,7 +158,7 @@ fn main() -> Result<()> { let db_path = db_path .map(|n| Path::new(n.as_str()).to_path_buf()) .unwrap_or(default_db_path); - let db = Arc::new(db::DB::new(&db_path)?); + let db = db::DB::new(&db_path)?; // Parse config info!("Parsing configuration"); @@ -176,10 +167,10 @@ fn main() -> Result<()> { if let Some(path) = config_file_path { let config = config::parse_toml_file(&path)?; info!("Applying configuration"); - config::amend(db.deref(), &config)?; + config::amend(&db, &config)?; } - let config = config::read(db.deref())?; - let auth_secret = config::get_auth_secret(db.deref())?; + let config = config::read(&db)?; + let auth_secret = config::get_auth_secret(&db)?; // Init index info!("Initializing index"); @@ -189,7 +180,7 @@ fn main() -> Result<()> { let db_auto_index = db.clone(); let command_sender_auto_index = command_sender.clone(); std::thread::spawn(move || { - index::self_trigger(db_auto_index.deref(), &command_sender_auto_index); + index::self_trigger(&db_auto_index, &command_sender_auto_index); }); // API mount target @@ -226,26 +217,25 @@ fn main() -> Result<()> { .unwrap_or_else(|| "5050".to_owned()) .parse() .with_context(|| "Invalid port number")?; - - let server = server::get_server( - port, - Some(auth_secret.as_slice()), - &api_url, - &web_url, - &web_dir_path, - &swagger_url, - &swagger_dir_path, - db.clone(), - command_sender, - )?; + let db_server = db.clone(); std::thread::spawn(move || { - server.launch(); + let _ = service::server::run( + port, + &auth_secret, + api_url, + web_url, + web_dir_path, + swagger_url, + swagger_dir_path, + db_server, + command_sender, + ); }); // Start DDNS updates let db_ddns = db.clone(); std::thread::spawn(move || { - ddns::run(db_ddns.deref()); + ddns::run(&db_ddns); }); // Send readiness notification diff --git a/src/playlist.rs b/src/playlist.rs index 5d66843..4d8bf88 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -1,6 +1,5 @@ use anyhow::*; use core::clone::Clone; -use core::ops::Deref; use diesel; use diesel::prelude::*; use diesel::sql_types; @@ -9,7 +8,7 @@ use std::path::Path; #[cfg(test)] use crate::db; -use crate::db::ConnectionSource; +use crate::db::DB; use crate::db::{playlist_songs, playlists, users}; use crate::index::{self, Song}; use crate::vfs::VFSSource; @@ -48,11 +47,8 @@ pub struct NewPlaylistSong { ordering: i32, } -pub fn list_playlists(owner: &str, db: &T) -> Result> -where - T: ConnectionSource + VFSSource, -{ - let connection = db.get_connection(); +pub fn list_playlists(owner: &str, db: &DB) -> Result> { + let connection = db.connect()?; let user: User; { @@ -60,29 +56,26 @@ where user = users .filter(name.eq(owner)) .select((id,)) - .first(connection.deref())?; + .first(&connection)?; } { use self::playlists::dsl::*; let found_playlists: Vec = Playlist::belonging_to(&user) .select(name) - .load(connection.deref())?; + .load(&connection)?; Ok(found_playlists) } } -pub fn save_playlist(playlist_name: &str, owner: &str, content: &[String], db: &T) -> Result<()> -where - T: ConnectionSource + VFSSource, -{ +pub fn save_playlist(playlist_name: &str, owner: &str, content: &[String], db: &DB) -> Result<()> { let user: User; let new_playlist: NewPlaylist; let playlist: Playlist; let vfs = db.get_vfs()?; { - let connection = db.get_connection(); + let connection = db.connect()?; // Find owner { @@ -90,7 +83,7 @@ where user = users .filter(name.eq(owner)) .select((id,)) - .get_result(connection.deref())?; + .get_result(&connection)?; } // Create playlist @@ -101,14 +94,14 @@ where diesel::insert_into(playlists::table) .values(&new_playlist) - .execute(connection.deref())?; + .execute(&connection)?; { use self::playlists::dsl::*; playlist = playlists .select((id, owner)) .filter(name.eq(playlist_name).and(owner.eq(user.id))) - .get_result(connection.deref())?; + .get_result(&connection)?; } } @@ -131,34 +124,29 @@ where } { - let connection = db.get_connection(); - connection - .deref() - .transaction::<_, diesel::result::Error, _>(|| { - // Delete old content (if any) - let old_songs = PlaylistSong::belonging_to(&playlist); - diesel::delete(old_songs).execute(connection.deref())?; + let connection = db.connect()?; + connection.transaction::<_, diesel::result::Error, _>(|| { + // Delete old content (if any) + let old_songs = PlaylistSong::belonging_to(&playlist); + diesel::delete(old_songs).execute(&connection)?; - // Insert content - diesel::insert_into(playlist_songs::table) - .values(&new_songs) - .execute(connection.deref())?; - Ok(()) - })?; + // Insert content + diesel::insert_into(playlist_songs::table) + .values(&new_songs) + .execute(&*connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822 + Ok(()) + })?; } Ok(()) } -pub fn read_playlist(playlist_name: &str, owner: &str, db: &T) -> Result> -where - T: ConnectionSource + VFSSource, -{ +pub fn read_playlist(playlist_name: &str, owner: &str, db: &DB) -> Result> { let vfs = db.get_vfs()?; let songs: Vec; { - let connection = db.get_connection(); + let connection = db.connect()?; let user: User; let playlist: Playlist; @@ -168,7 +156,7 @@ where user = users .filter(name.eq(owner)) .select((id,)) - .get_result(connection.deref())?; + .get_result(&connection)?; } // Find playlist @@ -177,7 +165,7 @@ where playlist = playlists .select((id, owner)) .filter(name.eq(playlist_name).and(owner.eq(user.id))) - .get_result(connection.deref())?; + .get_result(&connection)?; } // Select songs. Not using Diesel because we need to LEFT JOIN using a custom column @@ -191,7 +179,7 @@ where "#, ); let query = query.clone().bind::(playlist.id); - songs = query.get_results(connection.deref())?; + songs = query.get_results(&connection)?; } // Map real path to virtual paths @@ -203,11 +191,8 @@ where Ok(virtual_songs) } -pub fn delete_playlist(playlist_name: &str, owner: &str, db: &T) -> Result<()> -where - T: ConnectionSource + VFSSource, -{ - let connection = db.get_connection(); +pub fn delete_playlist(playlist_name: &str, owner: &str, db: &DB) -> Result<()> { + let connection = db.connect()?; let user: User; { @@ -215,13 +200,13 @@ where user = users .filter(name.eq(owner)) .select((id,)) - .first(connection.deref())?; + .first(&connection)?; } { use self::playlists::dsl::*; let q = Playlist::belonging_to(&user).filter(name.eq(playlist_name)); - diesel::delete(q).execute(connection.deref())?; + diesel::delete(q).execute(&connection)?; } Ok(()) diff --git a/src/service/constants.rs b/src/service/constants.rs new file mode 100644 index 0000000..a36931c --- /dev/null +++ b/src/service/constants.rs @@ -0,0 +1,5 @@ +pub const API_MAJOR_VERSION: i32 = 4; +pub const API_MINOR_VERSION: i32 = 0; +pub const COOKIE_SESSION: &str = "session"; +pub const COOKIE_USERNAME: &str = "username"; +pub const COOKIE_ADMIN: &str = "admin"; diff --git a/src/service/dto.rs b/src/service/dto.rs new file mode 100644 index 0000000..124e6c7 --- /dev/null +++ b/src/service/dto.rs @@ -0,0 +1,28 @@ +use serde::{Deserialize, Serialize}; + +#[derive(PartialEq, Debug, Serialize, Deserialize)] +pub struct Version { + pub major: i32, + pub minor: i32, +} + +#[derive(PartialEq, Debug, Serialize, Deserialize)] +pub struct InitialSetup { + pub has_any_users: bool, +} + +#[derive(Serialize, Deserialize)] +pub struct AuthCredentials { + pub username: String, + pub password: String, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct ListPlaylistsEntry { + pub name: String, +} + +#[derive(Serialize, Deserialize)] +pub struct SavePlaylistInput { + pub tracks: Vec, +} diff --git a/src/service/error.rs b/src/service/error.rs new file mode 100644 index 0000000..0ff3340 --- /dev/null +++ b/src/service/error.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum APIError { + #[error("Incorrect Credentials")] + IncorrectCredentials, + #[error("Unspecified")] + Unspecified, +} + +impl From for APIError { + fn from(_: anyhow::Error) -> Self { + APIError::Unspecified + } +} diff --git a/src/service/mod.rs b/src/service/mod.rs new file mode 100644 index 0000000..e975016 --- /dev/null +++ b/src/service/mod.rs @@ -0,0 +1,11 @@ +mod constants; +mod dto; +mod error; + +#[cfg(test)] +mod test; + +#[cfg(feature = "service-rocket")] +mod rocket; +#[cfg(feature = "service-rocket")] +pub use self::rocket::*; diff --git a/src/api.rs b/src/service/rocket/api.rs similarity index 68% rename from src/api.rs rename to src/service/rocket/api.rs index 1546150..19d227d 100644 --- a/src/api.rs +++ b/src/service/rocket/api.rs @@ -4,33 +4,28 @@ 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 serde::{Deserialize, Serialize}; use std::fs::File; use std::ops::Deref; use std::path::PathBuf; use std::str; use std::str::FromStr; use std::sync::Arc; -use thiserror::Error; use time::Duration; +use super::serve; use crate::config::{self, Config, Preferences}; use crate::db::DB; use crate::index; use crate::lastfm; use crate::playlist; -use crate::serve; +use crate::service::constants::*; +use crate::service::dto; +use crate::service::error::APIError; use crate::thumbnails; use crate::user; use crate::utils; use crate::vfs::VFSSource; -const CURRENT_MAJOR_VERSION: i32 = 4; -const CURRENT_MINOR_VERSION: i32 = 0; -const COOKIE_SESSION: &str = "session"; -const COOKIE_USERNAME: &str = "username"; -const COOKIE_ADMIN: &str = "admin"; - pub fn get_routes() -> Vec { routes![ version, @@ -61,14 +56,6 @@ pub fn get_routes() -> Vec { ] } -#[derive(Error, Debug)] -enum APIError { - #[error("Incorrect Credentials")] - IncorrectCredentials, - #[error("Unspecified")] - Unspecified, -} - impl<'r> rocket::response::Responder<'r> for APIError { fn respond_to(self, _: &rocket::request::Request<'_>) -> rocket::response::Result<'r> { let status = match self { @@ -79,12 +66,6 @@ impl<'r> rocket::response::Responder<'r> for APIError { } } -impl From for APIError { - fn from(_: anyhow::Error) -> Self { - APIError::Unspecified - } -} - struct Auth { username: String, } @@ -122,7 +103,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Auth { fn from_request(request: &'a Request<'r>) -> request::Outcome { let mut cookies = request.guard::>().unwrap(); - let db = match request.guard::>>() { + let db = match request.guard::>() { Outcome::Success(d) => d, _ => return Outcome::Failure((Status::InternalServerError, ())), }; @@ -169,16 +150,16 @@ impl<'a, 'r> FromRequest<'a, 'r> for AdminRights { type Error = (); fn from_request(request: &'a Request<'r>) -> request::Outcome { - let db = request.guard::>>()?; + let db = request.guard::>()?; - match user::count::(&db) { + match user::count(&db) { Err(_) => return Outcome::Failure((Status::InternalServerError, ())), Ok(0) => return Outcome::Success(AdminRights {}), _ => (), }; let auth = request.guard::()?; - match user::is_admin::(&db, &auth.username) { + match user::is_admin(&db, &auth.username) { Err(_) => Outcome::Failure((Status::InternalServerError, ())), Ok(true) => Outcome::Success(AdminRights {}), Ok(false) => Outcome::Failure((Status::Forbidden, ())), @@ -207,63 +188,44 @@ impl From for PathBuf { } } -#[derive(PartialEq, Debug, Serialize, Deserialize)] -pub struct Version { - pub major: i32, - pub minor: i32, -} - #[get("/version")] -fn version() -> Json { - let current_version = Version { - major: CURRENT_MAJOR_VERSION, - minor: CURRENT_MINOR_VERSION, +fn version() -> Json { + let current_version = dto::Version { + major: API_MAJOR_VERSION, + minor: API_MINOR_VERSION, }; Json(current_version) } -#[derive(PartialEq, Debug, Serialize, Deserialize)] -pub struct InitialSetup { - pub has_any_users: bool, -} - #[get("/initial_setup")] -fn initial_setup(db: State<'_, Arc>) -> Result> { - let initial_setup = InitialSetup { - has_any_users: user::count::(&db)? > 0, +fn initial_setup(db: State<'_, DB>) -> Result> { + let initial_setup = dto::InitialSetup { + has_any_users: user::count(&db)? > 0, }; Ok(Json(initial_setup)) } #[get("/settings")] -fn get_settings(db: State<'_, Arc>, _admin_rights: AdminRights) -> Result> { - let config = config::read::(&db)?; +fn get_settings(db: State<'_, DB>, _admin_rights: AdminRights) -> Result> { + let config = config::read(&db)?; Ok(Json(config)) } #[put("/settings", data = "")] -fn put_settings( - db: State<'_, Arc>, - _admin_rights: AdminRights, - config: Json, -) -> Result<()> { - config::amend::(&db, &config)?; +fn put_settings(db: State<'_, DB>, _admin_rights: AdminRights, config: Json) -> Result<()> { + config::amend(&db, &config)?; Ok(()) } #[get("/preferences")] -fn get_preferences(db: State<'_, Arc>, auth: Auth) -> Result> { - let preferences = config::read_preferences::(&db, &auth.username)?; +fn get_preferences(db: State<'_, DB>, auth: Auth) -> Result> { + let preferences = config::read_preferences(&db, &auth.username)?; Ok(Json(preferences)) } #[put("/preferences", data = "")] -fn put_preferences( - db: State<'_, Arc>, - auth: Auth, - preferences: Json, -) -> Result<()> { - config::write_preferences::(&db, &auth.username, &preferences)?; +fn put_preferences(db: State<'_, DB>, auth: Auth, preferences: Json) -> Result<()> { + config::write_preferences(&db, &auth.username, &preferences)?; Ok(()) } @@ -276,40 +238,29 @@ fn trigger_index( Ok(()) } -#[derive(Serialize, Deserialize)] -pub struct AuthCredentials { - pub username: String, - pub password: String, -} - -#[derive(Serialize)] -struct AuthOutput { - admin: bool, -} - #[post("/auth", data = "")] fn auth( - db: State<'_, Arc>, - credentials: Json, + db: State<'_, DB>, + credentials: Json, mut cookies: Cookies<'_>, ) -> std::result::Result<(), APIError> { - if !user::auth::(&db, &credentials.username, &credentials.password)? { + if !user::auth(&db, &credentials.username, &credentials.password)? { return Err(APIError::IncorrectCredentials); } - let is_admin = user::is_admin::(&db, &credentials.username)?; + let is_admin = user::is_admin(&db, &credentials.username)?; add_session_cookies(&mut cookies, &credentials.username, is_admin); Ok(()) } #[get("/browse")] -fn browse_root(db: State<'_, Arc>, _auth: Auth) -> Result>> { +fn browse_root(db: State<'_, DB>, _auth: Auth) -> Result>> { let result = index::browse(db.deref().deref(), &PathBuf::new())?; Ok(Json(result)) } #[get("/browse/")] fn browse( - db: State<'_, Arc>, + db: State<'_, DB>, _auth: Auth, path: VFSPathBuf, ) -> Result>> { @@ -318,42 +269,38 @@ fn browse( } #[get("/flatten")] -fn flatten_root(db: State<'_, Arc>, _auth: Auth) -> Result>> { +fn flatten_root(db: State<'_, DB>, _auth: Auth) -> Result>> { let result = index::flatten(db.deref().deref(), &PathBuf::new())?; Ok(Json(result)) } #[get("/flatten/")] -fn flatten( - db: State<'_, Arc>, - _auth: Auth, - path: VFSPathBuf, -) -> Result>> { +fn flatten(db: State<'_, DB>, _auth: Auth, path: VFSPathBuf) -> Result>> { let result = index::flatten(db.deref().deref(), &path.into() as &PathBuf)?; Ok(Json(result)) } #[get("/random")] -fn random(db: State<'_, Arc>, _auth: Auth) -> Result>> { +fn random(db: State<'_, DB>, _auth: Auth) -> Result>> { let result = index::get_random_albums(db.deref().deref(), 20)?; Ok(Json(result)) } #[get("/recent")] -fn recent(db: State<'_, Arc>, _auth: Auth) -> Result>> { +fn recent(db: State<'_, DB>, _auth: Auth) -> Result>> { let result = index::get_recent_albums(db.deref().deref(), 20)?; Ok(Json(result)) } #[get("/search")] -fn search_root(db: State<'_, Arc>, _auth: Auth) -> Result>> { +fn search_root(db: State<'_, DB>, _auth: Auth) -> Result>> { let result = index::search(db.deref().deref(), "")?; Ok(Json(result)) } #[get("/search/")] fn search( - db: State<'_, Arc>, + db: State<'_, DB>, _auth: Auth, query: String, ) -> Result>> { @@ -362,12 +309,7 @@ fn search( } #[get("/serve/")] -fn serve( - db: State<'_, Arc>, - _auth: Auth, - path: VFSPathBuf, -) -> Result> { - let db: &DB = db.deref().deref(); +fn serve(db: State<'_, DB>, _auth: Auth, path: VFSPathBuf) -> Result> { let vfs = db.get_vfs()?; let real_path = vfs.virtual_to_real(&path.into() as &PathBuf)?; @@ -381,56 +323,42 @@ fn serve( Ok(serve::RangeResponder::new(file)) } -#[derive(Debug, PartialEq, Serialize, Deserialize)] -pub struct ListPlaylistsEntry { - pub name: String, -} - #[get("/playlists")] -fn list_playlists(db: State<'_, Arc>, auth: Auth) -> Result>> { +fn list_playlists(db: State<'_, DB>, auth: Auth) -> Result>> { let playlist_names = playlist::list_playlists(&auth.username, db.deref().deref())?; - let playlists: Vec = playlist_names + let playlists: Vec = playlist_names .into_iter() - .map(|p| ListPlaylistsEntry { name: p }) + .map(|p| dto::ListPlaylistsEntry { name: p }) .collect(); Ok(Json(playlists)) } -#[derive(Serialize, Deserialize)] -pub struct SavePlaylistInput { - pub tracks: Vec, -} - #[put("/playlist/", data = "")] fn save_playlist( - db: State<'_, Arc>, + db: State<'_, DB>, auth: Auth, name: String, - playlist: Json, + playlist: Json, ) -> Result<()> { playlist::save_playlist(&name, &auth.username, &playlist.tracks, db.deref().deref())?; Ok(()) } #[get("/playlist/")] -fn read_playlist( - db: State<'_, Arc>, - auth: Auth, - name: String, -) -> Result>> { +fn read_playlist(db: State<'_, DB>, auth: Auth, name: String) -> Result>> { let songs = playlist::read_playlist(&name, &auth.username, db.deref().deref())?; Ok(Json(songs)) } #[delete("/playlist/")] -fn delete_playlist(db: State<'_, Arc>, auth: Auth, name: String) -> Result<()> { +fn delete_playlist(db: State<'_, DB>, auth: Auth, name: String) -> Result<()> { playlist::delete_playlist(&name, &auth.username, db.deref().deref())?; Ok(()) } #[put("/lastfm/now_playing/")] -fn lastfm_now_playing(db: State<'_, Arc>, auth: Auth, path: VFSPathBuf) -> Result<()> { +fn lastfm_now_playing(db: State<'_, DB>, auth: Auth, path: VFSPathBuf) -> Result<()> { if user::is_lastfm_linked(db.deref().deref(), &auth.username) { lastfm::now_playing(db.deref().deref(), &auth.username, &path.into() as &PathBuf)?; } @@ -438,7 +366,7 @@ fn lastfm_now_playing(db: State<'_, Arc>, auth: Auth, path: VFSPathBuf) -> R } #[post("/lastfm/scrobble/")] -fn lastfm_scrobble(db: State<'_, Arc>, auth: Auth, path: VFSPathBuf) -> Result<()> { +fn lastfm_scrobble(db: State<'_, DB>, auth: Auth, path: VFSPathBuf) -> Result<()> { if user::is_lastfm_linked(db.deref().deref(), &auth.username) { lastfm::scrobble(db.deref().deref(), &auth.username, &path.into() as &PathBuf)?; } @@ -447,7 +375,7 @@ fn lastfm_scrobble(db: State<'_, Arc>, auth: Auth, path: VFSPathBuf) -> Resu #[get("/lastfm/link?&")] fn lastfm_link( - db: State<'_, Arc>, + db: State<'_, DB>, auth: Auth, token: String, content: String, @@ -467,7 +395,7 @@ fn lastfm_link( } #[delete("/lastfm/link")] -fn lastfm_unlink(db: State<'_, Arc>, auth: Auth) -> Result<()> { +fn lastfm_unlink(db: State<'_, DB>, auth: Auth) -> Result<()> { lastfm::unlink(db.deref().deref(), &auth.username)?; Ok(()) } diff --git a/src/service/rocket/mod.rs b/src/service/rocket/mod.rs new file mode 100644 index 0000000..10fbc8d --- /dev/null +++ b/src/service/rocket/mod.rs @@ -0,0 +1,7 @@ +mod api; +mod serve; + +pub mod server; + +#[cfg(test)] +pub mod test; diff --git a/src/serve.rs b/src/service/rocket/serve.rs similarity index 100% rename from src/serve.rs rename to src/service/rocket/serve.rs diff --git a/src/server.rs b/src/service/rocket/server.rs similarity index 61% rename from src/server.rs rename to src/service/rocket/server.rs index 2898ae9..e02f23f 100644 --- a/src/server.rs +++ b/src/service/rocket/server.rs @@ -5,18 +5,19 @@ use rocket_contrib::serve::StaticFiles; use std::path::PathBuf; use std::sync::Arc; +use super::api; use crate::db::DB; use crate::index::CommandSender; pub fn get_server( port: u16, - auth_secret: Option<&[u8]>, + auth_secret: &[u8], api_url: &str, web_url: &str, web_dir_path: &PathBuf, swagger_url: &str, swagger_dir_path: &PathBuf, - db: Arc, + db: DB, command_sender: Arc, ) -> Result { let mut config = rocket::Config::build(Environment::Production) @@ -25,10 +26,8 @@ pub fn get_server( .keep_alive(0) .finalize()?; - if let Some(secret) = auth_secret { - let encoded = base64::encode(secret); - config.set_secret_key(encoded)?; - } + let encoded = base64::encode(auth_secret); + config.set_secret_key(encoded)?; let swagger_routes_rank = 0; let web_routes_rank = swagger_routes_rank + 1; @@ -36,7 +35,7 @@ pub fn get_server( Ok(rocket::custom(config) .manage(db) .manage(command_sender) - .mount(&api_url, crate::api::get_routes()) + .mount(&api_url, api::get_routes()) .mount( &swagger_url, StaticFiles::from(swagger_dir_path).rank(swagger_routes_rank), @@ -46,3 +45,29 @@ pub fn get_server( StaticFiles::from(web_dir_path).rank(web_routes_rank), )) } + +pub fn run( + port: u16, + auth_secret: &[u8], + api_url: String, + web_url: String, + web_dir_path: PathBuf, + swagger_url: String, + swagger_dir_path: PathBuf, + db: DB, + command_sender: Arc, +) -> Result<()> { + let server = get_server( + port, + auth_secret, + &api_url, + &web_url, + &web_dir_path, + &swagger_url, + &swagger_dir_path, + db, + command_sender, + )?; + server.launch(); + Ok(()) +} diff --git a/src/service/rocket/test.rs b/src/service/rocket/test.rs new file mode 100644 index 0000000..18ff43a --- /dev/null +++ b/src/service/rocket/test.rs @@ -0,0 +1,164 @@ +use http::response::{Builder, Response}; +use http::{HeaderMap, HeaderValue}; +use rocket; +use rocket::local::Client; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::fs; +use std::ops::{Deref, DerefMut}; +use std::path::PathBuf; +use std::sync::Arc; + +use super::server; +use crate::db::DB; +use crate::index; +use crate::service::test::TestService; + +pub struct RocketResponse<'r, 's> { + response: &'s mut rocket::Response<'r>, +} + +impl<'r, 's> RocketResponse<'r, 's> { + fn builder(&self) -> Builder { + let mut builder = Response::builder().status(self.response.status().code); + for header in self.response.headers().iter() { + builder = builder.header(header.name(), header.value()); + } + builder + } + + fn to_void(&self) -> Response<()> { + let builder = self.builder(); + builder.body(()).unwrap() + } + + fn to_bytes(&mut self) -> Response> { + let body = self.response.body().unwrap(); + let body = body.into_bytes().unwrap(); + let builder = self.builder(); + builder.body(body).unwrap() + } + + fn to_object(&mut self) -> Response { + let body = self.response.body_string().unwrap(); + let body = serde_json::from_str(&body).unwrap(); + let builder = self.builder(); + builder.body(body).unwrap() + } +} + +pub struct RocketTestService { + client: Client, + command_sender: Arc, +} + +pub type ServiceType = RocketTestService; + +impl TestService for RocketTestService { + fn new(db_name: &str) -> Self { + let mut db_path = PathBuf::new(); + db_path.push("test"); + db_path.push(format!("{}.sqlite", db_name)); + if db_path.exists() { + fs::remove_file(&db_path).unwrap(); + } + let db = DB::new(&db_path).unwrap(); + + let web_dir_path = PathBuf::from("web"); + let mut swagger_dir_path = PathBuf::from("docs"); + swagger_dir_path.push("swagger"); + let command_sender = index::init(db.clone()); + + let auth_secret: [u8; 32] = [0; 32]; + + let server = server::get_server( + 5050, + &auth_secret, + "/api", + "/", + &web_dir_path, + "/swagger", + &swagger_dir_path, + db.clone(), + command_sender.clone(), + ) + .unwrap(); + let client = Client::new(server).unwrap(); + RocketTestService { + client, + command_sender, + } + } + + fn get(&mut self, url: &str) -> Response<()> { + let mut response = self.client.get(url).dispatch(); + RocketResponse { + response: response.deref_mut(), + } + .to_void() + } + + fn get_bytes(&mut self, url: &str, headers: &HeaderMap) -> Response> { + let mut request = self.client.get(url); + for (name, value) in headers.iter() { + request.add_header(rocket::http::Header::new( + name.as_str().to_owned(), + value.to_str().unwrap().to_owned(), + )) + } + let mut response = request.dispatch(); + RocketResponse { + response: response.deref_mut(), + } + .to_bytes() + } + + fn post(&mut self, url: &str) -> Response<()> { + let mut response = self.client.post(url).dispatch(); + RocketResponse { + response: response.deref_mut(), + } + .to_void() + } + + fn delete(&mut self, url: &str) -> Response<()> { + let mut response = self.client.delete(url).dispatch(); + RocketResponse { + response: response.deref_mut(), + } + .to_void() + } + + fn get_json(&mut self, url: &str) -> Response { + let mut response = self.client.get(url).dispatch(); + RocketResponse { + response: response.deref_mut(), + } + .to_object() + } + + fn put_json(&mut self, url: &str, payload: &T) -> Response<()> { + let client = &self.client; + let body = serde_json::to_string(payload).unwrap(); + let mut response = client.put(url).body(&body).dispatch(); + RocketResponse { + response: response.deref_mut(), + } + .to_void() + } + + fn post_json(&mut self, url: &str, payload: &T) -> Response<()> { + let body = serde_json::to_string(payload).unwrap(); + let mut response = self.client.post(url).body(&body).dispatch(); + RocketResponse { + response: response.deref_mut(), + } + .to_void() + } +} + +impl Drop for RocketTestService { + fn drop(&mut self) { + self.command_sender.deref().exit().unwrap(); + } +} diff --git a/src/service/test.rs b/src/service/test.rs new file mode 100644 index 0000000..29a6ed9 --- /dev/null +++ b/src/service/test.rs @@ -0,0 +1,440 @@ +use cookie::Cookie; +use function_name::named; +use http::header::*; +use http::{HeaderMap, HeaderValue, Response, StatusCode}; +use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::path::PathBuf; +use std::time::Duration; + +use crate::service::constants::*; +use crate::service::dto; +use crate::{config, ddns, index, vfs}; + +#[cfg(feature = "service-rocket")] +pub use crate::service::rocket::test::ServiceType; + +const TEST_USERNAME: &str = "test_user"; +const TEST_PASSWORD: &str = "test_password"; +const TEST_MOUNT_NAME: &str = "collection"; +const TEST_MOUNT_SOURCE: &str = "test/collection"; + +pub trait TestService { + fn new(db_name: &str) -> Self; + fn get(&mut self, url: &str) -> Response<()>; + fn get_bytes(&mut self, url: &str, headers: &HeaderMap) -> Response>; + fn post(&mut self, url: &str) -> Response<()>; + fn delete(&mut self, url: &str) -> Response<()>; + fn get_json(&mut self, url: &str) -> Response; + fn put_json(&mut self, url: &str, payload: &T) -> Response<()>; + fn post_json(&mut self, url: &str, payload: &T) -> Response<()>; + + fn complete_initial_setup(&mut self) { + let configuration = config::Config { + album_art_pattern: None, + prefix_url: None, + reindex_every_n_seconds: None, + ydns: None, + users: Some(vec![config::ConfigUser { + name: TEST_USERNAME.into(), + password: TEST_PASSWORD.into(), + admin: true, + }]), + mount_dirs: Some(vec![vfs::MountPoint { + name: TEST_MOUNT_NAME.into(), + source: TEST_MOUNT_SOURCE.into(), + }]), + }; + self.put_json("/api/settings", &configuration); + } + + fn login(&mut self) { + let credentials = dto::AuthCredentials { + username: TEST_USERNAME.into(), + password: TEST_PASSWORD.into(), + }; + self.post_json("/api/auth", &credentials); + } + + fn index(&mut self) { + assert!(self.post("/api/trigger_index").status() == StatusCode::OK); + for _ in 1..20 { + let response = self.get_json::>("/api/browse"); + let entries = response.body(); + if entries.len() > 0 { + return; + } + std::thread::sleep(Duration::from_secs(1)); + } + panic!("index timeout"); + } +} + +#[named] +#[test] +fn test_service_index() { + let mut service = ServiceType::new(function_name!()); + service.get("/"); +} + +#[named] +#[test] +fn test_service_swagger_index() { + let mut service = ServiceType::new(function_name!()); + assert!(service.get("/swagger").status() == StatusCode::OK); +} + +#[named] +#[test] +fn test_service_swagger_index_with_trailing_slash() { + let mut service = ServiceType::new(function_name!()); + assert!(service.get("/swagger/").status() == StatusCode::OK); +} + +#[named] +#[test] +fn test_service_version() { + let mut service = ServiceType::new(function_name!()); + let response = service.get_json::("/api/version"); + let version = response.body(); + assert_eq!(version, &dto::Version { major: 4, minor: 0 }); +} + +#[named] +#[test] +fn test_service_initial_setup() { + let mut service = ServiceType::new(function_name!()); + { + let response = service.get_json::("/api/initial_setup"); + let initial_setup = response.body(); + assert_eq!( + initial_setup, + &dto::InitialSetup { + has_any_users: false + } + ); + } + service.complete_initial_setup(); + { + let response = service.get_json::("/api/initial_setup"); + let initial_setup = response.body(); + assert_eq!( + initial_setup, + &dto::InitialSetup { + has_any_users: true + } + ); + } +} + +#[named] +#[test] +fn test_service_settings() { + let mut service = ServiceType::new(function_name!()); + service.complete_initial_setup(); + + assert!(service.get("/api/settings").status() == StatusCode::UNAUTHORIZED); + service.login(); + + { + let response = service.get_json::("/api/settings"); + let configuration = response.body(); + assert_eq!( + configuration, + &config::Config { + album_art_pattern: Some("Folder.(jpg|png)".to_string()), + reindex_every_n_seconds: Some(1800), + mount_dirs: Some(vec![vfs::MountPoint { + name: TEST_MOUNT_NAME.into(), + source: TEST_MOUNT_SOURCE.into() + }]), + prefix_url: None, + users: Some(vec![config::ConfigUser { + name: TEST_USERNAME.into(), + password: "".into(), + admin: true + }]), + ydns: Some(ddns::DDNSConfig { + host: "".into(), + username: "".into(), + password: "".into() + }), + } + ); + } + + let mut configuration = config::Config { + album_art_pattern: Some("my_pattern".to_owned()), + reindex_every_n_seconds: Some(3600), + mount_dirs: Some(vec![ + vfs::MountPoint { + name: TEST_MOUNT_NAME.into(), + source: TEST_MOUNT_SOURCE.into(), + }, + vfs::MountPoint { + name: "more_music".into(), + source: "test/collection".into(), + }, + ]), + prefix_url: Some("my_prefix".to_owned()), + users: Some(vec![ + config::ConfigUser { + name: "test_user".into(), + password: "some_password".into(), + admin: true, + }, + config::ConfigUser { + name: "other_user".into(), + password: "some_other_password".into(), + admin: false, + }, + ]), + ydns: Some(ddns::DDNSConfig { + host: "my_host".into(), + username: "my_username".into(), + password: "my_password".into(), + }), + }; + + service.put_json("/api/settings", &configuration); + + configuration.users = Some(vec![ + config::ConfigUser { + name: "test_user".into(), + password: "".into(), + admin: true, + }, + config::ConfigUser { + name: "other_user".into(), + password: "".into(), + admin: false, + }, + ]); + + let response = service.get_json::("/api/settings"); + let received = response.body(); + assert_eq!(received, &configuration); +} + +#[named] +#[test] +fn test_service_preferences() { + // TODO +} + +#[named] +#[test] +fn test_service_trigger_index() { + let mut service = ServiceType::new(function_name!()); + service.complete_initial_setup(); + service.login(); + + let response = service.get_json::>("/api/random"); + let entries = response.body(); + assert_eq!(entries.len(), 0); + + service.index(); + + let response = service.get_json::>("/api/random"); + let entries = response.body(); + assert_eq!(entries.len(), 2); +} + +#[named] +#[test] +fn test_service_auth() { + let mut service = ServiceType::new(function_name!()); + service.complete_initial_setup(); + + { + let credentials = dto::AuthCredentials { + username: "garbage".into(), + password: "garbage".into(), + }; + assert!(service.post_json("/api/auth", &credentials).status() == StatusCode::UNAUTHORIZED); + } + { + let credentials = dto::AuthCredentials { + username: TEST_USERNAME.into(), + password: "garbage".into(), + }; + assert!(service.post_json("/api/auth", &credentials).status() == StatusCode::UNAUTHORIZED); + } + { + let credentials = dto::AuthCredentials { + username: TEST_USERNAME.into(), + password: TEST_PASSWORD.into(), + }; + let response = service.post_json("/api/auth", &credentials); + assert!(response.status() == StatusCode::OK); + let cookies: Vec = response + .headers() + .get_all(SET_COOKIE) + .iter() + .map(|c| Cookie::parse(c.to_str().unwrap()).unwrap()) + .collect(); + assert!(cookies.iter().any(|c| c.name() == COOKIE_SESSION)); + assert!(cookies.iter().any(|c| c.name() == COOKIE_USERNAME)); + assert!(cookies.iter().any(|c| c.name() == COOKIE_ADMIN)); + } +} + +#[named] +#[test] +fn test_service_browse() { + let mut service = ServiceType::new(function_name!()); + service.complete_initial_setup(); + service.login(); + service.index(); + + let response = service.get_json::>("/api/browse"); + let entries = response.body(); + assert_eq!(entries.len(), 1); + + let mut path = PathBuf::new(); + path.push("collection"); + path.push("Khemmis"); + path.push("Hunted"); + let uri = format!( + "/api/browse/{}", + percent_encode(path.to_string_lossy().as_ref().as_bytes(), NON_ALPHANUMERIC) + ); + + let response = service.get_json::>(&uri); + let entries = response.body(); + assert_eq!(entries.len(), 5); +} + +#[named] +#[test] +fn test_service_flatten() { + let mut service = ServiceType::new(function_name!()); + service.complete_initial_setup(); + service.login(); + service.index(); + + let response = service.get_json::>("/api/flatten"); + let entries = response.body(); + assert_eq!(entries.len(), 12); + + let response = service.get_json::>("/api/flatten/collection"); + let entries = response.body(); + assert_eq!(entries.len(), 12); +} + +#[named] +#[test] +fn test_service_random() { + let mut service = ServiceType::new(function_name!()); + service.complete_initial_setup(); + service.login(); + service.index(); + + let response = service.get_json::>("/api/random"); + let entries = response.body(); + assert_eq!(entries.len(), 2); +} + +#[named] +#[test] +fn test_service_recent() { + let mut service = ServiceType::new(function_name!()); + service.complete_initial_setup(); + service.login(); + service.index(); + + let response = service.get_json::>("/api/recent"); + let entries = response.body(); + assert_eq!(entries.len(), 2); +} + +#[named] +#[test] +fn test_service_search() { + let mut service = ServiceType::new(function_name!()); + service.complete_initial_setup(); + service.login(); + service.index(); + + let response = service.get_json::>("/api/search/door"); + let results = response.body(); + assert_eq!(results.len(), 1); + match results[0] { + index::CollectionFile::Song(ref s) => assert_eq!(s.title, Some("Beyond The Door".into())), + _ => panic!(), + } +} + +#[named] +#[test] +fn test_service_serve() { + let mut service = ServiceType::new(function_name!()); + service.complete_initial_setup(); + service.login(); + service.index(); + + let mut path = PathBuf::new(); + path.push("collection"); + path.push("Khemmis"); + path.push("Hunted"); + path.push("02 - Candlelight.mp3"); + let uri = format!( + "/api/serve/{}", + percent_encode(path.to_string_lossy().as_ref().as_bytes(), NON_ALPHANUMERIC) + ); + + let response = service.get_bytes(&uri, &HeaderMap::new()); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.body().len(), 24_142); + + { + let mut headers = HeaderMap::new(); + headers.append(RANGE, HeaderValue::from_str("bytes=100-299").unwrap()); + let response = service.get_bytes(&uri, &headers); + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + assert_eq!(response.body().len(), 200); + assert_eq!(response.headers().get(CONTENT_LENGTH).unwrap(), "200"); + } +} + +#[named] +#[test] +fn test_service_playlists() { + let mut service = ServiceType::new(function_name!()); + service.complete_initial_setup(); + service.login(); + service.index(); + + let response = service.get_json::>("/api/playlists"); + let playlists = response.body(); + assert_eq!(playlists.len(), 0); + + let response = service.get_json::>("/api/flatten"); + let mut my_songs = response.into_body(); + my_songs.pop(); + my_songs.pop(); + let my_playlist = dto::SavePlaylistInput { + tracks: my_songs.iter().map(|s| s.path.clone()).collect(), + }; + service.put_json("/api/playlist/my_playlist", &my_playlist); + + let response = service.get_json::>("/api/playlists"); + let playlists = response.body(); + assert_eq!( + playlists, + &vec![dto::ListPlaylistsEntry { + name: "my_playlist".into() + }] + ); + + let response = service.get_json::>("/api/playlist/my_playlist"); + let songs = response.body(); + assert_eq!(songs, &my_songs); + + service.delete("/api/playlist/my_playlist"); + + let response = service.get_json::>("/api/playlists"); + let playlists = response.body(); + assert_eq!(playlists.len(), 0); +} diff --git a/src/swagger.rs b/src/swagger.rs deleted file mode 100644 index 8183c72..0000000 --- a/src/swagger.rs +++ /dev/null @@ -1,19 +0,0 @@ -#[test] -fn test_index() { - use crate::test::get_test_environment; - use rocket::http::Status; - let env = get_test_environment("swagger_index.sqlite"); - let client = &env.client; - let response = client.get("/swagger").dispatch(); - assert_eq!(response.status(), Status::Ok); -} - -#[test] -fn test_index_with_trailing_slash() { - use crate::test::get_test_environment; - use rocket::http::Status; - let env = get_test_environment("swagger_index_with_trailing_slash.sqlite"); - let client = &env.client; - let response = client.get("/swagger/").dispatch(); - assert_eq!(response.status(), Status::Ok); -} diff --git a/src/test.rs b/src/test.rs deleted file mode 100644 index 72487ae..0000000 --- a/src/test.rs +++ /dev/null @@ -1,63 +0,0 @@ -use rocket; -use rocket::local::Client; -use std::fs; -use std::ops::Deref; -use std::path::PathBuf; -use std::sync::Arc; - -use crate::db::DB; -use crate::index; -use crate::server; - -pub struct TestEnvironment { - pub client: Client, - command_sender: Arc, - db: Arc, -} - -impl TestEnvironment { - pub fn update_index(&self) { - index::update(self.db.deref()).unwrap(); - } -} - -impl Drop for TestEnvironment { - fn drop(&mut self) { - self.command_sender.deref().exit().unwrap(); - } -} - -pub fn get_test_environment(db_name: &str) -> TestEnvironment { - let mut db_path = PathBuf::new(); - db_path.push("test"); - db_path.push(db_name); - if db_path.exists() { - fs::remove_file(&db_path).unwrap(); - } - - let db = Arc::new(DB::new(&db_path).unwrap()); - - let web_dir_path = PathBuf::from("web"); - let mut swagger_dir_path = PathBuf::from("docs"); - swagger_dir_path.push("swagger"); - let command_sender = index::init(db.clone()); - - let server = server::get_server( - 5050, - None, - "/api", - "/", - &web_dir_path, - "/swagger", - &swagger_dir_path, - db.clone(), - command_sender.clone(), - ) - .unwrap(); - let client = Client::new(server).unwrap(); - TestEnvironment { - client, - command_sender, - db, - } -} diff --git a/src/user.rs b/src/user.rs index a4055f7..e0b4dbe 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,10 +1,9 @@ use anyhow::*; -use core::ops::Deref; use diesel; use diesel::prelude::*; use crate::db::users; -use crate::db::ConnectionSource; +use crate::db::DB; #[derive(Debug, Insertable, Queryable)] #[table_name = "users"] @@ -38,16 +37,13 @@ fn verify_password(password_hash: &str, attempted_password: &str) -> bool { pbkdf2::pbkdf2_check(attempted_password, password_hash).is_ok() } -pub fn auth(db: &T, username: &str, password: &str) -> Result -where - T: ConnectionSource, -{ +pub fn auth(db: &DB, username: &str, password: &str) -> Result { use crate::db::users::dsl::*; - let connection = db.get_connection(); + let connection = db.connect()?; match users .select(password_hash) .filter(name.eq(username)) - .get_result(connection.deref()) + .get_result(&connection) { Err(diesel::result::Error::NotFound) => Ok(false), Ok(hash) => { @@ -58,88 +54,67 @@ where } } -pub fn count(db: &T) -> Result -where - T: ConnectionSource, -{ +pub fn count(db: &DB) -> Result { use crate::db::users::dsl::*; - let connection = db.get_connection(); - let count = users.count().get_result(connection.deref())?; + let connection = db.connect()?; + let count = users.count().get_result(&connection)?; Ok(count) } -pub fn exists(db: &T, username: &str) -> Result -where - T: ConnectionSource, -{ +pub fn exists(db: &DB, username: &str) -> Result { use crate::db::users::dsl::*; - let connection = db.get_connection(); + let connection = db.connect()?; let results: Vec = users .select(name) .filter(name.eq(username)) - .get_results(connection.deref())?; + .get_results(&connection)?; Ok(results.len() > 0) } -pub fn is_admin(db: &T, username: &str) -> Result -where - T: ConnectionSource, -{ +pub fn is_admin(db: &DB, username: &str) -> Result { use crate::db::users::dsl::*; - let connection = db.get_connection(); + let connection = db.connect()?; let is_admin: i32 = users .filter(name.eq(username)) .select(admin) - .get_result(connection.deref())?; + .get_result(&connection)?; Ok(is_admin != 0) } -pub fn lastfm_link(db: &T, username: &str, lastfm_login: &str, session_key: &str) -> Result<()> -where - T: ConnectionSource, -{ +pub fn lastfm_link(db: &DB, username: &str, lastfm_login: &str, session_key: &str) -> Result<()> { use crate::db::users::dsl::*; - let connection = db.get_connection(); + let connection = db.connect()?; diesel::update(users.filter(name.eq(username))) .set(( lastfm_username.eq(lastfm_login), lastfm_session_key.eq(session_key), )) - .execute(connection.deref())?; + .execute(&connection)?; Ok(()) } -pub fn get_lastfm_session_key(db: &T, username: &str) -> Result -where - T: ConnectionSource, -{ +pub fn get_lastfm_session_key(db: &DB, username: &str) -> Result { use crate::db::users::dsl::*; - let connection = db.get_connection(); + let connection = db.connect()?; let token = users .filter(name.eq(username)) .select(lastfm_session_key) - .get_result(connection.deref())?; + .get_result(&connection)?; match token { Some(t) => Ok(t), _ => Err(anyhow!("Missing LastFM credentials")), } } -pub fn is_lastfm_linked(db: &T, username: &str) -> bool -where - T: ConnectionSource, -{ +pub fn is_lastfm_linked(db: &DB, username: &str) -> bool { get_lastfm_session_key(db, username).is_ok() } -pub fn lastfm_unlink(db: &T, username: &str) -> Result<()> -where - T: ConnectionSource, -{ +pub fn lastfm_unlink(db: &DB, username: &str) -> Result<()> { use crate::db::users::dsl::*; - let connection = db.get_connection(); + let connection = db.connect()?; diesel::update(users.filter(name.eq(username))) .set((lastfm_session_key.eq(""), lastfm_username.eq(""))) - .execute(connection.deref())?; + .execute(&connection)?; Ok(()) } diff --git a/src/vfs.rs b/src/vfs.rs index 44f8584..b623c8b 100644 --- a/src/vfs.rs +++ b/src/vfs.rs @@ -1,5 +1,4 @@ use anyhow::*; -use core::ops::Deref; use diesel::prelude::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -7,7 +6,7 @@ use std::path::Path; use std::path::PathBuf; use crate::db::mount_points; -use crate::db::{ConnectionSource, DB}; +use crate::db::DB; pub trait VFSSource { fn get_vfs(&self) -> Result; @@ -17,10 +16,10 @@ impl VFSSource for DB { fn get_vfs(&self) -> Result { use self::mount_points::dsl::*; let mut vfs = VFS::new(); - let connection = self.get_connection(); + let connection = self.connect()?; let points: Vec = mount_points .select((source, name)) - .get_results(connection.deref())?; + .get_results(&connection)?; for point in points { vfs.mount(&Path::new(&point.source), &point.name)?; } diff --git a/src/web.rs b/src/web.rs deleted file mode 100644 index ffe7a3f..0000000 --- a/src/web.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[test] -fn test_index() { - use crate::test::get_test_environment; - use rocket::http::Status; - - let env = get_test_environment("web_index.sqlite"); - let client = &env.client; - let response = client.get("/").dispatch(); - assert_eq!(response.status(), Status::Ok); -}