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 | |
parent | 1c37d32a0985ff7390313833345b9299f9f0b196 (diff) | |
download | jellything-46c251655db7bb3d9aa814b1a5dde85336b0b9b1.tar jellything-46c251655db7bb3d9aa814b1a5dde85336b0b9b1.tar.bz2 jellything-46c251655db7bb3d9aa814b1a5dde85336b0b9b1.tar.zst |
replace sled with redb
26 files changed, 1025 insertions, 553 deletions
@@ -159,7 +159,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -191,7 +191,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -213,7 +213,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -224,7 +224,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -308,15 +308,6 @@ checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" [[package]] name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bincode" version = "2.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95" @@ -503,7 +494,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -694,7 +685,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -713,7 +704,7 @@ name = "ebml_derive" version = "0.1.0" dependencies = [ "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -861,16 +852,6 @@ dependencies = [ ] [[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] name = "futures" version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -926,7 +907,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -960,15 +941,6 @@ dependencies = [ ] [[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] name = "generator" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1305,7 +1277,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -1346,17 +1318,17 @@ version = "0.1.0" dependencies = [ "anyhow", "base64", - "bincode 2.0.0-rc.3", + "bincode", "jellyclient", "jellycommon", "log", "rand", + "redb", "serde", + "serde_json", "serde_yaml", "sha2", - "sled", "tokio", - "typed-sled", ] [[package]] @@ -1376,7 +1348,7 @@ dependencies = [ name = "jellycommon" version = "0.1.0" dependencies = [ - "bincode 2.0.0-rc.3", + "bincode", "chrono", "rocket", "serde", @@ -1388,7 +1360,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-recursion", - "bincode 2.0.0-rc.3", + "bincode", "futures", "jellybase", "jellyclient", @@ -1419,7 +1391,7 @@ name = "jellyremuxer" version = "0.1.0" dependencies = [ "anyhow", - "bincode 2.0.0-rc.3", + "bincode", "jellybase", "jellycommon", "jellymatroska", @@ -1452,7 +1424,7 @@ dependencies = [ "argon2", "async-recursion", "base64", - "bincode 2.0.0-rc.3", + "bincode", "chrono", "env_logger", "futures", @@ -1479,7 +1451,7 @@ version = "0.1.0" dependencies = [ "anyhow", "base64", - "bincode 2.0.0-rc.3", + "bincode", "clap", "env_logger", "indicatif", @@ -1671,7 +1643,7 @@ checksum = "9ab6ee21fd1855134cacf2f41afdf45f1bc456c7d7f6165d763b4647062dd2be" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -1850,7 +1822,7 @@ checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -1944,7 +1916,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -1973,37 +1945,12 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - -[[package]] -name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.9", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", + "parking_lot_core", ] [[package]] @@ -2014,7 +1961,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall", "smallvec", "windows-targets 0.48.5", ] @@ -2056,7 +2003,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -2066,26 +2013,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] -name = "pin-project" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.41", -] - -[[package]] name = "pin-project-lite" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2148,9 +2075,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] @@ -2163,7 +2090,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", "version_check", "yansi", ] @@ -2185,9 +2112,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2296,12 +2223,12 @@ dependencies = [ ] [[package]] -name = "redox_syscall" -version = "0.2.16" +name = "redb" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "72623e6275cd430215b741f41ebda34db93a13ebde253f908b70871c46afc5ba" dependencies = [ - "bitflags 1.3.2", + "libc", ] [[package]] @@ -2330,7 +2257,7 @@ checksum = "2566c4bf6845f2c2e83b27043c3f5dfcd5ba8f2937d6c00dc009bfb51a079dc4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -2442,7 +2369,7 @@ dependencies = [ "memchr", "multer", "num_cpus", - "parking_lot 0.12.1", + "parking_lot", "pin-project-lite", "rand", "ref-cast", @@ -2472,7 +2399,7 @@ dependencies = [ "proc-macro2", "quote", "rocket_http", - "syn 2.0.41", + "syn 2.0.48", "unicode-xid", ] @@ -2619,29 +2546,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -2736,22 +2663,6 @@ dependencies = [ ] [[package]] -name = "sled" -version = "0.34.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot 0.11.2", -] - -[[package]] name = "smallvec" version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2829,9 +2740,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.41" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -2886,7 +2797,7 @@ checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.4.1", + "redox_syscall", "rustix", "windows-sys 0.48.0", ] @@ -2917,7 +2828,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -2996,7 +2907,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot 0.12.1", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2 0.5.5", @@ -3012,7 +2923,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -3118,7 +3029,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -3167,19 +3078,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "typed-sled" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1060f05a4450ec5b758da60951b04f225a93a62079316630e76cf25c4034500d" -dependencies = [ - "bincode 1.3.3", - "pin-project", - "serde", - "sled", - "thiserror", -] - -[[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3370,7 +3268,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -3404,7 +3302,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/base/Cargo.toml b/base/Cargo.toml index ec5cbe7..36b93bc 100644 --- a/base/Cargo.toml +++ b/base/Cargo.toml @@ -14,6 +14,9 @@ base64 = "0.21.5" tokio = { workspace = true } anyhow = "1.0.75" bincode = "2.0.0-rc.3" -sled = "0.34.7" -typed-sled = "0.2.3" rand = "0.8.5" +redb = "1.5.0" +serde_json = "1.0.111" + +[features] +db_json = [] diff --git a/base/src/database.rs b/base/src/database.rs index 3f81cca..2a57937 100644 --- a/base/src/database.rs +++ b/base/src/database.rs @@ -3,43 +3,193 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2023 metamuffin <metamuffin.org> */ -use anyhow::Context; +use bincode::{Decode, Encode}; use jellycommon::{ user::{NodeUserData, User}, Node, }; use log::info; -use std::path::Path; -use typed_sled::Tree; +use std::{borrow::Borrow, ops::Deref, path::Path}; -pub use sled; -pub use typed_sled; +pub use redb::*; -pub struct Database { - pub db: sled::Db, +pub const T_USER: TableDefinition<&str, Ser<User>> = TableDefinition::new("user"); +pub const T_USER_NODE: TableDefinition<(&str, &str), Ser<NodeUserData>> = + TableDefinition::new("user_node"); +pub const T_INVITE: TableDefinition<&str, Ser<()>> = TableDefinition::new("invite"); +pub const T_NODE: TableDefinition<&str, Ser<Node>> = TableDefinition::new("node"); +pub const T_NODE_IMPORT: TableDefinition<&str, Ser<Vec<(Vec<usize>, Node)>>> = + TableDefinition::new("node_import"); - pub user: Tree<String, User>, - pub user_node: Tree<(String, String), NodeUserData>, - pub invite: Tree<String, ()>, - pub node: Tree<String, Node>, - - pub node_import: Tree<String, Vec<(Vec<usize>, Node)>>, +pub struct DataAcid { + pub inner: redb::Database, } -impl Database { +impl DataAcid { pub fn open(path: &Path) -> Result<Self, anyhow::Error> { - info!("opening database… (might take up to O(n) time)"); - let db = sled::open(path).context("opening database")?; - info!("creating trees"); - let r = Ok(Self { - user: Tree::open(&db, "user"), - invite: Tree::open(&db, "invite"), - node: Tree::open(&db, "node"), - user_node: Tree::open(&db, "user_node"), - node_import: Tree::open(&db, "node_import"), - db, - }); + info!("database"); + let db = redb::Database::create(path)?; + let r = Self { inner: db }; + + { + let txn = r.begin_write()?; + drop(txn.open_table(T_INVITE)?); + drop(txn.open_table(T_USER)?); + drop(txn.open_table(T_USER_NODE)?); + drop(txn.open_table(T_NODE)?); + drop(txn.open_table(T_NODE_IMPORT)?); + txn.commit()?; + } + info!("ready"); - r + Ok(r) + } +} + +impl Deref for DataAcid { + type Target = Database; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +pub trait TableExt<Key, KeyRef, Value> { + fn get(self, db: &DataAcid, key: KeyRef) -> anyhow::Result<Option<Value>>; + fn insert(self, db: &DataAcid, key: KeyRef, value: Value) -> anyhow::Result<()>; + fn remove(self, db: &DataAcid, key: KeyRef) -> anyhow::Result<Option<Value>>; +} +impl<'a, 'b, 'c, Key, Value, KeyRef> TableExt<Key, KeyRef, Value> + for TableDefinition<'a, Key, Ser<Value>> +where + Key: Borrow<<Key as RedbValue>::SelfType<'b>> + redb::RedbKey, + Value: bincode::Encode + bincode::Decode + std::fmt::Debug, + KeyRef: Borrow<<Key as redb::RedbValue>::SelfType<'c>>, +{ + fn get(self, db: &DataAcid, key: KeyRef) -> anyhow::Result<Option<Value>> { + let txn = db.inner.begin_read()?; + let table = txn.open_table(self)?; + let user = table.get(key)?.map(|v| v.value().0); + drop(table); + Ok(user) + } + fn insert(self, db: &DataAcid, key: KeyRef, value: Value) -> anyhow::Result<()> { + let txn = db.inner.begin_write()?; + let mut table = txn.open_table(self)?; + table.insert(key, Ser(value))?; + drop(table); + txn.commit()?; + Ok(()) + } + fn remove(self, db: &DataAcid, key: KeyRef) -> anyhow::Result<Option<Value>> { + let txn = db.inner.begin_write()?; + let mut table = txn.open_table(self)?; + let prev = table.remove(key)?.map(|v| v.value().0); + drop(table); + txn.commit()?; + Ok(prev) + } +} + +// pub trait TableIterExt< +// 'a, +// Key: redb::RedbKey + 'static, +// Value: redb::RedbValue + 'static, +// F: FnOnce(&redb::Range<'a, Key, Value>) -> anyhow::Result<T>, +// T: 'static, +// > +// { +// fn iter(self, db: &'a DataAcid, f: F) -> anyhow::Result<T>; +// } +// impl<'a, Key, Value, F, T> TableIterExt<'a, Key, Value, F, T> +// for TableDefinition<'static, Key, Value> +// where +// Key: redb::RedbKey, +// Value: redb::RedbValue, +// F: FnOnce(&redb::Range<'a, Key, Value>) -> anyhow::Result<T>, +// T: 'static, +// { +// fn iter(self, db: &DataAcid, f: F) -> anyhow::Result<T> { +// let txn = db.begin_read()?; +// let table = txn.open_table(self)?; +// let iter = table.iter()?; +// let ret = f(&iter)?; +// drop(iter); +// drop(table); +// drop(txn); +// Ok(ret) +// } +// } + +#[derive(Debug)] +#[cfg(not(feature = "db_json"))] +pub struct Ser<T>(pub T); +#[cfg(not(feature = "db_json"))] +impl<T: Encode + Decode + std::fmt::Debug> RedbValue for Ser<T> { + type SelfType<'a> = Ser<T> + where + Self: 'a; + type AsBytes<'a> = Vec<u8> + where + Self: 'a; + + fn fixed_width() -> Option<usize> { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + Ser(bincode::decode_from_slice(data, bincode::config::legacy()) + .unwrap() + .0) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'a, + Self: 'b, + { + bincode::encode_to_vec(&value.0, bincode::config::legacy()).unwrap() + } + + fn type_name() -> redb::TypeName { + TypeName::new("bincode") + } +} + +#[derive(Debug)] +#[cfg(feature = "db_json")] +pub struct Ser<T>(pub T); +#[cfg(feature = "db_json")] +impl<T: Serialize + for<'a> Deserialize<'a> + std::fmt::Debug> RedbValue for Ser<T> { + type SelfType<'a> = Ser<T> + where + Self: 'a; + type AsBytes<'a> = Vec<u8> + where + Self: 'a; + + fn fixed_width() -> Option<usize> { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + Ser(serde_json::from_slice(data).unwrap()) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'a, + Self: 'b, + { + serde_json::to_vec(&value.0).unwrap() + } + + fn type_name() -> redb::TypeName { + TypeName::new("json") } } diff --git a/common/src/lib.rs b/common/src/lib.rs index c126e65..a58dc48 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -14,14 +14,13 @@ pub mod user; pub use chrono; use bincode::{Decode, Encode}; -use chrono::{DateTime, Utc}; #[cfg(feature = "rocket")] use rocket::{FromFormField, UriDisplayQuery}; use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, path::PathBuf}; -#[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[derive(Debug, Clone, Deserialize, Serialize, Default, Encode, Decode)] pub struct Node { #[serde(default)] pub public: NodePublic, @@ -30,7 +29,7 @@ pub struct Node { } #[rustfmt::skip] -#[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[derive(Debug, Clone, Deserialize, Serialize, Default, Encode,Decode)] pub struct NodePrivate { #[serde(default)] pub id: Option<String>, #[serde(default)] pub poster: Option<AssetLocation>, @@ -39,7 +38,7 @@ pub struct NodePrivate { } #[rustfmt::skip] -#[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[derive(Debug, Clone, Deserialize, Serialize, Default, Encode, Decode)] pub struct NodePublic { #[serde(default)] pub kind: Option<NodeKind>, #[serde(default)] pub title: Option<String>, @@ -48,20 +47,20 @@ pub struct NodePublic { #[serde(default)] pub children: Vec<String>, #[serde(default)] pub tagline: Option<String>, #[serde(default)] pub description: Option<String>, - #[serde(default)] pub release_date: Option<DateTime<Utc>>, + #[serde(default)] pub release_date: Option<i64>, #[serde(default)] pub index: Option<usize>, #[serde(default)] pub media: Option<MediaInfo>, #[serde(default)] pub ratings: BTreeMap<Rating, f64>, #[serde(default)] pub federated: Option<String>, } -#[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[derive(Debug, Clone, Deserialize, Serialize, Default, Encode, Decode)] pub struct ImportOptions { pub id: String, pub sources: Vec<ImportSource>, } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, Encode, Decode)] #[serde(rename_all = "snake_case")] pub enum ImportSource { Override(Node), @@ -85,7 +84,7 @@ pub enum ImportSource { }, } -#[derive(Debug, Clone, Deserialize, Serialize, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Deserialize, Serialize, Hash, PartialEq, Eq, Encode, Decode)] #[serde(rename_all = "snake_case")] pub enum AssetLocation { Cache(PathBuf), @@ -96,7 +95,7 @@ pub enum AssetLocation { } #[rustfmt::skip] -#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default, Encode,Decode)] #[cfg_attr(feature = "rocket", derive(FromFormField, UriDisplayQuery))] #[serde(rename_all = "snake_case")] pub enum NodeKind { @@ -110,18 +109,13 @@ pub enum NodeKind { #[cfg_attr(feature = "rocket", field(value = "episode"))] Episode, } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, Encode, Decode)] #[serde(rename_all = "snake_case")] pub enum TrackSource { Local(LocalTrack), Remote(usize), } -pub enum PublicMediaSource { - Local, - Remote(String), -} - pub type TrackID = usize; #[derive(Debug, Clone, Deserialize, Serialize, Encode, Decode, Hash)] @@ -131,7 +125,7 @@ pub struct LocalTrack { pub codec_private: Option<Vec<u8>>, } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, Encode, Decode)] pub struct MediaInfo { pub duration: f64, // in seconds pub tracks: Vec<SourceTrack>, @@ -157,7 +151,9 @@ pub struct SourceTrack { pub federated: Vec<String>, } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash, Encode, Decode, +)] #[serde(rename_all = "snake_case")] pub enum Rating { Imdb, @@ -186,7 +182,7 @@ pub enum SourceTrackKind { } #[rustfmt::skip] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Encode, Decode)] #[cfg_attr(feature = "rocket", derive(FromFormField, UriDisplayQuery))] pub enum AssetRole { #[cfg_attr(feature = "rocket", field(value = "poster"))] Poster, diff --git a/common/src/stream.rs b/common/src/stream.rs index 4aa51d3..7b56d0e 100644 --- a/common/src/stream.rs +++ b/common/src/stream.rs @@ -1,3 +1,4 @@ +use bincode::{Decode, Encode}; /* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. @@ -18,7 +19,7 @@ pub struct StreamSpec { } #[rustfmt::skip] -#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, Encode, Decode)] #[serde(rename_all = "snake_case")] #[cfg_attr(feature = "rocket", derive(FromFormField, UriDisplayQuery))] pub enum StreamFormat { diff --git a/common/src/user.rs b/common/src/user.rs index 5f012dd..b3360c2 100644 --- a/common/src/user.rs +++ b/common/src/user.rs @@ -4,6 +4,7 @@ Copyright (C) 2023 metamuffin <metamuffin.org> */ use crate::{stream::StreamFormat, user}; +use bincode::{Decode, Encode}; #[cfg(feature = "rocket")] use rocket::{FromFormField, UriDisplayQuery}; use serde::{Deserialize, Serialize}; @@ -12,7 +13,7 @@ use std::{ fmt::Display, }; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] pub struct User { pub name: String, pub display_name: String, @@ -22,12 +23,12 @@ pub struct User { pub permissions: PermissionSet, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] pub struct NodeUserData { pub watched: WatchedState, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode)] #[serde(rename_all = "snake_case")] pub enum WatchedState { None, @@ -44,7 +45,7 @@ pub struct CreateSessionParams { pub drop_permissions: Option<HashSet<UserPermission>>, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Encode, Decode)] #[cfg_attr(feature = "rocket", derive(FromFormField, UriDisplayQuery))] #[serde(rename_all = "snake_case")] pub enum Theme { @@ -61,10 +62,10 @@ impl Theme { ]; } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default, Encode, Decode)] pub struct PermissionSet(pub HashMap<UserPermission, bool>); -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Encode, Decode)] #[serde(rename_all = "snake_case")] pub enum UserPermission { Admin, diff --git a/import/src/infojson.rs b/import/src/infojson.rs index c18c19d..e6b001f 100644 --- a/import/src/infojson.rs +++ b/import/src/infojson.rs @@ -5,7 +5,7 @@ */ use anyhow::Context; -use jellycommon::chrono::{format::Parsed, DateTime, Utc}; +use jellycommon::chrono::{format::Parsed, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -123,7 +123,7 @@ pub struct YHeatmapSample { pub value: f64, } -pub fn parse_upload_date(d: &str) -> anyhow::Result<DateTime<Utc>> { +pub fn parse_upload_date(d: &str) -> anyhow::Result<i64> { let (year, month, day) = (&d[0..4], &d[4..6], &d[6..8]); let (year, month, day) = ( year.parse().context("parsing year")?, @@ -139,5 +139,5 @@ pub fn parse_upload_date(d: &str) -> anyhow::Result<DateTime<Utc>> { p.hour_mod_12 = Some(0); p.minute = Some(0); p.second = Some(0); - Ok(p.to_datetime_with_timezone(&Utc)?) + Ok(p.to_datetime_with_timezone(&Utc)?.timestamp_millis()) } diff --git a/import/src/lib.rs b/import/src/lib.rs index 692cf7f..a4d1611 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -13,7 +13,7 @@ use async_recursion::async_recursion; use futures::{executor::block_on, stream::FuturesUnordered, StreamExt}; use jellybase::{ cache::{async_cache_file, cache_memory}, - database::Database, + database::{DataAcid, ReadableTable, Ser, T_NODE, T_NODE_IMPORT}, federation::Federation, AssetLocationExt, CONF, }; @@ -42,32 +42,54 @@ use tokio::{io::AsyncWriteExt, sync::Semaphore, task::spawn_blocking}; static IMPORT_SEM: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(1)); -pub async fn import(db: &Database, fed: &Federation) -> anyhow::Result<()> { +pub async fn import(db: &DataAcid, fed: &Federation) -> anyhow::Result<()> { let permit = IMPORT_SEM.try_acquire()?; - if !db.node_import.is_empty() { - info!("clearing temporary node tree from an aborted last import..."); - db.node_import.clear()?; + + { + let txn = db.inner.begin_write()?; + let mut table = txn.open_table(T_NODE_IMPORT)?; + if !table.is_empty()? { + info!("clearing temporary node tree from an aborted last import..."); + table.drain::<&str>(..)?; + } + drop(table); + txn.commit()?; } info!("loading sources..."); import_path(CONF.library_path.clone(), vec![], db, fed) .await .context("indexing")?; info!("removing old nodes..."); - db.node.clear()?; + { + let txn = db.inner.begin_write()?; + let mut table = txn.open_table(T_NODE)?; + table.drain::<&str>(..)?; + drop(table); + txn.commit()?; + } info!("merging nodes..."); merge_nodes(db).context("merging nodes")?; info!("generating paths..."); generate_node_paths(db).context("generating paths")?; info!("clearing temporary node tree..."); - db.node_import.clear()?; + { + let txn = db.inner.begin_write()?; + let mut table = txn.open_table(T_NODE_IMPORT)?; + table.drain::<&str>(..)?; + drop(table); + txn.commit()?; + } info!("import completed"); drop(permit); Ok(()) } -pub fn merge_nodes(db: &Database) -> anyhow::Result<()> { - for r in db.node_import.iter() { - let (id, mut nodes) = r?; +pub fn merge_nodes(db: &DataAcid) -> anyhow::Result<()> { + let txn_read = db.inner.begin_read()?; + let t_node_import = txn_read.open_table(T_NODE_IMPORT)?; + for r in t_node_import.iter()? { + let (id, nodes) = r?; + let mut nodes = nodes.value().0; nodes.sort_by(|(x, _), (y, _)| compare_index_path(x, y)); @@ -77,27 +99,36 @@ pub fn merge_nodes(db: &Database) -> anyhow::Result<()> { .reduce(|x, y| merge_node(x, y)) .unwrap(); - node.public.id = Some(id.clone()); + node.public.id = Some(id.value().to_owned()); node.public.path = vec![]; // will be reconstructed in the next pass - db.node.insert(&id, &node)?; + { + let txn_write = db.inner.begin_write()?; + let mut t_node = txn_write.open_table(T_NODE)?; + t_node.insert(id.value(), Ser(node))?; + drop(t_node); + txn_write.commit()?; + } } Ok(()) } -pub fn generate_node_paths(db: &Database) -> anyhow::Result<()> { - fn traverse(db: &Database, c: String, mut path: Vec<String>) -> anyhow::Result<()> { - let node = db - .node - .update_and_fetch(&c, |mut nc| { - if let Some(nc) = &mut nc { - if nc.public.path.is_empty() { - nc.public.path = path.clone(); - } - } - nc - })? - .ok_or(anyhow!("node {c:?} missing"))?; +pub fn generate_node_paths(db: &DataAcid) -> anyhow::Result<()> { + fn traverse(db: &DataAcid, c: String, mut path: Vec<String>) -> anyhow::Result<()> { + let node = { + let txn = db.inner.begin_write()?; + let table = txn.open_table(T_NODE)?; + + let mut node = table.get(&*c)?.ok_or(anyhow!("your mum"))?.value().0; + + if node.public.path.is_empty() { + node.public.path = path.clone(); + } + + drop(table); + txn.commit()?; + node + }; path.push(c); for c in node.public.children { @@ -126,7 +157,7 @@ fn compare_index_path(x: &[usize], y: &[usize]) -> Ordering { pub async fn import_path( path: PathBuf, index_path: Vec<usize>, - db: &Database, + db: &DataAcid, fed: &Federation, ) -> anyhow::Result<()> { if path.is_dir() { @@ -190,15 +221,19 @@ async fn process_source( s: ImportSource, path: &Path, index_path: &[usize], - db: &Database, + db: &DataAcid, fed: &Federation, ) -> anyhow::Result<()> { - let insert_node = move |id: &String, n: Node| -> anyhow::Result<()> { - db.node_import.fetch_and_update(id, |l| { - let mut l = l.unwrap_or_default(); - l.push((index_path.to_vec(), n.clone())); - Some(l) - })?; + let insert_node = move |id: &str, n: Node| -> anyhow::Result<()> { + let txn = db.inner.begin_write()?; + let mut table = txn.open_table(T_NODE_IMPORT)?; + + let mut node = table.get(id)?.map(|a| a.value().0).unwrap_or_default(); + node.push((index_path.to_vec(), n.clone())); + table.insert(id, Ser(node))?; + + drop(table); + txn.commit()?; Ok(()) }; match s { @@ -497,16 +532,20 @@ static SEM_REMOTE_IMPORT: Semaphore = Semaphore::const_new(16); async fn import_remote( id: String, host: &str, - db: &Database, + db: &DataAcid, session: &Arc<Session>, index_path: &[usize], ) -> anyhow::Result<()> { - let insert_node = move |id: &String, n: Node| -> anyhow::Result<()> { - db.node_import.fetch_and_update(id, |l| { - let mut l = l.unwrap_or_default(); - l.push((index_path.to_vec(), n.clone())); - Some(l) - })?; + let insert_node = move |id: &str, n: Node| -> anyhow::Result<()> { + let txn = db.inner.begin_write()?; + let mut table = txn.open_table(T_NODE_IMPORT)?; + + let mut node = table.get(id)?.map(|a| a.value().0).unwrap_or_default(); + node.push((index_path.to_vec(), n.clone())); + table.insert(id, Ser(node))?; + + drop(table); + txn.commit()?; Ok(()) }; let _permit = SEM_REMOTE_IMPORT.acquire().await.unwrap(); diff --git a/import/src/tmdb.rs b/import/src/tmdb.rs index 37447e6..95ebef4 100644 --- a/import/src/tmdb.rs +++ b/import/src/tmdb.rs @@ -5,7 +5,7 @@ */ use anyhow::Context; use bincode::{Decode, Encode}; -use jellycommon::chrono::{format::Parsed, DateTime, Utc}; +use jellycommon::chrono::{format::Parsed, Utc}; use log::info; use serde::Deserialize; @@ -115,7 +115,7 @@ pub async fn tmdb_image(path: &str) -> anyhow::Result<Vec<u8>> { Ok(res.bytes().await?.to_vec()) } -pub fn parse_release_date(d: &str) -> anyhow::Result<DateTime<Utc>> { +pub fn parse_release_date(d: &str) -> anyhow::Result<i64> { let (year, month, day) = (&d[0..4], &d[5..7], &d[8..10]); let (year, month, day) = ( year.parse().context("parsing year")?, @@ -131,5 +131,5 @@ pub fn parse_release_date(d: &str) -> anyhow::Result<DateTime<Utc>> { p.hour_mod_12 = Some(0); p.minute = Some(0); p.second = Some(0); - Ok(p.to_datetime_with_timezone(&Utc)?) + Ok(p.to_datetime_with_timezone(&Utc)?.timestamp_millis()) } 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(()) } diff --git a/tool/src/migrate.rs b/tool/src/migrate.rs index 5037bf4..c774832 100644 --- a/tool/src/migrate.rs +++ b/tool/src/migrate.rs @@ -6,15 +6,89 @@ use crate::{Action, MigrateMode}; use anyhow::{bail, Context}; use indicatif::ProgressIterator; -use jellybase::database::{typed_sled::Tree, Database}; +use jellybase::database::{DataAcid, ReadableTable, Ser, T_INVITE, T_USER, T_USER_NODE}; +use jellycommon::user::{NodeUserData, User}; use log::{info, warn}; -use serde::Serialize; +use std::io::{BufRead, BufReader}; use std::{ fs::File, - io::{BufRead, BufReader, BufWriter, Write}, + io::{BufWriter, Write}, path::Path, }; +// macro_rules! process_tree { +// ($mode:ident, $save_location:ident, $da:ident, $name:literal, $table:ident, $dt:tt) => {{ +// let path = $save_location.join($name); +// match $mode { +// MigrateMode::Export => { +// let mut o = BufWriter::new(File::create(path)?); +// let txn = $da.begin_read()?; +// let table = txn.open_table($table)?; + +// let len = table.len()?; +// for r in table.iter()?.progress_count(len.try_into().unwrap()) { +// let (k, v) = r?; +// serde_json::to_writer(&mut o, &(k.value(), v.value().0))?; +// writeln!(&mut o)?; +// } +// drop(table); +// } +// MigrateMode::Import => { +// { +// let txn = $da.begin_read()?; +// let table = txn.open_table($table)?; +// if !table.is_empty()? { +// bail!("tree not empty, `rm -rf` your db please :)") +// } +// } + +// let Ok(i) = File::open(&path) else { +// warn!("{path:?} does not exist; the import of that tree will be skipped."); +// return Ok(()); +// }; +// let i = BufReader::new(i); +// for l in i.lines() { +// let l = l?; +// let (k, v) = serde_json::from_str::<$dt>(&l).context("reading db dump item")?; +// { +// let txn = $da.begin_write()?; +// let mut table = txn.open_table($table)?; + +// table.insert(&convert(k), Ser(v))?; +// drop(table); +// txn.commit()? +// } +// } +// } +// } +// }}; +// } + +// pub(crate) fn migrate(action: Action) -> anyhow::Result<()> { +// match action { +// Action::Migrate { +// mode, +// save_location, +// database, +// } => { +// std::fs::create_dir_all(&save_location)?; + +// let da = DataAcid::open(&database)?; + +// info!("processing 'user'"); +// process_tree(mode, &save_location.join("user"), &da, T_USER); +// info!("processing 'user_node'"); +// process_tree(mode, &save_location.join("user_node"), &da, T_USER_NODE); +// info!("processing 'invite'"); +// process_tree(mode, &save_location.join("invite"), &da, T_INVITE); +// info!("done"); + +// Ok(()) +// } +// _ => unreachable!(), +// } +// } + pub(crate) fn migrate(action: Action) -> anyhow::Result<()> { match action { Action::Migrate { @@ -23,73 +97,261 @@ pub(crate) fn migrate(action: Action) -> anyhow::Result<()> { database, } => { std::fs::create_dir_all(&save_location)?; - let db = Database::open(&database)?; + + let da = DataAcid::open(&database)?; info!("processing 'user'"); - process_tree(mode, &save_location.join("user"), &db.user)?; + { + let path: &Path = &save_location.join("user"); + let da = &da; + match mode { + MigrateMode::Export => { + let mut o = BufWriter::new(File::create(path)?); + let txn = da.begin_read()?; + let table = txn.open_table(T_USER)?; + + let len = table.len()?; + for r in table.iter()?.progress_count(len.try_into().unwrap()) { + let (k, v) = r?; + serde_json::to_writer(&mut o, &(k.value(), v.value().0))?; + writeln!(&mut o)?; + } + drop(table); + } + MigrateMode::Import => { + { + let txn = da.begin_read()?; + let table = txn.open_table(T_USER)?; + if !table.is_empty()? { + bail!("tree not empty, `rm -rf` your db please :)") + } + } + + let Ok(i) = File::open(path) else { + warn!( + "{path:?} does not exist; the import of that tree will be skipped." + ); + return Ok(()); + }; + let i = BufReader::new(i); + for l in i.lines() { + let l = l?; + let (k, v) = serde_json::from_str::<(String, User)>(l.as_str()) + .context("reading db dump item")?; + { + let txn = da.begin_write()?; + let mut table = txn.open_table(T_USER)?; + table.insert(k.as_str(), Ser(v))?; + drop(table); + txn.commit()? + } + } + } + } + }; info!("processing 'user_node'"); - process_tree(mode, &save_location.join("user_node"), &db.user_node)?; + { + let path: &Path = &save_location.join("user_node"); + let da = &da; + match mode { + MigrateMode::Export => { + let mut o = BufWriter::new(File::create(path)?); + let txn = da.begin_read()?; + let table = txn.open_table(T_USER_NODE)?; + + let len = table.len()?; + for r in table.iter()?.progress_count(len.try_into().unwrap()) { + let (k, v) = r?; + serde_json::to_writer(&mut o, &(k.value(), v.value().0))?; + writeln!(&mut o)?; + } + drop(table); + } + MigrateMode::Import => { + { + let txn = da.begin_read()?; + let table = txn.open_table(T_USER_NODE)?; + if !table.is_empty()? { + bail!("tree not empty, `rm -rf` your db please :)") + } + } + + let Ok(i) = File::open(path) else { + warn!( + "{path:?} does not exist; the import of that tree will be skipped." + ); + return Ok(()); + }; + let i = BufReader::new(i); + for l in i.lines() { + let l = l?; + let (k, v) = serde_json::from_str::<((String, String), NodeUserData)>( + l.as_str(), + ) + .context("reading db dump item")?; + { + let txn = da.begin_write()?; + let mut table = txn.open_table(T_USER_NODE)?; + + table.insert((k.0.as_str(), k.1.as_str()), Ser(v))?; + drop(table); + txn.commit()? + } + } + } + } + }; info!("processing 'invite'"); - process_tree(mode, &save_location.join("invite"), &db.invite)?; - info!("processing 'node'"); - process_tree(mode, &save_location.join("node"), &db.node)?; + { + let path: &Path = &save_location.join("invite"); + let da = &da; + match mode { + MigrateMode::Export => { + let mut o = BufWriter::new(File::create(path)?); + let txn = da.begin_read()?; + let table = txn.open_table(T_INVITE)?; + + let len = table.len()?; + for r in table.iter()?.progress_count(len.try_into().unwrap()) { + let (k, v) = r?; + serde_json::to_writer(&mut o, &(k.value(), v.value().0))?; + writeln!(&mut o)?; + } + drop(table); + } + MigrateMode::Import => { + { + let txn = da.begin_read()?; + let table = txn.open_table(T_INVITE)?; + if !table.is_empty()? { + bail!("tree not empty, `rm -rf` your db please :)") + } + } + + let Ok(i) = File::open(path) else { + warn!( + "{path:?} does not exist; the import of that tree will be skipped." + ); + return Ok(()); + }; + let i = BufReader::new(i); + for l in i.lines() { + let l = l?; + let (k, _v) = serde_json::from_str::<(String, ())>(l.as_str()) + .context("reading db dump item")?; + { + let txn = da.begin_write()?; + let mut table = txn.open_table(T_INVITE)?; + + table.insert(k.as_str(), Ser(()))?; + drop(table); + txn.commit()? + } + } + } + } + }; info!("done"); - Ok(()) } _ => unreachable!(), } + Ok(()) } +/* -fn process_tree< - K: Serialize + for<'de> serde::Deserialize<'de>, - V: Serialize + for<'de> serde::Deserialize<'de>, ->( +fn process_tree<'c, 'd, K, V>( mode: MigrateMode, path: &Path, - tree: &Tree<K, V>, -) -> anyhow::Result<()> { + da: &DataAcid, + table: TableDefinition<'static, K, Ser<V>>, +) -> anyhow::Result<()> +where + K: RedbKey + Owny<'c> + Clone, + V: Encode + Decode + Debug + Serialize + Owny<'d> + Clone, + <K as Owny<'c>>::Owned: for<'a> serde::Deserialize<'a>, + <V as Owny<'d>>::Owned: for<'a> serde::Deserialize<'a>, +{ match mode { - MigrateMode::Export => export_tree(path, tree), - MigrateMode::Import => import_tree(path, tree), - } -} + MigrateMode::Export => { + // let mut o = BufWriter::new(File::create(path)?); + // let txn = da.begin_read()?; + // let table = txn.open_table(table)?; -fn export_tree< - K: Serialize + for<'de> serde::Deserialize<'de>, - V: Serialize + for<'de> serde::Deserialize<'de>, ->( - path: &Path, - tree: &Tree<K, V>, -) -> anyhow::Result<()> { - let mut o = BufWriter::new(File::create(path)?); - let len = tree.len(); - for r in tree.iter().progress_count(len.try_into().unwrap()) { - let (k, v) = r?; - serde_json::to_writer(&mut o, &(k, v))?; - writeln!(&mut o)?; - } - Ok(()) -} + // let len = table.len()?; + // for r in table.iter()?.progress_count(len.try_into().unwrap()) { + // let (k, v) = r?; + // serde_json::to_writer(&mut o, &(k, v.value().0))?; + // writeln!(&mut o)?; + // } + // drop(table); + } + MigrateMode::Import => { + { + let txn = da.begin_read()?; + let table = txn.open_table(table)?; + if !table.is_empty()? { + bail!("tree not empty, `rm -rf` your db please :)") + } + } -fn import_tree< - K: Serialize + for<'de> serde::Deserialize<'de>, - V: Serialize + for<'de> serde::Deserialize<'de>, ->( - path: &Path, - tree: &Tree<K, V>, -) -> anyhow::Result<()> { - if !tree.is_empty() { - bail!("tree not empty, `rm -rf` your db please :)") - } - let Ok(i) = File::open(path) else { - warn!("{path:?} does not exist; the import of that tree will be skipped."); - return Ok(()); - }; - let i = BufReader::new(i); - for l in i.lines() { - let l = l?; - let (k, v) = serde_json::from_str::<(K, V)>(&l).context("reading db dump item")?; - tree.insert(&k, &v)?; + let Ok(i) = File::open(path) else { + warn!("{path:?} does not exist; the import of that tree will be skipped."); + return Ok(()); + }; + let i = BufReader::new(i); + for l in i.lines() { + let l = l?; + let (k, v) = + serde_json::from_str::<(<K as Owny>::Owned, <V as Owny>::Owned)>(l.as_str()) + .context("reading db dump item")?; + { + let (k, v) = (k.borrow(), v.borrow()); + + let txn = da.begin_write()?; + let mut table = txn.open_table(table)?; + + table.insert(k, Ser(v))?; + drop(table); + txn.commit()? + } + } + } } Ok(()) -} +} */ + +// trait Owny<'a> { +// type Owned; +// fn borrow(x: &'a Self::Owned) -> Self; +// } +// impl<'a> Owny<'a> for &'a str { +// type Owned = String; +// fn borrow(x: &'a Self::Owned) -> Self { +// x.as_str() +// } +// } +// impl<'a> Owny<'a> for (&'a str, &'a str) { +// type Owned = (String, String); + +// fn borrow(x: &'a Self::Owned) -> Self { +// (x.0.as_str(), x.1.as_str()) +// } +// } +// impl Owny<'_> for User { +// type Owned = User; +// fn borrow(x: &Self::Owned) -> Self { +// x.to_owned() +// } +// } +// impl Owny<'_> for NodeUserData { +// type Owned = NodeUserData; +// fn borrow(x: &Self::Owned) -> Self { +// x.to_owned() +// } +// } +// impl Owny<'_> for () { +// type Owned = (); +// fn borrow(x: &Self::Owned) -> Self { +// x.to_owned() +// } +// } |