From 0cfcd90eba3b1ca6b8617340c19a7a2eadf4b05e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 19 Mar 2025 08:54:49 +0100 Subject: [PATCH] refactor: split gpodder repository and the sqlite data store implementation into separate crates The complete separation of concerns via the gpodder repository allows us to cleanly separate the server from the gpodder specification. This paves the way for a later Postgres implementation of the data store. --- Cargo.lock | 35 +- Cargo.toml | 14 +- gpodder/Cargo.lock | 520 ++++++++++ gpodder/Cargo.toml | 9 + src/gpodder/mod.rs => gpodder/src/lib.rs | 1 + {src/gpodder => gpodder/src}/models.rs | 0 {src/gpodder => gpodder/src}/repository.rs | 0 .env => gpodder_sqlite/.env | 0 gpodder_sqlite/Cargo.lock | 954 ++++++++++++++++++ gpodder_sqlite/Cargo.toml | 13 + diesel.toml => gpodder_sqlite/diesel.toml | 0 .../2025-02-23-095541_initial/down.sql | 0 .../2025-02-23-095541_initial/up.sql | 0 src/db/mod.rs => gpodder_sqlite/src/lib.rs | 18 +- .../src}/models/device.rs | 40 +- .../src}/models/device_subscription.rs | 7 +- .../src}/models/episode_action.rs | 10 +- {src/db => gpodder_sqlite/src}/models/mod.rs | 0 gpodder_sqlite/src/models/session.rs | 60 ++ .../src}/models/sync_group.rs | 2 +- gpodder_sqlite/src/models/user.rs | 47 + .../src}/repository/auth.rs | 53 +- gpodder_sqlite/src/repository/device.rs | 298 ++++++ .../src/repository/episode_action.rs | 176 ++++ .../src}/repository/mod.rs | 10 + .../src}/repository/subscription.rs | 264 ++--- {src/db => gpodder_sqlite/src}/schema.rs | 0 src/cli/gpo.rs | 9 +- src/cli/mod.rs | 9 +- src/cli/serve.rs | 12 +- src/db/models/session.rs | 62 -- src/db/models/user.rs | 67 -- src/db/repository/device.rs | 275 ----- src/db/repository/episode_action.rs | 170 ---- src/main.rs | 2 - src/server/error.rs | 14 +- src/server/gpodder/advanced/auth.rs | 11 +- src/server/gpodder/advanced/devices.rs | 17 +- src/server/gpodder/advanced/episodes.rs | 19 +- src/server/gpodder/advanced/subscriptions.rs | 17 +- src/server/gpodder/advanced/sync.rs | 17 +- src/server/gpodder/mod.rs | 2 +- src/server/gpodder/models.rs | 2 - src/server/gpodder/simple/subscriptions.rs | 11 +- src/server/mod.rs | 51 +- 45 files changed, 2416 insertions(+), 882 deletions(-) create mode 100644 gpodder/Cargo.lock create mode 100644 gpodder/Cargo.toml rename src/gpodder/mod.rs => gpodder/src/lib.rs (99%) rename {src/gpodder => gpodder/src}/models.rs (100%) rename {src/gpodder => gpodder/src}/repository.rs (100%) rename .env => gpodder_sqlite/.env (100%) create mode 100644 gpodder_sqlite/Cargo.lock create mode 100644 gpodder_sqlite/Cargo.toml rename diesel.toml => gpodder_sqlite/diesel.toml (100%) rename {migrations => gpodder_sqlite/migrations}/2025-02-23-095541_initial/down.sql (100%) rename {migrations => gpodder_sqlite/migrations}/2025-02-23-095541_initial/up.sql (100%) rename src/db/mod.rs => gpodder_sqlite/src/lib.rs (87%) rename {src/db => gpodder_sqlite/src}/models/device.rs (73%) rename {src/db => gpodder_sqlite/src}/models/device_subscription.rs (76%) rename {src/db => gpodder_sqlite/src}/models/episode_action.rs (90%) rename {src/db => gpodder_sqlite/src}/models/mod.rs (100%) create mode 100644 gpodder_sqlite/src/models/session.rs rename {src/db => gpodder_sqlite/src}/models/sync_group.rs (97%) create mode 100644 gpodder_sqlite/src/models/user.rs rename {src/db => gpodder_sqlite/src}/repository/auth.rs (67%) create mode 100644 gpodder_sqlite/src/repository/device.rs create mode 100644 gpodder_sqlite/src/repository/episode_action.rs rename {src/db => gpodder_sqlite/src}/repository/mod.rs (54%) rename {src/db => gpodder_sqlite/src}/repository/subscription.rs (55%) rename {src/db => gpodder_sqlite/src}/schema.rs (100%) delete mode 100644 src/db/models/session.rs delete mode 100644 src/db/models/user.rs delete mode 100644 src/db/repository/device.rs delete mode 100644 src/db/repository/episode_action.rs diff --git a/Cargo.lock b/Cargo.lock index 67158dc..44e9967 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -600,6 +600,28 @@ 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" @@ -659,12 +681,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "pin-project-lite", @@ -926,16 +948,15 @@ checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" name = "otter" version = "0.1.0" dependencies = [ - "argon2", "axum", "axum-extra", "chrono", "clap", "cookie", - "diesel", - "diesel_migrations", "figment", - "libsqlite3-sys", + "gpodder", + "gpodder_sqlite", + "http-body-util", "rand", "serde", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 73dc6b1..8f2f346 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,25 @@ +[workspace] +members = [ + 'gpodder', + 'gpodder_sqlite' +] + [package] name = "otter" version = "0.1.0" edition = "2021" [dependencies] -argon2 = "0.5.3" +gpodder = { path = "./gpodder" } +gpodder_sqlite = { path = "./gpodder_sqlite" } + 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"] } -libsqlite3-sys = { version = "0.31.0", features = ["bundled"] } +http-body-util = "0.1.3" rand = "0.8.5" serde = { version = "1.0.218", features = ["derive"] } tokio = { version = "1.43.0", features = ["full"] } diff --git a/gpodder/Cargo.lock b/gpodder/Cargo.lock new file mode 100644 index 0000000..dd49cdc --- /dev/null +++ b/gpodder/Cargo.lock @@ -0,0 +1,520 @@ +# 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 new file mode 100644 index 0000000..650529a --- /dev/null +++ b/gpodder/Cargo.toml @@ -0,0 +1,9 @@ +[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/src/gpodder/mod.rs b/gpodder/src/lib.rs similarity index 99% rename from src/gpodder/mod.rs rename to gpodder/src/lib.rs index c9abd8e..e834bcf 100644 --- a/src/gpodder/mod.rs +++ b/gpodder/src/lib.rs @@ -5,6 +5,7 @@ use std::fmt::Display; use chrono::{DateTime, Utc}; pub use models::*; + pub use repository::GpodderRepository; #[derive(Debug)] diff --git a/src/gpodder/models.rs b/gpodder/src/models.rs similarity index 100% rename from src/gpodder/models.rs rename to gpodder/src/models.rs diff --git a/src/gpodder/repository.rs b/gpodder/src/repository.rs similarity index 100% rename from src/gpodder/repository.rs rename to gpodder/src/repository.rs diff --git a/.env b/gpodder_sqlite/.env similarity index 100% rename from .env rename to gpodder_sqlite/.env diff --git a/gpodder_sqlite/Cargo.lock b/gpodder_sqlite/Cargo.lock new file mode 100644 index 0000000..a140641 --- /dev/null +++ b/gpodder_sqlite/Cargo.lock @@ -0,0 +1,954 @@ +# 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 new file mode 100644 index 0000000..7141b6d --- /dev/null +++ b/gpodder_sqlite/Cargo.toml @@ -0,0 +1,13 @@ +[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/diesel.toml b/gpodder_sqlite/diesel.toml similarity index 100% rename from diesel.toml rename to gpodder_sqlite/diesel.toml diff --git a/migrations/2025-02-23-095541_initial/down.sql b/gpodder_sqlite/migrations/2025-02-23-095541_initial/down.sql similarity index 100% rename from migrations/2025-02-23-095541_initial/down.sql rename to gpodder_sqlite/migrations/2025-02-23-095541_initial/down.sql diff --git a/migrations/2025-02-23-095541_initial/up.sql b/gpodder_sqlite/migrations/2025-02-23-095541_initial/up.sql similarity index 100% rename from migrations/2025-02-23-095541_initial/up.sql rename to gpodder_sqlite/migrations/2025-02-23-095541_initial/up.sql diff --git a/src/db/mod.rs b/gpodder_sqlite/src/lib.rs similarity index 87% rename from src/db/mod.rs rename to gpodder_sqlite/src/lib.rs index ef3eeb8..4b6ccbb 100644 --- a/src/db/mod.rs +++ b/gpodder_sqlite/src/lib.rs @@ -1,4 +1,4 @@ -pub mod models; +mod models; mod repository; mod schema; @@ -6,13 +6,6 @@ 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::{ @@ -64,6 +57,15 @@ 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; diff --git a/src/db/models/device.rs b/gpodder_sqlite/src/models/device.rs similarity index 73% rename from src/db/models/device.rs rename to gpodder_sqlite/src/models/device.rs index 2be8f75..25d3052 100644 --- a/src/db/models/device.rs +++ b/gpodder_sqlite/src/models/device.rs @@ -8,11 +8,10 @@ use diesel::{ sql_types::Text, sqlite::{Sqlite, SqliteValue}, }; -use serde::{Deserialize, Serialize}; -use crate::db::{schema::*, DbPool, DbResult}; +use crate::schema::*; -#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)] +#[derive(Clone, Queryable, Selectable)] #[diesel(table_name = devices)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct Device { @@ -24,7 +23,7 @@ pub struct Device { pub sync_group_id: Option, } -#[derive(Deserialize, Insertable)] +#[derive(Insertable)] #[diesel(table_name = devices)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct NewDevice { @@ -34,9 +33,8 @@ pub struct NewDevice { pub type_: DeviceType, } -#[derive(Serialize, Deserialize, FromSqlRow, Debug, AsExpression, Clone)] +#[derive(FromSqlRow, Debug, AsExpression, Clone)] #[diesel(sql_type = Text)] -#[serde(rename_all = "lowercase")] pub enum DeviceType { Desktop, Laptop, @@ -46,13 +44,6 @@ 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, @@ -82,22 +73,6 @@ 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 { @@ -109,13 +84,6 @@ 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/src/db/models/device_subscription.rs b/gpodder_sqlite/src/models/device_subscription.rs similarity index 76% rename from src/db/models/device_subscription.rs rename to gpodder_sqlite/src/models/device_subscription.rs index c2541c8..a77d2a3 100644 --- a/src/db/models/device_subscription.rs +++ b/gpodder_sqlite/src/models/device_subscription.rs @@ -1,9 +1,8 @@ use diesel::prelude::*; -use serde::{Deserialize, Serialize}; -use crate::db::schema::*; +use crate::schema::*; -#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)] +#[derive(Clone, Queryable, Selectable)] #[diesel(table_name = device_subscriptions)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct DeviceSubscription { @@ -14,7 +13,7 @@ pub struct DeviceSubscription { pub deleted: bool, } -#[derive(Deserialize, Insertable)] +#[derive(Insertable)] #[diesel(table_name = device_subscriptions)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct NewDeviceSubscription { diff --git a/src/db/models/episode_action.rs b/gpodder_sqlite/src/models/episode_action.rs similarity index 90% rename from src/db/models/episode_action.rs rename to gpodder_sqlite/src/models/episode_action.rs index b7fd89e..faef14f 100644 --- a/src/db/models/episode_action.rs +++ b/gpodder_sqlite/src/models/episode_action.rs @@ -9,11 +9,10 @@ use diesel::{ sqlite::{Sqlite, SqliteValue}, Selectable, }; -use serde::{Deserialize, Serialize}; -use crate::db::schema::*; +use crate::schema::*; -#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)] +#[derive(Clone, Queryable, Selectable)] #[diesel(table_name = episode_actions)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct EpisodeAction { @@ -30,7 +29,7 @@ pub struct EpisodeAction { pub total: Option, } -#[derive(Deserialize, Insertable)] +#[derive(Insertable)] #[diesel(table_name = episode_actions)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct NewEpisodeAction { @@ -46,9 +45,8 @@ pub struct NewEpisodeAction { pub total: Option, } -#[derive(Serialize, Deserialize, FromSqlRow, Debug, AsExpression, Clone)] +#[derive(FromSqlRow, Debug, AsExpression, Clone)] #[diesel(sql_type = Text)] -#[serde(rename_all = "lowercase")] pub enum ActionType { New, Download, diff --git a/src/db/models/mod.rs b/gpodder_sqlite/src/models/mod.rs similarity index 100% rename from src/db/models/mod.rs rename to gpodder_sqlite/src/models/mod.rs diff --git a/gpodder_sqlite/src/models/session.rs b/gpodder_sqlite/src/models/session.rs new file mode 100644 index 0000000..53fe6d9 --- /dev/null +++ b/gpodder_sqlite/src/models/session.rs @@ -0,0 +1,60 @@ +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/src/db/models/sync_group.rs b/gpodder_sqlite/src/models/sync_group.rs similarity index 97% rename from src/db/models/sync_group.rs rename to gpodder_sqlite/src/models/sync_group.rs index edeca8c..aad14e3 100644 --- a/src/db/models/sync_group.rs +++ b/gpodder_sqlite/src/models/sync_group.rs @@ -3,7 +3,7 @@ use diesel::{ prelude::*, }; -use crate::db::schema::*; +use crate::schema::*; #[derive(Queryable, Selectable)] #[diesel(table_name = sync_groups)] diff --git a/gpodder_sqlite/src/models/user.rs b/gpodder_sqlite/src/models/user.rs new file mode 100644 index 0000000..b7a15a9 --- /dev/null +++ b/gpodder_sqlite/src/models/user.rs @@ -0,0 +1,47 @@ +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 { + pub username: String, + pub password_hash: String, +} + +// 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/src/db/repository/auth.rs b/gpodder_sqlite/src/repository/auth.rs similarity index 67% rename from src/db/repository/auth.rs rename to gpodder_sqlite/src/repository/auth.rs index 3aafa05..e9b867a 100644 --- a/src/db/repository/auth.rs +++ b/gpodder_sqlite/src/repository/auth.rs @@ -1,26 +1,16 @@ use chrono::DateTime; use diesel::prelude::*; +use gpodder::AuthErr; use super::SqliteRepository; use crate::{ - db::{self, schema::*}, - gpodder::{self, AuthErr}, + models::{session::Session, user::User}, + schema::*, + DbError, }; -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 { +impl From for gpodder::User { + fn from(value: User) -> Self { Self { id: value.id, username: value.username, @@ -32,10 +22,11 @@ impl From for gpodder::User { impl gpodder::AuthStore for SqliteRepository { fn get_user(&self, username: &str) -> Result, AuthErr> { Ok(users::table - .select(db::User::as_select()) + .select(User::as_select()) .filter(users::username.eq(username)) - .first(&mut self.pool.get()?) - .optional()? + .first(&mut self.pool.get().map_err(DbError::from)?) + .optional() + .map_err(DbError::from)? .map(gpodder::User::from)) } @@ -43,35 +34,37 @@ impl gpodder::AuthStore for SqliteRepository { match sessions::table .inner_join(users::table) .filter(sessions::id.eq(session_id)) - .select((db::Session::as_select(), db::User::as_select())) - .get_result(&mut self.pool.get()?) + .select((Session::as_select(), User::as_select())) + .get_result(&mut self.pool.get().map_err(DbError::from)?) { Ok((session, user)) => Ok(Some(gpodder::Session { id: session.id, last_seen: DateTime::from_timestamp(session.last_seen, 0).unwrap(), user: user.into(), })), - Err(err) => Err(AuthErr::from(err)), + Err(err) => Err(DbError::from(err).into()), } } 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(|_| ())?, + .execute(&mut self.pool.get().map_err(DbError::from)?) + .map(|_| ()) + .map_err(DbError::from)?, ) } fn insert_session(&self, session: &gpodder::Session) -> Result<(), AuthErr> { - Ok(db::Session { + Ok(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(|_| ())?) + .execute(&mut self.pool.get().map_err(DbError::from)?) + .map(|_| ()) + .map_err(DbError::from)?) } fn refresh_session( @@ -81,7 +74,8 @@ 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()?)? + .execute(&mut self.pool.get().map_err(DbError::from)?) + .map_err(DbError::from)? == 0 { Err(AuthErr::UnknownSession) @@ -95,7 +89,8 @@ impl gpodder::AuthStore for SqliteRepository { Ok( diesel::delete(sessions::table.filter(sessions::last_seen.lt(min_last_seen))) - .execute(&mut self.pool.get()?)?, + .execute(&mut self.pool.get().map_err(DbError::from)?) + .map_err(DbError::from)?, ) } } diff --git a/gpodder_sqlite/src/repository/device.rs b/gpodder_sqlite/src/repository/device.rs new file mode 100644 index 0000000..d69caeb --- /dev/null +++ b/gpodder_sqlite/src/repository/device.rs @@ -0,0 +1,298 @@ +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 new file mode 100644 index 0000000..bf9aff1 --- /dev/null +++ b/gpodder_sqlite/src/repository/episode_action.rs @@ -0,0 +1,176 @@ +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/src/db/repository/mod.rs b/gpodder_sqlite/src/repository/mod.rs similarity index 54% rename from src/db/repository/mod.rs rename to gpodder_sqlite/src/repository/mod.rs index 01a81d4..6a80b28 100644 --- a/src/db/repository/mod.rs +++ b/gpodder_sqlite/src/repository/mod.rs @@ -3,6 +3,8 @@ mod device; mod episode_action; mod subscription; +use std::path::Path; + use super::DbPool; #[derive(Clone)] @@ -15,3 +17,11 @@ impl From for SqliteRepository { Self { pool: value } } } + +impl SqliteRepository { + pub fn from_path(path: impl AsRef) -> Result { + let pool = super::initialize_db(path, true)?; + + Ok(Self { pool }) + } +} diff --git a/src/db/repository/subscription.rs b/gpodder_sqlite/src/repository/subscription.rs similarity index 55% rename from src/db/repository/subscription.rs rename to gpodder_sqlite/src/repository/subscription.rs index 447a27d..da407a1 100644 --- a/src/db/repository/subscription.rs +++ b/gpodder_sqlite/src/repository/subscription.rs @@ -2,22 +2,15 @@ use std::collections::HashSet; use chrono::DateTime; use diesel::prelude::*; +use gpodder::AuthErr; use super::SqliteRepository; use crate::{ - db::{self, schema::*}, - gpodder, + models::device_subscription::{DeviceSubscription, NewDeviceSubscription}, + schema::*, + DbError, }; -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, @@ -80,7 +73,7 @@ fn set_subscriptions_for_single_device( .values( urls_to_insert .into_iter() - .map(|url| db::NewDeviceSubscription { + .map(|url| NewDeviceSubscription { device_id, podcast_url: url.to_string(), deleted: false, @@ -105,7 +98,7 @@ pub fn insert_subscriptions_for_single_device<'a>( diesel::insert_into(device_subscriptions::table) .values( urls.into_iter() - .map(|url| db::NewDeviceSubscription { + .map(|url| NewDeviceSubscription { device_id, podcast_url: url.to_string(), deleted: false, @@ -184,18 +177,26 @@ impl gpodder::SubscriptionRepository for SqliteRepository { &self, user: &gpodder::User, ) -> Result, gpodder::AuthErr> { - 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()) + (|| { + 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) } fn subscriptions_for_device( @@ -203,21 +204,29 @@ impl gpodder::SubscriptionRepository for SqliteRepository { user: &gpodder::User, device_id: &str, ) -> Result, gpodder::AuthErr> { - Ok(device_subscriptions::table - .inner_join(devices::table) - .filter( - devices::user_id - .eq(user.id) - .and(devices::device_id.eq(device_id)), + (|| { + 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(), ) - .select(( - device_subscriptions::podcast_url, - device_subscriptions::time_changed, - )) - .get_results::<(String, i64)>(&mut self.pool.get()?)? - .into_iter() - .map(Into::into) - .collect()) + })() + .map_err(AuthErr::from) } fn set_subscriptions_for_device( @@ -227,38 +236,39 @@ 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 { + 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)?; } - } else { - set_subscriptions_for_single_device(conn, device_id, &urls, time_changed)?; - } - Ok::<_, diesel::result::Error>(()) - })?; - - Ok(()) + Ok::<_, DbError>(()) + }) + })() + .map_err(AuthErr::from) } fn update_subscriptions_for_device( @@ -269,32 +279,42 @@ 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 { + for device_id in device_ids { + update_subscriptions_for_single_device( + conn, + device_id, + &add, + &remove, + time_changed, + )?; + } + } else { update_subscriptions_for_single_device( conn, device_id, @@ -303,20 +323,11 @@ impl gpodder::SubscriptionRepository for SqliteRepository { time_changed, )?; } - } else { - update_subscriptions_for_single_device( - conn, - device_id, - &add, - &remove, - time_changed, - )?; - } - Ok::<_, diesel::result::Error>(()) - })?; - - Ok(()) + Ok::<_, DbError>(()) + }) + })() + .map_err(AuthErr::from) } fn subscription_updates_for_device( @@ -325,36 +336,39 @@ 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(db::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(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((added, removed)) + Ok::<_, DbError>((added, removed)) + })() + .map_err(AuthErr::from) } } diff --git a/src/db/schema.rs b/gpodder_sqlite/src/schema.rs similarity index 100% rename from src/db/schema.rs rename to gpodder_sqlite/src/schema.rs diff --git a/src/cli/gpo.rs b/src/cli/gpo.rs index cbf5dbd..e4d951b 100644 --- a/src/cli/gpo.rs +++ b/src/cli/gpo.rs @@ -1,7 +1,5 @@ use clap::Subcommand; -use crate::db; - #[derive(Subcommand)] pub enum Command { /// Add devices of a specific user to the same sync group @@ -15,9 +13,10 @@ pub enum Command { impl Command { pub fn run(&self, config: &crate::config::Config) -> u8 { - 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); + let store = + gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME)) + .unwrap(); + let store = gpodder::GpodderRepository::new(store); match self { Self::Sync { username, devices } => { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index dc0a8ba..512c955 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,4 +1,4 @@ -mod db; +// mod db; mod gpo; mod serve; @@ -48,9 +48,8 @@ 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)] @@ -80,7 +79,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 67cf6e7..a434db0 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -1,18 +1,18 @@ use std::time::Duration; -use crate::{db, server}; +use crate::server; pub fn serve(config: &crate::config::Config) -> u8 { tracing_subscriber::fmt::init(); tracing::info!("Initializing database and running migrations"); - let pool = db::initialize_db(config.data_dir.join(crate::DB_FILENAME), true).unwrap(); - let repo = db::SqliteRepository::from(pool); + let store = + gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME)) + .unwrap(); + let store = gpodder::GpodderRepository::new(store); - let ctx = server::Context { - store: crate::gpodder::GpodderRepository::new(repo), - }; + let ctx = server::Context { store }; let app = server::app(ctx.clone()); let rt = tokio::runtime::Builder::new_multi_thread() diff --git a/src/db/models/session.rs b/src/db/models/session.rs deleted file mode 100644 index 273550a..0000000 --- a/src/db/models/session.rs +++ /dev/null @@ -1,62 +0,0 @@ -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/src/db/models/user.rs b/src/db/models/user.rs deleted file mode 100644 index f9c9bb9..0000000 --- a/src/db/models/user.rs +++ /dev/null @@ -1,67 +0,0 @@ -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/src/db/repository/device.rs b/src/db/repository/device.rs deleted file mode 100644 index cd1a4d9..0000000 --- a/src/db/repository/device.rs +++ /dev/null @@ -1,275 +0,0 @@ -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 deleted file mode 100644 index 5a45cd2..0000000 --- a/src/db/repository/episode_action.rs +++ /dev/null @@ -1,170 +0,0 @@ -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/main.rs b/src/main.rs index 2e17a7b..b42d31a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,5 @@ 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 5d7aeb4..dacc052 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::{db, ErrorExt}; +use crate::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,12 +40,6 @@ 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 b0bccaf..b4adaeb 100644 --- a/src/server/gpodder/advanced/auth.rs +++ b/src/server/gpodder/advanced/auth.rs @@ -10,13 +10,10 @@ use axum_extra::{ }; use cookie::time::Duration; -use crate::{ - gpodder, - server::{ - error::{AppError, AppResult}, - gpodder::SESSION_ID_COOKIE, - Context, - }, +use crate::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 859e1c1..67f4f79 100644 --- a/src/server/gpodder/advanced/devices.rs +++ b/src/server/gpodder/advanced/devices.rs @@ -5,17 +5,14 @@ use axum::{ Extension, Json, Router, }; -use crate::{ - gpodder, - server::{ - error::{AppError, AppResult}, - gpodder::{ - auth_middleware, - format::{Format, StringWithFormat}, - models, - }, - Context, +use crate::server::{ + error::{AppError, AppResult}, + gpodder::{ + auth_middleware, + format::{Format, StringWithFormat}, + models, }, + Context, }; pub fn router(ctx: Context) -> Router { diff --git a/src/server/gpodder/advanced/episodes.rs b/src/server/gpodder/advanced/episodes.rs index 6c7ff32..5ea62d6 100644 --- a/src/server/gpodder/advanced/episodes.rs +++ b/src/server/gpodder/advanced/episodes.rs @@ -7,18 +7,15 @@ use axum::{ use chrono::DateTime; use serde::{Deserialize, Serialize}; -use crate::{ - gpodder, - server::{ - error::{AppError, AppResult}, - gpodder::{ - auth_middleware, - format::{Format, StringWithFormat}, - models, - models::UpdatedUrlsResponse, - }, - Context, +use crate::server::{ + error::{AppError, AppResult}, + gpodder::{ + auth_middleware, + format::{Format, StringWithFormat}, + models, + models::UpdatedUrlsResponse, }, + Context, }; pub fn router(ctx: Context) -> Router { diff --git a/src/server/gpodder/advanced/subscriptions.rs b/src/server/gpodder/advanced/subscriptions.rs index e5691e1..b69f420 100644 --- a/src/server/gpodder/advanced/subscriptions.rs +++ b/src/server/gpodder/advanced/subscriptions.rs @@ -6,17 +6,14 @@ use axum::{ }; use serde::Deserialize; -use crate::{ - gpodder, - server::{ - error::{AppError, AppResult}, - gpodder::{ - auth_middleware, - format::{Format, StringWithFormat}, - models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse}, - }, - Context, +use crate::server::{ + error::{AppError, AppResult}, + gpodder::{ + auth_middleware, + format::{Format, StringWithFormat}, + models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse}, }, + Context, }; pub fn router(ctx: Context) -> Router { diff --git a/src/server/gpodder/advanced/sync.rs b/src/server/gpodder/advanced/sync.rs index fc97869..728452c 100644 --- a/src/server/gpodder/advanced/sync.rs +++ b/src/server/gpodder/advanced/sync.rs @@ -5,17 +5,14 @@ use axum::{ Extension, Json, Router, }; -use crate::{ - gpodder, - server::{ - error::{AppError, AppResult}, - gpodder::{ - auth_middleware, - format::{Format, StringWithFormat}, - models::{SyncStatus, SyncStatusDelta}, - }, - Context, +use crate::server::{ + error::{AppError, AppResult}, + gpodder::{ + auth_middleware, + format::{Format, StringWithFormat}, + models::{SyncStatus, SyncStatusDelta}, }, + Context, }; pub fn router(ctx: Context) -> Router { diff --git a/src/server/gpodder/mod.rs b/src/server/gpodder/mod.rs index 4746323..83442f3 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::{gpodder, server::error::AppError}; +use crate::server::error::AppError; use super::Context; diff --git a/src/server/gpodder/models.rs b/src/server/gpodder/models.rs index 20d459f..5e0f010 100644 --- a/src/server/gpodder/models.rs +++ b/src/server/gpodder/models.rs @@ -1,8 +1,6 @@ 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 6e9227c..a91b766 100644 --- a/src/server/gpodder/simple/subscriptions.rs +++ b/src/server/gpodder/simple/subscriptions.rs @@ -5,13 +5,10 @@ use axum::{ Extension, Json, Router, }; -use crate::{ - gpodder, - server::{ - error::{AppError, AppResult}, - gpodder::{auth_middleware, format::StringWithFormat}, - Context, - }, +use crate::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 93ac8d3..6223e4d 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,18 +1,27 @@ mod error; mod gpodder; -use axum::{extract::Request, middleware::Next, response::Response, Router}; +use axum::{ + body::Body, + extract::Request, + http::StatusCode, + middleware::Next, + response::{IntoResponse, Response}, + Router, +}; +use http_body_util::BodyExt; use tower_http::trace::TraceLayer; #[derive(Clone)] pub struct Context { - pub store: crate::gpodder::GpodderRepository, + pub store: ::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) } @@ -26,3 +35,41 @@ 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)) +}