From 12a9f2ec3ceb7dd9f831efa4ff20e6f04b52115f Mon Sep 17 00:00:00 2001 From: Antoine Gersant <antoine.gersant@lesforges.org> Date: Sat, 13 Jul 2024 01:20:27 -0700 Subject: [PATCH] Diesel -> SQLx --- .env | 1 + .gitignore | 2 + Cargo.lock | 917 +++++++++++++++--- Cargo.toml | 19 +- diesel.toml | 2 - docs/MAINTENANCE.md | 5 - migrations/201706250006_init/down.sql | 2 - migrations/201706250006_init/up.sql | 25 - .../down.sql | 15 - .../up.sql | 1 - migrations/201706272129_users_table/down.sql | 1 - migrations/201706272129_users_table/up.sql | 8 - .../201706272304_misc_settings_table/down.sql | 1 - .../201706272304_misc_settings_table/up.sql | 7 - .../201706272313_ddns_config_table/down.sql | 1 - .../201706272313_ddns_config_table/up.sql | 8 - .../201706272327_mount_points_table/down.sql | 1 - .../201706272327_mount_points_table/up.sql | 6 - .../201707091522_playlists_tables/down.sql | 2 - .../201707091522_playlists_tables/up.sql | 16 - .../20170929203228_add_prefix_url/down.sql | 11 - .../20170929203228_add_prefix_url/up.sql | 1 - .../20171015224223_add_song_duration/down.sql | 19 - .../20171015224223_add_song_duration/up.sql | 1 - .../down.sql | 13 - .../up.sql | 2 - .../down.sql | 15 - .../2019-08-08-042731_blob_auth_secret/up.sql | 15 - .../2019-09-28-231910_pbkdf2_simple/down.sql | 11 - .../2019-09-28-231910_pbkdf2_simple/up.sql | 10 - .../2020-01-08-231420_add_theme/down.sql | 14 - migrations/2020-01-08-231420_add_theme/up.sql | 2 - .../down.sql | 1 - .../up.sql | 11 - .../2021-05-01-011426_add_lyricist/down.sql | 20 - .../2021-05-01-011426_add_lyricist/up.sql | 4 - src/app.rs | 14 +- src/app/config.rs | 71 +- src/app/ddns.rs | 81 +- src/app/index.rs | 79 +- src/app/index/query.rs | 178 ++-- src/app/index/test.rs | 171 ++-- src/app/index/types.rs | 22 +- src/app/index/update.rs | 44 +- src/app/index/update/cleaner.rs | 54 +- src/app/index/update/collector.rs | 9 +- src/app/index/update/inserter.rs | 117 ++- src/app/lastfm.rs | 22 +- src/app/playlist.rs | 337 +++---- src/app/settings.rs | 77 +- src/app/test.rs | 20 +- src/app/user.rs | 323 +++--- src/app/vfs.rs | 55 +- src/db.rs | 88 +- src/db/20240711080449_init.sql | 95 ++ src/db/schema.rs | 103 -- src/main.rs | 23 +- src/paths.rs | 10 +- src/service/actix.rs | 8 +- src/service/actix/api.rs | 269 +++-- src/service/actix/test.rs | 34 +- src/service/dto.rs | 16 +- src/service/error.rs | 2 +- src/service/test.rs | 40 +- src/service/test/admin.rs | 62 +- src/service/test/auth.rs | 78 +- src/service/test/collection.rs | 236 ++--- src/service/test/ddns.rs | 52 +- src/service/test/lastfm.rs | 54 +- src/service/test/media.rs | 142 +-- src/service/test/playlist.rs | 122 +-- src/service/test/settings.rs | 70 +- src/service/test/swagger.rs | 16 +- src/service/test/user.rs | 168 ++-- src/service/test/web.rs | 8 +- update_db_schema.bat | 6 - 76 files changed, 2482 insertions(+), 2084 deletions(-) create mode 100644 .env delete mode 100644 diesel.toml delete mode 100644 migrations/201706250006_init/down.sql delete mode 100644 migrations/201706250006_init/up.sql delete mode 100644 migrations/201706250228_directories_date_added/down.sql delete mode 100644 migrations/201706250228_directories_date_added/up.sql delete mode 100644 migrations/201706272129_users_table/down.sql delete mode 100644 migrations/201706272129_users_table/up.sql delete mode 100644 migrations/201706272304_misc_settings_table/down.sql delete mode 100644 migrations/201706272304_misc_settings_table/up.sql delete mode 100644 migrations/201706272313_ddns_config_table/down.sql delete mode 100644 migrations/201706272313_ddns_config_table/up.sql delete mode 100644 migrations/201706272327_mount_points_table/down.sql delete mode 100644 migrations/201706272327_mount_points_table/up.sql delete mode 100644 migrations/201707091522_playlists_tables/down.sql delete mode 100644 migrations/201707091522_playlists_tables/up.sql delete mode 100644 migrations/20170929203228_add_prefix_url/down.sql delete mode 100644 migrations/20170929203228_add_prefix_url/up.sql delete mode 100644 migrations/20171015224223_add_song_duration/down.sql delete mode 100644 migrations/20171015224223_add_song_duration/up.sql delete mode 100644 migrations/20180303211100_add_last_fm_credentials/down.sql delete mode 100644 migrations/20180303211100_add_last_fm_credentials/up.sql delete mode 100644 migrations/2019-08-08-042731_blob_auth_secret/down.sql delete mode 100644 migrations/2019-08-08-042731_blob_auth_secret/up.sql delete mode 100644 migrations/2019-09-28-231910_pbkdf2_simple/down.sql delete mode 100644 migrations/2019-09-28-231910_pbkdf2_simple/up.sql delete mode 100644 migrations/2020-01-08-231420_add_theme/down.sql delete mode 100644 migrations/2020-01-08-231420_add_theme/up.sql delete mode 100644 migrations/2020-11-25-174000_remove_prefix_url/down.sql delete mode 100644 migrations/2020-11-25-174000_remove_prefix_url/up.sql delete mode 100644 migrations/2021-05-01-011426_add_lyricist/down.sql delete mode 100644 migrations/2021-05-01-011426_add_lyricist/up.sql create mode 100644 src/db/20240711080449_init.sql delete mode 100644 src/db/schema.rs delete mode 100644 update_db_schema.bat diff --git a/.env b/.env new file mode 100644 index 0000000..1686fee --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=sqlite:./src/db/schema.sqlite \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0ae085c..dc1c932 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ TestConfig.toml # Runtime artifacts *.sqlite +**/*.sqlite-shm +**/*.sqlite-wal polaris.log polaris.pid /thumbnails diff --git a/Cargo.lock b/Cargo.lock index bb09eb5..771a891 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,7 +152,7 @@ dependencies = [ "futures-core", "futures-util", "mio", - "socket2 0.5.3", + "socket2 0.5.7", "tokio", "tracing", ] @@ -256,7 +256,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.5.3", + "socket2 0.5.7", "time 0.3.28", "url", ] @@ -305,14 +305,15 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -339,6 +340,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "ape" version = "0.5.0" @@ -354,6 +361,15 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -443,6 +459,9 @@ name = "bitflags" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +dependencies = [ + "serde", +] [[package]] name = "block-buffer" @@ -546,6 +565,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const_fn" version = "0.4.9" @@ -605,6 +630,21 @@ 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.3.2" @@ -649,14 +689,20 @@ dependencies = [ ] [[package]] -name = "crossbeam-utils" -version = "0.8.16" +name = "crossbeam-queue" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ - "cfg-if", + "crossbeam-utils", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "crypto-common" version = "0.1.6" @@ -682,6 +728,17 @@ 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.8" @@ -701,50 +758,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "diesel" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98235fdc2f355d330a8244184ab6b4b33c28679c0b4158f63138e51d6cf7e88" -dependencies = [ - "diesel_derives", - "libsqlite3-sys", - "r2d2", - "time 0.3.28", -] - -[[package]] -name = "diesel_derives" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e054665eaf6d97d1e7125512bb2d35d07c73ac86cc6920174cb42d1ab697a554" -dependencies = [ - "diesel_table_macro_syntax", - "proc-macro2", - "quote", - "syn 2.0.31", -] - -[[package]] -name = "diesel_migrations" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac" -dependencies = [ - "diesel", - "migrations_internals", - "migrations_macros", -] - -[[package]] -name = "diesel_table_macro_syntax" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" -dependencies = [ - "syn 2.0.31", -] - [[package]] name = "digest" version = "0.10.7" @@ -752,6 +765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -762,11 +776,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +dependencies = [ + "serde", +] [[package]] name = "embed-resource" @@ -796,6 +819,39 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[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 = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + [[package]] name = "fdeflate" version = "0.3.0" @@ -821,6 +877,17 @@ 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 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -842,11 +909,49 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" @@ -861,9 +966,9 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" @@ -878,9 +983,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", + "futures-io", "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -962,6 +1069,19 @@ name = "hashbrown" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.0", +] [[package]] name = "headers" @@ -987,6 +1107,15 @@ dependencies = [ "http", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.3.2" @@ -999,6 +1128,15 @@ 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" @@ -1008,6 +1146,15 @@ 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.9" @@ -1152,6 +1299,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "lewton" @@ -1171,16 +1321,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] -name = "libsqlite3-sys" -version = "0.26.0" +name = "libm" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" 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 = "local-channel" version = "0.1.3" @@ -1221,6 +1383,16 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[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 = "md5" version = "0.7.0" @@ -1253,27 +1425,6 @@ dependencies = [ "log", ] -[[package]] -name = "migrations_internals" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" -dependencies = [ - "serde", - "toml 0.7.8", -] - -[[package]] -name = "migrations_macros" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08" -dependencies = [ - "migrations_internals", - "proc-macro2", - "quote", -] - [[package]] name = "mime" version = "0.3.17" @@ -1290,6 +1441,12 @@ 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.1" @@ -1302,14 +1459,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1361,6 +1518,33 @@ dependencies = [ "winapi-build", ] +[[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-integer" version = "0.1.45" @@ -1371,6 +1555,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -1389,6 +1584,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1471,9 +1667,9 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1505,6 +1701,15 @@ 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.0" @@ -1523,6 +1728,27 @@ 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.27" @@ -1555,8 +1781,6 @@ dependencies = [ "branca", "crossbeam-channel", "daemonize", - "diesel", - "diesel_migrations", "embed-resource", "fs_extra", "futures-util", @@ -1566,7 +1790,6 @@ dependencies = [ "id3", "image", "lewton", - "libsqlite3-sys", "log", "metaflac", "mp3-duration", @@ -1586,7 +1809,9 @@ dependencies = [ "serde_derive", "serde_json", "simplelog", + "sqlx", "thiserror", + "tokio", "toml 0.7.8", "ureq 2.7.1", "url", @@ -1651,17 +1876,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r2d2" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" -dependencies = [ - "log", - "parking_lot", - "scheduled-thread-pool", -] - [[package]] name = "rand" version = "0.8.5" @@ -1723,6 +1937,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[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 = "regex" version = "1.9.5" @@ -1761,12 +1984,32 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", + "spin 0.5.2", "untrusted", "web-sys", "winapi", ] +[[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 = "rustc-demangle" version = "0.1.23" @@ -1804,6 +2047,19 @@ dependencies = [ "wrapped-vec", ] +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustls" version = "0.19.1" @@ -1855,15 +2111,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" -[[package]] -name = "scheduled-thread-pool" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" -dependencies = [ - "parking_lot", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -2015,6 +2262,16 @@ dependencies = [ "libc", ] +[[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" @@ -2059,12 +2316,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2073,6 +2330,228 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +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.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.0.0", + "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.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +dependencies = [ + "atoi", + "base64 0.21.3", + "bitflags 2.4.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", + "serde", + "sha1 0.10.5", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +dependencies = [ + "atoi", + "base64 0.21.3", + "bitflags 2.4.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.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", + "urlencoding", +] + [[package]] name = "standback" version = "0.2.17" @@ -2131,6 +2610,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +[[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.5.0" @@ -2159,6 +2649,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -2273,19 +2775,43 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", "libc", "mio", + "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.3", - "windows-sys", + "socket2 0.5.7", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.31", +] + +[[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]] @@ -2354,9 +2880,21 @@ dependencies = [ "cfg-if", "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.31", +] + [[package]] name = "tracing-core" version = "0.1.31" @@ -2402,12 +2940,30 @@ 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-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "unicode-width" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[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.7.1" @@ -2460,6 +3016,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2498,6 +3060,12 @@ 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 = "wasm-bindgen" version = "0.2.87" @@ -2596,6 +3164,16 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" +[[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" @@ -2639,7 +3217,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -2648,13 +3235,29 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -2663,42 +3266,90 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "0.5.15" @@ -2737,6 +3388,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.31", +] + [[package]] name = "zeroize" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index e74955f..4e21678 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,6 @@ edition = "2021" build = "build.rs" [features] -default = ["bundle-sqlite"] -bundle-sqlite = ["libsqlite3-sys"] ui = ["native-windows-gui", "native-windows-derive"] [dependencies] @@ -18,16 +16,11 @@ ape = "0.5" base64 = "0.21" branca = "0.10.1" crossbeam-channel = "0.5" -diesel_migrations = { version = "2.0", features = ["sqlite"] } futures-util = { version = "0.3" } getopts = "0.2.21" http = "0.2.8" id3 = "1.7.0" lewton = "0.10.2" -libsqlite3-sys = { version = "0.26", features = [ - "bundled", - "bundled-windows", -], optional = true } log = "0.4.17" metaflac = "0.2.5" mp3-duration = "0.1.10" @@ -44,19 +37,16 @@ serde = { version = "1.0.147", features = ["derive"] } serde_derive = "1.0.147" serde_json = "1.0.87" simplelog = "0.12.0" +sqlx = { version = "0.7.4", features = ["migrate", "runtime-tokio", "sqlite"] } thiserror = "1.0.37" +tokio = { version = "1.38", features = ["macros", "rt-multi-thread"] } toml = "0.7" ureq = "2.7" url = "2.3" -[dependencies.diesel] -version = "2.0.2" -default_features = false -features = ["libsqlite3-sys", "r2d2", "sqlite"] - [dependencies.image] version = "0.24.4" -default_features = false +default-features = false features = ["bmp", "gif", "jpeg", "png"] [target.'cfg(windows)'.dependencies] @@ -81,3 +71,6 @@ winres = "0.1" actix-test = "0.1.0" headers = "0.3" fs_extra = "1.2.0" + +[profile.dev.package.sqlx-macros] +opt-level = 3 diff --git a/diesel.toml b/diesel.toml deleted file mode 100644 index 1e154a7..0000000 --- a/diesel.toml +++ /dev/null @@ -1,2 +0,0 @@ -[print_schema] -file = "src/db/schema.rs" diff --git a/docs/MAINTENANCE.md b/docs/MAINTENANCE.md index 56043dc..fd236fe 100644 --- a/docs/MAINTENANCE.md +++ b/docs/MAINTENANCE.md @@ -8,8 +8,3 @@ - Input a user-facing version name (eg: **0.13.0**) - Click the **Run workflow** button - After CI completes, move the release from Draft to Published - -## How to change the database schema - -- Add a new folder under `migrations` following the existing pattern -- Run `update_db_schema.bat` diff --git a/migrations/201706250006_init/down.sql b/migrations/201706250006_init/down.sql deleted file mode 100644 index a99d36b..0000000 --- a/migrations/201706250006_init/down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP TABLE directories; -DROP TABLE songs; \ No newline at end of file diff --git a/migrations/201706250006_init/up.sql b/migrations/201706250006_init/up.sql deleted file mode 100644 index fee773f..0000000 --- a/migrations/201706250006_init/up.sql +++ /dev/null @@ -1,25 +0,0 @@ -CREATE TABLE directories ( - id INTEGER PRIMARY KEY NOT NULL, - path TEXT NOT NULL, - parent TEXT, - artist TEXT, - year INTEGER, - album TEXT, - artwork TEXT, - UNIQUE(path) ON CONFLICT REPLACE -); - -CREATE TABLE songs ( - id INTEGER PRIMARY KEY NOT NULL, - path TEXT NOT NULL, - parent TEXT NOT NULL, - track_number INTEGER, - disc_number INTEGER, - title TEXT, - artist TEXT, - album_artist TEXT, - year INTEGER, - album TEXT, - artwork TEXT, - UNIQUE(path) ON CONFLICT REPLACE -); \ No newline at end of file diff --git a/migrations/201706250228_directories_date_added/down.sql b/migrations/201706250228_directories_date_added/down.sql deleted file mode 100644 index 3323036..0000000 --- a/migrations/201706250228_directories_date_added/down.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TEMPORARY TABLE directories_backup(id, path, parent, artist, year, album, artwork); -INSERT INTO directories_backup SELECT id, path, parent, artist, year, album, artwork FROM directories; -DROP TABLE directories; -CREATE TABLE directories ( - id INTEGER PRIMARY KEY NOT NULL, - path TEXT NOT NULL, - parent TEXT, - artist TEXT, - year INTEGER, - album TEXT, - artwork TEXT, - UNIQUE(path) ON CONFLICT REPLACE -); -INSERT INTO directories SELECT * FROM directories_backup; -DROP TABLE directories_backup; \ No newline at end of file diff --git a/migrations/201706250228_directories_date_added/up.sql b/migrations/201706250228_directories_date_added/up.sql deleted file mode 100644 index 7552c4e..0000000 --- a/migrations/201706250228_directories_date_added/up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE directories ADD COLUMN date_added INTEGER DEFAULT 0 NOT NULL; \ No newline at end of file diff --git a/migrations/201706272129_users_table/down.sql b/migrations/201706272129_users_table/down.sql deleted file mode 100644 index cc1f647..0000000 --- a/migrations/201706272129_users_table/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE users; diff --git a/migrations/201706272129_users_table/up.sql b/migrations/201706272129_users_table/up.sql deleted file mode 100644 index fd43421..0000000 --- a/migrations/201706272129_users_table/up.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE users ( - id INTEGER PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - password_salt BLOB NOT NULL, - password_hash BLOB NOT NULL, - admin INTEGER NOT NULL, - UNIQUE(name) -); diff --git a/migrations/201706272304_misc_settings_table/down.sql b/migrations/201706272304_misc_settings_table/down.sql deleted file mode 100644 index 4cab917..0000000 --- a/migrations/201706272304_misc_settings_table/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE misc_settings; diff --git a/migrations/201706272304_misc_settings_table/up.sql b/migrations/201706272304_misc_settings_table/up.sql deleted file mode 100644 index c748840..0000000 --- a/migrations/201706272304_misc_settings_table/up.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE misc_settings ( - id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0), - auth_secret TEXT NOT NULL, - index_sleep_duration_seconds INTEGER NOT NULL, - index_album_art_pattern TEXT NOT NULL -); -INSERT INTO misc_settings (id, auth_secret, index_sleep_duration_seconds, index_album_art_pattern) VALUES (0, hex(randomblob(64)), 1800, "Folder.(jpeg|jpg|png)"); diff --git a/migrations/201706272313_ddns_config_table/down.sql b/migrations/201706272313_ddns_config_table/down.sql deleted file mode 100644 index 72002c0..0000000 --- a/migrations/201706272313_ddns_config_table/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE ddns_config; diff --git a/migrations/201706272313_ddns_config_table/up.sql b/migrations/201706272313_ddns_config_table/up.sql deleted file mode 100644 index 7fbe94f..0000000 --- a/migrations/201706272313_ddns_config_table/up.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE ddns_config ( - id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0), - host TEXT NOT NULL, - username TEXT NOT NULL, - password TEXT NOT NULL -); - -INSERT INTO ddns_config (id, host, username, password) VALUES (0, "", "", ""); \ No newline at end of file diff --git a/migrations/201706272327_mount_points_table/down.sql b/migrations/201706272327_mount_points_table/down.sql deleted file mode 100644 index a7ff670..0000000 --- a/migrations/201706272327_mount_points_table/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE mount_points; diff --git a/migrations/201706272327_mount_points_table/up.sql b/migrations/201706272327_mount_points_table/up.sql deleted file mode 100644 index 9c3d6f9..0000000 --- a/migrations/201706272327_mount_points_table/up.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE mount_points ( - id INTEGER PRIMARY KEY NOT NULL, - source TEXT NOT NULL, - name TEXT NOT NULL, - UNIQUE(name) -); diff --git a/migrations/201707091522_playlists_tables/down.sql b/migrations/201707091522_playlists_tables/down.sql deleted file mode 100644 index 8060d04..0000000 --- a/migrations/201707091522_playlists_tables/down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP TABLE playlists; -DROP TABLE playlist_songs; diff --git a/migrations/201707091522_playlists_tables/up.sql b/migrations/201707091522_playlists_tables/up.sql deleted file mode 100644 index 69dea8f..0000000 --- a/migrations/201707091522_playlists_tables/up.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE playlists ( - id INTEGER PRIMARY KEY NOT NULL, - owner INTEGER NOT NULL, - name TEXT NOT NULL, - FOREIGN KEY(owner) REFERENCES users(id) ON DELETE CASCADE, - UNIQUE(owner, name) ON CONFLICT REPLACE -); - -CREATE TABLE playlist_songs ( - id INTEGER PRIMARY KEY NOT NULL, - playlist INTEGER NOT NULL, - path TEXT NOT NULL, - ordering INTEGER NOT NULL, - FOREIGN KEY(playlist) REFERENCES playlists(id) ON DELETE CASCADE ON UPDATE CASCADE, - UNIQUE(playlist, ordering) ON CONFLICT REPLACE -); diff --git a/migrations/20170929203228_add_prefix_url/down.sql b/migrations/20170929203228_add_prefix_url/down.sql deleted file mode 100644 index 33b8650..0000000 --- a/migrations/20170929203228_add_prefix_url/down.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TEMPORARY TABLE misc_settings_backup(id, auth_secret, index_sleep_duration_seconds, index_album_art_pattern); -INSERT INTO misc_settings_backup SELECT id, auth_secret, index_sleep_duration_seconds, index_album_art_pattern FROM misc_settings; -DROP TABLE misc_settings; -CREATE TABLE misc_settings ( - id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0), - auth_secret TEXT NOT NULL, - index_sleep_duration_seconds INTEGER NOT NULL, - index_album_art_pattern TEXT NOT NULL -); -INSERT INTO misc_settings SELECT * FROM misc_settings_backup; -DROP TABLE misc_settings_backup; diff --git a/migrations/20170929203228_add_prefix_url/up.sql b/migrations/20170929203228_add_prefix_url/up.sql deleted file mode 100644 index cc16d29..0000000 --- a/migrations/20170929203228_add_prefix_url/up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE misc_settings ADD COLUMN prefix_url TEXT NOT NULL DEFAULT ""; diff --git a/migrations/20171015224223_add_song_duration/down.sql b/migrations/20171015224223_add_song_duration/down.sql deleted file mode 100644 index 6f538da..0000000 --- a/migrations/20171015224223_add_song_duration/down.sql +++ /dev/null @@ -1,19 +0,0 @@ -CREATE TEMPORARY TABLE songs_backup(id, path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork); -INSERT INTO songs_backup SELECT id, path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork FROM songs; -DROP TABLE songs; -CREATE TABLE songs ( - id INTEGER PRIMARY KEY NOT NULL, - path TEXT NOT NULL, - parent TEXT NOT NULL, - track_number INTEGER, - disc_number INTEGER, - title TEXT, - artist TEXT, - album_artist TEXT, - year INTEGER, - album TEXT, - artwork TEXT, - UNIQUE(path) ON CONFLICT REPLACE -); -INSERT INTO songs SELECT * FROM songs_backup; -DROP TABLE songs_backup; diff --git a/migrations/20171015224223_add_song_duration/up.sql b/migrations/20171015224223_add_song_duration/up.sql deleted file mode 100644 index cea3ac5..0000000 --- a/migrations/20171015224223_add_song_duration/up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE songs ADD COLUMN duration INTEGER; diff --git a/migrations/20180303211100_add_last_fm_credentials/down.sql b/migrations/20180303211100_add_last_fm_credentials/down.sql deleted file mode 100644 index c6fda4e..0000000 --- a/migrations/20180303211100_add_last_fm_credentials/down.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE TEMPORARY TABLE users_backup(id, name, password_salt, password_hash, admin); -INSERT INTO users_backup SELECT id, name, password_salt, password_hash, admin FROM users; -DROP TABLE users; -CREATE TABLE users ( - id INTEGER PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - password_salt BLOB NOT NULL, - password_hash BLOB NOT NULL, - admin INTEGER NOT NULL, - UNIQUE(name) -); -INSERT INTO users SELECT * FROM users_backup; -DROP TABLE users_backup; diff --git a/migrations/20180303211100_add_last_fm_credentials/up.sql b/migrations/20180303211100_add_last_fm_credentials/up.sql deleted file mode 100644 index 40cc2e1..0000000 --- a/migrations/20180303211100_add_last_fm_credentials/up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE users ADD COLUMN lastfm_username TEXT; -ALTER TABLE users ADD COLUMN lastfm_session_key TEXT; diff --git a/migrations/2019-08-08-042731_blob_auth_secret/down.sql b/migrations/2019-08-08-042731_blob_auth_secret/down.sql deleted file mode 100644 index 8dc48c9..0000000 --- a/migrations/2019-08-08-042731_blob_auth_secret/down.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TEMPORARY TABLE misc_settings_backup(id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url); -INSERT INTO misc_settings_backup -SELECT id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url -FROM misc_settings; -DROP TABLE misc_settings; -CREATE TABLE misc_settings ( - id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0), - auth_secret BLOB NOT NULL DEFAULT (hex(randomblob(32))), - index_sleep_duration_seconds INTEGER NOT NULL, - index_album_art_pattern TEXT NOT NULL, - prefix_url TEXT NOT NULL DEFAULT "" -); -INSERT INTO misc_settings(id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url) -SELECT * FROM misc_settings_backup; -DROP TABLE misc_settings_backup; diff --git a/migrations/2019-08-08-042731_blob_auth_secret/up.sql b/migrations/2019-08-08-042731_blob_auth_secret/up.sql deleted file mode 100644 index 9f15588..0000000 --- a/migrations/2019-08-08-042731_blob_auth_secret/up.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TEMPORARY TABLE misc_settings_backup(id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url); -INSERT INTO misc_settings_backup -SELECT id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url -FROM misc_settings; -DROP TABLE misc_settings; -CREATE TABLE misc_settings ( - 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, - prefix_url TEXT NOT NULL DEFAULT "" -); -INSERT INTO misc_settings(id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url) -SELECT * FROM misc_settings_backup; -DROP TABLE misc_settings_backup; diff --git a/migrations/2019-09-28-231910_pbkdf2_simple/down.sql b/migrations/2019-09-28-231910_pbkdf2_simple/down.sql deleted file mode 100644 index 573f83f..0000000 --- a/migrations/2019-09-28-231910_pbkdf2_simple/down.sql +++ /dev/null @@ -1,11 +0,0 @@ -DROP TABLE users; -CREATE TABLE users ( - id INTEGER PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - password_salt BLOB NOT NULL, - password_hash BLOB NOT NULL, - admin INTEGER NOT NULL, - lastfm_username TEXT, - lastfm_session_key TEXT, - UNIQUE(name) -); diff --git a/migrations/2019-09-28-231910_pbkdf2_simple/up.sql b/migrations/2019-09-28-231910_pbkdf2_simple/up.sql deleted file mode 100644 index 99a4fe5..0000000 --- a/migrations/2019-09-28-231910_pbkdf2_simple/up.sql +++ /dev/null @@ -1,10 +0,0 @@ -DROP TABLE users; -CREATE TABLE users ( - id INTEGER PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - password_hash TEXT NOT NULL, - admin INTEGER NOT NULL, - lastfm_username TEXT, - lastfm_session_key TEXT, - UNIQUE(name) -); diff --git a/migrations/2020-01-08-231420_add_theme/down.sql b/migrations/2020-01-08-231420_add_theme/down.sql deleted file mode 100644 index 5bf69d8..0000000 --- a/migrations/2020-01-08-231420_add_theme/down.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TEMPORARY TABLE users_backup(id, name, password_hash, admin, lastfm_username, lastfm_session_key); -INSERT INTO users_backup SELECT id, name, password_hash, admin, lastfm_username, lastfm_session_key FROM users; -DROP TABLE users; -CREATE TABLE users ( - id INTEGER PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - password_hash TEXT NOT NULL, - admin INTEGER NOT NULL, - lastfm_username TEXT, - lastfm_session_key TEXT, - UNIQUE(name) -); -INSERT INTO users SELECT * FROM users_backup; -DROP TABLE users_backup; diff --git a/migrations/2020-01-08-231420_add_theme/up.sql b/migrations/2020-01-08-231420_add_theme/up.sql deleted file mode 100644 index 67addb5..0000000 --- a/migrations/2020-01-08-231420_add_theme/up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE users ADD COLUMN web_theme_base TEXT; -ALTER TABLE users ADD COLUMN web_theme_accent TEXT; diff --git a/migrations/2020-11-25-174000_remove_prefix_url/down.sql b/migrations/2020-11-25-174000_remove_prefix_url/down.sql deleted file mode 100644 index cc16d29..0000000 --- a/migrations/2020-11-25-174000_remove_prefix_url/down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE misc_settings ADD COLUMN prefix_url TEXT NOT NULL DEFAULT ""; diff --git a/migrations/2020-11-25-174000_remove_prefix_url/up.sql b/migrations/2020-11-25-174000_remove_prefix_url/up.sql deleted file mode 100644 index 3016c77..0000000 --- a/migrations/2020-11-25-174000_remove_prefix_url/up.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TEMPORARY TABLE misc_settings_backup(id, auth_secret, index_sleep_duration_seconds, index_album_art_pattern); -INSERT INTO misc_settings_backup SELECT id, auth_secret, index_sleep_duration_seconds, index_album_art_pattern FROM misc_settings; -DROP TABLE misc_settings; -CREATE TABLE misc_settings ( - 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 -); -INSERT INTO misc_settings SELECT * FROM misc_settings_backup; -DROP TABLE misc_settings_backup; diff --git a/migrations/2021-05-01-011426_add_lyricist/down.sql b/migrations/2021-05-01-011426_add_lyricist/down.sql deleted file mode 100644 index 5a5b228..0000000 --- a/migrations/2021-05-01-011426_add_lyricist/down.sql +++ /dev/null @@ -1,20 +0,0 @@ -CREATE TEMPORARY TABLE songs_backup(id, path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork, duration); -INSERT INTO songs_backup SELECT id, path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork, duration FROM songs; -DROP TABLE songs; -CREATE TABLE songs ( - id INTEGER PRIMARY KEY NOT NULL, - path TEXT NOT NULL, - parent TEXT NOT NULL, - track_number INTEGER, - disc_number INTEGER, - title TEXT, - artist TEXT, - album_artist TEXT, - year INTEGER, - album TEXT, - artwork TEXT, - duration INTEGER, - UNIQUE(path) ON CONFLICT REPLACE -); -INSERT INTO songs SELECT * FROM songs_backup; -DROP TABLE songs_backup; diff --git a/migrations/2021-05-01-011426_add_lyricist/up.sql b/migrations/2021-05-01-011426_add_lyricist/up.sql deleted file mode 100644 index 16f7863..0000000 --- a/migrations/2021-05-01-011426_add_lyricist/up.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE songs ADD COLUMN lyricist TEXT; -ALTER TABLE songs ADD COLUMN composer TEXT; -ALTER TABLE songs ADD COLUMN genre TEXT; -ALTER TABLE songs ADD COLUMN label TEXT; diff --git a/src/app.rs b/src/app.rs index 93b9da4..d173b72 100644 --- a/src/app.rs +++ b/src/app.rs @@ -32,10 +32,8 @@ pub enum Error { #[derive(Clone)] pub struct App { pub port: u16, - pub auth_secret: settings::AuthSecret, pub web_dir_path: PathBuf, pub swagger_dir_path: PathBuf, - pub db: DB, pub index: index::Index, pub config_manager: config::Manager, pub ddns_manager: ddns::Manager, @@ -48,8 +46,8 @@ pub struct App { } impl App { - pub fn new(port: u16, paths: Paths) -> Result<Self, Error> { - let db = DB::new(&paths.db_file_path)?; + 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.web_dir_path) .map_err(|e| Error::Io(paths.web_dir_path.clone(), e))?; fs::create_dir_all(&paths.swagger_dir_path) @@ -61,7 +59,7 @@ impl App { let vfs_manager = vfs::Manager::new(db.clone()); let settings_manager = settings::Manager::new(db.clone()); - let auth_secret = settings_manager.get_auth_secret()?; + 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 = index::Index::new(db.clone(), vfs_manager.clone(), settings_manager.clone()); @@ -77,14 +75,11 @@ impl App { if let Some(config_path) = paths.config_file_path { let config = config::Config::from_path(&config_path)?; - config_manager.apply(&config)?; + config_manager.apply(&config).await?; } - let auth_secret = settings_manager.get_auth_secret()?; - Ok(Self { port, - auth_secret, web_dir_path: paths.web_dir_path, swagger_dir_path: paths.swagger_dir_path, index, @@ -96,7 +91,6 @@ impl App { thumbnail_manager, user_manager, vfs_manager, - db, }) } } diff --git a/src/app/config.rs b/src/app/config.rs index c20c608..d527ee4 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -64,28 +64,28 @@ impl Manager { } } - pub fn apply(&self, config: &Config) -> Result<(), Error> { + pub async fn apply(&self, config: &Config) -> Result<(), Error> { if let Some(new_settings) = &config.settings { - self.settings_manager.amend(new_settings)?; + self.settings_manager.amend(new_settings).await?; } if let Some(mount_dirs) = &config.mount_dirs { - self.vfs_manager.set_mount_dirs(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)?; + 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()?; + 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)?; + self.user_manager.delete(&old_user.name).await?; } // Insert new users @@ -93,13 +93,17 @@ impl Manager { .iter() .filter(|u| !old_users.iter().any(|old_user| old_user.name == u.name)) { - self.user_manager.create(new_user)?; + self.user_manager.create(new_user).await?; } // Update users for user in users { - self.user_manager.set_password(&user.name, &user.password)?; - self.user_manager.set_is_admin(&user.name, user.admin)?; + self.user_manager + .set_password(&user.name, &user.password) + .await?; + self.user_manager + .set_is_admin(&user.name, user.admin) + .await?; } } @@ -114,9 +118,9 @@ mod test { use crate::app::test; use crate::test_name; - #[test] - fn apply_saves_misc_settings() { - let ctx = test::ContextBuilder::new(test_name!()).build(); + #[tokio::test] + async fn apply_saves_misc_settings() { + let ctx = test::ContextBuilder::new(test_name!()).build().await; let new_config = Config { settings: Some(settings::NewSettings { album_art_pattern: Some("🖼️\\.jpg".into()), @@ -125,8 +129,8 @@ mod test { ..Default::default() }; - ctx.config_manager.apply(&new_config).unwrap(); - let settings = ctx.settings_manager.read().unwrap(); + ctx.config_manager.apply(&new_config).await.unwrap(); + let settings = ctx.settings_manager.read().await.unwrap(); let new_settings = new_config.settings.unwrap(); assert_eq!( settings.index_album_art_pattern, @@ -138,9 +142,9 @@ mod test { ); } - #[test] - fn apply_saves_mount_points() { - let ctx = test::ContextBuilder::new(test_name!()).build(); + #[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 { @@ -150,36 +154,37 @@ mod test { ..Default::default() }; - ctx.config_manager.apply(&new_config).unwrap(); - let actual_mount_dirs: Vec<vfs::MountDir> = ctx.vfs_manager.mount_dirs().unwrap(); + 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 apply_saves_ddns_settings() { - let ctx = test::ContextBuilder::new(test_name!()).build(); + #[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 { - host: "🐸🐸🐸.ydns.eu".into(), - username: "kfr🐸g".into(), - password: "tasty🐞".into(), + ddns_host: "🐸🐸🐸.ydns.eu".into(), + ddns_username: "kfr🐸g".into(), + ddns_password: "tasty🐞".into(), }), ..Default::default() }; - ctx.config_manager.apply(&new_config).unwrap(); - let actual_ddns = ctx.ddns_manager.config().unwrap(); + 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 apply_can_toggle_admin() { + #[tokio::test] + async fn apply_can_toggle_admin() { let ctx = test::ContextBuilder::new(test_name!()) .user("Walter", "Tasty🍖", true) - .build(); + .build() + .await; - assert!(ctx.user_manager.list().unwrap()[0].is_admin()); + assert!(ctx.user_manager.list().await.unwrap()[0].is_admin()); let new_config = Config { users: Some(vec![user::NewUser { @@ -189,7 +194,7 @@ mod test { }]), ..Default::default() }; - ctx.config_manager.apply(&new_config).unwrap(); - assert!(!ctx.user_manager.list().unwrap()[0].is_admin()); + ctx.config_manager.apply(&new_config).await.unwrap(); + assert!(!ctx.user_manager.list().await.unwrap()[0].is_admin()); } } diff --git a/src/app/ddns.rs b/src/app/ddns.rs index 980c67e..ba08099 100644 --- a/src/app/ddns.rs +++ b/src/app/ddns.rs @@ -1,11 +1,9 @@ use base64::prelude::*; -use diesel::prelude::*; use log::{debug, error}; use serde::{Deserialize, Serialize}; -use std::thread; -use std::time; +use std::time::Duration; -use crate::db::{self, ddns_config, DB}; +use crate::db::{self, DB}; const DDNS_UPDATE_URL: &str = "https://ydns.io/api/v1/update/"; @@ -18,15 +16,14 @@ pub enum Error { #[error(transparent)] DatabaseConnection(#[from] db::Error), #[error(transparent)] - Database(#[from] diesel::result::Error), + Database(#[from] sqlx::Error), } -#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Eq, Queryable, Serialize)] -#[diesel(table_name = ddns_config)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] pub struct Config { - pub host: String, - pub username: String, - pub password: String, + pub ddns_host: String, + pub ddns_username: String, + pub ddns_password: String, } #[derive(Clone)] @@ -39,15 +36,15 @@ impl Manager { Self { db } } - fn update_my_ip(&self) -> Result<(), Error> { - let config = self.config()?; - if config.host.is_empty() || config.username.is_empty() { + async fn update_my_ip(&self) -> Result<(), Error> { + let config = self.config().await?; + if config.ddns_host.is_empty() || config.ddns_username.is_empty() { debug!("Skipping DDNS update because credentials are missing"); return Ok(()); } - let full_url = format!("{}?host={}", DDNS_UPDATE_URL, &config.host); - let credentials = format!("{}:{}", &config.username, &config.password); + let full_url = format!("{}?host={}", DDNS_UPDATE_URL, &config.ddns_host); + let credentials = format!("{}:{}", &config.ddns_username, &config.ddns_password); let response = ureq::get(full_url.as_str()) .set( "Authorization", @@ -62,40 +59,38 @@ impl Manager { } } - pub fn config(&self) -> Result<Config, Error> { - use crate::db::ddns_config::dsl::*; - let mut connection = self.db.connect()?; - Ok(ddns_config - .select((host, username, password)) - .get_result(&mut connection)?) + pub async fn config(&self) -> Result<Config, Error> { + Ok(sqlx::query_as!( + Config, + "SELECT ddns_host, ddns_username, ddns_password FROM config" + ) + .fetch_one(self.db.connect().await?.as_mut()) + .await?) } - pub fn set_config(&self, new_config: &Config) -> Result<(), Error> { - use crate::db::ddns_config::dsl::*; - let mut connection = self.db.connect()?; - diesel::update(ddns_config) - .set(( - host.eq(&new_config.host), - username.eq(&new_config.username), - password.eq(&new_config.password), - )) - .execute(&mut connection)?; + pub async fn set_config(&self, new_config: &Config) -> Result<(), Error> { + sqlx::query!( + "UPDATE config SET ddns_host = $1, ddns_username = $2, ddns_password = $3", + new_config.ddns_host, + new_config.ddns_username, + new_config.ddns_password + ) + .execute(self.db.connect().await?.as_mut()) + .await?; Ok(()) } pub fn begin_periodic_updates(&self) { - let cloned = self.clone(); - std::thread::spawn(move || { - cloned.run(); + tokio::spawn({ + let ddns = self.clone(); + async move { + loop { + if let Err(e) = ddns.update_my_ip().await { + error!("Dynamic DNS update error: {:?}", e); + } + tokio::time::sleep(Duration::from_secs(60 * 30)).await; + } + } }); } - - fn run(&self) { - loop { - if let Err(e) = self.update_my_ip() { - error!("Dynamic DNS update error: {:?}", e); - } - thread::sleep(time::Duration::from_secs(60 * 30)); - } - } } diff --git a/src/app/index.rs b/src/app/index.rs index 0375ac2..b0b00cf 100644 --- a/src/app/index.rs +++ b/src/app/index.rs @@ -1,6 +1,7 @@ use log::error; -use std::sync::{Arc, Condvar, Mutex}; +use std::sync::Arc; use std::time::Duration; +use tokio::sync::Notify; use crate::app::{settings, vfs}; use crate::db::DB; @@ -20,7 +21,7 @@ pub struct Index { db: DB, vfs_manager: vfs::Manager, settings_manager: settings::Manager, - pending_reindex: Arc<(Mutex<bool>, Condvar)>, + pending_reindex: Arc<Notify>, } impl Index { @@ -29,63 +30,45 @@ impl Index { db, vfs_manager, settings_manager, - - pending_reindex: Arc::new(( - #[allow(clippy::mutex_atomic)] - Mutex::new(false), - Condvar::new(), - )), + pending_reindex: Arc::new(Notify::new()), }; - let commands_index = index.clone(); - std::thread::spawn(move || { - commands_index.process_commands(); + tokio::spawn({ + let index = index.clone(); + async move { + loop { + index.pending_reindex.notified().await; + if let Err(e) = index.update().await { + error!("Error while updating index: {}", e); + } + } + } }); index } pub fn trigger_reindex(&self) { - let (lock, cvar) = &*self.pending_reindex; - let mut pending_reindex = lock.lock().unwrap(); - *pending_reindex = true; - cvar.notify_one(); + self.pending_reindex.notify_one(); } pub fn begin_periodic_updates(&self) { - let auto_index = self.clone(); - std::thread::spawn(move || { - auto_index.automatic_reindex(); + tokio::spawn({ + let index = self.clone(); + async move { + loop { + index.trigger_reindex(); + 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) + }); + tokio::time::sleep(sleep_duration).await; + } + } }); } - - fn process_commands(&self) { - loop { - { - let (lock, cvar) = &*self.pending_reindex; - let mut pending = lock.lock().unwrap(); - while !*pending { - pending = cvar.wait(pending).unwrap(); - } - *pending = false; - } - if let Err(e) = self.update() { - error!("Error while updating index: {}", e); - } - } - } - - fn automatic_reindex(&self) { - loop { - self.trigger_reindex(); - let sleep_duration = self - .settings_manager - .get_index_sleep_duration() - .unwrap_or_else(|e| { - error!("Could not retrieve index sleep duration: {}", e); - Duration::from_secs(1800) - }); - std::thread::sleep(sleep_duration); - } - } } diff --git a/src/app/index/query.rs b/src/app/index/query.rs index 5494635..ad921d0 100644 --- a/src/app/index/query.rs +++ b/src/app/index/query.rs @@ -1,15 +1,12 @@ -use diesel::dsl::sql; -use diesel::prelude::*; -use diesel::sql_types; use std::path::{Path, PathBuf}; use super::*; -use crate::db::{self, directories, songs}; +use crate::db; #[derive(thiserror::Error, Debug)] pub enum QueryError { #[error(transparent)] - Database(#[from] diesel::result::Error), + Database(#[from] sqlx::Error), #[error(transparent)] DatabaseConnection(#[from] db::Error), #[error("Song was not found: `{0}`")] @@ -18,25 +15,21 @@ pub enum QueryError { Vfs(#[from] vfs::Error), } -sql_function!( - #[aggregate] - fn random() -> Integer; -); - impl Index { - pub fn browse<P>(&self, virtual_path: P) -> Result<Vec<CollectionFile>, QueryError> + pub async fn browse<P>(&self, virtual_path: P) -> Result<Vec<CollectionFile>, QueryError> where P: AsRef<Path>, { let mut output = Vec::new(); - let vfs = self.vfs_manager.get_vfs()?; - let mut connection = self.db.connect()?; + let vfs = self.vfs_manager.get_vfs().await?; + let mut connection = self.db.connect().await?; if virtual_path.as_ref().components().count() == 0 { // Browse top-level - let real_directories: Vec<Directory> = directories::table - .filter(directories::parent.is_null()) - .load(&mut connection)?; + let real_directories = + sqlx::query_as!(Directory, "SELECT * FROM directories WHERE parent IS NULL") + .fetch_all(connection.as_mut()) + .await?; let virtual_directories = real_directories .into_iter() .filter_map(|d| d.virtualize(&vfs)); @@ -46,19 +39,28 @@ impl Index { let real_path = vfs.virtual_to_real(virtual_path)?; let real_path_string = real_path.as_path().to_string_lossy().into_owned(); - let real_directories: Vec<Directory> = directories::table - .filter(directories::parent.eq(&real_path_string)) - .order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC")) - .load(&mut connection)?; + let real_directories = sqlx::query_as!( + Directory, + "SELECT * FROM directories WHERE parent = $1 ORDER BY path COLLATE NOCASE ASC", + real_path_string + ) + .fetch_all(connection.as_mut()) + .await?; + let virtual_directories = real_directories .into_iter() .filter_map(|d| d.virtualize(&vfs)); + output.extend(virtual_directories.map(CollectionFile::Directory)); - let real_songs: Vec<Song> = songs::table - .filter(songs::parent.eq(&real_path_string)) - .order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC")) - .load(&mut connection)?; + let real_songs = sqlx::query_as!( + Song, + "SELECT * FROM songs WHERE parent = $1 ORDER BY path COLLATE NOCASE ASC", + real_path_string + ) + .fetch_all(connection.as_mut()) + .await?; + let virtual_songs = real_songs.into_iter().filter_map(|s| s.virtualize(&vfs)); output.extend(virtual_songs.map(CollectionFile::Song)); } @@ -66,76 +68,88 @@ impl Index { Ok(output) } - pub fn flatten<P>(&self, virtual_path: P) -> Result<Vec<Song>, QueryError> + pub async fn flatten<P>(&self, virtual_path: P) -> Result<Vec<Song>, QueryError> where P: AsRef<Path>, { - use self::songs::dsl::*; - let vfs = self.vfs_manager.get_vfs()?; - let mut connection = self.db.connect()?; + let vfs = self.vfs_manager.get_vfs().await?; + let mut connection = self.db.connect().await?; - let real_songs: Vec<Song> = if virtual_path.as_ref().parent().is_some() { + let real_songs = if virtual_path.as_ref().parent().is_some() { let real_path = vfs.virtual_to_real(virtual_path)?; let song_path_filter = { let mut path_buf = real_path; path_buf.push("%"); path_buf.as_path().to_string_lossy().into_owned() }; - songs - .filter(path.like(&song_path_filter)) - .order(path) - .load(&mut connection)? + sqlx::query_as!( + Song, + "SELECT * FROM songs WHERE path LIKE $1 ORDER BY path COLLATE NOCASE ASC", + song_path_filter + ) + .fetch_all(connection.as_mut()) + .await? } else { - songs.order(path).load(&mut connection)? + sqlx::query_as!(Song, "SELECT * FROM songs ORDER BY path COLLATE NOCASE ASC") + .fetch_all(connection.as_mut()) + .await? }; let virtual_songs = real_songs.into_iter().filter_map(|s| s.virtualize(&vfs)); Ok(virtual_songs.collect::<Vec<_>>()) } - pub fn get_random_albums(&self, count: i64) -> Result<Vec<Directory>, QueryError> { - use self::directories::dsl::*; - let vfs = self.vfs_manager.get_vfs()?; - let mut connection = self.db.connect()?; - let real_directories: Vec<Directory> = directories - .filter(album.is_not_null()) - .limit(count) - .order(random()) - .load(&mut connection)?; + pub async fn get_random_albums(&self, count: i64) -> Result<Vec<Directory>, QueryError> { + let vfs = self.vfs_manager.get_vfs().await?; + let mut connection = self.db.connect().await?; + + let real_directories = sqlx::query_as!( + Directory, + "SELECT * FROM directories WHERE album IS NOT NULL ORDER BY RANDOM() DESC LIMIT $1", + count + ) + .fetch_all(connection.as_mut()) + .await?; + let virtual_directories = real_directories .into_iter() .filter_map(|d| d.virtualize(&vfs)); Ok(virtual_directories.collect::<Vec<_>>()) } - pub fn get_recent_albums(&self, count: i64) -> Result<Vec<Directory>, QueryError> { - use self::directories::dsl::*; - let vfs = self.vfs_manager.get_vfs()?; - let mut connection = self.db.connect()?; - let real_directories: Vec<Directory> = directories - .filter(album.is_not_null()) - .order(date_added.desc()) - .limit(count) - .load(&mut connection)?; + pub async fn get_recent_albums(&self, count: i64) -> Result<Vec<Directory>, QueryError> { + let vfs = self.vfs_manager.get_vfs().await?; + let mut connection = self.db.connect().await?; + + let real_directories = sqlx::query_as!( + Directory, + "SELECT * FROM directories WHERE album IS NOT NULL ORDER BY date_added DESC LIMIT $1", + count + ) + .fetch_all(connection.as_mut()) + .await?; + let virtual_directories = real_directories .into_iter() .filter_map(|d| d.virtualize(&vfs)); Ok(virtual_directories.collect::<Vec<_>>()) } - pub fn search(&self, query: &str) -> Result<Vec<CollectionFile>, QueryError> { - let vfs = self.vfs_manager.get_vfs()?; - let mut connection = self.db.connect()?; + pub async fn search(&self, query: &str) -> Result<Vec<CollectionFile>, QueryError> { + let vfs = self.vfs_manager.get_vfs().await?; + let mut connection = self.db.connect().await?; let like_test = format!("%{}%", query); let mut output = Vec::new(); // Find dirs with matching path and parent not matching { - use self::directories::dsl::*; - let real_directories: Vec<Directory> = directories - .filter(path.like(&like_test)) - .filter(parent.not_like(&like_test)) - .load(&mut connection)?; + let real_directories = sqlx::query_as!( + Directory, + "SELECT * FROM directories WHERE path LIKE $1 AND parent NOT LIKE $1", + like_test + ) + .fetch_all(connection.as_mut()) + .await?; let virtual_directories = real_directories .into_iter() @@ -146,17 +160,22 @@ impl Index { // Find songs with matching title/album/artist and non-matching parent { - use self::songs::dsl::*; - let real_songs: Vec<Song> = songs - .filter( - path.like(&like_test) - .or(title.like(&like_test)) - .or(album.like(&like_test)) - .or(artist.like(&like_test)) - .or(album_artist.like(&like_test)), - ) - .filter(parent.not_like(&like_test)) - .load(&mut connection)?; + let real_songs = sqlx::query_as!( + Song, + r#" + SELECT * FROM songs + WHERE ( path LIKE $1 + OR title LIKE $1 + OR album LIKE $1 + OR artist LIKE $1 + OR album_artist LIKE $1 + ) + AND parent NOT LIKE $1 + "#, + like_test + ) + .fetch_all(connection.as_mut()) + .await?; let virtual_songs = real_songs.into_iter().filter_map(|d| d.virtualize(&vfs)); @@ -166,17 +185,20 @@ impl Index { Ok(output) } - pub fn get_song(&self, virtual_path: &Path) -> Result<Song, QueryError> { - let vfs = self.vfs_manager.get_vfs()?; - let mut connection = self.db.connect()?; + pub async fn get_song(&self, virtual_path: &Path) -> Result<Song, QueryError> { + let vfs = self.vfs_manager.get_vfs().await?; + let mut connection = self.db.connect().await?; 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(&mut connection)?; + let real_song = sqlx::query_as!( + Song, + "SELECT * FROM songs WHERE path = $1", + real_path_string + ) + .fetch_one(connection.as_mut()) + .await?; match real_song.virtualize(&vfs) { Some(s) => Ok(s), diff --git a/src/app/index/test.rs b/src/app/index/test.rs index 7ae7e87..a03b1b8 100644 --- a/src/app/index/test.rs +++ b/src/app/index/test.rs @@ -1,32 +1,37 @@ -use diesel::prelude::*; use std::default::Default; use std::path::{Path, PathBuf}; use super::*; use crate::app::test; -use crate::db::{directories, songs}; use crate::test_name; const TEST_MOUNT_NAME: &str = "root"; -#[test] -fn update_adds_new_content() { +#[tokio::test] +async fn update_adds_new_content() { let ctx = test::ContextBuilder::new(test_name!()) .mount(TEST_MOUNT_NAME, "test-data/small-collection") - .build(); + .build() + .await; - ctx.index.update().unwrap(); - ctx.index.update().unwrap(); // Validates that subsequent updates don't run into conflicts + ctx.index.update().await.unwrap(); + ctx.index.update().await.unwrap(); // Validates that subsequent updates don't run into conflicts - let mut connection = ctx.db.connect().unwrap(); - let all_directories: Vec<Directory> = directories::table.load(&mut connection).unwrap(); - let all_songs: Vec<Song> = songs::table.load(&mut connection).unwrap(); + let mut connection = ctx.db.connect().await.unwrap(); + let all_directories = sqlx::query_as!(Directory, "SELECT * FROM directories") + .fetch_all(connection.as_mut()) + .await + .unwrap(); + let all_songs = sqlx::query_as!(Song, "SELECT * FROM songs") + .fetch_all(connection.as_mut()) + .await + .unwrap(); assert_eq!(all_directories.len(), 6); assert_eq!(all_songs.len(), 13); } -#[test] -fn update_removes_missing_content() { +#[tokio::test] +async fn update_removes_missing_content() { let builder = test::ContextBuilder::new(test_name!()); let original_collection_dir: PathBuf = ["test-data", "small-collection"].iter().collect(); @@ -42,39 +47,53 @@ fn update_removes_missing_content() { let ctx = builder .mount(TEST_MOUNT_NAME, test_collection_dir.to_str().unwrap()) - .build(); + .build() + .await; - ctx.index.update().unwrap(); + ctx.index.update().await.unwrap(); { - let mut connection = ctx.db.connect().unwrap(); - let all_directories: Vec<Directory> = directories::table.load(&mut connection).unwrap(); - let all_songs: Vec<Song> = songs::table.load(&mut connection).unwrap(); + let mut connection = ctx.db.connect().await.unwrap(); + let all_directories = sqlx::query_as!(Directory, "SELECT * FROM directories") + .fetch_all(connection.as_mut()) + .await + .unwrap(); + let all_songs = sqlx::query_as!(Song, "SELECT * FROM songs") + .fetch_all(connection.as_mut()) + .await + .unwrap(); assert_eq!(all_directories.len(), 6); assert_eq!(all_songs.len(), 13); } let khemmis_directory = test_collection_dir.join("Khemmis"); std::fs::remove_dir_all(khemmis_directory).unwrap(); - ctx.index.update().unwrap(); + ctx.index.update().await.unwrap(); { - let mut connection = ctx.db.connect().unwrap(); - let all_directories: Vec<Directory> = directories::table.load(&mut connection).unwrap(); - let all_songs: Vec<Song> = songs::table.load(&mut connection).unwrap(); + let mut connection = ctx.db.connect().await.unwrap(); + let all_directories = sqlx::query_as!(Directory, "SELECT * FROM directories") + .fetch_all(connection.as_mut()) + .await + .unwrap(); + let all_songs = sqlx::query_as!(Song, "SELECT * FROM songs") + .fetch_all(connection.as_mut()) + .await + .unwrap(); assert_eq!(all_directories.len(), 4); assert_eq!(all_songs.len(), 8); } } -#[test] -fn can_browse_top_level() { +#[tokio::test] +async fn can_browse_top_level() { let ctx = test::ContextBuilder::new(test_name!()) .mount(TEST_MOUNT_NAME, "test-data/small-collection") - .build(); - ctx.index.update().unwrap(); + .build() + .await; + ctx.index.update().await.unwrap(); let root_path = Path::new(TEST_MOUNT_NAME); - let files = ctx.index.browse(Path::new("")).unwrap(); + let files = ctx.index.browse(Path::new("")).await.unwrap(); assert_eq!(files.len(), 1); match files[0] { CollectionFile::Directory(ref d) => assert_eq!(d.path, root_path.to_str().unwrap()), @@ -82,17 +101,18 @@ fn can_browse_top_level() { } } -#[test] -fn can_browse_directory() { +#[tokio::test] +async fn can_browse_directory() { let khemmis_path: PathBuf = [TEST_MOUNT_NAME, "Khemmis"].iter().collect(); let tobokegao_path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao"].iter().collect(); let ctx = test::ContextBuilder::new(test_name!()) .mount(TEST_MOUNT_NAME, "test-data/small-collection") - .build(); - ctx.index.update().unwrap(); + .build() + .await; + ctx.index.update().await.unwrap(); - let files = ctx.index.browse(Path::new(TEST_MOUNT_NAME)).unwrap(); + let files = ctx.index.browse(Path::new(TEST_MOUNT_NAME)).await.unwrap(); assert_eq!(files.len(), 2); match files[0] { @@ -106,73 +126,79 @@ fn can_browse_directory() { } } -#[test] -fn can_flatten_root() { +#[tokio::test] +async fn can_flatten_root() { let ctx = test::ContextBuilder::new(test_name!()) .mount(TEST_MOUNT_NAME, "test-data/small-collection") - .build(); - ctx.index.update().unwrap(); - let songs = ctx.index.flatten(Path::new(TEST_MOUNT_NAME)).unwrap(); + .build() + .await; + ctx.index.update().await.unwrap(); + let songs = ctx.index.flatten(Path::new(TEST_MOUNT_NAME)).await.unwrap(); assert_eq!(songs.len(), 13); assert_eq!(songs[0].title, Some("Above The Water".to_owned())); } -#[test] -fn can_flatten_directory() { +#[tokio::test] +async fn can_flatten_directory() { let ctx = test::ContextBuilder::new(test_name!()) .mount(TEST_MOUNT_NAME, "test-data/small-collection") - .build(); - ctx.index.update().unwrap(); + .build() + .await; + ctx.index.update().await.unwrap(); let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao"].iter().collect(); - let songs = ctx.index.flatten(path).unwrap(); + let songs = ctx.index.flatten(path).await.unwrap(); assert_eq!(songs.len(), 8); } -#[test] -fn can_flatten_directory_with_shared_prefix() { +#[tokio::test] +async fn can_flatten_directory_with_shared_prefix() { let ctx = test::ContextBuilder::new(test_name!()) .mount(TEST_MOUNT_NAME, "test-data/small-collection") - .build(); - ctx.index.update().unwrap(); + .build() + .await; + ctx.index.update().await.unwrap(); let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic"].iter().collect(); // Prefix of '(Picnic Remixes)' - let songs = ctx.index.flatten(path).unwrap(); + let songs = ctx.index.flatten(path).await.unwrap(); assert_eq!(songs.len(), 7); } -#[test] -fn can_get_random_albums() { +#[tokio::test] +async fn can_get_random_albums() { let ctx = test::ContextBuilder::new(test_name!()) .mount(TEST_MOUNT_NAME, "test-data/small-collection") - .build(); - ctx.index.update().unwrap(); - let albums = ctx.index.get_random_albums(1).unwrap(); + .build() + .await; + ctx.index.update().await.unwrap(); + let albums = ctx.index.get_random_albums(1).await.unwrap(); assert_eq!(albums.len(), 1); } -#[test] -fn can_get_recent_albums() { +#[tokio::test] +async fn can_get_recent_albums() { let ctx = test::ContextBuilder::new(test_name!()) .mount(TEST_MOUNT_NAME, "test-data/small-collection") - .build(); - ctx.index.update().unwrap(); - let albums = ctx.index.get_recent_albums(2).unwrap(); + .build() + .await; + ctx.index.update().await.unwrap(); + let albums = ctx.index.get_recent_albums(2).await.unwrap(); assert_eq!(albums.len(), 2); assert!(albums[0].date_added >= albums[1].date_added); } -#[test] -fn can_get_a_song() { +#[tokio::test] +async fn can_get_a_song() { let ctx = test::ContextBuilder::new(test_name!()) .mount(TEST_MOUNT_NAME, "test-data/small-collection") - .build(); + .build() + .await; - ctx.index.update().unwrap(); + ctx.index.update().await.unwrap(); let picnic_virtual_dir: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic"].iter().collect(); let song_virtual_path = picnic_virtual_dir.join("05 - シャーベット (Sherbet).mp3"); let artwork_virtual_path = picnic_virtual_dir.join("Folder.png"); - let song = ctx.index.get_song(&song_virtual_path).unwrap(); + let song = ctx.index.get_song(&song_virtual_path).await.unwrap(); assert_eq!(song.path, song_virtual_path.to_string_lossy().as_ref()); assert_eq!(song.track_number, Some(5)); assert_eq!(song.disc_number, None); @@ -187,29 +213,31 @@ fn can_get_a_song() { ); } -#[test] -fn indexes_embedded_artwork() { +#[tokio::test] +async fn indexes_embedded_artwork() { let ctx = test::ContextBuilder::new(test_name!()) .mount(TEST_MOUNT_NAME, "test-data/small-collection") - .build(); + .build() + .await; - ctx.index.update().unwrap(); + ctx.index.update().await.unwrap(); let picnic_virtual_dir: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic"].iter().collect(); let song_virtual_path = picnic_virtual_dir.join("07 - なぜ (Why).mp3"); - let song = ctx.index.get_song(&song_virtual_path).unwrap(); + let song = ctx.index.get_song(&song_virtual_path).await.unwrap(); assert_eq!( song.artwork, Some(song_virtual_path.to_string_lossy().into_owned()) ); } -#[test] -fn album_art_pattern_is_case_insensitive() { +#[tokio::test] +async fn album_art_pattern_is_case_insensitive() { let ctx = test::ContextBuilder::new(test_name!()) .mount(TEST_MOUNT_NAME, "test-data/small-collection") - .build(); + .build() + .await; let patterns = vec!["folder", "FOLDER"]; @@ -219,12 +247,13 @@ fn album_art_pattern_is_case_insensitive() { album_art_pattern: Some(pattern.to_owned()), ..Default::default() }) + .await .unwrap(); - ctx.index.update().unwrap(); + ctx.index.update().await.unwrap(); let hunted_virtual_dir: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect(); let artwork_virtual_path = hunted_virtual_dir.join("Folder.jpg"); - let song = &ctx.index.flatten(&hunted_virtual_dir).unwrap()[0]; + let song = &ctx.index.flatten(&hunted_virtual_dir).await.unwrap()[0]; assert_eq!( song.artwork, Some(artwork_virtual_path.to_string_lossy().into_owned()) diff --git a/src/app/index/types.rs b/src/app/index/types.rs index 427cf90..f0bf7c8 100644 --- a/src/app/index/types.rs +++ b/src/app/index/types.rs @@ -2,7 +2,6 @@ use serde::{Deserialize, Serialize}; use std::path::Path; use crate::app::vfs::VFS; -use crate::db::songs; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum CollectionFile { @@ -10,23 +9,22 @@ pub enum CollectionFile { Song(Song), } -#[derive(Debug, PartialEq, Eq, Queryable, QueryableByName, Serialize, Deserialize)] -#[diesel(table_name = songs)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Song { #[serde(skip_serializing, skip_deserializing)] - id: i32, + pub id: i64, pub path: String, #[serde(skip_serializing, skip_deserializing)] pub parent: String, - pub track_number: Option<i32>, - pub disc_number: Option<i32>, + pub track_number: Option<i64>, + pub disc_number: Option<i64>, pub title: Option<String>, pub artist: Option<String>, pub album_artist: Option<String>, - pub year: Option<i32>, + pub year: Option<i64>, pub album: Option<String>, pub artwork: Option<String>, - pub duration: Option<i32>, + pub duration: Option<i64>, pub lyricist: Option<String>, pub composer: Option<String>, pub genre: Option<String>, @@ -49,18 +47,18 @@ impl Song { } } -#[derive(Debug, PartialEq, Eq, Queryable, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Directory { #[serde(skip_serializing, skip_deserializing)] - id: i32, + pub id: i64, pub path: String, #[serde(skip_serializing, skip_deserializing)] pub parent: Option<String>, pub artist: Option<String>, - pub year: Option<i32>, + pub year: Option<i64>, pub album: Option<String>, pub artwork: Option<String>, - pub date_added: i32, + pub date_added: i64, } impl Directory { diff --git a/src/app/index/update.rs b/src/app/index/update.rs index aa916ff..8064227 100644 --- a/src/app/index/update.rs +++ b/src/app/index/update.rs @@ -20,7 +20,7 @@ pub enum Error { #[error(transparent)] IndexClean(#[from] cleaner::Error), #[error(transparent)] - Database(#[from] diesel::result::Error), + Database(#[from] sqlx::Error), #[error(transparent)] DatabaseConnection(#[from] db::Error), #[error(transparent)] @@ -28,46 +28,44 @@ pub enum Error { } impl Index { - pub fn update(&self) -> Result<(), Error> { + pub async fn update(&self) -> Result<(), Error> { let start = time::Instant::now(); info!("Beginning library index update"); - let album_art_pattern = self.settings_manager.get_index_album_art_pattern().ok(); + let album_art_pattern = self + .settings_manager + .get_index_album_art_pattern() + .await + .ok(); let cleaner = Cleaner::new(self.db.clone(), self.vfs_manager.clone()); - cleaner.clean()?; + cleaner.clean().await?; - let (insert_sender, insert_receiver) = crossbeam_channel::unbounded(); - let inserter_db = self.db.clone(); - let insertion_thread = std::thread::spawn(move || { - let mut inserter = Inserter::new(inserter_db, insert_receiver); - inserter.insert(); + let (insert_sender, insert_receiver) = tokio::sync::mpsc::unbounded_channel(); + let insertion = tokio::spawn({ + let db = self.db.clone(); + async { + let mut inserter = Inserter::new(db, insert_receiver); + inserter.insert().await; + } }); let (collect_sender, collect_receiver) = crossbeam_channel::unbounded(); - let collector_thread = std::thread::spawn(move || { + let collection = tokio::task::spawn_blocking(|| { let collector = Collector::new(collect_receiver, insert_sender, album_art_pattern); collector.collect(); }); - let vfs = self.vfs_manager.get_vfs()?; - let traverser_thread = std::thread::spawn(move || { + let vfs = self.vfs_manager.get_vfs().await?; + let traversal = tokio::task::spawn_blocking(move || { let mounts = vfs.mounts(); let traverser = Traverser::new(collect_sender); traverser.traverse(mounts.iter().map(|p| p.source.clone()).collect()); }); - if let Err(e) = traverser_thread.join() { - error!("Error joining on traverser thread: {:?}", e); - } - - if let Err(e) = collector_thread.join() { - error!("Error joining on collector thread: {:?}", e); - } - - if let Err(e) = insertion_thread.join() { - error!("Error joining on inserter thread: {:?}", e); - } + traversal.await.unwrap(); + collection.await.unwrap(); + insertion.await.unwrap(); info!( "Library index update took {} seconds", diff --git a/src/app/index/update/cleaner.rs b/src/app/index/update/cleaner.rs index bc3bf69..6ce94f8 100644 --- a/src/app/index/update/cleaner.rs +++ b/src/app/index/update/cleaner.rs @@ -1,16 +1,16 @@ -use diesel::prelude::*; use rayon::prelude::*; +use sqlx::{QueryBuilder, Sqlite}; use std::path::Path; use crate::app::vfs; -use crate::db::{self, directories, songs, DB}; +use crate::db::{self, DB}; const INDEX_BUILDING_CLEAN_BUFFER_SIZE: usize = 500; // Deletions in each transaction #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] - Database(#[from] diesel::result::Error), + Database(#[from] sqlx::Error), #[error(transparent)] DatabaseConnection(#[from] db::Error), #[error(transparent)] @@ -29,19 +29,23 @@ impl Cleaner { Self { db, vfs_manager } } - pub fn clean(&self) -> Result<(), Error> { - let vfs = self.vfs_manager.get_vfs()?; + pub async fn clean(&self) -> Result<(), Error> { + let vfs = self.vfs_manager.get_vfs().await?; - let all_directories: Vec<String> = { - let mut connection = self.db.connect()?; - directories::table - .select(directories::path) - .load(&mut connection)? - }; + let (all_directories, all_songs) = { + let mut connection = self.db.connect().await?; - let all_songs: Vec<String> = { - let mut connection = self.db.connect()?; - songs::table.select(songs::path).load(&mut connection)? + let directories = sqlx::query_scalar!("SELECT path FROM directories") + .fetch_all(connection.as_mut()) + .await + .unwrap(); + + let songs = sqlx::query_scalar!("SELECT path FROM songs") + .fetch_all(connection.as_mut()) + .await + .unwrap(); + + (directories, songs) }; let list_missing_directories = || { @@ -69,14 +73,26 @@ impl Cleaner { thread_pool.join(list_missing_directories, list_missing_songs); { - let mut connection = self.db.connect()?; + let mut connection = self.db.connect().await?; + for chunk in missing_directories[..].chunks(INDEX_BUILDING_CLEAN_BUFFER_SIZE) { - diesel::delete(directories::table.filter(directories::path.eq_any(chunk))) - .execute(&mut connection)?; + QueryBuilder::<Sqlite>::new("DELETE FROM directories WHERE path IN ") + .push_tuples(chunk, |mut b, path| { + b.push_bind(path); + }) + .build() + .execute(connection.as_mut()) + .await?; } + for chunk in missing_songs[..].chunks(INDEX_BUILDING_CLEAN_BUFFER_SIZE) { - diesel::delete(songs::table.filter(songs::path.eq_any(chunk))) - .execute(&mut connection)?; + QueryBuilder::<Sqlite>::new("DELETE FROM songs WHERE path IN ") + .push_tuples(chunk, |mut b, path| { + b.push_bind(path); + }) + .build() + .execute(connection.as_mut()) + .await?; } } diff --git a/src/app/index/update/collector.rs b/src/app/index/update/collector.rs index c790086..89af45d 100644 --- a/src/app/index/update/collector.rs +++ b/src/app/index/update/collector.rs @@ -1,19 +1,18 @@ -use crossbeam_channel::{Receiver, Sender}; use log::error; use regex::Regex; use super::*; pub struct Collector { - receiver: Receiver<traverser::Directory>, - sender: Sender<inserter::Item>, + receiver: crossbeam_channel::Receiver<traverser::Directory>, + sender: tokio::sync::mpsc::UnboundedSender<inserter::Item>, album_art_pattern: Option<Regex>, } impl Collector { pub fn new( - receiver: Receiver<traverser::Directory>, - sender: Sender<inserter::Item>, + receiver: crossbeam_channel::Receiver<traverser::Directory>, + sender: tokio::sync::mpsc::UnboundedSender<inserter::Item>, album_art_pattern: Option<Regex>, ) -> Self { Self { diff --git a/src/app/index/update/inserter.rs b/src/app/index/update/inserter.rs index 4c73e44..63de282 100644 --- a/src/app/index/update/inserter.rs +++ b/src/app/index/update/inserter.rs @@ -1,13 +1,11 @@ -use crossbeam_channel::Receiver; -use diesel::prelude::*; use log::error; +use sqlx::{QueryBuilder, Sqlite}; +use tokio::sync::mpsc::UnboundedReceiver; -use crate::db::{directories, songs, DB}; +use crate::db::DB; const INDEX_BUILDING_INSERT_BUFFER_SIZE: usize = 1000; // Insertions in each transaction -#[derive(Debug, Insertable)] -#[diesel(table_name = songs)] pub struct Song { pub path: String, pub parent: String, @@ -26,8 +24,6 @@ pub struct Song { pub label: Option<String>, } -#[derive(Debug, Insertable)] -#[diesel(table_name = directories)] pub struct Directory { pub path: String, pub parent: Option<String>, @@ -44,14 +40,14 @@ pub enum Item { } pub struct Inserter { - receiver: Receiver<Item>, + receiver: UnboundedReceiver<Item>, new_directories: Vec<Directory>, new_songs: Vec<Song>, db: DB, } impl Inserter { - pub fn new(db: DB, receiver: Receiver<Item>) -> Self { + pub fn new(db: DB, receiver: UnboundedReceiver<Item>) -> Self { let new_directories = Vec::with_capacity(INDEX_BUILDING_INSERT_BUFFER_SIZE); let new_songs = Vec::with_capacity(INDEX_BUILDING_INSERT_BUFFER_SIZE); Self { @@ -62,63 +58,90 @@ impl Inserter { } } - pub fn insert(&mut self) { - while let Ok(item) = self.receiver.recv() { - self.insert_item(item); + pub async fn insert(&mut self) { + while let Some(item) = self.receiver.recv().await { + self.insert_item(item).await; } + self.flush_directories().await; + self.flush_songs().await; } - fn insert_item(&mut self, insert: Item) { + async fn insert_item(&mut self, insert: Item) { match insert { Item::Directory(d) => { self.new_directories.push(d); if self.new_directories.len() >= INDEX_BUILDING_INSERT_BUFFER_SIZE { - self.flush_directories(); + self.flush_directories().await; } } Item::Song(s) => { self.new_songs.push(s); if self.new_songs.len() >= INDEX_BUILDING_INSERT_BUFFER_SIZE { - self.flush_songs(); + self.flush_songs().await; } } }; } - fn flush_directories(&mut self) { - let res = self.db.connect().ok().and_then(|mut connection| { - diesel::insert_into(directories::table) - .values(&self.new_directories) - .execute(&mut *connection) // TODO https://github.com/diesel-rs/diesel/issues/1822 - .ok() - }); - if res.is_none() { - error!("Could not insert new directories in database"); - } - self.new_directories.clear(); + async fn flush_directories(&mut self) { + let Ok(mut connection) = self.db.connect().await else { + error!("Could not acquire connection to insert new directories in database"); + return; + }; + + let result = QueryBuilder::<Sqlite>::new( + "INSERT INTO directories(path, parent, artist, year, album, artwork, date_added) ", + ) + .push_values(&self.new_directories, |mut b, directory| { + b.push_bind(&directory.path) + .push_bind(&directory.parent) + .push_bind(&directory.artist) + .push_bind(directory.year) + .push_bind(&directory.album) + .push_bind(&directory.artwork) + .push_bind(directory.date_added); + }) + .build() + .execute(connection.as_mut()) + .await; + + match result { + Ok(_) => self.new_directories.clear(), + Err(_) => error!("Could not insert new directories in database"), + }; } - fn flush_songs(&mut self) { - let res = self.db.connect().ok().and_then(|mut connection| { - diesel::insert_into(songs::table) - .values(&self.new_songs) - .execute(&mut *connection) // TODO https://github.com/diesel-rs/diesel/issues/1822 - .ok() - }); - if res.is_none() { - error!("Could not insert new songs in database"); - } - self.new_songs.clear(); - } -} + async fn flush_songs(&mut self) { + let Ok(mut connection) = self.db.connect().await else { + error!("Could not acquire connection to insert new songs in database"); + return; + }; -impl Drop for Inserter { - fn drop(&mut self) { - if !self.new_directories.is_empty() { - self.flush_directories(); - } - if !self.new_songs.is_empty() { - self.flush_songs(); - } + let result = QueryBuilder::<Sqlite>::new("INSERT INTO songs(path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork, duration, lyricist, composer, genre, label) ") + .push_values(&self.new_songs, |mut b, song| { + b.push_bind(&song.path) + .push_bind(&song.parent) + .push_bind(song.track_number) + .push_bind(song.disc_number) + .push_bind(&song.title) + .push_bind(&song.artist) + .push_bind(&song.album_artist) + .push_bind(song.year) + .push_bind(&song.album) + .push_bind(&song.artwork) + .push_bind(song.duration) + .push_bind(&song.lyricist) + .push_bind(&song.composer) + .push_bind(&song.genre) + .push_bind(&song.label); + }) + .build() + .execute(connection.as_mut()) + .await; + + match result { + Ok(_) => self.new_songs.clear(), + Err(_) => error!("Could not insert new songs in database"), + }; } } diff --git a/src/app/lastfm.rs b/src/app/lastfm.rs index 74f060f..0ace649 100644 --- a/src/app/lastfm.rs +++ b/src/app/lastfm.rs @@ -44,7 +44,7 @@ impl Manager { .map_err(|e| e.into()) } - pub fn link(&self, username: &str, lastfm_token: &str) -> Result<(), Error> { + pub async fn link(&self, username: &str, lastfm_token: &str) -> Result<(), Error> { let mut scrobbler = Scrobbler::new(LASTFM_API_KEY, LASTFM_API_SECRET); let auth_response = scrobbler .authenticate_with_token(lastfm_token) @@ -52,28 +52,30 @@ impl Manager { self.user_manager .lastfm_link(username, &auth_response.name, &auth_response.key) + .await .map_err(|e| e.into()) } - pub fn unlink(&self, username: &str) -> Result<(), Error> { + pub async fn unlink(&self, username: &str) -> Result<(), Error> { self.user_manager .lastfm_unlink(username) + .await .map_err(|e| e.into()) } - pub fn scrobble(&self, username: &str, track: &Path) -> Result<(), Error> { + pub async fn scrobble(&self, username: &str, track: &Path) -> Result<(), Error> { let mut scrobbler = Scrobbler::new(LASTFM_API_KEY, LASTFM_API_SECRET); - let scrobble = self.scrobble_from_path(track)?; - let auth_token = self.user_manager.get_lastfm_session_key(username)?; + let scrobble = self.scrobble_from_path(track).await?; + let auth_token = self.user_manager.get_lastfm_session_key(username).await?; scrobbler.authenticate_with_session_key(&auth_token); scrobbler.scrobble(&scrobble).map_err(Error::Scrobble)?; Ok(()) } - pub fn now_playing(&self, username: &str, track: &Path) -> Result<(), Error> { + pub async fn now_playing(&self, username: &str, track: &Path) -> Result<(), Error> { let mut scrobbler = Scrobbler::new(LASTFM_API_KEY, LASTFM_API_SECRET); - let scrobble = self.scrobble_from_path(track)?; - let auth_token = self.user_manager.get_lastfm_session_key(username)?; + let scrobble = self.scrobble_from_path(track).await?; + let auth_token = self.user_manager.get_lastfm_session_key(username).await?; scrobbler.authenticate_with_session_key(&auth_token); scrobbler .now_playing(&scrobble) @@ -81,8 +83,8 @@ impl Manager { Ok(()) } - fn scrobble_from_path(&self, track: &Path) -> Result<Scrobble, Error> { - let song = self.index.get_song(track)?; + async fn scrobble_from_path(&self, track: &Path) -> Result<Scrobble, Error> { + let song = self.index.get_song(track).await?; Ok(Scrobble::new( song.artist.as_deref().unwrap_or(""), song.title.as_deref().unwrap_or(""), diff --git a/src/app/playlist.rs b/src/app/playlist.rs index a2addfc..a0cb18a 100644 --- a/src/app/playlist.rs +++ b/src/app/playlist.rs @@ -1,17 +1,14 @@ use core::clone::Clone; -use diesel::prelude::*; -use diesel::sql_types; -use diesel::BelongingToDsl; -use std::path::Path; +use sqlx::{Acquire, QueryBuilder, Sqlite}; use crate::app::index::Song; use crate::app::vfs; -use crate::db::{self, playlist_songs, playlists, users, DB}; +use crate::db::{self, DB}; #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] - Database(#[from] diesel::result::Error), + Database(#[from] sqlx::Error), #[error(transparent)] DatabaseConnection(#[from] db::Error), #[error("User not found")] @@ -33,148 +30,138 @@ impl Manager { Self { db, vfs_manager } } - pub fn list_playlists(&self, owner: &str) -> Result<Vec<String>, Error> { - let mut connection = self.db.connect()?; + pub async fn list_playlists(&self, owner: &str) -> Result<Vec<String>, Error> { + let mut connection = self.db.connect().await?; - let user: User = { - use self::users::dsl::*; - users - .filter(name.eq(owner)) - .select((id,)) - .first(&mut connection) - .optional()? - .ok_or(Error::UserNotFound)? - }; + let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner) + .fetch_optional(connection.as_mut()) + .await? + .ok_or(Error::UserNotFound)?; - { - use self::playlists::dsl::*; - let found_playlists: Vec<String> = Playlist::belonging_to(&user) - .select(name) - .load(&mut connection)?; - Ok(found_playlists) - } + Ok( + sqlx::query_scalar!("SELECT name FROM playlists WHERE owner = $1", user_id) + .fetch_all(connection.as_mut()) + .await?, + ) } - pub fn save_playlist( + pub async fn save_playlist( &self, playlist_name: &str, owner: &str, content: &[String], ) -> Result<(), Error> { - let new_playlist: NewPlaylist; - let playlist: Playlist; - let vfs = self.vfs_manager.get_vfs()?; + let vfs = self.vfs_manager.get_vfs().await?; - { - let mut connection = self.db.connect()?; - - // Find owner - let user: User = { - use self::users::dsl::*; - users - .filter(name.eq(owner)) - .select((id,)) - .first(&mut connection) - .optional()? - .ok_or(Error::UserNotFound)? - }; - - // Create playlist - new_playlist = NewPlaylist { - name: playlist_name.into(), - owner: user.id, - }; - - diesel::insert_into(playlists::table) - .values(&new_playlist) - .execute(&mut connection)?; - - playlist = { - use self::playlists::dsl::*; - playlists - .select((id, owner)) - .filter(name.eq(playlist_name).and(owner.eq(user.id))) - .get_result(&mut connection)? - } + struct PlaylistSong { + path: String, + ordering: i64, } - let mut new_songs: Vec<NewPlaylistSong> = Vec::with_capacity(content.len()); - + let mut new_songs: Vec<PlaylistSong> = Vec::with_capacity(content.len()); for (i, path) in content.iter().enumerate() { - let virtual_path = Path::new(&path); if let Some(real_path) = vfs - .virtual_to_real(virtual_path) + .virtual_to_real(path) .ok() .and_then(|p| p.to_str().map(|s| s.to_owned())) { - new_songs.push(NewPlaylistSong { - playlist: playlist.id, + new_songs.push(PlaylistSong { path: real_path, - ordering: i as i32, + ordering: i as i64, }); } } - { - let mut connection = self.db.connect()?; - connection.transaction::<_, diesel::result::Error, _>(|connection| { - // Delete old content (if any) - let old_songs = PlaylistSong::belonging_to(&playlist); - diesel::delete(old_songs).execute(connection)?; + // Create playlist + let mut connection = self.db.connect().await?; - // Insert content - diesel::insert_into(playlist_songs::table) - .values(&new_songs) - .execute(&mut *connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822 - Ok(()) - })?; + // Find owner + let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner) + .fetch_optional(connection.as_mut()) + .await? + .ok_or(Error::UserNotFound)?; + + sqlx::query!( + "INSERT INTO playlists (owner, name) VALUES($1, $2)", + user_id, + playlist_name + ) + .execute(connection.as_mut()) + .await?; + + let playlist_id = sqlx::query_scalar!( + "SELECT id FROM playlists WHERE owner = $1 AND name = $2", + user_id, + playlist_name + ) + .fetch_one(connection.as_mut()) + .await?; + + connection.acquire().await?; + + sqlx::query!( + "DELETE FROM playlist_songs WHERE playlist = $1", + playlist_id + ) + .execute(connection.as_mut()) + .await?; + + if !new_songs.is_empty() { + QueryBuilder::<Sqlite>::new("INSERT INTO playlist_songs (playlist, path, ordering) ") + .push_values(new_songs, |mut b, song| { + b.push_bind(playlist_id) + .push_bind(song.path) + .push_bind(song.ordering); + }) + .build() + .execute(connection.as_mut()) + .await?; } Ok(()) } - pub fn read_playlist(&self, playlist_name: &str, owner: &str) -> Result<Vec<Song>, Error> { - let vfs = self.vfs_manager.get_vfs()?; - let songs: Vec<Song>; + pub async fn read_playlist( + &self, + playlist_name: &str, + owner: &str, + ) -> Result<Vec<Song>, Error> { + let vfs = self.vfs_manager.get_vfs().await?; - { - let mut connection = self.db.connect()?; + let songs = { + let mut connection = self.db.connect().await?; // Find owner - let user: User = { - use self::users::dsl::*; - users - .filter(name.eq(owner)) - .select((id,)) - .first(&mut connection) - .optional()? - .ok_or(Error::UserNotFound)? - }; + let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner) + .fetch_optional(connection.as_mut()) + .await? + .ok_or(Error::UserNotFound)?; // Find playlist - let playlist: Playlist = { - use self::playlists::dsl::*; - playlists - .select((id, owner)) - .filter(name.eq(playlist_name).and(owner.eq(user.id))) - .get_result(&mut connection) - .optional()? - .ok_or(Error::PlaylistNotFound)? - }; + let playlist_id = sqlx::query_scalar!( + "SELECT id FROM playlists WHERE name = $1 and owner = $2", + playlist_name, + user_id + ) + .fetch_optional(connection.as_mut()) + .await? + .ok_or(Error::PlaylistNotFound)?; - // Select songs. Not using Diesel because we need to LEFT JOIN using a custom column - let query = diesel::sql_query( + // List songs + sqlx::query_as!( + Song, r#" - SELECT s.id, s.path, s.parent, s.track_number, s.disc_number, s.title, s.artist, s.album_artist, s.year, s.album, s.artwork, s.duration, s.lyricist, s.composer, s.genre, s.label - FROM playlist_songs ps - LEFT JOIN songs s ON ps.path = s.path - WHERE ps.playlist = ? - ORDER BY ps.ordering - "#, - ); - let query = query.bind::<sql_types::Integer, _>(playlist.id); - songs = query.get_results(&mut connection)?; - } + SELECT s.* + FROM playlist_songs ps + INNER JOIN songs s ON ps.path = s.path + WHERE ps.playlist = $1 + ORDER BY ps.ordering + "#, + playlist_id + ) + .fetch_all(connection.as_mut()) + .await? + }; // Map real path to virtual paths let virtual_songs = songs @@ -185,64 +172,30 @@ impl Manager { Ok(virtual_songs) } - pub fn delete_playlist(&self, playlist_name: &str, owner: &str) -> Result<(), Error> { - let mut connection = self.db.connect()?; + pub async fn delete_playlist(&self, playlist_name: &str, owner: &str) -> Result<(), Error> { + let mut connection = self.db.connect().await?; - let user: User = { - use self::users::dsl::*; - users - .filter(name.eq(owner)) - .select((id,)) - .first(&mut connection) - .optional()? - .ok_or(Error::UserNotFound)? - }; + let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner) + .fetch_optional(connection.as_mut()) + .await? + .ok_or(Error::UserNotFound)?; - { - use self::playlists::dsl::*; - let q = Playlist::belonging_to(&user).filter(name.eq(playlist_name)); - match diesel::delete(q).execute(&mut connection)? { - 0 => Err(Error::PlaylistNotFound), - _ => Ok(()), - } + let num_deletions = sqlx::query_scalar!( + "DELETE FROM playlists WHERE owner = $1 AND name = $2", + user_id, + playlist_name + ) + .execute(connection.as_mut()) + .await? + .rows_affected(); + + match num_deletions { + 0 => Err(Error::PlaylistNotFound), + _ => Ok(()), } } } -#[derive(Identifiable, Queryable, Associations)] -#[diesel(belongs_to(User, foreign_key = owner))] -struct Playlist { - id: i32, - owner: i32, -} - -#[derive(Identifiable, Queryable, Associations)] -#[diesel(belongs_to(Playlist, foreign_key = playlist))] -struct PlaylistSong { - id: i32, - playlist: i32, -} - -#[derive(Insertable)] -#[diesel(table_name = playlists)] -struct NewPlaylist { - name: String, - owner: i32, -} - -#[derive(Insertable)] -#[diesel(table_name = playlist_songs)] -struct NewPlaylistSong { - playlist: i32, - path: String, - ordering: i32, -} - -#[derive(Identifiable, Queryable)] -struct User { - id: i32, -} - #[cfg(test)] mod test { use std::path::{Path, PathBuf}; @@ -255,33 +208,41 @@ mod test { const TEST_PLAYLIST_NAME: &str = "Chill & Grill"; const TEST_MOUNT_NAME: &str = "root"; - #[test] - fn save_playlist_golden_path() { + #[tokio::test] + async fn save_playlist_golden_path() { let ctx = test::ContextBuilder::new(test_name!()) .user(TEST_USER, TEST_PASSWORD, false) - .build(); + .build() + .await; ctx.playlist_manager .save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &Vec::new()) + .await .unwrap(); - let found_playlists = ctx.playlist_manager.list_playlists(TEST_USER).unwrap(); + let found_playlists = ctx + .playlist_manager + .list_playlists(TEST_USER) + .await + .unwrap(); assert_eq!(found_playlists.len(), 1); assert_eq!(found_playlists[0], TEST_PLAYLIST_NAME); } - #[test] - fn save_playlist_is_idempotent() { + #[tokio::test] + async fn save_playlist_is_idempotent() { let ctx = test::ContextBuilder::new(test_name!()) .user(TEST_USER, TEST_PASSWORD, false) .mount(TEST_MOUNT_NAME, "test-data/small-collection") - .build(); + .build() + .await; - ctx.index.update().unwrap(); + ctx.index.update().await.unwrap(); let playlist_content: Vec<String> = ctx .index .flatten(Path::new(TEST_MOUNT_NAME)) + .await .unwrap() .into_iter() .map(|s| s.path) @@ -290,51 +251,63 @@ mod test { ctx.playlist_manager .save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content) + .await .unwrap(); ctx.playlist_manager .save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content) + .await .unwrap(); let songs = ctx .playlist_manager .read_playlist(TEST_PLAYLIST_NAME, TEST_USER) + .await .unwrap(); assert_eq!(songs.len(), 13); } - #[test] - fn delete_playlist_golden_path() { + #[tokio::test] + async fn delete_playlist_golden_path() { let ctx = test::ContextBuilder::new(test_name!()) .user(TEST_USER, TEST_PASSWORD, false) - .build(); + .build() + .await; let playlist_content = Vec::new(); ctx.playlist_manager .save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content) + .await .unwrap(); ctx.playlist_manager .delete_playlist(TEST_PLAYLIST_NAME, TEST_USER) + .await .unwrap(); - let found_playlists = ctx.playlist_manager.list_playlists(TEST_USER).unwrap(); + let found_playlists = ctx + .playlist_manager + .list_playlists(TEST_USER) + .await + .unwrap(); assert_eq!(found_playlists.len(), 0); } - #[test] - fn read_playlist_golden_path() { + #[tokio::test] + async fn read_playlist_golden_path() { let ctx = test::ContextBuilder::new(test_name!()) .user(TEST_USER, TEST_PASSWORD, false) .mount(TEST_MOUNT_NAME, "test-data/small-collection") - .build(); + .build() + .await; - ctx.index.update().unwrap(); + ctx.index.update().await.unwrap(); let playlist_content: Vec<String> = ctx .index .flatten(Path::new(TEST_MOUNT_NAME)) + .await .unwrap() .into_iter() .map(|s| s.path) @@ -343,11 +316,13 @@ mod test { ctx.playlist_manager .save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content) + .await .unwrap(); let songs = ctx .playlist_manager .read_playlist(TEST_PLAYLIST_NAME, TEST_USER) + .await .unwrap(); assert_eq!(songs.len(), 13); diff --git a/src/app/settings.rs b/src/app/settings.rs index 71ff6c7..9aca6ef 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -1,10 +1,8 @@ -use diesel::prelude::*; use regex::Regex; use serde::Deserialize; -use std::convert::TryInto; use std::time::Duration; -use crate::db::{self, misc_settings, DB}; +use crate::db::{self, DB}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -19,7 +17,7 @@ pub enum Error { #[error("Index album art pattern is not a valid regex")] IndexAlbumArtPatternInvalid, #[error(transparent)] - Database(#[from] diesel::result::Error), + Database(#[from] sqlx::Error), } #[derive(Clone, Default)] @@ -27,15 +25,15 @@ pub struct AuthSecret { pub key: [u8; 32], } -#[derive(Debug, Queryable)] +#[derive(Debug)] pub struct Settings { - pub index_sleep_duration_seconds: i32, + 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<i32>, + pub reindex_every_n_seconds: Option<i64>, pub album_art_pattern: Option<String>, } @@ -49,64 +47,57 @@ impl Manager { Self { db } } - pub fn get_auth_secret(&self) -> Result<AuthSecret, Error> { - use self::misc_settings::dsl::*; - let mut connection = self.db.connect()?; - let secret: Vec<u8> = misc_settings - .select(auth_secret) - .get_result(&mut connection) - .map_err(|e| match e { - diesel::result::Error::NotFound => Error::AuthenticationSecretNotFound, - e => e.into(), - })?; - secret + 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 fn get_index_sleep_duration(&self) -> Result<Duration, Error> { - let settings = self.read()?; + 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 fn get_index_album_art_pattern(&self) -> Result<Regex, Error> { - let settings = self.read()?; + 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 fn read(&self) -> Result<Settings, Error> { - use self::misc_settings::dsl::*; - let mut connection = self.db.connect()?; - - let settings: Settings = misc_settings - .select((index_sleep_duration_seconds, index_album_art_pattern)) - .get_result(&mut connection) - .map_err(|e| match e { - diesel::result::Error::NotFound => Error::MiscSettingsNotFound, - e => e.into(), - })?; - - Ok(settings) + 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 fn amend(&self, new_settings: &NewSettings) -> Result<(), Error> { - let mut connection = self.db.connect()?; + 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 { - diesel::update(misc_settings::table) - .set(misc_settings::index_sleep_duration_seconds.eq(sleep_duration)) - .execute(&mut connection)?; + 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 { - diesel::update(misc_settings::table) - .set(misc_settings::index_album_art_pattern.eq(album_art_pattern)) - .execute(&mut connection)?; + 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 7207a06..a00628c 100644 --- a/src/app/test.rs +++ b/src/app/test.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::app::{config, ddns, index::Index, lastfm, playlist, settings, thumbnail, user, vfs}; +use crate::app::{config, ddns, index::Index, playlist, settings, user, vfs}; use crate::db::DB; use crate::test::*; @@ -9,13 +9,10 @@ pub struct Context { pub index: Index, pub config_manager: config::Manager, pub ddns_manager: ddns::Manager, - pub lastfm_manager: lastfm::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, - pub test_directory: PathBuf, } pub struct ContextBuilder { @@ -53,14 +50,12 @@ impl ContextBuilder { }); self } - - pub fn build(self) -> Context { - let cache_output_dir = self.test_directory.join("cache"); + pub async fn build(self) -> Context { let db_path = self.test_directory.join("db.sqlite"); - let db = DB::new(&db_path).unwrap(); + let db = DB::new(&db_path).await.unwrap(); let settings_manager = settings::Manager::new(db.clone()); - let auth_secret = settings_manager.get_auth_secret().unwrap(); + 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()); @@ -72,23 +67,18 @@ impl ContextBuilder { ); let index = Index::new(db.clone(), vfs_manager.clone(), settings_manager.clone()); let playlist_manager = playlist::Manager::new(db.clone(), vfs_manager.clone()); - let thumbnail_manager = thumbnail::Manager::new(cache_output_dir); - let lastfm_manager = lastfm::Manager::new(index.clone(), user_manager.clone()); - config_manager.apply(&self.config).unwrap(); + config_manager.apply(&self.config).await.unwrap(); Context { db, index, config_manager, ddns_manager, - lastfm_manager, playlist_manager, settings_manager, - thumbnail_manager, user_manager, vfs_manager, - test_directory: self.test_directory, } } } diff --git a/src/app/user.rs b/src/app/user.rs index b925f83..16e8234 100644 --- a/src/app/user.rs +++ b/src/app/user.rs @@ -1,4 +1,3 @@ -use diesel::prelude::*; use pbkdf2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use pbkdf2::Pbkdf2; use rand::rngs::OsRng; @@ -6,12 +5,12 @@ use serde::{Deserialize, Serialize}; use std::time::{SystemTime, UNIX_EPOCH}; use crate::app::settings::AuthSecret; -use crate::db::{self, users, DB}; +use crate::db::{self, DB}; #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] - Database(#[from] diesel::result::Error), + Database(#[from] sqlx::Error), #[error(transparent)] DatabaseConnection(#[from] db::Error), #[error("Cannot use empty username")] @@ -36,12 +35,10 @@ pub enum Error { BrancaTokenEncoding, } -#[derive(Debug, Insertable, Queryable)] -#[diesel(table_name = users)] +#[derive(Debug)] pub struct User { pub name: String, - pub password_hash: String, - pub admin: i32, + pub admin: i64, } impl User { @@ -90,61 +87,62 @@ impl Manager { Self { db, auth_secret } } - pub fn create(&self, new_user: &NewUser) -> Result<(), Error> { + pub async fn create(&self, new_user: &NewUser) -> Result<(), Error> { if new_user.name.is_empty() { return Err(Error::EmptyUsername); } let password_hash = hash_password(&new_user.password)?; - let mut connection = self.db.connect()?; - let new_user = User { - name: new_user.name.to_owned(), + + sqlx::query!( + "INSERT INTO users (name, password_hash, admin) VALUES($1, $2, $3)", + new_user.name, password_hash, - admin: new_user.admin as i32, - }; + new_user.admin + ) + .execute(self.db.connect().await?.as_mut()) + .await?; - diesel::insert_into(users::table) - .values(&new_user) - .execute(&mut connection)?; Ok(()) } - pub fn delete(&self, username: &str) -> Result<(), Error> { - use crate::db::users::dsl::*; - let mut connection = self.db.connect()?; - diesel::delete(users.filter(name.eq(username))).execute(&mut connection)?; + pub async fn delete(&self, username: &str) -> Result<(), Error> { + sqlx::query!("DELETE FROM users WHERE name = $1", username) + .execute(self.db.connect().await?.as_mut()) + .await?; Ok(()) } - pub fn set_password(&self, username: &str, password: &str) -> Result<(), Error> { + pub async fn set_password(&self, username: &str, password: &str) -> Result<(), Error> { let hash = hash_password(password)?; - let mut connection = self.db.connect()?; - use crate::db::users::dsl::*; - diesel::update(users.filter(name.eq(username))) - .set(password_hash.eq(hash)) - .execute(&mut connection)?; + sqlx::query!( + "UPDATE users SET password_hash = $1 WHERE name = $2", + hash, + username + ) + .execute(self.db.connect().await?.as_mut()) + .await?; Ok(()) } - pub fn set_is_admin(&self, username: &str, is_admin: bool) -> Result<(), Error> { - use crate::db::users::dsl::*; - let mut connection = self.db.connect()?; - diesel::update(users.filter(name.eq(username))) - .set(admin.eq(is_admin as i32)) - .execute(&mut connection)?; + pub async fn set_is_admin(&self, username: &str, is_admin: bool) -> Result<(), Error> { + sqlx::query!( + "UPDATE users SET admin = $1 WHERE name = $2", + is_admin, + username + ) + .execute(self.db.connect().await?.as_mut()) + .await?; Ok(()) } - pub fn login(&self, username: &str, password: &str) -> Result<AuthToken, Error> { - use crate::db::users::dsl::*; - let mut connection = self.db.connect()?; - match users - .select(password_hash) - .filter(name.eq(username)) - .get_result(&mut connection) + pub async fn login(&self, username: &str, password: &str) -> Result<AuthToken, Error> { + match sqlx::query_scalar!("SELECT password_hash FROM users WHERE name = $1", username) + .fetch_optional(self.db.connect().await?.as_mut()) + .await? { - Err(diesel::result::Error::NotFound) => Err(Error::IncorrectUsername), - Ok(hash) => { + None => Err(Error::IncorrectUsername), + Some(hash) => { let hash: String = hash; if verify_password(&hash, password) { let authorization = Authorization { @@ -156,17 +154,16 @@ impl Manager { Err(Error::IncorrectPassword) } } - Err(e) => Err(e.into()), } } - pub fn authenticate( + pub async fn authenticate( &self, auth_token: &AuthToken, scope: AuthorizationScope, ) -> Result<Authorization, Error> { let authorization = self.decode_auth_token(auth_token, scope)?; - if self.exists(&authorization.username)? { + if self.exists(&authorization.username).await? { Ok(authorization) } else { Err(Error::IncorrectUsername) @@ -208,86 +205,76 @@ impl Manager { .map(AuthToken) } - pub fn count(&self) -> Result<i64, Error> { - use crate::db::users::dsl::*; - let mut connection = self.db.connect()?; - let count = users.count().get_result(&mut connection)?; + pub async fn count(&self) -> Result<i32, Error> { + let count = sqlx::query_scalar!("SELECT COUNT(*) FROM users") + .fetch_one(self.db.connect().await?.as_mut()) + .await?; Ok(count) } - pub fn list(&self) -> Result<Vec<User>, Error> { - use crate::db::users::dsl::*; - let mut connection = self.db.connect()?; - let listed_users = users - .select((name, password_hash, admin)) - .get_results(&mut connection)?; + pub async fn list(&self) -> Result<Vec<User>, Error> { + let listed_users = sqlx::query_as!(User, "SELECT name, admin FROM users") + .fetch_all(self.db.connect().await?.as_mut()) + .await?; Ok(listed_users) } - pub fn exists(&self, username: &str) -> Result<bool, Error> { - use crate::db::users::dsl::*; - let mut connection = self.db.connect()?; - let results: Vec<String> = users - .select(name) - .filter(name.eq(username)) - .get_results(&mut connection)?; - Ok(!results.is_empty()) + pub async fn exists(&self, username: &str) -> Result<bool, Error> { + Ok( + 0 < sqlx::query_scalar!("SELECT COUNT(*) FROM users WHERE name = $1", username) + .fetch_one(self.db.connect().await?.as_mut()) + .await?, + ) } - pub fn is_admin(&self, username: &str) -> Result<bool, Error> { - use crate::db::users::dsl::*; - let mut connection = self.db.connect()?; - let is_admin: i32 = users - .filter(name.eq(username)) - .select(admin) - .get_result(&mut connection)?; - Ok(is_admin != 0) + pub async fn is_admin(&self, username: &str) -> Result<bool, Error> { + Ok( + 0 < sqlx::query_scalar!("SELECT admin FROM users WHERE name = $1", username) + .fetch_one(self.db.connect().await?.as_mut()) + .await?, + ) } - pub fn read_preferences(&self, username: &str) -> Result<Preferences, Error> { - use crate::db::users::dsl::*; - let mut connection = self.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(&mut connection)?; - Ok(Preferences { - web_theme_base: theme_base, - web_theme_accent: theme_accent, - lastfm_username: read_lastfm_username, - }) + pub async fn read_preferences(&self, username: &str) -> Result<Preferences, Error> { + Ok(sqlx::query_as!( + Preferences, + "SELECT web_theme_base, web_theme_accent, lastfm_username FROM users WHERE name = $1", + username + ) + .fetch_one(self.db.connect().await?.as_mut()) + .await?) } - pub fn write_preferences( + pub async fn write_preferences( &self, username: &str, preferences: &Preferences, ) -> Result<(), Error> { - use crate::db::users::dsl::*; - let mut connection = self.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(&mut connection)?; + sqlx::query!( + "UPDATE users SET web_theme_base = $1, web_theme_accent = $2 WHERE name = $3", + preferences.web_theme_base, + preferences.web_theme_accent, + username + ) + .execute(self.db.connect().await?.as_mut()) + .await?; Ok(()) } - pub fn lastfm_link( + pub async fn lastfm_link( &self, username: &str, lastfm_login: &str, session_key: &str, ) -> Result<(), Error> { - use crate::db::users::dsl::*; - let mut connection = self.db.connect()?; - diesel::update(users.filter(name.eq(username))) - .set(( - lastfm_username.eq(lastfm_login), - lastfm_session_key.eq(session_key), - )) - .execute(&mut connection)?; + sqlx::query!( + "UPDATE users SET lastfm_username = $1, lastfm_session_key = $2 WHERE name = $3", + lastfm_login, + session_key, + username + ) + .execute(self.db.connect().await?.as_mut()) + .await?; Ok(()) } @@ -298,27 +285,29 @@ impl Manager { }) } - pub fn get_lastfm_session_key(&self, username: &str) -> Result<String, Error> { - use crate::db::users::dsl::*; - let mut connection = self.db.connect()?; - let token: Option<String> = users - .filter(name.eq(username)) - .select(lastfm_session_key) - .get_result(&mut connection)?; + pub async fn get_lastfm_session_key(&self, username: &str) -> Result<String, Error> { + let token: Option<String> = sqlx::query_scalar!( + "SELECT lastfm_session_key FROM users WHERE name = $1", + username + ) + .fetch_one(self.db.connect().await?.as_mut()) + .await?; token.ok_or(Error::MissingLastFMSessionKey) } - pub fn is_lastfm_linked(&self, username: &str) -> bool { - self.get_lastfm_session_key(username).is_ok() + pub async fn is_lastfm_linked(&self, username: &str) -> bool { + self.get_lastfm_session_key(username).await.is_ok() } - pub fn lastfm_unlink(&self, username: &str) -> Result<(), Error> { - use crate::db::users::dsl::*; - let mut connection = self.db.connect()?; + pub async fn lastfm_unlink(&self, username: &str) -> Result<(), Error> { let null: Option<String> = None; - diesel::update(users.filter(name.eq(username))) - .set((lastfm_session_key.eq(&null), lastfm_username.eq(&null))) - .execute(&mut connection)?; + sqlx::query!( + "UPDATE users SET lastfm_session_key = $1, lastfm_username = $1 WHERE name = $2", + null, + username + ) + .execute(self.db.connect().await?.as_mut()) + .await?; Ok(()) } } @@ -352,9 +341,9 @@ mod test { const TEST_USERNAME: &str = "Walter"; const TEST_PASSWORD: &str = "super_secret!"; - #[test] - fn create_delete_user_golden_path() { - let ctx = test::ContextBuilder::new(test_name!()).build(); + #[tokio::test] + async fn create_delete_user_golden_path() { + let ctx = test::ContextBuilder::new(test_name!()).build().await; let new_user = NewUser { name: TEST_USERNAME.to_owned(), @@ -362,56 +351,56 @@ mod test { admin: false, }; - ctx.user_manager.create(&new_user).unwrap(); - assert_eq!(ctx.user_manager.list().unwrap().len(), 1); + ctx.user_manager.create(&new_user).await.unwrap(); + assert_eq!(ctx.user_manager.list().await.unwrap().len(), 1); - ctx.user_manager.delete(&new_user.name).unwrap(); - assert_eq!(ctx.user_manager.list().unwrap().len(), 0); + ctx.user_manager.delete(&new_user.name).await.unwrap(); + assert_eq!(ctx.user_manager.list().await.unwrap().len(), 0); } - #[test] - fn cannot_create_user_with_blank_username() { - let ctx = test::ContextBuilder::new(test_name!()).build(); + #[tokio::test] + async fn cannot_create_user_with_blank_username() { + let ctx = test::ContextBuilder::new(test_name!()).build().await; let new_user = NewUser { name: "".to_owned(), password: TEST_PASSWORD.to_owned(), admin: false, }; assert!(matches!( - ctx.user_manager.create(&new_user).unwrap_err(), + ctx.user_manager.create(&new_user).await.unwrap_err(), Error::EmptyUsername )); } - #[test] - fn cannot_create_user_with_blank_password() { - let ctx = test::ContextBuilder::new(test_name!()).build(); + #[tokio::test] + async fn cannot_create_user_with_blank_password() { + let ctx = test::ContextBuilder::new(test_name!()).build().await; let new_user = NewUser { name: TEST_USERNAME.to_owned(), password: "".to_owned(), admin: false, }; assert!(matches!( - ctx.user_manager.create(&new_user).unwrap_err(), + ctx.user_manager.create(&new_user).await.unwrap_err(), Error::EmptyPassword )); } - #[test] - fn cannot_create_duplicate_user() { - let ctx = test::ContextBuilder::new(test_name!()).build(); + #[tokio::test] + async fn cannot_create_duplicate_user() { + let ctx = test::ContextBuilder::new(test_name!()).build().await; let new_user = NewUser { name: TEST_USERNAME.to_owned(), password: TEST_PASSWORD.to_owned(), admin: false, }; - ctx.user_manager.create(&new_user).unwrap(); - ctx.user_manager.create(&new_user).unwrap_err(); + ctx.user_manager.create(&new_user).await.unwrap(); + ctx.user_manager.create(&new_user).await.unwrap_err(); } - #[test] - fn can_read_write_preferences() { - let ctx = test::ContextBuilder::new(test_name!()).build(); + #[tokio::test] + async fn can_read_write_preferences() { + let ctx = test::ContextBuilder::new(test_name!()).build().await; let new_preferences = Preferences { web_theme_base: Some("very-dark-theme".to_owned()), @@ -424,19 +413,20 @@ mod test { password: TEST_PASSWORD.to_owned(), admin: false, }; - ctx.user_manager.create(&new_user).unwrap(); + ctx.user_manager.create(&new_user).await.unwrap(); ctx.user_manager .write_preferences(TEST_USERNAME, &new_preferences) + .await .unwrap(); - let read_preferences = ctx.user_manager.read_preferences("Walter").unwrap(); + let read_preferences = ctx.user_manager.read_preferences("Walter").await.unwrap(); assert_eq!(new_preferences, read_preferences); } - #[test] - fn login_rejects_bad_password() { - let ctx = test::ContextBuilder::new(test_name!()).build(); + #[tokio::test] + async fn login_rejects_bad_password() { + let ctx = test::ContextBuilder::new(test_name!()).build().await; let new_user = NewUser { name: TEST_USERNAME.to_owned(), @@ -444,30 +434,35 @@ mod test { admin: false, }; - ctx.user_manager.create(&new_user).unwrap(); + ctx.user_manager.create(&new_user).await.unwrap(); assert!(matches!( ctx.user_manager .login(TEST_USERNAME, "not the password") + .await .unwrap_err(), Error::IncorrectPassword )); } - #[test] - fn login_golden_path() { - let ctx = test::ContextBuilder::new(test_name!()).build(); + #[tokio::test] + async fn login_golden_path() { + let ctx = test::ContextBuilder::new(test_name!()).build().await; let new_user = NewUser { name: TEST_USERNAME.to_owned(), password: TEST_PASSWORD.to_owned(), admin: false, }; - ctx.user_manager.create(&new_user).unwrap(); - assert!(ctx.user_manager.login(TEST_USERNAME, TEST_PASSWORD).is_ok()) + ctx.user_manager.create(&new_user).await.unwrap(); + assert!(ctx + .user_manager + .login(TEST_USERNAME, TEST_PASSWORD) + .await + .is_ok()) } - #[test] - fn authenticate_rejects_bad_token() { - let ctx = test::ContextBuilder::new(test_name!()).build(); + #[tokio::test] + async fn authenticate_rejects_bad_token() { + let ctx = test::ContextBuilder::new(test_name!()).build().await; let new_user = NewUser { name: TEST_USERNAME.to_owned(), @@ -475,17 +470,18 @@ mod test { admin: false, }; - ctx.user_manager.create(&new_user).unwrap(); + ctx.user_manager.create(&new_user).await.unwrap(); let fake_token = AuthToken("fake token".to_owned()); assert!(ctx .user_manager .authenticate(&fake_token, AuthorizationScope::PolarisAuth) + .await .is_err()) } - #[test] - fn authenticate_golden_path() { - let ctx = test::ContextBuilder::new(test_name!()).build(); + #[tokio::test] + async fn authenticate_golden_path() { + let ctx = test::ContextBuilder::new(test_name!()).build().await; let new_user = NewUser { name: TEST_USERNAME.to_owned(), @@ -493,14 +489,16 @@ mod test { admin: false, }; - ctx.user_manager.create(&new_user).unwrap(); + ctx.user_manager.create(&new_user).await.unwrap(); let token = ctx .user_manager .login(TEST_USERNAME, TEST_PASSWORD) + .await .unwrap(); let authorization = ctx .user_manager .authenticate(&token, AuthorizationScope::PolarisAuth) + .await .unwrap(); assert_eq!( authorization, @@ -511,9 +509,9 @@ mod test { ) } - #[test] - fn authenticate_validates_scope() { - let ctx = test::ContextBuilder::new(test_name!()).build(); + #[tokio::test] + async fn authenticate_validates_scope() { + let ctx = test::ContextBuilder::new(test_name!()).build().await; let new_user = NewUser { name: TEST_USERNAME.to_owned(), @@ -521,14 +519,15 @@ mod test { admin: false, }; - ctx.user_manager.create(&new_user).unwrap(); + ctx.user_manager.create(&new_user).await.unwrap(); let token = ctx .user_manager .generate_lastfm_link_token(TEST_USERNAME) .unwrap(); let authorization = ctx .user_manager - .authenticate(&token, AuthorizationScope::PolarisAuth); + .authenticate(&token, AuthorizationScope::PolarisAuth) + .await; assert!(matches!( authorization.unwrap_err(), Error::IncorrectAuthorizationScope diff --git a/src/app/vfs.rs b/src/app/vfs.rs index efd825b..c304515 100644 --- a/src/app/vfs.rs +++ b/src/app/vfs.rs @@ -1,10 +1,10 @@ use core::ops::Deref; -use diesel::prelude::*; use regex::Regex; use serde::{Deserialize, Serialize}; +use sqlx::{Acquire, QueryBuilder, Sqlite}; use std::path::{self, Path, PathBuf}; -use crate::db::{self, mount_points, DB}; +use crate::db::{self, DB}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -15,11 +15,10 @@ pub enum Error { #[error(transparent)] DatabaseConnection(#[from] db::Error), #[error(transparent)] - Database(#[from] diesel::result::Error), + Database(#[from] sqlx::Error), } -#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Eq, Queryable, Serialize)] -#[diesel(table_name = mount_points)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] pub struct MountDir { pub source: String, pub name: String, @@ -98,31 +97,39 @@ impl Manager { Self { db } } - pub fn get_vfs(&self) -> Result<VFS, Error> { - let mount_dirs = self.mount_dirs()?; + 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 fn mount_dirs(&self) -> Result<Vec<MountDir>, Error> { - use self::mount_points::dsl::*; - let mut connection = self.db.connect()?; - let mount_dirs: Vec<MountDir> = mount_points - .select((source, name)) - .get_results(&mut connection)?; - Ok(mount_dirs) + 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 fn set_mount_dirs(&self, mount_dirs: &[MountDir]) -> Result<(), Error> { - let mut connection = self.db.connect()?; - connection.transaction::<_, diesel::result::Error, _>(|connection| { - use self::mount_points::dsl::*; - diesel::delete(mount_points).execute(&mut *connection)?; - diesel::insert_into(mount_points) - .values(mount_dirs) - .execute(&mut *connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822 - Ok(()) - })?; + 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(()) } } diff --git a/src/db.rs b/src/db.rs index 4f9fb36..cce477e 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,15 +1,13 @@ -use diesel::r2d2::{self, ConnectionManager, PooledConnection}; -use diesel::sqlite::SqliteConnection; -use diesel::RunQueryDsl; -use diesel_migrations::EmbeddedMigrations; -use diesel_migrations::MigrationHarness; use std::path::{Path, PathBuf}; -mod schema; +use sqlx::{ + migrate::Migrator, + pool::PoolConnection, + sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqliteSynchronous}, + Sqlite, +}; -pub use self::schema::*; - -const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); +static MIGRATOR: Migrator = sqlx::migrate!("src/db"); #[derive(thiserror::Error, Debug)] pub enum Error { @@ -25,74 +23,46 @@ pub enum Error { #[derive(Clone)] pub struct DB { - pool: r2d2::Pool<ConnectionManager<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(diesel::r2d2::Error::QueryError)?; - Ok(()) - } + pool: SqlitePool, } impl DB { - pub fn new(path: &Path) -> Result<DB, Error> { + 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 manager = ConnectionManager::<SqliteConnection>::new(path.to_string_lossy()); - let pool = diesel::r2d2::Pool::builder() - .connection_customizer(Box::new(ConnectionCustomizer {})) - .build(manager) - .or(Err(Error::ConnectionPoolBuild))?; + + 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()?; + db.migrate_up().await?; Ok(db) } - pub fn connect(&self) -> Result<PooledConnection<ConnectionManager<SqliteConnection>>, Error> { - self.pool.get().or(Err(Error::ConnectionPool)) + pub async fn connect(&self) -> Result<PoolConnection<Sqlite>, Error> { + self.pool.acquire().await.map_err(|_| Error::ConnectionPool) } - #[cfg(test)] - fn migrate_down(&self) -> Result<(), Error> { - let mut connection = self.connect()?; - connection - .revert_all_migrations(MIGRATIONS) - .and(Ok(())) - .or(Err(Error::Migration)) - } - - fn migrate_up(&self) -> Result<(), Error> { - let mut connection = self.connect()?; - connection - .run_pending_migrations(MIGRATIONS) + async fn migrate_up(&self) -> Result<(), Error> { + MIGRATOR + .run(&self.pool) + .await .and(Ok(())) .or(Err(Error::Migration)) } } -#[test] -fn run_migrations() { +#[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).unwrap(); - - db.migrate_down().unwrap(); - db.migrate_up().unwrap(); + 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 new file mode 100644 index 0000000..d4b9c60 --- /dev/null +++ b/src/db/20240711080449_init.sql @@ -0,0 +1,95 @@ +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, + lastfm_username TEXT, + lastfm_session_key TEXT, + web_theme_base TEXT, + web_theme_accent TEXT, + UNIQUE(name) +); + +CREATE TABLE directories ( + id INTEGER PRIMARY KEY NOT NULL, + path TEXT NOT NULL, + parent TEXT, + artist TEXT, + year INTEGER, + album TEXT, + artwork TEXT, + date_added INTEGER DEFAULT 0 NOT NULL, + UNIQUE(path) ON CONFLICT REPLACE +); + +CREATE TABLE songs ( + id INTEGER PRIMARY KEY NOT NULL, + path TEXT NOT NULL, + parent TEXT NOT NULL, + track_number INTEGER, + disc_number INTEGER, + title TEXT, + artist TEXT, + album_artist TEXT, + year INTEGER, + album TEXT, + artwork TEXT, + duration INTEGER, + lyricist TEXT, + composer TEXT, + genre TEXT, + label TEXT, + UNIQUE(path) ON CONFLICT REPLACE +); + +CREATE TABLE playlists ( + id INTEGER PRIMARY KEY NOT NULL, + owner INTEGER NOT NULL, + name TEXT NOT NULL, + FOREIGN KEY(owner) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE(owner, name) ON CONFLICT REPLACE +); + +CREATE TABLE playlist_songs ( + id INTEGER PRIMARY KEY NOT NULL, + playlist INTEGER NOT NULL, + path TEXT NOT NULL, + ordering INTEGER NOT NULL, + FOREIGN KEY(playlist) REFERENCES playlists(id) ON DELETE CASCADE ON UPDATE CASCADE, + UNIQUE(playlist, ordering) ON CONFLICT REPLACE +); diff --git a/src/db/schema.rs b/src/db/schema.rs deleted file mode 100644 index dd1b512..0000000 --- a/src/db/schema.rs +++ /dev/null @@ -1,103 +0,0 @@ -table! { - ddns_config (id) { - id -> Integer, - host -> Text, - username -> Text, - password -> Text, - } -} - -table! { - directories (id) { - id -> Integer, - path -> Text, - parent -> Nullable<Text>, - artist -> Nullable<Text>, - year -> Nullable<Integer>, - album -> Nullable<Text>, - artwork -> Nullable<Text>, - date_added -> Integer, - } -} - -table! { - misc_settings (id) { - id -> Integer, - auth_secret -> Binary, - index_sleep_duration_seconds -> Integer, - index_album_art_pattern -> Text, - } -} - -table! { - mount_points (id) { - id -> Integer, - source -> Text, - name -> Text, - } -} - -table! { - playlist_songs (id) { - id -> Integer, - playlist -> Integer, - path -> Text, - ordering -> Integer, - } -} - -table! { - playlists (id) { - id -> Integer, - owner -> Integer, - name -> Text, - } -} - -table! { - songs (id) { - id -> Integer, - path -> Text, - parent -> Text, - track_number -> Nullable<Integer>, - disc_number -> Nullable<Integer>, - title -> Nullable<Text>, - artist -> Nullable<Text>, - album_artist -> Nullable<Text>, - year -> Nullable<Integer>, - album -> Nullable<Text>, - artwork -> Nullable<Text>, - duration -> Nullable<Integer>, - lyricist -> Nullable<Text>, - composer -> Nullable<Text>, - genre -> Nullable<Text>, - label -> Nullable<Text>, - } -} - -table! { - users (id) { - id -> Integer, - name -> Text, - password_hash -> Text, - admin -> Integer, - lastfm_username -> Nullable<Text>, - lastfm_session_key -> Nullable<Text>, - web_theme_base -> Nullable<Text>, - web_theme_accent -> Nullable<Text>, - } -} - -joinable!(playlist_songs -> playlists (playlist)); -joinable!(playlists -> users (owner)); - -allow_tables_to_appear_in_same_query!( - ddns_config, - directories, - misc_settings, - mount_points, - playlist_songs, - playlists, - songs, - users, -); diff --git a/src/main.rs b/src/main.rs index df07abc..a265812 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,8 @@ #![cfg_attr(all(windows, feature = "ui"), windows_subsystem = "windows")] #![recursion_limit = "256"] -#[macro_use] -extern crate diesel; -#[macro_use] -extern crate diesel_migrations; - -use log::info; +use log::{error, info}; +use options::CLIOptions; use simplelog::{ ColorChoice, CombinedLogger, LevelFilter, SharedLogger, TermLogger, TerminalMode, WriteLogger, }; @@ -27,6 +23,8 @@ mod utils; pub enum Error { #[error(transparent)] App(#[from] app::Error), + #[error("Could not start web services")] + ServiceStartup(std::io::Error), #[error("Could not parse command line arguments:\n\n{0}")] CliArgsParsing(getopts::Fail), #[cfg(unix)] @@ -139,16 +137,21 @@ fn main() -> Result<(), Error> { info!("Swagger files location is {:#?}", paths.swagger_dir_path); info!("Web client files location is {:#?}", paths.web_dir_path); + async_main(cli_options, paths) +} + +#[tokio::main] +async fn async_main(cli_options: CLIOptions, paths: paths::Paths) -> Result<(), Error> { // Create and run app - let app = app::App::new(cli_options.port.unwrap_or(5050), paths)?; + let app = app::App::new(cli_options.port.unwrap_or(5050), paths).await?; app.index.begin_periodic_updates(); app.ddns_manager.begin_periodic_updates(); // Start server info!("Starting up server"); - std::thread::spawn(move || { - let _ = service::run(app); - }); + if let Err(e) = service::launch(app) { + return Err(Error::ServiceStartup(e)); + } // Send readiness notification #[cfg(unix)] diff --git a/src/paths.rs b/src/paths.rs index 207eab5..601681b 100644 --- a/src/paths.rs +++ b/src/paths.rs @@ -80,23 +80,23 @@ impl Paths { pub fn new(cli_options: &CLIOptions) -> Self { let mut paths = Self::from_build(); if let Some(path) = &cli_options.cache_dir_path { - paths.cache_dir_path = path.clone(); + path.clone_into(&mut paths.cache_dir_path); } if let Some(path) = &cli_options.config_file_path { paths.config_file_path = Some(path.clone()); } if let Some(path) = &cli_options.database_file_path { - paths.db_file_path = path.clone(); + path.clone_into(&mut paths.db_file_path); } #[cfg(unix)] if let Some(path) = &cli_options.pid_file_path { - paths.pid_file_path = path.clone(); + path.clone_into(&mut paths.pid_file_path); } if let Some(path) = &cli_options.swagger_dir_path { - paths.swagger_dir_path = path.clone(); + path.clone_into(&mut paths.swagger_dir_path); } if let Some(path) = &cli_options.web_dir_path { - paths.web_dir_path = path.clone(); + path.clone_into(&mut paths.web_dir_path); } let log_to_file = cli_options.log_file_path.is_some() || !cli_options.foreground; diff --git a/src/service/actix.rs b/src/service/actix.rs index 0fce678..56de21d 100644 --- a/src/service/actix.rs +++ b/src/service/actix.rs @@ -1,7 +1,6 @@ use actix_web::{ dev::Service, middleware::{Compress, Logger, NormalizePath}, - rt::System, web::{self, ServiceConfig}, App as ActixApp, HttpServer, }; @@ -43,9 +42,9 @@ pub fn make_config(app: App) -> impl FnOnce(&mut ServiceConfig) + Clone { } } -pub fn run(app: App) -> Result<(), std::io::Error> { +pub fn launch(app: App) -> Result<(), std::io::Error> { let address = ("0.0.0.0", app.port); - System::new().block_on( + tokio::spawn( HttpServer::new(move || { ActixApp::new() .wrap(Logger::default()) @@ -72,5 +71,6 @@ pub fn run(app: App) -> Result<(), std::io::Error> { e })? .run(), - ) + ); + Ok(()) } diff --git a/src/service/actix/api.rs b/src/service/actix/api.rs index d3600ee..a84fd50 100644 --- a/src/service/actix/api.rs +++ b/src/service/actix/api.rs @@ -16,7 +16,7 @@ use base64::prelude::*; use futures_util::future::err; use percent_encoding::percent_decode_str; use std::future::Future; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::pin::Pin; use std::str; @@ -141,25 +141,27 @@ impl FromRequest for Auth { // Auth via bearer token in query parameter if let Ok(query) = query_params_future.await { let auth_token = user::AuthToken(query.auth_token.clone()); - let authorization = block(move || { - user_manager.authenticate(&auth_token, user::AuthorizationScope::PolarisAuth) - }) - .await?; - return Ok(Auth { - username: authorization.username, - }); + if let Ok(auth) = user_manager + .authenticate(&auth_token, user::AuthorizationScope::PolarisAuth) + .await + { + return Ok(Auth { + username: auth.username, + }); + } } // Auth via bearer token in authorization header if let Ok(bearer_auth) = bearer_auth_future.await { let auth_token = user::AuthToken(bearer_auth.token().to_owned()); - let authorization = block(move || { - user_manager.authenticate(&auth_token, user::AuthorizationScope::PolarisAuth) - }) - .await?; - return Ok(Auth { - username: authorization.username, - }); + if let Ok(auth) = user_manager + .authenticate(&auth_token, user::AuthorizationScope::PolarisAuth) + .await + { + return Ok(Auth { + username: auth.username, + }); + } } Err(ErrorUnauthorized(APIError::AuthenticationRequired)) @@ -185,21 +187,18 @@ impl FromRequest for AdminRights { let auth_future = Auth::from_request(request, payload); Box::pin(async move { - let user_manager_count = user_manager.clone(); - let user_count = block(move || user_manager_count.count()).await; + let user_count = user_manager.count().await; match user_count { - Err(e) => return Err(e.into()), + Err(_) => return Err(ErrorInternalServerError(APIError::Internal)), Ok(0) => return Ok(AdminRights { auth: None }), _ => (), }; let auth = auth_future.await?; - let username = auth.username.clone(); - let is_admin = block(move || user_manager.is_admin(&username)).await?; - if is_admin { - Ok(AdminRights { auth: Some(auth) }) - } else { - Err(ErrorForbidden(APIError::AdminPermissionRequired)) + match user_manager.is_admin(&auth.username).await { + Ok(true) => Ok(AdminRights { auth: Some(auth) }), + Ok(false) => Err(ErrorForbidden(APIError::AdminPermissionRequired)), + Err(_) => Err(ErrorInternalServerError(APIError::Internal)), } }) } @@ -228,18 +227,6 @@ impl Responder for MediaFile { } } -async fn block<F, I, E>(f: F) -> Result<I, APIError> -where - F: FnOnce() -> Result<I, E> + Send + 'static, - I: Send + 'static, - E: Send + std::fmt::Debug + 'static + Into<APIError>, -{ - actix_web::web::block(f) - .await - .map_err(|_| APIError::Internal) - .and_then(|r| r.map_err(|e| e.into())) -} - #[get("/version")] async fn version() -> Json<dto::Version> { let current_version = dto::Version { @@ -253,14 +240,13 @@ async fn version() -> Json<dto::Version> { async fn initial_setup( user_manager: Data<user::Manager>, ) -> Result<Json<dto::InitialSetup>, APIError> { - let initial_setup = block(move || -> Result<dto::InitialSetup, APIError> { - let users = user_manager.list()?; + let initial_setup = { + let users = user_manager.list().await?; let has_any_admin = users.iter().any(|u| u.is_admin()); - Ok(dto::InitialSetup { + dto::InitialSetup { has_any_users: has_any_admin, - }) - }) - .await?; + } + }; Ok(Json(initial_setup)) } @@ -270,7 +256,7 @@ async fn apply_config( config_manager: Data<config::Manager>, config: Json<dto::Config>, ) -> Result<HttpResponse, APIError> { - block(move || config_manager.apply(&config.to_owned().into())).await?; + config_manager.apply(&config.to_owned().into()).await?; Ok(HttpResponse::new(StatusCode::OK)) } @@ -279,7 +265,7 @@ async fn get_settings( settings_manager: Data<settings::Manager>, _admin_rights: AdminRights, ) -> Result<Json<dto::Settings>, APIError> { - let settings = block(move || settings_manager.read()).await?; + let settings = settings_manager.read().await?; Ok(Json(settings.into())) } @@ -289,7 +275,9 @@ async fn put_settings( settings_manager: Data<settings::Manager>, new_settings: Json<dto::NewSettings>, ) -> Result<HttpResponse, APIError> { - block(move || settings_manager.amend(&new_settings.to_owned().into())).await?; + settings_manager + .amend(&new_settings.to_owned().into()) + .await?; Ok(HttpResponse::new(StatusCode::OK)) } @@ -298,7 +286,7 @@ async fn list_mount_dirs( vfs_manager: Data<vfs::Manager>, _admin_rights: AdminRights, ) -> Result<Json<Vec<dto::MountDir>>, APIError> { - let mount_dirs = block(move || vfs_manager.mount_dirs()).await?; + let mount_dirs = vfs_manager.mount_dirs().await?; let mount_dirs = mount_dirs.into_iter().map(|m| m.into()).collect(); Ok(Json(mount_dirs)) } @@ -310,7 +298,7 @@ async fn put_mount_dirs( new_mount_dirs: Json<Vec<dto::MountDir>>, ) -> Result<HttpResponse, APIError> { let new_mount_dirs: Vec<MountDir> = new_mount_dirs.iter().cloned().map(|m| m.into()).collect(); - block(move || vfs_manager.set_mount_dirs(&new_mount_dirs)).await?; + vfs_manager.set_mount_dirs(&new_mount_dirs).await?; Ok(HttpResponse::new(StatusCode::OK)) } @@ -319,7 +307,7 @@ async fn get_ddns_config( ddns_manager: Data<ddns::Manager>, _admin_rights: AdminRights, ) -> Result<Json<dto::DDNSConfig>, APIError> { - let ddns_config = block(move || ddns_manager.config()).await?; + let ddns_config = ddns_manager.config().await?; Ok(Json(ddns_config.into())) } @@ -329,7 +317,9 @@ async fn put_ddns_config( ddns_manager: Data<ddns::Manager>, new_ddns_config: Json<dto::DDNSConfig>, ) -> Result<HttpResponse, APIError> { - block(move || ddns_manager.set_config(&new_ddns_config.to_owned().into())).await?; + ddns_manager + .set_config(&new_ddns_config.to_owned().into()) + .await?; Ok(HttpResponse::new(StatusCode::OK)) } @@ -338,7 +328,7 @@ async fn list_users( user_manager: Data<user::Manager>, _admin_rights: AdminRights, ) -> Result<Json<Vec<dto::User>>, APIError> { - let users = block(move || user_manager.list()).await?; + let users = user_manager.list().await?; let users = users.into_iter().map(|u| u.into()).collect(); Ok(Json(users)) } @@ -350,7 +340,7 @@ async fn create_user( new_user: Json<dto::NewUser>, ) -> Result<HttpResponse, APIError> { let new_user = new_user.to_owned().into(); - block(move || user_manager.create(&new_user)).await?; + user_manager.create(&new_user).await?; Ok(HttpResponse::new(StatusCode::OK)) } @@ -367,16 +357,14 @@ async fn update_user( } } - block(move || -> Result<(), APIError> { - if let Some(password) = &user_update.new_password { - user_manager.set_password(&name, password)?; - } - if let Some(is_admin) = &user_update.new_is_admin { - user_manager.set_is_admin(&name, *is_admin)?; - } - Ok(()) - }) - .await?; + if let Some(password) = &user_update.new_password { + user_manager.set_password(&name, password).await?; + } + + if let Some(is_admin) = &user_update.new_is_admin { + user_manager.set_is_admin(&name, *is_admin).await?; + } + Ok(HttpResponse::new(StatusCode::OK)) } @@ -391,7 +379,7 @@ async fn delete_user( return Err(APIError::DeletingOwnAccount); } } - block(move || user_manager.delete(&name)).await?; + user_manager.delete(&name).await?; Ok(HttpResponse::new(StatusCode::OK)) } @@ -400,7 +388,7 @@ async fn get_preferences( user_manager: Data<user::Manager>, auth: Auth, ) -> Result<Json<user::Preferences>, APIError> { - let preferences = block(move || user_manager.read_preferences(&auth.username)).await?; + let preferences = user_manager.read_preferences(&auth.username).await?; Ok(Json(preferences)) } @@ -410,7 +398,9 @@ async fn put_preferences( auth: Auth, preferences: Json<user::Preferences>, ) -> Result<HttpResponse, APIError> { - block(move || user_manager.write_preferences(&auth.username, &preferences)).await?; + user_manager + .write_preferences(&auth.username, &preferences) + .await?; Ok(HttpResponse::new(StatusCode::OK)) } @@ -429,18 +419,18 @@ async fn login( credentials: Json<dto::Credentials>, ) -> Result<HttpResponse, APIError> { let username = credentials.username.clone(); - let (user::AuthToken(token), is_admin) = - block(move || -> Result<(user::AuthToken, bool), APIError> { - let auth_token = user_manager.login(&credentials.username, &credentials.password)?; - let is_admin = user_manager.is_admin(&credentials.username)?; - Ok((auth_token, is_admin)) - }) + + let user::AuthToken(token) = user_manager + .login(&credentials.username, &credentials.password) .await?; + let is_admin = user_manager.is_admin(&credentials.username).await?; + let authorization = dto::Authorization { username: username.clone(), token, is_admin, }; + let response = HttpResponse::Ok().json(authorization); Ok(response) } @@ -450,7 +440,7 @@ async fn browse_root( index: Data<Index>, _auth: Auth, ) -> Result<Json<Vec<index::CollectionFile>>, APIError> { - let result = block(move || index.browse(Path::new(""))).await?; + let result = index.browse(Path::new("")).await?; Ok(Json(result)) } @@ -460,17 +450,14 @@ async fn browse( _auth: Auth, path: web::Path<String>, ) -> Result<Json<Vec<index::CollectionFile>>, APIError> { - let result = block(move || { - let path = percent_decode_str(&path).decode_utf8_lossy(); - index.browse(Path::new(path.as_ref())) - }) - .await?; + let path = percent_decode_str(&path).decode_utf8_lossy(); + let result = index.browse(Path::new(path.as_ref())).await?; Ok(Json(result)) } #[get("/flatten")] async fn flatten_root(index: Data<Index>, _auth: Auth) -> Result<Json<Vec<index::Song>>, APIError> { - let songs = block(move || index.flatten(Path::new(""))).await?; + let songs = index.flatten(Path::new("")).await?; Ok(Json(songs)) } @@ -480,23 +467,20 @@ async fn flatten( _auth: Auth, path: web::Path<String>, ) -> Result<Json<Vec<index::Song>>, APIError> { - let songs = block(move || { - let path = percent_decode_str(&path).decode_utf8_lossy(); - index.flatten(Path::new(path.as_ref())) - }) - .await?; + let path = percent_decode_str(&path).decode_utf8_lossy(); + let songs = index.flatten(Path::new(path.as_ref())).await?; Ok(Json(songs)) } #[get("/random")] async fn random(index: Data<Index>, _auth: Auth) -> Result<Json<Vec<index::Directory>>, APIError> { - let result = block(move || index.get_random_albums(20)).await?; + let result = index.get_random_albums(20).await?; Ok(Json(result)) } #[get("/recent")] async fn recent(index: Data<Index>, _auth: Auth) -> Result<Json<Vec<index::Directory>>, APIError> { - let result = block(move || index.get_recent_albums(20)).await?; + let result = index.get_recent_albums(20).await?; Ok(Json(result)) } @@ -505,7 +489,7 @@ async fn search_root( index: Data<Index>, _auth: Auth, ) -> Result<Json<Vec<index::CollectionFile>>, APIError> { - let result = block(move || index.search("")).await?; + let result = index.search("").await?; Ok(Json(result)) } @@ -515,7 +499,7 @@ async fn search( _auth: Auth, query: web::Path<String>, ) -> Result<Json<Vec<index::CollectionFile>>, APIError> { - let result = block(move || index.search(&query)).await?; + let result = index.search(&query).await?; Ok(Json(result)) } @@ -525,13 +509,9 @@ async fn get_audio( _auth: Auth, path: web::Path<String>, ) -> Result<MediaFile, APIError> { - let audio_path = block(move || { - let vfs = vfs_manager.get_vfs()?; - let path = percent_decode_str(&path).decode_utf8_lossy(); - vfs.virtual_to_real(Path::new(path.as_ref())) - }) - .await?; - + let vfs = vfs_manager.get_vfs().await?; + let path = percent_decode_str(&path).decode_utf8_lossy(); + let audio_path = vfs.virtual_to_real(Path::new(path.as_ref()))?; let named_file = NamedFile::open(audio_path).map_err(|_| APIError::AudioFileIOError)?; Ok(MediaFile::new(named_file)) } @@ -545,19 +525,11 @@ async fn get_thumbnail( options_input: web::Query<dto::ThumbnailOptions>, ) -> Result<MediaFile, APIError> { let options = thumbnail::Options::from(options_input.0); - - let thumbnail_path = block(move || -> Result<PathBuf, APIError> { - let vfs = vfs_manager.get_vfs()?; - let path = percent_decode_str(&path).decode_utf8_lossy(); - let image_path = vfs.virtual_to_real(Path::new(path.as_ref()))?; - thumbnails_manager - .get_thumbnail(&image_path, &options) - .map_err(|e| e.into()) - }) - .await?; - + let vfs = vfs_manager.get_vfs().await?; + let path = percent_decode_str(&path).decode_utf8_lossy(); + let image_path = vfs.virtual_to_real(Path::new(path.as_ref()))?; + let thumbnail_path = thumbnails_manager.get_thumbnail(&image_path, &options)?; let named_file = NamedFile::open(thumbnail_path).map_err(|_| APIError::ThumbnailFileIOError)?; - Ok(MediaFile::new(named_file)) } @@ -566,7 +538,7 @@ async fn list_playlists( playlist_manager: Data<playlist::Manager>, auth: Auth, ) -> Result<Json<Vec<dto::ListPlaylistsEntry>>, APIError> { - let playlist_names = block(move || playlist_manager.list_playlists(&auth.username)).await?; + let playlist_names = playlist_manager.list_playlists(&auth.username).await?; let playlists: Vec<dto::ListPlaylistsEntry> = playlist_names .into_iter() .map(|p| dto::ListPlaylistsEntry { name: p }) @@ -582,7 +554,9 @@ async fn save_playlist( name: web::Path<String>, playlist: Json<dto::SavePlaylistInput>, ) -> Result<HttpResponse, APIError> { - block(move || playlist_manager.save_playlist(&name, &auth.username, &playlist.tracks)).await?; + playlist_manager + .save_playlist(&name, &auth.username, &playlist.tracks) + .await?; Ok(HttpResponse::new(StatusCode::OK)) } @@ -592,7 +566,9 @@ async fn read_playlist( auth: Auth, name: web::Path<String>, ) -> Result<Json<Vec<index::Song>>, APIError> { - let songs = block(move || playlist_manager.read_playlist(&name, &auth.username)).await?; + let songs = playlist_manager + .read_playlist(&name, &auth.username) + .await?; Ok(Json(songs)) } @@ -602,7 +578,9 @@ async fn delete_playlist( auth: Auth, name: web::Path<String>, ) -> Result<HttpResponse, APIError> { - block(move || playlist_manager.delete_playlist(&name, &auth.username)).await?; + playlist_manager + .delete_playlist(&name, &auth.username) + .await?; Ok(HttpResponse::new(StatusCode::OK)) } @@ -613,15 +591,13 @@ async fn lastfm_now_playing( auth: Auth, path: web::Path<String>, ) -> Result<HttpResponse, APIError> { - block(move || -> Result<(), APIError> { - if !user_manager.is_lastfm_linked(&auth.username) { - return Err(APIError::LastFMAccountNotLinked); - } - let path = percent_decode_str(&path).decode_utf8_lossy(); - lastfm_manager.now_playing(&auth.username, Path::new(path.as_ref()))?; - Ok(()) - }) - .await?; + if !user_manager.is_lastfm_linked(&auth.username).await { + return Err(APIError::LastFMAccountNotLinked); + } + let path = percent_decode_str(&path).decode_utf8_lossy(); + lastfm_manager + .now_playing(&auth.username, Path::new(path.as_ref())) + .await?; Ok(HttpResponse::new(StatusCode::OK)) } @@ -632,15 +608,13 @@ async fn lastfm_scrobble( auth: Auth, path: web::Path<String>, ) -> Result<HttpResponse, APIError> { - block(move || -> Result<(), APIError> { - if !user_manager.is_lastfm_linked(&auth.username) { - return Err(APIError::LastFMAccountNotLinked); - } - let path = percent_decode_str(&path).decode_utf8_lossy(); - lastfm_manager.scrobble(&auth.username, Path::new(path.as_ref()))?; - Ok(()) - }) - .await?; + if !user_manager.is_lastfm_linked(&auth.username).await { + return Err(APIError::LastFMAccountNotLinked); + } + let path = percent_decode_str(&path).decode_utf8_lossy(); + lastfm_manager + .scrobble(&auth.username, Path::new(path.as_ref())) + .await?; Ok(HttpResponse::new(StatusCode::OK)) } @@ -649,8 +623,7 @@ async fn lastfm_link_token( lastfm_manager: Data<lastfm::Manager>, auth: Auth, ) -> Result<Json<dto::LastFMLinkToken>, APIError> { - let user::AuthToken(value) = - block(move || lastfm_manager.generate_link_token(&auth.username)).await?; + let user::AuthToken(value) = lastfm_manager.generate_link_token(&auth.username)?; Ok(Json(dto::LastFMLinkToken { value })) } @@ -660,27 +633,27 @@ async fn lastfm_link( user_manager: Data<user::Manager>, payload: web::Query<dto::LastFMLink>, ) -> Result<HttpResponse, APIError> { - let popup_content_string = block(move || { - let auth_token = user::AuthToken(payload.auth_token.clone()); - let authorization = - user_manager.authenticate(&auth_token, user::AuthorizationScope::LastFMLink)?; - let lastfm_token = &payload.token; - lastfm_manager.link(&authorization.username, lastfm_token)?; + let auth_token = user::AuthToken(payload.auth_token.clone()); + let authorization = user_manager + .authenticate(&auth_token, user::AuthorizationScope::LastFMLink) + .await?; + let lastfm_token = &payload.token; + lastfm_manager + .link(&authorization.username, lastfm_token) + .await?; - // Percent decode - let base64_content = percent_decode_str(&payload.content).decode_utf8_lossy(); + // Percent decode + let base64_content = percent_decode_str(&payload.content).decode_utf8_lossy(); - // Base64 decode - let popup_content = BASE64_STANDARD_NO_PAD - .decode(base64_content.as_bytes()) - .map_err(|_| APIError::LastFMLinkContentBase64DecodeError)?; + // Base64 decode + let popup_content = BASE64_STANDARD_NO_PAD + .decode(base64_content.as_bytes()) + .map_err(|_| APIError::LastFMLinkContentBase64DecodeError)?; - // UTF-8 decode - str::from_utf8(&popup_content) - .map_err(|_| APIError::LastFMLinkContentEncodingError) - .map(|s| s.to_owned()) - }) - .await?; + // UTF-8 decode + let popup_content_string = str::from_utf8(&popup_content) + .map_err(|_| APIError::LastFMLinkContentEncodingError) + .map(|s| s.to_owned())?; Ok(HttpResponse::build(StatusCode::OK) .content_type("text/html; charset=utf-8") @@ -692,6 +665,6 @@ async fn lastfm_unlink( lastfm_manager: Data<lastfm::Manager>, auth: Auth, ) -> Result<HttpResponse, APIError> { - block(move || lastfm_manager.unlink(&auth.username)).await?; + lastfm_manager.unlink(&auth.username).await?; Ok(HttpResponse::new(StatusCode::OK)) } diff --git a/src/service/actix/test.rs b/src/service/actix/test.rs index 3a2c94d..7312f56 100644 --- a/src/service/actix/test.rs +++ b/src/service/actix/test.rs @@ -1,7 +1,6 @@ use actix_test::TestServer; use actix_web::{ middleware::{Compress, Logger}, - rt::{System, SystemRunner}, web::Bytes, App as ActixApp, }; @@ -18,7 +17,6 @@ use crate::service::test::TestService; use crate::test::*; pub struct ActixTestService { - system_runner: SystemRunner, authorization: Option<dto::Authorization>, server: TestServer, } @@ -26,7 +24,7 @@ pub struct ActixTestService { pub type ServiceType = ActixTestService; impl ActixTestService { - fn process_internal<T: Serialize + Clone + 'static>( + async fn process_internal<T: Serialize + Clone + 'static>( &mut self, request: &Request<T>, ) -> (Builder, Option<Bytes>) { @@ -50,9 +48,7 @@ impl ActixTestService { actix_request = actix_request.bearer_auth(&authorization.token); } - let mut actix_response = self - .system_runner - .block_on(async move { actix_request.send_json(&body).await.unwrap() }); + let mut actix_response = actix_request.send_json(&body).await.unwrap(); let mut response_builder = Response::builder().status(actix_response.status()); let headers = response_builder.headers_mut().unwrap(); @@ -62,10 +58,7 @@ impl ActixTestService { let is_success = actix_response.status().is_success(); let body = if is_success { - Some( - self.system_runner - .block_on(async move { actix_response.body().await.unwrap() }), - ) + Some(actix_response.body().await.unwrap()) } else { None }; @@ -75,7 +68,7 @@ impl ActixTestService { } impl TestService for ActixTestService { - fn new(test_name: &str) -> Self { + async fn new(test_name: &str) -> Self { let output_dir = prepare_test_directory(test_name); let paths = Paths { @@ -89,9 +82,8 @@ impl TestService for ActixTestService { web_dir_path: ["test-data", "web"].iter().collect(), }; - let app = App::new(5050, paths).unwrap(); + let app = App::new(5050, paths).await.unwrap(); - let system_runner = System::new(); let server = actix_test::start(move || { let config = make_config(app.clone()); ActixApp::new() @@ -102,31 +94,33 @@ impl TestService for ActixTestService { ActixTestService { authorization: None, - system_runner, server, } } - fn fetch<T: Serialize + Clone + 'static>(&mut self, request: &Request<T>) -> Response<()> { - let (response_builder, _body) = self.process_internal(request); + async fn fetch<T: Serialize + Clone + 'static>( + &mut self, + request: &Request<T>, + ) -> Response<()> { + let (response_builder, _body) = self.process_internal(request).await; response_builder.body(()).unwrap() } - fn fetch_bytes<T: Serialize + Clone + 'static>( + async fn fetch_bytes<T: Serialize + Clone + 'static>( &mut self, request: &Request<T>, ) -> Response<Vec<u8>> { - let (response_builder, body) = self.process_internal(request); + let (response_builder, body) = self.process_internal(request).await; response_builder .body(body.unwrap().deref().to_owned()) .unwrap() } - fn fetch_json<T: Serialize + Clone + 'static, U: DeserializeOwned>( + async fn fetch_json<T: Serialize + Clone + 'static, U: DeserializeOwned>( &mut self, request: &Request<T>, ) -> Response<U> { - let (response_builder, body) = self.process_internal(request); + let (response_builder, body) = self.process_internal(request).await; let body = serde_json::from_slice(&body.unwrap()).unwrap(); response_builder.body(body).unwrap() } diff --git a/src/service/dto.rs b/src/service/dto.rs index 8bd79a3..161fb3b 100644 --- a/src/service/dto.rs +++ b/src/service/dto.rs @@ -139,9 +139,9 @@ pub struct DDNSConfig { impl From<DDNSConfig> for ddns::Config { fn from(c: DDNSConfig) -> Self { Self { - host: c.host, - username: c.username, - password: c.password, + ddns_host: c.host, + ddns_username: c.username, + ddns_password: c.password, } } } @@ -149,9 +149,9 @@ impl From<DDNSConfig> for ddns::Config { impl From<ddns::Config> for DDNSConfig { fn from(c: ddns::Config) -> Self { Self { - host: c.host, - username: c.username, - password: c.password, + host: c.ddns_host, + username: c.ddns_username, + password: c.ddns_password, } } } @@ -204,7 +204,7 @@ impl From<Config> for config::Config { #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct NewSettings { pub album_art_pattern: Option<String>, - pub reindex_every_n_seconds: Option<i32>, + pub reindex_every_n_seconds: Option<i64>, } impl From<NewSettings> for settings::NewSettings { @@ -219,7 +219,7 @@ impl From<NewSettings> for settings::NewSettings { #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct Settings { pub album_art_pattern: String, - pub reindex_every_n_seconds: i32, + pub reindex_every_n_seconds: i64, } impl From<settings::Settings> for Settings { diff --git a/src/service/error.rs b/src/service/error.rs index 0f6ee4e..18bd337 100644 --- a/src/service/error.rs +++ b/src/service/error.rs @@ -18,7 +18,7 @@ pub enum APIError { #[error("Could not encode Branca token")] BrancaTokenEncoding, #[error("Database error:\n\n{0}")] - Database(diesel::result::Error), + Database(sqlx::Error), #[error("DDNS update query failed with HTTP status {0}")] DdnsUpdateQueryFailed(u16), #[error("Cannot delete your own account")] diff --git a/src/service/test.rs b/src/service/test.rs index 0544306..6a35276 100644 --- a/src/service/test.rs +++ b/src/service/test.rs @@ -26,18 +26,19 @@ use crate::service::test::constants::*; pub use crate::service::actix::test::ServiceType; pub trait TestService { - fn new(test_name: &str) -> Self; - fn fetch<T: Serialize + Clone + 'static>(&mut self, request: &Request<T>) -> Response<()>; - fn fetch_bytes<T: Serialize + Clone + 'static>( + async fn new(test_name: &str) -> Self; + async fn fetch<T: Serialize + Clone + 'static>(&mut self, request: &Request<T>) + -> Response<()>; + async fn fetch_bytes<T: Serialize + Clone + 'static>( &mut self, request: &Request<T>, ) -> Response<Vec<u8>>; - fn fetch_json<T: Serialize + Clone + 'static, U: DeserializeOwned>( + async fn fetch_json<T: Serialize + Clone + 'static, U: DeserializeOwned>( &mut self, request: &Request<T>, ) -> Response<U>; - fn complete_initial_setup(&mut self) { + async fn complete_initial_setup(&mut self) { let configuration = dto::Config { users: Some(vec![ dto::NewUser { @@ -58,40 +59,43 @@ pub trait TestService { ..Default::default() }; let request = protocol::apply_config(configuration); - let response = self.fetch(&request); + let response = self.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } - fn login_internal(&mut self, username: &str, password: &str) { + async fn login_internal(&mut self, username: &str, password: &str) { let request = protocol::login(username, password); - let response = self.fetch_json::<_, dto::Authorization>(&request); + let response = self.fetch_json::<_, dto::Authorization>(&request).await; assert_eq!(response.status(), StatusCode::OK); let authorization = response.into_body(); self.set_authorization(Some(authorization)); } - fn login_admin(&mut self) { - self.login_internal(TEST_USERNAME_ADMIN, TEST_PASSWORD_ADMIN); + async fn login_admin(&mut self) { + self.login_internal(TEST_USERNAME_ADMIN, TEST_PASSWORD_ADMIN) + .await; } - fn login(&mut self) { - self.login_internal(TEST_USERNAME, TEST_PASSWORD); + async fn login(&mut self) { + self.login_internal(TEST_USERNAME, TEST_PASSWORD).await; } - fn logout(&mut self) { + async fn logout(&mut self) { self.set_authorization(None); } fn set_authorization(&mut self, authorization: Option<dto::Authorization>); - fn index(&mut self) { + async fn index(&mut self) { let request = protocol::trigger_index(); - let response = self.fetch(&request); + let response = self.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); loop { let browse_request = protocol::browse(Path::new("")); - let response = self.fetch_json::<(), Vec<index::CollectionFile>>(&browse_request); + let response = self + .fetch_json::<(), Vec<index::CollectionFile>>(&browse_request) + .await; let entries = response.body(); if !entries.is_empty() { break; @@ -101,7 +105,9 @@ pub trait TestService { loop { let flatten_request = protocol::flatten(Path::new("")); - let response = self.fetch_json::<_, Vec<index::Song>>(&flatten_request); + let response = self + .fetch_json::<_, Vec<index::Song>>(&flatten_request) + .await; let entries = response.body(); if !entries.is_empty() { break; diff --git a/src/service/test/admin.rs b/src/service/test/admin.rs index f5e679f..64e1f3b 100644 --- a/src/service/test/admin.rs +++ b/src/service/test/admin.rs @@ -5,20 +5,20 @@ use crate::service::dto; use crate::service::test::{protocol, ServiceType, TestService}; use crate::test_name; -#[test] -fn returns_api_version() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn returns_api_version() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::version(); - let response = service.fetch_json::<_, dto::Version>(&request); + let response = service.fetch_json::<_, dto::Version>(&request).await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn initial_setup_golden_path() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn initial_setup_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::initial_setup(); { - let response = service.fetch_json::<_, dto::InitialSetup>(&request); + let response = service.fetch_json::<_, dto::InitialSetup>(&request).await; assert_eq!(response.status(), StatusCode::OK); let initial_setup = response.body(); assert_eq!( @@ -28,9 +28,9 @@ fn initial_setup_golden_path() { } ); } - service.complete_initial_setup(); + service.complete_initial_setup().await; { - let response = service.fetch_json::<_, dto::InitialSetup>(&request); + let response = service.fetch_json::<_, dto::InitialSetup>(&request).await; assert_eq!(response.status(), StatusCode::OK); let initial_setup = response.body(); assert_eq!( @@ -42,40 +42,44 @@ fn initial_setup_golden_path() { } } -#[test] -fn trigger_index_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); +#[actix_web::test] +async fn trigger_index_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; let request = protocol::random(); - let response = service.fetch_json::<_, Vec<index::Directory>>(&request); + let response = service + .fetch_json::<_, Vec<index::Directory>>(&request) + .await; let entries = response.body(); assert_eq!(entries.len(), 0); - service.index(); + service.index().await; - let response = service.fetch_json::<_, Vec<index::Directory>>(&request); + let response = service + .fetch_json::<_, Vec<index::Directory>>(&request) + .await; let entries = response.body(); assert_eq!(entries.len(), 3); } -#[test] -fn trigger_index_requires_auth() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn trigger_index_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let request = protocol::trigger_index(); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn trigger_index_requires_admin() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn trigger_index_requires_admin() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let request = protocol::trigger_index(); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::FORBIDDEN); } diff --git a/src/service/test/auth.rs b/src/service/test/auth.rs index 70fb278..852dd3b 100644 --- a/src/service/test/auth.rs +++ b/src/service/test/auth.rs @@ -5,33 +5,33 @@ use crate::service::dto; use crate::service::test::{constants::*, protocol, ServiceType, TestService}; use crate::test_name; -#[test] -fn login_rejects_bad_username() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn login_rejects_bad_username() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let request = protocol::login("garbage", TEST_PASSWORD); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn login_rejects_bad_password() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn login_rejects_bad_password() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let request = protocol::login(TEST_USERNAME, "garbage"); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn login_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn login_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let request = protocol::login(TEST_USERNAME, TEST_PASSWORD); - let response = service.fetch_json::<_, dto::Authorization>(&request); + let response = service.fetch_json::<_, dto::Authorization>(&request).await; assert_eq!(response.status(), StatusCode::OK); let authorization = response.body(); @@ -40,73 +40,73 @@ fn login_golden_path() { assert!(!authorization.token.is_empty()); } -#[test] -fn authentication_via_bearer_http_header_rejects_bad_token() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn authentication_via_bearer_http_header_rejects_bad_token() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let mut request = protocol::random(); let bearer = headers::Authorization::bearer("garbage").unwrap(); request.headers_mut().typed_insert(bearer); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn authentication_via_bearer_http_header_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn authentication_via_bearer_http_header_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let authorization = { let request = protocol::login(TEST_USERNAME, TEST_PASSWORD); - let response = service.fetch_json::<_, dto::Authorization>(&request); + let response = service.fetch_json::<_, dto::Authorization>(&request).await; assert_eq!(response.status(), StatusCode::OK); response.into_body() }; - service.logout(); + service.logout().await; let mut request = protocol::random(); let bearer = headers::Authorization::bearer(&authorization.token).unwrap(); request.headers_mut().typed_insert(bearer); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn authentication_via_query_param_rejects_bad_token() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn authentication_via_query_param_rejects_bad_token() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let mut request = protocol::random(); *request.uri_mut() = (request.uri().to_string() + "?auth_token=garbage-token") .parse() .unwrap(); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn authentication_via_query_param_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn authentication_via_query_param_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let authorization = { let request = protocol::login(TEST_USERNAME, TEST_PASSWORD); - let response = service.fetch_json::<_, dto::Authorization>(&request); + let response = service.fetch_json::<_, dto::Authorization>(&request).await; assert_eq!(response.status(), StatusCode::OK); response.into_body() }; - service.logout(); + service.logout().await; let mut request = protocol::random(); *request.uri_mut() = format!("{}?auth_token={}", request.uri(), authorization.token) .parse() .unwrap(); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } diff --git a/src/service/test/collection.rs b/src/service/test/collection.rs index a95aa75..0e5e614 100644 --- a/src/service/test/collection.rs +++ b/src/service/test/collection.rs @@ -5,214 +5,230 @@ use crate::app::index; use crate::service::test::{add_trailing_slash, constants::*, protocol, ServiceType, TestService}; use crate::test_name; -#[test] -fn browse_requires_auth() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn browse_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::browse(&PathBuf::new()); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn browse_root() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +#[actix_web::test] +async fn browse_root() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let request = protocol::browse(&PathBuf::new()); - let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request); + let response = service + .fetch_json::<_, Vec<index::CollectionFile>>(&request) + .await; assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 1); } -#[test] -fn browse_directory() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +#[actix_web::test] +async fn browse_directory() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect(); let request = protocol::browse(&path); - let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request); + let response = service + .fetch_json::<_, Vec<index::CollectionFile>>(&request) + .await; assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 5); } -#[test] -fn browse_bad_directory() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn browse_bad_directory() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let path: PathBuf = ["not_my_collection"].iter().collect(); let request = protocol::browse(&path); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::NOT_FOUND); } -#[test] -fn flatten_requires_auth() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn flatten_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::flatten(&PathBuf::new()); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn flatten_root() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +#[actix_web::test] +async fn flatten_root() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let request = protocol::flatten(&PathBuf::new()); - let response = service.fetch_json::<_, Vec<index::Song>>(&request); + let response = service.fetch_json::<_, Vec<index::Song>>(&request).await; assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 13); } -#[test] -fn flatten_directory() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +#[actix_web::test] +async fn flatten_directory() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let request = protocol::flatten(Path::new(TEST_MOUNT_NAME)); - let response = service.fetch_json::<_, Vec<index::Song>>(&request); + let response = service.fetch_json::<_, Vec<index::Song>>(&request).await; assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 13); } -#[test] -fn flatten_bad_directory() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn flatten_bad_directory() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let path: PathBuf = ["not_my_collection"].iter().collect(); let request = protocol::flatten(&path); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::NOT_FOUND); } -#[test] -fn random_requires_auth() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn random_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::random(); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn random_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +#[actix_web::test] +async fn random_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let request = protocol::random(); - let response = service.fetch_json::<_, Vec<index::Directory>>(&request); + let response = service + .fetch_json::<_, Vec<index::Directory>>(&request) + .await; assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 3); } -#[test] -fn random_with_trailing_slash() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +#[actix_web::test] +async fn random_with_trailing_slash() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let mut request = protocol::random(); add_trailing_slash(&mut request); - let response = service.fetch_json::<_, Vec<index::Directory>>(&request); + let response = service + .fetch_json::<_, Vec<index::Directory>>(&request) + .await; assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 3); } -#[test] -fn recent_requires_auth() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn recent_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::recent(); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn recent_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +#[actix_web::test] +async fn recent_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let request = protocol::recent(); - let response = service.fetch_json::<_, Vec<index::Directory>>(&request); + let response = service + .fetch_json::<_, Vec<index::Directory>>(&request) + .await; assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 3); } -#[test] -fn recent_with_trailing_slash() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +#[actix_web::test] +async fn recent_with_trailing_slash() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let mut request = protocol::recent(); add_trailing_slash(&mut request); - let response = service.fetch_json::<_, Vec<index::Directory>>(&request); + let response = service + .fetch_json::<_, Vec<index::Directory>>(&request) + .await; assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 3); } -#[test] -fn search_requires_auth() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn search_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::search(""); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn search_without_query() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn search_without_query() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let request = protocol::search(""); - let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request); + let response = service + .fetch_json::<_, Vec<index::CollectionFile>>(&request) + .await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn search_with_query() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +#[actix_web::test] +async fn search_with_query() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let request = protocol::search("door"); - let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request); + let response = service + .fetch_json::<_, Vec<index::CollectionFile>>(&request) + .await; let results = response.body(); assert_eq!(results.len(), 1); match results[0] { diff --git a/src/service/test/ddns.rs b/src/service/test/ddns.rs index 8c269b5..6ab74aa 100644 --- a/src/service/test/ddns.rs +++ b/src/service/test/ddns.rs @@ -4,60 +4,60 @@ use crate::service::dto; use crate::service::test::{protocol, ServiceType, TestService}; use crate::test_name; -#[test] -fn get_ddns_config_requires_admin() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn get_ddns_config_requires_admin() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::get_ddns_config(); - service.complete_initial_setup(); + service.complete_initial_setup().await; - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - service.login(); - let response = service.fetch(&request); + service.login().await; + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::FORBIDDEN); } -#[test] -fn get_ddns_config_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); +#[actix_web::test] +async fn get_ddns_config_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; let request = protocol::get_ddns_config(); - let response = service.fetch_json::<_, dto::DDNSConfig>(&request); + let response = service.fetch_json::<_, dto::DDNSConfig>(&request).await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn put_ddns_config_requires_admin() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn put_ddns_config_requires_admin() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::put_ddns_config(dto::DDNSConfig { host: "test".to_owned(), username: "test".to_owned(), password: "test".to_owned(), }); - service.complete_initial_setup(); + service.complete_initial_setup().await; - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - service.login(); - let response = service.fetch(&request); + service.login().await; + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::FORBIDDEN); } -#[test] -fn put_ddns_config_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); +#[actix_web::test] +async fn put_ddns_config_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; let request = protocol::put_ddns_config(dto::DDNSConfig { host: "test".to_owned(), username: "test".to_owned(), password: "test".to_owned(), }); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } diff --git a/src/service/test/lastfm.rs b/src/service/test/lastfm.rs index 7373124..83a8364 100644 --- a/src/service/test/lastfm.rs +++ b/src/service/test/lastfm.rs @@ -5,56 +5,58 @@ use crate::service::dto; use crate::service::test::{constants::*, protocol, ServiceType, TestService}; use crate::test_name; -#[test] -fn lastfm_scrobble_ignores_unlinked_user() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +#[actix_web::test] +async fn lastfm_scrobble_ignores_unlinked_user() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"] .iter() .collect(); let request = protocol::lastfm_scrobble(&path); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::NO_CONTENT); } -#[test] -fn lastfm_now_playing_ignores_unlinked_user() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +#[actix_web::test] +async fn lastfm_now_playing_ignores_unlinked_user() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"] .iter() .collect(); let request = protocol::lastfm_now_playing(&path); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::NO_CONTENT); } -#[test] -fn lastfm_link_token_requires_auth() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn lastfm_link_token_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::lastfm_link_token(); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn lastfm_link_token_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn lastfm_link_token_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let request = protocol::lastfm_link_token(); - let response = service.fetch_json::<_, dto::LastFMLinkToken>(&request); + let response = service + .fetch_json::<_, dto::LastFMLinkToken>(&request) + .await; assert_eq!(response.status(), StatusCode::OK); let link_token = response.body(); assert!(!link_token.value.is_empty()); diff --git a/src/service/test/media.rs b/src/service/test/media.rs index 86e5833..a6fbd0b 100644 --- a/src/service/test/media.rs +++ b/src/service/test/media.rs @@ -5,33 +5,33 @@ use crate::service::dto::ThumbnailSize; use crate::service::test::{constants::*, protocol, ServiceType, TestService}; use crate::test_name; -#[test] -fn audio_requires_auth() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn audio_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"] .iter() .collect(); let request = protocol::audio(&path); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn audio_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +#[actix_web::test] +async fn audio_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"] .iter() .collect(); let request = protocol::audio(&path); - let response = service.fetch_bytes(&request); + let response = service.fetch_bytes(&request).await; assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.body().len(), 24_142); assert_eq!( @@ -40,13 +40,13 @@ fn audio_golden_path() { ); } -#[test] -fn audio_does_not_encode_content() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +#[actix_web::test] +async fn audio_does_not_encode_content() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"] .iter() @@ -59,7 +59,7 @@ fn audio_does_not_encode_content() { HeaderValue::from_str("gzip, deflate, br").unwrap(), ); - let response = service.fetch_bytes(&request); + let response = service.fetch_bytes(&request).await; assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.body().len(), 24_142); assert_eq!(response.headers().get(header::TRANSFER_ENCODING), None); @@ -69,13 +69,13 @@ fn audio_does_not_encode_content() { ); } -#[test] -fn audio_partial_content() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +#[actix_web::test] +async fn audio_partial_content() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"] .iter() @@ -88,7 +88,7 @@ fn audio_partial_content() { HeaderValue::from_str("bytes=100-299").unwrap(), ); - let response = service.fetch_bytes(&request); + let response = service.fetch_bytes(&request).await; assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); assert_eq!(response.body().len(), 200); assert_eq!( @@ -97,22 +97,22 @@ fn audio_partial_content() { ); } -#[test] -fn audio_bad_path_returns_not_found() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn audio_bad_path_returns_not_found() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let path: PathBuf = ["not_my_collection"].iter().collect(); let request = protocol::audio(&path); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::NOT_FOUND); } -#[test] -fn thumbnail_requires_auth() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn thumbnail_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "Folder.jpg"] .iter() @@ -121,17 +121,17 @@ fn thumbnail_requires_auth() { let size = None; let pad = None; let request = protocol::thumbnail(&path, size, pad); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn thumbnail_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +#[actix_web::test] +async fn thumbnail_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "Folder.jpg"] .iter() @@ -140,60 +140,60 @@ fn thumbnail_golden_path() { let size = None; let pad = None; let request = protocol::thumbnail(&path, size, pad); - let response = service.fetch_bytes(&request); + let response = service.fetch_bytes(&request).await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn thumbnail_bad_path_returns_not_found() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn thumbnail_bad_path_returns_not_found() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let path: PathBuf = ["not_my_collection"].iter().collect(); let size = None; let pad = None; let request = protocol::thumbnail(&path, size, pad); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::NOT_FOUND); } -#[test] -fn thumbnail_size_default() { - thumbnail_size(&test_name!(), None, None, 400); +#[actix_web::test] +async fn thumbnail_size_default() { + thumbnail_size(&test_name!(), None, None, 400).await; } -#[test] -fn thumbnail_size_small() { - thumbnail_size(&test_name!(), Some(ThumbnailSize::Small), None, 400); +#[actix_web::test] +async fn thumbnail_size_small() { + thumbnail_size(&test_name!(), Some(ThumbnailSize::Small), None, 400).await; } -#[test] +#[actix_web::test] #[cfg(not(tarpaulin))] -fn thumbnail_size_large() { - thumbnail_size(&test_name!(), Some(ThumbnailSize::Large), None, 1200); +async fn thumbnail_size_large() { + thumbnail_size(&test_name!(), Some(ThumbnailSize::Large), None, 1200).await; } -#[test] +#[actix_web::test] #[cfg(not(tarpaulin))] -fn thumbnail_size_native() { - thumbnail_size(&test_name!(), Some(ThumbnailSize::Native), None, 1423); +async fn thumbnail_size_native() { + thumbnail_size(&test_name!(), Some(ThumbnailSize::Native), None, 1423).await; } -fn thumbnail_size(name: &str, size: Option<ThumbnailSize>, pad: Option<bool>, expected: u32) { - let mut service = ServiceType::new(name); - service.complete_initial_setup(); - service.login_admin(); - service.index(); - service.login(); +async fn thumbnail_size(name: &str, size: Option<ThumbnailSize>, pad: Option<bool>, expected: u32) { + let mut service = ServiceType::new(name).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic", "Folder.png"] .iter() .collect(); let request = protocol::thumbnail(&path, size, pad); - let response = service.fetch_bytes(&request); + let response = service.fetch_bytes(&request).await; assert_eq!(response.status(), StatusCode::OK); let thumbnail = image::load_from_memory(response.body()).unwrap().to_rgb8(); assert_eq!(thumbnail.width(), expected); diff --git a/src/service/test/playlist.rs b/src/service/test/playlist.rs index 3677800..0c5a94f 100644 --- a/src/service/test/playlist.rs +++ b/src/service/test/playlist.rs @@ -5,130 +5,132 @@ use crate::service::dto; use crate::service::test::{constants::*, protocol, ServiceType, TestService}; use crate::test_name; -#[test] -fn list_playlists_requires_auth() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn list_playlists_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::playlists(); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn list_playlists_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn list_playlists_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let request = protocol::playlists(); - let response = service.fetch_json::<_, Vec<dto::ListPlaylistsEntry>>(&request); + let response = service + .fetch_json::<_, Vec<dto::ListPlaylistsEntry>>(&request) + .await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn save_playlist_requires_auth() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn save_playlist_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() }; let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn save_playlist_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn save_playlist_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() }; let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn save_playlist_large() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn save_playlist_large() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let tracks = (0..100_000) .map(|_| "My Super Cool Song".to_string()) .collect(); let my_playlist = dto::SavePlaylistInput { tracks }; let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn get_playlist_requires_auth() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn get_playlist_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::read_playlist(TEST_PLAYLIST_NAME); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn get_playlist_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn get_playlist_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; { let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() }; let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } let request = protocol::read_playlist(TEST_PLAYLIST_NAME); - let response = service.fetch_json::<_, Vec<index::Song>>(&request); + let response = service.fetch_json::<_, Vec<index::Song>>(&request).await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn get_playlist_bad_name_returns_not_found() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn get_playlist_bad_name_returns_not_found() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let request = protocol::read_playlist(TEST_PLAYLIST_NAME); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::NOT_FOUND); } -#[test] -fn delete_playlist_requires_auth() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn delete_playlist_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::delete_playlist(TEST_PLAYLIST_NAME); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn delete_playlist_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn delete_playlist_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; { let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() }; let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } let request = protocol::delete_playlist(TEST_PLAYLIST_NAME); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn delete_playlist_bad_name_returns_not_found() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn delete_playlist_bad_name_returns_not_found() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let request = protocol::delete_playlist(TEST_PLAYLIST_NAME); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::NOT_FOUND); } diff --git a/src/service/test/settings.rs b/src/service/test/settings.rs index cde1160..4622eb5 100644 --- a/src/service/test/settings.rs +++ b/src/service/test/settings.rs @@ -4,72 +4,72 @@ use crate::service::dto::{self, Settings}; use crate::service::test::{protocol, ServiceType, TestService}; use crate::test_name; -#[test] -fn get_settings_requires_auth() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn get_settings_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let request = protocol::get_settings(); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn get_settings_requires_admin() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn get_settings_requires_admin() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let request = protocol::get_settings(); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::FORBIDDEN); } -#[test] -fn get_settings_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); +#[actix_web::test] +async fn get_settings_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; let request = protocol::get_settings(); - let response = service.fetch_json::<_, dto::Settings>(&request); + let response = service.fetch_json::<_, dto::Settings>(&request).await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn put_settings_requires_auth() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn put_settings_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let request = protocol::put_settings(dto::NewSettings::default()); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn put_settings_requires_admin() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn put_settings_requires_admin() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let request = protocol::put_settings(dto::NewSettings::default()); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::FORBIDDEN); } -#[test] -fn put_settings_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); +#[actix_web::test] +async fn put_settings_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; let request = protocol::put_settings(dto::NewSettings { album_art_pattern: Some("test_pattern".to_owned()), reindex_every_n_seconds: Some(31), }); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); let request = protocol::get_settings(); - let response = service.fetch_json::<_, dto::Settings>(&request); + let response = service.fetch_json::<_, dto::Settings>(&request).await; let settings = response.body(); assert_eq!( settings, diff --git a/src/service/test/swagger.rs b/src/service/test/swagger.rs index 35bb167..e3ebae2 100644 --- a/src/service/test/swagger.rs +++ b/src/service/test/swagger.rs @@ -3,19 +3,19 @@ use http::StatusCode; use crate::service::test::{add_trailing_slash, protocol, ServiceType, TestService}; use crate::test_name; -#[test] -fn can_get_swagger_index() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn can_get_swagger_index() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::swagger_index(); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn can_get_swagger_index_with_trailing_slash() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn can_get_swagger_index_with_trailing_slash() { + let mut service = ServiceType::new(&test_name!()).await; let mut request = protocol::swagger_index(); add_trailing_slash(&mut request); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } diff --git a/src/service/test/user.rs b/src/service/test/user.rs index 19c15d8..695c917 100644 --- a/src/service/test/user.rs +++ b/src/service/test/user.rs @@ -6,53 +6,53 @@ use crate::service::dto; use crate::service::test::{constants::*, protocol, ServiceType, TestService}; use crate::test_name; -#[test] -fn list_users_requires_admin() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn list_users_requires_admin() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let request = protocol::list_users(); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - service.login(); - let response = service.fetch(&request); + service.login().await; + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::FORBIDDEN); } -#[test] -fn list_users_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); +#[actix_web::test] +async fn list_users_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; let request = protocol::list_users(); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn create_user_requires_admin() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn create_user_requires_admin() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let request = protocol::create_user(dto::NewUser { name: "Walter".into(), password: "secret".into(), admin: false, }); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - service.login(); - let response = service.fetch(&request); + service.login().await; + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::FORBIDDEN); } -#[test] -fn create_user_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login_admin(); +#[actix_web::test] +async fn create_user_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; let new_user = dto::NewUser { name: "Walter".into(), @@ -60,39 +60,39 @@ fn create_user_golden_path() { admin: false, }; let request = protocol::create_user(new_user); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn update_user_requires_admin() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn update_user_requires_admin() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let request = protocol::update_user("Walter", dto::UserUpdate::default()); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - service.login(); - let response = service.fetch(&request); + service.login().await; + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::FORBIDDEN); } -#[test] -fn update_user_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn update_user_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let request = protocol::update_user("Walter", dto::UserUpdate::default()); - service.login_admin(); - let response = service.fetch(&request); + service.login_admin().await; + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn update_user_cannot_unadmin_self() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn update_user_cannot_unadmin_self() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let request = protocol::update_user( TEST_USERNAME_ADMIN, dto::UserUpdate { @@ -101,80 +101,80 @@ fn update_user_cannot_unadmin_self() { }, ); - service.login_admin(); - let response = service.fetch(&request); + service.login_admin().await; + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::CONFLICT); } -#[test] -fn delete_user_requires_admin() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn delete_user_requires_admin() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let request = protocol::delete_user("Walter"); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - service.login(); - let response = service.fetch(&request); + service.login().await; + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::FORBIDDEN); } -#[test] -fn delete_user_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn delete_user_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let request = protocol::delete_user("Walter"); - service.login_admin(); - let response = service.fetch(&request); + service.login_admin().await; + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn delete_user_cannot_delete_self() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); +#[actix_web::test] +async fn delete_user_cannot_delete_self() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; let request = protocol::delete_user(TEST_USERNAME_ADMIN); - service.login_admin(); - let response = service.fetch(&request); + service.login_admin().await; + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::CONFLICT); } -#[test] -fn get_preferences_requires_auth() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn get_preferences_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::get_preferences(); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn get_preferences_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn get_preferences_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let request = protocol::get_preferences(); - let response = service.fetch_json::<_, user::Preferences>(&request); + let response = service.fetch_json::<_, user::Preferences>(&request).await; assert_eq!(response.status(), StatusCode::OK); } -#[test] -fn put_preferences_requires_auth() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn put_preferences_requires_auth() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::put_preferences(user::Preferences::default()); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } -#[test] -fn put_preferences_golden_path() { - let mut service = ServiceType::new(&test_name!()); - service.complete_initial_setup(); - service.login(); +#[actix_web::test] +async fn put_preferences_golden_path() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login().await; let request = protocol::put_preferences(user::Preferences::default()); - let response = service.fetch(&request); + let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::OK); } diff --git a/src/service/test/web.rs b/src/service/test/web.rs index 00169d2..5d95288 100644 --- a/src/service/test/web.rs +++ b/src/service/test/web.rs @@ -3,10 +3,10 @@ use http::StatusCode; use crate::service::test::{protocol, ServiceType, TestService}; use crate::test_name; -#[test] -fn serves_web_client() { - let mut service = ServiceType::new(&test_name!()); +#[actix_web::test] +async fn serves_web_client() { + let mut service = ServiceType::new(&test_name!()).await; let request = protocol::web_index(); - let response = service.fetch_bytes(&request); + let response = service.fetch_bytes(&request).await; assert_eq!(response.status(), StatusCode::OK); } diff --git a/update_db_schema.bat b/update_db_schema.bat deleted file mode 100644 index 9d17e94..0000000 --- a/update_db_schema.bat +++ /dev/null @@ -1,6 +0,0 @@ -cargo install diesel_cli --no-default-features --features "sqlite-bundled" - -mkdir tmp -diesel --database-url tmp/print-schema.sqlite setup -diesel --database-url tmp/print-schema.sqlite migration run -rmdir /q /s tmp \ No newline at end of file