diff --git a/.gitignore b/.gitignore index fc4762b..b6aaed4 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ TestConfig.toml *.sqlite **/*.sqlite-shm **/*.sqlite-wal +auth.secret collection.index polaris.log polaris.ndb diff --git a/CHANGELOG.md b/CHANGELOG.md index f2b0e62..fed4cf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,8 @@ - The `/thumbnail` endpoint supports a new size labeled `tiny` which returns 40x40px images. - Persistent data, such as playlists, is now saved in a directory that may be configured with the `--data` CLI option or the `POLARIS_DATA_DIR` environment variable. - Removed last.fm integration due to maintenance concerns (abandoned libraries, broken account linking) and mismatch with project goals. -- Removed `/config` API endpoint. +- Removed the `/config` API endpoint. +- Removed the `/ddns` API endpoints, merged into the existing `/settings` endpoints. ### Web client diff --git a/Cargo.lock b/Cargo.lock index a859236..e945120 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,15 +89,6 @@ dependencies = [ "syn 2.0.72", ] -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - [[package]] name = "auto-future" version = "1.0.0" @@ -314,9 +305,6 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -dependencies = [ - "serde", -] [[package]] name = "block-buffer" @@ -427,21 +415,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "cookie" version = "0.18.1" @@ -461,21 +434,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" version = "1.4.2" @@ -504,15 +462,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-queue" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -544,17 +493,6 @@ dependencies = [ "libc", ] -[[package]] -name = "der" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - [[package]] name = "deranged" version = "0.3.11" @@ -577,25 +515,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid", "crypto-common", "subtle", ] -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" -dependencies = [ - "serde", -] [[package]] name = "embed-resource" @@ -666,28 +594,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "etcetera" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", -] - -[[package]] -name = "event-listener" -version = "5.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - [[package]] name = "extended" version = "0.1.0" @@ -735,17 +641,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "flume" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" -dependencies = [ - "futures-core", - "futures-sink", - "spin", -] - [[package]] name = "fnv" version = "1.0.7" @@ -791,28 +686,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" -[[package]] -name = "futures-executor" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-intrusive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] - [[package]] name = "futures-io" version = "0.3.30" @@ -917,15 +790,6 @@ dependencies = [ "serde", ] -[[package]] -name = "hashlink" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" -dependencies = [ - "hashbrown", -] - [[package]] name = "headers" version = "0.4.0" @@ -950,12 +814,6 @@ dependencies = [ "http 1.1.0", ] -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hermit-abi" version = "0.3.9" @@ -968,15 +826,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - [[package]] name = "hmac" version = "0.12.1" @@ -986,15 +835,6 @@ dependencies = [ "digest", ] -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "http" version = "0.2.12" @@ -1166,9 +1006,6 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] [[package]] name = "lewton" @@ -1187,39 +1024,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - -[[package]] -name = "libsqlite3-sys" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - [[package]] name = "log" version = "0.4.22" @@ -1242,16 +1052,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - [[package]] name = "memchr" version = "2.7.4" @@ -1284,12 +1084,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.7.4" @@ -1418,33 +1212,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num-bigint-dig" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" -dependencies = [ - "byteorder", - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand", - "smallvec", - "zeroize", -] - [[package]] name = "num-complex" version = "0.4.6" @@ -1469,17 +1236,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1487,7 +1243,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -1552,35 +1307,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "parking" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" - -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.3", - "smallvec", - "windows-targets 0.52.6", -] - [[package]] name = "password-hash" version = "0.4.2" @@ -1592,12 +1318,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pbkdf2" version = "0.11.0" @@ -1610,15 +1330,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.1" @@ -1657,33 +1368,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" - [[package]] name = "png" version = "0.17.13" @@ -1742,7 +1426,6 @@ dependencies = [ "serde_derive", "serde_json", "simplelog", - "sqlx", "symphonia", "thiserror", "tinyvec", @@ -1906,24 +1589,6 @@ dependencies = [ "libc", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" -dependencies = [ - "bitflags 2.6.0", -] - [[package]] name = "regex" version = "1.10.6" @@ -1978,26 +1643,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rsa" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core", - "signature", - "spki", - "subtle", - "zeroize", -] - [[package]] name = "rust-multipart-rfc7578_2" version = "0.6.1" @@ -2110,12 +1755,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "sd-notify" version = "0.4.2" @@ -2216,16 +1855,6 @@ dependencies = [ "digest", ] -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core", -] - [[package]] name = "simd-adler32" version = "0.3.7" @@ -2272,9 +1901,6 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -dependencies = [ - "serde", -] [[package]] name = "socket2" @@ -2291,220 +1917,6 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sqlformat" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" -dependencies = [ - "nom", - "unicode_categories", -] - -[[package]] -name = "sqlx" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27144619c6e5802f1380337a209d2ac1c431002dd74c6e60aebff3c506dc4f0c" -dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", -] - -[[package]] -name = "sqlx-core" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a999083c1af5b5d6c071d34a708a19ba3e02106ad82ef7bbd69f5e48266b613b" -dependencies = [ - "atoi", - "byteorder", - "bytes", - "crc", - "crossbeam-queue", - "either", - "event-listener", - "futures-channel", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown", - "hashlink", - "hex", - "indexmap", - "log", - "memchr", - "once_cell", - "paste", - "percent-encoding", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlformat", - "thiserror", - "tokio", - "tokio-stream", - "tracing", - "url", -] - -[[package]] -name = "sqlx-macros" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23217eb7d86c584b8cbe0337b9eacf12ab76fe7673c513141ec42565698bb88" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn 2.0.72", -] - -[[package]] -name = "sqlx-macros-core" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a099220ae541c5db479c6424bdf1b200987934033c2584f79a0e1693601e776" -dependencies = [ - "dotenvy", - "either", - "heck", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "sqlx-core", - "sqlx-sqlite", - "syn 2.0.72", - "tempfile", - "tokio", - "url", -] - -[[package]] -name = "sqlx-mysql" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5afe4c38a9b417b6a9a5eeffe7235d0a106716495536e7727d1c7f4b1ff3eba6" -dependencies = [ - "atoi", - "base64 0.22.1", - "bitflags 2.6.0", - "byteorder", - "bytes", - "crc", - "digest", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "percent-encoding", - "rand", - "rsa", - "sha1", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "tracing", - "whoami", -] - -[[package]] -name = "sqlx-postgres" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1dbb157e65f10dbe01f729339c06d239120221c9ad9fa0ba8408c4cc18ecf21" -dependencies = [ - "atoi", - "base64 0.22.1", - "bitflags 2.6.0", - "byteorder", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "hex", - "hkdf", - "hmac", - "home", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "rand", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "tracing", - "whoami", -] - -[[package]] -name = "sqlx-sqlite" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2cdd83c008a622d94499c0006d8ee5f821f36c89b7d625c900e5dc30b5c5ee" -dependencies = [ - "atoi", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde", - "serde_urlencoded", - "sqlx-core", - "tracing", - "url", -] [[package]] name = "stacker" @@ -2525,17 +1937,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - [[package]] name = "subtle" version = "2.6.1" @@ -2890,17 +2291,6 @@ dependencies = [ "syn 2.0.72", ] -[[package]] -name = "tokio-stream" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.11" @@ -3035,21 +2425,9 @@ checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - [[package]] name = "tracing-core" version = "0.1.32" @@ -3121,24 +2499,12 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-properties" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" - [[package]] name = "unicode-width" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "untrusted" version = "0.9.0" @@ -3171,12 +2537,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.5" @@ -3228,12 +2588,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "webpki-roots" version = "0.26.3" @@ -3249,16 +2603,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" -[[package]] -name = "whoami" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" -dependencies = [ - "redox_syscall 0.4.1", - "wasite", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 7263127..d21a2b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,11 +70,6 @@ version = "0.25.2" default-features = false features = ["bmp", "gif", "jpeg", "png"] -[dependencies.sqlx] -version = "0.8.0" -default-features = false -features = ["macros", "migrate", "runtime-tokio", "sqlite"] - [target.'cfg(windows)'.dependencies] native-windows-gui = { version = "1.0.13", default-features = false, features = [ "cursor", @@ -97,6 +92,3 @@ winres = "0.1" axum-test = "15.7" bytes = "1.7.1" percent-encoding = "2.2" - -[profile.dev.package.sqlx-macros] -opt-level = 3 diff --git a/docs/swagger/polaris-api.json b/docs/swagger/polaris-api.json index 7186dad..5b45c50 100644 --- a/docs/swagger/polaris-api.json +++ b/docs/swagger/polaris-api.json @@ -99,36 +99,6 @@ ] } }, - "/config": { - "put": { - "tags": [ - "Configuration" - ], - "summary": "Amends the server settings, mount directories and list of users", - "operationId": "putConfig", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#components/schemas/Config" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation" - } - }, - "security": [ - { - "admin_http_bearer": [], - "admin_query_parameter": [] - } - ] - } - }, "/mount_dirs": { "get": { "tags": [ @@ -1020,26 +990,6 @@ } } }, - "Config": { - "type": "object", - "properties": { - "settings": { - "$ref": "#/components/schemas/Settings" - }, - "mount_dirs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MountDir" - } - }, - "users": { - "type": "array", - "items": { - "$ref": "#/components/schemas/User" - } - } - } - }, "User": { "type": "object", "properties": { diff --git a/flake.nix b/flake.nix index 93ce75e..a6140c8 100644 --- a/flake.nix +++ b/flake.nix @@ -45,16 +45,12 @@ cargo-edit cargo-watch rust-analyzer - sqlx-cli samply - sqlitebrowser ]; env = { # Required by rust-analyzer RUST_SRC_PATH = "${pkgs.rustToolchain}/lib/rustlib/src/rust/library"; - # SQLx dev database URL - DATABASE_URL=sqlite:./src/db/schema.sqlite; }; }; }); diff --git a/res/unix/Makefile b/res/unix/Makefile index f19719c..8471fef 100644 --- a/res/unix/Makefile +++ b/res/unix/Makefile @@ -7,11 +7,13 @@ EXEC_PREFIX ?= $(PREFIX) BINDIR ?= $(EXEC_PREFIX)/bin DATAROOTDIR ?= $(PREFIX)/share DATADIR ?= $(DATAROOTDIR) +SYSCONFDIR ?= $(PREFIX)/etc LOCALSTATEDIR ?= $(PREFIX)/var RUNSTATEDIR ?= $(LOCALSTATEDIR)/run %-system: POLARIS_BIN_PATH := $(BINDIR)/polaris %-system: export POLARIS_WEB_DIR := $(DATADIR)/polaris/web %-system: export POLARIS_SWAGGER_DIR := $(DATADIR)/polaris/swagger +%-system: export POLARIS_CONFIG_DIR := $(SYSCONFDIR)/polaris %-system: export POLARIS_DATA_DIR := $(LOCALSTATEDIR)/lib/polaris %-system: export POLARIS_DB_DIR := $(LOCALSTATEDIR)/lib/polaris %-system: export POLARIS_LOG_DIR := $(LOCALSTATEDIR)/log/polaris @@ -19,10 +21,12 @@ RUNSTATEDIR ?= $(LOCALSTATEDIR)/run %-system: export POLARIS_PID_DIR := $(RUNSTATEDIR)/polaris XDG_CACHE_HOME ?= $(HOME)/.cache +XDG_CONFIG_HOME ?= $(HOME)/.config XDG_DATA_HOME ?= $(HOME)/.local/share XDG_BINDIR ?= $(HOME)/.local/bin XDG_DATADIR ?= $(XDG_DATA_HOME)/polaris XDG_CACHEDIR ?= $(XDG_CACHE_HOME)/polaris +XDG_CONFIGDIR ?= $(XDG_CONFIG_HOME)/polaris ifdef $(XDG_RUNTIME_DIR) XDG_PIDDIR ?= $(XDG_RUNTIME_DIR)/polaris else @@ -31,6 +35,7 @@ endif %-xdg: POLARIS_BIN_PATH := $(XDG_BINDIR)/polaris %-xdg: export POLARIS_WEB_DIR := $(XDG_DATADIR)/web %-xdg: export POLARIS_SWAGGER_DIR := $(XDG_DATADIR)/swagger +%-xdg: export POLARIS_CONFIG_DIR := $(XDG_CONFIGDIR) %-xdg: export POLARIS_DATA_DIR := $(XDG_DATADIR) %-xdg: export POLARIS_DB_DIR := $(XDG_DATADIR) %-xdg: export POLARIS_LOG_DIR := $(XDG_CACHEDIR) @@ -60,6 +65,7 @@ list-paths: $(info POLARIS_BIN_PATH is $(POLARIS_BIN_PATH)) $(info POLARIS_WEB_DIR is $(POLARIS_WEB_DIR)) $(info POLARIS_SWAGGER_DIR is $(POLARIS_SWAGGER_DIR)) + $(info POLARIS_CONFIG_DIR is $(POLARIS_CONFIG_DIR)) $(info POLARIS_DATA_DIR is $(POLARIS_DATA_DIR)) $(info POLARIS_DB_DIR is $(POLARIS_DB_DIR)) $(info POLARIS_LOG_DIR is $(POLARIS_LOG_DIR)) @@ -93,6 +99,7 @@ uninstall-bin: uninstall-data: rm -rf $(POLARIS_WEB_DIR) rm -rf $(POLARIS_SWAGGER_DIR) + rm -rf $(POLARIS_CONFIG_DIR) rm -rf $(POLARIS_DATA_DIR) rm -rf $(POLARIS_DB_DIR) rm -rf $(POLARIS_LOG_DIR) diff --git a/src/app.rs b/src/app.rs index 67556f4..ba4b763 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,7 +1,10 @@ use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; + +use config::AuthSecret; +use rand::rngs::OsRng; +use rand::RngCore; -use crate::db::DB; use crate::paths::Paths; pub mod config; @@ -12,10 +15,8 @@ pub mod ndb; pub mod peaks; pub mod playlist; pub mod scanner; -pub mod settings; pub mod thumbnail; pub mod user; -pub mod vfs; #[cfg(test)] pub mod test; @@ -64,15 +65,6 @@ pub enum Error { #[error(transparent)] PeaksDeserialization(bitcode::Error), - #[error(transparent)] - Database(#[from] sqlx::Error), - #[error("Could not initialize database connection pool")] - ConnectionPoolBuild, - #[error("Could not acquire database connection from pool")] - ConnectionPool, - #[error("Could not apply database migrations: {0}")] - Migration(sqlx::migrate::MigrateError), - #[error(transparent)] NativeDatabase(#[from] native_db::db_type::Error), #[error("Could not initialize database")] @@ -148,19 +140,13 @@ pub struct App { pub scanner: scanner::Scanner, pub index_manager: index::Manager, pub config_manager: config::Manager, - pub ddns_manager: ddns::Manager, pub peaks_manager: peaks::Manager, pub playlist_manager: playlist::Manager, - pub settings_manager: settings::Manager, pub thumbnail_manager: thumbnail::Manager, - pub user_manager: user::Manager, - pub vfs_manager: vfs::Manager, } impl App { pub async fn new(port: u16, paths: Paths) -> Result<Self, Error> { - let db = DB::new(&paths.db_file_path).await?; - fs::create_dir_all(&paths.data_dir_path) .map_err(|e| Error::Io(paths.data_dir_path.clone(), e))?; @@ -177,34 +163,17 @@ impl App { fs::create_dir_all(&thumbnails_dir_path) .map_err(|e| Error::Io(thumbnails_dir_path.clone(), e))?; + let auth_secret_file_path = paths.data_dir_path.join("auth.secret"); + let auth_secret = Self::get_or_create_auth_secret(&auth_secret_file_path); + + let config_manager = config::Manager::new(&paths.config_file_path).await?; let ndb_manager = ndb::Manager::new(&paths.data_dir_path)?; - let vfs_manager = vfs::Manager::new(db.clone()); - let settings_manager = settings::Manager::new(db.clone()); - let auth_secret = settings_manager.get_auth_secret().await?; - let ddns_manager = ddns::Manager::new(db.clone()); - let user_manager = user::Manager::new(db.clone(), auth_secret); let index_manager = index::Manager::new(&paths.data_dir_path).await?; - let scanner = scanner::Scanner::new( - index_manager.clone(), - settings_manager.clone(), - vfs_manager.clone(), - ) - .await?; - let config_manager = config::Manager::new( - settings_manager.clone(), - user_manager.clone(), - vfs_manager.clone(), - ddns_manager.clone(), - ); + let scanner = scanner::Scanner::new(index_manager.clone(), config_manager.clone()).await?; let peaks_manager = peaks::Manager::new(peaks_dir_path); let playlist_manager = playlist::Manager::new(ndb_manager); let thumbnail_manager = thumbnail::Manager::new(thumbnails_dir_path); - if let Some(config_path) = paths.config_file_path { - let config = config::Config::from_path(&config_path)?; - config_manager.apply(&config).await?; - } - Ok(Self { port, web_dir_path: paths.web_dir_path, @@ -212,13 +181,28 @@ impl App { scanner, index_manager, config_manager, - ddns_manager, peaks_manager, playlist_manager, - settings_manager, thumbnail_manager, - user_manager, - vfs_manager, }) } + + async fn get_or_create_auth_secret(path: &Path) -> Result<config::AuthSecret, Error> { + match tokio::fs::read(&path).await { + Ok(s) => Ok(config::AuthSecret { + key: s + .try_into() + .map_err(|_| Error::AuthenticationSecretInvalid)?, + }), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + let mut secret = AuthSecret::default(); + OsRng.fill_bytes(&mut secret.key); + tokio::fs::write(&path, &secret.key) + .await + .map_err(|_| Error::AuthenticationSecretInvalid)?; + Ok(secret) + } + Err(e) => return Err(Error::Io(path.to_owned(), e)), + } + } } diff --git a/src/app/config.rs b/src/app/config.rs index d0e9ff2..cc867fd 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -1,14 +1,57 @@ -use serde::Deserialize; -use std::{io::Read, path::Path}; +use std::{ + io::Read, + ops::Deref, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; -use crate::app::{ddns, settings, user, vfs, Error}; +use pbkdf2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; +use pbkdf2::Pbkdf2; +use rand::rngs::OsRng; +use regex::Regex; +use serde::{Deserialize, Serialize}; -#[derive(Default, Deserialize)] +use crate::app::Error; + +#[derive(Clone, Default)] +pub struct AuthSecret { + pub key: [u8; 32], +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct MountDir { + pub source: String, + pub name: String, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct User { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub admin: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] + pub initial_password: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub hashed_password: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub web_theme_base: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub web_theme_accent: Option<String>, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] pub struct Config { - pub settings: Option<settings::NewSettings>, - pub mount_dirs: Option<Vec<vfs::MountDir>>, - pub ydns: Option<ddns::Config>, - pub users: Option<Vec<user::NewUser>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub reindex_every_n_seconds: Option<u64>, + #[serde(skip_serializing_if = "Option::is_none")] + pub album_art_pattern: Option<String>, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub mount_dirs: Vec<MountDir>, + #[serde(skip_serializing_if = "Option::is_none")] + pub ddns_url: Option<String>, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub users: Vec<User>, } impl Config { @@ -26,72 +69,124 @@ impl Config { #[derive(Clone)] pub struct Manager { - settings_manager: settings::Manager, - user_manager: user::Manager, - vfs_manager: vfs::Manager, - ddns_manager: ddns::Manager, + config_file_path: PathBuf, + config: Arc<tokio::sync::RwLock<Config>>, } impl Manager { - pub fn new( - settings_manager: settings::Manager, - user_manager: user::Manager, - vfs_manager: vfs::Manager, - ddns_manager: ddns::Manager, - ) -> Self { - Self { - settings_manager, - user_manager, - vfs_manager, - ddns_manager, - } + pub async fn new(config_file_path: &Path) -> Result<Self, Error> { + let config = Config::default(); // TODO read from disk!! + let manager = Self { + config_file_path: config_file_path.to_owned(), + config: Arc::default(), + }; + manager.apply(config); + Ok(manager) } - pub async fn apply(&self, config: &Config) -> Result<(), Error> { - if let Some(new_settings) = &config.settings { - self.settings_manager.amend(new_settings).await?; - } + pub async fn apply(&self, mut config: Config) -> Result<(), Error> { + config + .users + .retain(|u| u.initial_password.is_some() || u.hashed_password.is_some()); - if let Some(mount_dirs) = &config.mount_dirs { - self.vfs_manager.set_mount_dirs(mount_dirs).await?; - } - - if let Some(ddns_config) = &config.ydns { - self.ddns_manager.set_config(ddns_config).await?; - } - - if let Some(ref users) = config.users { - let old_users: Vec<user::User> = self.user_manager.list().await?; - - // Delete users that are not in new list - for old_user in old_users - .iter() - .filter(|old_user| !users.iter().any(|u| u.name == old_user.name)) - { - self.user_manager.delete(&old_user.name).await?; - } - - // Insert new users - for new_user in users - .iter() - .filter(|u| !old_users.iter().any(|old_user| old_user.name == u.name)) - { - self.user_manager.create(new_user).await?; - } - - // Update users - for user in users { - self.user_manager - .set_password(&user.name, &user.password) - .await?; - self.user_manager - .set_is_admin(&user.name, user.admin) - .await?; + for user in &mut config.users { + if let (Some(password), None) = (&user.initial_password, &user.hashed_password) { + user.hashed_password = Some(hash_password(&password)?); } } + *self.config.write().await = config; + + // TODO persistence + Ok(()) } + + pub async fn get_index_sleep_duration(&self) -> Duration { + let seconds = self + .config + .read() + .await + .reindex_every_n_seconds + .unwrap_or(1800); + Duration::from_secs(seconds) + } + + pub async fn get_index_album_art_pattern(&self) -> String { + self.config + .read() + .await + .album_art_pattern + .clone() + .unwrap_or("Folder.(jpeg|jpg|png)".to_owned()) + } + + pub async fn get_ddns_update_url(&self) -> Option<String> { + self.config.read().await.ddns_url.clone() + } + + pub async fn get_users(&self) -> Vec<User> { + self.config.read().await.users.clone() + } + + pub async fn get_user(&self, username: &str) -> Result<User, Error> { + let config = self.config.read().await; + let user = config.users.iter().find(|u| u.name == username); + user.cloned().ok_or(Error::UserNotFound) + } + + pub async fn delete_user(&self, username: &str) { + let mut config = self.config.write().await; + config.users.retain(|u| u.name != username); + // TODO persistence + } + + pub async fn get_mounts(&self) -> Vec<MountDir> { + self.config.read().await.mount_dirs.clone() + } + + pub async fn resolve_virtual_path<P: AsRef<Path>>( + &self, + virtual_path: P, + ) -> Result<PathBuf, Error> { + let config = self.config.read().await; + for mount in &config.mount_dirs { + let mounth_source = sanitize_path(&mount.source); + let mount_path = Path::new(&mount.name); + if let Ok(p) = virtual_path.as_ref().strip_prefix(mount_path) { + return if p.components().count() == 0 { + Ok(mounth_source) + } else { + Ok(mounth_source.join(p)) + }; + } + } + Err(Error::CouldNotMapToRealPath(virtual_path.as_ref().into())) + } + + pub async fn set_mounts(&self, mount_dirs: Vec<MountDir>) { + self.config.write().await.mount_dirs = mount_dirs; + // TODO persistence + } +} + +fn sanitize_path(source: &str) -> PathBuf { + let separator_regex = Regex::new(r"\\|/").unwrap(); + let mut correct_separator = String::new(); + correct_separator.push(std::path::MAIN_SEPARATOR); + let path_string = separator_regex.replace_all(source, correct_separator.as_str()); + PathBuf::from(path_string.deref()) +} + +fn hash_password(password: &str) -> Result<String, Error> { + if password.is_empty() { + return Err(Error::EmptyPassword); + } + let salt = SaltString::generate(&mut OsRng); + match Pbkdf2.hash_password(password.as_bytes(), &salt) { + Ok(h) => Ok(h.to_string()), + Err(_) => Err(Error::PasswordHashing), + } } #[cfg(test)] @@ -102,82 +197,121 @@ mod test { use crate::test_name; #[tokio::test] - async fn apply_saves_misc_settings() { + async fn can_apply_config() { let ctx = test::ContextBuilder::new(test_name!()).build().await; let new_config = Config { - settings: Some(settings::NewSettings { - album_art_pattern: Some("🖼️\\.jpg".into()), - reindex_every_n_seconds: Some(100), - }), + reindex_every_n_seconds: Some(100), + album_art_pattern: Some("cool_pattern".to_owned()), + mount_dirs: vec![MountDir { + source: "/home/music".to_owned(), + name: "Library".to_owned(), + }], + ddns_url: Some("https://cooldns.com".to_owned()), + users: vec![], + }; + ctx.config_manager.apply(new_config.clone()).await.unwrap(); + assert_eq!(new_config, ctx.config_manager.config.read().await.clone(),); + } + + #[tokio::test] + async fn applying_config_adds_or_preserves_password_hashes() { + let ctx = test::ContextBuilder::new(test_name!()).build().await; + + let new_config = Config { + users: vec![ + User { + name: "walter".to_owned(), + initial_password: Some("super salmon 64".to_owned()), + ..Default::default() + }, + User { + name: "lara".to_owned(), + hashed_password: Some("hash".to_owned()), + ..Default::default() + }, + ], ..Default::default() }; - ctx.config_manager.apply(&new_config).await.unwrap(); - let settings = ctx.settings_manager.read().await.unwrap(); - let new_settings = new_config.settings.unwrap(); + ctx.config_manager.apply(new_config).await.unwrap(); + let actual_config = ctx.config_manager.config.read().await.clone(); + + assert_eq!(actual_config.users[0].name, "walter"); assert_eq!( - settings.index_album_art_pattern, - new_settings.album_art_pattern.unwrap() + actual_config.users[0].initial_password, + Some("super salmon 64".to_owned()) ); + assert!(actual_config.users[0].hashed_password.is_some()); + assert_eq!( - settings.index_sleep_duration_seconds, - new_settings.reindex_every_n_seconds.unwrap() + actual_config.users[1], + User { + name: "lara".to_owned(), + hashed_password: Some("hash".to_owned()), + ..Default::default() + } ); } - #[tokio::test] - async fn apply_saves_mount_points() { - let ctx = test::ContextBuilder::new(test_name!()).build().await; - - let new_config = Config { - mount_dirs: Some(vec![vfs::MountDir { - source: "/home/music".into(), - name: "🎵📁".into(), - }]), - ..Default::default() - }; - - ctx.config_manager.apply(&new_config).await.unwrap(); - let actual_mount_dirs: Vec<vfs::MountDir> = ctx.vfs_manager.mount_dirs().await.unwrap(); - assert_eq!(actual_mount_dirs, new_config.mount_dirs.unwrap()); + #[test] + fn converts_virtual_to_real() { + let vfs = VFS::new(vec![Mount { + name: "root".to_owned(), + source: Path::new("test_dir").to_owned(), + }]); + let real_path: PathBuf = ["test_dir", "somewhere", "something.png"].iter().collect(); + let virtual_path: PathBuf = ["root", "somewhere", "something.png"].iter().collect(); + let converted_path = vfs.virtual_to_real(virtual_path.as_path()).unwrap(); + assert_eq!(converted_path, real_path); } - #[tokio::test] - async fn apply_saves_ddns_settings() { - let ctx = test::ContextBuilder::new(test_name!()).build().await; - - let new_config = Config { - ydns: Some(ddns::Config { - ddns_host: "🐸🐸🐸.ydns.eu".into(), - ddns_username: "kfr🐸g".into(), - ddns_password: "tasty🐞".into(), - }), - ..Default::default() - }; - - ctx.config_manager.apply(&new_config).await.unwrap(); - let actual_ddns = ctx.ddns_manager.config().await.unwrap(); - assert_eq!(actual_ddns, new_config.ydns.unwrap()); + #[test] + fn converts_virtual_to_real_top_level() { + let vfs = VFS::new(vec![Mount { + name: "root".to_owned(), + source: Path::new("test_dir").to_owned(), + }]); + let real_path = Path::new("test_dir"); + let converted_path = vfs.virtual_to_real(Path::new("root")).unwrap(); + assert_eq!(converted_path, real_path); } - #[tokio::test] - async fn apply_can_toggle_admin() { - let ctx = test::ContextBuilder::new(test_name!()) - .user("Walter", "Tasty🍖", true) - .build() - .await; + #[test] + fn cleans_path_string() { + let mut correct_path = path::PathBuf::new(); + if cfg!(target_os = "windows") { + correct_path.push("C:\\"); + } else { + correct_path.push("/usr"); + } + correct_path.push("some"); + correct_path.push("path"); - assert!(ctx.user_manager.list().await.unwrap()[0].is_admin()); - - let new_config = Config { - users: Some(vec![user::NewUser { - name: "Walter".into(), - password: "Tasty🍖".into(), - admin: false, - }]), - ..Default::default() + let tests = if cfg!(target_os = "windows") { + vec![ + r#"C:/some/path"#, + r#"C:\some\path"#, + r#"C:\some\path\"#, + r#"C:\some\path\\\\"#, + r#"C:\some/path//"#, + ] + } else { + vec![ + r#"/usr/some/path"#, + r#"/usr\some\path"#, + r#"/usr\some\path\"#, + r#"/usr\some\path\\\\"#, + r#"/usr\some/path//"#, + ] }; - ctx.config_manager.apply(&new_config).await.unwrap(); - assert!(!ctx.user_manager.list().await.unwrap()[0].is_admin()); + + for test in tests { + let mount_dir = MountDir { + source: test.to_owned(), + name: "name".to_owned(), + }; + let mount: Mount = mount_dir.into(); + assert_eq!(mount.source, correct_path); + } } } diff --git a/src/app/scanner.rs b/src/app/scanner.rs index 559f0ae..5f62de2 100644 --- a/src/app/scanner.rs +++ b/src/app/scanner.rs @@ -10,7 +10,7 @@ use std::{cmp::min, time::Duration}; use tokio::sync::Notify; use tokio::time::Instant; -use crate::app::{formats, index, settings, vfs, Error}; +use crate::app::{config, formats, index, Error}; #[derive(Debug, PartialEq, Eq)] pub struct Directory { @@ -40,21 +40,18 @@ pub struct Song { #[derive(Clone)] pub struct Scanner { index_manager: index::Manager, - settings_manager: settings::Manager, - vfs_manager: vfs::Manager, + config_manager: config::Manager, pending_scan: Arc<Notify>, } impl Scanner { pub async fn new( index_manager: index::Manager, - settings_manager: settings::Manager, - vfs_manager: vfs::Manager, + config_manager: config::Manager, ) -> Result<Self, Error> { let scanner = Self { index_manager, - vfs_manager, - settings_manager, + config_manager, pending_scan: Arc::new(Notify::new()), }; @@ -83,14 +80,7 @@ impl Scanner { async move { loop { index.trigger_scan(); - let sleep_duration = index - .settings_manager - .get_index_sleep_duration() - .await - .unwrap_or_else(|e| { - error!("Could not retrieve index sleep duration: {}", e); - Duration::from_secs(1800) - }); + let sleep_duration = index.config_manager.get_index_sleep_duration().await; tokio::time::sleep(sleep_duration).await; } } @@ -104,22 +94,17 @@ impl Scanner { let was_empty = self.index_manager.is_index_empty().await; let mut partial_update_time = Instant::now(); - let album_art_pattern = self - .settings_manager - .get_index_album_art_pattern() - .await - .ok(); + let album_art_pattern = self.config_manager.get_index_album_art_pattern().await; + let album_art_regex = Regex::new(&format!("(?i){}", &album_art_pattern)).ok(); let (scan_directories_output, collection_directories_input) = channel(); let (scan_songs_output, collection_songs_input) = channel(); - let vfs = self.vfs_manager.get_vfs().await?; - let scan = Scan::new( scan_directories_output, scan_songs_output, - vfs.mounts().clone(), - album_art_pattern, + self.config_manager.get_mounts().await, + album_art_regex, ); let scan_task = tokio::task::spawn_blocking(|| scan.run()); @@ -203,7 +188,7 @@ impl Scanner { struct Scan { directories_output: Sender<Directory>, songs_output: Sender<Song>, - mounts: Vec<vfs::Mount>, + mounts: Vec<config::MountDir>, artwork_regex: Option<Regex>, } @@ -211,7 +196,7 @@ impl Scan { pub fn new( directories_output: Sender<Directory>, songs_output: Sender<Song>, - mounts: Vec<vfs::Mount>, + mounts: Vec<config::MountDir>, artwork_regex: Option<Regex>, ) -> Self { Self { @@ -371,9 +356,9 @@ mod test { async fn scan_finds_songs_and_directories() { let (directories_sender, directories_receiver) = channel(); let (songs_sender, songs_receiver) = channel(); - let mounts = vec![vfs::Mount { - source: PathBuf::from_iter(["test-data", "small-collection"]), - name: "root".to_string(), + let mounts = vec![config::MountDir { + source: "test-data/small-collection".to_owned(), + name: "root".to_owned(), }]; let artwork_regex = None; @@ -391,9 +376,9 @@ mod test { async fn scan_finds_embedded_artwork() { let (directories_sender, _) = channel(); let (songs_sender, songs_receiver) = channel(); - let mounts = vec![vfs::Mount { - source: PathBuf::from_iter(["test-data", "small-collection"]), - name: "root".to_string(), + let mounts = vec![config::MountDir { + source: "test-data/small-collection".to_owned(), + name: "root".to_owned(), }]; let artwork_regex = None; @@ -414,9 +399,9 @@ mod test { for pattern in patterns.into_iter() { let (directories_sender, _) = channel(); let (songs_sender, songs_receiver) = channel(); - let mounts = vec![vfs::Mount { - source: PathBuf::from_iter(["test-data", "small-collection"]), - name: "root".to_string(), + let mounts = vec![config::MountDir { + source: "test-data/small-collection".to_owned(), + name: "root".to_owned(), }]; let artwork_regex = Some(Regex::new(pattern).unwrap()); diff --git a/src/app/settings.rs b/src/app/settings.rs deleted file mode 100644 index bd5bb1d..0000000 --- a/src/app/settings.rs +++ /dev/null @@ -1,90 +0,0 @@ -use regex::Regex; -use serde::Deserialize; -use std::time::Duration; - -use crate::app::Error; -use crate::db::DB; - -#[derive(Clone, Default)] -pub struct AuthSecret { - pub key: [u8; 32], -} - -#[derive(Debug)] -pub struct Settings { - pub index_sleep_duration_seconds: i64, - pub index_album_art_pattern: String, -} - -#[derive(Debug, Default, Deserialize)] -pub struct NewSettings { - pub reindex_every_n_seconds: Option<i64>, - pub album_art_pattern: Option<String>, -} - -#[derive(Clone)] -pub struct Manager { - pub db: DB, -} - -impl Manager { - pub fn new(db: DB) -> Self { - Self { db } - } - - pub async fn get_auth_secret(&self) -> Result<AuthSecret, Error> { - sqlx::query_scalar!("SELECT auth_secret FROM config") - .fetch_one(self.db.connect().await?.as_mut()) - .await? - .try_into() - .map_err(|_| Error::AuthenticationSecretInvalid) - .map(|key| AuthSecret { key }) - } - - pub async fn get_index_sleep_duration(&self) -> Result<Duration, Error> { - let settings = self.read().await?; - Ok(Duration::from_secs( - settings.index_sleep_duration_seconds as u64, - )) - } - - pub async fn get_index_album_art_pattern(&self) -> Result<Regex, Error> { - let settings = self.read().await?; - let regex = Regex::new(&format!("(?i){}", &settings.index_album_art_pattern)) - .map_err(|_| Error::IndexAlbumArtPatternInvalid)?; - Ok(regex) - } - - pub async fn read(&self) -> Result<Settings, Error> { - Ok(sqlx::query_as!( - Settings, - "SELECT index_sleep_duration_seconds,index_album_art_pattern FROM config" - ) - .fetch_one(self.db.connect().await?.as_mut()) - .await?) - } - - pub async fn amend(&self, new_settings: &NewSettings) -> Result<(), Error> { - let mut connection = self.db.connect().await?; - - if let Some(sleep_duration) = new_settings.reindex_every_n_seconds { - sqlx::query!( - "UPDATE config SET index_sleep_duration_seconds = $1", - sleep_duration - ) - .execute(connection.as_mut()) - .await?; - } - - if let Some(ref album_art_pattern) = new_settings.album_art_pattern { - sqlx::query!( - "UPDATE config SET index_album_art_pattern = $1", - album_art_pattern - ) - .execute(connection.as_mut()) - .await?; - } - - Ok(()) - } -} diff --git a/src/app/test.rs b/src/app/test.rs index 0457d74..6f888a3 100644 --- a/src/app/test.rs +++ b/src/app/test.rs @@ -1,18 +1,15 @@ use std::path::PathBuf; -use crate::app::{config, ddns, index, ndb, playlist, scanner, settings, user, vfs}; -use crate::db::DB; +use crate::app::{config, index, ndb, playlist, scanner}; use crate::test::*; +use super::config::AuthSecret; + pub struct Context { pub index_manager: index::Manager, pub scanner: scanner::Scanner, pub config_manager: config::Manager, - pub ddns_manager: ddns::Manager, pub playlist_manager: playlist::Manager, - pub settings_manager: settings::Manager, - pub user_manager: user::Manager, - pub vfs_manager: vfs::Manager, } pub struct ContextBuilder { @@ -29,64 +26,41 @@ impl ContextBuilder { } pub fn user(mut self, name: &str, password: &str, is_admin: bool) -> Self { - self.config - .users - .get_or_insert(Vec::new()) - .push(user::NewUser { - name: name.to_owned(), - password: password.to_owned(), - admin: is_admin, - }); + self.config.users.push(config::User { + name: name.to_owned(), + initial_password: Some(password.to_owned()), + admin: Some(is_admin), + ..Default::default() + }); self } pub fn mount(mut self, name: &str, source: &str) -> Self { - self.config - .mount_dirs - .get_or_insert(Vec::new()) - .push(vfs::MountDir { - name: name.to_owned(), - source: source.to_owned(), - }); + self.config.mount_dirs.push(config::MountDir { + name: name.to_owned(), + source: source.to_owned(), + }); self } pub async fn build(self) -> Context { - let db_path = self.test_directory.join("db.sqlite"); + let config_path = self.test_directory.join("polaris.toml"); - let db = DB::new(&db_path).await.unwrap(); + let auth_secret = AuthSecret::default(); + let config_manager = config::Manager::new(&config_path).await.unwrap(); let ndb_manager = ndb::Manager::new(&self.test_directory).unwrap(); - let settings_manager = settings::Manager::new(db.clone()); - let auth_secret = settings_manager.get_auth_secret().await.unwrap(); - let user_manager = user::Manager::new(db.clone(), auth_secret); - let vfs_manager = vfs::Manager::new(db.clone()); - let ddns_manager = ddns::Manager::new(db.clone()); - let config_manager = config::Manager::new( - settings_manager.clone(), - user_manager.clone(), - vfs_manager.clone(), - ddns_manager.clone(), - ); let index_manager = index::Manager::new(&self.test_directory).await.unwrap(); - let scanner = scanner::Scanner::new( - index_manager.clone(), - settings_manager.clone(), - vfs_manager.clone(), - ) - .await - .unwrap(); + let scanner = scanner::Scanner::new(index_manager.clone(), config_manager.clone()) + .await + .unwrap(); let playlist_manager = playlist::Manager::new(ndb_manager.clone()); - config_manager.apply(&self.config).await.unwrap(); + config_manager.apply(self.config).await.unwrap(); Context { index_manager, scanner, config_manager, - ddns_manager, playlist_manager, - settings_manager, - user_manager, - vfs_manager, } } } diff --git a/src/app/vfs.rs b/src/app/vfs.rs deleted file mode 100644 index 8eb5006..0000000 --- a/src/app/vfs.rs +++ /dev/null @@ -1,178 +0,0 @@ -use core::ops::Deref; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use sqlx::{Acquire, QueryBuilder, Sqlite}; -use std::path::{self, Path, PathBuf}; - -use crate::app::Error; -use crate::db::DB; - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] -pub struct MountDir { - pub source: String, - pub name: String, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] -pub struct Mount { - pub source: PathBuf, - pub name: String, -} - -impl From<MountDir> for Mount { - fn from(m: MountDir) -> Self { - let separator_regex = Regex::new(r"\\|/").unwrap(); - let mut correct_separator = String::new(); - correct_separator.push(path::MAIN_SEPARATOR); - let path_string = separator_regex.replace_all(&m.source, correct_separator.as_str()); - let source = PathBuf::from(path_string.deref()); - Self { - name: m.name, - source, - } - } -} - -#[allow(clippy::upper_case_acronyms)] -pub struct VFS { - mounts: Vec<Mount>, -} - -impl VFS { - pub fn new(mounts: Vec<Mount>) -> VFS { - VFS { mounts } - } - - pub fn virtual_to_real<P: AsRef<Path>>(&self, virtual_path: P) -> Result<PathBuf, Error> { - for mount in &self.mounts { - let mount_path = Path::new(&mount.name); - if let Ok(p) = virtual_path.as_ref().strip_prefix(mount_path) { - return if p.components().count() == 0 { - Ok(mount.source.clone()) - } else { - Ok(mount.source.join(p)) - }; - } - } - Err(Error::CouldNotMapToRealPath(virtual_path.as_ref().into())) - } - - pub fn mounts(&self) -> &Vec<Mount> { - &self.mounts - } -} - -#[derive(Clone)] -pub struct Manager { - db: DB, -} - -impl Manager { - pub fn new(db: DB) -> Self { - Self { db } - } - - pub async fn get_vfs(&self) -> Result<VFS, Error> { - let mount_dirs = self.mount_dirs().await?; - let mounts = mount_dirs.into_iter().map(|p| p.into()).collect(); - Ok(VFS::new(mounts)) - } - - pub async fn mount_dirs(&self) -> Result<Vec<MountDir>, Error> { - Ok( - sqlx::query_as!(MountDir, "SELECT source, name FROM mount_points") - .fetch_all(self.db.connect().await?.as_mut()) - .await?, - ) - } - - pub async fn set_mount_dirs(&self, mount_dirs: &[MountDir]) -> Result<(), Error> { - let mut connection = self.db.connect().await?; - - connection.begin().await?; - - sqlx::query!("DELETE FROM mount_points") - .execute(connection.as_mut()) - .await?; - - if !mount_dirs.is_empty() { - QueryBuilder::<Sqlite>::new("INSERT INTO mount_points(source, name) ") - .push_values(mount_dirs, |mut b, dir| { - b.push_bind(&dir.source).push_bind(&dir.name); - }) - .build() - .execute(connection.as_mut()) - .await?; - } - - Ok(()) - } -} - -#[cfg(test)] -mod test { - - use super::*; - - #[test] - fn converts_virtual_to_real() { - let vfs = VFS::new(vec![Mount { - name: "root".to_owned(), - source: Path::new("test_dir").to_owned(), - }]); - let real_path: PathBuf = ["test_dir", "somewhere", "something.png"].iter().collect(); - let virtual_path: PathBuf = ["root", "somewhere", "something.png"].iter().collect(); - let converted_path = vfs.virtual_to_real(virtual_path.as_path()).unwrap(); - assert_eq!(converted_path, real_path); - } - - #[test] - fn converts_virtual_to_real_top_level() { - let vfs = VFS::new(vec![Mount { - name: "root".to_owned(), - source: Path::new("test_dir").to_owned(), - }]); - let real_path = Path::new("test_dir"); - let converted_path = vfs.virtual_to_real(Path::new("root")).unwrap(); - assert_eq!(converted_path, real_path); - } - - #[test] - fn cleans_path_string() { - let mut correct_path = path::PathBuf::new(); - if cfg!(target_os = "windows") { - correct_path.push("C:\\"); - } else { - correct_path.push("/usr"); - } - correct_path.push("some"); - correct_path.push("path"); - - let tests = if cfg!(target_os = "windows") { - vec![ - r#"C:/some/path"#, - r#"C:\some\path"#, - r#"C:\some\path\"#, - r#"C:\some\path\\\\"#, - r#"C:\some/path//"#, - ] - } else { - vec![ - r#"/usr/some/path"#, - r#"/usr\some\path"#, - r#"/usr\some\path\"#, - r#"/usr\some\path\\\\"#, - r#"/usr\some/path//"#, - ] - }; - - for test in tests { - let mount_dir = MountDir { - source: test.to_owned(), - name: "name".to_owned(), - }; - let mount: Mount = mount_dir.into(); - assert_eq!(mount.source, correct_path); - } - } -} diff --git a/src/db.rs b/src/db.rs deleted file mode 100644 index b5c5706..0000000 --- a/src/db.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::path::Path; - -use sqlx::{ - migrate::Migrator, - pool::PoolConnection, - sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqliteSynchronous}, - Sqlite, -}; - -use crate::app::Error; - -static MIGRATOR: Migrator = sqlx::migrate!("src/db"); - -#[derive(Clone)] -pub struct DB { - pool: SqlitePool, -} - -impl DB { - pub async fn new(path: &Path) -> Result<DB, Error> { - let directory = path.parent().unwrap(); - std::fs::create_dir_all(directory).map_err(|e| Error::Io(directory.to_owned(), e))?; - - let pool = SqlitePool::connect_lazy_with( - SqliteConnectOptions::new() - .create_if_missing(true) - .filename(path) - .journal_mode(SqliteJournalMode::Wal) - .synchronous(SqliteSynchronous::Normal), - ); - - let db = DB { pool }; - db.migrate_up().await?; - Ok(db) - } - - pub async fn connect(&self) -> Result<PoolConnection<Sqlite>, Error> { - self.pool.acquire().await.map_err(|_| Error::ConnectionPool) - } - - async fn migrate_up(&self) -> Result<(), Error> { - MIGRATOR - .run(&self.pool) - .await - .and(Ok(())) - .map_err(Error::Migration) - } -} - -#[tokio::test] -async fn run_migrations() { - use crate::test::*; - use crate::test_name; - let output_dir = prepare_test_directory(test_name!()); - let db_path = output_dir.join("db.sqlite"); - let db = DB::new(&db_path).await.unwrap(); - db.migrate_up().await.unwrap(); -} diff --git a/src/db/20240711080449_init.sql b/src/db/20240711080449_init.sql deleted file mode 100644 index a6eb629..0000000 --- a/src/db/20240711080449_init.sql +++ /dev/null @@ -1,51 +0,0 @@ -CREATE TABLE config ( - id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0), - auth_secret BLOB NOT NULL DEFAULT (randomblob(32)), - index_sleep_duration_seconds INTEGER NOT NULL, - index_album_art_pattern TEXT NOT NULL, - ddns_host TEXT NOT NULL, - ddns_username TEXT NOT NULL, - ddns_password TEXT NOT NULL -); - -INSERT INTO config ( - id, - auth_secret, - index_sleep_duration_seconds, - index_album_art_pattern, - ddns_host, - ddns_username, - ddns_password -) VALUES ( - 0, - randomblob(32), - 1800, - "Folder.(jpeg|jpg|png)", - "", - "", - "" -); - -CREATE TABLE mount_points ( - id INTEGER PRIMARY KEY NOT NULL, - source TEXT NOT NULL, - name TEXT NOT NULL, - UNIQUE(name) -); - -CREATE TABLE users ( - id INTEGER PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - password_hash TEXT NOT NULL, - admin INTEGER NOT NULL, - web_theme_base TEXT, - web_theme_accent TEXT, - UNIQUE(name) -); - -CREATE TABLE collection_index ( - id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0), - content BLOB -); - -INSERT INTO collection_index (id, content) VALUES (0, NULL); diff --git a/src/main.rs b/src/main.rs index 6b50451..cd2dc43 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,6 @@ use std::fs; use std::path::{Path, PathBuf}; mod app; -mod db; mod options; mod paths; mod server; diff --git a/src/paths.rs b/src/paths.rs index d2133c9..70e1cda 100644 --- a/src/paths.rs +++ b/src/paths.rs @@ -4,7 +4,7 @@ use crate::options::CLIOptions; pub struct Paths { pub cache_dir_path: PathBuf, - pub config_file_path: Option<PathBuf>, + pub config_file_path: PathBuf, pub data_dir_path: PathBuf, pub db_file_path: PathBuf, pub log_file_path: Option<PathBuf>, @@ -21,7 +21,7 @@ impl Default for Paths { fn default() -> Self { Self { cache_dir_path: ["."].iter().collect(), - config_file_path: None, + config_file_path: [".", "polaris.toml"].iter().collect(), data_dir_path: ["."].iter().collect(), db_file_path: [".", "db.sqlite"].iter().collect(), log_file_path: Some([".", "polaris.log"].iter().collect()), @@ -40,7 +40,7 @@ impl Default for Paths { local_app_data.join(["Permafrost", "Polaris"].iter().collect::<PathBuf>()); Self { cache_dir_path: install_directory.clone(), - config_file_path: None, + config_file_path: install_directory.join("polaris.toml"), data_dir_path: install_directory.clone(), db_file_path: install_directory.join("db.sqlite"), log_file_path: Some(install_directory.join("polaris.log")), @@ -58,10 +58,12 @@ impl Paths { .map(PathBuf::from) .map(|p| p.join("db.sqlite")) .unwrap_or(defaults.db_file_path), - config_file_path: None, cache_dir_path: option_env!("POLARIS_CACHE_DIR") .map(PathBuf::from) .unwrap_or(defaults.cache_dir_path), + config_file_path: option_env!("POLARIS_CONFIG_DIR") + .map(|p| [p, "polaris.toml"].iter().collect()) + .unwrap_or(defaults.config_file_path), data_dir_path: option_env!("POLARIS_DATA_DIR") .map(PathBuf::from) .unwrap_or(defaults.data_dir_path), @@ -89,7 +91,7 @@ impl Paths { path.clone_into(&mut paths.cache_dir_path); } if let Some(path) = &cli_options.config_file_path { - paths.config_file_path = Some(path.clone()); + path.clone_into(&mut paths.config_file_path); } if let Some(path) = &cli_options.data_dir_path { path.clone_into(&mut paths.data_dir_path); diff --git a/src/server/axum.rs b/src/server/axum.rs index dedc3cb..eca74c2 100644 --- a/src/server/axum.rs +++ b/src/server/axum.rs @@ -59,12 +59,6 @@ impl FromRef<App> for app::config::Manager { } } -impl FromRef<App> for app::ddns::Manager { - fn from_ref(app: &App) -> Self { - app.ddns_manager.clone() - } -} - impl FromRef<App> for app::peaks::Manager { fn from_ref(app: &App) -> Self { app.peaks_manager.clone() @@ -77,26 +71,8 @@ impl FromRef<App> for app::playlist::Manager { } } -impl FromRef<App> for app::user::Manager { - fn from_ref(app: &App) -> Self { - app.user_manager.clone() - } -} - -impl FromRef<App> for app::settings::Manager { - fn from_ref(app: &App) -> Self { - app.settings_manager.clone() - } -} - impl FromRef<App> for app::thumbnail::Manager { fn from_ref(app: &App) -> Self { app.thumbnail_manager.clone() } } - -impl FromRef<App> for app::vfs::Manager { - fn from_ref(app: &App) -> Self { - app.vfs_manager.clone() - } -} diff --git a/src/server/axum/api.rs b/src/server/axum/api.rs index e39ca65..27c2ff7 100644 --- a/src/server/axum/api.rs +++ b/src/server/axum/api.rs @@ -12,7 +12,7 @@ use axum_range::{KnownSize, Ranged}; use tower_http::{compression::CompressionLayer, CompressionLevel}; use crate::{ - app::{ddns, index, peaks, playlist, scanner, settings, thumbnail, user, vfs, App}, + app::{config, index, peaks, playlist, scanner, thumbnail, App}, server::{ dto, error::APIError, APIMajorVersion, API_ARRAY_SEPARATOR, API_MAJOR_VERSION, API_MINOR_VERSION, @@ -32,8 +32,6 @@ pub fn router() -> Router<App> { .route("/settings", put(put_settings)) .route("/mount_dirs", get(get_mount_dirs)) .route("/mount_dirs", put(put_mount_dirs)) - .route("/ddns", get(get_ddns)) - .route("/ddns", put(put_ddns)) .route("/trigger_index", post(post_trigger_index)) // User management .route("/user", post(post_user)) @@ -88,11 +86,11 @@ async fn get_version() -> Json<dto::Version> { } async fn get_initial_setup( - State(user_manager): State<user::Manager>, + State(config_manager): State<config::Manager>, ) -> Result<Json<dto::InitialSetup>, APIError> { let initial_setup = { - let users = user_manager.list().await?; - let has_any_admin = users.iter().any(|u| u.is_admin()); + let users = config_manager.get_users().await; + let has_any_admin = users.iter().any(|u| u.admin == Some(true)); dto::InitialSetup { has_any_users: has_any_admin, } @@ -101,16 +99,23 @@ async fn get_initial_setup( } async fn get_settings( - State(settings_manager): State<settings::Manager>, _admin_rights: AdminRights, + State(config_manager): State<config::Manager>, ) -> Result<Json<dto::Settings>, APIError> { - let settings = settings_manager.read().await?; - Ok(Json(settings.into())) + let settings = dto::Settings { + album_art_pattern: config_manager.get_index_album_art_pattern().await, + reindex_every_n_seconds: config_manager.get_index_sleep_duration().await.as_secs(), + ddns_update_url: config_manager + .get_ddns_update_url() + .await + .unwrap_or_default(), + }; + Ok(Json(settings)) } async fn put_settings( _admin_rights: AdminRights, - State(settings_manager): State<settings::Manager>, + State(config_manager): State<config::Manager>, Json(new_settings): Json<dto::NewSettings>, ) -> Result<(), APIError> { settings_manager @@ -121,43 +126,26 @@ async fn put_settings( async fn get_mount_dirs( _admin_rights: AdminRights, - State(vfs_manager): State<vfs::Manager>, + State(config_manager): State<config::Manager>, ) -> Result<Json<Vec<dto::MountDir>>, APIError> { - let mount_dirs = vfs_manager.mount_dirs().await?; + let mount_dirs = config_manager.get_mounts().await; let mount_dirs = mount_dirs.into_iter().map(|m| m.into()).collect(); Ok(Json(mount_dirs)) } async fn put_mount_dirs( _admin_rights: AdminRights, - State(vfs_manager): State<vfs::Manager>, + State(config_manager): State<config::Manager>, new_mount_dirs: Json<Vec<dto::MountDir>>, ) -> Result<(), APIError> { - let new_mount_dirs: Vec<vfs::MountDir> = + let new_mount_dirs: Vec<config::MountDir> = new_mount_dirs.iter().cloned().map(|m| m.into()).collect(); - vfs_manager.set_mount_dirs(&new_mount_dirs).await?; - Ok(()) -} - -async fn get_ddns( - _admin_rights: AdminRights, - State(ddns_manager): State<ddns::Manager>, -) -> Result<Json<dto::DDNSConfig>, APIError> { - let ddns_config = ddns_manager.config().await?; - Ok(Json(ddns_config.into())) -} - -async fn put_ddns( - _admin_rights: AdminRights, - State(ddns_manager): State<ddns::Manager>, - Json(new_ddns_config): Json<dto::DDNSConfig>, -) -> Result<(), APIError> { - ddns_manager.set_config(&new_ddns_config.into()).await?; + config_manager.set_mounts(new_mount_dirs).await; Ok(()) } async fn post_auth( - State(user_manager): State<user::Manager>, + State(config_manager): State<config::Manager>, credentials: Json<dto::Credentials>, ) -> Result<Json<dto::Authorization>, APIError> { let username = credentials.username.clone(); @@ -178,16 +166,16 @@ async fn post_auth( async fn get_users( _admin_rights: AdminRights, - State(user_manager): State<user::Manager>, + State(config_manager): State<config::Manager>, ) -> Result<Json<Vec<dto::User>>, APIError> { - let users = user_manager.list().await?; + let users = config_manager.get_users().await; let users = users.into_iter().map(|u| u.into()).collect(); Ok(Json(users)) } async fn post_user( _admin_rights: AdminRights, - State(user_manager): State<user::Manager>, + State(config_manager): State<config::Manager>, Json(new_user): Json<dto::NewUser>, ) -> Result<(), APIError> { user_manager.create(&new_user.into()).await?; @@ -196,7 +184,7 @@ async fn post_user( async fn put_user( admin_rights: AdminRights, - State(user_manager): State<user::Manager>, + State(config_manager): State<config::Manager>, Path(name): Path<String>, user_update: Json<dto::UserUpdate>, ) -> Result<(), APIError> { @@ -219,7 +207,7 @@ async fn put_user( async fn delete_user( admin_rights: AdminRights, - State(user_manager): State<user::Manager>, + State(config_manager): State<config::Manager>, Path(name): Path<String>, ) -> Result<(), APIError> { if let Some(auth) = &admin_rights.get_auth() { @@ -227,22 +215,26 @@ async fn delete_user( return Err(APIError::DeletingOwnAccount); } } - user_manager.delete(&name).await?; + config_manager.delete_user(&name).await; Ok(()) } async fn get_preferences( auth: Auth, - State(user_manager): State<user::Manager>, -) -> Result<Json<user::Preferences>, APIError> { - let preferences = user_manager.read_preferences(auth.get_username()).await?; + State(config_manager): State<config::Manager>, +) -> Result<Json<dto::Preferences>, APIError> { + let user = config_manager.get_user(auth.get_username()).await?; + let preferences = dto::Preferences { + web_theme_base: user.web_theme_base, + web_theme_accent: user.web_theme_accent, + }; Ok(Json(preferences)) } async fn put_preferences( auth: Auth, - State(user_manager): State<user::Manager>, - Json(preferences): Json<user::Preferences>, + State(config_manager): State<config::Manager>, + Json(preferences): Json<dto::NewPreferences>, ) -> Result<(), APIError> { user_manager .write_preferences(auth.get_username(), &preferences) @@ -450,12 +442,11 @@ async fn get_songs( async fn get_peaks( _auth: Auth, - State(vfs_manager): State<vfs::Manager>, + State(config_manager): State<config::Manager>, State(peaks_manager): State<peaks::Manager>, Path(path): Path<PathBuf>, ) -> Result<dto::Peaks, APIError> { - let vfs = vfs_manager.get_vfs().await?; - let audio_path = vfs.virtual_to_real(&path)?; + let audio_path = config_manager.resolve_virtual_path(&path).await?; let peaks = peaks_manager.get_peaks(&audio_path).await?; Ok(peaks.interleaved) } @@ -662,12 +653,11 @@ async fn delete_playlist( async fn get_audio( _auth: Auth, - State(vfs_manager): State<vfs::Manager>, + State(config_manager): State<config::Manager>, Path(path): Path<PathBuf>, range: Option<TypedHeader<Range>>, ) -> Result<impl IntoResponse, APIError> { - let vfs = vfs_manager.get_vfs().await?; - let audio_path = vfs.virtual_to_real(&path)?; + let audio_path = config_manager.resolve_virtual_path(&path).await?; let Ok(file) = tokio::fs::File::open(audio_path).await else { return Err(APIError::AudioFileIOError); @@ -683,15 +673,14 @@ async fn get_audio( async fn get_thumbnail( _auth: Auth, - State(vfs_manager): State<vfs::Manager>, + State(config_manager): State<config::Manager>, State(thumbnails_manager): State<thumbnail::Manager>, Path(path): Path<PathBuf>, Query(options_input): Query<dto::ThumbnailOptions>, range: Option<TypedHeader<Range>>, ) -> Result<impl IntoResponse, APIError> { let options = thumbnail::Options::from(options_input); - let vfs = vfs_manager.get_vfs().await?; - let image_path = vfs.virtual_to_real(&path)?; + let image_path = config_manager.resolve_virtual_path(&path).await?; let thumbnail_path = thumbnails_manager .get_thumbnail(&image_path, &options) diff --git a/src/server/axum/auth.rs b/src/server/axum/auth.rs index de816db..eddb5ff 100644 --- a/src/server/axum/auth.rs +++ b/src/server/axum/auth.rs @@ -6,7 +6,7 @@ use headers::authorization::{Bearer, Credentials}; use http::request::Parts; use crate::{ - app::user, + app::config, server::{dto, error::APIError}, }; @@ -24,13 +24,13 @@ impl Auth { #[async_trait] impl<S> FromRequestParts<S> for Auth where - user::Manager: FromRef<S>, + config::Manager: FromRef<S>, S: Send + Sync, { type Rejection = APIError; async fn from_request_parts(parts: &mut Parts, app: &S) -> Result<Self, Self::Rejection> { - let user_manager = user::Manager::from_ref(app); + let config_manager = config::Manager::from_ref(app); let header_token = parts .headers @@ -73,13 +73,13 @@ impl AdminRights { #[async_trait] impl<S> FromRequestParts<S> for AdminRights where - user::Manager: FromRef<S>, + config::Manager: FromRef<S>, S: Send + Sync, { type Rejection = APIError; async fn from_request_parts(parts: &mut Parts, app: &S) -> Result<Self, Self::Rejection> { - let user_manager = user::Manager::from_ref(app); + let config_manager = config::Manager::from_ref(app); let user_count = user_manager.count().await?; if user_count == 0 { diff --git a/src/server/axum/error.rs b/src/server/axum/error.rs index c19de67..4c7a5ba 100644 --- a/src/server/axum/error.rs +++ b/src/server/axum/error.rs @@ -19,7 +19,6 @@ impl IntoResponse for APIError { StatusCode::from_u16(s).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR) } APIError::NativeDatabase(_) => StatusCode::INTERNAL_SERVER_ERROR, - APIError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, APIError::DeletingOwnAccount => StatusCode::CONFLICT, APIError::DirectoryNotFound(_) => StatusCode::NOT_FOUND, APIError::ArtistNotFound => StatusCode::NOT_FOUND, diff --git a/src/server/axum/test.rs b/src/server/axum/test.rs index dba2266..01eb99c 100644 --- a/src/server/axum/test.rs +++ b/src/server/axum/test.rs @@ -23,7 +23,7 @@ impl TestService for AxumTestService { let paths = Paths { cache_dir_path: ["test-output", test_name].iter().collect(), - config_file_path: None, + config_file_path: output_dir.join("polaris.toml"), data_dir_path: ["test-output", test_name].iter().collect(), db_file_path: output_dir.join("db.sqlite"), #[cfg(unix)] diff --git a/src/server/dto/v8.rs b/src/server/dto/v8.rs index ad43fde..fe8760f 100644 --- a/src/server/dto/v8.rs +++ b/src/server/dto/v8.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::app::{config, ddns, index, peaks, playlist, settings, thumbnail, user, vfs}; +use crate::app::{config, index, peaks, playlist, thumbnail, user}; use std::{collections::HashMap, convert::From, path::PathBuf}; #[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -111,11 +111,11 @@ pub struct User { pub is_admin: bool, } -impl From<user::User> for User { - fn from(u: user::User) -> Self { +impl From<config::User> for User { + fn from(u: config::User) -> Self { Self { name: u.name, - is_admin: u.admin != 0, + is_admin: u.admin == Some(true), } } } @@ -143,31 +143,16 @@ pub struct UserUpdate { pub new_is_admin: Option<bool>, } -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] -pub struct DDNSConfig { - pub host: String, - pub username: String, - pub password: String, +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct Preferences { + pub web_theme_base: Option<String>, + pub web_theme_accent: Option<String>, } -impl From<DDNSConfig> for ddns::Config { - fn from(c: DDNSConfig) -> Self { - Self { - ddns_host: c.host, - ddns_username: c.username, - ddns_password: c.password, - } - } -} - -impl From<ddns::Config> for DDNSConfig { - fn from(c: ddns::Config) -> Self { - Self { - host: c.ddns_host, - username: c.ddns_username, - password: c.ddns_password, - } - } +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct NewPreferences { + pub web_theme_base: Option<String>, + pub web_theme_accent: Option<String>, } #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] @@ -176,7 +161,7 @@ pub struct MountDir { pub name: String, } -impl From<MountDir> for vfs::MountDir { +impl From<MountDir> for config::MountDir { fn from(m: MountDir) -> Self { Self { name: m.name, @@ -185,8 +170,8 @@ impl From<MountDir> for vfs::MountDir { } } -impl From<vfs::MountDir> for MountDir { - fn from(m: vfs::MountDir) -> Self { +impl From<config::MountDir> for MountDir { + fn from(m: config::MountDir) -> Self { Self { name: m.name, source: m.source, @@ -194,55 +179,18 @@ impl From<vfs::MountDir> for MountDir { } } -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct Config { - pub settings: Option<NewSettings>, - pub users: Option<Vec<NewUser>>, - pub mount_dirs: Option<Vec<MountDir>>, - pub ydns: Option<DDNSConfig>, -} - -impl From<Config> for config::Config { - fn from(s: Config) -> Self { - Self { - settings: s.settings.map(|s| s.into()), - mount_dirs: s - .mount_dirs - .map(|v| v.into_iter().map(|m| m.into()).collect()), - users: s.users.map(|v| v.into_iter().map(|u| u.into()).collect()), - ydns: s.ydns.map(|c| c.into()), - } - } -} - #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct NewSettings { pub album_art_pattern: Option<String>, pub reindex_every_n_seconds: Option<i64>, -} - -impl From<NewSettings> for settings::NewSettings { - fn from(s: NewSettings) -> Self { - Self { - album_art_pattern: s.album_art_pattern, - reindex_every_n_seconds: s.reindex_every_n_seconds, - } - } + pub ddns_update_url: Option<String>, } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct Settings { pub album_art_pattern: String, - pub reindex_every_n_seconds: i64, -} - -impl From<settings::Settings> for Settings { - fn from(s: settings::Settings) -> Self { - Self { - album_art_pattern: s.index_album_art_pattern, - reindex_every_n_seconds: s.index_sleep_duration_seconds, - } - } + pub reindex_every_n_seconds: u64, + pub ddns_update_url: String, } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -513,5 +461,3 @@ pub struct GetRecentAlbumsParameters { pub offset: Option<usize>, pub count: Option<usize>, } - -// TODO: Preferences should have dto types diff --git a/src/server/error.rs b/src/server/error.rs index 04c8de9..901b150 100644 --- a/src/server/error.rs +++ b/src/server/error.rs @@ -21,8 +21,6 @@ pub enum APIError { AuthenticationRequired, #[error("Could not encode Branca token")] BrancaTokenEncoding, - #[error("Database error:\n\n{0}")] - Database(sqlx::Error), #[error("Native Database error:\n\n{0}")] NativeDatabase(native_db::db_type::Error), #[error("Directory not found: {0}")] @@ -110,11 +108,6 @@ impl From<app::Error> for APIError { app::Error::NativeDatabaseCreationError(_) => APIError::Internal, app::Error::NativeDatabase(e) => APIError::NativeDatabase(e), - app::Error::Database(e) => APIError::Database(e), - app::Error::ConnectionPoolBuild => APIError::Internal, - app::Error::ConnectionPool => APIError::Internal, - app::Error::Migration(_) => APIError::Internal, - app::Error::UpdateQueryFailed(s) => APIError::DdnsUpdateQueryFailed(s), app::Error::UpdateQueryTransport => APIError::DdnsUpdateQueryFailed(0), diff --git a/src/server/test/protocol.rs b/src/server/test/protocol.rs index d08d2ef..21faa4c 100644 --- a/src/server/test/protocol.rs +++ b/src/server/test/protocol.rs @@ -3,7 +3,7 @@ use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; use std::path::Path; use crate::server::dto; -use crate::{app::user, server::dto::ThumbnailSize}; +use crate::server::dto::ThumbnailSize; pub trait ProtocolVersion { fn header_value() -> i32; @@ -92,22 +92,6 @@ pub fn put_settings(settings: dto::NewSettings) -> Request<dto::NewSettings> { .unwrap() } -pub fn get_ddns_config() -> Request<()> { - Request::builder() - .method(Method::GET) - .uri("/api/ddns") - .body(()) - .unwrap() -} - -pub fn put_ddns_config(ddns_config: dto::DDNSConfig) -> Request<dto::DDNSConfig> { - Request::builder() - .method(Method::PUT) - .uri("/api/ddns") - .body(ddns_config) - .unwrap() -} - pub fn list_users() -> Request<()> { Request::builder() .method(Method::GET) @@ -148,7 +132,7 @@ pub fn get_preferences() -> Request<()> { .unwrap() } -pub fn put_preferences(preferences: user::Preferences) -> Request<user::Preferences> { +pub fn put_preferences(preferences: dto::NewPreferences) -> Request<dto::NewPreferences> { Request::builder() .method(Method::PUT) .uri("/api/preferences") diff --git a/src/server/test/user.rs b/src/server/test/user.rs index 1d49a9d..c05cd84 100644 --- a/src/server/test/user.rs +++ b/src/server/test/user.rs @@ -1,7 +1,6 @@ use http::StatusCode; use std::default::Default; -use crate::app::user; use crate::server::dto; use crate::server::test::{constants::*, protocol, ServiceType, TestService}; use crate::test_name; @@ -156,14 +155,14 @@ async fn get_preferences_golden_path() { service.login().await; let request = protocol::get_preferences(); - let response = service.fetch_json::<_, user::Preferences>(&request).await; + let response = service.fetch_json::<_, dto::Preferences>(&request).await; assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn put_preferences_requires_auth() { let mut service = ServiceType::new(&test_name!()).await; - let request = protocol::put_preferences(user::Preferences::default()); + let request = protocol::put_preferences(dto::NewPreferences::default()); let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -174,7 +173,7 @@ async fn put_preferences_golden_path() { service.complete_initial_setup().await; service.login().await; - let request = protocol::put_preferences(user::Preferences::default()); + let request = protocol::put_preferences(dto::NewPreferences::default()); let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); }