diff --git a/gpodder_sqlite/.env b/.env similarity index 100% rename from gpodder_sqlite/.env rename to .env diff --git a/Cargo.lock b/Cargo.lock index 44e9967..67158dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -600,28 +600,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "gpodder" -version = "0.1.0" -dependencies = [ - "argon2", - "chrono", - "rand", -] - -[[package]] -name = "gpodder_sqlite" -version = "0.1.0" -dependencies = [ - "chrono", - "diesel", - "diesel_migrations", - "gpodder", - "libsqlite3-sys", - "rand", - "tracing", -] - [[package]] name = "hashbrown" version = "0.15.2" @@ -681,12 +659,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http", "http-body", "pin-project-lite", @@ -948,15 +926,16 @@ checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" name = "otter" version = "0.1.0" dependencies = [ + "argon2", "axum", "axum-extra", "chrono", "clap", "cookie", + "diesel", + "diesel_migrations", "figment", - "gpodder", - "gpodder_sqlite", - "http-body-util", + "libsqlite3-sys", "rand", "serde", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 8f2f346..73dc6b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,19 @@ -[workspace] -members = [ - 'gpodder', - 'gpodder_sqlite' -] - [package] name = "otter" version = "0.1.0" edition = "2021" [dependencies] -gpodder = { path = "./gpodder" } -gpodder_sqlite = { path = "./gpodder_sqlite" } - +argon2 = "0.5.3" axum = { version = "0.8.1", features = ["macros"] } axum-extra = { version = "0.10", features = ["cookie", "typed-header"] } chrono = { version = "0.4.39", features = ["serde"] } clap = { version = "4.5.30", features = ["derive", "env"] } cookie = "0.18.1" +diesel = { version = "2.2.7", features = ["r2d2", "sqlite", "returning_clauses_for_sqlite_3_35"] } +diesel_migrations = { version = "2.2.0", features = ["sqlite"] } figment = { version = "0.10.19", features = ["env", "toml"] } -http-body-util = "0.1.3" +libsqlite3-sys = { version = "0.31.0", features = ["bundled"] } rand = "0.8.5" serde = { version = "1.0.218", features = ["derive"] } tokio = { version = "1.43.0", features = ["full"] } diff --git a/gpodder_sqlite/diesel.toml b/diesel.toml similarity index 100% rename from gpodder_sqlite/diesel.toml rename to diesel.toml diff --git a/gpodder/Cargo.lock b/gpodder/Cargo.lock deleted file mode 100644 index dd49cdc..0000000 --- a/gpodder/Cargo.lock +++ /dev/null @@ -1,520 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures", - "password-hash", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "base64ct" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" - -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "cc" -version = "1.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "gpodder" -version = "0.1.0" -dependencies = [ - "argon2", - "chrono", - "rand", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.171" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" - -[[package]] -name = "log" -version = "0.4.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" - -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rustversion" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-link" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "zerocopy" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/gpodder/Cargo.toml b/gpodder/Cargo.toml deleted file mode 100644 index 650529a..0000000 --- a/gpodder/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "gpodder" -version = "0.1.0" -edition = "2021" - -[dependencies] -chrono = { version = "0.4.39", features = ["serde"] } -argon2 = "0.5.3" -rand = "0.8.5" diff --git a/gpodder/src/lib.rs b/gpodder/src/lib.rs deleted file mode 100644 index 0f31248..0000000 --- a/gpodder/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod models; -mod repository; -mod store; - -pub use models::*; -pub use repository::GpodderRepository; -pub use store::{ - AuthErr, AuthStore, DeviceRepository, EpisodeActionRepository, Store, SubscriptionRepository, -}; diff --git a/gpodder_sqlite/Cargo.lock b/gpodder_sqlite/Cargo.lock deleted file mode 100644 index a140641..0000000 --- a/gpodder_sqlite/Cargo.lock +++ /dev/null @@ -1,954 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures", - "password-hash", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "base64ct" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" - -[[package]] -name = "bitflags" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" - -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "cc" -version = "1.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "diesel" -version = "2.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470eb10efc8646313634c99bb1593f402a6434cbd86e266770c6e39219adb86a" -dependencies = [ - "diesel_derives", - "libsqlite3-sys", - "r2d2", - "time", -] - -[[package]] -name = "diesel_derives" -version = "2.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93958254b70bea63b4187ff73d10180599d9d8d177071b7f91e6da4e0c0ad55" -dependencies = [ - "diesel_table_macro_syntax", - "dsl_auto_type", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "diesel_migrations" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a73ce704bad4231f001bff3314d91dce4aba0770cee8b233991859abc15c1f6" -dependencies = [ - "diesel", - "migrations_internals", - "migrations_macros", -] - -[[package]] -name = "diesel_table_macro_syntax" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" -dependencies = [ - "syn", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "dsl_auto_type" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b" -dependencies = [ - "darling", - "either", - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "gpodder" -version = "0.1.0" -dependencies = [ - "argon2", - "chrono", - "rand", -] - -[[package]] -name = "gpodder_sqlite" -version = "0.1.0" -dependencies = [ - "chrono", - "diesel", - "diesel_migrations", - "gpodder", - "rand", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "indexmap" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.171" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" - -[[package]] -name = "libsqlite3-sys" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8935b44e7c13394a179a438e0cebba0fe08fe01b54f152e29a93b5cf993fd4" -dependencies = [ - "pkg-config", - "vcpkg", -] - -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "migrations_internals" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd01039851e82f8799046eabbb354056283fb265c8ec0996af940f4e85a380ff" -dependencies = [ - "serde", - "toml", -] - -[[package]] -name = "migrations_macros" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb161cc72176cb37aa47f1fc520d3ef02263d67d661f44f05d05a079e1237fd" -dependencies = [ - "migrations_internals", - "proc-macro2", - "quote", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" - -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_syscall" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" -dependencies = [ - "bitflags", -] - -[[package]] -name = "rustversion" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "smallvec" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "time" -version = "0.3.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" - -[[package]] -name = "time-macros" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "toml" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", -] - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-link" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[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.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" -dependencies = [ - "memchr", -] - -[[package]] -name = "zerocopy" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/gpodder_sqlite/Cargo.toml b/gpodder_sqlite/Cargo.toml deleted file mode 100644 index 7141b6d..0000000 --- a/gpodder_sqlite/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "gpodder_sqlite" -version = "0.1.0" -edition = "2021" - -[dependencies] -gpodder = { path = "../gpodder" } -diesel = { version = "2.2.7", features = ["r2d2", "sqlite", "returning_clauses_for_sqlite_3_35"] } -diesel_migrations = { version = "2.2.0", features = ["sqlite"] } -tracing = "0.1.41" -chrono = { version = "0.4.39", features = ["serde"] } -rand = "0.8.5" -libsqlite3-sys = { version = "0.31.0", features = ["bundled"] } diff --git a/gpodder_sqlite/src/models/session.rs b/gpodder_sqlite/src/models/session.rs deleted file mode 100644 index 53fe6d9..0000000 --- a/gpodder_sqlite/src/models/session.rs +++ /dev/null @@ -1,60 +0,0 @@ -use diesel::prelude::*; - -use crate::schema::*; - -#[derive(Clone, Queryable, Selectable, Insertable, Associations)] -#[diesel(belongs_to(super::user::User))] -#[diesel(table_name = sessions)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite))] -pub struct Session { - pub id: i64, - pub user_id: i64, - pub last_seen: i64, -} - -impl Session { - // pub fn new_for_user(pool: &DbPool, user_id: i64, last_seen: i64) -> DbResult { - // let id: i64 = rand::thread_rng().gen(); - - // Ok(Self { - // id, - // user_id, - // last_seen, - // } - // .insert_into(sessions::table) - // .returning(Self::as_returning()) - // .get_result(&mut pool.get()?)?) - // } - - // pub fn user_from_id(pool: &DbPool, id: i64) -> DbResult> { - // Ok(sessions::dsl::sessions - // .inner_join(users::table) - // .filter(sessions::id.eq(id)) - // .select(User::as_select()) - // .get_result(&mut pool.get()?) - // .optional()?) - // } - - // pub fn user(&self, pool: &DbPool) -> DbResult> { - // Self::user_from_id(pool, self.id) - // } - - // pub fn by_id(pool: &DbPool, id: i64) -> DbResult> { - // Ok(sessions::dsl::sessions - // .find(id) - // .get_result(&mut pool.get()?) - // .optional()?) - // } - - // pub fn remove(self, pool: &DbPool) -> DbResult { - // Self::remove_by_id(pool, self.id) - // } - - // pub fn remove_by_id(pool: &DbPool, id: i64) -> DbResult { - // Ok( - // diesel::delete(sessions::dsl::sessions.filter(sessions::id.eq(id))) - // .execute(&mut pool.get()?)? - // > 0, - // ) - // } -} diff --git a/gpodder_sqlite/src/models/user.rs b/gpodder_sqlite/src/models/user.rs deleted file mode 100644 index 72ea62b..0000000 --- a/gpodder_sqlite/src/models/user.rs +++ /dev/null @@ -1,47 +0,0 @@ -use diesel::prelude::*; - -use crate::schema::*; - -#[derive(Clone, Queryable, Selectable)] -#[diesel(table_name = users)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite))] -pub struct User { - pub id: i64, - pub username: String, - pub password_hash: String, -} - -#[derive(Insertable)] -#[diesel(table_name = users)] -#[diesel(check_for_backend(diesel::sqlite::Sqlite))] -pub struct NewUser<'a> { - pub username: &'a str, - pub password_hash: &'a str, -} - -// impl NewUser { -// pub fn new(username: String, password: String) -> Self { -// Self { -// username, -// password_hash: hash_password(&password), -// } -// } -// } - -// impl User { -// pub fn by_username(pool: &DbPool, username: impl AsRef) -> DbResult> { -// Ok(users::dsl::users -// .select(User::as_select()) -// .filter(users::username.eq(username.as_ref())) -// .first(&mut pool.get()?) -// .optional()?) -// } - -// pub fn verify_password(&self, password: impl AsRef) -> bool { -// let password_hash = PasswordHash::new(&self.password_hash).unwrap(); - -// Argon2::default() -// .verify_password(password.as_ref().as_bytes(), &password_hash) -// .is_ok() -// } -// } diff --git a/gpodder_sqlite/src/repository/device.rs b/gpodder_sqlite/src/repository/device.rs deleted file mode 100644 index d69caeb..0000000 --- a/gpodder_sqlite/src/repository/device.rs +++ /dev/null @@ -1,298 +0,0 @@ -use std::collections::HashSet; - -use chrono::{DateTime, Utc}; -use diesel::{alias, dsl::not, prelude::*}; -use gpodder::AuthErr; - -use super::SqliteRepository; -use crate::{ - models::{ - device::{Device, DeviceType, NewDevice}, - sync_group::SyncGroup, - }, - schema::*, - DbError, -}; - -impl From for gpodder::DeviceType { - fn from(value: DeviceType) -> Self { - match value { - DeviceType::Desktop => Self::Desktop, - DeviceType::Laptop => Self::Laptop, - DeviceType::Mobile => Self::Mobile, - DeviceType::Server => Self::Server, - DeviceType::Other => Self::Other, - } - } -} - -impl From for DeviceType { - fn from(value: gpodder::DeviceType) -> Self { - match value { - gpodder::DeviceType::Desktop => Self::Desktop, - gpodder::DeviceType::Laptop => Self::Laptop, - gpodder::DeviceType::Mobile => Self::Mobile, - gpodder::DeviceType::Server => Self::Server, - gpodder::DeviceType::Other => Self::Other, - } - } -} - -impl gpodder::DeviceRepository for SqliteRepository { - fn devices_for_user( - &self, - user: &gpodder::User, - ) -> Result, gpodder::AuthErr> { - (|| { - Ok::<_, DbError>( - devices::table - .select(Device::as_select()) - .filter(devices::user_id.eq(user.id)) - .get_results(&mut self.pool.get()?)? - .into_iter() - .map(|d| gpodder::Device { - id: d.device_id, - caption: d.caption, - r#type: d.type_.into(), - // TODO implement subscription count - subscriptions: 0, - }) - .collect(), - ) - })() - .map_err(AuthErr::from) - } - - fn update_device_info( - &self, - user: &gpodder::User, - device_id: &str, - patch: gpodder::DevicePatch, - ) -> Result<(), gpodder::AuthErr> { - (|| { - if let Some(mut device) = devices::table - .select(Device::as_select()) - .filter( - devices::user_id - .eq(user.id) - .and(devices::device_id.eq(device_id)), - ) - .get_result(&mut self.pool.get()?) - .optional()? - { - if let Some(caption) = patch.caption { - device.caption = caption; - } - - if let Some(type_) = patch.r#type { - device.type_ = type_.into(); - } - - diesel::update(devices::table.filter(devices::id.eq(device.id))) - .set(( - devices::caption.eq(&device.caption), - devices::type_.eq(&device.type_), - )) - .execute(&mut self.pool.get()?)?; - } else { - let device = NewDevice { - device_id: device_id.to_string(), - user_id: user.id, - caption: patch.caption.unwrap_or(String::new()), - type_: patch.r#type.unwrap_or(gpodder::DeviceType::Other).into(), - }; - - diesel::insert_into(devices::table) - .values(device) - .execute(&mut self.pool.get()?)?; - } - - Ok::<_, DbError>(()) - })() - .map_err(AuthErr::from) - } - - fn merge_sync_groups( - &self, - user: &gpodder::User, - device_ids: Vec<&str>, - ) -> Result { - (|| { - let conn = &mut self.pool.get()?; - - conn.transaction(|conn| { - let devices: Vec<(i64, Option)> = devices::table - .select((devices::id, devices::sync_group_id)) - .filter( - devices::user_id - .eq(user.id) - .and(devices::device_id.eq_any(device_ids)), - ) - .get_results(conn)?; - - let mut sync_group_ids: Vec = devices - .iter() - .filter_map(|(_, group_id)| *group_id) - .collect(); - - // Remove any duplicates, giving us each sync group ID once - sync_group_ids.sort(); - sync_group_ids.dedup(); - - // If any of the devices are already in a sync group, we reuse the first one we find. - // Otherwise, we generate a new one. - let sync_group_id = if let Some(id) = sync_group_ids.pop() { - id - } else { - SyncGroup::new(conn)?.id - }; - - // Move all devices in the other sync groups into the new sync group - diesel::update( - devices::table.filter(devices::sync_group_id.eq_any(sync_group_ids.iter())), - ) - .set(devices::sync_group_id.eq(sync_group_id)) - .execute(conn)?; - - // Add the non-synchronized devices into the new sync group - let unsynced_device_ids = - devices.iter().filter_map( - |(id, group_id)| if group_id.is_none() { Some(id) } else { None }, - ); - - diesel::update(devices::table.filter(devices::id.eq_any(unsynced_device_ids))) - .set(devices::sync_group_id.eq(sync_group_id)) - .execute(conn)?; - - // Remove the other now unused sync groups - diesel::delete(sync_groups::table.filter(sync_groups::id.eq_any(sync_group_ids))) - .execute(conn)?; - - Ok::<_, DbError>(sync_group_id) - }) - })() - .map_err(AuthErr::from) - } - - fn remove_from_sync_group( - &self, - user: &gpodder::User, - device_ids: Vec<&str>, - ) -> Result<(), gpodder::AuthErr> { - (|| { - let conn = &mut self.pool.get()?; - - diesel::update( - devices::table.filter( - devices::user_id - .eq(user.id) - .and(devices::device_id.eq_any(device_ids)), - ), - ) - .set(devices::sync_group_id.eq(None::)) - .execute(conn)?; - - // This is in a different transaction on purpose, as the success of this removal shouldn't - // fail the entire query - SyncGroup::remove_unused(conn)?; - - Ok::<_, DbError>(()) - })() - .map_err(AuthErr::from) - } - - fn synchronize_sync_group( - &self, - group_id: i64, - time_changed: DateTime, - ) -> Result<(), gpodder::AuthErr> { - (|| { - let time_changed = time_changed.timestamp(); - let conn = &mut self.pool.get()?; - - conn.transaction(|conn| { - let device_ids: Vec = devices::table - .filter(devices::sync_group_id.eq(group_id)) - .select(devices::id) - .get_results(conn)?; - - // For each device in the group, we get the list of subscriptions not yet in its own - // non-deleted list, and add it to the database - for device_id in device_ids.iter().copied() { - let d1 = alias!(device_subscriptions as d1); - - let own_subscriptions = d1 - .filter( - d1.field(device_subscriptions::device_id) - .eq(device_id) - .and(d1.field(device_subscriptions::deleted).eq(false)), - ) - .select(d1.field(device_subscriptions::podcast_url)); - - let urls_to_add = device_subscriptions::table - .select(device_subscriptions::podcast_url) - .filter( - device_subscriptions::device_id - .eq_any(device_ids.iter()) - .and(device_subscriptions::deleted.eq(false)) - .and(not( - device_subscriptions::podcast_url.eq_any(own_subscriptions) - )), - ) - .distinct() - .load_iter(conn)? - .collect::, _>>()?; - - super::subscription::insert_subscriptions_for_single_device( - conn, - device_id, - urls_to_add.iter(), - time_changed, - )?; - } - - Ok::<_, DbError>(()) - }) - })() - .map_err(AuthErr::from) - } - - fn devices_by_sync_group( - &self, - user: &gpodder::User, - ) -> Result<(Vec, Vec>), gpodder::AuthErr> { - (|| { - let mut not_synchronized = Vec::new(); - let mut synchronized = Vec::new(); - - let conn = &mut self.pool.get()?; - let mut devices = devices::table - .select((devices::device_id, devices::sync_group_id)) - .filter(devices::user_id.eq(user.id)) - .order(devices::sync_group_id) - .load_iter::<(String, Option), _>(conn)?; - - let mut cur_group = &mut not_synchronized; - let mut cur_group_id: Option = None; - - while let Some((device_id, group_id)) = devices.next().transpose()? { - if group_id != cur_group_id { - if group_id.is_none() { - cur_group = &mut not_synchronized; - } else { - synchronized.push(Vec::new()); - let index = synchronized.len() - 1; - cur_group = &mut synchronized[index]; - } - - cur_group_id = group_id; - } - - cur_group.push(device_id); - } - - Ok::<_, DbError>((not_synchronized, synchronized)) - })() - .map_err(AuthErr::from) - } -} diff --git a/gpodder_sqlite/src/repository/episode_action.rs b/gpodder_sqlite/src/repository/episode_action.rs deleted file mode 100644 index bf9aff1..0000000 --- a/gpodder_sqlite/src/repository/episode_action.rs +++ /dev/null @@ -1,176 +0,0 @@ -use chrono::{DateTime, Utc}; -use diesel::prelude::*; -use gpodder::AuthErr; - -use super::SqliteRepository; -use crate::{ - models::{ - device::Device, - episode_action::{ActionType, EpisodeAction, NewEpisodeAction}, - }, - schema::*, - DbError, -}; - -impl From for NewEpisodeAction { - fn from(value: gpodder::EpisodeAction) -> Self { - let (action, started, position, total) = match value.action { - gpodder::EpisodeActionType::New => (ActionType::New, None, None, None), - gpodder::EpisodeActionType::Delete => (ActionType::Delete, None, None, None), - gpodder::EpisodeActionType::Download => (ActionType::Download, None, None, None), - gpodder::EpisodeActionType::Play { - started, - position, - total, - } => (ActionType::Play, started, Some(position), total), - }; - - NewEpisodeAction { - user_id: 0, - device_id: None, - podcast_url: value.podcast, - episode_url: value.episode, - time_changed: 0, - timestamp: value.timestamp.map(|t| t.timestamp()), - action, - started, - position, - total, - } - } -} - -fn to_gpodder_action( - (device_id, db_action): (Option, EpisodeAction), -) -> gpodder::EpisodeAction { - let action = match db_action.action { - ActionType::Play => gpodder::EpisodeActionType::Play { - started: db_action.started, - // SAFETY: the condition that this isn't null if the action type is "play" is - // explicitely enforced by the database using a CHECK constraint. - position: db_action.position.unwrap(), - total: db_action.total, - }, - ActionType::New => gpodder::EpisodeActionType::New, - ActionType::Delete => gpodder::EpisodeActionType::Delete, - ActionType::Download => gpodder::EpisodeActionType::Download, - }; - - gpodder::EpisodeAction { - podcast: db_action.podcast_url, - episode: db_action.episode_url, - timestamp: db_action - .timestamp - // SAFETY the input to the from_timestamp function is always the result of a - // previous timestamp() function call, which is guaranteed to be each other's - // reverse - .map(|ts| DateTime::from_timestamp(ts, 0).unwrap()), - time_changed: DateTime::from_timestamp(db_action.time_changed, 0).unwrap(), - device: device_id, - action, - } -} - -impl gpodder::EpisodeActionRepository for SqliteRepository { - fn add_episode_actions( - &self, - user: &gpodder::User, - actions: Vec, - time_changed: DateTime, - ) -> Result<(), gpodder::AuthErr> { - (|| { - let time_changed = time_changed.timestamp(); - - // TODO optimize this query - // 1. The lookup for a device could be replaced with a subquery, although Diesel seems to - // have a problem using an Option to match equality with a String - // 2. Ideally the for loop would be replaced with a single query inserting multiple values, - // although each value would need its own subquery - // - // NOTE this function usually gets called from the same device, so optimizing the - // amount of device lookups required would be useful. - self.pool.get()?.transaction(|conn| { - for action in actions { - let device_id = if let Some(device) = &action.device { - Some(Device::device_id_to_id(conn, user.id, device)?) - } else { - None - }; - - let mut new_action: NewEpisodeAction = action.into(); - new_action.user_id = user.id; - new_action.device_id = device_id; - new_action.time_changed = time_changed; - - diesel::insert_into(episode_actions::table) - .values(&new_action) - .execute(conn)?; - } - - Ok::<_, DbError>(()) - }) - })() - .map_err(AuthErr::from) - } - - fn episode_actions_for_user( - &self, - user: &gpodder::User, - since: Option>, - podcast: Option, - device: Option, - aggregated: bool, - ) -> Result, gpodder::AuthErr> { - (|| { - let since = since.map(|ts| ts.timestamp()).unwrap_or(0); - let conn = &mut self.pool.get()?; - - let mut query = episode_actions::table - .left_join(devices::table) - .filter( - episode_actions::user_id - .eq(user.id) - .and(episode_actions::time_changed.ge(since)), - ) - .select((devices::device_id.nullable(), EpisodeAction::as_select())) - .into_boxed(); - - if let Some(device_id) = device { - query = query.filter(devices::device_id.eq(device_id)); - } - - if let Some(podcast_url) = podcast { - query = query.filter(episode_actions::podcast_url.eq(podcast_url)); - } - - let db_actions: Vec<(Option, EpisodeAction)> = if aggregated { - // https://stackoverflow.com/a/7745635 - // For each episode URL, we want to return the row with the highest `time_changed` - // value. We achieve this be left joining with self on the URL, as well as whether the - // left row's time_changed value is less than the right one. Rows with the largest - // time_changed value for a given URL will join with a NULL value (because of the left - // join), so we filter those out to retrieve the correct rows. - let a2 = diesel::alias!(episode_actions as a2); - - query - .left_join( - a2.on(episode_actions::episode_url - .eq(a2.field(episode_actions::episode_url)) - .and( - episode_actions::time_changed - .lt(a2.field(episode_actions::time_changed)), - )), - ) - .filter(a2.field(episode_actions::episode_url).is_null()) - .get_results(conn)? - } else { - query.get_results(conn)? - }; - - let actions = db_actions.into_iter().map(to_gpodder_action).collect(); - - Ok::<_, DbError>(actions) - })() - .map_err(AuthErr::from) - } -} diff --git a/gpodder_sqlite/src/repository/mod.rs b/gpodder_sqlite/src/repository/mod.rs deleted file mode 100644 index 7fab3bf..0000000 --- a/gpodder_sqlite/src/repository/mod.rs +++ /dev/null @@ -1,33 +0,0 @@ -mod auth; -mod device; -mod episode_action; -mod subscription; - -use std::path::Path; - -use super::DbPool; - -#[derive(Clone)] -pub struct SqliteRepository { - pool: DbPool, -} - -impl From for SqliteRepository { - fn from(value: DbPool) -> Self { - Self { pool: value } - } -} - -impl SqliteRepository { - pub fn from_path(path: impl AsRef) -> Result { - let pool = super::initialize_db(path, true)?; - - Ok(Self { pool }) - } - - pub fn in_memory() -> Result { - let pool = super::initialize_db_in_memory(true)?; - - Ok(Self { pool }) - } -} diff --git a/gpodder_sqlite/tests/auth_test.rs b/gpodder_sqlite/tests/auth_test.rs deleted file mode 100644 index 5dd26f4..0000000 --- a/gpodder_sqlite/tests/auth_test.rs +++ /dev/null @@ -1,49 +0,0 @@ -mod common; - -use chrono::SubsecRound; -use gpodder::{AuthStore, Session}; -use gpodder_sqlite::SqliteRepository; - -#[test] -fn test_create_user() { - let store = SqliteRepository::in_memory().unwrap(); - - let user = store.get_user("test1"); - assert!(user.is_ok()); - assert_eq!(user.unwrap(), None); - - let new_user = store.insert_user("test1", "dummyhash"); - - assert!(new_user.is_ok()); - - let new_user = new_user.unwrap(); - assert_eq!(new_user.username, "test1"); - assert_eq!(new_user.password_hash, "dummyhash"); - - let user = store.get_user("test1"); - - assert!(user.is_ok()); - assert_eq!(user.unwrap(), Some(new_user)); -} - -#[test] -fn test_create_session() { - let (store, users) = common::setup(); - - let session = store.get_session(123).expect("operation shouldn't fail"); - assert_eq!(session, None); - - let new_session = Session { - id: 123, - // - last_seen: chrono::Utc::now().trunc_subsecs(0), - user: users[0].clone(), - }; - - store - .insert_session(&new_session) - .expect("insert session shouldn't fail"); - - let session = store.get_session(123).expect("operation shouldn't fail"); - assert_eq!(session, Some(new_session)); -} diff --git a/gpodder_sqlite/tests/common.rs b/gpodder_sqlite/tests/common.rs deleted file mode 100644 index 37b5cc4..0000000 --- a/gpodder_sqlite/tests/common.rs +++ /dev/null @@ -1,16 +0,0 @@ -use gpodder::{AuthStore, User}; -use gpodder_sqlite::SqliteRepository; - -pub fn setup() -> (SqliteRepository, Vec) { - let store = SqliteRepository::in_memory().unwrap(); - let mut users = Vec::new(); - - for i in 0..4 { - let username = format!("test{}", i + 1); - let password_hash = format!("dummyhash{}", i + 1); - - users.push(store.insert_user(&username, &password_hash).unwrap()); - } - - (store, users) -} diff --git a/gpodder_sqlite/migrations/2025-02-23-095541_initial/down.sql b/migrations/2025-02-23-095541_initial/down.sql similarity index 100% rename from gpodder_sqlite/migrations/2025-02-23-095541_initial/down.sql rename to migrations/2025-02-23-095541_initial/down.sql diff --git a/gpodder_sqlite/migrations/2025-02-23-095541_initial/up.sql b/migrations/2025-02-23-095541_initial/up.sql similarity index 100% rename from gpodder_sqlite/migrations/2025-02-23-095541_initial/up.sql rename to migrations/2025-02-23-095541_initial/up.sql diff --git a/src/cli/gpo.rs b/src/cli/gpo.rs index e4d951b..cbf5dbd 100644 --- a/src/cli/gpo.rs +++ b/src/cli/gpo.rs @@ -1,5 +1,7 @@ use clap::Subcommand; +use crate::db; + #[derive(Subcommand)] pub enum Command { /// Add devices of a specific user to the same sync group @@ -13,10 +15,9 @@ pub enum Command { impl Command { pub fn run(&self, config: &crate::config::Config) -> u8 { - let store = - gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME)) - .unwrap(); - let store = gpodder::GpodderRepository::new(store); + let pool = db::initialize_db(config.data_dir.join(crate::DB_FILENAME), true).unwrap(); + let repo = db::SqliteRepository::from(pool); + let store = crate::gpodder::GpodderRepository::new(repo); match self { Self::Sync { username, devices } => { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 512c955..dc0a8ba 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,4 +1,4 @@ -// mod db; +mod db; mod gpo; mod serve; @@ -48,8 +48,9 @@ pub struct ClapConfig { #[derive(Subcommand)] pub enum Command { Serve, - // #[command(subcommand)] - // Db(db::DbCommand), + #[command(subcommand)] + Db(db::DbCommand), + /// Perform operations on the database through the Gpodder abstraction, allowing operations /// identical to the ones performed by the API. #[command(subcommand)] @@ -79,7 +80,7 @@ impl Cli { match &self.cmd { Command::Serve => serve::serve(&config), - // Command::Db(cmd) => cmd.run(&config), + Command::Db(cmd) => cmd.run(&config), Command::Gpo(cmd) => cmd.run(&config), } } diff --git a/src/cli/serve.rs b/src/cli/serve.rs index a434db0..67cf6e7 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -1,18 +1,18 @@ use std::time::Duration; -use crate::server; +use crate::{db, server}; pub fn serve(config: &crate::config::Config) -> u8 { tracing_subscriber::fmt::init(); tracing::info!("Initializing database and running migrations"); - let store = - gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME)) - .unwrap(); - let store = gpodder::GpodderRepository::new(store); + let pool = db::initialize_db(config.data_dir.join(crate::DB_FILENAME), true).unwrap(); + let repo = db::SqliteRepository::from(pool); - let ctx = server::Context { store }; + let ctx = server::Context { + store: crate::gpodder::GpodderRepository::new(repo), + }; let app = server::app(ctx.clone()); let rt = tokio::runtime::Builder::new_multi_thread() diff --git a/gpodder_sqlite/src/lib.rs b/src/db/mod.rs similarity index 78% rename from gpodder_sqlite/src/lib.rs rename to src/db/mod.rs index f2f0082..ef3eeb8 100644 --- a/gpodder_sqlite/src/lib.rs +++ b/src/db/mod.rs @@ -1,4 +1,4 @@ -mod models; +pub mod models; mod repository; mod schema; @@ -6,6 +6,13 @@ use diesel::connection::InstrumentationEvent; use diesel::r2d2::CustomizeConnection; use diesel::Connection; +pub use models::device::{Device, DeviceType, NewDevice}; +pub use models::device_subscription::{DeviceSubscription, NewDeviceSubscription}; +pub use models::episode_action::{ActionType, EpisodeAction, NewEpisodeAction}; +pub use models::session::Session; +pub use models::sync_group::SyncGroup; +pub use models::user::{NewUser, User}; + pub use repository::SqliteRepository; use diesel::{ @@ -57,15 +64,6 @@ impl From for DbError { } } -impl From for gpodder::AuthErr { - fn from(value: DbError) -> Self { - match value { - DbError::Pool(err) => Self::Other(Box::new(err)), - DbError::Db(err) => Self::Other(Box::new(err)), - } - } -} - #[derive(Debug)] pub struct AddQueryDebugLogs; @@ -94,16 +92,3 @@ pub fn initialize_db(path: impl AsRef, run_migrations: bool) -> Result Result { - let manager = ConnectionManager::::new(":memory:"); - let pool = Pool::builder() - .connection_customizer(Box::new(AddQueryDebugLogs)) - .build(manager)?; - - if run_migrations { - pool.get()?.run_pending_migrations(MIGRATIONS).unwrap(); - } - - Ok(pool) -} diff --git a/gpodder_sqlite/src/models/device.rs b/src/db/models/device.rs similarity index 73% rename from gpodder_sqlite/src/models/device.rs rename to src/db/models/device.rs index 25d3052..2be8f75 100644 --- a/gpodder_sqlite/src/models/device.rs +++ b/src/db/models/device.rs @@ -8,10 +8,11 @@ use diesel::{ sql_types::Text, sqlite::{Sqlite, SqliteValue}, }; +use serde::{Deserialize, Serialize}; -use crate::schema::*; +use crate::db::{schema::*, DbPool, DbResult}; -#[derive(Clone, Queryable, Selectable)] +#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)] #[diesel(table_name = devices)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct Device { @@ -23,7 +24,7 @@ pub struct Device { pub sync_group_id: Option, } -#[derive(Insertable)] +#[derive(Deserialize, Insertable)] #[diesel(table_name = devices)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct NewDevice { @@ -33,8 +34,9 @@ pub struct NewDevice { pub type_: DeviceType, } -#[derive(FromSqlRow, Debug, AsExpression, Clone)] +#[derive(Serialize, Deserialize, FromSqlRow, Debug, AsExpression, Clone)] #[diesel(sql_type = Text)] +#[serde(rename_all = "lowercase")] pub enum DeviceType { Desktop, Laptop, @@ -44,6 +46,13 @@ pub enum DeviceType { } impl Device { + pub fn for_user(pool: &DbPool, user_id: i64) -> DbResult> { + Ok(devices::dsl::devices + .select(Self::as_select()) + .filter(devices::user_id.eq(user_id)) + .get_results(&mut pool.get()?)?) + } + pub fn device_id_to_id( conn: &mut SqliteConnection, user_id: i64, @@ -73,6 +82,22 @@ impl Device { ) .get_result(conn) } + + pub fn update(&self, pool: &DbPool) -> DbResult<()> { + Ok(diesel::update( + devices::table.filter( + devices::user_id + .eq(self.user_id) + .and(devices::device_id.eq(&self.device_id)), + ), + ) + .set(( + devices::caption.eq(&self.caption), + devices::type_.eq(&self.type_), + )) + .execute(&mut pool.get()?) + .map(|_| ())?) + } } impl NewDevice { @@ -84,6 +109,13 @@ impl NewDevice { type_, } } + + pub fn insert(self, pool: &DbPool) -> DbResult { + Ok(diesel::insert_into(devices::table) + .values(&self) + .returning(Device::as_returning()) + .get_result(&mut pool.get()?)?) + } } impl fmt::Display for DeviceType { diff --git a/gpodder_sqlite/src/models/device_subscription.rs b/src/db/models/device_subscription.rs similarity index 76% rename from gpodder_sqlite/src/models/device_subscription.rs rename to src/db/models/device_subscription.rs index a77d2a3..c2541c8 100644 --- a/gpodder_sqlite/src/models/device_subscription.rs +++ b/src/db/models/device_subscription.rs @@ -1,8 +1,9 @@ use diesel::prelude::*; +use serde::{Deserialize, Serialize}; -use crate::schema::*; +use crate::db::schema::*; -#[derive(Clone, Queryable, Selectable)] +#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)] #[diesel(table_name = device_subscriptions)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct DeviceSubscription { @@ -13,7 +14,7 @@ pub struct DeviceSubscription { pub deleted: bool, } -#[derive(Insertable)] +#[derive(Deserialize, Insertable)] #[diesel(table_name = device_subscriptions)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct NewDeviceSubscription { diff --git a/gpodder_sqlite/src/models/episode_action.rs b/src/db/models/episode_action.rs similarity index 90% rename from gpodder_sqlite/src/models/episode_action.rs rename to src/db/models/episode_action.rs index faef14f..b7fd89e 100644 --- a/gpodder_sqlite/src/models/episode_action.rs +++ b/src/db/models/episode_action.rs @@ -9,10 +9,11 @@ use diesel::{ sqlite::{Sqlite, SqliteValue}, Selectable, }; +use serde::{Deserialize, Serialize}; -use crate::schema::*; +use crate::db::schema::*; -#[derive(Clone, Queryable, Selectable)] +#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)] #[diesel(table_name = episode_actions)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct EpisodeAction { @@ -29,7 +30,7 @@ pub struct EpisodeAction { pub total: Option, } -#[derive(Insertable)] +#[derive(Deserialize, Insertable)] #[diesel(table_name = episode_actions)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct NewEpisodeAction { @@ -45,8 +46,9 @@ pub struct NewEpisodeAction { pub total: Option, } -#[derive(FromSqlRow, Debug, AsExpression, Clone)] +#[derive(Serialize, Deserialize, FromSqlRow, Debug, AsExpression, Clone)] #[diesel(sql_type = Text)] +#[serde(rename_all = "lowercase")] pub enum ActionType { New, Download, diff --git a/gpodder_sqlite/src/models/mod.rs b/src/db/models/mod.rs similarity index 100% rename from gpodder_sqlite/src/models/mod.rs rename to src/db/models/mod.rs diff --git a/src/db/models/session.rs b/src/db/models/session.rs new file mode 100644 index 0000000..273550a --- /dev/null +++ b/src/db/models/session.rs @@ -0,0 +1,62 @@ +use diesel::prelude::*; +use rand::Rng; + +use super::user::User; +use crate::db::{schema::*, DbPool, DbResult}; + +#[derive(Clone, Queryable, Selectable, Insertable, Associations)] +#[diesel(belongs_to(super::user::User))] +#[diesel(table_name = sessions)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct Session { + pub id: i64, + pub user_id: i64, + pub last_seen: i64, +} + +impl Session { + pub fn new_for_user(pool: &DbPool, user_id: i64, last_seen: i64) -> DbResult { + let id: i64 = rand::thread_rng().gen(); + + Ok(Self { + id, + user_id, + last_seen, + } + .insert_into(sessions::table) + .returning(Self::as_returning()) + .get_result(&mut pool.get()?)?) + } + + pub fn user_from_id(pool: &DbPool, id: i64) -> DbResult> { + Ok(sessions::dsl::sessions + .inner_join(users::table) + .filter(sessions::id.eq(id)) + .select(User::as_select()) + .get_result(&mut pool.get()?) + .optional()?) + } + + pub fn user(&self, pool: &DbPool) -> DbResult> { + Self::user_from_id(pool, self.id) + } + + pub fn by_id(pool: &DbPool, id: i64) -> DbResult> { + Ok(sessions::dsl::sessions + .find(id) + .get_result(&mut pool.get()?) + .optional()?) + } + + pub fn remove(self, pool: &DbPool) -> DbResult { + Self::remove_by_id(pool, self.id) + } + + pub fn remove_by_id(pool: &DbPool, id: i64) -> DbResult { + Ok( + diesel::delete(sessions::dsl::sessions.filter(sessions::id.eq(id))) + .execute(&mut pool.get()?)? + > 0, + ) + } +} diff --git a/gpodder_sqlite/src/models/sync_group.rs b/src/db/models/sync_group.rs similarity index 97% rename from gpodder_sqlite/src/models/sync_group.rs rename to src/db/models/sync_group.rs index aad14e3..edeca8c 100644 --- a/gpodder_sqlite/src/models/sync_group.rs +++ b/src/db/models/sync_group.rs @@ -3,7 +3,7 @@ use diesel::{ prelude::*, }; -use crate::schema::*; +use crate::db::schema::*; #[derive(Queryable, Selectable)] #[diesel(table_name = sync_groups)] diff --git a/src/db/models/user.rs b/src/db/models/user.rs new file mode 100644 index 0000000..f9c9bb9 --- /dev/null +++ b/src/db/models/user.rs @@ -0,0 +1,67 @@ +use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; +use diesel::prelude::*; +use rand::rngs::OsRng; +use serde::{Deserialize, Serialize}; + +use crate::db::{schema::*, DbPool, DbResult}; + +#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct User { + pub id: i64, + pub username: String, + pub password_hash: String, +} + +#[derive(Deserialize, Insertable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct NewUser { + pub username: String, + pub password_hash: String, +} + +fn hash_password(password: impl AsRef) -> String { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + + argon2 + .hash_password(password.as_ref().as_bytes(), &salt) + .unwrap() + .to_string() +} + +impl NewUser { + pub fn new(username: String, password: String) -> Self { + Self { + username, + password_hash: hash_password(&password), + } + } + + pub fn insert(self, pool: &DbPool) -> DbResult { + Ok(diesel::insert_into(users::table) + .values(self) + .returning(User::as_returning()) + .get_result(&mut pool.get()?)?) + } +} + +impl User { + pub fn by_username(pool: &DbPool, username: impl AsRef) -> DbResult> { + Ok(users::dsl::users + .select(User::as_select()) + .filter(users::username.eq(username.as_ref())) + .first(&mut pool.get()?) + .optional()?) + } + + pub fn verify_password(&self, password: impl AsRef) -> bool { + let password_hash = PasswordHash::new(&self.password_hash).unwrap(); + + Argon2::default() + .verify_password(password.as_ref().as_bytes(), &password_hash) + .is_ok() + } +} diff --git a/gpodder_sqlite/src/repository/auth.rs b/src/db/repository/auth.rs similarity index 55% rename from gpodder_sqlite/src/repository/auth.rs rename to src/db/repository/auth.rs index 1b60028..3aafa05 100644 --- a/gpodder_sqlite/src/repository/auth.rs +++ b/src/db/repository/auth.rs @@ -1,19 +1,26 @@ use chrono::DateTime; use diesel::prelude::*; -use gpodder::AuthErr; use super::SqliteRepository; use crate::{ - models::{ - session::Session, - user::{NewUser, User}, - }, - schema::*, - DbError, + db::{self, schema::*}, + gpodder::{self, AuthErr}, }; -impl From for gpodder::User { - fn from(value: User) -> Self { +impl From for gpodder::AuthErr { + fn from(value: diesel::r2d2::PoolError) -> Self { + Self::Other(Box::new(value)) + } +} + +impl From for gpodder::AuthErr { + fn from(value: diesel::result::Error) -> Self { + Self::Other(Box::new(value)) + } +} + +impl From for gpodder::User { + fn from(value: db::User) -> Self { Self { id: value.id, username: value.username, @@ -25,65 +32,46 @@ impl From for gpodder::User { impl gpodder::AuthStore for SqliteRepository { fn get_user(&self, username: &str) -> Result, AuthErr> { Ok(users::table - .select(User::as_select()) + .select(db::User::as_select()) .filter(users::username.eq(username)) - .first(&mut self.pool.get().map_err(DbError::from)?) - .optional() - .map_err(DbError::from)? + .first(&mut self.pool.get()?) + .optional()? .map(gpodder::User::from)) } - fn insert_user(&self, username: &str, password_hash: &str) -> Result { - let conn = &mut self.pool.get().map_err(DbError::from)?; - - Ok(diesel::insert_into(users::table) - .values(NewUser { - username, - password_hash, - }) - .returning(User::as_returning()) - .get_result(conn) - .map(gpodder::User::from) - .map_err(DbError::from)?) - } - fn get_session(&self, session_id: i64) -> Result, AuthErr> { match sessions::table .inner_join(users::table) .filter(sessions::id.eq(session_id)) - .select((Session::as_select(), User::as_select())) - .get_result(&mut self.pool.get().map_err(DbError::from)?) - .optional() + .select((db::Session::as_select(), db::User::as_select())) + .get_result(&mut self.pool.get()?) { - Ok(Some((session, user))) => Ok(Some(gpodder::Session { + Ok((session, user)) => Ok(Some(gpodder::Session { id: session.id, last_seen: DateTime::from_timestamp(session.last_seen, 0).unwrap(), user: user.into(), })), - Ok(None) => Ok(None), - Err(err) => Err(DbError::from(err).into()), + Err(err) => Err(AuthErr::from(err)), } } fn remove_session(&self, session_id: i64) -> Result<(), AuthErr> { Ok( diesel::delete(sessions::table.filter(sessions::id.eq(session_id))) - .execute(&mut self.pool.get().map_err(DbError::from)?) - .map(|_| ()) - .map_err(DbError::from)?, + .execute(&mut self.pool.get()?) + .map(|_| ())?, ) } fn insert_session(&self, session: &gpodder::Session) -> Result<(), AuthErr> { - Ok(Session { + Ok(db::Session { id: session.id, user_id: session.user.id, last_seen: session.last_seen.timestamp(), } .insert_into(sessions::table) - .execute(&mut self.pool.get().map_err(DbError::from)?) - .map(|_| ()) - .map_err(DbError::from)?) + .execute(&mut self.pool.get()?) + .map(|_| ())?) } fn refresh_session( @@ -93,8 +81,7 @@ impl gpodder::AuthStore for SqliteRepository { ) -> Result<(), AuthErr> { if diesel::update(sessions::table.filter(sessions::id.eq(session.id))) .set(sessions::last_seen.eq(timestamp.timestamp())) - .execute(&mut self.pool.get().map_err(DbError::from)?) - .map_err(DbError::from)? + .execute(&mut self.pool.get()?)? == 0 { Err(AuthErr::UnknownSession) @@ -108,8 +95,7 @@ impl gpodder::AuthStore for SqliteRepository { Ok( diesel::delete(sessions::table.filter(sessions::last_seen.lt(min_last_seen))) - .execute(&mut self.pool.get().map_err(DbError::from)?) - .map_err(DbError::from)?, + .execute(&mut self.pool.get()?)?, ) } } diff --git a/src/db/repository/device.rs b/src/db/repository/device.rs new file mode 100644 index 0000000..cd1a4d9 --- /dev/null +++ b/src/db/repository/device.rs @@ -0,0 +1,275 @@ +use std::collections::HashSet; + +use chrono::{DateTime, Utc}; +use diesel::{alias, dsl::not, prelude::*}; + +use super::SqliteRepository; +use crate::{ + db::{self, schema::*, SyncGroup}, + gpodder, +}; + +impl From for gpodder::DeviceType { + fn from(value: db::DeviceType) -> Self { + match value { + db::DeviceType::Desktop => Self::Desktop, + db::DeviceType::Laptop => Self::Laptop, + db::DeviceType::Mobile => Self::Mobile, + db::DeviceType::Server => Self::Server, + db::DeviceType::Other => Self::Other, + } + } +} + +impl From for db::DeviceType { + fn from(value: gpodder::DeviceType) -> Self { + match value { + gpodder::DeviceType::Desktop => Self::Desktop, + gpodder::DeviceType::Laptop => Self::Laptop, + gpodder::DeviceType::Mobile => Self::Mobile, + gpodder::DeviceType::Server => Self::Server, + gpodder::DeviceType::Other => Self::Other, + } + } +} + +impl gpodder::DeviceRepository for SqliteRepository { + fn devices_for_user( + &self, + user: &gpodder::User, + ) -> Result, gpodder::AuthErr> { + Ok(devices::table + .select(db::Device::as_select()) + .filter(devices::user_id.eq(user.id)) + .get_results(&mut self.pool.get()?)? + .into_iter() + .map(|d| gpodder::Device { + id: d.device_id, + caption: d.caption, + r#type: d.type_.into(), + // TODO implement subscription count + subscriptions: 0, + }) + .collect()) + } + + fn update_device_info( + &self, + user: &gpodder::User, + device_id: &str, + patch: gpodder::DevicePatch, + ) -> Result<(), gpodder::AuthErr> { + if let Some(mut device) = devices::table + .select(db::Device::as_select()) + .filter( + devices::user_id + .eq(user.id) + .and(devices::device_id.eq(device_id)), + ) + .get_result(&mut self.pool.get()?) + .optional()? + { + if let Some(caption) = patch.caption { + device.caption = caption; + } + + if let Some(type_) = patch.r#type { + device.type_ = type_.into(); + } + + diesel::update(devices::table.filter(devices::id.eq(device.id))) + .set(( + devices::caption.eq(&device.caption), + devices::type_.eq(&device.type_), + )) + .execute(&mut self.pool.get()?)?; + } else { + let device = db::NewDevice { + device_id: device_id.to_string(), + user_id: user.id, + caption: patch.caption.unwrap_or(String::new()), + type_: patch.r#type.unwrap_or(gpodder::DeviceType::Other).into(), + }; + + diesel::insert_into(devices::table) + .values(device) + .execute(&mut self.pool.get()?)?; + } + + Ok(()) + } + + fn merge_sync_groups( + &self, + user: &gpodder::User, + device_ids: Vec<&str>, + ) -> Result { + let conn = &mut self.pool.get()?; + + Ok(conn.transaction(|conn| { + let devices: Vec<(i64, Option)> = devices::table + .select((devices::id, devices::sync_group_id)) + .filter( + devices::user_id + .eq(user.id) + .and(devices::device_id.eq_any(device_ids)), + ) + .get_results(conn)?; + + let mut sync_group_ids: Vec = devices + .iter() + .filter_map(|(_, group_id)| *group_id) + .collect(); + + // Remove any duplicates, giving us each sync group ID once + sync_group_ids.sort(); + sync_group_ids.dedup(); + + // If any of the devices are already in a sync group, we reuse the first one we find. + // Otherwise, we generate a new one. + let sync_group_id = if let Some(id) = sync_group_ids.pop() { + id + } else { + db::SyncGroup::new(conn)?.id + }; + + // Move all devices in the other sync groups into the new sync group + diesel::update( + devices::table.filter(devices::sync_group_id.eq_any(sync_group_ids.iter())), + ) + .set(devices::sync_group_id.eq(sync_group_id)) + .execute(conn)?; + + // Add the non-synchronized devices into the new sync group + let unsynced_device_ids = + devices + .iter() + .filter_map(|(id, group_id)| if group_id.is_none() { Some(id) } else { None }); + + diesel::update(devices::table.filter(devices::id.eq_any(unsynced_device_ids))) + .set(devices::sync_group_id.eq(sync_group_id)) + .execute(conn)?; + + // Remove the other now unused sync groups + diesel::delete(sync_groups::table.filter(sync_groups::id.eq_any(sync_group_ids))) + .execute(conn)?; + + Ok::<_, diesel::result::Error>(sync_group_id) + })?) + } + + fn remove_from_sync_group( + &self, + user: &gpodder::User, + device_ids: Vec<&str>, + ) -> Result<(), gpodder::AuthErr> { + let conn = &mut self.pool.get()?; + + diesel::update( + devices::table.filter( + devices::user_id + .eq(user.id) + .and(devices::device_id.eq_any(device_ids)), + ), + ) + .set(devices::sync_group_id.eq(None::)) + .execute(conn)?; + + // This is in a different transaction on purpose, as the success of this removal shouldn't + // fail the entire query + SyncGroup::remove_unused(conn)?; + + Ok(()) + } + + fn synchronize_sync_group( + &self, + group_id: i64, + time_changed: DateTime, + ) -> Result<(), gpodder::AuthErr> { + let time_changed = time_changed.timestamp(); + let conn = &mut self.pool.get()?; + + conn.transaction(|conn| { + let device_ids: Vec = devices::table + .filter(devices::sync_group_id.eq(group_id)) + .select(devices::id) + .get_results(conn)?; + + // For each device in the group, we get the list of subscriptions not yet in its own + // non-deleted list, and add it to the database + for device_id in device_ids.iter().copied() { + let d1 = alias!(device_subscriptions as d1); + + let own_subscriptions = d1 + .filter( + d1.field(device_subscriptions::device_id) + .eq(device_id) + .and(d1.field(device_subscriptions::deleted).eq(false)), + ) + .select(d1.field(device_subscriptions::podcast_url)); + + let urls_to_add = device_subscriptions::table + .select(device_subscriptions::podcast_url) + .filter( + device_subscriptions::device_id + .eq_any(device_ids.iter()) + .and(device_subscriptions::deleted.eq(false)) + .and(not( + device_subscriptions::podcast_url.eq_any(own_subscriptions) + )), + ) + .distinct() + .load_iter(conn)? + .collect::, _>>()?; + + super::subscription::insert_subscriptions_for_single_device( + conn, + device_id, + urls_to_add.iter(), + time_changed, + )?; + } + + Ok::<_, diesel::result::Error>(()) + })?; + + Ok(()) + } + + fn devices_by_sync_group( + &self, + user: &gpodder::User, + ) -> Result<(Vec, Vec>), gpodder::AuthErr> { + let mut not_synchronized = Vec::new(); + let mut synchronized = Vec::new(); + + let conn = &mut self.pool.get()?; + let mut devices = devices::table + .select((devices::device_id, devices::sync_group_id)) + .filter(devices::user_id.eq(user.id)) + .order(devices::sync_group_id) + .load_iter::<(String, Option), _>(conn)?; + + let mut cur_group = &mut not_synchronized; + let mut cur_group_id: Option = None; + + while let Some((device_id, group_id)) = devices.next().transpose()? { + if group_id != cur_group_id { + if group_id.is_none() { + cur_group = &mut not_synchronized; + } else { + synchronized.push(Vec::new()); + let index = synchronized.len() - 1; + cur_group = &mut synchronized[index]; + } + + cur_group_id = group_id; + } + + cur_group.push(device_id); + } + + Ok((not_synchronized, synchronized)) + } +} diff --git a/src/db/repository/episode_action.rs b/src/db/repository/episode_action.rs new file mode 100644 index 0000000..5a45cd2 --- /dev/null +++ b/src/db/repository/episode_action.rs @@ -0,0 +1,170 @@ +use chrono::{DateTime, Utc}; +use diesel::prelude::*; + +use super::SqliteRepository; +use crate::{ + db::{self, schema::*}, + gpodder, +}; + +impl From for db::NewEpisodeAction { + fn from(value: gpodder::EpisodeAction) -> Self { + let (action, started, position, total) = match value.action { + gpodder::EpisodeActionType::New => (db::ActionType::New, None, None, None), + gpodder::EpisodeActionType::Delete => (db::ActionType::Delete, None, None, None), + gpodder::EpisodeActionType::Download => (db::ActionType::Download, None, None, None), + gpodder::EpisodeActionType::Play { + started, + position, + total, + } => (db::ActionType::Play, started, Some(position), total), + }; + + db::NewEpisodeAction { + user_id: 0, + device_id: None, + podcast_url: value.podcast, + episode_url: value.episode, + time_changed: 0, + timestamp: value.timestamp.map(|t| t.timestamp()), + action, + started, + position, + total, + } + } +} + +impl From<(Option, db::EpisodeAction)> for gpodder::EpisodeAction { + fn from((device_id, db_action): (Option, db::EpisodeAction)) -> Self { + let action = match db_action.action { + db::ActionType::Play => gpodder::EpisodeActionType::Play { + started: db_action.started, + // SAFETY: the condition that this isn't null if the action type is "play" is + // explicitely enforced by the database using a CHECK constraint. + position: db_action.position.unwrap(), + total: db_action.total, + }, + db::ActionType::New => gpodder::EpisodeActionType::New, + db::ActionType::Delete => gpodder::EpisodeActionType::Delete, + db::ActionType::Download => gpodder::EpisodeActionType::Download, + }; + + Self { + podcast: db_action.podcast_url, + episode: db_action.episode_url, + timestamp: db_action + .timestamp + // SAFETY the input to the from_timestamp function is always the result of a + // previous timestamp() function call, which is guaranteed to be each other's + // reverse + .map(|ts| DateTime::from_timestamp(ts, 0).unwrap()), + time_changed: DateTime::from_timestamp(db_action.time_changed, 0).unwrap(), + device: device_id, + action, + } + } +} + +impl gpodder::EpisodeActionRepository for SqliteRepository { + fn add_episode_actions( + &self, + user: &gpodder::User, + actions: Vec, + time_changed: DateTime, + ) -> Result<(), gpodder::AuthErr> { + let time_changed = time_changed.timestamp(); + + // TODO optimize this query + // 1. The lookup for a device could be replaced with a subquery, although Diesel seems to + // have a problem using an Option to match equality with a String + // 2. Ideally the for loop would be replaced with a single query inserting multiple values, + // although each value would need its own subquery + self.pool.get()?.transaction(|conn| { + for action in actions { + let device_id = if let Some(device) = &action.device { + Some(db::Device::device_id_to_id(conn, user.id, device)?) + } else { + None + }; + + let mut new_action: db::NewEpisodeAction = action.into(); + new_action.user_id = user.id; + new_action.device_id = device_id; + new_action.time_changed = time_changed; + + diesel::insert_into(episode_actions::table) + .values(&new_action) + .execute(conn)?; + } + + Ok::<_, diesel::result::Error>(()) + })?; + + Ok(()) + } + + fn episode_actions_for_user( + &self, + user: &gpodder::User, + since: Option>, + podcast: Option, + device: Option, + aggregated: bool, + ) -> Result, gpodder::AuthErr> { + let since = since.map(|ts| ts.timestamp()).unwrap_or(0); + let conn = &mut self.pool.get()?; + + let mut query = episode_actions::table + .left_join(devices::table) + .filter( + episode_actions::user_id + .eq(user.id) + .and(episode_actions::time_changed.ge(since)), + ) + .select(( + devices::device_id.nullable(), + db::EpisodeAction::as_select(), + )) + .into_boxed(); + + if let Some(device_id) = device { + query = query.filter(devices::device_id.eq(device_id)); + } + + if let Some(podcast_url) = podcast { + query = query.filter(episode_actions::podcast_url.eq(podcast_url)); + } + + let db_actions: Vec<(Option, db::EpisodeAction)> = if aggregated { + // https://stackoverflow.com/a/7745635 + // For each episode URL, we want to return the row with the highest `time_changed` + // value. We achieve this be left joining with self on the URL, as well as whether the + // left row's time_changed value is less than the right one. Rows with the largest + // time_changed value for a given URL will join with a NULL value (because of the left + // join), so we filter those out to retrieve the correct rows. + let a2 = diesel::alias!(episode_actions as a2); + + query + .left_join( + a2.on(episode_actions::episode_url + .eq(a2.field(episode_actions::episode_url)) + .and( + episode_actions::time_changed + .lt(a2.field(episode_actions::time_changed)), + )), + ) + .filter(a2.field(episode_actions::episode_url).is_null()) + .get_results(conn)? + } else { + query.get_results(conn)? + }; + + let actions = db_actions + .into_iter() + .map(gpodder::EpisodeAction::from) + .collect(); + + Ok(actions) + } +} diff --git a/src/db/repository/mod.rs b/src/db/repository/mod.rs new file mode 100644 index 0000000..01a81d4 --- /dev/null +++ b/src/db/repository/mod.rs @@ -0,0 +1,17 @@ +mod auth; +mod device; +mod episode_action; +mod subscription; + +use super::DbPool; + +#[derive(Clone)] +pub struct SqliteRepository { + pool: DbPool, +} + +impl From for SqliteRepository { + fn from(value: DbPool) -> Self { + Self { pool: value } + } +} diff --git a/gpodder_sqlite/src/repository/subscription.rs b/src/db/repository/subscription.rs similarity index 55% rename from gpodder_sqlite/src/repository/subscription.rs rename to src/db/repository/subscription.rs index da407a1..447a27d 100644 --- a/gpodder_sqlite/src/repository/subscription.rs +++ b/src/db/repository/subscription.rs @@ -2,15 +2,22 @@ use std::collections::HashSet; use chrono::DateTime; use diesel::prelude::*; -use gpodder::AuthErr; use super::SqliteRepository; use crate::{ - models::device_subscription::{DeviceSubscription, NewDeviceSubscription}, - schema::*, - DbError, + db::{self, schema::*}, + gpodder, }; +impl From<(String, i64)> for gpodder::Subscription { + fn from((url, ts): (String, i64)) -> Self { + Self { + url, + time_changed: DateTime::from_timestamp(ts, 0).unwrap(), + } + } +} + fn set_subscriptions_for_single_device( conn: &mut SqliteConnection, device_id: i64, @@ -73,7 +80,7 @@ fn set_subscriptions_for_single_device( .values( urls_to_insert .into_iter() - .map(|url| NewDeviceSubscription { + .map(|url| db::NewDeviceSubscription { device_id, podcast_url: url.to_string(), deleted: false, @@ -98,7 +105,7 @@ pub fn insert_subscriptions_for_single_device<'a>( diesel::insert_into(device_subscriptions::table) .values( urls.into_iter() - .map(|url| NewDeviceSubscription { + .map(|url| db::NewDeviceSubscription { device_id, podcast_url: url.to_string(), deleted: false, @@ -177,26 +184,18 @@ impl gpodder::SubscriptionRepository for SqliteRepository { &self, user: &gpodder::User, ) -> Result, gpodder::AuthErr> { - (|| { - Ok::<_, DbError>( - device_subscriptions::table - .inner_join(devices::table) - .filter(devices::user_id.eq(user.id)) - .select(( - device_subscriptions::podcast_url, - device_subscriptions::time_changed, - )) - .distinct() - .get_results::<(String, i64)>(&mut self.pool.get()?)? - .into_iter() - .map(|(url, ts)| gpodder::Subscription { - url, - time_changed: DateTime::from_timestamp(ts, 0).unwrap(), - }) - .collect(), - ) - })() - .map_err(AuthErr::from) + Ok(device_subscriptions::table + .inner_join(devices::table) + .filter(devices::user_id.eq(user.id)) + .select(( + device_subscriptions::podcast_url, + device_subscriptions::time_changed, + )) + .distinct() + .get_results::<(String, i64)>(&mut self.pool.get()?)? + .into_iter() + .map(Into::into) + .collect()) } fn subscriptions_for_device( @@ -204,29 +203,21 @@ impl gpodder::SubscriptionRepository for SqliteRepository { user: &gpodder::User, device_id: &str, ) -> Result, gpodder::AuthErr> { - (|| { - Ok::<_, DbError>( - device_subscriptions::table - .inner_join(devices::table) - .filter( - devices::user_id - .eq(user.id) - .and(devices::device_id.eq(device_id)), - ) - .select(( - device_subscriptions::podcast_url, - device_subscriptions::time_changed, - )) - .get_results::<(String, i64)>(&mut self.pool.get()?)? - .into_iter() - .map(|(url, ts)| gpodder::Subscription { - url, - time_changed: DateTime::from_timestamp(ts, 0).unwrap(), - }) - .collect(), + Ok(device_subscriptions::table + .inner_join(devices::table) + .filter( + devices::user_id + .eq(user.id) + .and(devices::device_id.eq(device_id)), ) - })() - .map_err(AuthErr::from) + .select(( + device_subscriptions::podcast_url, + device_subscriptions::time_changed, + )) + .get_results::<(String, i64)>(&mut self.pool.get()?)? + .into_iter() + .map(Into::into) + .collect()) } fn set_subscriptions_for_device( @@ -236,39 +227,38 @@ impl gpodder::SubscriptionRepository for SqliteRepository { urls: Vec, time_changed: chrono::DateTime, ) -> Result<(), gpodder::AuthErr> { - (|| { - let time_changed = time_changed.timestamp(); - let urls: HashSet = urls.into_iter().collect(); + let time_changed = time_changed.timestamp(); + let urls: HashSet = urls.into_iter().collect(); - self.pool.get()?.transaction(|conn| { - let (device_id, group_id) = devices::table - .select((devices::id, devices::sync_group_id)) - .filter( - devices::user_id - .eq(user.id) - .and(devices::device_id.eq(device_id)), - ) - .get_result::<(i64, Option)>(conn)?; + self.pool.get()?.transaction(|conn| { + let (device_id, group_id) = devices::table + .select((devices::id, devices::sync_group_id)) + .filter( + devices::user_id + .eq(user.id) + .and(devices::device_id.eq(device_id)), + ) + .get_result::<(i64, Option)>(conn)?; - // If the device is part of a sync group, we need to perform the update on every device - // in the group - if let Some(group_id) = group_id { - let device_ids: Vec = devices::table - .filter(devices::sync_group_id.eq(group_id)) - .select(devices::id) - .get_results(conn)?; + // If the device is part of a sync group, we need to perform the update on every device + // in the group + if let Some(group_id) = group_id { + let device_ids: Vec = devices::table + .filter(devices::sync_group_id.eq(group_id)) + .select(devices::id) + .get_results(conn)?; - for device_id in device_ids { - set_subscriptions_for_single_device(conn, device_id, &urls, time_changed)?; - } - } else { + for device_id in device_ids { set_subscriptions_for_single_device(conn, device_id, &urls, time_changed)?; } + } else { + set_subscriptions_for_single_device(conn, device_id, &urls, time_changed)?; + } - Ok::<_, DbError>(()) - }) - })() - .map_err(AuthErr::from) + Ok::<_, diesel::result::Error>(()) + })?; + + Ok(()) } fn update_subscriptions_for_device( @@ -279,42 +269,32 @@ impl gpodder::SubscriptionRepository for SqliteRepository { remove: Vec, time_changed: chrono::DateTime, ) -> Result<(), gpodder::AuthErr> { - (|| { - let time_changed = time_changed.timestamp(); + let time_changed = time_changed.timestamp(); - // TODO URLs that are in both the added and removed lists will currently get "re-added", - // meaning their change timestamp will be updated even though they haven't really changed. - let add: HashSet<_> = add.into_iter().collect(); - let remove: HashSet<_> = remove.into_iter().collect(); + // TODO URLs that are in both the added and removed lists will currently get "re-added", + // meaning their change timestamp will be updated even though they haven't really changed. + let add: HashSet<_> = add.into_iter().collect(); + let remove: HashSet<_> = remove.into_iter().collect(); - self.pool.get()?.transaction(|conn| { - let (device_id, group_id) = devices::table - .select((devices::id, devices::sync_group_id)) - .filter( - devices::user_id - .eq(user.id) - .and(devices::device_id.eq(device_id)), - ) - .get_result::<(i64, Option)>(conn)?; + self.pool.get()?.transaction(|conn| { + let (device_id, group_id) = devices::table + .select((devices::id, devices::sync_group_id)) + .filter( + devices::user_id + .eq(user.id) + .and(devices::device_id.eq(device_id)), + ) + .get_result::<(i64, Option)>(conn)?; - // If the device is part of a sync group, we need to perform the update on every device - // in the group - if let Some(group_id) = group_id { - let device_ids: Vec = devices::table - .filter(devices::sync_group_id.eq(group_id)) - .select(devices::id) - .get_results(conn)?; + // If the device is part of a sync group, we need to perform the update on every device + // in the group + if let Some(group_id) = group_id { + let device_ids: Vec = devices::table + .filter(devices::sync_group_id.eq(group_id)) + .select(devices::id) + .get_results(conn)?; - for device_id in device_ids { - update_subscriptions_for_single_device( - conn, - device_id, - &add, - &remove, - time_changed, - )?; - } - } else { + for device_id in device_ids { update_subscriptions_for_single_device( conn, device_id, @@ -323,11 +303,20 @@ impl gpodder::SubscriptionRepository for SqliteRepository { time_changed, )?; } + } else { + update_subscriptions_for_single_device( + conn, + device_id, + &add, + &remove, + time_changed, + )?; + } - Ok::<_, DbError>(()) - }) - })() - .map_err(AuthErr::from) + Ok::<_, diesel::result::Error>(()) + })?; + + Ok(()) } fn subscription_updates_for_device( @@ -336,39 +325,36 @@ impl gpodder::SubscriptionRepository for SqliteRepository { device_id: &str, since: chrono::DateTime, ) -> Result<(Vec, Vec), gpodder::AuthErr> { - (|| { - let since = since.timestamp(); + let since = since.timestamp(); - let (mut added, mut removed) = (Vec::new(), Vec::new()); + let (mut added, mut removed) = (Vec::new(), Vec::new()); - let query = device_subscriptions::table - .inner_join(devices::table) - .filter( - devices::user_id - .eq(user.id) - .and(devices::device_id.eq(device_id)) - .and(device_subscriptions::time_changed.ge(since)), - ) - .select(DeviceSubscription::as_select()); + let query = device_subscriptions::table + .inner_join(devices::table) + .filter( + devices::user_id + .eq(user.id) + .and(devices::device_id.eq(device_id)) + .and(device_subscriptions::time_changed.ge(since)), + ) + .select(db::DeviceSubscription::as_select()); - for sub in query.load_iter(&mut self.pool.get()?)? { - let sub = sub?; + for sub in query.load_iter(&mut self.pool.get()?)? { + let sub = sub?; - if sub.deleted { - removed.push(gpodder::Subscription { - url: sub.podcast_url, - time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(), - }); - } else { - added.push(gpodder::Subscription { - url: sub.podcast_url, - time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(), - }); - } + if sub.deleted { + removed.push(gpodder::Subscription { + url: sub.podcast_url, + time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(), + }); + } else { + added.push(gpodder::Subscription { + url: sub.podcast_url, + time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(), + }); } + } - Ok::<_, DbError>((added, removed)) - })() - .map_err(AuthErr::from) + Ok((added, removed)) } } diff --git a/gpodder_sqlite/src/schema.rs b/src/db/schema.rs similarity index 100% rename from gpodder_sqlite/src/schema.rs rename to src/db/schema.rs diff --git a/gpodder/src/store.rs b/src/gpodder/mod.rs similarity index 91% rename from gpodder/src/store.rs rename to src/gpodder/mod.rs index 800a95d..c9abd8e 100644 --- a/gpodder/src/store.rs +++ b/src/gpodder/mod.rs @@ -1,7 +1,11 @@ +pub mod models; +mod repository; + use std::fmt::Display; -use crate::models::*; use chrono::{DateTime, Utc}; +pub use models::*; +pub use repository::GpodderRepository; #[derive(Debug)] pub enum AuthErr { @@ -36,17 +40,12 @@ impl Store for T where pub trait AuthStore { /// Retrieve the session with the given session ID - fn get_session(&self, session_id: i64) -> Result, AuthErr>; + fn get_session(&self, session_id: i64) -> Result, AuthErr>; /// Retrieve the user with the given username - fn get_user(&self, username: &str) -> Result, AuthErr>; - - /// Insert a new user into the data store - fn insert_user(&self, username: &str, password_hash: &str) -> Result; + fn get_user(&self, username: &str) -> Result, AuthErr>; /// Create a new session for a user with the given session ID - /// - /// The `last_seen` timestamp's precision should be at least accurate to the second fn insert_session(&self, session: &Session) -> Result<(), AuthErr>; /// Remove the session with the given session ID @@ -110,10 +109,10 @@ pub trait SubscriptionRepository { &self, user: &User, device_id: &str, - ) -> Result, AuthErr>; + ) -> Result, AuthErr>; /// Return all subscriptions for a given user - fn subscriptions_for_user(&self, user: &User) -> Result, AuthErr>; + fn subscriptions_for_user(&self, user: &User) -> Result, AuthErr>; /// Replace the list of subscriptions for a device and all devices in its sync group with the /// given list diff --git a/gpodder/src/models.rs b/src/gpodder/models.rs similarity index 76% rename from gpodder/src/models.rs rename to src/gpodder/models.rs index cde5c50..b68e73b 100644 --- a/gpodder/src/models.rs +++ b/src/gpodder/models.rs @@ -1,13 +1,12 @@ use chrono::{DateTime, Utc}; -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone)] pub struct User { pub id: i64, pub username: String, pub password_hash: String, } -#[derive(Clone, Debug, PartialEq, Eq)] pub enum DeviceType { Desktop, Laptop, @@ -16,7 +15,6 @@ pub enum DeviceType { Other, } -#[derive(Clone, Debug, PartialEq, Eq)] pub struct Device { pub id: String, pub caption: String, @@ -24,13 +22,11 @@ pub struct Device { pub subscriptions: i64, } -#[derive(Clone, Debug, PartialEq, Eq)] pub struct DevicePatch { pub caption: Option, pub r#type: Option, } -#[derive(Clone, Debug, PartialEq, Eq)] pub enum EpisodeActionType { Download, Play { @@ -42,7 +38,6 @@ pub enum EpisodeActionType { New, } -#[derive(Clone, Debug, PartialEq, Eq)] pub struct EpisodeAction { pub podcast: String, pub episode: String, @@ -52,14 +47,12 @@ pub struct EpisodeAction { pub action: EpisodeActionType, } -#[derive(Clone, Debug, PartialEq, Eq)] pub struct Session { pub id: i64, pub last_seen: DateTime, pub user: User, } -#[derive(Clone, Debug, PartialEq, Eq)] pub struct Subscription { pub url: String, pub time_changed: DateTime, diff --git a/gpodder/src/repository.rs b/src/gpodder/repository.rs similarity index 92% rename from gpodder/src/repository.rs rename to src/gpodder/repository.rs index dd9d12a..942ef26 100644 --- a/gpodder/src/repository.rs +++ b/src/gpodder/repository.rs @@ -1,13 +1,10 @@ use std::{collections::HashSet, sync::Arc}; -use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; +use argon2::{Argon2, PasswordHash, PasswordVerifier}; use chrono::{DateTime, TimeDelta, Utc}; -use rand::{rngs::OsRng, Rng}; +use rand::Rng; -use crate::{ - models, - store::{AuthErr, Store}, -}; +use super::{models, AuthErr, Store}; const MAX_SESSION_AGE: i64 = 60 * 60 * 24 * 7; @@ -41,17 +38,6 @@ impl GpodderRepository { self.store.get_user(username)?.ok_or(AuthErr::UnknownUser) } - pub fn create_user(&self, username: &str, password: &str) -> Result { - let salt = SaltString::generate(&mut OsRng); - - let password_hash = Argon2::default() - .hash_password(password.as_bytes(), &salt) - .unwrap() - .to_string(); - - self.store.insert_user(username, &password_hash) - } - pub fn validate_credentials( &self, username: &str, diff --git a/src/main.rs b/src/main.rs index b42d31a..2e17a7b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ mod cli; mod config; +mod db; +mod gpodder; mod server; use clap::Parser; diff --git a/src/server/error.rs b/src/server/error.rs index dacc052..5d7aeb4 100644 --- a/src/server/error.rs +++ b/src/server/error.rs @@ -2,13 +2,13 @@ use std::fmt; use axum::{http::StatusCode, response::IntoResponse}; -use crate::ErrorExt; +use crate::{db, ErrorExt}; pub type AppResult = Result; #[derive(Debug)] pub enum AppError { - // Db(db::DbError), + Db(db::DbError), IO(std::io::Error), Other(Box), BadRequest, @@ -19,7 +19,7 @@ pub enum AppError { impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - // Self::Db(_) => write!(f, "database error"), + Self::Db(_) => write!(f, "database error"), Self::IO(_) => write!(f, "io error"), Self::Other(_) => write!(f, "other error"), Self::BadRequest => write!(f, "bad request"), @@ -32,7 +32,7 @@ impl fmt::Display for AppError { impl std::error::Error for AppError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { - // Self::Db(err) => Some(err), + Self::Db(err) => Some(err), Self::IO(err) => Some(err), Self::Other(err) => Some(err.as_ref()), Self::NotFound | Self::Unauthorized | Self::BadRequest => None, @@ -40,6 +40,12 @@ impl std::error::Error for AppError { } } +impl From for AppError { + fn from(value: db::DbError) -> Self { + Self::Db(value) + } +} + impl From for AppError { fn from(value: std::io::Error) -> Self { Self::IO(value) diff --git a/src/server/gpodder/advanced/auth.rs b/src/server/gpodder/advanced/auth.rs index b4adaeb..b0bccaf 100644 --- a/src/server/gpodder/advanced/auth.rs +++ b/src/server/gpodder/advanced/auth.rs @@ -10,10 +10,13 @@ use axum_extra::{ }; use cookie::time::Duration; -use crate::server::{ - error::{AppError, AppResult}, - gpodder::SESSION_ID_COOKIE, - Context, +use crate::{ + gpodder, + server::{ + error::{AppError, AppResult}, + gpodder::SESSION_ID_COOKIE, + Context, + }, }; pub fn router() -> Router { diff --git a/src/server/gpodder/advanced/devices.rs b/src/server/gpodder/advanced/devices.rs index 67f4f79..859e1c1 100644 --- a/src/server/gpodder/advanced/devices.rs +++ b/src/server/gpodder/advanced/devices.rs @@ -5,14 +5,17 @@ use axum::{ Extension, Json, Router, }; -use crate::server::{ - error::{AppError, AppResult}, - gpodder::{ - auth_middleware, - format::{Format, StringWithFormat}, - models, +use crate::{ + gpodder, + server::{ + error::{AppError, AppResult}, + gpodder::{ + auth_middleware, + format::{Format, StringWithFormat}, + models, + }, + Context, }, - Context, }; pub fn router(ctx: Context) -> Router { diff --git a/src/server/gpodder/advanced/episodes.rs b/src/server/gpodder/advanced/episodes.rs index 5ea62d6..6c7ff32 100644 --- a/src/server/gpodder/advanced/episodes.rs +++ b/src/server/gpodder/advanced/episodes.rs @@ -7,15 +7,18 @@ use axum::{ use chrono::DateTime; use serde::{Deserialize, Serialize}; -use crate::server::{ - error::{AppError, AppResult}, - gpodder::{ - auth_middleware, - format::{Format, StringWithFormat}, - models, - models::UpdatedUrlsResponse, +use crate::{ + gpodder, + server::{ + error::{AppError, AppResult}, + gpodder::{ + auth_middleware, + format::{Format, StringWithFormat}, + models, + models::UpdatedUrlsResponse, + }, + Context, }, - Context, }; pub fn router(ctx: Context) -> Router { diff --git a/src/server/gpodder/advanced/subscriptions.rs b/src/server/gpodder/advanced/subscriptions.rs index b69f420..e5691e1 100644 --- a/src/server/gpodder/advanced/subscriptions.rs +++ b/src/server/gpodder/advanced/subscriptions.rs @@ -6,14 +6,17 @@ use axum::{ }; use serde::Deserialize; -use crate::server::{ - error::{AppError, AppResult}, - gpodder::{ - auth_middleware, - format::{Format, StringWithFormat}, - models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse}, +use crate::{ + gpodder, + server::{ + error::{AppError, AppResult}, + gpodder::{ + auth_middleware, + format::{Format, StringWithFormat}, + models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse}, + }, + Context, }, - Context, }; pub fn router(ctx: Context) -> Router { diff --git a/src/server/gpodder/advanced/sync.rs b/src/server/gpodder/advanced/sync.rs index 728452c..fc97869 100644 --- a/src/server/gpodder/advanced/sync.rs +++ b/src/server/gpodder/advanced/sync.rs @@ -5,14 +5,17 @@ use axum::{ Extension, Json, Router, }; -use crate::server::{ - error::{AppError, AppResult}, - gpodder::{ - auth_middleware, - format::{Format, StringWithFormat}, - models::{SyncStatus, SyncStatusDelta}, +use crate::{ + gpodder, + server::{ + error::{AppError, AppResult}, + gpodder::{ + auth_middleware, + format::{Format, StringWithFormat}, + models::{SyncStatus, SyncStatusDelta}, + }, + Context, }, - Context, }; pub fn router(ctx: Context) -> Router { diff --git a/src/server/gpodder/mod.rs b/src/server/gpodder/mod.rs index 83442f3..4746323 100644 --- a/src/server/gpodder/mod.rs +++ b/src/server/gpodder/mod.rs @@ -17,7 +17,7 @@ use axum_extra::{ }; use tower_http::set_header::SetResponseHeaderLayer; -use crate::server::error::AppError; +use crate::{gpodder, server::error::AppError}; use super::Context; diff --git a/src/server/gpodder/models.rs b/src/server/gpodder/models.rs index 5e0f010..20d459f 100644 --- a/src/server/gpodder/models.rs +++ b/src/server/gpodder/models.rs @@ -1,6 +1,8 @@ use chrono::{DateTime, NaiveDateTime, Utc}; use serde::{Deserialize, Serialize}; +use crate::gpodder; + #[derive(Deserialize, Debug)] pub struct SubscriptionDelta { pub add: Vec, diff --git a/src/server/gpodder/simple/subscriptions.rs b/src/server/gpodder/simple/subscriptions.rs index a91b766..6e9227c 100644 --- a/src/server/gpodder/simple/subscriptions.rs +++ b/src/server/gpodder/simple/subscriptions.rs @@ -5,10 +5,13 @@ use axum::{ Extension, Json, Router, }; -use crate::server::{ - error::{AppError, AppResult}, - gpodder::{auth_middleware, format::StringWithFormat}, - Context, +use crate::{ + gpodder, + server::{ + error::{AppError, AppResult}, + gpodder::{auth_middleware, format::StringWithFormat}, + Context, + }, }; pub fn router(ctx: Context) -> Router { diff --git a/src/server/mod.rs b/src/server/mod.rs index 6223e4d..93ac8d3 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,27 +1,18 @@ mod error; mod gpodder; -use axum::{ - body::Body, - extract::Request, - http::StatusCode, - middleware::Next, - response::{IntoResponse, Response}, - Router, -}; -use http_body_util::BodyExt; +use axum::{extract::Request, middleware::Next, response::Response, Router}; use tower_http::trace::TraceLayer; #[derive(Clone)] pub struct Context { - pub store: ::gpodder::GpodderRepository, + pub store: crate::gpodder::GpodderRepository, } pub fn app(ctx: Context) -> Router { Router::new() .merge(gpodder::router(ctx.clone())) .layer(axum::middleware::from_fn(header_logger)) - .layer(axum::middleware::from_fn(body_logger)) .layer(TraceLayer::new_for_http()) .with_state(ctx) } @@ -35,41 +26,3 @@ async fn header_logger(request: Request, next: Next) -> Response { res } - -async fn body_logger(request: Request, next: Next) -> Response { - let (parts, body) = request.into_parts(); - - let bytes = match body - .collect() - .await - .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()) - { - Ok(res) => res.to_bytes(), - Err(err) => { - return err; - } - }; - - tracing::debug!("request body = {:?}", String::from_utf8(bytes.to_vec())); - - let res = next - .run(Request::from_parts(parts, Body::from(bytes))) - .await; - - let (parts, body) = res.into_parts(); - - let bytes = match body - .collect() - .await - .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()) - { - Ok(res) => res.to_bytes(), - Err(err) => { - return err; - } - }; - - tracing::debug!("response body = {:?}", String::from_utf8(bytes.to_vec())); - - Response::from_parts(parts, Body::from(bytes)) -}