Compare commits
5 Commits
86687a7b96
...
0bb0c5657a
Author | SHA1 | Date |
---|---|---|
|
0bb0c5657a | |
|
705b347775 | |
|
b44a47fefd | |
|
2a8917f21d | |
|
0cfcd90eba |
|
@ -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",
|
||||||
|
|
14
Cargo.toml
14
Cargo.toml
|
@ -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"] }
|
||||||
|
|
|
@ -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",
|
||||||
|
]
|
|
@ -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"
|
|
@ -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,
|
||||||
|
};
|
|
@ -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>,
|
|
@ -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,
|
|
@ -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
|
|
@ -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",
|
||||||
|
]
|
|
@ -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"] }
|
|
@ -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)
|
||||||
|
}
|
|
@ -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 {
|
|
@ -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 {
|
|
@ -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,
|
|
@ -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,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
}
|
|
@ -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)]
|
|
@ -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()
|
||||||
|
// }
|
||||||
|
// }
|
|
@ -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)?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 })
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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));
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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 } => {
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue