refactor: split gpodder repository and the sqlite data store implementation into separate crates
The complete separation of concerns via the gpodder repository allows us to cleanly separate the server from the gpodder specification. This paves the way for a later Postgres implementation of the data store.main
parent
86687a7b96
commit
0cfcd90eba
|
@ -600,6 +600,28 @@ version = "0.31.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "gpodder"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"chrono",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpodder_sqlite"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"diesel",
|
||||
"diesel_migrations",
|
||||
"gpodder",
|
||||
"libsqlite3-sys",
|
||||
"rand",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
|
@ -659,12 +681,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
|
||||
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
|
@ -926,16 +948,15 @@ checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
|
|||
name = "otter"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"chrono",
|
||||
"clap",
|
||||
"cookie",
|
||||
"diesel",
|
||||
"diesel_migrations",
|
||||
"figment",
|
||||
"libsqlite3-sys",
|
||||
"gpodder",
|
||||
"gpodder_sqlite",
|
||||
"http-body-util",
|
||||
"rand",
|
||||
"serde",
|
||||
"tokio",
|
||||
|
|
14
Cargo.toml
14
Cargo.toml
|
@ -1,19 +1,25 @@
|
|||
[workspace]
|
||||
members = [
|
||||
'gpodder',
|
||||
'gpodder_sqlite'
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "otter"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
argon2 = "0.5.3"
|
||||
gpodder = { path = "./gpodder" }
|
||||
gpodder_sqlite = { path = "./gpodder_sqlite" }
|
||||
|
||||
axum = { version = "0.8.1", features = ["macros"] }
|
||||
axum-extra = { version = "0.10", features = ["cookie", "typed-header"] }
|
||||
chrono = { version = "0.4.39", features = ["serde"] }
|
||||
clap = { version = "4.5.30", features = ["derive", "env"] }
|
||||
cookie = "0.18.1"
|
||||
diesel = { version = "2.2.7", features = ["r2d2", "sqlite", "returning_clauses_for_sqlite_3_35"] }
|
||||
diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
|
||||
figment = { version = "0.10.19", features = ["env", "toml"] }
|
||||
libsqlite3-sys = { version = "0.31.0", features = ["bundled"] }
|
||||
http-body-util = "0.1.3"
|
||||
rand = "0.8.5"
|
||||
serde = { version = "1.0.218", features = ["derive"] }
|
||||
tokio = { version = "1.43.0", features = ["full"] }
|
||||
|
|
|
@ -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"
|
|
@ -5,6 +5,7 @@ use std::fmt::Display;
|
|||
|
||||
use chrono::{DateTime, Utc};
|
||||
pub use models::*;
|
||||
|
||||
pub use repository::GpodderRepository;
|
||||
|
||||
#[derive(Debug)]
|
|
@ -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 schema;
|
||||
|
||||
|
@ -6,13 +6,6 @@ use diesel::connection::InstrumentationEvent;
|
|||
use diesel::r2d2::CustomizeConnection;
|
||||
use diesel::Connection;
|
||||
|
||||
pub use models::device::{Device, DeviceType, NewDevice};
|
||||
pub use models::device_subscription::{DeviceSubscription, NewDeviceSubscription};
|
||||
pub use models::episode_action::{ActionType, EpisodeAction, NewEpisodeAction};
|
||||
pub use models::session::Session;
|
||||
pub use models::sync_group::SyncGroup;
|
||||
pub use models::user::{NewUser, User};
|
||||
|
||||
pub use repository::SqliteRepository;
|
||||
|
||||
use diesel::{
|
||||
|
@ -64,6 +57,15 @@ impl From<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)]
|
||||
pub struct AddQueryDebugLogs;
|
||||
|
|
@ -8,11 +8,10 @@ use diesel::{
|
|||
sql_types::Text,
|
||||
sqlite::{Sqlite, SqliteValue},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::db::{schema::*, DbPool, DbResult};
|
||||
use crate::schema::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)]
|
||||
#[derive(Clone, Queryable, Selectable)]
|
||||
#[diesel(table_name = devices)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct Device {
|
||||
|
@ -24,7 +23,7 @@ pub struct Device {
|
|||
pub sync_group_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Insertable)]
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = devices)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct NewDevice {
|
||||
|
@ -34,9 +33,8 @@ pub struct NewDevice {
|
|||
pub type_: DeviceType,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromSqlRow, Debug, AsExpression, Clone)]
|
||||
#[derive(FromSqlRow, Debug, AsExpression, Clone)]
|
||||
#[diesel(sql_type = Text)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DeviceType {
|
||||
Desktop,
|
||||
Laptop,
|
||||
|
@ -46,13 +44,6 @@ pub enum DeviceType {
|
|||
}
|
||||
|
||||
impl Device {
|
||||
pub fn for_user(pool: &DbPool, user_id: i64) -> DbResult<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(
|
||||
conn: &mut SqliteConnection,
|
||||
user_id: i64,
|
||||
|
@ -82,22 +73,6 @@ impl Device {
|
|||
)
|
||||
.get_result(conn)
|
||||
}
|
||||
|
||||
pub fn update(&self, pool: &DbPool) -> DbResult<()> {
|
||||
Ok(diesel::update(
|
||||
devices::table.filter(
|
||||
devices::user_id
|
||||
.eq(self.user_id)
|
||||
.and(devices::device_id.eq(&self.device_id)),
|
||||
),
|
||||
)
|
||||
.set((
|
||||
devices::caption.eq(&self.caption),
|
||||
devices::type_.eq(&self.type_),
|
||||
))
|
||||
.execute(&mut pool.get()?)
|
||||
.map(|_| ())?)
|
||||
}
|
||||
}
|
||||
|
||||
impl NewDevice {
|
||||
|
@ -109,13 +84,6 @@ impl NewDevice {
|
|||
type_,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(self, pool: &DbPool) -> DbResult<Device> {
|
||||
Ok(diesel::insert_into(devices::table)
|
||||
.values(&self)
|
||||
.returning(Device::as_returning())
|
||||
.get_result(&mut pool.get()?)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DeviceType {
|
|
@ -1,9 +1,8 @@
|
|||
use diesel::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::db::schema::*;
|
||||
use crate::schema::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)]
|
||||
#[derive(Clone, Queryable, Selectable)]
|
||||
#[diesel(table_name = device_subscriptions)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct DeviceSubscription {
|
||||
|
@ -14,7 +13,7 @@ pub struct DeviceSubscription {
|
|||
pub deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Insertable)]
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = device_subscriptions)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct NewDeviceSubscription {
|
|
@ -9,11 +9,10 @@ use diesel::{
|
|||
sqlite::{Sqlite, SqliteValue},
|
||||
Selectable,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::db::schema::*;
|
||||
use crate::schema::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)]
|
||||
#[derive(Clone, Queryable, Selectable)]
|
||||
#[diesel(table_name = episode_actions)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct EpisodeAction {
|
||||
|
@ -30,7 +29,7 @@ pub struct EpisodeAction {
|
|||
pub total: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Insertable)]
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = episode_actions)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct NewEpisodeAction {
|
||||
|
@ -46,9 +45,8 @@ pub struct NewEpisodeAction {
|
|||
pub total: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromSqlRow, Debug, AsExpression, Clone)]
|
||||
#[derive(FromSqlRow, Debug, AsExpression, Clone)]
|
||||
#[diesel(sql_type = Text)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ActionType {
|
||||
New,
|
||||
Download,
|
|
@ -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::*,
|
||||
};
|
||||
|
||||
use crate::db::schema::*;
|
||||
use crate::schema::*;
|
||||
|
||||
#[derive(Queryable, Selectable)]
|
||||
#[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 {
|
||||
pub username: String,
|
||||
pub password_hash: String,
|
||||
}
|
||||
|
||||
// impl NewUser {
|
||||
// pub fn new(username: String, password: String) -> Self {
|
||||
// Self {
|
||||
// username,
|
||||
// password_hash: hash_password(&password),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl User {
|
||||
// pub fn by_username(pool: &DbPool, username: impl AsRef<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,16 @@
|
|||
use chrono::DateTime;
|
||||
use diesel::prelude::*;
|
||||
use gpodder::AuthErr;
|
||||
|
||||
use super::SqliteRepository;
|
||||
use crate::{
|
||||
db::{self, schema::*},
|
||||
gpodder::{self, AuthErr},
|
||||
models::{session::Session, user::User},
|
||||
schema::*,
|
||||
DbError,
|
||||
};
|
||||
|
||||
impl From<diesel::r2d2::PoolError> for gpodder::AuthErr {
|
||||
fn from(value: diesel::r2d2::PoolError) -> 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 {
|
||||
impl From<User> for gpodder::User {
|
||||
fn from(value: User) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
username: value.username,
|
||||
|
@ -32,10 +22,11 @@ impl From<db::User> for gpodder::User {
|
|||
impl gpodder::AuthStore for SqliteRepository {
|
||||
fn get_user(&self, username: &str) -> Result<Option<gpodder::models::User>, AuthErr> {
|
||||
Ok(users::table
|
||||
.select(db::User::as_select())
|
||||
.select(User::as_select())
|
||||
.filter(users::username.eq(username))
|
||||
.first(&mut self.pool.get()?)
|
||||
.optional()?
|
||||
.first(&mut self.pool.get().map_err(DbError::from)?)
|
||||
.optional()
|
||||
.map_err(DbError::from)?
|
||||
.map(gpodder::User::from))
|
||||
}
|
||||
|
||||
|
@ -43,35 +34,37 @@ impl gpodder::AuthStore for SqliteRepository {
|
|||
match sessions::table
|
||||
.inner_join(users::table)
|
||||
.filter(sessions::id.eq(session_id))
|
||||
.select((db::Session::as_select(), db::User::as_select()))
|
||||
.get_result(&mut self.pool.get()?)
|
||||
.select((Session::as_select(), User::as_select()))
|
||||
.get_result(&mut self.pool.get().map_err(DbError::from)?)
|
||||
{
|
||||
Ok((session, user)) => Ok(Some(gpodder::Session {
|
||||
id: session.id,
|
||||
last_seen: DateTime::from_timestamp(session.last_seen, 0).unwrap(),
|
||||
user: user.into(),
|
||||
})),
|
||||
Err(err) => Err(AuthErr::from(err)),
|
||||
Err(err) => Err(DbError::from(err).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_session(&self, session_id: i64) -> Result<(), AuthErr> {
|
||||
Ok(
|
||||
diesel::delete(sessions::table.filter(sessions::id.eq(session_id)))
|
||||
.execute(&mut self.pool.get()?)
|
||||
.map(|_| ())?,
|
||||
.execute(&mut self.pool.get().map_err(DbError::from)?)
|
||||
.map(|_| ())
|
||||
.map_err(DbError::from)?,
|
||||
)
|
||||
}
|
||||
|
||||
fn insert_session(&self, session: &gpodder::Session) -> Result<(), AuthErr> {
|
||||
Ok(db::Session {
|
||||
Ok(Session {
|
||||
id: session.id,
|
||||
user_id: session.user.id,
|
||||
last_seen: session.last_seen.timestamp(),
|
||||
}
|
||||
.insert_into(sessions::table)
|
||||
.execute(&mut self.pool.get()?)
|
||||
.map(|_| ())?)
|
||||
.execute(&mut self.pool.get().map_err(DbError::from)?)
|
||||
.map(|_| ())
|
||||
.map_err(DbError::from)?)
|
||||
}
|
||||
|
||||
fn refresh_session(
|
||||
|
@ -81,7 +74,8 @@ impl gpodder::AuthStore for SqliteRepository {
|
|||
) -> Result<(), AuthErr> {
|
||||
if diesel::update(sessions::table.filter(sessions::id.eq(session.id)))
|
||||
.set(sessions::last_seen.eq(timestamp.timestamp()))
|
||||
.execute(&mut self.pool.get()?)?
|
||||
.execute(&mut self.pool.get().map_err(DbError::from)?)
|
||||
.map_err(DbError::from)?
|
||||
== 0
|
||||
{
|
||||
Err(AuthErr::UnknownSession)
|
||||
|
@ -95,7 +89,8 @@ impl gpodder::AuthStore for SqliteRepository {
|
|||
|
||||
Ok(
|
||||
diesel::delete(sessions::table.filter(sessions::last_seen.lt(min_last_seen)))
|
||||
.execute(&mut self.pool.get()?)?,
|
||||
.execute(&mut self.pool.get().map_err(DbError::from)?)
|
||||
.map_err(DbError::from)?,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@ mod device;
|
|||
mod episode_action;
|
||||
mod subscription;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use super::DbPool;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -15,3 +17,11 @@ impl From<DbPool> for SqliteRepository {
|
|||
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 })
|
||||
}
|
||||
}
|
|
@ -2,22 +2,15 @@ use std::collections::HashSet;
|
|||
|
||||
use chrono::DateTime;
|
||||
use diesel::prelude::*;
|
||||
use gpodder::AuthErr;
|
||||
|
||||
use super::SqliteRepository;
|
||||
use crate::{
|
||||
db::{self, schema::*},
|
||||
gpodder,
|
||||
models::device_subscription::{DeviceSubscription, NewDeviceSubscription},
|
||||
schema::*,
|
||||
DbError,
|
||||
};
|
||||
|
||||
impl From<(String, i64)> for gpodder::Subscription {
|
||||
fn from((url, ts): (String, i64)) -> Self {
|
||||
Self {
|
||||
url,
|
||||
time_changed: DateTime::from_timestamp(ts, 0).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_subscriptions_for_single_device(
|
||||
conn: &mut SqliteConnection,
|
||||
device_id: i64,
|
||||
|
@ -80,7 +73,7 @@ fn set_subscriptions_for_single_device(
|
|||
.values(
|
||||
urls_to_insert
|
||||
.into_iter()
|
||||
.map(|url| db::NewDeviceSubscription {
|
||||
.map(|url| NewDeviceSubscription {
|
||||
device_id,
|
||||
podcast_url: url.to_string(),
|
||||
deleted: false,
|
||||
|
@ -105,7 +98,7 @@ pub fn insert_subscriptions_for_single_device<'a>(
|
|||
diesel::insert_into(device_subscriptions::table)
|
||||
.values(
|
||||
urls.into_iter()
|
||||
.map(|url| db::NewDeviceSubscription {
|
||||
.map(|url| NewDeviceSubscription {
|
||||
device_id,
|
||||
podcast_url: url.to_string(),
|
||||
deleted: false,
|
||||
|
@ -184,18 +177,26 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
|
|||
&self,
|
||||
user: &gpodder::User,
|
||||
) -> Result<Vec<gpodder::Subscription>, gpodder::AuthErr> {
|
||||
Ok(device_subscriptions::table
|
||||
.inner_join(devices::table)
|
||||
.filter(devices::user_id.eq(user.id))
|
||||
.select((
|
||||
device_subscriptions::podcast_url,
|
||||
device_subscriptions::time_changed,
|
||||
))
|
||||
.distinct()
|
||||
.get_results::<(String, i64)>(&mut self.pool.get()?)?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect())
|
||||
(|| {
|
||||
Ok::<_, DbError>(
|
||||
device_subscriptions::table
|
||||
.inner_join(devices::table)
|
||||
.filter(devices::user_id.eq(user.id))
|
||||
.select((
|
||||
device_subscriptions::podcast_url,
|
||||
device_subscriptions::time_changed,
|
||||
))
|
||||
.distinct()
|
||||
.get_results::<(String, i64)>(&mut self.pool.get()?)?
|
||||
.into_iter()
|
||||
.map(|(url, ts)| gpodder::Subscription {
|
||||
url,
|
||||
time_changed: DateTime::from_timestamp(ts, 0).unwrap(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})()
|
||||
.map_err(AuthErr::from)
|
||||
}
|
||||
|
||||
fn subscriptions_for_device(
|
||||
|
@ -203,21 +204,29 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
|
|||
user: &gpodder::User,
|
||||
device_id: &str,
|
||||
) -> Result<Vec<gpodder::Subscription>, gpodder::AuthErr> {
|
||||
Ok(device_subscriptions::table
|
||||
.inner_join(devices::table)
|
||||
.filter(
|
||||
devices::user_id
|
||||
.eq(user.id)
|
||||
.and(devices::device_id.eq(device_id)),
|
||||
(|| {
|
||||
Ok::<_, DbError>(
|
||||
device_subscriptions::table
|
||||
.inner_join(devices::table)
|
||||
.filter(
|
||||
devices::user_id
|
||||
.eq(user.id)
|
||||
.and(devices::device_id.eq(device_id)),
|
||||
)
|
||||
.select((
|
||||
device_subscriptions::podcast_url,
|
||||
device_subscriptions::time_changed,
|
||||
))
|
||||
.get_results::<(String, i64)>(&mut self.pool.get()?)?
|
||||
.into_iter()
|
||||
.map(|(url, ts)| gpodder::Subscription {
|
||||
url,
|
||||
time_changed: DateTime::from_timestamp(ts, 0).unwrap(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.select((
|
||||
device_subscriptions::podcast_url,
|
||||
device_subscriptions::time_changed,
|
||||
))
|
||||
.get_results::<(String, i64)>(&mut self.pool.get()?)?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect())
|
||||
})()
|
||||
.map_err(AuthErr::from)
|
||||
}
|
||||
|
||||
fn set_subscriptions_for_device(
|
||||
|
@ -227,38 +236,39 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
|
|||
urls: Vec<String>,
|
||||
time_changed: chrono::DateTime<chrono::Utc>,
|
||||
) -> 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| {
|
||||
let (device_id, group_id) = devices::table
|
||||
.select((devices::id, devices::sync_group_id))
|
||||
.filter(
|
||||
devices::user_id
|
||||
.eq(user.id)
|
||||
.and(devices::device_id.eq(device_id)),
|
||||
)
|
||||
.get_result::<(i64, Option<i64>)>(conn)?;
|
||||
self.pool.get()?.transaction(|conn| {
|
||||
let (device_id, group_id) = devices::table
|
||||
.select((devices::id, devices::sync_group_id))
|
||||
.filter(
|
||||
devices::user_id
|
||||
.eq(user.id)
|
||||
.and(devices::device_id.eq(device_id)),
|
||||
)
|
||||
.get_result::<(i64, Option<i64>)>(conn)?;
|
||||
|
||||
// If the device is part of a sync group, we need to perform the update on every device
|
||||
// in the group
|
||||
if let Some(group_id) = group_id {
|
||||
let device_ids: Vec<i64> = devices::table
|
||||
.filter(devices::sync_group_id.eq(group_id))
|
||||
.select(devices::id)
|
||||
.get_results(conn)?;
|
||||
// If the device is part of a sync group, we need to perform the update on every device
|
||||
// in the group
|
||||
if let Some(group_id) = group_id {
|
||||
let device_ids: Vec<i64> = devices::table
|
||||
.filter(devices::sync_group_id.eq(group_id))
|
||||
.select(devices::id)
|
||||
.get_results(conn)?;
|
||||
|
||||
for device_id in device_ids {
|
||||
for device_id in device_ids {
|
||||
set_subscriptions_for_single_device(conn, device_id, &urls, time_changed)?;
|
||||
}
|
||||
} else {
|
||||
set_subscriptions_for_single_device(conn, device_id, &urls, time_changed)?;
|
||||
}
|
||||
} else {
|
||||
set_subscriptions_for_single_device(conn, device_id, &urls, time_changed)?;
|
||||
}
|
||||
|
||||
Ok::<_, diesel::result::Error>(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
Ok::<_, DbError>(())
|
||||
})
|
||||
})()
|
||||
.map_err(AuthErr::from)
|
||||
}
|
||||
|
||||
fn update_subscriptions_for_device(
|
||||
|
@ -269,32 +279,42 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
|
|||
remove: Vec<String>,
|
||||
time_changed: chrono::DateTime<chrono::Utc>,
|
||||
) -> Result<(), gpodder::AuthErr> {
|
||||
let time_changed = time_changed.timestamp();
|
||||
(|| {
|
||||
let time_changed = time_changed.timestamp();
|
||||
|
||||
// TODO URLs that are in both the added and removed lists will currently get "re-added",
|
||||
// meaning their change timestamp will be updated even though they haven't really changed.
|
||||
let add: HashSet<_> = add.into_iter().collect();
|
||||
let remove: HashSet<_> = remove.into_iter().collect();
|
||||
// TODO URLs that are in both the added and removed lists will currently get "re-added",
|
||||
// meaning their change timestamp will be updated even though they haven't really changed.
|
||||
let add: HashSet<_> = add.into_iter().collect();
|
||||
let remove: HashSet<_> = remove.into_iter().collect();
|
||||
|
||||
self.pool.get()?.transaction(|conn| {
|
||||
let (device_id, group_id) = devices::table
|
||||
.select((devices::id, devices::sync_group_id))
|
||||
.filter(
|
||||
devices::user_id
|
||||
.eq(user.id)
|
||||
.and(devices::device_id.eq(device_id)),
|
||||
)
|
||||
.get_result::<(i64, Option<i64>)>(conn)?;
|
||||
self.pool.get()?.transaction(|conn| {
|
||||
let (device_id, group_id) = devices::table
|
||||
.select((devices::id, devices::sync_group_id))
|
||||
.filter(
|
||||
devices::user_id
|
||||
.eq(user.id)
|
||||
.and(devices::device_id.eq(device_id)),
|
||||
)
|
||||
.get_result::<(i64, Option<i64>)>(conn)?;
|
||||
|
||||
// If the device is part of a sync group, we need to perform the update on every device
|
||||
// in the group
|
||||
if let Some(group_id) = group_id {
|
||||
let device_ids: Vec<i64> = devices::table
|
||||
.filter(devices::sync_group_id.eq(group_id))
|
||||
.select(devices::id)
|
||||
.get_results(conn)?;
|
||||
// If the device is part of a sync group, we need to perform the update on every device
|
||||
// in the group
|
||||
if let Some(group_id) = group_id {
|
||||
let device_ids: Vec<i64> = devices::table
|
||||
.filter(devices::sync_group_id.eq(group_id))
|
||||
.select(devices::id)
|
||||
.get_results(conn)?;
|
||||
|
||||
for device_id in device_ids {
|
||||
for device_id in device_ids {
|
||||
update_subscriptions_for_single_device(
|
||||
conn,
|
||||
device_id,
|
||||
&add,
|
||||
&remove,
|
||||
time_changed,
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
update_subscriptions_for_single_device(
|
||||
conn,
|
||||
device_id,
|
||||
|
@ -303,20 +323,11 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
|
|||
time_changed,
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
update_subscriptions_for_single_device(
|
||||
conn,
|
||||
device_id,
|
||||
&add,
|
||||
&remove,
|
||||
time_changed,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok::<_, diesel::result::Error>(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
Ok::<_, DbError>(())
|
||||
})
|
||||
})()
|
||||
.map_err(AuthErr::from)
|
||||
}
|
||||
|
||||
fn subscription_updates_for_device(
|
||||
|
@ -325,36 +336,39 @@ impl gpodder::SubscriptionRepository for SqliteRepository {
|
|||
device_id: &str,
|
||||
since: chrono::DateTime<chrono::Utc>,
|
||||
) -> 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
|
||||
.inner_join(devices::table)
|
||||
.filter(
|
||||
devices::user_id
|
||||
.eq(user.id)
|
||||
.and(devices::device_id.eq(device_id))
|
||||
.and(device_subscriptions::time_changed.ge(since)),
|
||||
)
|
||||
.select(db::DeviceSubscription::as_select());
|
||||
let query = device_subscriptions::table
|
||||
.inner_join(devices::table)
|
||||
.filter(
|
||||
devices::user_id
|
||||
.eq(user.id)
|
||||
.and(devices::device_id.eq(device_id))
|
||||
.and(device_subscriptions::time_changed.ge(since)),
|
||||
)
|
||||
.select(DeviceSubscription::as_select());
|
||||
|
||||
for sub in query.load_iter(&mut self.pool.get()?)? {
|
||||
let sub = sub?;
|
||||
for sub in query.load_iter(&mut self.pool.get()?)? {
|
||||
let sub = sub?;
|
||||
|
||||
if sub.deleted {
|
||||
removed.push(gpodder::Subscription {
|
||||
url: sub.podcast_url,
|
||||
time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(),
|
||||
});
|
||||
} else {
|
||||
added.push(gpodder::Subscription {
|
||||
url: sub.podcast_url,
|
||||
time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(),
|
||||
});
|
||||
if sub.deleted {
|
||||
removed.push(gpodder::Subscription {
|
||||
url: sub.podcast_url,
|
||||
time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(),
|
||||
});
|
||||
} else {
|
||||
added.push(gpodder::Subscription {
|
||||
url: sub.podcast_url,
|
||||
time_changed: DateTime::from_timestamp(sub.time_changed, 0).unwrap(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((added, removed))
|
||||
Ok::<_, DbError>((added, removed))
|
||||
})()
|
||||
.map_err(AuthErr::from)
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
use crate::db;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Command {
|
||||
/// Add devices of a specific user to the same sync group
|
||||
|
@ -15,9 +13,10 @@ pub enum Command {
|
|||
|
||||
impl Command {
|
||||
pub fn run(&self, config: &crate::config::Config) -> u8 {
|
||||
let pool = db::initialize_db(config.data_dir.join(crate::DB_FILENAME), true).unwrap();
|
||||
let repo = db::SqliteRepository::from(pool);
|
||||
let store = crate::gpodder::GpodderRepository::new(repo);
|
||||
let store =
|
||||
gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME))
|
||||
.unwrap();
|
||||
let store = gpodder::GpodderRepository::new(store);
|
||||
|
||||
match self {
|
||||
Self::Sync { username, devices } => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
mod db;
|
||||
// mod db;
|
||||
mod gpo;
|
||||
mod serve;
|
||||
|
||||
|
@ -48,9 +48,8 @@ pub struct ClapConfig {
|
|||
#[derive(Subcommand)]
|
||||
pub enum Command {
|
||||
Serve,
|
||||
#[command(subcommand)]
|
||||
Db(db::DbCommand),
|
||||
|
||||
// #[command(subcommand)]
|
||||
// Db(db::DbCommand),
|
||||
/// Perform operations on the database through the Gpodder abstraction, allowing operations
|
||||
/// identical to the ones performed by the API.
|
||||
#[command(subcommand)]
|
||||
|
@ -80,7 +79,7 @@ impl Cli {
|
|||
|
||||
match &self.cmd {
|
||||
Command::Serve => serve::serve(&config),
|
||||
Command::Db(cmd) => cmd.run(&config),
|
||||
// Command::Db(cmd) => cmd.run(&config),
|
||||
Command::Gpo(cmd) => cmd.run(&config),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use crate::{db, server};
|
||||
use crate::server;
|
||||
|
||||
pub fn serve(config: &crate::config::Config) -> u8 {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
tracing::info!("Initializing database and running migrations");
|
||||
|
||||
let pool = db::initialize_db(config.data_dir.join(crate::DB_FILENAME), true).unwrap();
|
||||
let repo = db::SqliteRepository::from(pool);
|
||||
let store =
|
||||
gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME))
|
||||
.unwrap();
|
||||
let store = gpodder::GpodderRepository::new(store);
|
||||
|
||||
let ctx = server::Context {
|
||||
store: crate::gpodder::GpodderRepository::new(repo),
|
||||
};
|
||||
let ctx = server::Context { store };
|
||||
let app = server::app(ctx.clone());
|
||||
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
|
|
|
@ -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,7 +1,5 @@
|
|||
mod cli;
|
||||
mod config;
|
||||
mod db;
|
||||
mod gpodder;
|
||||
mod server;
|
||||
|
||||
use clap::Parser;
|
||||
|
|
|
@ -2,13 +2,13 @@ use std::fmt;
|
|||
|
||||
use axum::{http::StatusCode, response::IntoResponse};
|
||||
|
||||
use crate::{db, ErrorExt};
|
||||
use crate::ErrorExt;
|
||||
|
||||
pub type AppResult<T> = Result<T, AppError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AppError {
|
||||
Db(db::DbError),
|
||||
// Db(db::DbError),
|
||||
IO(std::io::Error),
|
||||
Other(Box<dyn std::error::Error + 'static + Send + Sync>),
|
||||
BadRequest,
|
||||
|
@ -19,7 +19,7 @@ pub enum AppError {
|
|||
impl fmt::Display for AppError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Db(_) => write!(f, "database error"),
|
||||
// Self::Db(_) => write!(f, "database error"),
|
||||
Self::IO(_) => write!(f, "io error"),
|
||||
Self::Other(_) => write!(f, "other error"),
|
||||
Self::BadRequest => write!(f, "bad request"),
|
||||
|
@ -32,7 +32,7 @@ impl fmt::Display for AppError {
|
|||
impl std::error::Error for AppError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Db(err) => Some(err),
|
||||
// Self::Db(err) => Some(err),
|
||||
Self::IO(err) => Some(err),
|
||||
Self::Other(err) => Some(err.as_ref()),
|
||||
Self::NotFound | Self::Unauthorized | Self::BadRequest => None,
|
||||
|
@ -40,12 +40,6 @@ impl std::error::Error for AppError {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<db::DbError> for AppError {
|
||||
fn from(value: db::DbError) -> Self {
|
||||
Self::Db(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for AppError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::IO(value)
|
||||
|
|
|
@ -10,13 +10,10 @@ use axum_extra::{
|
|||
};
|
||||
use cookie::time::Duration;
|
||||
|
||||
use crate::{
|
||||
gpodder,
|
||||
server::{
|
||||
error::{AppError, AppResult},
|
||||
gpodder::SESSION_ID_COOKIE,
|
||||
Context,
|
||||
},
|
||||
use crate::server::{
|
||||
error::{AppError, AppResult},
|
||||
gpodder::SESSION_ID_COOKIE,
|
||||
Context,
|
||||
};
|
||||
|
||||
pub fn router() -> Router<Context> {
|
||||
|
|
|
@ -5,17 +5,14 @@ use axum::{
|
|||
Extension, Json, Router,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
gpodder,
|
||||
server::{
|
||||
error::{AppError, AppResult},
|
||||
gpodder::{
|
||||
auth_middleware,
|
||||
format::{Format, StringWithFormat},
|
||||
models,
|
||||
},
|
||||
Context,
|
||||
use crate::server::{
|
||||
error::{AppError, AppResult},
|
||||
gpodder::{
|
||||
auth_middleware,
|
||||
format::{Format, StringWithFormat},
|
||||
models,
|
||||
},
|
||||
Context,
|
||||
};
|
||||
|
||||
pub fn router(ctx: Context) -> Router<Context> {
|
||||
|
|
|
@ -7,18 +7,15 @@ use axum::{
|
|||
use chrono::DateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
gpodder,
|
||||
server::{
|
||||
error::{AppError, AppResult},
|
||||
gpodder::{
|
||||
auth_middleware,
|
||||
format::{Format, StringWithFormat},
|
||||
models,
|
||||
models::UpdatedUrlsResponse,
|
||||
},
|
||||
Context,
|
||||
use crate::server::{
|
||||
error::{AppError, AppResult},
|
||||
gpodder::{
|
||||
auth_middleware,
|
||||
format::{Format, StringWithFormat},
|
||||
models,
|
||||
models::UpdatedUrlsResponse,
|
||||
},
|
||||
Context,
|
||||
};
|
||||
|
||||
pub fn router(ctx: Context) -> Router<Context> {
|
||||
|
|
|
@ -6,17 +6,14 @@ use axum::{
|
|||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
gpodder,
|
||||
server::{
|
||||
error::{AppError, AppResult},
|
||||
gpodder::{
|
||||
auth_middleware,
|
||||
format::{Format, StringWithFormat},
|
||||
models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse},
|
||||
},
|
||||
Context,
|
||||
use crate::server::{
|
||||
error::{AppError, AppResult},
|
||||
gpodder::{
|
||||
auth_middleware,
|
||||
format::{Format, StringWithFormat},
|
||||
models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse},
|
||||
},
|
||||
Context,
|
||||
};
|
||||
|
||||
pub fn router(ctx: Context) -> Router<Context> {
|
||||
|
|
|
@ -5,17 +5,14 @@ use axum::{
|
|||
Extension, Json, Router,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
gpodder,
|
||||
server::{
|
||||
error::{AppError, AppResult},
|
||||
gpodder::{
|
||||
auth_middleware,
|
||||
format::{Format, StringWithFormat},
|
||||
models::{SyncStatus, SyncStatusDelta},
|
||||
},
|
||||
Context,
|
||||
use crate::server::{
|
||||
error::{AppError, AppResult},
|
||||
gpodder::{
|
||||
auth_middleware,
|
||||
format::{Format, StringWithFormat},
|
||||
models::{SyncStatus, SyncStatusDelta},
|
||||
},
|
||||
Context,
|
||||
};
|
||||
|
||||
pub fn router(ctx: Context) -> Router<Context> {
|
||||
|
|
|
@ -17,7 +17,7 @@ use axum_extra::{
|
|||
};
|
||||
use tower_http::set_header::SetResponseHeaderLayer;
|
||||
|
||||
use crate::{gpodder, server::error::AppError};
|
||||
use crate::server::error::AppError;
|
||||
|
||||
use super::Context;
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::gpodder;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct SubscriptionDelta {
|
||||
pub add: Vec<String>,
|
||||
|
|
|
@ -5,13 +5,10 @@ use axum::{
|
|||
Extension, Json, Router,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
gpodder,
|
||||
server::{
|
||||
error::{AppError, AppResult},
|
||||
gpodder::{auth_middleware, format::StringWithFormat},
|
||||
Context,
|
||||
},
|
||||
use crate::server::{
|
||||
error::{AppError, AppResult},
|
||||
gpodder::{auth_middleware, format::StringWithFormat},
|
||||
Context,
|
||||
};
|
||||
|
||||
pub fn router(ctx: Context) -> Router<Context> {
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
mod error;
|
||||
mod gpodder;
|
||||
|
||||
use axum::{extract::Request, middleware::Next, response::Response, Router};
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::Request,
|
||||
http::StatusCode,
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
Router,
|
||||
};
|
||||
use http_body_util::BodyExt;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
pub store: crate::gpodder::GpodderRepository,
|
||||
pub store: ::gpodder::GpodderRepository,
|
||||
}
|
||||
|
||||
pub fn app(ctx: Context) -> Router {
|
||||
Router::new()
|
||||
.merge(gpodder::router(ctx.clone()))
|
||||
.layer(axum::middleware::from_fn(header_logger))
|
||||
.layer(axum::middleware::from_fn(body_logger))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(ctx)
|
||||
}
|
||||
|
@ -26,3 +35,41 @@ async fn header_logger(request: Request, next: Next) -> Response {
|
|||
|
||||
res
|
||||
}
|
||||
|
||||
async fn body_logger(request: Request, next: Next) -> Response {
|
||||
let (parts, body) = request.into_parts();
|
||||
|
||||
let bytes = match body
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())
|
||||
{
|
||||
Ok(res) => res.to_bytes(),
|
||||
Err(err) => {
|
||||
return err;
|
||||
}
|
||||
};
|
||||
|
||||
tracing::debug!("request body = {:?}", String::from_utf8(bytes.to_vec()));
|
||||
|
||||
let res = next
|
||||
.run(Request::from_parts(parts, Body::from(bytes)))
|
||||
.await;
|
||||
|
||||
let (parts, body) = res.into_parts();
|
||||
|
||||
let bytes = match body
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())
|
||||
{
|
||||
Ok(res) => res.to_bytes(),
|
||||
Err(err) => {
|
||||
return err;
|
||||
}
|
||||
};
|
||||
|
||||
tracing::debug!("response body = {:?}", String::from_utf8(bytes.to_vec()));
|
||||
|
||||
Response::from_parts(parts, Body::from(bytes))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue