mirror of
https://github.com/agersant/polaris
synced 2024-11-10 10:14:12 +00:00
Merge pull request #56 from agersant/split-backends
Decouple rocket usage from the rest of the code, database improvements
This commit is contained in:
commit
f12d0809d4
26 changed files with 1099 additions and 1136 deletions
8
.github/workflows/build.yaml
vendored
8
.github/workflows/build.yaml
vendored
|
@ -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 }}
|
||||
|
|
96
Cargo.lock
generated
96
Cargo.lock
generated
|
@ -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"
|
||||
|
|
14
Cargo.toml
14
Cargo.toml
|
@ -5,19 +5,24 @@ authors = ["Antoine Gersant <antoine.gersant@lesforges.org>"]
|
|||
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"
|
526
src/api_tests.rs
526
src/api_tests.rs
|
@ -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<index::Directory> = 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<index::Directory> = 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<index::CollectionFile> =
|
||||
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<index::CollectionFile> =
|
||||
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<index::CollectionFile> =
|
||||
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<index::CollectionFile> =
|
||||
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<index::Song> = 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<index::Song> = 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<index::Directory> = 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<index::Directory> = 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<index::CollectionFile> = 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<api::ListPlaylistsEntry> =
|
||||
serde_json::from_str(&response_body).unwrap();
|
||||
assert_eq!(response_json.len(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
let songs: Vec<index::Song>;
|
||||
{
|
||||
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<api::ListPlaylistsEntry> =
|
||||
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<index::Song> = 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<api::ListPlaylistsEntry> =
|
||||
serde_json::from_str(&response_body).unwrap();
|
||||
assert_eq!(response_json.len(), 0);
|
||||
}
|
||||
}
|
117
src/config.rs
117
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<Config> {
|
|||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn read<T>(db: &T) -> Result<Config>
|
||||
where
|
||||
T: ConnectionSource,
|
||||
{
|
||||
pub fn read(db: &DB) -> Result<Config> {
|
||||
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<T>(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<T>(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<String> = users::table
|
||||
.select(users::name)
|
||||
.get_results(connection.deref())?;
|
||||
let old_usernames: Vec<String> =
|
||||
users::table.select(users::name).get_results(&connection)?;
|
||||
|
||||
// Delete users that are not in new list
|
||||
let delete_usernames: Vec<String> = 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<T>(db: &T, username: &str) -> Result<Preferences>
|
||||
where
|
||||
T: ConnectionSource,
|
||||
{
|
||||
pub fn read_preferences(db: &DB, username: &str) -> Result<Preferences> {
|
||||
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<T>(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<T>(db: &T) -> Result<Vec<u8>>
|
||||
where
|
||||
T: ConnectionSource,
|
||||
{
|
||||
pub fn get_auth_secret(db: &DB) -> Result<Vec<u8>> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Mutex<SqliteConnection>>;
|
||||
#[derive(Clone)]
|
||||
pub struct DB {
|
||||
pool: r2d2::Pool<ConnectionManager<SqliteConnection>>,
|
||||
}
|
||||
|
||||
pub struct DB {
|
||||
connection: Arc<Mutex<SqliteConnection>>,
|
||||
#[derive(Debug)]
|
||||
struct ConnectionCustomizer {}
|
||||
impl diesel::r2d2::CustomizeConnection<SqliteConnection, diesel::r2d2::Error> 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<DB> {
|
||||
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::<SqliteConnection>::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<PooledConnection<ConnectionManager<SqliteConnection>>> {
|
||||
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<Mutex<SqliteConnection>> {
|
||||
self.connection.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn get_test_db(name: &str) -> DB {
|
||||
use crate::config;
|
||||
|
|
17
src/ddns.rs
17
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<DDNSConfig> {
|
||||
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<T>(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<T>(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);
|
||||
|
|
180
src/index.rs
180
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<DB>) -> Arc<CommandSender> {
|
||||
pub fn init(db: DB) -> Arc<CommandSender> {
|
||||
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<NewSong>,
|
||||
new_directories: Vec<NewDirectory>,
|
||||
connection: &'conn Mutex<SqliteConnection>,
|
||||
db: DB,
|
||||
album_art_pattern: Regex,
|
||||
}
|
||||
|
||||
impl<'conn> IndexBuilder<'conn> {
|
||||
impl IndexBuilder {
|
||||
#[cfg_attr(feature = "profile-index", flame)]
|
||||
fn new(
|
||||
connection: &Mutex<SqliteConnection>,
|
||||
album_art_pattern: Regex,
|
||||
) -> Result<IndexBuilder<'_>> {
|
||||
fn new(db: DB, album_art_pattern: Regex) -> Result<IndexBuilder> {
|
||||
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, _>(|| {
|
||||
let connection = self.db.connect()?;
|
||||
diesel::insert_into(songs::table)
|
||||
.values(&self.new_songs)
|
||||
.execute(connection)?;
|
||||
Ok(())
|
||||
})?;
|
||||
.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, _>(|| {
|
||||
let connection = self.db.connect()?;
|
||||
diesel::insert_into(directories::table)
|
||||
.values(&self.new_directories)
|
||||
.execute(connection)?;
|
||||
Ok(())
|
||||
})?;
|
||||
.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<T>(db: &T) -> Result<()>
|
||||
where
|
||||
T: ConnectionSource + VFSSource,
|
||||
{
|
||||
fn clean(db: &DB) -> Result<()> {
|
||||
let vfs = db.get_vfs()?;
|
||||
|
||||
{
|
||||
let all_songs: Vec<String>;
|
||||
{
|
||||
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::<Vec<_>>();
|
||||
|
||||
{
|
||||
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<String>;
|
||||
{
|
||||
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::<Vec<_>>();
|
||||
|
||||
{
|
||||
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<T>(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<T>(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<T>(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<T>(db: &T, command_buffer: &Arc<CommandSender>)
|
||||
where
|
||||
T: ConnectionSource,
|
||||
{
|
||||
pub fn self_trigger(db: &DB, command_buffer: &Arc<CommandSender>) {
|
||||
loop {
|
||||
{
|
||||
let command_buffer = command_buffer.deref();
|
||||
|
@ -504,19 +473,20 @@ where
|
|||
return;
|
||||
}
|
||||
}
|
||||
let sleep_duration;
|
||||
{
|
||||
let connection = db.get_connection();
|
||||
let settings: Result<MiscSettings> = misc_settings::table
|
||||
.get_result(connection.deref())
|
||||
.map_err(|e| e.into());
|
||||
if let Err(ref e) = settings {
|
||||
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);
|
||||
}
|
||||
sleep_duration = settings
|
||||
.map(|s| s.index_sleep_duration_seconds)
|
||||
.unwrap_or(1800);
|
||||
}
|
||||
1800
|
||||
})
|
||||
};
|
||||
thread::sleep(time::Duration::from_secs(sleep_duration as u64));
|
||||
}
|
||||
}
|
||||
|
@ -551,20 +521,19 @@ fn virtualize_directory(vfs: &VFS, mut directory: Directory) -> Option<Directory
|
|||
Some(directory)
|
||||
}
|
||||
|
||||
pub fn browse<T, P>(db: &T, virtual_path: P) -> Result<Vec<CollectionFile>>
|
||||
pub fn browse<P>(db: &DB, virtual_path: P) -> Result<Vec<CollectionFile>>
|
||||
where
|
||||
T: ConnectionSource + VFSSource,
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
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<Directory> = 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<Directory> = directories::table
|
||||
.filter(directories::parent.eq(&real_path_string))
|
||||
.order(sql::<sql_types::Bool>("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<Song> = songs::table
|
||||
.filter(songs::parent.eq(&real_path_string))
|
||||
.order(sql::<sql_types::Bool>("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<T, P>(db: &T, virtual_path: P) -> Result<Vec<Song>>
|
||||
pub fn flatten<P>(db: &DB, virtual_path: P) -> Result<Vec<Song>>
|
||||
where
|
||||
T: ConnectionSource + VFSSource,
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
use self::songs::dsl::*;
|
||||
let vfs = db.get_vfs()?;
|
||||
let connection = db.get_connection();
|
||||
let connection = db.connect()?;
|
||||
|
||||
let real_songs: Vec<Song> = 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::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub fn get_random_albums<T>(db: &T, count: i64) -> Result<Vec<Directory>>
|
||||
where
|
||||
T: ConnectionSource + VFSSource,
|
||||
{
|
||||
pub fn get_random_albums(db: &DB, count: i64) -> Result<Vec<Directory>> {
|
||||
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::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub fn get_recent_albums<T>(db: &T, count: i64) -> Result<Vec<Directory>>
|
||||
where
|
||||
T: ConnectionSource + VFSSource,
|
||||
{
|
||||
pub fn get_recent_albums(db: &DB, count: i64) -> Result<Vec<Directory>> {
|
||||
use self::directories::dsl::*;
|
||||
let vfs = db.get_vfs()?;
|
||||
let connection = db.get_connection();
|
||||
let connection = db.connect()?;
|
||||
let real_directories: Vec<Directory> = 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::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub fn search<T>(db: &T, query: &str) -> Result<Vec<CollectionFile>>
|
||||
where
|
||||
T: ConnectionSource + VFSSource,
|
||||
{
|
||||
pub fn search(db: &DB, query: &str) -> Result<Vec<CollectionFile>> {
|
||||
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<Directory> = 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<T>(db: &T, virtual_path: &Path) -> Result<Song>
|
||||
where
|
||||
T: ConnectionSource + VFSSource,
|
||||
{
|
||||
pub fn get_song(db: &DB, virtual_path: &Path) -> Result<Song> {
|
||||
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<Directory> = directories::table.load(connection.deref()).unwrap();
|
||||
let all_songs: Vec<Song> = songs::table.load(connection.deref()).unwrap();
|
||||
let connection = db.connect().unwrap();
|
||||
let all_directories: Vec<Directory> = directories::table.load(&connection).unwrap();
|
||||
let all_songs: Vec<Song> = 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<Song> = songs::table
|
||||
.filter(songs::title.eq("シャーベット (Sherbet)"))
|
||||
.load(connection.deref())
|
||||
.load(&connection)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(songs.len(), 1);
|
||||
|
|
|
@ -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<T>(db: &T, track: &Path) -> Result<Scrobble>
|
||||
where
|
||||
T: ConnectionSource + VFSSource,
|
||||
{
|
||||
fn scrobble_from_path(db: &DB, track: &Path) -> Result<Scrobble> {
|
||||
let song = index::get_song(db, track)?;
|
||||
Ok(Scrobble::new(
|
||||
song.artist.unwrap_or_else(|| "".into()),
|
||||
|
@ -54,27 +50,18 @@ where
|
|||
))
|
||||
}
|
||||
|
||||
pub fn link<T>(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<T>(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<T>(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<T>(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)?;
|
||||
|
|
50
src/main.rs
50
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
|
||||
|
|
|
@ -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<T>(owner: &str, db: &T) -> Result<Vec<String>>
|
||||
where
|
||||
T: ConnectionSource + VFSSource,
|
||||
{
|
||||
let connection = db.get_connection();
|
||||
pub fn list_playlists(owner: &str, db: &DB) -> Result<Vec<String>> {
|
||||
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<String> = Playlist::belonging_to(&user)
|
||||
.select(name)
|
||||
.load(connection.deref())?;
|
||||
.load(&connection)?;
|
||||
Ok(found_playlists)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_playlist<T>(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,18 +124,16 @@ where
|
|||
}
|
||||
|
||||
{
|
||||
let connection = db.get_connection();
|
||||
connection
|
||||
.deref()
|
||||
.transaction::<_, diesel::result::Error, _>(|| {
|
||||
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.deref())?;
|
||||
diesel::delete(old_songs).execute(&connection)?;
|
||||
|
||||
// Insert content
|
||||
diesel::insert_into(playlist_songs::table)
|
||||
.values(&new_songs)
|
||||
.execute(connection.deref())?;
|
||||
.execute(&*connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
@ -150,15 +141,12 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_playlist<T>(playlist_name: &str, owner: &str, db: &T) -> Result<Vec<Song>>
|
||||
where
|
||||
T: ConnectionSource + VFSSource,
|
||||
{
|
||||
pub fn read_playlist(playlist_name: &str, owner: &str, db: &DB) -> Result<Vec<Song>> {
|
||||
let vfs = db.get_vfs()?;
|
||||
let songs: Vec<Song>;
|
||||
|
||||
{
|
||||
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::<sql_types::Integer, _>(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<T>(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(())
|
||||
|
|
5
src/service/constants.rs
Normal file
5
src/service/constants.rs
Normal file
|
@ -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";
|
28
src/service/dto.rs
Normal file
28
src/service/dto.rs
Normal file
|
@ -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<String>,
|
||||
}
|
15
src/service/error.rs
Normal file
15
src/service/error.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum APIError {
|
||||
#[error("Incorrect Credentials")]
|
||||
IncorrectCredentials,
|
||||
#[error("Unspecified")]
|
||||
Unspecified,
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for APIError {
|
||||
fn from(_: anyhow::Error) -> Self {
|
||||
APIError::Unspecified
|
||||
}
|
||||
}
|
11
src/service/mod.rs
Normal file
11
src/service/mod.rs
Normal file
|
@ -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::*;
|
|
@ -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<rocket::Route> {
|
||||
routes![
|
||||
version,
|
||||
|
@ -61,14 +56,6 @@ pub fn get_routes() -> Vec<rocket::Route> {
|
|||
]
|
||||
}
|
||||
|
||||
#[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<anyhow::Error> 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<Self, ()> {
|
||||
let mut cookies = request.guard::<Cookies<'_>>().unwrap();
|
||||
let db = match request.guard::<State<'_, Arc<DB>>>() {
|
||||
let db = match request.guard::<State<'_, DB>>() {
|
||||
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<Self, ()> {
|
||||
let db = request.guard::<State<'_, Arc<DB>>>()?;
|
||||
let db = request.guard::<State<'_, DB>>()?;
|
||||
|
||||
match user::count::<DB>(&db) {
|
||||
match user::count(&db) {
|
||||
Err(_) => return Outcome::Failure((Status::InternalServerError, ())),
|
||||
Ok(0) => return Outcome::Success(AdminRights {}),
|
||||
_ => (),
|
||||
};
|
||||
|
||||
let auth = request.guard::<Auth>()?;
|
||||
match user::is_admin::<DB>(&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<VFSPathBuf> for PathBuf {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Serialize, Deserialize)]
|
||||
pub struct Version {
|
||||
pub major: i32,
|
||||
pub minor: i32,
|
||||
}
|
||||
|
||||
#[get("/version")]
|
||||
fn version() -> Json<Version> {
|
||||
let current_version = Version {
|
||||
major: CURRENT_MAJOR_VERSION,
|
||||
minor: CURRENT_MINOR_VERSION,
|
||||
fn version() -> Json<dto::Version> {
|
||||
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<DB>>) -> Result<Json<InitialSetup>> {
|
||||
let initial_setup = InitialSetup {
|
||||
has_any_users: user::count::<DB>(&db)? > 0,
|
||||
fn initial_setup(db: State<'_, DB>) -> Result<Json<dto::InitialSetup>> {
|
||||
let initial_setup = dto::InitialSetup {
|
||||
has_any_users: user::count(&db)? > 0,
|
||||
};
|
||||
Ok(Json(initial_setup))
|
||||
}
|
||||
|
||||
#[get("/settings")]
|
||||
fn get_settings(db: State<'_, Arc<DB>>, _admin_rights: AdminRights) -> Result<Json<Config>> {
|
||||
let config = config::read::<DB>(&db)?;
|
||||
fn get_settings(db: State<'_, DB>, _admin_rights: AdminRights) -> Result<Json<Config>> {
|
||||
let config = config::read(&db)?;
|
||||
Ok(Json(config))
|
||||
}
|
||||
|
||||
#[put("/settings", data = "<config>")]
|
||||
fn put_settings(
|
||||
db: State<'_, Arc<DB>>,
|
||||
_admin_rights: AdminRights,
|
||||
config: Json<Config>,
|
||||
) -> Result<()> {
|
||||
config::amend::<DB>(&db, &config)?;
|
||||
fn put_settings(db: State<'_, DB>, _admin_rights: AdminRights, config: Json<Config>) -> Result<()> {
|
||||
config::amend(&db, &config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/preferences")]
|
||||
fn get_preferences(db: State<'_, Arc<DB>>, auth: Auth) -> Result<Json<Preferences>> {
|
||||
let preferences = config::read_preferences::<DB>(&db, &auth.username)?;
|
||||
fn get_preferences(db: State<'_, DB>, auth: Auth) -> Result<Json<Preferences>> {
|
||||
let preferences = config::read_preferences(&db, &auth.username)?;
|
||||
Ok(Json(preferences))
|
||||
}
|
||||
|
||||
#[put("/preferences", data = "<preferences>")]
|
||||
fn put_preferences(
|
||||
db: State<'_, Arc<DB>>,
|
||||
auth: Auth,
|
||||
preferences: Json<Preferences>,
|
||||
) -> Result<()> {
|
||||
config::write_preferences::<DB>(&db, &auth.username, &preferences)?;
|
||||
fn put_preferences(db: State<'_, DB>, auth: Auth, preferences: Json<Preferences>) -> 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 = "<credentials>")]
|
||||
fn auth(
|
||||
db: State<'_, Arc<DB>>,
|
||||
credentials: Json<AuthCredentials>,
|
||||
db: State<'_, DB>,
|
||||
credentials: Json<dto::AuthCredentials>,
|
||||
mut cookies: Cookies<'_>,
|
||||
) -> std::result::Result<(), APIError> {
|
||||
if !user::auth::<DB>(&db, &credentials.username, &credentials.password)? {
|
||||
if !user::auth(&db, &credentials.username, &credentials.password)? {
|
||||
return Err(APIError::IncorrectCredentials);
|
||||
}
|
||||
let is_admin = user::is_admin::<DB>(&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<DB>>, _auth: Auth) -> Result<Json<Vec<index::CollectionFile>>> {
|
||||
fn browse_root(db: State<'_, DB>, _auth: Auth) -> Result<Json<Vec<index::CollectionFile>>> {
|
||||
let result = index::browse(db.deref().deref(), &PathBuf::new())?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[get("/browse/<path>")]
|
||||
fn browse(
|
||||
db: State<'_, Arc<DB>>,
|
||||
db: State<'_, DB>,
|
||||
_auth: Auth,
|
||||
path: VFSPathBuf,
|
||||
) -> Result<Json<Vec<index::CollectionFile>>> {
|
||||
|
@ -318,42 +269,38 @@ fn browse(
|
|||
}
|
||||
|
||||
#[get("/flatten")]
|
||||
fn flatten_root(db: State<'_, Arc<DB>>, _auth: Auth) -> Result<Json<Vec<index::Song>>> {
|
||||
fn flatten_root(db: State<'_, DB>, _auth: Auth) -> Result<Json<Vec<index::Song>>> {
|
||||
let result = index::flatten(db.deref().deref(), &PathBuf::new())?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[get("/flatten/<path>")]
|
||||
fn flatten(
|
||||
db: State<'_, Arc<DB>>,
|
||||
_auth: Auth,
|
||||
path: VFSPathBuf,
|
||||
) -> Result<Json<Vec<index::Song>>> {
|
||||
fn flatten(db: State<'_, DB>, _auth: Auth, path: VFSPathBuf) -> Result<Json<Vec<index::Song>>> {
|
||||
let result = index::flatten(db.deref().deref(), &path.into() as &PathBuf)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[get("/random")]
|
||||
fn random(db: State<'_, Arc<DB>>, _auth: Auth) -> Result<Json<Vec<index::Directory>>> {
|
||||
fn random(db: State<'_, DB>, _auth: Auth) -> Result<Json<Vec<index::Directory>>> {
|
||||
let result = index::get_random_albums(db.deref().deref(), 20)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[get("/recent")]
|
||||
fn recent(db: State<'_, Arc<DB>>, _auth: Auth) -> Result<Json<Vec<index::Directory>>> {
|
||||
fn recent(db: State<'_, DB>, _auth: Auth) -> Result<Json<Vec<index::Directory>>> {
|
||||
let result = index::get_recent_albums(db.deref().deref(), 20)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[get("/search")]
|
||||
fn search_root(db: State<'_, Arc<DB>>, _auth: Auth) -> Result<Json<Vec<index::CollectionFile>>> {
|
||||
fn search_root(db: State<'_, DB>, _auth: Auth) -> Result<Json<Vec<index::CollectionFile>>> {
|
||||
let result = index::search(db.deref().deref(), "")?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[get("/search/<query>")]
|
||||
fn search(
|
||||
db: State<'_, Arc<DB>>,
|
||||
db: State<'_, DB>,
|
||||
_auth: Auth,
|
||||
query: String,
|
||||
) -> Result<Json<Vec<index::CollectionFile>>> {
|
||||
|
@ -362,12 +309,7 @@ fn search(
|
|||
}
|
||||
|
||||
#[get("/serve/<path>")]
|
||||
fn serve(
|
||||
db: State<'_, Arc<DB>>,
|
||||
_auth: Auth,
|
||||
path: VFSPathBuf,
|
||||
) -> Result<serve::RangeResponder<File>> {
|
||||
let db: &DB = db.deref().deref();
|
||||
fn serve(db: State<'_, DB>, _auth: Auth, path: VFSPathBuf) -> Result<serve::RangeResponder<File>> {
|
||||
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<DB>>, auth: Auth) -> Result<Json<Vec<ListPlaylistsEntry>>> {
|
||||
fn list_playlists(db: State<'_, DB>, auth: Auth) -> Result<Json<Vec<dto::ListPlaylistsEntry>>> {
|
||||
let playlist_names = playlist::list_playlists(&auth.username, db.deref().deref())?;
|
||||
let playlists: Vec<ListPlaylistsEntry> = playlist_names
|
||||
let playlists: Vec<dto::ListPlaylistsEntry> = 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<String>,
|
||||
}
|
||||
|
||||
#[put("/playlist/<name>", data = "<playlist>")]
|
||||
fn save_playlist(
|
||||
db: State<'_, Arc<DB>>,
|
||||
db: State<'_, DB>,
|
||||
auth: Auth,
|
||||
name: String,
|
||||
playlist: Json<SavePlaylistInput>,
|
||||
playlist: Json<dto::SavePlaylistInput>,
|
||||
) -> Result<()> {
|
||||
playlist::save_playlist(&name, &auth.username, &playlist.tracks, db.deref().deref())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/playlist/<name>")]
|
||||
fn read_playlist(
|
||||
db: State<'_, Arc<DB>>,
|
||||
auth: Auth,
|
||||
name: String,
|
||||
) -> Result<Json<Vec<index::Song>>> {
|
||||
fn read_playlist(db: State<'_, DB>, auth: Auth, name: String) -> Result<Json<Vec<index::Song>>> {
|
||||
let songs = playlist::read_playlist(&name, &auth.username, db.deref().deref())?;
|
||||
Ok(Json(songs))
|
||||
}
|
||||
|
||||
#[delete("/playlist/<name>")]
|
||||
fn delete_playlist(db: State<'_, Arc<DB>>, 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/<path>")]
|
||||
fn lastfm_now_playing(db: State<'_, Arc<DB>>, 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<DB>>, auth: Auth, path: VFSPathBuf) -> R
|
|||
}
|
||||
|
||||
#[post("/lastfm/scrobble/<path>")]
|
||||
fn lastfm_scrobble(db: State<'_, Arc<DB>>, 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<DB>>, auth: Auth, path: VFSPathBuf) -> Resu
|
|||
|
||||
#[get("/lastfm/link?<token>&<content>")]
|
||||
fn lastfm_link(
|
||||
db: State<'_, Arc<DB>>,
|
||||
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<DB>>, auth: Auth) -> Result<()> {
|
||||
fn lastfm_unlink(db: State<'_, DB>, auth: Auth) -> Result<()> {
|
||||
lastfm::unlink(db.deref().deref(), &auth.username)?;
|
||||
Ok(())
|
||||
}
|
7
src/service/rocket/mod.rs
Normal file
7
src/service/rocket/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod api;
|
||||
mod serve;
|
||||
|
||||
pub mod server;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test;
|
|
@ -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: DB,
|
||||
command_sender: Arc<CommandSender>,
|
||||
) -> Result<rocket::Rocket> {
|
||||
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);
|
||||
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<CommandSender>,
|
||||
) -> 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(())
|
||||
}
|
164
src/service/rocket/test.rs
Normal file
164
src/service/rocket/test.rs
Normal file
|
@ -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<Vec<u8>> {
|
||||
let body = self.response.body().unwrap();
|
||||
let body = body.into_bytes().unwrap();
|
||||
let builder = self.builder();
|
||||
builder.body(body).unwrap()
|
||||
}
|
||||
|
||||
fn to_object<T: DeserializeOwned>(&mut self) -> Response<T> {
|
||||
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<index::CommandSender>,
|
||||
}
|
||||
|
||||
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<HeaderValue>) -> Response<Vec<u8>> {
|
||||
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<T: DeserializeOwned>(&mut self, url: &str) -> Response<T> {
|
||||
let mut response = self.client.get(url).dispatch();
|
||||
RocketResponse {
|
||||
response: response.deref_mut(),
|
||||
}
|
||||
.to_object()
|
||||
}
|
||||
|
||||
fn put_json<T: Serialize>(&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<T: Serialize>(&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();
|
||||
}
|
||||
}
|
440
src/service/test.rs
Normal file
440
src/service/test.rs
Normal file
|
@ -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<HeaderValue>) -> Response<Vec<u8>>;
|
||||
fn post(&mut self, url: &str) -> Response<()>;
|
||||
fn delete(&mut self, url: &str) -> Response<()>;
|
||||
fn get_json<T: DeserializeOwned>(&mut self, url: &str) -> Response<T>;
|
||||
fn put_json<T: Serialize>(&mut self, url: &str, payload: &T) -> Response<()>;
|
||||
fn post_json<T: Serialize>(&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::<Vec<index::CollectionFile>>("/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::<dto::Version>("/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::<dto::InitialSetup>("/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::<dto::InitialSetup>("/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::<config::Config>("/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::<config::Config>("/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::<Vec<index::Directory>>("/api/random");
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 0);
|
||||
|
||||
service.index();
|
||||
|
||||
let response = service.get_json::<Vec<index::Directory>>("/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<Cookie> = 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::<Vec<index::CollectionFile>>("/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::<Vec<index::CollectionFile>>(&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::<Vec<index::Song>>("/api/flatten");
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 12);
|
||||
|
||||
let response = service.get_json::<Vec<index::Song>>("/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::<Vec<index::Directory>>("/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::<Vec<index::Directory>>("/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::<Vec<index::CollectionFile>>("/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::<Vec<dto::ListPlaylistsEntry>>("/api/playlists");
|
||||
let playlists = response.body();
|
||||
assert_eq!(playlists.len(), 0);
|
||||
|
||||
let response = service.get_json::<Vec<index::Song>>("/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::<Vec<dto::ListPlaylistsEntry>>("/api/playlists");
|
||||
let playlists = response.body();
|
||||
assert_eq!(
|
||||
playlists,
|
||||
&vec![dto::ListPlaylistsEntry {
|
||||
name: "my_playlist".into()
|
||||
}]
|
||||
);
|
||||
|
||||
let response = service.get_json::<Vec<index::Song>>("/api/playlist/my_playlist");
|
||||
let songs = response.body();
|
||||
assert_eq!(songs, &my_songs);
|
||||
|
||||
service.delete("/api/playlist/my_playlist");
|
||||
|
||||
let response = service.get_json::<Vec<dto::ListPlaylistsEntry>>("/api/playlists");
|
||||
let playlists = response.body();
|
||||
assert_eq!(playlists.len(), 0);
|
||||
}
|
|
@ -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);
|
||||
}
|
63
src/test.rs
63
src/test.rs
|
@ -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<index::CommandSender>,
|
||||
db: Arc<DB>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
71
src/user.rs
71
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<T>(db: &T, username: &str, password: &str) -> Result<bool>
|
||||
where
|
||||
T: ConnectionSource,
|
||||
{
|
||||
pub fn auth(db: &DB, username: &str, password: &str) -> Result<bool> {
|
||||
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<T>(db: &T) -> Result<i64>
|
||||
where
|
||||
T: ConnectionSource,
|
||||
{
|
||||
pub fn count(db: &DB) -> Result<i64> {
|
||||
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<T>(db: &T, username: &str) -> Result<bool>
|
||||
where
|
||||
T: ConnectionSource,
|
||||
{
|
||||
pub fn exists(db: &DB, username: &str) -> Result<bool> {
|
||||
use crate::db::users::dsl::*;
|
||||
let connection = db.get_connection();
|
||||
let connection = db.connect()?;
|
||||
let results: Vec<String> = users
|
||||
.select(name)
|
||||
.filter(name.eq(username))
|
||||
.get_results(connection.deref())?;
|
||||
.get_results(&connection)?;
|
||||
Ok(results.len() > 0)
|
||||
}
|
||||
|
||||
pub fn is_admin<T>(db: &T, username: &str) -> Result<bool>
|
||||
where
|
||||
T: ConnectionSource,
|
||||
{
|
||||
pub fn is_admin(db: &DB, username: &str) -> Result<bool> {
|
||||
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<T>(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<T>(db: &T, username: &str) -> Result<String>
|
||||
where
|
||||
T: ConnectionSource,
|
||||
{
|
||||
pub fn get_lastfm_session_key(db: &DB, username: &str) -> Result<String> {
|
||||
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<T>(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<T>(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(())
|
||||
}
|
||||
|
|
|
@ -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<VFS>;
|
||||
|
@ -17,10 +16,10 @@ impl VFSSource for DB {
|
|||
fn get_vfs(&self) -> Result<VFS> {
|
||||
use self::mount_points::dsl::*;
|
||||
let mut vfs = VFS::new();
|
||||
let connection = self.get_connection();
|
||||
let connection = self.connect()?;
|
||||
let points: Vec<MountPoint> = mount_points
|
||||
.select((source, name))
|
||||
.get_results(connection.deref())?;
|
||||
.get_results(&connection)?;
|
||||
for point in points {
|
||||
vfs.mount(&Path::new(&point.source), &point.name)?;
|
||||
}
|
||||
|
|
10
src/web.rs
10
src/web.rs
|
@ -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);
|
||||
}
|
Loading…
Reference in a new issue