From 35ae80f183904466667af73c7921b4ade399569a Mon Sep 17 00:00:00 2001 From: metamuffin Date: Wed, 30 Apr 2025 11:46:28 +0200 Subject: split base into asset_token and db --- Cargo.lock | 59 ++-- Cargo.toml | 17 +- base/Cargo.toml | 25 -- base/src/assetfed.rs | 87 ------ base/src/database.rs | 605 -------------------------------------- base/src/federation.rs | 64 ---- base/src/lib.rs | 29 -- base/src/permission.rs | 64 ---- database/Cargo.toml | 16 + database/src/lib.rs | 555 ++++++++++++++++++++++++++++++++++ database/src/search.rs | 64 ++++ import/Cargo.toml | 4 +- import/asset_token/Cargo.toml | 20 ++ import/asset_token/src/lib.rs | 105 +++++++ import/src/infojson.rs | 2 +- import/src/lib.rs | 16 +- import/src/tmdb.rs | 4 +- import/src/trakt.rs | 2 +- logic/Cargo.toml | 3 +- logic/src/admin/user.rs | 2 +- logic/src/home.rs | 2 +- logic/src/items.rs | 2 +- logic/src/lib.rs | 2 + logic/src/login.rs | 2 +- logic/src/node.rs | 2 +- logic/src/search.rs | 2 +- logic/src/stats.rs | 3 +- server/Cargo.toml | 1 - server/src/api.rs | 5 +- server/src/compat/jellyfin/mod.rs | 3 +- server/src/compat/youtube.rs | 3 +- server/src/config.rs | 4 +- server/src/helper/session.rs | 6 +- server/src/logic/stream.rs | 6 +- server/src/logic/userdata.rs | 3 +- server/src/main.rs | 3 +- server/src/routes.rs | 2 +- server/src/ui/account/mod.rs | 2 +- server/src/ui/account/settings.rs | 11 +- server/src/ui/admin/mod.rs | 7 +- server/src/ui/admin/user.rs | 4 +- server/src/ui/assets.rs | 4 +- server/src/ui/home.rs | 3 +- server/src/ui/items.rs | 4 +- server/src/ui/node.rs | 4 +- server/src/ui/player.rs | 4 +- server/src/ui/search.rs | 3 +- server/src/ui/stats.rs | 4 +- 48 files changed, 869 insertions(+), 975 deletions(-) delete mode 100644 base/Cargo.toml delete mode 100644 base/src/assetfed.rs delete mode 100644 base/src/database.rs delete mode 100644 base/src/federation.rs delete mode 100644 base/src/lib.rs delete mode 100644 base/src/permission.rs create mode 100644 database/Cargo.toml create mode 100644 database/src/lib.rs create mode 100644 database/src/search.rs create mode 100644 import/asset_token/Cargo.toml create mode 100644 import/asset_token/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ccf4e74..461e8cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1742,51 +1742,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] -name = "jellybase" +name = "jellycache" version = "0.1.0" dependencies = [ - "aes-gcm-siv", "anyhow", "base64", "bincode", "humansize", - "jellycache", - "jellycommon", "log", "rand 0.9.1", - "redb", "serde", - "serde_json", - "serde_yaml", "sha2", - "tantivy", "tokio", ] [[package]] -name = "jellycache" +name = "jellycommon" version = "0.1.0" dependencies = [ - "anyhow", - "base64", "bincode", - "humansize", - "log", - "rand 0.9.1", + "blake3", + "chrono", + "hex", "serde", - "sha2", - "tokio", ] [[package]] -name = "jellycommon" +name = "jellydb" version = "0.1.0" dependencies = [ + "anyhow", "bincode", - "blake3", - "chrono", - "hex", + "jellycommon", + "log", + "redb", "serde", + "tantivy", ] [[package]] @@ -1799,8 +1790,10 @@ dependencies = [ "bincode", "crossbeam-channel", "futures", - "jellybase", "jellycache", + "jellycommon", + "jellydb", + "jellyimport-asset-token", "jellyimport-fallback-generator", "jellyremuxer", "log", @@ -1814,6 +1807,26 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "jellyimport-asset-token" +version = "0.1.0" +dependencies = [ + "aes-gcm-siv", + "anyhow", + "base64", + "bincode", + "humansize", + "jellycache", + "jellycommon", + "log", + "rand 0.9.1", + "serde", + "serde_json", + "serde_yaml", + "sha2", + "tokio", +] + [[package]] name = "jellyimport-fallback-generator" version = "0.1.0" @@ -1834,8 +1847,9 @@ dependencies = [ "base64", "bincode", "env_logger", - "jellybase", "jellycommon", + "jellydb", + "jellyimport-asset-token", "log", "rand 0.9.1", "serde", @@ -1895,7 +1909,6 @@ dependencies = [ "env_logger", "futures", "glob", - "jellybase", "jellycache", "jellycommon", "jellyimport", diff --git a/Cargo.toml b/Cargo.toml index 89259bc..da554f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,19 @@ [workspace] members = [ - "server", - "remuxer", + "cache", "common", - "tool", - "matroska", + "database", "ebml_derive", - "transcoder", - "base", "import", + "import/asset_token", "import/fallback_generator", - "ui", "logic", - "cache", + "matroska", + "remuxer", + "server", + "tool", + "transcoder", + "ui", ] resolver = "2" diff --git a/base/Cargo.toml b/base/Cargo.toml deleted file mode 100644 index ca4f454..0000000 --- a/base/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "jellybase" -version = "0.1.0" -edition = "2021" - -[dependencies] -jellycommon = { path = "../common" } -jellycache = { path = "../cache" } -serde = { version = "1.0.217", features = ["derive"] } -serde_yaml = "0.9.34" -log = { workspace = true } -sha2 = "0.10.8" -base64 = "0.22.1" -tokio = { workspace = true } -anyhow = "1.0.95" -bincode = "2.0.0-rc.3" -rand = "0.9.0" -redb = "2.4.0" -tantivy = "0.22.0" -serde_json = "1.0.138" -aes-gcm-siv = "0.11.1" -humansize = "2.1.3" - -[features] -db_json = [] diff --git a/base/src/assetfed.rs b/base/src/assetfed.rs deleted file mode 100644 index ea62e0d..0000000 --- a/base/src/assetfed.rs +++ /dev/null @@ -1,87 +0,0 @@ -/* - 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. - Copyright (C) 2025 metamuffin -*/ -use crate::CONF; -use aes_gcm_siv::{ - aead::{generic_array::GenericArray, Aead}, - Aes256GcmSiv, KeyInit, -}; -use anyhow::{anyhow, bail, Context}; -use base64::Engine; -use bincode::{Decode, Encode}; -use jellycache::CachePath; -use jellycommon::{Asset, LocalTrack}; -use log::warn; -use serde::Serialize; -use std::{path::PathBuf, sync::LazyLock}; - -const VERSION: u32 = 3; - -static ASSET_KEY: LazyLock = LazyLock::new(|| { - if let Some(sk) = &CONF.asset_key { - let r = base64::engine::general_purpose::STANDARD - .decode(sk) - .expect("key invalid; should be valid base64"); - aes_gcm_siv::Aes256GcmSiv::new_from_slice(&r) - .expect("key has the wrong length; should be 32 bytes") - } else { - warn!("session_key not configured; generating a random one."); - aes_gcm_siv::Aes256GcmSiv::new_from_slice(&[(); 32].map(|_| rand::random())).unwrap() - } -}); - -#[derive(Debug, Encode, Decode, Serialize)] -pub enum AssetInner { - Federated { host: String, asset: Vec }, - Cache(CachePath), - Assets(PathBuf), - Media(PathBuf), - LocalTrack(LocalTrack), -} - -impl AssetInner { - pub fn ser(&self) -> Asset { - let mut plaintext = Vec::new(); - plaintext.extend(u32::to_le_bytes(VERSION)); - plaintext.extend(bincode::encode_to_vec(self, bincode::config::standard()).unwrap()); - - while plaintext.len() % 16 == 0 { - plaintext.push(0); - } - - let nonce = [(); 12].map(|_| rand::random()); - let mut ciphertext = ASSET_KEY - .encrypt(&GenericArray::from(nonce), plaintext.as_slice()) - .unwrap(); - ciphertext.extend(nonce); - - Asset(base64::engine::general_purpose::URL_SAFE.encode(&ciphertext)) - } - pub fn deser(s: &str) -> anyhow::Result { - let ciphertext = base64::engine::general_purpose::URL_SAFE.decode(s)?; - let (ciphertext, nonce) = ciphertext.split_at(ciphertext.len() - 12); - let plaintext = ASSET_KEY - .decrypt(nonce.into(), ciphertext) - .map_err(|_| anyhow!("asset token decrypt failed"))?; - - let version = u32::from_le_bytes(plaintext[0..4].try_into().unwrap()); - if version != VERSION { - bail!("asset token version mismatch"); - } - - let (data, _): (AssetInner, _) = - bincode::decode_from_slice(&plaintext[4..], bincode::config::standard()) - .context("asset token has invalid format")?; - Ok(data) - } - - /// Returns `true` if the asset inner is [`Federated`]. - /// - /// [`Federated`]: AssetInner::Federated - #[must_use] - pub fn is_federated(&self) -> bool { - matches!(self, Self::Federated { .. }) - } -} diff --git a/base/src/database.rs b/base/src/database.rs deleted file mode 100644 index c3ca5d4..0000000 --- a/base/src/database.rs +++ /dev/null @@ -1,605 +0,0 @@ -/* - 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. - Copyright (C) 2025 metamuffin -*/ -use anyhow::{anyhow, bail, Context, Result}; -use bincode::{config::standard, Decode, Encode}; -use jellycommon::{ - user::{NodeUserData, User}, - Node, NodeID, -}; -use log::info; -use redb::{Durability, ReadableTable, StorageError, TableDefinition}; -use std::{ - fs::create_dir_all, - hash::{DefaultHasher, Hasher}, - path::{Path, PathBuf}, - str::FromStr, - sync::{Arc, RwLock}, - time::SystemTime, -}; -use tantivy::{ - collector::{Count, TopDocs}, - directory::MmapDirectory, - doc, - query::QueryParser, - schema::{Field, Schema, Value, FAST, INDEXED, STORED, STRING, TEXT}, - DateOptions, DateTime, Index, IndexReader, IndexWriter, ReloadPolicy, TantivyDocument, -}; - -const T_USER: TableDefinition<&str, Ser> = TableDefinition::new("user"); -const T_USER_NODE: TableDefinition<(&str, [u8; 32]), Ser> = - TableDefinition::new("user_node"); -const T_INVITE: TableDefinition<&str, ()> = TableDefinition::new("invite"); -const T_NODE: TableDefinition<[u8; 32], Ser> = TableDefinition::new("node"); -const T_NODE_CHILDREN: TableDefinition<([u8; 32], [u8; 32]), ()> = - TableDefinition::new("node_children"); -const T_TAG_NODE: TableDefinition<(&str, [u8; 32]), ()> = TableDefinition::new("tag_node"); -const T_NODE_EXTERNAL_ID: TableDefinition<(&str, &str), [u8; 32]> = - TableDefinition::new("node_external_id"); -const T_IMPORT_FILE_MTIME: TableDefinition<&[u8], u64> = TableDefinition::new("import_file_mtime"); -const T_NODE_MTIME: TableDefinition<[u8; 32], u64> = TableDefinition::new("node_mtime"); -const T_NODE_MEDIA_PATHS: TableDefinition<([u8; 32], &str), ()> = - TableDefinition::new("node_media_paths"); - -#[derive(Clone)] -pub struct Database { - inner: Arc, - text_search: Arc, -} - -impl Database { - pub fn open(path: &Path) -> Result { - create_dir_all(path).context("creating database directory")?; - info!("opening kv store..."); - let db = redb::Database::create(path.join("data")).context("opening kv store")?; - info!("opening node index..."); - let ft_node = NodeTextSearchIndex::new(path).context("in node index")?; - let r = Self { - inner: db.into(), - text_search: ft_node.into(), - }; - - { - // this creates all tables such that read operations on them do not fail. - let txn = r.inner.begin_write()?; - txn.open_table(T_INVITE)?; - txn.open_table(T_USER)?; - txn.open_table(T_USER_NODE)?; - txn.open_table(T_NODE)?; - txn.open_table(T_NODE_MTIME)?; - txn.open_table(T_NODE_CHILDREN)?; - txn.open_table(T_NODE_EXTERNAL_ID)?; - txn.open_table(T_NODE_MEDIA_PATHS)?; - txn.open_table(T_IMPORT_FILE_MTIME)?; - txn.commit()?; - } - - info!("ready"); - Ok(r) - } - - pub fn get_node_slug(&self, slug: &str) -> Result>> { - self.get_node(NodeID::from_slug(slug)) - } - - pub fn get_node(&self, id: NodeID) -> Result>> { - let txn = self.inner.begin_read()?; - let t_node = txn.open_table(T_NODE)?; - if let Some(node) = t_node.get(id.0)? { - Ok(Some(node.value().0.into())) - } else { - Ok(None) - } - } - pub fn get_node_external_id(&self, platform: &str, eid: &str) -> Result> { - let txn = self.inner.begin_read()?; - let t_node_external_id = txn.open_table(T_NODE_EXTERNAL_ID)?; - if let Some(id) = t_node_external_id.get((platform, eid))? { - Ok(Some(NodeID(id.value()))) - } else { - Ok(None) - } - } - pub fn get_node_children(&self, id: NodeID) -> Result> { - let txn = self.inner.begin_read()?; - let t_node_children = txn.open_table(T_NODE_CHILDREN)?; - Ok(t_node_children - .range((id.0, NodeID::MIN.0)..(id.0, NodeID::MAX.0))? - .map(|r| r.map(|r| NodeID(r.0.value().1))) - .collect::, StorageError>>()?) - } - pub fn get_tag_nodes(&self, tag: &str) -> Result> { - let txn = self.inner.begin_read()?; - let t_tag_node = txn.open_table(T_TAG_NODE)?; - Ok(t_tag_node - .range((tag, NodeID::MIN.0)..(tag, NodeID::MAX.0))? - .map(|r| r.map(|r| NodeID(r.0.value().1))) - .collect::, StorageError>>()?) - } - pub fn get_nodes_modified_since(&self, since: u64) -> Result> { - let txn = self.inner.begin_read()?; - let t_node_mtime = txn.open_table(T_NODE_MTIME)?; - Ok(t_node_mtime - .iter()? - .flat_map(|r| r.map(|r| (NodeID(r.0.value()), r.1.value()))) - .filter(|(_, mtime)| *mtime >= since) - .map(|(id, _)| id) - .collect()) - } - - pub fn clear_nodes(&self) -> Result<()> { - let mut txn = self.inner.begin_write()?; - let mut t_node = txn.open_table(T_NODE)?; - let mut t_node_mtime = txn.open_table(T_NODE_MTIME)?; - let mut t_node_children = txn.open_table(T_NODE_CHILDREN)?; - let mut t_node_external_id = txn.open_table(T_NODE_EXTERNAL_ID)?; - let mut t_import_file_mtime = txn.open_table(T_IMPORT_FILE_MTIME)?; - let mut t_node_media_paths = txn.open_table(T_NODE_MEDIA_PATHS)?; - t_node.retain(|_, _| false)?; - t_node_mtime.retain(|_, _| false)?; - t_node_children.retain(|_, _| false)?; - t_node_external_id.retain(|_, _| false)?; - t_import_file_mtime.retain(|_, _| false)?; - t_node_media_paths.retain(|_, _| false)?; - drop(( - t_node, - t_node_mtime, - t_node_children, - t_node_external_id, - t_import_file_mtime, - t_node_media_paths, - )); - txn.set_durability(Durability::Eventual); - txn.commit()?; - Ok(()) - } - - pub fn get_node_udata(&self, id: NodeID, username: &str) -> Result> { - let txn = self.inner.begin_read()?; - let t_node = txn.open_table(T_USER_NODE)?; - if let Some(node) = t_node.get((username, id.0))? { - Ok(Some(node.value().0)) - } else { - Ok(None) - } - } - - pub fn update_node_init( - &self, - id: NodeID, - update: impl FnOnce(&mut Node) -> Result<()>, - ) -> Result<()> { - let time = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(); - let mut txn = self.inner.begin_write()?; - let mut t_node = txn.open_table(T_NODE)?; - let mut t_node_mtime = txn.open_table(T_NODE_MTIME)?; - let mut t_node_children = txn.open_table(T_NODE_CHILDREN)?; - let mut t_node_external_id = txn.open_table(T_NODE_EXTERNAL_ID)?; - let mut t_tag_node = txn.open_table(T_TAG_NODE)?; - let mut node = t_node.get(id.0)?.map(|v| v.value().0).unwrap_or_default(); - - let mut dh_before = HashWriter(DefaultHasher::new()); - bincode::encode_into_writer(&node, &mut dh_before, standard()).unwrap(); - update(&mut node)?; - let mut dh_after = HashWriter(DefaultHasher::new()); - bincode::encode_into_writer(&node, &mut dh_after, standard()).unwrap(); - - if dh_before.0.finish() == dh_after.0.finish() { - return Ok(()); - } - - for parent in &node.parents { - t_node_children.insert((parent.0, id.0), ())?; - } - for (pl, eid) in &node.external_ids { - t_node_external_id.insert((pl.as_str(), eid.as_str()), id.0)?; - } - for tag in &node.tags { - t_tag_node.insert((tag.as_str(), id.0), ())?; - } - t_node.insert(&id.0, Ser(node))?; - t_node_mtime.insert(&id.0, time)?; - drop(( - t_node, - t_node_mtime, - t_node_children, - t_node_external_id, - t_tag_node, - )); - txn.set_durability(Durability::Eventual); - txn.commit()?; - Ok(()) - } - pub fn get_node_media_paths(&self, id: NodeID) -> Result> { - let txn = self.inner.begin_read()?; - let table = txn.open_table(T_NODE_MEDIA_PATHS)?; - let mut paths = Vec::new(); - // TODO fix this - for p in table.range((id.0, "\0")..(id.0, "\x7f"))? { - paths.push(PathBuf::from_str(p?.0.value().1)?); - } - Ok(paths) - } - pub fn insert_node_media_path(&self, id: NodeID, path: &Path) -> Result<()> { - let txn = self.inner.begin_write()?; - let mut table = txn.open_table(T_NODE_MEDIA_PATHS)?; - table.insert((id.0, path.to_str().unwrap()), ())?; - drop(table); - txn.commit()?; - Ok(()) - } - - pub fn update_node_udata( - &self, - node: NodeID, - username: &str, - update: impl FnOnce(&mut NodeUserData) -> Result<()>, - ) -> Result<()> { - let txn = self.inner.begin_write()?; - let mut user_nodes = txn.open_table(T_USER_NODE)?; - - let mut udata = user_nodes - .get((username, node.0))? - .map(|x| x.value().0) - .unwrap_or_default(); - - update(&mut udata)?; - - user_nodes.insert((username, node.0), Ser(udata))?; - drop(user_nodes); - txn.commit()?; - Ok(()) - } - - pub fn get_user(&self, username: &str) -> Result> { - let txn = self.inner.begin_read()?; - let t_user = txn.open_table(T_USER)?; - if let Some(user) = t_user.get(username)? { - Ok(Some(user.value().0)) - } else { - Ok(None) - } - } - pub fn update_user( - &self, - username: &str, - update: impl FnOnce(&mut User) -> Result<()>, - ) -> Result<()> { - let txn = self.inner.begin_write()?; - let mut users = txn.open_table(T_USER)?; - let mut user = users - .get(username)? - .ok_or(anyhow!("user does not exist"))? - .value() - .0; - update(&mut user)?; - users.insert(&username, Ser(user))?; - drop(users); - txn.commit()?; - - Ok(()) - } - pub fn delete_user(&self, username: &str) -> Result { - let txn = self.inner.begin_write()?; - let mut table = txn.open_table(T_USER)?; - let r = table.remove(username)?.is_some(); - drop(table); - txn.commit()?; - Ok(r) - } - pub fn list_users(&self) -> Result> { - let txn = self.inner.begin_read()?; - let table = txn.open_table(T_USER)?; - let i = table - .iter()? - .map(|a| { - let (_, y) = a.unwrap(); // TODO - y.value().0 - }) - .collect::>(); - drop(table); - Ok(i) - } - pub fn list_invites(&self) -> Result> { - let txn = self.inner.begin_read()?; - let table = txn.open_table(T_INVITE)?; - let i = table - .iter()? - .map(|a| { - let (x, _) = a.unwrap(); - x.value().to_owned() - }) - .collect::>(); - drop(table); - Ok(i) - } - pub fn create_invite(&self, inv: &str) -> Result<()> { - let txn = self.inner.begin_write()?; - let mut table = txn.open_table(T_INVITE)?; - table.insert(inv, ())?; - drop(table); - txn.commit()?; - Ok(()) - } - pub fn delete_invite(&self, inv: &str) -> Result { - let txn = self.inner.begin_write()?; - let mut table = txn.open_table(T_INVITE)?; - let r = table.remove(inv)?.is_some(); - drop(table); - txn.commit()?; - Ok(r) - } - pub fn register_user(&self, invite: &str, username: &str, user: User) -> Result<()> { - let txn = self.inner.begin_write()?; - let mut invites = txn.open_table(T_INVITE)?; - let mut users = txn.open_table(T_USER)?; - - if invites.remove(invite)?.is_none() { - bail!("invitation invalid"); - } - let prev_user = users.insert(username, Ser(user))?.map(|x| x.value().0); - if prev_user.is_some() { - bail!("username taken"); - } - - drop(users); - drop(invites); - txn.commit()?; - Ok(()) - } - pub fn list_nodes_with_udata(&self, username: &str) -> Result, NodeUserData)>> { - let txn = self.inner.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(), Arc::new(y.value().0)); - let z = node_users - .get(&(username, x)) - .unwrap() - .map(|z| z.value().0) - .unwrap_or_default(); - (y, z) - }) - .collect::>(); - drop(nodes); - Ok(i) - } - pub fn search(&self, query: &str, limit: usize, offset: usize) -> Result<(usize, Vec)> { - let query = QueryParser::for_index( - &self.text_search.index, - vec![self.text_search.title, self.text_search.description], - ) - .parse_query(query) - .context("parsing query")?; - - let searcher = self.text_search.reader.searcher(); - let sres = searcher.search(&query, &TopDocs::with_limit(limit).and_offset(offset))?; - let scount = searcher.search(&query, &Count)?; - - let mut results = Vec::new(); - for (_, daddr) in sres { - let doc: TantivyDocument = searcher.doc(daddr)?; - let id = doc - .get_first(self.text_search.id) - .unwrap() - .as_bytes() - .unwrap(); - let id = NodeID(id.try_into().unwrap()); - results.push(id); - } - Ok((scount, results)) - } - - pub fn search_create_index(&self) -> Result<()> { - let mut w = self.text_search.writer.write().unwrap(); - w.delete_all_documents()?; - - let txn = self.inner.begin_read()?; - let nodes = txn.open_table(T_NODE)?; - for node in nodes.iter()? { - let (x, y) = node?; - let (id, node) = (x.value().to_owned(), y.value().0); - - w.add_document(doc!( - self.text_search.id => id.to_vec(), - self.text_search.title => node.title.unwrap_or_default(), - self.text_search.description => node.description.unwrap_or_default(), - self.text_search.releasedate => DateTime::from_timestamp_millis(node.release_date.unwrap_or_default()), - self.text_search.f_index => node.index.unwrap_or_default() as u64, - ))?; - } - - w.commit()?; - Ok(()) - } - - pub fn create_admin_user(&self, username: &str, password_hash: Vec) -> Result<()> { - let txn = self.inner.begin_write().unwrap(); - let mut users = txn.open_table(T_USER).unwrap(); - - let admin = users.get(username).unwrap().map(|x| x.value().0); - users - .insert( - username, - Ser(User { - admin: true, - name: username.to_owned(), - password: password_hash, - ..admin.unwrap_or_else(|| User { - display_name: "Admin".to_string(), - ..Default::default() - }) - }), - ) - .unwrap(); - - drop(users); - txn.commit().unwrap(); - Ok(()) - } - pub fn get_import_file_mtime(&self, path: &Path) -> Result> { - let bytes = path.as_os_str().as_encoded_bytes(); - let txn = self.inner.begin_read()?; - let table = txn.open_table(T_IMPORT_FILE_MTIME)?; - if let Some(v) = table.get(bytes)? { - Ok(Some(v.value())) - } else { - Ok(None) - } - } - pub fn set_import_file_mtime(&self, path: &Path, mtime: u64) -> Result<()> { - let bytes = path.as_os_str().as_encoded_bytes(); - let txn = self.inner.begin_write()?; - let mut table = txn.open_table(T_IMPORT_FILE_MTIME)?; - table.insert(bytes, mtime)?; - drop(table); - txn.commit()?; - Ok(()) - } -} - -pub struct NodeTextSearchIndex { - pub schema: Schema, - pub reader: IndexReader, - pub writer: RwLock, - pub index: Index, - pub id: Field, - pub title: Field, - pub releasedate: Field, - pub description: Field, - pub parent: Field, - pub f_index: Field, -} -impl NodeTextSearchIndex { - fn new(path: &Path) -> anyhow::Result { - let mut schema = Schema::builder(); - let id = schema.add_text_field("id", TEXT | STORED | FAST); - let title = schema.add_text_field("title", TEXT); - let description = schema.add_text_field("description", TEXT); - let parent = schema.add_text_field("parent", STRING | FAST); - let f_index = schema.add_u64_field("index", FAST); - let releasedate = schema.add_date_field( - "releasedate", - DateOptions::from(INDEXED) - .set_fast() - .set_precision(tantivy::DateTimePrecision::Seconds), - ); - let schema = schema.build(); - create_dir_all(path.join("node_index"))?; - let directory = - MmapDirectory::open(path.join("node_index")).context("opening index directory")?; - let index = Index::open_or_create(directory, schema.clone()).context("creating index")?; - let reader = index - .reader_builder() - .reload_policy(ReloadPolicy::OnCommitWithDelay) - .try_into() - .context("creating reader")?; - let writer = index.writer(30_000_000).context("creating writer")?; - Ok(Self { - index, - writer: writer.into(), - reader, - schema, - parent, - f_index, - releasedate, - id, - description, - title, - }) - } -} - -pub struct HashWriter(DefaultHasher); -impl bincode::enc::write::Writer for HashWriter { - fn write(&mut self, bytes: &[u8]) -> std::result::Result<(), bincode::error::EncodeError> { - self.0.write(bytes); - Ok(()) - } -} - -#[derive(Debug)] -#[cfg(not(feature = "db_json"))] -pub struct Ser(pub T); -#[cfg(not(feature = "db_json"))] -impl redb::Value for Ser { - type SelfType<'a> - = Ser - where - Self: 'a; - type AsBytes<'a> - = Vec - where - Self: 'a; - - fn fixed_width() -> Option { - 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 { - redb::TypeName::new("bincode") - } -} - -#[derive(Debug)] -#[cfg(feature = "db_json")] -pub struct Ser(pub T); -#[cfg(feature = "db_json")] -impl Deserialize<'a> + std::fmt::Debug> redb::Value for Ser { - type SelfType<'a> - = Ser - where - Self: 'a; - type AsBytes<'a> - = Vec - where - Self: 'a; - - fn fixed_width() -> Option { - 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 { - redb::TypeName::new("json") - } -} diff --git a/base/src/federation.rs b/base/src/federation.rs deleted file mode 100644 index b24d113..0000000 --- a/base/src/federation.rs +++ /dev/null @@ -1,64 +0,0 @@ -/* - 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. - Copyright (C) 2025 metamuffin -*/ - -// use anyhow::anyhow; -// use jellyclient::{Instance, Session}; -// use jellycommon::{config::FederationAccount, user::CreateSessionParams}; -// use std::{collections::HashMap, sync::Arc}; -// use tokio::sync::RwLock; - -// pub struct Federation { -// instances: HashMap, -// sessions: RwLock>>, -// } - -// impl Federation { -// pub fn initialize() -> Self { -// let instances = SECRETS -// .federation -// .iter() -// .map(|(k, FederationAccount { tls, .. })| { -// (k.to_owned(), Instance::new(k.to_owned(), *tls)) -// }) -// .collect::>(); - -// Self { -// instances, -// sessions: Default::default(), -// } -// } - -// pub fn get_instance(&self, host: &String) -> anyhow::Result<&Instance> { -// self.instances.get(host).ok_or(anyhow!("unknown instance")) -// } - -// pub async fn get_session(&self, host: &String) -> anyhow::Result> { -// let mut w = self.sessions.write().await; -// if let Some(s) = w.get(host) { -// Ok(s.to_owned()) -// } else { -// let FederationAccount { -// username, password, .. -// } = SECRETS -// .federation -// .get(host) -// .ok_or(anyhow!("no credentials of the remote server"))?; -// let s = Arc::new( -// self.get_instance(host)? -// .to_owned() -// .login(CreateSessionParams { -// username: username.to_owned(), -// password: password.to_owned(), -// expire: None, -// drop_permissions: None, -// }) -// .await?, -// ); -// w.insert(host.to_owned(), s.clone()); -// Ok(s) -// } -// } -// } diff --git a/base/src/lib.rs b/base/src/lib.rs deleted file mode 100644 index 55a9927..0000000 --- a/base/src/lib.rs +++ /dev/null @@ -1,29 +0,0 @@ -/* - 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. - Copyright (C) 2025 metamuffin -*/ -pub mod assetfed; -pub mod database; -pub mod federation; -pub mod permission; - -pub use jellycommon as common; -use serde::{Deserialize, Serialize}; -use std::sync::LazyLock; -use std::sync::Mutex; - -#[rustfmt::skip] -#[derive(Debug, Deserialize, Serialize, Default)] -pub struct Config { - asset_key: Option, -} - -pub static CONF_PRELOAD: Mutex> = Mutex::new(None); -static CONF: LazyLock = LazyLock::new(|| { - CONF_PRELOAD - .lock() - .unwrap() - .take() - .expect("cache config not preloaded. logic error") -}); diff --git a/base/src/permission.rs b/base/src/permission.rs deleted file mode 100644 index 7914f0b..0000000 --- a/base/src/permission.rs +++ /dev/null @@ -1,64 +0,0 @@ -/* - 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. - Copyright (C) 2025 metamuffin -*/ -use anyhow::anyhow; -use jellycommon::{ - user::{PermissionSet, UserPermission}, - Node, -}; - -pub trait PermissionSetExt { - fn check_explicit(&self, perm: &UserPermission) -> Option; - fn check(&self, perm: &UserPermission) -> bool { - self.check_explicit(perm).unwrap_or(perm.default_value()) - } - fn assert(&self, perm: &UserPermission) -> Result<(), anyhow::Error>; -} - -impl PermissionSetExt for PermissionSet { - fn check_explicit(&self, perm: &UserPermission) -> Option { - self.0 - .get(perm) - // .or(CONF.default_permission_set.0.get(perm)) - .copied() - } - fn assert(&self, perm: &UserPermission) -> Result<(), anyhow::Error> { - if self.check(perm) { - Ok(()) - } else { - Err(anyhow!( - "sorry, you need special permission {perm:?} for this action." - )) - } - } -} - -pub trait NodePermissionExt { - fn only_if_permitted(self, perms: &PermissionSet) -> Self; -} -impl NodePermissionExt for Option { - fn only_if_permitted(self, perms: &PermissionSet) -> Self { - self.and_then(|node| { - if check_node_permission(perms, &node) { - Some(node) - } else { - None - } - }) - } -} -fn check_node_permission(_perms: &PermissionSet, _node: &Node) -> bool { - // if let Some(v) = perms.check_explicit(&UserPermission::AccessNode(node.id.clone().unwrap())) { - // v - // } else { - // TODO - // for com in node.parents.clone().into_iter() { - // if let Some(v) = perms.check_explicit(&UserPermission::AccessNode(com.to_owned())) { - // return v; - // } - // } - true - // } -} diff --git a/database/Cargo.toml b/database/Cargo.toml new file mode 100644 index 0000000..6e5ddcf --- /dev/null +++ b/database/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "jellydb" +version = "0.1.0" +edition = "2024" + +[dependencies] +tantivy = "0.22.0" +jellycommon = { path = "../common" } +serde = { version = "1.0.217", features = ["derive"] } +log = { workspace = true } +bincode = "2.0.0-rc.3" +redb = "2.4.0" +anyhow = "1.0.95" + +[features] +db_json = [] diff --git a/database/src/lib.rs b/database/src/lib.rs new file mode 100644 index 0000000..b84ddc9 --- /dev/null +++ b/database/src/lib.rs @@ -0,0 +1,555 @@ +/* + 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. + Copyright (C) 2025 metamuffin +*/ +pub mod search; + +use anyhow::{Context, Result, anyhow, bail}; +use bincode::{Decode, Encode, config::standard}; +use jellycommon::{ + Node, NodeID, + user::{NodeUserData, User}, +}; +use log::info; +use redb::{Durability, ReadableTable, StorageError, TableDefinition}; +use search::NodeTextSearchIndex; +use std::{ + fs::create_dir_all, + hash::{DefaultHasher, Hasher}, + path::{Path, PathBuf}, + str::FromStr, + sync::Arc, + time::SystemTime, +}; +use tantivy::{ + DateTime, TantivyDocument, + collector::{Count, TopDocs}, + doc, + query::QueryParser, + schema::Value, +}; + +const T_USER: TableDefinition<&str, Ser> = TableDefinition::new("user"); +const T_USER_NODE: TableDefinition<(&str, [u8; 32]), Ser> = + TableDefinition::new("user_node"); +const T_INVITE: TableDefinition<&str, ()> = TableDefinition::new("invite"); +const T_NODE: TableDefinition<[u8; 32], Ser> = TableDefinition::new("node"); +const T_NODE_CHILDREN: TableDefinition<([u8; 32], [u8; 32]), ()> = + TableDefinition::new("node_children"); +const T_TAG_NODE: TableDefinition<(&str, [u8; 32]), ()> = TableDefinition::new("tag_node"); +const T_NODE_EXTERNAL_ID: TableDefinition<(&str, &str), [u8; 32]> = + TableDefinition::new("node_external_id"); +const T_IMPORT_FILE_MTIME: TableDefinition<&[u8], u64> = TableDefinition::new("import_file_mtime"); +const T_NODE_MTIME: TableDefinition<[u8; 32], u64> = TableDefinition::new("node_mtime"); +const T_NODE_MEDIA_PATHS: TableDefinition<([u8; 32], &str), ()> = + TableDefinition::new("node_media_paths"); + +#[derive(Clone)] +pub struct Database { + inner: Arc, + text_search: Arc, +} + +impl Database { + pub fn open(path: &Path) -> Result { + create_dir_all(path).context("creating database directory")?; + info!("opening kv store..."); + let db = redb::Database::create(path.join("data")).context("opening kv store")?; + info!("opening node index..."); + let ft_node = NodeTextSearchIndex::new(path).context("in node index")?; + let r = Self { + inner: db.into(), + text_search: ft_node.into(), + }; + + { + // this creates all tables such that read operations on them do not fail. + let txn = r.inner.begin_write()?; + txn.open_table(T_INVITE)?; + txn.open_table(T_USER)?; + txn.open_table(T_USER_NODE)?; + txn.open_table(T_NODE)?; + txn.open_table(T_NODE_MTIME)?; + txn.open_table(T_NODE_CHILDREN)?; + txn.open_table(T_NODE_EXTERNAL_ID)?; + txn.open_table(T_NODE_MEDIA_PATHS)?; + txn.open_table(T_IMPORT_FILE_MTIME)?; + txn.commit()?; + } + + info!("ready"); + Ok(r) + } + + pub fn get_node_slug(&self, slug: &str) -> Result>> { + self.get_node(NodeID::from_slug(slug)) + } + + pub fn get_node(&self, id: NodeID) -> Result>> { + let txn = self.inner.begin_read()?; + let t_node = txn.open_table(T_NODE)?; + if let Some(node) = t_node.get(id.0)? { + Ok(Some(node.value().0.into())) + } else { + Ok(None) + } + } + pub fn get_node_external_id(&self, platform: &str, eid: &str) -> Result> { + let txn = self.inner.begin_read()?; + let t_node_external_id = txn.open_table(T_NODE_EXTERNAL_ID)?; + if let Some(id) = t_node_external_id.get((platform, eid))? { + Ok(Some(NodeID(id.value()))) + } else { + Ok(None) + } + } + pub fn get_node_children(&self, id: NodeID) -> Result> { + let txn = self.inner.begin_read()?; + let t_node_children = txn.open_table(T_NODE_CHILDREN)?; + Ok(t_node_children + .range((id.0, NodeID::MIN.0)..(id.0, NodeID::MAX.0))? + .map(|r| r.map(|r| NodeID(r.0.value().1))) + .collect::, StorageError>>()?) + } + pub fn get_tag_nodes(&self, tag: &str) -> Result> { + let txn = self.inner.begin_read()?; + let t_tag_node = txn.open_table(T_TAG_NODE)?; + Ok(t_tag_node + .range((tag, NodeID::MIN.0)..(tag, NodeID::MAX.0))? + .map(|r| r.map(|r| NodeID(r.0.value().1))) + .collect::, StorageError>>()?) + } + pub fn get_nodes_modified_since(&self, since: u64) -> Result> { + let txn = self.inner.begin_read()?; + let t_node_mtime = txn.open_table(T_NODE_MTIME)?; + Ok(t_node_mtime + .iter()? + .flat_map(|r| r.map(|r| (NodeID(r.0.value()), r.1.value()))) + .filter(|(_, mtime)| *mtime >= since) + .map(|(id, _)| id) + .collect()) + } + + pub fn clear_nodes(&self) -> Result<()> { + let mut txn = self.inner.begin_write()?; + let mut t_node = txn.open_table(T_NODE)?; + let mut t_node_mtime = txn.open_table(T_NODE_MTIME)?; + let mut t_node_children = txn.open_table(T_NODE_CHILDREN)?; + let mut t_node_external_id = txn.open_table(T_NODE_EXTERNAL_ID)?; + let mut t_import_file_mtime = txn.open_table(T_IMPORT_FILE_MTIME)?; + let mut t_node_media_paths = txn.open_table(T_NODE_MEDIA_PATHS)?; + t_node.retain(|_, _| false)?; + t_node_mtime.retain(|_, _| false)?; + t_node_children.retain(|_, _| false)?; + t_node_external_id.retain(|_, _| false)?; + t_import_file_mtime.retain(|_, _| false)?; + t_node_media_paths.retain(|_, _| false)?; + drop(( + t_node, + t_node_mtime, + t_node_children, + t_node_external_id, + t_import_file_mtime, + t_node_media_paths, + )); + txn.set_durability(Durability::Eventual); + txn.commit()?; + Ok(()) + } + + pub fn get_node_udata(&self, id: NodeID, username: &str) -> Result> { + let txn = self.inner.begin_read()?; + let t_node = txn.open_table(T_USER_NODE)?; + if let Some(node) = t_node.get((username, id.0))? { + Ok(Some(node.value().0)) + } else { + Ok(None) + } + } + + pub fn update_node_init( + &self, + id: NodeID, + update: impl FnOnce(&mut Node) -> Result<()>, + ) -> Result<()> { + let time = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let mut txn = self.inner.begin_write()?; + let mut t_node = txn.open_table(T_NODE)?; + let mut t_node_mtime = txn.open_table(T_NODE_MTIME)?; + let mut t_node_children = txn.open_table(T_NODE_CHILDREN)?; + let mut t_node_external_id = txn.open_table(T_NODE_EXTERNAL_ID)?; + let mut t_tag_node = txn.open_table(T_TAG_NODE)?; + let mut node = t_node.get(id.0)?.map(|v| v.value().0).unwrap_or_default(); + + let mut dh_before = HashWriter(DefaultHasher::new()); + bincode::encode_into_writer(&node, &mut dh_before, standard()).unwrap(); + update(&mut node)?; + let mut dh_after = HashWriter(DefaultHasher::new()); + bincode::encode_into_writer(&node, &mut dh_after, standard()).unwrap(); + + if dh_before.0.finish() == dh_after.0.finish() { + return Ok(()); + } + + for parent in &node.parents { + t_node_children.insert((parent.0, id.0), ())?; + } + for (pl, eid) in &node.external_ids { + t_node_external_id.insert((pl.as_str(), eid.as_str()), id.0)?; + } + for tag in &node.tags { + t_tag_node.insert((tag.as_str(), id.0), ())?; + } + t_node.insert(&id.0, Ser(node))?; + t_node_mtime.insert(&id.0, time)?; + drop(( + t_node, + t_node_mtime, + t_node_children, + t_node_external_id, + t_tag_node, + )); + txn.set_durability(Durability::Eventual); + txn.commit()?; + Ok(()) + } + pub fn get_node_media_paths(&self, id: NodeID) -> Result> { + let txn = self.inner.begin_read()?; + let table = txn.open_table(T_NODE_MEDIA_PATHS)?; + let mut paths = Vec::new(); + // TODO fix this + for p in table.range((id.0, "\0")..(id.0, "\x7f"))? { + paths.push(PathBuf::from_str(p?.0.value().1)?); + } + Ok(paths) + } + pub fn insert_node_media_path(&self, id: NodeID, path: &Path) -> Result<()> { + let txn = self.inner.begin_write()?; + let mut table = txn.open_table(T_NODE_MEDIA_PATHS)?; + table.insert((id.0, path.to_str().unwrap()), ())?; + drop(table); + txn.commit()?; + Ok(()) + } + + pub fn update_node_udata( + &self, + node: NodeID, + username: &str, + update: impl FnOnce(&mut NodeUserData) -> Result<()>, + ) -> Result<()> { + let txn = self.inner.begin_write()?; + let mut user_nodes = txn.open_table(T_USER_NODE)?; + + let mut udata = user_nodes + .get((username, node.0))? + .map(|x| x.value().0) + .unwrap_or_default(); + + update(&mut udata)?; + + user_nodes.insert((username, node.0), Ser(udata))?; + drop(user_nodes); + txn.commit()?; + Ok(()) + } + + pub fn get_user(&self, username: &str) -> Result> { + let txn = self.inner.begin_read()?; + let t_user = txn.open_table(T_USER)?; + if let Some(user) = t_user.get(username)? { + Ok(Some(user.value().0)) + } else { + Ok(None) + } + } + pub fn update_user( + &self, + username: &str, + update: impl FnOnce(&mut User) -> Result<()>, + ) -> Result<()> { + let txn = self.inner.begin_write()?; + let mut users = txn.open_table(T_USER)?; + let mut user = users + .get(username)? + .ok_or(anyhow!("user does not exist"))? + .value() + .0; + update(&mut user)?; + users.insert(&username, Ser(user))?; + drop(users); + txn.commit()?; + + Ok(()) + } + pub fn delete_user(&self, username: &str) -> Result { + let txn = self.inner.begin_write()?; + let mut table = txn.open_table(T_USER)?; + let r = table.remove(username)?.is_some(); + drop(table); + txn.commit()?; + Ok(r) + } + pub fn list_users(&self) -> Result> { + let txn = self.inner.begin_read()?; + let table = txn.open_table(T_USER)?; + let i = table + .iter()? + .map(|a| { + let (_, y) = a.unwrap(); // TODO + y.value().0 + }) + .collect::>(); + drop(table); + Ok(i) + } + pub fn list_invites(&self) -> Result> { + let txn = self.inner.begin_read()?; + let table = txn.open_table(T_INVITE)?; + let i = table + .iter()? + .map(|a| { + let (x, _) = a.unwrap(); + x.value().to_owned() + }) + .collect::>(); + drop(table); + Ok(i) + } + pub fn create_invite(&self, inv: &str) -> Result<()> { + let txn = self.inner.begin_write()?; + let mut table = txn.open_table(T_INVITE)?; + table.insert(inv, ())?; + drop(table); + txn.commit()?; + Ok(()) + } + pub fn delete_invite(&self, inv: &str) -> Result { + let txn = self.inner.begin_write()?; + let mut table = txn.open_table(T_INVITE)?; + let r = table.remove(inv)?.is_some(); + drop(table); + txn.commit()?; + Ok(r) + } + pub fn register_user(&self, invite: &str, username: &str, user: User) -> Result<()> { + let txn = self.inner.begin_write()?; + let mut invites = txn.open_table(T_INVITE)?; + let mut users = txn.open_table(T_USER)?; + + if invites.remove(invite)?.is_none() { + bail!("invitation invalid"); + } + let prev_user = users.insert(username, Ser(user))?.map(|x| x.value().0); + if prev_user.is_some() { + bail!("username taken"); + } + + drop(users); + drop(invites); + txn.commit()?; + Ok(()) + } + pub fn list_nodes_with_udata(&self, username: &str) -> Result, NodeUserData)>> { + let txn = self.inner.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(), Arc::new(y.value().0)); + let z = node_users + .get(&(username, x)) + .unwrap() + .map(|z| z.value().0) + .unwrap_or_default(); + (y, z) + }) + .collect::>(); + drop(nodes); + Ok(i) + } + pub fn search(&self, query: &str, limit: usize, offset: usize) -> Result<(usize, Vec)> { + let query = QueryParser::for_index( + &self.text_search.index, + vec![self.text_search.title, self.text_search.description], + ) + .parse_query(query) + .context("parsing query")?; + + let searcher = self.text_search.reader.searcher(); + let sres = searcher.search(&query, &TopDocs::with_limit(limit).and_offset(offset))?; + let scount = searcher.search(&query, &Count)?; + + let mut results = Vec::new(); + for (_, daddr) in sres { + let doc: TantivyDocument = searcher.doc(daddr)?; + let id = doc + .get_first(self.text_search.id) + .unwrap() + .as_bytes() + .unwrap(); + let id = NodeID(id.try_into().unwrap()); + results.push(id); + } + Ok((scount, results)) + } + + pub fn search_create_index(&self) -> Result<()> { + let mut w = self.text_search.writer.write().unwrap(); + w.delete_all_documents()?; + + let txn = self.inner.begin_read()?; + let nodes = txn.open_table(T_NODE)?; + for node in nodes.iter()? { + let (x, y) = node?; + let (id, node) = (x.value().to_owned(), y.value().0); + + w.add_document(doc!( + self.text_search.id => id.to_vec(), + self.text_search.title => node.title.unwrap_or_default(), + self.text_search.description => node.description.unwrap_or_default(), + self.text_search.releasedate => DateTime::from_timestamp_millis(node.release_date.unwrap_or_default()), + self.text_search.f_index => node.index.unwrap_or_default() as u64, + ))?; + } + + w.commit()?; + Ok(()) + } + + pub fn create_admin_user(&self, username: &str, password_hash: Vec) -> Result<()> { + let txn = self.inner.begin_write().unwrap(); + let mut users = txn.open_table(T_USER).unwrap(); + + let admin = users.get(username).unwrap().map(|x| x.value().0); + users + .insert( + username, + Ser(User { + admin: true, + name: username.to_owned(), + password: password_hash, + ..admin.unwrap_or_else(|| User { + display_name: "Admin".to_string(), + ..Default::default() + }) + }), + ) + .unwrap(); + + drop(users); + txn.commit().unwrap(); + Ok(()) + } + pub fn get_import_file_mtime(&self, path: &Path) -> Result> { + let bytes = path.as_os_str().as_encoded_bytes(); + let txn = self.inner.begin_read()?; + let table = txn.open_table(T_IMPORT_FILE_MTIME)?; + if let Some(v) = table.get(bytes)? { + Ok(Some(v.value())) + } else { + Ok(None) + } + } + pub fn set_import_file_mtime(&self, path: &Path, mtime: u64) -> Result<()> { + let bytes = path.as_os_str().as_encoded_bytes(); + let txn = self.inner.begin_write()?; + let mut table = txn.open_table(T_IMPORT_FILE_MTIME)?; + table.insert(bytes, mtime)?; + drop(table); + txn.commit()?; + Ok(()) + } +} + +pub struct HashWriter(DefaultHasher); +impl bincode::enc::write::Writer for HashWriter { + fn write(&mut self, bytes: &[u8]) -> std::result::Result<(), bincode::error::EncodeError> { + self.0.write(bytes); + Ok(()) + } +} + +#[derive(Debug)] +#[cfg(not(feature = "db_json"))] +pub struct Ser(pub T); +#[cfg(not(feature = "db_json"))] +impl redb::Value for Ser { + type SelfType<'a> + = Ser + where + Self: 'a; + type AsBytes<'a> + = Vec + where + Self: 'a; + + fn fixed_width() -> Option { + 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 { + redb::TypeName::new("bincode") + } +} + +#[derive(Debug)] +#[cfg(feature = "db_json")] +pub struct Ser(pub T); +#[cfg(feature = "db_json")] +impl Deserialize<'a> + std::fmt::Debug> redb::Value for Ser { + type SelfType<'a> + = Ser + where + Self: 'a; + type AsBytes<'a> + = Vec + where + Self: 'a; + + fn fixed_width() -> Option { + 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 { + redb::TypeName::new("json") + } +} diff --git a/database/src/search.rs b/database/src/search.rs new file mode 100644 index 0000000..a7c074f --- /dev/null +++ b/database/src/search.rs @@ -0,0 +1,64 @@ +/* + 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. + Copyright (C) 2025 metamuffin +*/ +use anyhow::Context; +use std::{fs::create_dir_all, path::Path, sync::RwLock}; +use tantivy::{ + DateOptions, Index, IndexReader, IndexWriter, ReloadPolicy, + directory::MmapDirectory, + schema::{FAST, Field, INDEXED, STORED, STRING, Schema, TEXT}, +}; + +pub struct NodeTextSearchIndex { + pub schema: Schema, + pub reader: IndexReader, + pub writer: RwLock, + pub index: Index, + pub id: Field, + pub title: Field, + pub releasedate: Field, + pub description: Field, + pub parent: Field, + pub f_index: Field, +} +impl NodeTextSearchIndex { + pub(crate) fn new(path: &Path) -> anyhow::Result { + let mut schema = Schema::builder(); + let id = schema.add_text_field("id", TEXT | STORED | FAST); + let title = schema.add_text_field("title", TEXT); + let description = schema.add_text_field("description", TEXT); + let parent = schema.add_text_field("parent", STRING | FAST); + let f_index = schema.add_u64_field("index", FAST); + let releasedate = schema.add_date_field( + "releasedate", + DateOptions::from(INDEXED) + .set_fast() + .set_precision(tantivy::DateTimePrecision::Seconds), + ); + let schema = schema.build(); + create_dir_all(path.join("node_index"))?; + let directory = + MmapDirectory::open(path.join("node_index")).context("opening index directory")?; + let index = Index::open_or_create(directory, schema.clone()).context("creating index")?; + let reader = index + .reader_builder() + .reload_policy(ReloadPolicy::OnCommitWithDelay) + .try_into() + .context("creating reader")?; + let writer = index.writer(30_000_000).context("creating writer")?; + Ok(Self { + index, + writer: writer.into(), + reader, + schema, + parent, + f_index, + releasedate, + id, + description, + title, + }) + } +} diff --git a/import/Cargo.toml b/import/Cargo.toml index 112df40..d0b16b4 100644 --- a/import/Cargo.toml +++ b/import/Cargo.toml @@ -4,10 +4,12 @@ version = "0.1.0" edition = "2021" [dependencies] -jellybase = { path = "../base" } jellyremuxer = { path = "../remuxer" } jellycache = { path = "../cache" } +jellycommon = { path = "../common" } +jellydb = { path = "../database" } jellyimport-fallback-generator = { path = "fallback_generator" } +jellyimport-asset-token = { path = "asset_token" } rayon = "1.10.0" crossbeam-channel = "0.5.14" diff --git a/import/asset_token/Cargo.toml b/import/asset_token/Cargo.toml new file mode 100644 index 0000000..95615ce --- /dev/null +++ b/import/asset_token/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "jellyimport-asset-token" +version = "0.1.0" +edition = "2021" + +[dependencies] +jellycommon = { path = "../../common" } +jellycache = { path = "../../cache" } +serde = { version = "1.0.217", features = ["derive"] } +serde_yaml = "0.9.34" +log = { workspace = true } +sha2 = "0.10.8" +base64 = "0.22.1" +tokio = { workspace = true } +anyhow = "1.0.95" +bincode = "2.0.0-rc.3" +rand = "0.9.0" +serde_json = "1.0.138" +aes-gcm-siv = "0.11.1" +humansize = "2.1.3" diff --git a/import/asset_token/src/lib.rs b/import/asset_token/src/lib.rs new file mode 100644 index 0000000..87ea261 --- /dev/null +++ b/import/asset_token/src/lib.rs @@ -0,0 +1,105 @@ +/* + 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. + Copyright (C) 2025 metamuffin +*/ +pub mod assetfed; + +use aes_gcm_siv::{ + aead::{generic_array::GenericArray, Aead}, + Aes256GcmSiv, KeyInit, +}; +use anyhow::{anyhow, bail, Context}; +use base64::Engine; +use bincode::{Decode, Encode}; +use jellycache::CachePath; +pub use jellycommon as common; +use jellycommon::{Asset, LocalTrack}; +use log::warn; +use serde::{Deserialize, Serialize}; +use std::sync::Mutex; +use std::{path::PathBuf, sync::LazyLock}; + +#[rustfmt::skip] +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct Config { + asset_key: Option, +} + +pub static CONF_PRELOAD: Mutex> = Mutex::new(None); +static CONF: LazyLock = LazyLock::new(|| { + CONF_PRELOAD + .lock() + .unwrap() + .take() + .expect("cache config not preloaded. logic error") +}); + +const VERSION: u32 = 3; + +static ASSET_KEY: LazyLock = LazyLock::new(|| { + if let Some(sk) = &CONF.asset_key { + let r = base64::engine::general_purpose::STANDARD + .decode(sk) + .expect("key invalid; should be valid base64"); + aes_gcm_siv::Aes256GcmSiv::new_from_slice(&r) + .expect("key has the wrong length; should be 32 bytes") + } else { + warn!("session_key not configured; generating a random one."); + aes_gcm_siv::Aes256GcmSiv::new_from_slice(&[(); 32].map(|_| rand::random())).unwrap() + } +}); + +#[derive(Debug, Encode, Decode, Serialize)] +pub enum AssetInner { + Federated { host: String, asset: Vec }, + Cache(CachePath), + Assets(PathBuf), + Media(PathBuf), + LocalTrack(LocalTrack), +} + +impl AssetInner { + pub fn ser(&self) -> Asset { + let mut plaintext = Vec::new(); + plaintext.extend(u32::to_le_bytes(VERSION)); + plaintext.extend(bincode::encode_to_vec(self, bincode::config::standard()).unwrap()); + + while plaintext.len() % 16 == 0 { + plaintext.push(0); + } + + let nonce = [(); 12].map(|_| rand::random()); + let mut ciphertext = ASSET_KEY + .encrypt(&GenericArray::from(nonce), plaintext.as_slice()) + .unwrap(); + ciphertext.extend(nonce); + + Asset(base64::engine::general_purpose::URL_SAFE.encode(&ciphertext)) + } + pub fn deser(s: &str) -> anyhow::Result { + let ciphertext = base64::engine::general_purpose::URL_SAFE.decode(s)?; + let (ciphertext, nonce) = ciphertext.split_at(ciphertext.len() - 12); + let plaintext = ASSET_KEY + .decrypt(nonce.into(), ciphertext) + .map_err(|_| anyhow!("asset token decrypt failed"))?; + + let version = u32::from_le_bytes(plaintext[0..4].try_into().unwrap()); + if version != VERSION { + bail!("asset token version mismatch"); + } + + let (data, _): (AssetInner, _) = + bincode::decode_from_slice(&plaintext[4..], bincode::config::standard()) + .context("asset token has invalid format")?; + Ok(data) + } + + /// Returns `true` if the asset inner is [`Federated`]. + /// + /// [`Federated`]: AssetInner::Federated + #[must_use] + pub fn is_federated(&self) -> bool { + matches!(self, Self::Federated { .. }) + } +} diff --git a/import/src/infojson.rs b/import/src/infojson.rs index 1efbae9..e0ebc43 100644 --- a/import/src/infojson.rs +++ b/import/src/infojson.rs @@ -5,7 +5,7 @@ */ use anyhow::Context; use bincode::{Decode, Encode}; -use jellybase::common::chrono::{format::Parsed, Utc}; +use jellycommon::chrono::{format::Parsed, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; diff --git a/import/src/lib.rs b/import/src/lib.rs index 784b717..426a96a 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -14,18 +14,18 @@ pub mod vgmdb; pub mod wikidata; pub mod wikimedia_commons; +use jellydb::Database; +pub use jellyimport_asset_token as asset_token; +use jellyimport_asset_token::AssetInner; + use acoustid::{acoustid_fingerprint, AcoustID}; use anyhow::{anyhow, bail, Context, Result}; use infojson::YVideo; -use jellybase::{ - assetfed::AssetInner, - common::{ - Appearance, Chapter, LocalTrack, MediaInfo, Node, NodeID, NodeKind, ObjectIds, PeopleGroup, - Person, Rating, SourceTrack, SourceTrackKind, TmdbKind, TrackSource, TraktKind, Visibility, - }, - database::Database, -}; use jellycache::cache_file; +use jellycommon::{ + Appearance, Chapter, LocalTrack, MediaInfo, Node, NodeID, NodeKind, ObjectIds, PeopleGroup, + Person, Rating, SourceTrack, SourceTrackKind, TmdbKind, TrackSource, TraktKind, Visibility, +}; use jellyimport_fallback_generator::generate_fallback; use jellyremuxer::metadata::checked_matroska_metadata; use log::info; diff --git a/import/src/tmdb.rs b/import/src/tmdb.rs index dff0e95..ceb1650 100644 --- a/import/src/tmdb.rs +++ b/import/src/tmdb.rs @@ -6,11 +6,11 @@ use crate::USER_AGENT; use anyhow::{anyhow, bail, Context}; use bincode::{Decode, Encode}; -use jellybase::common::{ +use jellycache::{async_cache_file, async_cache_memory, CachePath}; +use jellycommon::{ chrono::{format::Parsed, Utc}, TmdbKind, }; -use jellycache::{async_cache_file, async_cache_memory, CachePath}; use log::info; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, diff --git a/import/src/trakt.rs b/import/src/trakt.rs index 434a3a0..f25fa9e 100644 --- a/import/src/trakt.rs +++ b/import/src/trakt.rs @@ -6,8 +6,8 @@ use crate::USER_AGENT; use anyhow::Context; use bincode::{Decode, Encode}; -use jellybase::common::{Appearance, ObjectIds, PeopleGroup, Person, TraktKind}; use jellycache::async_cache_memory; +use jellycommon::{Appearance, ObjectIds, PeopleGroup, Person, TraktKind}; use log::info; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, diff --git a/logic/Cargo.toml b/logic/Cargo.toml index ec5ee2b..23016f9 100644 --- a/logic/Cargo.toml +++ b/logic/Cargo.toml @@ -4,8 +4,9 @@ version = "0.1.0" edition = "2024" [dependencies] -jellybase = { path = "../base" } +jellyimport-asset-token = { path = "../import/asset_token" } jellycommon = { path = "../common" } +jellydb = { path = "../database" } log = "0.4.27" anyhow = "1.0.98" base64 = "0.22.1" diff --git a/logic/src/admin/user.rs b/logic/src/admin/user.rs index 2d788cb..3ec3852 100644 --- a/logic/src/admin/user.rs +++ b/logic/src/admin/user.rs @@ -6,8 +6,8 @@ use crate::session::AdminSession; use anyhow::Result; -use jellybase::database::Database; use jellycommon::api::ApiAdminUsersResponse; +use jellydb::Database; pub fn admin_users(db: &Database, _session: &AdminSession) -> Result { // TODO dont return useless info like passwords diff --git a/logic/src/home.rs b/logic/src/home.rs index b774a9f..ad3fee5 100644 --- a/logic/src/home.rs +++ b/logic/src/home.rs @@ -6,13 +6,13 @@ use crate::{node::DatabaseNodeUserDataExt, session::Session}; use anyhow::{Context, Result}; -use jellybase::database::Database; use jellycommon::{ NodeID, NodeKind, Rating, Visibility, api::ApiHomeResponse, chrono::{Datelike, Utc}, user::WatchedState, }; +use jellydb::Database; pub fn home(db: &Database, session: &Session) -> Result { let mut items = db.list_nodes_with_udata(&session.user.name)?; diff --git a/logic/src/items.rs b/logic/src/items.rs index 67c45cb..99fb767 100644 --- a/logic/src/items.rs +++ b/logic/src/items.rs @@ -6,11 +6,11 @@ use crate::{filter_sort::filter_and_sort_nodes, session::Session}; use anyhow::Result; -use jellybase::database::Database; use jellycommon::{ Visibility, api::{ApiItemsResponse, NodeFilterSort, SortOrder, SortProperty}, }; +use jellydb::Database; pub fn all_items( db: &Database, diff --git a/logic/src/lib.rs b/logic/src/lib.rs index 64656f5..79d27d9 100644 --- a/logic/src/lib.rs +++ b/logic/src/lib.rs @@ -15,6 +15,8 @@ pub mod search; pub mod session; pub mod stats; +pub use jellydb::Database; + use serde::{Deserialize, Serialize}; use std::sync::LazyLock; use std::sync::Mutex; diff --git a/logic/src/login.rs b/logic/src/login.rs index 26a6b7f..72a5903 100644 --- a/logic/src/login.rs +++ b/logic/src/login.rs @@ -6,8 +6,8 @@ use crate::{CONF, session::create}; use anyhow::{Result, anyhow}; use argon2::{Argon2, PasswordHasher, password_hash::Salt}; -use jellybase::database::Database; use jellycommon::user::UserPermission; +use jellydb::Database; use log::info; use std::{collections::HashSet, time::Duration}; diff --git a/logic/src/node.rs b/logic/src/node.rs index 8a53bec..c8ff820 100644 --- a/logic/src/node.rs +++ b/logic/src/node.rs @@ -5,12 +5,12 @@ */ use crate::{filter_sort::filter_and_sort_nodes, session::Session}; use anyhow::{Result, anyhow}; -use jellybase::database::Database; use jellycommon::{ Node, NodeID, NodeKind, Visibility, api::{ApiNodeResponse, NodeFilterSort, SortOrder, SortProperty}, user::NodeUserData, }; +use jellydb::Database; use std::{cmp::Reverse, collections::BTreeMap, sync::Arc}; pub fn get_node( diff --git a/logic/src/search.rs b/logic/src/search.rs index 8e41e27..68975f1 100644 --- a/logic/src/search.rs +++ b/logic/src/search.rs @@ -5,8 +5,8 @@ */ use crate::{node::DatabaseNodeUserDataExt, session::Session}; use anyhow::Result; -use jellybase::database::Database; use jellycommon::{Visibility, api::ApiSearchResponse}; +use jellydb::Database; use std::time::Instant; pub fn search( diff --git a/logic/src/stats.rs b/logic/src/stats.rs index 2569180..2e962e2 100644 --- a/logic/src/stats.rs +++ b/logic/src/stats.rs @@ -6,11 +6,11 @@ use crate::session::Session; use anyhow::Result; -use jellybase::database::Database; use jellycommon::{ Node, NodeKind, Visibility, api::{ApiStatsResponse, StatsBin}, }; +use jellydb::Database; use std::collections::BTreeMap; pub fn stats(db: &Database, session: &Session) -> Result { @@ -34,7 +34,6 @@ pub fn stats(db: &Database, session: &Session) -> Result { } } } - } let mut total = StatsBin::default(); diff --git a/server/Cargo.toml b/server/Cargo.toml index be8abdb..0bc1960 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -5,7 +5,6 @@ edition = "2021" [dependencies] jellycommon = { path = "../common" } -jellybase = { path = "../base" } jellystream = { path = "../stream" } jellytranscoder = { path = "../transcoder" } jellyimport = { path = "../import" } diff --git a/server/src/api.rs b/server/src/api.rs index fb5ee88..5ec3c5c 100644 --- a/server/src/api.rs +++ b/server/src/api.rs @@ -4,12 +4,13 @@ Copyright (C) 2025 metamuffin */ use super::ui::error::MyResult; -use crate::{database::Database, helper::A}; -use jellybase::assetfed::AssetInner; +use crate::helper::A; use jellycommon::{user::CreateSessionParams, NodeID, Visibility}; +use jellyimport::asset_token::AssetInner; use jellylogic::{ login::login_logic, session::{AdminSession, Session}, + Database, }; use rocket::{ get, diff --git a/server/src/compat/jellyfin/mod.rs b/server/src/compat/jellyfin/mod.rs index 1c602ca..e8a74d7 100644 --- a/server/src/compat/jellyfin/mod.rs +++ b/server/src/compat/jellyfin/mod.rs @@ -7,7 +7,6 @@ pub mod models; use crate::{helper::A, ui::error::MyResult}; use anyhow::{anyhow, Context}; -use jellybase::database::Database; use jellycommon::{ api::{FilterProperty, NodeFilterSort, SortOrder, SortProperty}, routes::{u_asset, u_node_slug_backdrop, u_node_slug_poster}, @@ -17,7 +16,7 @@ use jellycommon::{ }; use jellylogic::{ filter_sort::filter_and_sort_nodes, login::login_logic, node::DatabaseNodeUserDataExt, - session::Session, + session::Session, Database, }; use jellyui::{get_brand, get_slogan, node_page::aspect_class}; use models::*; diff --git a/server/src/compat/youtube.rs b/server/src/compat/youtube.rs index 67a34fc..0a69d14 100644 --- a/server/src/compat/youtube.rs +++ b/server/src/compat/youtube.rs @@ -5,9 +5,8 @@ */ use crate::{helper::A, ui::error::MyResult}; use anyhow::anyhow; -use jellybase::database::Database; use jellycommon::routes::{u_node_slug, u_node_slug_player}; -use jellylogic::session::Session; +use jellylogic::{session::Session, Database}; use rocket::{get, response::Redirect, State}; #[get("/watch?")] diff --git a/server/src/config.rs b/server/src/config.rs index 68148dd..202948a 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -16,7 +16,7 @@ struct Config { stream: jellystream::Config, cache: jellycache::Config, server: crate::Config, - base: jellybase::Config, + base: jellyimport::asset_token::Config, logic: jellylogic::Config, import: jellyimport::Config, } @@ -36,7 +36,7 @@ pub async fn load_config() -> Result<()> { *jellytranscoder::CONF_PRELOAD.lock().unwrap() = Some(config.transcoder); *jellycache::CONF_PRELOAD.lock().unwrap() = Some(config.cache); *jellylogic::CONF_PRELOAD.lock().unwrap() = Some(config.logic); - *jellybase::CONF_PRELOAD.lock().unwrap() = Some(config.base); + *jellyimport::asset_token::CONF_PRELOAD.lock().unwrap() = Some(config.base); *jellyimport::CONF_PRELOAD.lock().unwrap() = Some(config.import); *crate::CONF_PRELOAD.lock().unwrap() = Some(config.server); *jellyui::CONF_PRELOAD.lock().unwrap() = Some(config.ui); diff --git a/server/src/helper/session.rs b/server/src/helper/session.rs index b77f9fa..7e23152 100644 --- a/server/src/helper/session.rs +++ b/server/src/helper/session.rs @@ -5,8 +5,10 @@ */ use crate::ui::error::MyError; use anyhow::anyhow; -use jellybase::database::Database; -use jellylogic::session::{validate, AdminSession, Session}; +use jellylogic::{ + session::{validate, AdminSession, Session}, + Database, +}; use log::warn; use rocket::{ async_trait, diff --git a/server/src/logic/stream.rs b/server/src/logic/stream.rs index 89589c7..c21edaa 100644 --- a/server/src/logic/stream.rs +++ b/server/src/logic/stream.rs @@ -3,11 +3,11 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ -use crate::{database::Database, helper::A, ui::error::MyError}; +use crate::{helper::A, ui::error::MyError}; use anyhow::{anyhow, Result}; -use jellybase::assetfed::AssetInner; use jellycommon::{stream::StreamSpec, TrackSource}; -use jellylogic::session::Session; +use jellyimport::asset_token::AssetInner; +use jellylogic::{session::Session, Database}; use jellystream::SMediaInfo; use log::{info, warn}; use rocket::{ diff --git a/server/src/logic/userdata.rs b/server/src/logic/userdata.rs index 25d3893..ac3cb83 100644 --- a/server/src/logic/userdata.rs +++ b/server/src/logic/userdata.rs @@ -4,13 +4,12 @@ Copyright (C) 2025 metamuffin */ use crate::{helper::A, ui::error::MyResult}; -use jellybase::database::Database; use jellycommon::{ routes::u_node_id, user::{NodeUserData, WatchedState}, NodeID, }; -use jellylogic::session::Session; +use jellylogic::{session::Session, Database}; use rocket::{ form::Form, get, post, response::Redirect, serde::json::Json, FromForm, FromFormField, State, UriDisplayQuery, diff --git a/server/src/main.rs b/server/src/main.rs index ea75208..0e237f9 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -9,7 +9,7 @@ use anyhow::Context; use config::load_config; -use database::Database; +use jellylogic::Database; use jellylogic::{admin::log::enable_logging, login::create_admin_account}; use log::{error, info, warn}; use routes::build_rocket; @@ -17,7 +17,6 @@ use serde::{Deserialize, Serialize}; use std::sync::Mutex; use std::{path::PathBuf, process::exit, sync::LazyLock}; -pub use jellybase::database; pub mod api; pub mod compat; pub mod config; diff --git a/server/src/routes.rs b/server/src/routes.rs index da3a389..cc68067 100644 --- a/server/src/routes.rs +++ b/server/src/routes.rs @@ -3,7 +3,6 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ -use crate::database::Database; use crate::logic::playersync::{r_playersync, PlayersyncChannels}; use crate::ui::account::{r_account_login, r_account_logout, r_account_register}; use crate::ui::{ @@ -62,6 +61,7 @@ use crate::{ }, }; use base64::Engine; +use jellylogic::Database; use log::warn; use rand::random; use rocket::{ diff --git a/server/src/ui/account/mod.rs b/server/src/ui/account/mod.rs index a9c28ea..f3cd450 100644 --- a/server/src/ui/account/mod.rs +++ b/server/src/ui/account/mod.rs @@ -7,7 +7,6 @@ pub mod settings; use super::error::MyError; use crate::{ - database::Database, helper::A, locale::AcceptLanguage, ui::{error::MyResult, home::rocket_uri_macro_r_home}, @@ -18,6 +17,7 @@ use jellyimport::is_importing; use jellylogic::{ login::{hash_password, login_logic}, session::Session, + Database, }; use jellyui::{ account::{AccountLogin, AccountLogout, AccountRegister, AccountRegisterSuccess}, diff --git a/server/src/ui/account/settings.rs b/server/src/ui/account/settings.rs index 5355321..93f442c 100644 --- a/server/src/ui/account/settings.rs +++ b/server/src/ui/account/settings.rs @@ -4,11 +4,10 @@ Copyright (C) 2025 metamuffin */ use super::{format_form_error, hash_password}; -use crate::{database::Database, helper::A, locale::AcceptLanguage, ui::error::MyResult}; -use jellybase::permission::PermissionSetExt; -use jellycommon::user::{PlayerKind, Theme, UserPermission}; +use crate::{helper::A, locale::AcceptLanguage, ui::error::MyResult}; +use jellycommon::user::{PlayerKind, Theme}; use jellyimport::is_importing; -use jellylogic::session::Session; +use jellylogic::{session::Session, Database}; use jellyui::{ account::settings::SettingsPage, locale::{tr, Language}, @@ -74,10 +73,6 @@ pub fn r_account_settings_post( ) -> MyResult> { let AcceptLanguage(lang) = lang; let A(session) = session; - session - .user - .permissions - .assert(&UserPermission::ManageSelf)?; let form = match &form.value { Some(v) => v, diff --git a/server/src/ui/admin/mod.rs b/server/src/ui/admin/mod.rs index 62c5940..9bf85b5 100644 --- a/server/src/ui/admin/mod.rs +++ b/server/src/ui/admin/mod.rs @@ -10,12 +10,11 @@ use super::{ assets::{resolve_asset, AVIF_QUALITY, AVIF_SPEED}, error::MyResult, }; -use crate::{database::Database, helper::A, locale::AcceptLanguage}; +use crate::{helper::A, locale::AcceptLanguage}; use anyhow::{anyhow, Context}; -use jellybase::assetfed::AssetInner; use jellycommon::routes::u_admin_dashboard; -use jellyimport::{import_wrap, is_importing, IMPORT_ERRORS}; -use jellylogic::session::AdminSession; +use jellyimport::{asset_token::AssetInner, import_wrap, is_importing, IMPORT_ERRORS}; +use jellylogic::{session::AdminSession, Database}; use jellyui::{ admin::AdminDashboardPage, render_page, diff --git a/server/src/ui/admin/user.rs b/server/src/ui/admin/user.rs index fb646ab..eedf66c 100644 --- a/server/src/ui/admin/user.rs +++ b/server/src/ui/admin/user.rs @@ -3,11 +3,11 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ -use crate::{database::Database, helper::A, locale::AcceptLanguage, ui::error::MyResult}; +use crate::{helper::A, locale::AcceptLanguage, ui::error::MyResult}; use anyhow::{anyhow, Context}; use jellycommon::user::UserPermission; use jellyimport::is_importing; -use jellylogic::{admin::user::admin_users, session::AdminSession}; +use jellylogic::{admin::user::admin_users, session::AdminSession, Database}; use jellyui::{ admin::user::{AdminUserPage, AdminUsersPage}, render_page, diff --git a/server/src/ui/assets.rs b/server/src/ui/assets.rs index 596661a..4e09417 100644 --- a/server/src/ui/assets.rs +++ b/server/src/ui/assets.rs @@ -6,9 +6,9 @@ use super::error::MyResult; use crate::{helper::{cache::CacheControlFile, A}, CONF}; use anyhow::{anyhow, bail, Context}; -use jellybase::{assetfed::AssetInner, database::Database}; use jellycommon::{LocalTrack, NodeID, PeopleGroup, SourceTrackKind, TrackSource}; -use jellylogic::session::Session; +use jellyimport::asset_token::AssetInner; +use jellylogic::{session::Session, Database}; use log::info; use rocket::{get, http::ContentType, response::Redirect, State}; use std::path::PathBuf; diff --git a/server/src/ui/home.rs b/server/src/ui/home.rs index 9c9c1ca..d323b11 100644 --- a/server/src/ui/home.rs +++ b/server/src/ui/home.rs @@ -6,10 +6,9 @@ use super::error::MyResult; use crate::{api::AcceptJson, helper::A, locale::AcceptLanguage}; -use jellybase::database::Database; use jellycommon::api::ApiHomeResponse; use jellyimport::is_importing; -use jellylogic::session::Session; +use jellylogic::{session::Session, Database}; use jellyui::{ home::HomePage, render_page, diff --git a/server/src/ui/items.rs b/server/src/ui/items.rs index e5aa050..31902f8 100644 --- a/server/src/ui/items.rs +++ b/server/src/ui/items.rs @@ -4,10 +4,10 @@ Copyright (C) 2025 metamuffin */ use super::error::MyError; -use crate::{api::AcceptJson, database::Database, helper::A, locale::AcceptLanguage}; +use crate::{api::AcceptJson, helper::A, locale::AcceptLanguage}; use jellycommon::api::{ApiItemsResponse, NodeFilterSort}; use jellyimport::is_importing; -use jellylogic::{items::all_items, session::Session}; +use jellylogic::{items::all_items, session::Session, Database}; use jellyui::{ items::ItemsPage, render_page, diff --git a/server/src/ui/node.rs b/server/src/ui/node.rs index 1441cfc..eced16a 100644 --- a/server/src/ui/node.rs +++ b/server/src/ui/node.rs @@ -4,13 +4,13 @@ Copyright (C) 2025 metamuffin */ use super::error::MyResult; -use crate::{api::AcceptJson, database::Database, helper::A, locale::AcceptLanguage}; +use crate::{api::AcceptJson, helper::A, locale::AcceptLanguage}; use jellycommon::{ api::{ApiNodeResponse, NodeFilterSort}, NodeID, }; use jellyimport::is_importing; -use jellylogic::{node::get_node, session::Session}; +use jellylogic::{node::get_node, session::Session, Database}; use jellyui::{ node_page::NodePage, render_page, diff --git a/server/src/ui/player.rs b/server/src/ui/player.rs index 300e9d2..614bf4a 100644 --- a/server/src/ui/player.rs +++ b/server/src/ui/player.rs @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin */ use super::error::MyResult; -use crate::{database::Database, helper::A, locale::AcceptLanguage, CONF}; +use crate::{helper::A, locale::AcceptLanguage, CONF}; use jellycommon::{ api::NodeFilterSort, stream::{StreamContainer, StreamSpec}, @@ -12,7 +12,7 @@ use jellycommon::{ NodeID, }; use jellyimport::is_importing; -use jellylogic::{node::get_node, session::Session}; +use jellylogic::{node::get_node, session::Session, Database}; use jellyui::{ node_page::NodePage, render_page, diff --git a/server/src/ui/search.rs b/server/src/ui/search.rs index 1c2ea70..92fcfce 100644 --- a/server/src/ui/search.rs +++ b/server/src/ui/search.rs @@ -6,10 +6,9 @@ use super::error::MyResult; use crate::{api::AcceptJson, helper::A, locale::AcceptLanguage}; use anyhow::anyhow; -use jellybase::database::Database; use jellycommon::api::ApiSearchResponse; use jellyimport::is_importing; -use jellylogic::{search::search, session::Session}; +use jellylogic::{search::search, session::Session, Database}; use jellyui::{ render_page, scaffold::{RenderInfo, SessionInfo}, diff --git a/server/src/ui/stats.rs b/server/src/ui/stats.rs index b6a74e5..d991fa0 100644 --- a/server/src/ui/stats.rs +++ b/server/src/ui/stats.rs @@ -4,10 +4,10 @@ Copyright (C) 2025 metamuffin */ use super::error::MyError; -use crate::{api::AcceptJson, database::Database, helper::A, locale::AcceptLanguage}; +use crate::{api::AcceptJson, helper::A, locale::AcceptLanguage}; use jellycommon::api::ApiStatsResponse; use jellyimport::is_importing; -use jellylogic::{session::Session, stats::stats}; +use jellylogic::{session::Session, stats::stats, Database}; use jellyui::{ render_page, scaffold::{RenderInfo, SessionInfo}, -- cgit v1.2.3-70-g09d2