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 | |
parent | 1c37d32a0985ff7390313833345b9299f9f0b196 (diff) | |
download | jellything-46c251655db7bb3d9aa814b1a5dde85336b0b9b1.tar jellything-46c251655db7bb3d9aa814b1a5dde85336b0b9b1.tar.bz2 jellything-46c251655db7bb3d9aa814b1a5dde85336b0b9b1.tar.zst |
replace sled with redb
Diffstat (limited to 'server/src')
-rw-r--r-- | server/src/main.rs | 31 | ||||
-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 |
16 files changed, 367 insertions, 245 deletions
diff --git a/server/src/main.rs b/server/src/main.rs index acb8c87..6862a98 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -8,10 +8,14 @@ #![feature(let_chains)] use crate::routes::ui::{account::hash_password, admin::log::enable_logging}; -use database::Database; -use jellybase::{federation::Federation, CONF}; +use database::DataAcid; +use jellybase::{ + database::{ReadableTable, Ser, T_USER}, + federation::Federation, + CONF, +}; use jellycommon::user::{PermissionSet, Theme, User}; -use log::{error, warn, info}; +use log::{error, info, warn}; use routes::build_rocket; use tokio::fs::create_dir_all; @@ -25,16 +29,20 @@ async fn main() { log::warn!("authentification bypass enabled"); create_dir_all(&CONF.cache_path).await.unwrap(); - let database = Database::open(&CONF.database_path).unwrap(); + let database = DataAcid::open(&CONF.database_path).unwrap(); let federation = Federation::initialize(); if let Some(username) = &CONF.admin_username && let Some(password) = &CONF.admin_password { - database - .user - .fetch_and_update(&username, |admin| { - Some(User { + let txn = database.begin_write().unwrap(); + let mut users = txn.open_table(T_USER).unwrap(); + + let admin = users.get(username.as_str()).unwrap().map(|x| x.value().0); + users + .insert( + username.as_str(), + Ser(User { admin: true, name: username.clone(), password: hash_password(&username, &password), @@ -46,9 +54,12 @@ async fn main() { theme: Theme::Dark, permissions: PermissionSet::default(), }) - }) - }) + }), + ) .unwrap(); + + drop(users); + txn.commit().unwrap(); } else { info!("admin account disabled") } 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(()) } |