diff options
-rw-r--r-- | Cargo.lock | 12 | ||||
-rw-r--r-- | common/src/lib.rs | 11 | ||||
-rw-r--r-- | server/Cargo.toml | 1 | ||||
-rw-r--r-- | server/src/import.rs | 43 | ||||
-rw-r--r-- | server/src/main.rs | 17 | ||||
-rw-r--r-- | server/src/routes/mod.rs | 5 | ||||
-rw-r--r-- | server/src/routes/ui/account/admin.rs | 58 | ||||
-rw-r--r-- | server/src/routes/ui/error.rs | 2 | ||||
-rw-r--r-- | server/src/routes/ui/layout.rs | 9 | ||||
-rw-r--r-- | tools/src/bin/import.rs | 23 |
10 files changed, 108 insertions, 73 deletions
@@ -228,6 +228,17 @@ dependencies = [ ] [[package]] +name = "async-recursion" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] name = "async-std" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1248,6 +1259,7 @@ dependencies = [ "aes-gcm-siv", "anyhow", "argon2", + "async-recursion", "async-std", "base64", "bincode 2.0.0-rc.3", diff --git a/common/src/lib.rs b/common/src/lib.rs index 3f91a6c..34de946 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -3,8 +3,8 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2023 metamuffin <metamuffin.org> */ -pub mod r#impl; pub mod config; +pub mod r#impl; use bincode::{Decode, Encode}; use serde::{Deserialize, Serialize}; @@ -19,6 +19,7 @@ pub struct Node { #[rustfmt::skip] #[derive(Debug, Clone, Deserialize, Serialize)] pub struct NodePrivate { + #[serde(default)] pub import: Option<ImportOptions>, #[serde(default)] pub poster: Option<PathBuf>, #[serde(default)] pub backdrop: Option<PathBuf>, #[serde(default)] pub source: Option<MediaSource>, @@ -37,6 +38,14 @@ pub struct NodePublic { #[serde(default)] pub media: Option<MediaInfo>, } +#[rustfmt::skip] +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ImportOptions { + pub host: String, + pub id: String, + #[serde(default)] pub prefix: Option<String>, +} + #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum NodeKind { diff --git a/server/Cargo.toml b/server/Cargo.toml index eccdd74..6118240 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -25,6 +25,7 @@ argon2 = "0.5.1" aes-gcm-siv = "0.11.1" async-std = "1.12.0" +async-recursion = "1.0.4" tokio = { version = "1.29.1", features = ["io-util"] } tokio-util = { version = "0.7.8", features = ["io", "io-util"] } diff --git a/server/src/import.rs b/server/src/import.rs index c8f4f10..b306332 100644 --- a/server/src/import.rs +++ b/server/src/import.rs @@ -5,27 +5,38 @@ */ use crate::{database::Database, CONF}; use anyhow::{bail, Context, Ok}; +use async_recursion::async_recursion; use jellycommon::Node; use log::info; -use std::{ffi::OsStr, fs::File, os::unix::prelude::OsStrExt, path::PathBuf}; +use std::{ffi::OsStr, fs::File, os::unix::prelude::OsStrExt, path::PathBuf, sync::LazyLock}; +use tokio::sync::Semaphore; -pub fn import(db: &Database) -> anyhow::Result<()> { +static IMPORT_SEM: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(1)); + +pub async fn import(db: &Database) -> anyhow::Result<()> { info!("clearing node tree"); - db.node.clear()?; - info!("importing..."); - import_path(CONF.library_path.clone(), db, None).context("indexing")?; + let permit = IMPORT_SEM.try_acquire()?; + { + db.node.clear()?; + info!("importing..."); + import_path(CONF.library_path.clone(), db, None) + .await + .context("indexing")?; + info!("import completed"); + } + drop(permit); Ok(()) } -pub fn import_path( +#[async_recursion] +pub async fn import_path( path: PathBuf, db: &Database, parent: Option<String>, ) -> anyhow::Result<Vec<String>> { if path.is_dir() { let mpath = path.join("directory.json"); - let children = path.read_dir()?.filter_map(|e| { - let e = e.unwrap(); + let children_paths = path.read_dir()?.map(Result::unwrap).filter_map(|e| { if e.path().extension() == Some(&OsStr::from_bytes(b"jelly")) || e.metadata().unwrap().is_dir() { @@ -35,23 +46,21 @@ pub fn import_path( } }); let identifier = path.file_name().unwrap().to_str().unwrap().to_string(); - let children = children - .map(|e| import_path(e, db, Some(identifier.clone()))) - .collect::<anyhow::Result<Vec<_>>>()? - .into_iter() - .flatten() - .collect(); + let mut children_ids = Vec::new(); + for p in children_paths { + children_ids.extend(import_path(p, db, Some(identifier.clone())).await?) + } if mpath.exists() { let mut data: Node = serde_json::from_reader(File::open(mpath).context("metadata missing")?)?; - data.public.children = children; + data.public.children = children_ids; data.public.parent = parent; info!("insert {identifier}"); db.node.insert(&identifier, &data)?; Ok(vec![identifier]) } else { - Ok(children) + Ok(children_ids) } } else if path.is_file() { info!("loading item {path:?}"); @@ -65,7 +74,7 @@ pub fn import_path( .strip_suffix(".jelly") .unwrap() .to_string(); - + info!("insert {identifier}"); data.public.parent = parent; db.node.insert(&identifier, &data)?; diff --git a/server/src/main.rs b/server/src/main.rs index 341f72b..5b2d070 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -5,13 +5,12 @@ */ #![feature(lazy_cell)] -use std::fs::File; use database::Database; use jellycommon::config::GlobalConfig; use jellyremuxer::RemuxerContext; use once_cell::sync::Lazy; -use rocket::launch; use routes::build_rocket; +use std::fs::File; pub mod database; pub mod import; @@ -20,8 +19,7 @@ pub mod routes; pub static CONF: Lazy<GlobalConfig> = Lazy::new(|| serde_json::from_reader(File::open("data/config.json").unwrap()).unwrap()); -#[launch] -fn rocket() -> _ { +fn main() { env_logger::builder() .filter_level(log::LevelFilter::Info) .parse_env("LOG") @@ -30,9 +28,16 @@ fn rocket() -> _ { #[cfg(feature = "bypass-auth")] log::warn!("authentification bypass enabled"); + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(async_main()) +} +async fn async_main() { let remuxer = RemuxerContext::new(); let database = Database::open(&CONF.database_path).unwrap(); - import::import(&database).unwrap(); + import::import(&database).await.unwrap(); database.create_admin(); - build_rocket(remuxer, database) + build_rocket(remuxer, database).launch().await.unwrap(); } diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 7f4789d..be4d2cb 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -18,8 +18,8 @@ use stream::r_stream; use ui::{ account::{ admin::{ - r_account_admin_dashboard, r_account_admin_invite, r_account_admin_remove_invite, - r_account_admin_remove_user, + r_account_admin_dashboard, r_account_admin_import, r_account_admin_invite, + r_account_admin_remove_invite, r_account_admin_remove_user, }, r_account_login, r_account_login_post, r_account_logout, r_account_logout_post, r_account_register, r_account_register_post, @@ -92,6 +92,7 @@ pub fn build_rocket(remuxer: RemuxerContext, database: Database) -> Rocket<Build r_account_admin_invite, r_account_admin_remove_user, r_account_admin_remove_invite, + r_account_admin_import, r_account_settings, r_account_settings_post, r_api_version, diff --git a/server/src/routes/ui/account/admin.rs b/server/src/routes/ui/account/admin.rs index 37457b0..7124f4a 100644 --- a/server/src/routes/ui/account/admin.rs +++ b/server/src/routes/ui/account/admin.rs @@ -5,10 +5,11 @@ */ use crate::{ database::Database, + import::import, routes::ui::{ account::session::Session, error::MyResult, - layout::{DynLayoutPage, LayoutPage}, + layout::{DynLayoutPage, FlashDisplay, LayoutPage}, }, uri, }; @@ -24,16 +25,28 @@ pub fn r_account_admin_dashboard( if !session.user.admin { Err(anyhow!("you not admin"))? } + admin_dashboard(database, None) +} +pub fn admin_dashboard<'a>( + database: &Database, + flash: Option<MyResult<String>>, +) -> MyResult<DynLayoutPage<'a>> { // TODO this doesnt scale, pagination! let users = database.user.iter().collect::<Result<Vec<_>, _>>()?; let invites = database.invite.iter().collect::<Result<Vec<_>, _>>()?; + let flash = flash.map(|f| f.map_err(|e| format!("{e:?}"))); Ok(LayoutPage { title: "Admin Dashboard".to_string(), content: markup::new! { h1 { "Admin Panel" } - h2 { "Invitations"} + @FlashDisplay { flash: flash.clone() } + h2 { "Library" } + form[method="POST", action=uri!(r_account_admin_import())] { + input[type="submit", value="(Re-)Import Library"]; + } + h2 { "Invitations" } form[method="POST", action=uri!(r_account_admin_invite())] { input[type="submit", value="Generate new invite code"]; } @@ -71,13 +84,7 @@ pub fn r_account_admin_invite( let i = format!("{}", rand::thread_rng().gen::<u128>()); database.invite.insert(&i, &())?; - Ok(LayoutPage { - title: "Admin Dashboard".to_string(), - content: markup::new! { - pre { code { @i } } - }, - ..Default::default() - }) + admin_dashboard(database, Some(Ok(format!("Invite: {}", i)))) } #[derive(FromForm)] @@ -99,14 +106,7 @@ pub fn r_account_admin_remove_user( .remove(&form.name)? .ok_or(anyhow!("user did not exist"))?; - Ok(LayoutPage { - title: "User removed".to_string(), - content: markup::new! { - p { "User removed" } - a[href=uri!(r_account_admin_dashboard())] {"Back"} - }, - ..Default::default() - }) + admin_dashboard(database, Some(Ok("User removed".into()))) } #[derive(FromForm)] @@ -128,12 +128,20 @@ pub fn r_account_admin_remove_invite( .remove(&form.invite)? .ok_or(anyhow!("invite did not exist"))?; - Ok(LayoutPage { - title: "Invite invalidated".to_string(), - content: markup::new! { - p { "Invite invalidated" } - a[href=uri!(r_account_admin_dashboard())] {"Back"} - }, - ..Default::default() - }) + admin_dashboard(database, Some(Ok("Invite invalidated".into()))) +} + +#[post("/account/admin/import")] +pub async fn r_account_admin_import( + session: Session, + database: &State<Database>, +) -> MyResult<DynLayoutPage<'static>> { + if !session.user.admin { + Err(anyhow!("you not admin"))? + } + let r = import(&database).await; + admin_dashboard( + &database, + Some(r.map_err(|e| e.into()).map(|_| "Import successful".into())), + ) } diff --git a/server/src/routes/ui/error.rs b/server/src/routes/ui/error.rs index 01aebd3..190650f 100644 --- a/server/src/routes/ui/error.rs +++ b/server/src/routes/ui/error.rs @@ -90,4 +90,4 @@ impl From<serde_json::Error> for MyError { fn from(err: serde_json::Error) -> Self { MyError(anyhow::anyhow!("{err}")) } -}
\ No newline at end of file +} diff --git a/server/src/routes/ui/layout.rs b/server/src/routes/ui/layout.rs index 7844e22..7e51e5c 100644 --- a/server/src/routes/ui/layout.rs +++ b/server/src/routes/ui/layout.rs @@ -62,6 +62,15 @@ markup::define! { } } } + + FlashDisplay(flash: Option<Result<String, String>>) { + @if let Some(flash) = &flash { + @match flash { + Ok(mesg) => { section.message { p.success { @mesg } } } + Err(err) => { section.message { p.error { @format!("{err}") } } } + } + } + } } pub type DynLayoutPage<'a> = LayoutPage<markup::DynRender<'a>>; diff --git a/tools/src/bin/import.rs b/tools/src/bin/import.rs index 12cd161..fc44682 100644 --- a/tools/src/bin/import.rs +++ b/tools/src/bin/import.rs @@ -5,7 +5,7 @@ Copyright (C) 2023 metamuffin <metamuffin.org> */ use anyhow::Context; use clap::{Parser, Subcommand}; -use jellycommon::{config::GlobalConfig, MediaInfo, Node, NodeKind, NodePrivate, NodePublic}; +use jellycommon::{MediaInfo, Node, NodeKind, NodePrivate, NodePublic}; use jellymatroska::read::EbmlReader; use jellyremuxer::import::import_read; use jellytools::tmdb::{tmdb_details, tmdb_image, tmdb_search}; @@ -32,13 +32,6 @@ enum Action { #[arg(short, long)] series: bool, }, - Remote { - config: PathBuf, - host: String, - #[arg(short, long)] - remote_id: Option<String>, - id: String, - }, Set { #[arg(short = 'I', long)] item: PathBuf, @@ -173,6 +166,7 @@ fn main() -> anyhow::Result<()> { let node = Node { private: NodePrivate { + import: None, backdrop: backdrop.clone(), poster: poster.clone(), source, @@ -202,19 +196,6 @@ fn main() -> anyhow::Result<()> { Ok(()) } - Action::Remote { - host, - config, - remote_id, - id, - } => { - let config: GlobalConfig = - serde_json::from_reader(File::open(config).unwrap()).unwrap(); - - let (username, password, tls) = &config.remote_credentials[&id]; - - Ok(()) - } Action::Set { .. } => { // let mut iteminfo: ItemInfo = match File::open(item.clone()) { // Ok(f) => serde_json::from_reader(f)?, |