Compare commits

...

5 Commits

Author SHA1 Message Date
Jef Roosens 0bb0c5657a
docs(gpodder): document session last_seen timestamp's precision should be at least to the second 2025-03-19 10:55:20 +01:00
Jef Roosens 705b347775
feat(gpodder_sqlite): set up testing 2025-03-19 10:47:07 +01:00
Jef Roosens b44a47fefd
feat(gpodder): add create_user method to AuthStore 2025-03-19 10:46:34 +01:00
Jef Roosens 2a8917f21d
refactor: split up gpodder module files 2025-03-19 09:05:41 +01:00
Jef Roosens 0cfcd90eba
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.
2025-03-19 08:54:49 +01:00
49 changed files with 2580 additions and 913 deletions

35
Cargo.lock generated
View File

@ -600,6 +600,28 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 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]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.2" version = "0.15.2"
@ -659,12 +681,12 @@ dependencies = [
[[package]] [[package]]
name = "http-body-util" name = "http-body-util"
version = "0.1.2" version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-util", "futures-core",
"http", "http",
"http-body", "http-body",
"pin-project-lite", "pin-project-lite",
@ -926,16 +948,15 @@ checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
name = "otter" name = "otter"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"argon2",
"axum", "axum",
"axum-extra", "axum-extra",
"chrono", "chrono",
"clap", "clap",
"cookie", "cookie",
"diesel",
"diesel_migrations",
"figment", "figment",
"libsqlite3-sys", "gpodder",
"gpodder_sqlite",
"http-body-util",
"rand", "rand",
"serde", "serde",
"tokio", "tokio",

View File

@ -1,19 +1,25 @@
[workspace]
members = [
'gpodder',
'gpodder_sqlite'
]
[package] [package]
name = "otter" name = "otter"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
argon2 = "0.5.3" gpodder = { path = "./gpodder" }
gpodder_sqlite = { path = "./gpodder_sqlite" }
axum = { version = "0.8.1", features = ["macros"] } axum = { version = "0.8.1", features = ["macros"] }
axum-extra = { version = "0.10", features = ["cookie", "typed-header"] } axum-extra = { version = "0.10", features = ["cookie", "typed-header"] }
chrono = { version = "0.4.39", features = ["serde"] } chrono = { version = "0.4.39", features = ["serde"] }
clap = { version = "4.5.30", features = ["derive", "env"] } clap = { version = "4.5.30", features = ["derive", "env"] }
cookie = "0.18.1" 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"] } 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" rand = "0.8.5"
serde = { version = "1.0.218", features = ["derive"] } serde = { version = "1.0.218", features = ["derive"] }
tokio = { version = "1.43.0", features = ["full"] } tokio = { version = "1.43.0", features = ["full"] }

520
gpodder/Cargo.lock generated 100644
View File

@ -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",
]

View File

@ -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"

View File

@ -0,0 +1,9 @@
pub mod models;
mod repository;
mod store;
pub use models::*;
pub use repository::GpodderRepository;
pub use store::{
AuthErr, AuthStore, DeviceRepository, EpisodeActionRepository, Store, SubscriptionRepository,
};

View File

