aboutsummaryrefslogtreecommitdiff
path: root/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'server/src')
-rw-r--r--server/src/main.rs31
-rw-r--r--server/src/routes/api/mod.rs12
-rw-r--r--server/src/routes/mod.rs4
-rw-r--r--server/src/routes/stream.rs10
-rw-r--r--server/src/routes/ui/account/mod.rs71
-rw-r--r--server/src/routes/ui/account/session/guard.rs18
-rw-r--r--server/src/routes/ui/account/settings.rs53
-rw-r--r--server/src/routes/ui/admin/mod.rs45
-rw-r--r--server/src/routes/ui/admin/user.rs70
-rw-r--r--server/src/routes/ui/assets.rs25
-rw-r--r--server/src/routes/ui/browser.rs43
-rw-r--r--server/src/routes/ui/error.rs40
-rw-r--r--server/src/routes/ui/home.rs62
-rw-r--r--server/src/routes/ui/node.rs35
-rw-r--r--server/src/routes/ui/player.rs10
-rw-r--r--server/src/routes/userdata.rs83
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(())
}