diff options
| author | metamuffin <metamuffin@disroot.org> | 2024-01-20 00:50:20 +0100 | 
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2024-01-20 00:50:20 +0100 | 
| commit | 46c251655db7bb3d9aa814b1a5dde85336b0b9b1 (patch) | |
| tree | ab0696f2c92e8854ce6aa0737877cc15184bd8b6 /server/src/routes | |
| parent | 1c37d32a0985ff7390313833345b9299f9f0b196 (diff) | |
| download | jellything-46c251655db7bb3d9aa814b1a5dde85336b0b9b1.tar jellything-46c251655db7bb3d9aa814b1a5dde85336b0b9b1.tar.bz2 jellything-46c251655db7bb3d9aa814b1a5dde85336b0b9b1.tar.zst | |
replace sled with redb
Diffstat (limited to 'server/src/routes')
| -rw-r--r-- | server/src/routes/api/mod.rs | 12 | ||||
| -rw-r--r-- | server/src/routes/mod.rs | 4 | ||||
| -rw-r--r-- | server/src/routes/stream.rs | 10 | ||||
| -rw-r--r-- | server/src/routes/ui/account/mod.rs | 71 | ||||
| -rw-r--r-- | server/src/routes/ui/account/session/guard.rs | 18 | ||||
| -rw-r--r-- | server/src/routes/ui/account/settings.rs | 53 | ||||
| -rw-r--r-- | server/src/routes/ui/admin/mod.rs | 45 | ||||
| -rw-r--r-- | server/src/routes/ui/admin/user.rs | 70 | ||||
| -rw-r--r-- | server/src/routes/ui/assets.rs | 25 | ||||
| -rw-r--r-- | server/src/routes/ui/browser.rs | 43 | ||||
| -rw-r--r-- | server/src/routes/ui/error.rs | 40 | ||||
| -rw-r--r-- | server/src/routes/ui/home.rs | 62 | ||||
| -rw-r--r-- | server/src/routes/ui/node.rs | 35 | ||||
| -rw-r--r-- | server/src/routes/ui/player.rs | 10 | ||||
| -rw-r--r-- | server/src/routes/userdata.rs | 83 | 
15 files changed, 346 insertions, 235 deletions
| diff --git a/server/src/routes/api/mod.rs b/server/src/routes/api/mod.rs index 828b576..d8ea167 100644 --- a/server/src/routes/api/mod.rs +++ b/server/src/routes/api/mod.rs @@ -7,8 +7,9 @@ use super::ui::{      account::{login_logic, session::AdminSession},      error::MyResult,  }; -use crate::database::Database; +use crate::database::DataAcid;  use anyhow::{anyhow, Context}; +use jellybase::database::{TableExt, T_NODE};  use jellycommon::{user::CreateSessionParams, Node};  use rocket::{      get, @@ -35,7 +36,7 @@ pub fn r_api_version() -> &'static str {  #[post("/api/create_session", data = "<data>")]  pub fn r_api_account_login( -    database: &State<Database>, +    database: &State<DataAcid>,      data: Json<CreateSessionParams>,  ) -> MyResult<Value> {      let token = login_logic( @@ -51,13 +52,12 @@ pub fn r_api_account_login(  #[get("/api/node_raw/<id>")]  pub fn r_api_node_raw(      admin: AdminSession, -    database: &State<Database>, +    database: &State<DataAcid>,      id: &str,  ) -> MyResult<Json<Node>> {      drop(admin); -    let node = database -        .node -        .get(&id.to_string()) +    let node = T_NODE +        .get(database, id)          .context("retrieving library node")?          .ok_or(anyhow!("node does not exist"))?;      Ok(Json(node)) diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 47fd6d2..6bc5127 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -3,7 +3,7 @@      which is licensed under the GNU Affero General Public License (version 3); see /COPYING.      Copyright (C) 2023 metamuffin <metamuffin.org>  */ -use crate::{database::Database, routes::ui::error::MyResult}; +use crate::{database::DataAcid, routes::ui::error::MyResult};  use api::{r_api_account_login, r_api_node_raw, r_api_root, r_api_version};  use base64::Engine;  use jellybase::{federation::Federation, CONF}; @@ -49,7 +49,7 @@ macro_rules! uri {      };  } -pub fn build_rocket(database: Database, federation: Federation) -> Rocket<Build> { +pub fn build_rocket(database: DataAcid, federation: Federation) -> Rocket<Build> {      rocket::build()          .configure(Config {              address: std::env::var("BIND_ADDR") diff --git a/server/src/routes/stream.rs b/server/src/routes/stream.rs index e8b14b5..c033bda 100644 --- a/server/src/routes/stream.rs +++ b/server/src/routes/stream.rs @@ -4,9 +4,10 @@      Copyright (C) 2023 metamuffin <metamuffin.org>  */  use super::ui::{account::session::Session, error::MyError}; -use crate::database::Database; +use crate::database::DataAcid;  use anyhow::{anyhow, Result};  use jellybase::{ +    database::{TableExt, T_NODE},      federation::Federation,      permission::{NodePermissionExt, PermissionSetExt},      CONF, @@ -46,14 +47,13 @@ pub async fn r_stream_head(  pub async fn r_stream(      session: Session,      federation: &State<Federation>, -    db: &State<Database>, +    db: &State<DataAcid>,      id: &str,      range: Option<RequestRange>,      spec: StreamSpec,  ) -> Result<Either<StreamResponse, RedirectResponse>, MyError> { -    let node = db -        .node -        .get(&id.to_string())? +    let node = T_NODE +        .get(&db, id)?          .only_if_permitted(&session.user.permissions)          .ok_or(anyhow!("node does not exist"))?;      let source = node diff --git a/server/src/routes/ui/account/mod.rs b/server/src/routes/ui/account/mod.rs index cd8695f..8af92a0 100644 --- a/server/src/routes/ui/account/mod.rs +++ b/server/src/routes/ui/account/mod.rs @@ -8,7 +8,7 @@ pub mod settings;  use super::{error::MyError, layout::LayoutPage};  use crate::{ -    database::Database, +    database::DataAcid,      routes::ui::{          account::session::Session, error::MyResult, home::rocket_uri_macro_r_home,          layout::DynLayoutPage, @@ -18,7 +18,10 @@ use crate::{  use anyhow::anyhow;  use argon2::{password_hash::Salt, Argon2, PasswordHasher};  use chrono::Duration; -use jellybase::CONF; +use jellybase::{ +    database::{Ser, TableExt, T_INVITE, T_USER}, +    CONF, +};  use jellycommon::user::{PermissionSet, Theme, User, UserPermission};  use rocket::{      form::{Contextual, Form}, @@ -121,7 +124,7 @@ pub fn r_account_logout() -> DynLayoutPage<'static> {  #[post("/account/register", data = "<form>")]  pub fn r_account_register_post<'a>( -    database: &'a State<Database>, +    database: &'a State<DataAcid>,      _sess: Option<Session>,      form: Form<Contextual<'a, RegisterForm>>,  ) -> MyResult<DynLayoutPage<'a>> { @@ -131,15 +134,17 @@ pub fn r_account_register_post<'a>(          None => return Err(format_form_error(form)),      }; -    if database.invite.remove(&form.invitation).unwrap().is_none() { -        return Err(MyError(anyhow!("invitation invalid"))); +    let txn = database.begin_write()?; +    let mut invites = txn.open_table(T_INVITE)?; +    let mut users = txn.open_table(T_USER)?; + +    if invites.remove(&*form.invitation)?.is_none() { +        Err(anyhow!("invitation invalid"))?;      } -    match database -        .user -        .compare_and_swap( -            &form.username, -            None, -            Some(&User { +    let prev_user = users +        .insert( +            &*form.username, +            Ser(User {                  display_name: form.username.clone(),                  name: form.username.clone(),                  password: hash_password(&form.username, &form.password), @@ -147,27 +152,32 @@ pub fn r_account_register_post<'a>(                  theme: Theme::Dark,                  permissions: PermissionSet::default(),              }), -        ) -        .unwrap() -    { -        Ok(_) => Ok(LayoutPage { -            title: "Registration successful".to_string(), -            content: markup::new! { -                h1 { @if logged_in { -                    "Registration successful, you may switch account now." -                } else { -                    "Registration successful, you may log in now." -                }} -            }, -            ..Default::default() -        }), -        Err(_) => Err(MyError(anyhow!("username is taken"))), +        )? +        .map(|x| x.value().0); +    if prev_user.is_some() { +        Err(anyhow!("username taken"))?;      } + +    drop(users); +    drop(invites); +    txn.commit()?; + +    Ok(LayoutPage { +        title: "Registration successful".to_string(), +        content: markup::new! { +            h1 { @if logged_in { +                "Registration successful, you may switch account now." +            } else { +                "Registration successful, you may log in now." +            }} +        }, +        ..Default::default() +    })  }  #[post("/account/login", data = "<form>")]  pub fn r_account_login_post( -    database: &State<Database>, +    database: &State<DataAcid>,      jar: &CookieJar,      form: Form<Contextual<LoginForm>>,  ) -> MyResult<Redirect> { @@ -194,7 +204,7 @@ pub fn r_account_logout_post(jar: &CookieJar) -> MyResult<Redirect> {  }  pub fn login_logic( -    database: &Database, +    database: &DataAcid,      username: &str,      password: &str,      expire: Option<i64>, @@ -203,9 +213,8 @@ pub fn login_logic(      // hashing the password regardless if the accounts exists to prevent timing attacks      let password = hash_password(username, password); -    let mut user = database -        .user -        .get(&username.to_string())? +    let mut user = T_USER +        .get(database, username)?          .ok_or(anyhow!("invalid password"))?;      if user.password != password { diff --git a/server/src/routes/ui/account/session/guard.rs b/server/src/routes/ui/account/session/guard.rs index ae1ebd3..b2fd408 100644 --- a/server/src/routes/ui/account/session/guard.rs +++ b/server/src/routes/ui/account/session/guard.rs @@ -4,8 +4,9 @@      Copyright (C) 2023 metamuffin <metamuffin.org>  */  use super::{AdminSession, Session}; -use crate::{database::Database, routes::ui::error::MyError}; +use crate::{database::DataAcid, routes::ui::error::MyError};  use anyhow::anyhow; +use jellybase::database::{ReadableTable, T_USER};  use log::warn;  use rocket::{      async_trait, @@ -35,8 +36,19 @@ impl Session {              username = "admin".to_string();          } -        let db = req.guard::<&State<Database>>().await.unwrap(); -        let user = db.user.get(&username)?.ok_or(anyhow!("user not found"))?; +        let db = req.guard::<&State<DataAcid>>().await.unwrap(); + +        let user = { +            let txn = db.inner.begin_read()?; +            let table = txn.open_table(T_USER)?; +            let user = table +                .get(&*username)? +                .ok_or(anyhow!("user not found"))? +                .value() +                .0; +            drop(table); +            user +        };          Ok(Session { user })      } diff --git a/server/src/routes/ui/account/settings.rs b/server/src/routes/ui/account/settings.rs index f14478b..ecc0723 100644 --- a/server/src/routes/ui/account/settings.rs +++ b/server/src/routes/ui/account/settings.rs @@ -5,7 +5,7 @@  */  use super::{format_form_error, hash_password};  use crate::{ -    database::Database, +    database::DataAcid,      routes::ui::{          account::{rocket_uri_macro_r_account_login, session::Session},          error::MyResult, @@ -13,7 +13,11 @@ use crate::{      },      uri,  }; -use jellybase::permission::PermissionSetExt; +use anyhow::anyhow; +use jellybase::{ +    database::{ReadableTable, Ser, T_USER}, +    permission::PermissionSetExt, +};  use jellycommon::user::{Theme, UserPermission};  use markup::{Render, RenderAttributeValue};  use rocket::{ @@ -97,7 +101,7 @@ pub fn r_account_settings(session: Session) -> DynLayoutPage<'static> {  #[post("/account/settings", data = "<form>")]  pub fn r_account_settings_post(      session: Session, -    database: &State<Database>, +    database: &State<DataAcid>,      form: Form<Contextual<SettingsForm>>,  ) -> MyResult<DynLayoutPage<'static>> {      session @@ -111,23 +115,32 @@ pub fn r_account_settings_post(      };      let mut out = String::new(); -    database.user.fetch_and_update(&session.user.name, |k| { -        k.map(|mut k| { -            if let Some(password) = &form.password { -                k.password = hash_password(&session.user.name, password); -                out += "Password updated\n"; -            } -            if let Some(display_name) = &form.display_name { -                k.display_name = display_name.clone(); -                out += "Display name updated\n"; -            } -            if let Some(theme) = form.theme { -                k.theme = theme; -                out += "Theme updated\n"; -            } -            k -        }) -    })?; + +    let txn = database.begin_write()?; +    let mut users = txn.open_table(T_USER)?; + +    let mut user = users +        .get(&*session.user.name)? +        .ok_or(anyhow!("user missing"))? +        .value() +        .0; + +    if let Some(password) = &form.password { +        user.password = hash_password(&session.user.name, password); +        out += "Password updated\n"; +    } +    if let Some(display_name) = &form.display_name { +        user.display_name = display_name.clone(); +        out += "Display name updated\n"; +    } +    if let Some(theme) = form.theme { +        user.theme = theme; +        out += "Theme updated\n"; +    } + +    users.insert(&*session.user.name, Ser(user))?; +    drop(users); +    txn.commit()?;      Ok(settings_page(          session, // using the old session here, results in outdated theme being displayed diff --git a/server/src/routes/ui/admin/mod.rs b/server/src/routes/ui/admin/mod.rs index b976192..60ed416 100644 --- a/server/src/routes/ui/admin/mod.rs +++ b/server/src/routes/ui/admin/mod.rs @@ -8,7 +8,7 @@ pub mod user;  use super::account::session::AdminSession;  use crate::{ -    database::Database, +    database::DataAcid,      routes::ui::{          admin::log::rocket_uri_macro_r_admin_log,          error::MyResult, @@ -17,7 +17,11 @@ use crate::{      uri,  };  use anyhow::anyhow; -use jellybase::{federation::Federation, CONF}; +use jellybase::{ +    database::{ReadableTable, TableExt, T_INVITE}, +    federation::Federation, +    CONF, +};  use jellyimport::import;  use rand::Rng;  use rocket::{form::Form, get, post, FromForm, State}; @@ -27,16 +31,28 @@ use user::rocket_uri_macro_r_admin_users;  #[get("/admin/dashboard")]  pub fn r_admin_dashboard(      _session: AdminSession, -    database: &State<Database>, +    database: &State<DataAcid>,  ) -> MyResult<DynLayoutPage<'static>> {      admin_dashboard(database, None)  }  pub fn admin_dashboard<'a>( -    database: &Database, +    database: &DataAcid,      flash: Option<MyResult<String>>,  ) -> MyResult<DynLayoutPage<'a>> { -    let invites = database.invite.iter().collect::<Result<Vec<_>, _>>()?; +    let invites = { +        let txn = database.begin_read()?; +        let table = txn.open_table(T_INVITE)?; +        let i = table +            .iter()? +            .map(|a| { +                let (x, _) = a.unwrap(); +                x.value().to_owned() +            }) +            .collect::<Vec<_>>(); +        drop(table); +        i +    };      let flash = flash.map(|f| f.map_err(|e| format!("{e:?}")));      Ok(LayoutPage { @@ -64,8 +80,8 @@ pub fn admin_dashboard<'a>(              ul { @for t in &invites {                  li {                      form[method="POST", action=uri!(r_admin_remove_invite())] { -                        span { @t.0 } -                        input[type="text", name="invite", value=&t.0, hidden]; +                        span { @t } +                        input[type="text", name="invite", value=&t, hidden];                          input[type="submit", value="Invalidate"];                      }                  } @@ -78,10 +94,10 @@ pub fn admin_dashboard<'a>(  #[post("/admin/generate_invite")]  pub fn r_admin_invite(      _session: AdminSession, -    database: &State<Database>, +    database: &State<DataAcid>,  ) -> MyResult<DynLayoutPage<'static>> {      let i = format!("{}", rand::thread_rng().gen::<u128>()); -    database.invite.insert(&i, &())?; +    T_INVITE.insert(&database, &*i, ())?;      admin_dashboard(database, Some(Ok(format!("Invite: {}", i))))  } @@ -94,13 +110,12 @@ pub struct DeleteInvite {  #[post("/admin/remove_invite", data = "<form>")]  pub fn r_admin_remove_invite(      session: AdminSession, -    database: &State<Database>, +    database: &State<DataAcid>,      form: Form<DeleteInvite>,  ) -> MyResult<DynLayoutPage<'static>> {      drop(session); -    database -        .invite -        .remove(&form.invite)? +    T_INVITE +        .remove(&database, form.invite.as_str())?          .ok_or(anyhow!("invite did not exist"))?;      admin_dashboard(database, Some(Ok("Invite invalidated".into()))) @@ -109,7 +124,7 @@ pub fn r_admin_remove_invite(  #[post("/admin/import")]  pub async fn r_admin_import(      session: AdminSession, -    database: &State<Database>, +    database: &State<DataAcid>,      federation: &State<Federation>,  ) -> MyResult<DynLayoutPage<'static>> {      drop(session); @@ -127,7 +142,7 @@ pub async fn r_admin_import(  #[post("/admin/delete_cache")]  pub async fn r_admin_delete_cache(      session: AdminSession, -    database: &State<Database>, +    database: &State<DataAcid>,  ) -> MyResult<DynLayoutPage<'static>> {      drop(session);      let t = Instant::now(); diff --git a/server/src/routes/ui/admin/user.rs b/server/src/routes/ui/admin/user.rs index 5c2c737..7d619c0 100644 --- a/server/src/routes/ui/admin/user.rs +++ b/server/src/routes/ui/admin/user.rs @@ -4,7 +4,7 @@      Copyright (C) 2023 metamuffin <metamuffin.org>  */  use crate::{ -    database::Database, +    database::DataAcid,      routes::ui::{          account::session::AdminSession,          error::MyResult, @@ -13,23 +13,36 @@ use crate::{      uri,  };  use anyhow::{anyhow, Context}; +use jellybase::database::{ReadableTable, Ser, TableExt, T_USER};  use jellycommon::user::{PermissionSet, UserPermission};  use rocket::{form::Form, get, post, FromForm, FromFormField, State};  #[get("/admin/users")]  pub fn r_admin_users(      _session: AdminSession, -    database: &State<Database>, +    database: &State<DataAcid>,  ) -> MyResult<DynLayoutPage<'static>> {      user_management(database, None)  }  fn user_management<'a>( -    database: &Database, +    database: &DataAcid,      flash: Option<MyResult<String>>,  ) -> MyResult<DynLayoutPage<'a>> {      // TODO this doesnt scale, pagination! -    let users = database.user.iter().collect::<Result<Vec<_>, _>>()?; +    let users = { +        let txn = database.begin_read()?; +        let table = txn.open_table(T_USER)?; +        let i = table +            .iter()? +            .map(|a| { +                let (x, y) = a.unwrap(); +                (x.value().to_owned(), y.value().0) +            }) +            .collect::<Vec<_>>(); +        drop(table); +        i +    };      let flash = flash.map(|f| f.map_err(|e| format!("{e:?}")));      Ok(LayoutPage { @@ -51,20 +64,19 @@ fn user_management<'a>(  #[get("/admin/user/<name>")]  pub fn r_admin_user<'a>(      _session: AdminSession, -    database: &State<Database>, +    database: &State<DataAcid>,      name: &'a str,  ) -> MyResult<DynLayoutPage<'a>> {      manage_single_user(database, None, name.to_string())  }  fn manage_single_user<'a>( -    database: &Database, +    database: &DataAcid,      flash: Option<MyResult<String>>,      name: String,  ) -> MyResult<DynLayoutPage<'a>> { -    let user = database -        .user -        .get(&name)? +    let user = T_USER +        .get(&database, &*name)?          .ok_or(anyhow!("user does not exist"))?;      let flash = flash.map(|f| f.map_err(|e| format!("{e:?}"))); @@ -140,26 +152,31 @@ pub enum GrantState {  #[post("/admin/update_user_permission", data = "<form>")]  pub fn r_admin_user_permission(      session: AdminSession, -    database: &State<Database>, +    database: &State<DataAcid>,      form: Form<UserPermissionForm>,  ) -> MyResult<DynLayoutPage<'static>> {      drop(session);      let perm = serde_json::from_str::<UserPermission>(&form.permission)          .context("parsing provided permission")?; -    database -        .user -        .update_and_fetch(&form.name, |user| { -            user.map(|mut user| { -                match form.action { -                    GrantState::Grant => drop(user.permissions.0.insert(perm.clone(), true)), -                    GrantState::Revoke => drop(user.permissions.0.insert(perm.clone(), false)), -                    GrantState::Unset => drop(user.permissions.0.remove(&perm)), -                } -                user -            }) -        })? -        .ok_or(anyhow!("user did not exist"))?; +    let txn = database.begin_write()?; +    let mut users = txn.open_table(T_USER)?; + +    let mut user = users +        .get(&*form.name)? +        .ok_or(anyhow!("user missing"))? +        .value() +        .0; + +    match form.action { +        GrantState::Grant => drop(user.permissions.0.insert(perm.clone(), true)), +        GrantState::Revoke => drop(user.permissions.0.insert(perm.clone(), false)), +        GrantState::Unset => drop(user.permissions.0.remove(&perm)), +    } + +    users.insert(&*form.name, Ser(user))?; +    drop(users); +    txn.commit()?;      manage_single_user(          database, @@ -171,13 +188,12 @@ pub fn r_admin_user_permission(  #[post("/admin/remove_user", data = "<form>")]  pub fn r_admin_remove_user(      session: AdminSession, -    database: &State<Database>, +    database: &State<DataAcid>,      form: Form<DeleteUser>,  ) -> MyResult<DynLayoutPage<'static>> {      drop(session); -    database -        .user -        .remove(&form.name)? +    T_USER +        .remove(&database, form.name.as_str())?          .ok_or(anyhow!("user did not exist"))?;      user_management(database, Some(Ok("User removed".into())))  } diff --git a/server/src/routes/ui/assets.rs b/server/src/routes/ui/assets.rs index ddbc2ee..b1a13da 100644 --- a/server/src/routes/ui/assets.rs +++ b/server/src/routes/ui/assets.rs @@ -4,12 +4,15 @@      Copyright (C) 2023 metamuffin <metamuffin.org>  */  use crate::{ -    database::Database, +    database::DataAcid,      routes::ui::{account::session::Session, error::MyResult, CacheControlFile},  };  use anyhow::{anyhow, Context};  use jellybase::{ -    cache::async_cache_file, federation::Federation, permission::NodePermissionExt, +    cache::async_cache_file, +    database::{TableExt, T_NODE}, +    federation::Federation, +    permission::NodePermissionExt,      AssetLocationExt,  };  pub use jellycommon::AssetRole; @@ -22,14 +25,13 @@ use tokio::fs::File;  #[get("/n/<id>/asset?<role>&<width>")]  pub async fn r_item_assets(      session: Session, -    db: &State<Database>, +    db: &State<DataAcid>,      id: &str,      role: AssetRole,      width: Option<usize>,  ) -> MyResult<(ContentType, CacheControlFile)> { -    let node = db -        .node -        .get(&id.to_string())? +    let node = T_NODE +        .get(&db, id)?          .only_if_permitted(&session.user.permissions)          .ok_or(anyhow!("node does not exist"))?;      let mut asset = match role { @@ -38,7 +40,9 @@ pub async fn r_item_assets(      };      if let None = asset {          if let Some(parent) = &node.public.path.last() { -            let parent = db.node.get(parent)?.ok_or(anyhow!("node does not exist"))?; +            let parent = T_NODE +                .get(&db, parent.as_str())? +                .ok_or(anyhow!("node does not exist"))?;              asset = match role {                  AssetRole::Backdrop => parent.private.backdrop,                  AssetRole::Poster => parent.private.poster, @@ -55,15 +59,14 @@ pub async fn r_item_assets(  #[get("/n/<id>/thumbnail?<t>&<width>")]  pub async fn r_node_thumbnail(      session: Session, -    db: &State<Database>, +    db: &State<DataAcid>,      fed: &State<Federation>,      id: &str,      t: f64,      width: Option<usize>,  ) -> MyResult<(ContentType, CacheControlFile)> { -    let node = db -        .node -        .get(&id.to_string())? +    let node = T_NODE +        .get(&db, id)?          .only_if_permitted(&session.user.permissions)          .ok_or(anyhow!("node does not exist"))?; diff --git a/server/src/routes/ui/browser.rs b/server/src/routes/ui/browser.rs index 509f242..e811516 100644 --- a/server/src/routes/ui/browser.rs +++ b/server/src/routes/ui/browser.rs @@ -10,9 +10,8 @@ use super::{      node::NodeCard,      sort::{filter_and_sort_nodes, NodeFilterSort, NodeFilterSortForm},  }; -use crate::{database::Database, uri}; -use anyhow::Context; -use jellycommon::{user::NodeUserData, NodePublic}; +use crate::{database::DataAcid, uri}; +use jellybase::database::{ReadableTable, T_NODE, T_USER_NODE};  use rocket::{get, State};  /// This function is a stub and only useful for use in the uri! macro. @@ -22,25 +21,31 @@ pub fn r_all_items() {}  #[get("/items?<page>&<filter..>")]  pub fn r_all_items_filter(      sess: Session, -    db: &State<Database>, +    db: &State<DataAcid>,      page: Option<usize>,      filter: NodeFilterSort,  ) -> Result<DynLayoutPage<'_>, MyError> { -    let mut items = db -        .node -        .iter() -        .map(|e| { -            let (i, n) = e.context("listing")?; -            let u = db -                .user_node -                .get(&(sess.user.name.clone(), i.clone()))? -                .unwrap_or_default(); -            Ok((i, n, u)) -        }) -        .collect::<anyhow::Result<Vec<_>>>()? -        .into_iter() -        .map(|(k, n, u)| (k, n.public, u)) -        .collect::<Vec<(String, NodePublic, NodeUserData)>>(); +    let mut items = { +        let txn = db.begin_read()?; +        let nodes = txn.open_table(T_NODE)?; +        let node_users = txn.open_table(T_USER_NODE)?; +        let i = nodes +            .iter()? +            .map(|a| { +                let (x, y) = a.unwrap(); +                let (x, y) = (x.value().to_owned(), y.value().0); +                let z = node_users +                    .get(&(sess.user.name.as_str(), x.as_str())) +                    .unwrap() +                    .map(|z| z.value().0) +                    .unwrap_or_default(); +                let y = y.public; +                (x, y, z) +            }) +            .collect::<Vec<_>>(); +        drop(nodes); +        i +    };      filter_and_sort_nodes(&filter, &mut items); diff --git a/server/src/routes/ui/error.rs b/server/src/routes/ui/error.rs index 07a6bed..98c6b7f 100644 --- a/server/src/routes/ui/error.rs +++ b/server/src/routes/ui/error.rs @@ -5,7 +5,7 @@  */  use super::layout::{DynLayoutPage, LayoutPage};  use crate::{routes::ui::account::rocket_uri_macro_r_account_login, uri}; -use jellybase::{database::sled, AssetLocationExt}; +use jellybase::AssetLocationExt;  use jellycommon::AssetLocation;  use log::info;  use rocket::{ @@ -96,13 +96,43 @@ impl From<std::io::Error> for MyError {          MyError(anyhow::anyhow!("{err}"))      }  } -impl From<sled::Error> for MyError { -    fn from(err: sled::Error) -> Self { +impl From<serde_json::Error> for MyError { +    fn from(err: serde_json::Error) -> Self {          MyError(anyhow::anyhow!("{err}"))      }  } -impl From<serde_json::Error> for MyError { -    fn from(err: serde_json::Error) -> Self { +impl From<jellybase::database::CommitError> for MyError { +    fn from(err: jellybase::database::CommitError) -> Self { +        MyError(anyhow::anyhow!("{err}")) +    } +} +impl From<jellybase::database::CompactionError> for MyError { +    fn from(err: jellybase::database::CompactionError) -> Self { +        MyError(anyhow::anyhow!("{err}")) +    } +} +impl From<jellybase::database::DatabaseError> for MyError { +    fn from(err: jellybase::database::DatabaseError) -> Self { +        MyError(anyhow::anyhow!("{err}")) +    } +} +impl From<jellybase::database::SavepointError> for MyError { +    fn from(err: jellybase::database::SavepointError) -> Self { +        MyError(anyhow::anyhow!("{err}")) +    } +} +impl From<jellybase::database::StorageError> for MyError { +    fn from(err: jellybase::database::StorageError) -> Self { +        MyError(anyhow::anyhow!("{err}")) +    } +} +impl From<jellybase::database::TableError> for MyError { +    fn from(err: jellybase::database::TableError) -> Self { +        MyError(anyhow::anyhow!("{err}")) +    } +} +impl From<jellybase::database::TransactionError> for MyError { +    fn from(err: jellybase::database::TransactionError) -> Self {          MyError(anyhow::anyhow!("{err}"))      }  } diff --git a/server/src/routes/ui/home.rs b/server/src/routes/ui/home.rs index d332447..9a00532 100644 --- a/server/src/routes/ui/home.rs +++ b/server/src/routes/ui/home.rs @@ -9,44 +9,48 @@ use super::{      node::{DatabaseNodeUserDataExt, NodeCard},  };  use crate::{ -    database::Database, +    database::DataAcid,      routes::ui::{error::MyResult, layout::DynLayoutPage},  };  use anyhow::Context;  use chrono::{Datelike, Utc}; -use jellybase::CONF; -use jellycommon::{ -    user::{NodeUserData, WatchedState}, -    NodePublic, +use jellybase::{ +    database::{ReadableTable, TableExt, T_NODE, T_USER_NODE}, +    CONF,  }; +use jellycommon::user::WatchedState;  use rocket::{get, State};  use tokio::fs::read_to_string;  #[get("/")] -pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { -    let mut items = db -        .node -        .iter() -        .map(|e| { -            let (i, n) = e.context("listing")?; -            let u = db -                .user_node -                .get(&(sess.user.name.clone(), i.clone()))? -                .unwrap_or_default(); -            Ok((i, n, u)) -        }) -        .collect::<anyhow::Result<Vec<_>>>()? -        .into_iter() -        .map(|(k, n, u)| (k, n.public, u)) -        .collect::<Vec<(String, NodePublic, NodeUserData)>>(); - +pub fn r_home(sess: Session, db: &State<DataAcid>) -> MyResult<DynLayoutPage> { +    let mut items = { +        let txn = db.begin_read()?; +        let nodes = txn.open_table(T_NODE)?; +        let node_users = txn.open_table(T_USER_NODE)?; +        let i = nodes +            .iter()? +            .map(|a| { +                let (x, y) = a.unwrap(); +                let (x, y) = (x.value().to_owned(), y.value().0); +                let z = node_users +                    .get(&(sess.user.name.as_str(), x.as_str())) +                    .unwrap() +                    .map(|z| z.value().0) +                    .unwrap_or_default(); +                let y = y.public; +                (x, y, z) +            }) +            .collect::<Vec<_>>(); +        drop(nodes); +        i +    };      let random = (0..16)          .flat_map(|i| Some(items[cheap_daily_random(i).checked_rem(items.len())?].clone()))          .collect::<Vec<_>>(); -    let toplevel = db -        .node -        .get(&"library".to_string())? +    let toplevel = T_NODE +        .get(&db, "library")?          .context("root node missing")?          .public          .children @@ -56,11 +60,7 @@ pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> {          .into_iter()          .collect::<Vec<_>>(); -    items.sort_by_key(|(_, n, _)| { -        n.release_date -            .map(|d| -d.naive_utc().timestamp()) -            .unwrap_or(i64::MAX) -    }); +    items.sort_by_key(|(_, n, _)| n.release_date.map(|d| -d).unwrap_or(i64::MAX));      let latest = items          .iter() @@ -73,7 +73,7 @@ pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> {          .filter(|(_, _, u)| matches!(u.watched, WatchedState::Progress(_)))          .map(|k| k.to_owned())          .collect::<Vec<_>>(); -     +      let watchlist = items          .iter()          .filter(|(_, _, u)| matches!(u.watched, WatchedState::Pending)) diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs index 0dbb027..c055953 100644 --- a/server/src/routes/ui/node.rs +++ b/server/src/routes/ui/node.rs @@ -9,7 +9,7 @@ use super::{      sort::{filter_and_sort_nodes, NodeFilterSort, NodeFilterSortForm},  };  use crate::{ -    database::Database, +    database::DataAcid,      routes::{          api::AcceptJson,          ui::{ @@ -22,8 +22,12 @@ use crate::{      },      uri,  }; -use anyhow::{anyhow, Context, Result}; -use jellybase::permission::NodePermissionExt; +use anyhow::{anyhow, Result}; +use chrono::NaiveDateTime; +use jellybase::{ +    database::{TableExt, T_NODE, T_USER_NODE}, +    permission::NodePermissionExt, +};  use jellycommon::{      user::{NodeUserData, WatchedState},      Chapter, MediaInfo, NodeKind, NodePublic, Rating, SourceTrackKind, @@ -40,21 +44,18 @@ pub fn r_library_node(id: String) {  pub async fn r_library_node_filter<'a>(      session: Session,      id: &'a str, -    db: &'a State<Database>, +    db: &'a State<DataAcid>,      aj: AcceptJson,      filter: NodeFilterSort,  ) -> Result<Either<DynLayoutPage<'a>, Json<NodePublic>>, MyError> { -    let node = db -        .node -        .get(&id.to_string()) -        .context("retrieving library node")? +    let node = T_NODE +        .get(&db, id)?          .only_if_permitted(&session.user.permissions)          .ok_or(anyhow!("node does not exist"))?          .public; -    let udata = db -        .user_node -        .get(&(session.user.name.clone(), id.to_string()))? +    let udata = T_USER_NODE +        .get(&db, &(session.user.name.as_str(), id))?          .unwrap_or_default();      if *aj { @@ -192,7 +193,7 @@ markup::define! {                  p { @m.resolution_name() }              }              @if let Some(d) = &node.release_date { -                p { @d.format("%Y-%m-%d").to_string() } +                p { @NaiveDateTime::from_timestamp_millis(*d).unwrap().and_utc().to_string() }              }              @if !node.children.is_empty() {                  p { @format!("{} items", node.children.len()) } @@ -244,7 +245,7 @@ pub trait DatabaseNodeUserDataExt {          session: &Session,      ) -> Result<(String, NodePublic, NodeUserData)>;  } -impl DatabaseNodeUserDataExt for Database { +impl DatabaseNodeUserDataExt for DataAcid {      fn get_node_with_userdata(          &self,          id: &str, @@ -252,12 +253,12 @@ impl DatabaseNodeUserDataExt for Database {      ) -> Result<(String, NodePublic, NodeUserData)> {          Ok((              id.to_owned(), -            self.node -                .get(&id.to_owned())? +            T_NODE +                .get(self, id)?                  .ok_or(anyhow!("node does not exist: {id}"))?                  .public, -            self.user_node -                .get(&(session.user.name.to_owned(), id.to_owned()))? +            T_USER_NODE +                .get(self, &(session.user.name.as_str(), id))?                  .unwrap_or_default(),          ))      } diff --git a/server/src/routes/ui/player.rs b/server/src/routes/ui/player.rs index e3c5cb2..62f014c 100644 --- a/server/src/routes/ui/player.rs +++ b/server/src/routes/ui/player.rs @@ -5,7 +5,7 @@  */  use super::{account::session::Session, layout::LayoutPage};  use crate::{ -    database::Database, +    database::DataAcid,      routes::{          stream::rocket_uri_macro_r_stream,          ui::{ @@ -17,6 +17,7 @@ use crate::{      uri,  };  use anyhow::anyhow; +use jellybase::database::{TableExt, T_NODE};  use jellycommon::{      stream::{StreamFormat, StreamSpec},      Node, SourceTrackKind, TrackID, @@ -44,14 +45,11 @@ impl PlayerConfig {  #[get("/n/<id>/player?<conf..>", rank = 4)]  pub fn r_player<'a>(      _sess: Session, -    db: &'a State<Database>, +    db: &'a State<DataAcid>,      id: &'a str,      conf: PlayerConfig,  ) -> MyResult<DynLayoutPage<'a>> { -    let item = db -        .node -        .get(&id.to_string())? -        .ok_or(anyhow!("node does not exist"))?; +    let item = T_NODE.get(db, id)?.ok_or(anyhow!("node does not exist"))?;      let spec = StreamSpec {          tracks: None diff --git a/server/src/routes/userdata.rs b/server/src/routes/userdata.rs index 2ded24a..8803bde 100644 --- a/server/src/routes/userdata.rs +++ b/server/src/routes/userdata.rs @@ -3,10 +3,10 @@      which is licensed under the GNU Affero General Public License (version 3); see /COPYING.      Copyright (C) 2023 metamuffin <metamuffin.org>  */ -use super::ui::{account::session::Session, error::MyResult}; +use super::ui::{account::session::Session, error::MyResult, node::DatabaseNodeUserDataExt};  use crate::routes::ui::node::rocket_uri_macro_r_library_node;  use anyhow::anyhow; -use jellybase::database::Database; +use jellybase::database::{DataAcid, ReadableTable, Ser, TableExt, T_NODE, T_USER_NODE};  use jellycommon::user::{NodeUserData, WatchedState};  use rocket::{      get, post, response::Redirect, serde::json::Json, FromFormField, State, UriDisplayQuery, @@ -22,38 +22,41 @@ pub enum UrlWatchedState {  #[get("/n/<id>/userdata")]  pub fn r_node_userdata(      session: Session, -    db: &State<Database>, +    db: &State<DataAcid>,      id: &str,  ) -> MyResult<Json<NodeUserData>> { -    db.node -        .get(&id.to_string())? -        .ok_or(anyhow!("node does not exist"))?; -    let key = (session.user.name.clone(), id.to_owned()); -    Ok(Json(db.user_node.get(&key)?.unwrap_or_default())) +    let (_, _, u) = db.get_node_with_userdata(id, &session)?; +    Ok(Json(u))  }  #[post("/n/<id>/watched?<state>")]  pub async fn r_player_watched(      session: Session, -    db: &State<Database>, +    db: &State<DataAcid>,      id: &str,      state: UrlWatchedState,  ) -> MyResult<Redirect> { -    db.node -        .get(&id.to_string())? -        .ok_or(anyhow!("node does not exist"))?; +    T_NODE.get(db, id)?.ok_or(anyhow!("node does not exist"))?; -    let key = (session.user.name.clone(), id.to_owned()); +    // let key = (session.user.name.clone(), id.to_owned()); -    db.user_node.fetch_and_update(&key, |t| { -        let mut t = t.unwrap_or_default(); -        t.watched = match state { -            UrlWatchedState::None => WatchedState::None, -            UrlWatchedState::Watched => WatchedState::Watched, -            UrlWatchedState::Pending => WatchedState::Pending, -        }; -        Some(t) -    })?; +    let txn = db.begin_write()?; +    let mut user_nodes = txn.open_table(T_USER_NODE)?; + +    let mut udata = user_nodes +        .get((session.user.name.as_str(), id))? +        .map(|x| x.value().0) +        .unwrap_or_default(); + +    udata.watched = match state { +        UrlWatchedState::None => WatchedState::None, +        UrlWatchedState::Watched => WatchedState::Watched, +        UrlWatchedState::Pending => WatchedState::Pending, +    }; + +    user_nodes.insert((session.user.name.as_str(), id), Ser(udata))?; +    drop(user_nodes); +    txn.commit()?;      Ok(Redirect::found(rocket::uri!(r_library_node(id))))  } @@ -61,24 +64,30 @@ pub async fn r_player_watched(  #[post("/n/<id>/progress?<t>")]  pub async fn r_player_progress(      session: Session, -    db: &State<Database>, +    db: &State<DataAcid>,      id: &str,      t: f64,  ) -> MyResult<()> { -    db.node -        .get(&id.to_string())? -        .ok_or(anyhow!("node does not exist"))?; +    T_NODE.get(db, id)?.ok_or(anyhow!("node does not exist"))?; + +    let txn = db.begin_write()?; +    let mut user_nodes = txn.open_table(T_USER_NODE)?; + +    let mut udata = user_nodes +        .get((session.user.name.as_str(), id))? +        .map(|x| x.value().0) +        .unwrap_or_default(); + +    udata.watched = match udata.watched { +        WatchedState::None | WatchedState::Pending | WatchedState::Progress(_) => { +            WatchedState::Progress(t) +        } +        WatchedState::Watched => WatchedState::Watched, +    }; + +    user_nodes.insert((session.user.name.as_str(), id), Ser(udata))?; +    drop(user_nodes); +    txn.commit()?; -    let key = (session.user.name.clone(), id.to_owned()); -    db.user_node.fetch_and_update(&key, |d| { -        let mut d = d.unwrap_or_default(); -        d.watched = match d.watched { -            WatchedState::None | WatchedState::Pending | WatchedState::Progress(_) => { -                WatchedState::Progress(t) -            } -            WatchedState::Watched => WatchedState::Watched, -        }; -        Some(d) -    })?;      Ok(())  } | 