@ -1,12 +1,13 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
#[derive(Clone)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct User { pub struct User {
pub id: i64, pub id: i64,
pub username: String, pub username: String,
pub password_hash: String, pub password_hash: String,
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DeviceType { pub enum DeviceType {
Desktop, Desktop,
Laptop, Laptop,
@ -15,6 +16,7 @@ pub enum DeviceType {
Other, Other,
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Device { pub struct Device {
pub id: String, pub id: String,
pub caption: String, pub caption: String,
@ -22,11 +24,13 @@ pub struct Device {
pub subscriptions: i64, pub subscriptions: i64,
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DevicePatch { pub struct DevicePatch {
pub caption: Option<String>, pub caption: Option<String>,
pub r#type: Option<DeviceType>, pub r#type: Option<DeviceType>,
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EpisodeActionType { pub enum EpisodeActionType {
Download, Download,
Play { Play {
@ -38,6 +42,7 @@ pub enum EpisodeActionType {
New, New,
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EpisodeAction { pub struct EpisodeAction {
pub podcast: String, pub podcast: String,
pub episode: String, pub episode: String,
@ -47,12 +52,14 @@ pub struct EpisodeAction {
pub action: EpisodeActionType, pub action: EpisodeActionType,
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Session { pub struct Session {
pub id: i64, pub id: i64,
pub last_seen: DateTime<Utc>, pub last_seen: DateTime<Utc>,
pub user: User, pub user: User,
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Subscription { pub struct Subscription {
pub url: String, pub url: String,
pub time_changed: DateTime<Utc>, pub time_changed: DateTime<Utc>,

View File

@ -1,10 +1,13 @@
use std::{collections::HashSet, sync::Arc}; use std::{collections::HashSet, sync::Arc};
use argon2::{Argon2, PasswordHash, PasswordVerifier}; use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Utc};
use rand::Rng; use rand::{rngs::OsRng, Rng};
use super::{models, AuthErr, Store}; use crate::{
models,
store::{AuthErr, Store},
};
const MAX_SESSION_AGE: i64 = 60 * 60 * 24 * 7; const MAX_SESSION_AGE: i64 = 60 * 60 * 24 * 7;
@ -38,6 +41,17 @@ impl GpodderRepository {
self.store.get_user(username)?.ok_or(AuthErr::UnknownUser) self.store.get_user(username)?.ok_or(AuthErr::UnknownUser)
} }
pub fn create_user(&self, username: &str, password: &str) -> Result<models::User, AuthErr> {
let salt = SaltString::generate(&mut OsRng);
let password_hash = Argon2::default()
.hash_password(password.as_bytes(), &salt)
.unwrap()
.to_string();
self.store.insert_user(username, &password_hash)
}
pub fn validate_credentials( pub fn validate_credentials(
&self, &self,
username: &str, username: &str,

View File

@ -1,11 +1,7 @@
pub mod models;
mod repository;
use std::fmt::Display; use std::fmt::Display;
use crate::models::*;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
pub use models::*;
pub use repository::GpodderRepository;
#[derive(Debug)] #[derive(Debug)]
pub enum AuthErr { pub enum AuthErr {
@ -40,12 +36,17 @@ impl<T> Store for T where
pub trait AuthStore { pub trait AuthStore {
/// Retrieve the session with the given session ID /// Retrieve the session with the given session ID
fn get_session(&self, session_id: i64) -> Result<Option<models::Session>, AuthErr>; fn get_session(&self, session_id: i64) -> Result<Option<Session>, AuthErr>;
/// Retrieve the user with the given username /// Retrieve the user with the given username
fn get_user(&self, username: &str) -> Result<Option<models::User>, AuthErr>; fn get_user(&self, username: &str) -> Result<Option<User>, AuthErr>;
/// Insert a new user into the data store
fn insert_user(&self, username: &str, password_hash: &str) -> Result<User, AuthErr>;
/// Create a new session for a user with the given session ID /// Create a new session for a user with the given session ID
///
/// The `last_seen` timestamp's precision should be at least accurate to the second
fn insert_session(&self, session: &Session) -> Result<(), AuthErr>; fn insert_session(&self, session: &Session) -> Result<(), AuthErr>;
/// Remove the session with the given session ID /// Remove the session with the given session ID
@ -109,10 +110,10 @@ pub trait SubscriptionRepository {
&self, &self,
user: &User, user: &User,
device_id: &str, device_id: &str,
) -> Result<Vec<models::Subscription>, AuthErr>; ) -> Result<Vec<Subscription>, AuthErr>;
/// Return all subscriptions for a given user /// Return all subscriptions for a given user
fn subscriptions_for_user(&self, user: &User) -> Result<Vec<models::Subscription>, AuthErr>; fn subscriptions_for_user(&self, user: &User) -> Result<Vec<Subscription>, AuthErr>;
/// Replace the list of subscriptions for a device and all devices in its sync group with the /// Replace the list of subscriptions for a device and all devices in its sync group with the
/// given list /// given list

954
gpodder_sqlite/Cargo.lock generated 100644
View File

@ -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",
]

View File

@ -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"] }

View File

@ -1,4 +1,4 @@
pub mod models; mod models;
mod repository; mod repository;
mod schema; mod schema;
@ -6,13 +6,6 @@ use diesel::connection::InstrumentationEvent;
use diesel::r2d2::CustomizeConnection; use diesel::r2d2::CustomizeConnection;
use diesel::Connection; 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; pub use repository::SqliteRepository;
use diesel::{ use diesel::{
@ -64,6 +57,15 @@ impl From<diesel::result::Error> for DbError {
} }
} }
impl From<DbError> 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)] #[derive(Debug)]
pub struct AddQueryDebugLogs; pub struct AddQueryDebugLogs;
@ -92,3 +94,16 @@ pub fn initialize_db(path: impl AsRef<Path>, run_migrations: bool) -> Result<DbP
Ok(pool) Ok(pool)
} }
pub fn initialize_db_in_memory(run_migrations: bool) -> Result<DbPool, DbError> {
let manager = ConnectionManager::<SqliteConnection>::new(":memory:");
let pool = Pool::builder()
.connection_customizer(Box::new(AddQueryDebugLogs))
.build(manager)?;
if run_migrations {
pool.get()?.run_pending_migrations(MIGRATIONS).unwrap();
}
Ok(pool)
}

View File

@ -8,11 +8,10 @@ use diesel::{
sql_types::Text, sql_types::Text,
sqlite::{Sqlite, SqliteValue}, 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(table_name = devices)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Device { pub struct Device {
@ -24,7 +23,7 @@ pub struct Device {
pub sync_group_id: Option<i64>, pub sync_group_id: Option<i64>,
} }
#[derive(Deserialize, Insertable)] #[derive(Insertable)]
#[diesel(table_name = devices)] #[diesel(table_name = devices)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct NewDevice { pub struct NewDevice {
@ -34,9 +33,8 @@ pub struct NewDevice {
pub type_: DeviceType, pub type_: DeviceType,
} }
#[derive(Serialize, Deserialize, FromSqlRow, Debug, AsExpression, Clone)] #[derive(FromSqlRow, Debug, AsExpression, Clone)]
#[diesel(sql_type = Text)] #[diesel(sql_type = Text)]
#[serde(rename_all = "lowercase")]
pub enum DeviceType { pub enum DeviceType {
Desktop, Desktop,
Laptop, Laptop,
@ -46,13 +44,6 @@ pub enum DeviceType {
} }
impl Device { impl Device {
pub fn for_user(pool: &DbPool, user_id: i64) -> DbResult<Vec<Self>> {
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( pub fn device_id_to_id(
conn: &mut SqliteConnection, conn: &mut SqliteConnection,
user_id: i64, user_id: i64,
@ -82,22 +73,6 @@ impl Device {
) )
.get_result(conn) .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 { impl NewDevice {
@ -109,13 +84,6 @@ impl NewDevice {
type_, type_,
} }
} }
pub fn insert(self, pool: &DbPool) -> DbResult<Device> {
Ok(diesel::insert_into(devices::table)
.values(&self)
.returning(Device::as_returning())
.get_result(&mut pool.get()?)?)
}
} }
impl fmt::Display for DeviceType { impl fmt::Display for DeviceType {

View File

@ -1,9 +1,8 @@
use diesel::prelude::*; 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(table_name = device_subscriptions)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct DeviceSubscription { pub struct DeviceSubscription {
@ -14,7 +13,7 @@ pub struct DeviceSubscription {
pub deleted: bool, pub deleted: bool,
} }
#[derive(Deserialize, Insertable)] #[derive(Insertable)]
#[diesel(table_name = device_subscriptions)] #[diesel(table_name = device_subscriptions)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct NewDeviceSubscription { pub struct NewDeviceSubscription {

View File

@ -9,11 +9,10 @@ use diesel::{
sqlite::{Sqlite, SqliteValue}, sqlite::{Sqlite, SqliteValue},
Selectable, 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(table_name = episode_actions)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct EpisodeAction { pub struct EpisodeAction {
@ -30,7 +29,7 @@ pub struct EpisodeAction {
pub total: Option<i32>, pub total: Option<i32>,
} }
#[derive(Deserialize, Insertable)] #[derive(Insertable)]
#[diesel(table_name = episode_actions)] #[diesel(table_name = episode_actions)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct NewEpisodeAction { pub struct NewEpisodeAction {
@ -46,9 +45,8 @@ pub struct NewEpisodeAction {
pub total: Option<i32>, pub total: Option<i32>,
} }
#[derive(Serialize, Deserialize, FromSqlRow, Debug, AsExpression, Clone)] #[derive(FromSqlRow, Debug, AsExpression, Clone)]
#[diesel(sql_type = Text)] #[diesel(sql_type = Text)]
#[serde(rename_all = "lowercase")]
pub enum ActionType { pub enum ActionType {
New, New,
Download, Download,

View File

@ -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<Self> {
// 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<Option<super::user::User>> {
// 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<Option<super::user::User>> {
// Self::user_from_id(pool, self.id)
// }
// pub fn by_id(pool: &DbPool, id: i64) -> DbResult<Option<Self>> {
// Ok(sessions::dsl::sessions
// .find(id)
// .get_result(&mut pool.get()?)
// .optional()?)
// }
// pub fn remove(self, pool: &DbPool) -> DbResult<bool> {
// Self::remove_by_id(pool, self.id)
// }
// pub fn remove_by_id(pool: &DbPool, id: i64) -> DbResult<bool> {
// Ok(
// diesel::delete(sessions::dsl::sessions.filter(sessions::id.eq(id)))
// .execute(&mut pool.get()?)?
// > 0,
// )
// }
}

View File

@ -3,7 +3,7 @@ use diesel::{
prelude::*, prelude::*,
}; };
use crate::db::schema::*; use crate::schema::*;
#[derive(Queryable, Selectable)] #[derive(Queryable, Selectable)]
#[diesel(table_name = sync_groups)] #[diesel(table_name = sync_groups)]

View File

@ -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<'a> {
pub username: &'a str,
pub password_hash: &'a str,
}
// impl NewUser {
// pub fn new(username: String, password: String) -> Self {
// Self {
// username,
// password_hash: hash_password(&password),
// }
// }
// }
// impl User {
// pub fn by_username(pool: &DbPool, username: impl AsRef<str>) -> DbResult<Option<Self>> {
// 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<str>) -> bool {
// let password_hash = PasswordHash::new(&self.password_hash).unwrap();
// Argon2::default()
// .verify_password(password.as_ref().as_bytes(), &password_hash)
// .is_ok()
// }
// }

View File

@ -1,26 +1,19 @@
use chrono::DateTime; use chrono::DateTime;
use diesel::prelude::*; use diesel::prelude::*;
use gpodder::AuthErr;
use super::SqliteRepository; use super::SqliteRepository;
use crate::{ use crate::{
db::{self, schema::*}, models::{
gpodder::{self, AuthErr}, session::Session,
user::{NewUser, User},
},
schema::*,
DbError,
}; };
impl From<diesel::r2d2::PoolError> for gpodder::AuthErr { impl From<User> for gpodder::User {
fn from(value: diesel::r2d2::PoolError) -> Self { fn from(value: User) -> Self {
Self::Other(Box::new(value))
}
}
impl From<diesel::result::Error> for gpodder::AuthErr {
fn from(value: diesel::result::Error) -> Self {
Self::Other(Box::new(value))
}
}
impl From<db::User> for gpodder::User {
fn from(value: db::User) -> Self {
Self { Self {
id: value.id, id: value.id,
username: value.username, username: value.username,
@ -32,46 +25,65 @@ impl From<db::User> for gpodder::User {
impl gpodder::AuthStore for SqliteRepository { impl gpodder::AuthStore for SqliteRepository {
fn get_user(&self, username: &str) -> Result<Option<gpodder::models::User>, AuthErr> { fn get_user(&self, username: &str) -> Result<Option<gpodder::models::User>, AuthErr> {
Ok(users::table Ok(users::table
.select(db::User::as_select()) .select(User::as_select())
.filter(users::username.eq(username)) .filter(users::username.eq(username))
.first(&mut self.pool.get()?) .first(&mut self.pool.get().map_err(DbError::from)?)
.optional()? .optional()
.map_err(DbError::from)?
.map(gpodder::User::from)) .map(gpodder::User::from))
} }
fn insert_user(&self, username: &str, password_hash: &str) -> Result<gpodder::User, AuthErr> {
let conn = &mut self.pool.get().map_err(DbError::from)?;
Ok(diesel::insert_into(users::table)
.values(NewUser {
username,
password_hash,
})
.returning(User::as_returning())
.get_result(conn)
.map(gpodder::User::from)
.map_err(DbError::from)?)
}
fn get_session(&self, session_id: i64) -> Result<Option<gpodder::models::Session>, AuthErr> { fn get_session(&self, session_id: i64) -> Result<Option<gpodder::models::Session>, AuthErr> {
match sessions::table match sessions::table
.inner_join(users::table) .inner_join(users::table)
.filter(sessions::id.eq(session_id)) .filter(sessions::id.eq(session_id))
.select((db::Session::as_select(), db::User::as_select())) .select((Session::as_select(), User::as_select()))
.get_result(&mut self.pool.get()?) .get_result(&mut self.pool.get().map_err(DbError::from)?)
.optional()
{ {
Ok((session, user)) => Ok(Some(gpodder::Session { Ok(Some((session, user))) => Ok(Some(gpodder::Session {
id: session.id, id: session.id,
last_seen: DateTime::from_timestamp(session.last_seen, 0).unwrap(), last_seen: DateTime::from_timestamp(session.last_seen, 0).unwrap(),
user: user.into(), user: user.into(),
})), })),
Err(err) => Err(AuthErr::from(err)), Ok(None) => Ok(None),
Err(err) => Err(DbError::from(err).into()),
} }
} }
fn remove_session(&self, session_id: i64) -> Result<(), AuthErr> { fn remove_session(&self, session_id: i64) -> Result<(), AuthErr> {
Ok( Ok(
diesel::delete(sessions::table.filter(sessions::id.eq(session_id))) diesel::delete(sessions::table.filter(sessions::id.eq(session_id)))
.execute(&mut self.pool.get()?) .execute(&mut self.pool.get().map_err(DbError::from)?)
.map(|_| ())?, .map(|_| ())
.map_err(DbError::from)?,
) )
} }
fn insert_session(&self, session: &gpodder::Session) -> Result<(), AuthErr> { fn insert_session(&self, session: &gpodder::Session) -> Result<(), AuthErr> {
Ok(db::Session { Ok(Session {
id: session.id, id: session.id,
user_id: session.user.id, user_id: session.user.id,
last_seen: session.last_seen.timestamp(), last_seen: session.last_seen.timestamp(),
} }
.insert_into(sessions::table) .insert_into(sessions::table)
.execute(&mut self.pool.get()?) .execute(&mut self.pool.get().map_err(DbError::from)?)
.map(|_| ())?) .map(|_| ())
.map_err(DbError::from)?)
} }
fn refresh_session( fn refresh_session(
@ -81,7 +93,8 @@ impl gpodder::AuthStore for SqliteRepository {
) -> Result<(), AuthErr> { ) -> Result<(), AuthErr> {
if diesel::update(sessions::table.filter(sessions::id.eq(session.id))) if diesel::update(sessions::table.filter(sessions::id.eq(session.id)))
.set(sessions::last_seen.eq(timestamp.timestamp())) .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 == 0
{ {
Err(AuthErr::UnknownSession) Err(AuthErr::UnknownSession)
@ -95,7 +108,8 @@ impl gpodder::AuthStore for SqliteRepository {
Ok( Ok(
diesel::delete(sessions::table.filter(sessions::last_seen.lt(min_last_seen))) 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)?,
) )
} }
} }

View File

@ -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<DeviceType> 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<gpodder::DeviceType> 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<Vec<gpodder::Device>, 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<i64, gpodder::AuthErr> {
(|| {
let conn = &mut self.pool.get()?;
conn.transaction(|conn| {
let devices: Vec<(i64, Option<i64>)> = 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<i64> = 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::<i64>))
.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<Utc>,
) -> Result<(), gpodder::AuthErr> {
(|| {
let time_changed = time_changed.timestamp();
let conn = &mut self.pool.get()?;
conn.transaction(|conn| {
let device_ids: Vec<i64> = 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::<Result<HashSet<String>, _>>()?;
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<String>, Vec<Vec<String>>), 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<i64>), _>(conn)?;
let mut cur_group = &mut not_synchronized;
let mut cur_group_id: Option<i64> = 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)
}
}

View File

@ -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<gpodder::EpisodeAction> 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<String>, 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<gpodder::EpisodeAction>,
time_changed: DateTime<Utc>,
) -> 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<String> 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<DateTime<Utc>>,
podcast: Option<String>,
device: Option<String>,
aggregated: bool,
) -> Result<Vec<gpodder::EpisodeAction>, 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<String>, 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)
}
}

View File

@ -0,0 +1,33 @@
mod auth;
mod device;
mod episode_action;
mod subscription;
use std::path::Path;
use super::DbPool;
#[derive(Clone)]
pub struct SqliteRepository {
pool: DbPool,
}
impl From<DbPool> for SqliteRepository {
fn from(value: DbPool) -> Self {
Self { pool: value }
}
}
impl SqliteRepository {
pub fn from_path(path: impl AsRef<Path>) -> Result<Self, gpodder::AuthErr> {
let pool = super::initialize_db(path, true)?;
Ok(Self { pool })
}
pub fn in_memory() -> Result<Self, gpodder::AuthErr> {
let pool = super::initialize_db_in_memory(true)?;
Ok(Self { pool })
}
}

View File

@ -2,22 +2,15 @@ use std::collections::HashSet;
use chrono::DateTime; use chrono::DateTime;
use diesel::prelude::*; use diesel::prelude::*;
use gpodder::AuthErr;
use super::SqliteRepository; use super::SqliteRepository;
use crate::{ use crate::{
db::{self, schema::*}, models::device_subscription::{DeviceSubscription, NewDeviceSubscription},
gpodder, 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( fn set_subscriptions_for_single_device(
conn: &mut SqliteConnection, conn: &mut SqliteConnection,
device_id: i64, device_id: i64,
@ -80,7 +73,7 @@ fn set_subscriptions_for_single_device(
.values( .values(
urls_to_insert urls_to_insert
.into_iter() .into_iter()
.map(|url| db::NewDeviceSubscription { .map(|url| NewDeviceSubscription {
device_id, device_id,
podcast_url: url.to_string(), podcast_url: url.to_string(),
deleted: false, deleted: false,
@ -105,7 +98,7 @@ pub fn insert_subscriptions_for_single_device<'a>(
diesel::insert_into(device_subscriptions::table) diesel::insert_into(device_subscriptions::table)
.values( .values(
urls.into_iter() urls.into_iter()
.map(|url| db::NewDeviceSubscription { .map(|url| NewDeviceSubscription {
device_id, device_id,
podcast_url: url.to_string(), podcast_url: url.to_string(),
deleted: false, deleted: false,
@ -184,18 +177,26 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
&self, &self,
user: &gpodder::User, user: &gpodder::User,
) -> Result<Vec<gpodder::Subscription>, gpodder::AuthErr> { ) -> Result<Vec<gpodder::Subscription>, gpodder::AuthErr> {
Ok(device_subscriptions::table (|| {
.inner_join(devices::table) Ok::<_, DbError>(
.filter(devices::user_id.eq(user.id)) device_subscriptions::table
.select(( .inner_join(devices::table)
device_subscriptions::podcast_url, .filter(devices::user_id.eq(user.id))
device_subscriptions::time_changed, .select((
)) device_subscriptions::podcast_url,
.distinct() device_subscriptions::time_changed,
.get_results::<(String, i64)>(&mut self.pool.get()?)? ))
.into_iter() .distinct()
.map(Into::into) .get_results::<(String, i64)>(&mut self.pool.get()?)?
.collect()) .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( fn subscriptions_for_device(
@ -203,21 +204,29 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
user: &gpodder::User, user: &gpodder::User,
device_id: &str, device_id: &str,
) -> Result<Vec<gpodder::Subscription>, gpodder::AuthErr> { ) -> Result<Vec<gpodder::Subscription>, gpodder::AuthErr> {
Ok(device_subscriptions::table (|| {
.inner_join(devices::table) Ok::<_, DbError>(
.filter( device_subscriptions::table
devices::user_id .inner_join(devices::table)
.eq(user.id) .filter(
.and(devices::device_id.eq(device_id)), 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, .map_err(AuthErr::from)
device_subscriptions::time_changed,
))
.get_results::<(String, i64)>(&mut self.pool.get()?)?
.into_iter()
.map(Into::into)
.collect())
} }
fn set_subscriptions_for_device( fn set_subscriptions_for_device(
@ -227,38 +236,39 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
urls: Vec<String>, urls: Vec<String>,
time_changed: chrono::DateTime<chrono::Utc>, time_changed: chrono::DateTime<chrono::Utc>,
) -> Result<(), gpodder::AuthErr> { ) -> Result<(), gpodder::AuthErr> {
let time_changed = time_changed.timestamp(); (|| {
let urls: HashSet<String> = urls.into_iter().collect(); let time_changed = time_changed.timestamp();
let urls: HashSet<String> = urls.into_iter().collect();
self.pool.get()?.transaction(|conn| { self.pool.get()?.transaction(|conn| {
let (device_id, group_id) = devices::table let (device_id, group_id) = devices::table
.select((devices::id, devices::sync_group_id)) .select((devices::id, devices::sync_group_id))
.filter( .filter(
devices::user_id devices::user_id
.eq(user.id) .eq(user.id)
.and(devices::device_id.eq(device_id)), .and(devices::device_id.eq(device_id)),
) )
.get_result::<(i64, Option<i64>)>(conn)?; .get_result::<(i64, Option<i64>)>(conn)?;
// If the device is part of a sync group, we need to perform the update on every device // If the device is part of a sync group, we need to perform the update on every device
// in the group // in the group
if let Some(group_id) = group_id { if let Some(group_id) = group_id {
let device_ids: Vec<i64> = devices::table let device_ids: Vec<i64> = devices::table
.filter(devices::sync_group_id.eq(group_id)) .filter(devices::sync_group_id.eq(group_id))
.select(devices::id) .select(devices::id)
.get_results(conn)?; .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)?; 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::<_, DbError>(())
})?; })
})()
Ok(()) .map_err(AuthErr::from)
} }
fn update_subscriptions_for_device( fn update_subscriptions_for_device(
@ -269,32 +279,42 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
remove: Vec<String>, remove: Vec<String>,
time_changed: chrono::DateTime<chrono::Utc>, time_changed: chrono::DateTime<chrono::Utc>,
) -> Result<(), gpodder::AuthErr> { ) -> 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", // 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. // meaning their change timestamp will be updated even though they haven't really changed.
let add: HashSet<_> = add.into_iter().collect(); let add: HashSet<_> = add.into_iter().collect();
let remove: HashSet<_> = remove.into_iter().collect(); let remove: HashSet<_> = remove.into_iter().collect();
self.pool.get()?.transaction(|conn| { self.pool.get()?.transaction(|conn| {
let (device_id, group_id) = devices::table let (device_id, group_id) = devices::table
.select((devices::id, devices::sync_group_id)) .select((devices::id, devices::sync_group_id))
.filter( .filter(
devices::user_id devices::user_id
.eq(user.id) .eq(user.id)
.and(devices::device_id.eq(device_id)), .and(devices::device_id.eq(device_id)),
) )
.get_result::<(i64, Option<i64>)>(conn)?; .get_result::<(i64, Option<i64>)>(conn)?;
// If the device is part of a sync group, we need to perform the update on every device // If the device is part of a sync group, we need to perform the update on every device
// in the group // in the group
if let Some(group_id) = group_id { if let Some(group_id) = group_id {
let device_ids: Vec<i64> = devices::table let device_ids: Vec<i64> = devices::table
.filter(devices::sync_group_id.eq(group_id)) .filter(devices::sync_group_id.eq(group_id))
.select(devices::id) .select(devices::id)
.get_results(conn)?; .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( update_subscriptions_for_single_device(
conn, conn,
device_id, device_id,
@ -303,20 +323,11 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
time_changed, time_changed,
)?; )?;
} }
} else {
update_subscriptions_for_single_device(
conn,
device_id,
&add,
&remove,
time_changed,
)?;
}
Ok::<_, diesel::result::Error>(()) Ok::<_, DbError>(())
})?; })
})()
Ok(()) .map_err(AuthErr::from)
} }
fn subscription_updates_for_device( fn subscription_updates_for_device(
@ -325,36 +336,39 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
device_id: &str, device_id: &str,
since: chrono::DateTime<chrono::Utc>, since: chrono::DateTime<chrono::Utc>,
) -> Result<(Vec<gpodder::Subscription>, Vec<gpodder::Subscription>), gpodder::AuthErr> { ) -> Result<(Vec<gpodder::Subscription>, Vec<gpodder::Subscription>), 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 let query = device_subscriptions::table
.inner_join(devices::table) .inner_join(devices::table)
.filter( .filter(
devices::user_id devices::user_id
.eq(user.id) .eq(user.id)
.and(devices::device_id.eq(device_id)) .and(devices::device_id.eq(device_id))
.and(device_subscriptions::time_changed.ge(since)), .and(device_subscriptions::time_changed.ge(since)),
) )
.select(db::DeviceSubscription::as_select()); .select(DeviceSubscription::as_select());
for sub in query.load_iter(&mut self.pool.get()?)? { for sub in query.load_iter(&mut self.pool.get()?)? {
let sub = sub?; let sub = sub?;
if sub.deleted { if sub.deleted {
removed.push(gpodder::Subscription { removed.push(gpodder::Subscription {
url: sub.podcast_url, url: sub.podcast_url,
time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(), time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(),
}); });
} else { } else {
added.push(gpodder::Subscription { added.push(gpodder::Subscription {
url: sub.podcast_url, url: sub.podcast_url,
time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(), time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(),
}); });
}
} }
}
Ok((added, removed)) Ok::<_, DbError>((added, removed))
})()
.map_err(AuthErr::from)
} }
} }

View File

@ -0,0 +1,49 @@
mod common;
use chrono::SubsecRound;
use gpodder::{AuthStore, Session};
use gpodder_sqlite::SqliteRepository;
#[test]
fn test_create_user() {
let store = SqliteRepository::in_memory().unwrap();
let user = store.get_user("test1");
assert!(user.is_ok());
assert_eq!(user.unwrap(), None);
let new_user = store.insert_user("test1", "dummyhash");
assert!(new_user.is_ok());
let new_user = new_user.unwrap();
assert_eq!(new_user.username, "test1");
assert_eq!(new_user.password_hash, "dummyhash");
let user = store.get_user("test1");
assert!(user.is_ok());
assert_eq!(user.unwrap(), Some(new_user));
}
#[test]
fn test_create_session() {
let (store, users) = common::setup();
let session = store.get_session(123).expect("operation shouldn't fail");
assert_eq!(session, None);
let new_session = Session {
id: 123,
//
last_seen: chrono::Utc::now().trunc_subsecs(0),
user: users[0].clone(),
};
store
.insert_session(&new_session)
.expect("insert session shouldn't fail");
let session = store.get_session(123).expect("operation shouldn't fail");
assert_eq!(session, Some(new_session));
}

View File

@ -0,0 +1,16 @@
use gpodder::{AuthStore, User};
use gpodder_sqlite::SqliteRepository;
pub fn setup() -> (SqliteRepository, Vec<User>) {
let store = SqliteRepository::in_memory().unwrap();
let mut users = Vec::new();
for i in 0..4 {
let username = format!("test{}", i + 1);
let password_hash = format!("dummyhash{}", i + 1);
users.push(store.insert_user(&username, &password_hash).unwrap());
}
(store, users)
}

View File

@ -1,7 +1,5 @@
use clap::Subcommand; use clap::Subcommand;
use crate::db;
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum Command { pub enum Command {
/// Add devices of a specific user to the same sync group /// Add devices of a specific user to the same sync group
@ -15,9 +13,10 @@ pub enum Command {
impl Command { impl Command {
pub fn run(&self, config: &crate::config::Config) -> u8 { pub fn run(&self, config: &crate::config::Config) -> u8 {
let pool = db::initialize_db(config.data_dir.join(crate::DB_FILENAME), true).unwrap(); let store =
let repo = db::SqliteRepository::from(pool); gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME))
let store = crate::gpodder::GpodderRepository::new(repo); .unwrap();
let store = gpodder::GpodderRepository::new(store);
match self { match self {
Self::Sync { username, devices } => { Self::Sync { username, devices } => {

View File

@ -1,4 +1,4 @@
mod db; // mod db;
mod gpo; mod gpo;
mod serve; mod serve;
@ -48,9 +48,8 @@ pub struct ClapConfig {
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum Command { pub enum Command {
Serve, Serve,
#[command(subcommand)] // #[command(subcommand)]
Db(db::DbCommand), // Db(db::DbCommand),
/// Perform operations on the database through the Gpodder abstraction, allowing operations /// Perform operations on the database through the Gpodder abstraction, allowing operations
/// identical to the ones performed by the API. /// identical to the ones performed by the API.
#[command(subcommand)] #[command(subcommand)]
@ -80,7 +79,7 @@ impl Cli {
match &self.cmd { match &self.cmd {
Command::Serve => serve::serve(&config), Command::Serve => serve::serve(&config),
Command::Db(cmd) => cmd.run(&config), // Command::Db(cmd) => cmd.run(&config),
Command::Gpo(cmd) => cmd.run(&config), Command::Gpo(cmd) => cmd.run(&config),
} }
} }

View File

@ -1,18 +1,18 @@
use std::time::Duration; use std::time::Duration;
use crate::{db, server}; use crate::server;
pub fn serve(config: &crate::config::Config) -> u8 { pub fn serve(config: &crate::config::Config) -> u8 {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
tracing::info!("Initializing database and running migrations"); tracing::info!("Initializing database and running migrations");
let pool = db::initialize_db(config.data_dir.join(crate::DB_FILENAME), true).unwrap(); let store =
let repo = db::SqliteRepository::from(pool); gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME))
.unwrap();
let store = gpodder::GpodderRepository::new(store);
let ctx = server::Context { let ctx = server::Context { store };
store: crate::gpodder::GpodderRepository::new(repo),
};
let app = server::app(ctx.clone()); let app = server::app(ctx.clone());
let rt = tokio::runtime::Builder::new_multi_thread() let rt = tokio::runtime::Builder::new_multi_thread()

View File

@ -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<Self> {
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<Option<super::user::User>> {
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<Option<super::user::User>> {
Self::user_from_id(pool, self.id)
}
pub fn by_id(pool: &DbPool, id: i64) -> DbResult<Option<Self>> {
Ok(sessions::dsl::sessions
.find(id)
.get_result(&mut pool.get()?)
.optional()?)
}
pub fn remove(self, pool: &DbPool) -> DbResult<bool> {
Self::remove_by_id(pool, self.id)
}
pub fn remove_by_id(pool: &DbPool, id: i64) -> DbResult<bool> {
Ok(
diesel::delete(sessions::dsl::sessions.filter(sessions::id.eq(id)))
.execute(&mut pool.get()?)?
> 0,
)
}
}

View File

@ -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<str>) -> 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<User> {
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<str>) -> DbResult<Option<Self>> {
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<str>) -> bool {
let password_hash = PasswordHash::new(&self.password_hash).unwrap();
Argon2::default()
.verify_password(password.as_ref().as_bytes(), &password_hash)
.is_ok()
}
}

View File

@ -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<db::DeviceType> 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<gpodder::DeviceType> 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<Vec<gpodder::Device>, 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<i64, gpodder::AuthErr> {
let conn = &mut self.pool.get()?;
Ok(conn.transaction(|conn| {
let devices: Vec<(i64, Option<i64>)> = 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<i64> = 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::<i64>))
.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<Utc>,
) -> Result<(), gpodder::AuthErr> {
let time_changed = time_changed.timestamp();
let conn = &mut self.pool.get()?;
conn.transaction(|conn| {
let device_ids: Vec<i64> = 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::<Result<HashSet<String>, _>>()?;
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<String>, Vec<Vec<String>>), 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<i64>), _>(conn)?;
let mut cur_group = &mut not_synchronized;
let mut cur_group_id: Option<i64> = 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))
}
}

View File

@ -1,170 +0,0 @@
use chrono::{DateTime, Utc};
use diesel::prelude::*;
use super::SqliteRepository;
use crate::{
db::{self, schema::*},
gpodder,
};
impl From<gpodder::EpisodeAction> 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<String>, db::EpisodeAction)> for gpodder::EpisodeAction {
fn from((device_id, db_action): (Option<String>, 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<gpodder::EpisodeAction>,
time_changed: DateTime<Utc>,
) -> 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<String> 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<DateTime<Utc>>,
podcast: Option<String>,
device: Option<String>,
aggregated: bool,
) -> Result<Vec<gpodder::EpisodeAction>, 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<String>, 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)
}
}

View File

@ -1,17 +0,0 @@
mod auth;
mod device;
mod episode_action;
mod subscription;
use super::DbPool;
#[derive(Clone)]
pub struct SqliteRepository {
pool: DbPool,
}
impl From<DbPool> for SqliteRepository {
fn from(value: DbPool) -> Self {
Self { pool: value }
}
}

View File

@ -1,7 +1,5 @@
mod cli; mod cli;
mod config; mod config;
mod db;
mod gpodder;
mod server; mod server;
use clap::Parser; use clap::Parser;

View File

@ -2,13 +2,13 @@ use std::fmt;
use axum::{http::StatusCode, response::IntoResponse}; use axum::{http::StatusCode, response::IntoResponse};
use crate::{db, ErrorExt}; use crate::ErrorExt;
pub type AppResult<T> = Result<T, AppError>; pub type AppResult<T> = Result<T, AppError>;
#[derive(Debug)] #[derive(Debug)]
pub enum AppError { pub enum AppError {
Db(db::DbError), // Db(db::DbError),
IO(std::io::Error), IO(std::io::Error),
Other(Box<dyn std::error::Error + 'static + Send + Sync>), Other(Box<dyn std::error::Error + 'static + Send + Sync>),
BadRequest, BadRequest,
@ -19,7 +19,7 @@ pub enum AppError {
impl fmt::Display for AppError { impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::Db(_) => write!(f, "database error"), // Self::Db(_) => write!(f, "database error"),
Self::IO(_) => write!(f, "io error"), Self::IO(_) => write!(f, "io error"),
Self::Other(_) => write!(f, "other error"), Self::Other(_) => write!(f, "other error"),
Self::BadRequest => write!(f, "bad request"), Self::BadRequest => write!(f, "bad request"),
@ -32,7 +32,7 @@ impl fmt::Display for AppError {
impl std::error::Error for AppError { impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self { match self {
Self::Db(err) => Some(err), // Self::Db(err) => Some(err),
Self::IO(err) => Some(err), Self::IO(err) => Some(err),
Self::Other(err) => Some(err.as_ref()), Self::Other(err) => Some(err.as_ref()),
Self::NotFound | Self::Unauthorized | Self::BadRequest => None, Self::NotFound | Self::Unauthorized | Self::BadRequest => None,
@ -40,12 +40,6 @@ impl std::error::Error for AppError {
} }
} }
impl From<db::DbError> for AppError {
fn from(value: db::DbError) -> Self {
Self::Db(value)
}
}
impl From<std::io::Error> for AppError { impl From<std::io::Error> for AppError {
fn from(value: std::io::Error) -> Self { fn from(value: std::io::Error) -> Self {
Self::IO(value) Self::IO(value)

View File

@ -10,13 +10,10 @@ use axum_extra::{
}; };
use cookie::time::Duration; use cookie::time::Duration;
use crate::{ use crate::server::{
gpodder, error::{AppError, AppResult},
server::{ gpodder::SESSION_ID_COOKIE,
error::{AppError, AppResult}, Context,
gpodder::SESSION_ID_COOKIE,
Context,
},
}; };
pub fn router() -> Router<Context> { pub fn router() -> Router<Context> {

View File

@ -5,17 +5,14 @@ use axum::{
Extension, Json, Router, Extension, Json, Router,
}; };
use crate::{ use crate::server::{
gpodder, error::{AppError, AppResult},
server::{ gpodder::{
error::{AppError, AppResult}, auth_middleware,
gpodder::{ format::{Format, StringWithFormat},
auth_middleware, models,
format::{Format, StringWithFormat},
models,
},
Context,
}, },
Context,
}; };
pub fn router(ctx: Context) -> Router<Context> { pub fn router(ctx: Context) -> Router<Context> {

View File

@ -7,18 +7,15 @@ use axum::{
use chrono::DateTime; use chrono::DateTime;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::server::{
gpodder, error::{AppError, AppResult},
server::{ gpodder::{
error::{AppError, AppResult}, auth_middleware,
gpodder::{ format::{Format, StringWithFormat},
auth_middleware, models,
format::{Format, StringWithFormat}, models::UpdatedUrlsResponse,
models,
models::UpdatedUrlsResponse,
},
Context,
}, },
Context,
}; };
pub fn router(ctx: Context) -> Router<Context> { pub fn router(ctx: Context) -> Router<Context> {

View File

@ -6,17 +6,14 @@ use axum::{
}; };
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::server::{
gpodder, error::{AppError, AppResult},
server::{ gpodder::{
error::{AppError, AppResult}, auth_middleware,
gpodder::{ format::{Format, StringWithFormat},
auth_middleware, models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse},
format::{Format, StringWithFormat},
models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse},
},
Context,
}, },
Context,
}; };
pub fn router(ctx: Context) -> Router<Context> { pub fn router(ctx: Context) -> Router<Context> {

View File

@ -5,17 +5,14 @@ use axum::{
Extension, Json, Router, Extension, Json, Router,
}; };
use crate::{ use crate::server::{
gpodder, error::{AppError, AppResult},
server::{ gpodder::{
error::{AppError, AppResult}, auth_middleware,
gpodder::{ format::{Format, StringWithFormat},
auth_middleware, models::{SyncStatus, SyncStatusDelta},
format::{Format, StringWithFormat},
models::{SyncStatus, SyncStatusDelta},
},
Context,
}, },
Context,
}; };
pub fn router(ctx: Context) -> Router<Context> { pub fn router(ctx: Context) -> Router<Context> {

View File

@ -17,7 +17,7 @@ use axum_extra::{
}; };
use tower_http::set_header::SetResponseHeaderLayer; use tower_http::set_header::SetResponseHeaderLayer;
use crate::{gpodder, server::error::AppError}; use crate::server::error::AppError;
use super::Context; use super::Context;

View File

@ -1,8 +1,6 @@
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::gpodder;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct SubscriptionDelta { pub struct SubscriptionDelta {
pub add: Vec<String>, pub add: Vec<String>,

View File

@ -5,13 +5,10 @@ use axum::{
Extension, Json, Router, Extension, Json, Router,
}; };
use crate::{ use crate::server::{
gpodder, error::{AppError, AppResult},
server::{ gpodder::{auth_middleware, format::StringWithFormat},
error::{AppError, AppResult}, Context,
gpodder::{auth_middleware, format::StringWithFormat},
Context,
},
}; };
pub fn router(ctx: Context) -> Router<Context> { pub fn router(ctx: Context) -> Router<Context> {

View File

@ -1,18 +1,27 @@
mod error; mod error;
mod gpodder; 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; use tower_http::trace::TraceLayer;
#[derive(Clone)] #[derive(Clone)]
pub struct Context { pub struct Context {
pub store: crate::gpodder::GpodderRepository, pub store: ::gpodder::GpodderRepository,
} }
pub fn app(ctx: Context) -> Router { pub fn app(ctx: Context) -> Router {
Router::new() Router::new()
.merge(gpodder::router(ctx.clone())) .merge(gpodder::router(ctx.clone()))
.layer(axum::middleware::from_fn(header_logger)) .layer(axum::middleware::from_fn(header_logger))
.layer(axum::middleware::from_fn(body_logger))
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())
.with_state(ctx) .with_state(ctx)
} }
@ -26,3 +35,41 @@ async fn header_logger(request: Request, next: Next) -> Response {
res 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))
}