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.
This commit is contained in:
parent
86687a7b96
commit
0cfcd90eba
45 changed files with 2416 additions and 882 deletions
148
gpodder_sqlite/src/models/device.rs
Normal file
148
gpodder_sqlite/src/models/device.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
use std::{fmt, str::FromStr};
|
||||
|
||||
use diesel::{
|
||||
deserialize::{FromSql, FromSqlRow},
|
||||
expression::AsExpression,
|
||||
prelude::*,
|
||||
serialize::ToSql,
|
||||
sql_types::Text,
|
||||
sqlite::{Sqlite, SqliteValue},
|
||||
};
|
||||
|
||||
use crate::schema::*;
|
||||
|
||||
#[derive(Clone, Queryable, Selectable)]
|
||||
#[diesel(table_name = devices)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct Device {
|
||||
pub id: i64,
|
||||
pub device_id: String,
|
||||
pub user_id: i64,
|
||||
pub caption: String,
|
||||
pub type_: DeviceType,
|
||||
pub sync_group_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = devices)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct NewDevice {
|
||||
pub device_id: String,
|
||||
pub user_id: i64,
|
||||
pub caption: String,
|
||||
pub type_: DeviceType,
|
||||
}
|
||||
|
||||
#[derive(FromSqlRow, Debug, AsExpression, Clone)]
|
||||
#[diesel(sql_type = Text)]
|
||||
pub enum DeviceType {
|
||||
Desktop,
|
||||
Laptop,
|
||||
Mobile,
|
||||
Server,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn device_id_to_id(
|
||||
conn: &mut SqliteConnection,
|
||||
user_id: i64,
|
||||
device_id: &str,
|
||||
) -> diesel::QueryResult<i64> {
|
||||
devices::table
|
||||
.select(devices::id)
|
||||
.filter(
|
||||
devices::user_id
|
||||
.eq(user_id)
|
||||
.and(devices::device_id.eq(device_id)),
|
||||
)
|
||||
.get_result(conn)
|
||||
}
|
||||
|
||||
pub fn by_device_id(
|
||||
conn: &mut SqliteConnection,
|
||||
user_id: i64,
|
||||
device_id: &str,
|
||||
) -> diesel::QueryResult<Self> {
|
||||
devices::dsl::devices
|
||||
.select(Self::as_select())
|
||||
.filter(
|
||||
devices::user_id
|
||||
.eq(user_id)
|
||||
.and(devices::device_id.eq(device_id)),
|
||||
)
|
||||
.get_result(conn)
|
||||
}
|
||||
}
|
||||
|
||||
impl NewDevice {
|
||||
pub fn new(user_id: i64, device_id: String, caption: String, type_: DeviceType) -> Self {
|
||||
Self {
|
||||
device_id,
|
||||
user_id,
|
||||
caption,
|
||||
type_,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DeviceType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Desktop => "desktop",
|
||||
Self::Laptop => "laptop",
|
||||
Self::Mobile => "mobile",
|
||||
Self::Server => "server",
|
||||
Self::Other => "other",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceTypeParseErr(String);
|
||||
|
||||
impl fmt::Display for DeviceTypeParseErr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "invalid device type '{}'", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DeviceTypeParseErr {}
|
||||
|
||||
impl FromStr for DeviceType {
|
||||
type Err = DeviceTypeParseErr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"desktop" => Ok(Self::Desktop),
|
||||
"laptop" => Ok(Self::Laptop),
|
||||
"mobile" => Ok(Self::Mobile),
|
||||
"server" => Ok(Self::Server),
|
||||
"other" => Ok(Self::Other),
|
||||
_ => Err(DeviceTypeParseErr(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql<Text, Sqlite> for DeviceType {
|
||||
fn from_sql(bytes: SqliteValue) -> diesel::deserialize::Result<Self> {
|
||||
let s = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
|
||||
|
||||
Ok(s.as_str().parse()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql<Text, Sqlite> for DeviceType {
|
||||
fn to_sql<'b>(
|
||||
&'b self,
|
||||
out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
|
||||
) -> diesel::serialize::Result {
|
||||
out.set_value(self.to_string());
|
||||
|
||||
Ok(diesel::serialize::IsNull::No)
|
||||
}
|
||||
}
|
||||
24
gpodder_sqlite/src/models/device_subscription.rs
Normal file
24
gpodder_sqlite/src/models/device_subscription.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use diesel::prelude::*;
|
||||
|
||||
use crate::schema::*;
|
||||
|
||||
#[derive(Clone, Queryable, Selectable)]
|
||||
#[diesel(table_name = device_subscriptions)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct DeviceSubscription {
|
||||
pub id: i64,
|
||||
pub device_id: i64,
|
||||
pub podcast_url: String,
|
||||
pub time_changed: i64,
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = device_subscriptions)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct NewDeviceSubscription {
|
||||
pub device_id: i64,
|
||||
pub podcast_url: String,
|
||||
pub time_changed: i64,
|
||||
pub deleted: bool,
|
||||
}
|
||||
114
gpodder_sqlite/src/models/episode_action.rs
Normal file
114
gpodder_sqlite/src/models/episode_action.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
use std::{fmt, str::FromStr};
|
||||
|
||||
use diesel::{
|
||||
deserialize::{FromSql, FromSqlRow},
|
||||
expression::AsExpression,
|
||||
prelude::{Insertable, Queryable},
|
||||
serialize::ToSql,
|
||||
sql_types::Text,
|
||||
sqlite::{Sqlite, SqliteValue},
|
||||
Selectable,
|
||||
};
|
||||
|
||||
use crate::schema::*;
|
||||
|
||||
#[derive(Clone, Queryable, Selectable)]
|
||||
#[diesel(table_name = episode_actions)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct EpisodeAction {
|
||||
pub id: i64,
|
||||
pub user_id: i64,
|
||||
pub device_id: Option<i64>,
|
||||
pub podcast_url: String,
|
||||
pub episode_url: String,
|
||||
pub time_changed: i64,
|
||||
pub timestamp: Option<i64>,
|
||||
pub action: ActionType,
|
||||
pub started: Option<i32>,
|
||||
pub position: Option<i32>,
|
||||
pub total: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = episode_actions)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct NewEpisodeAction {
|
||||
pub user_id: i64,
|
||||
pub device_id: Option<i64>,
|
||||
pub podcast_url: String,
|
||||
pub episode_url: String,
|
||||
pub time_changed: i64,
|
||||
pub timestamp: Option<i64>,
|
||||
pub action: ActionType,
|
||||
pub started: Option<i32>,
|
||||
pub position: Option<i32>,
|
||||
pub total: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(FromSqlRow, Debug, AsExpression, Clone)]
|
||||
#[diesel(sql_type = Text)]
|
||||
pub enum ActionType {
|
||||
New,
|
||||
Download,
|
||||
Play,
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl fmt::Display for ActionType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::New => "new",
|
||||
Self::Download => "download",
|
||||
Self::Play => "play",
|
||||
Self::Delete => "delete",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ActionTypeParseErr(String);
|
||||
|
||||
impl fmt::Display for ActionTypeParseErr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "invalid action type '{}'", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ActionTypeParseErr {}
|
||||
|
||||
impl FromStr for ActionType {
|
||||
type Err = ActionTypeParseErr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"new" => Ok(Self::New),
|
||||
"download" => Ok(Self::Download),
|
||||
"delete" => Ok(Self::Delete),
|
||||
"play" => Ok(Self::Play),
|
||||
_ => Err(ActionTypeParseErr(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql<Text, Sqlite> for ActionType {
|
||||
fn from_sql(bytes: SqliteValue) -> diesel::deserialize::Result<Self> {
|
||||
let s = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
|
||||
|
||||
Ok(s.as_str().parse()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql<Text, Sqlite> for ActionType {
|
||||
fn to_sql<'b>(
|
||||
&'b self,
|
||||
out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
|
||||
) -> diesel::serialize::Result {
|
||||
out.set_value(self.to_string());
|
||||
|
||||
Ok(diesel::serialize::IsNull::No)
|
||||
}
|
||||
}
|
||||
6
gpodder_sqlite/src/models/mod.rs
Normal file
6
gpodder_sqlite/src/models/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub mod device;
|
||||
pub mod device_subscription;
|
||||
pub mod episode_action;
|
||||
pub mod session;
|
||||
pub mod sync_group;
|
||||
pub mod user;
|
||||
60
gpodder_sqlite/src/models/session.rs
Normal file
60
gpodder_sqlite/src/models/session.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
use diesel::prelude::*;
|
||||
|
||||
use crate::schema::*;
|
||||
|
||||
#[derive(Clone, Queryable, Selectable, Insertable, Associations)]
|
||||
#[diesel(belongs_to(super::user::User))]
|
||||
#[diesel(table_name = sessions)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct Session {
|
||||
pub id: i64,
|
||||
pub user_id: i64,
|
||||
pub last_seen: i64,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
// pub fn new_for_user(pool: &DbPool, user_id: i64, last_seen: i64) -> DbResult<Self> {
|
||||
// let id: i64 = rand::thread_rng().gen();
|
||||
|
||||
// Ok(Self {
|
||||
// id,
|
||||
// user_id,
|
||||
// last_seen,
|
||||
// }
|
||||
// .insert_into(sessions::table)
|
||||
// .returning(Self::as_returning())
|
||||
// .get_result(&mut pool.get()?)?)
|
||||
// }
|
||||
|
||||
// pub fn user_from_id(pool: &DbPool, id: i64) -> DbResult<Option<super::user::User>> {
|
||||
// Ok(sessions::dsl::sessions
|
||||
// .inner_join(users::table)
|
||||
// .filter(sessions::id.eq(id))
|
||||
// .select(User::as_select())
|
||||
// .get_result(&mut pool.get()?)
|
||||
// .optional()?)
|
||||
// }
|
||||
|
||||
// pub fn user(&self, pool: &DbPool) -> DbResult<Option<super::user::User>> {
|
||||
// Self::user_from_id(pool, self.id)
|
||||
// }
|
||||
|
||||
// pub fn by_id(pool: &DbPool, id: i64) -> DbResult<Option<Self>> {
|
||||
// Ok(sessions::dsl::sessions
|
||||
// .find(id)
|
||||
// .get_result(&mut pool.get()?)
|
||||
// .optional()?)
|
||||
// }
|
||||
|
||||
// pub fn remove(self, pool: &DbPool) -> DbResult<bool> {
|
||||
// Self::remove_by_id(pool, self.id)
|
||||
// }
|
||||
|
||||
// pub fn remove_by_id(pool: &DbPool, id: i64) -> DbResult<bool> {
|
||||
// Ok(
|
||||
// diesel::delete(sessions::dsl::sessions.filter(sessions::id.eq(id)))
|
||||
// .execute(&mut pool.get()?)?
|
||||
// > 0,
|
||||
// )
|
||||
// }
|
||||
}
|
||||
33
gpodder_sqlite/src/models/sync_group.rs
Normal file
33
gpodder_sqlite/src/models/sync_group.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use diesel::{
|
||||
dsl::{exists, not},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
use crate::schema::*;
|
||||
|
||||
#[derive(Queryable, Selectable)]
|
||||
#[diesel(table_name = sync_groups)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct SyncGroup {
|
||||
pub id: i64,
|
||||
}
|
||||
|
||||
impl SyncGroup {
|
||||
pub fn new(conn: &mut SqliteConnection) -> QueryResult<Self> {
|
||||
diesel::insert_into(sync_groups::table)
|
||||
.default_values()
|
||||
.returning(SyncGroup::as_returning())
|
||||
.get_result(conn)
|
||||
}
|
||||
|
||||
pub fn remove_unused(conn: &mut SqliteConnection) -> QueryResult<usize> {
|
||||
diesel::delete(
|
||||
sync_groups::table.filter(not(exists(
|
||||
devices::table
|
||||
.select(1.into_sql::<diesel::sql_types::Integer>())
|
||||
.filter(devices::sync_group_id.eq(sync_groups::id.nullable())),
|
||||
))),
|
||||
)
|
||||
.execute(conn)
|
||||
}
|
||||
}
|
||||
47
gpodder_sqlite/src/models/user.rs
Normal file
47
gpodder_sqlite/src/models/user.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use diesel::prelude::*;
|
||||
|
||||
use crate::schema::*;
|
||||
|
||||
#[derive(Clone, Queryable, Selectable)]
|
||||
#[diesel(table_name = users)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
pub username: String,
|
||||
pub password_hash: String,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = users)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct NewUser {
|
||||
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()
|
||||
// }
|
||||
// }
|
||||
Loading…
Add table
Add a link
Reference in a new issue