diff --git a/Cargo.lock b/Cargo.lock index 660af10e749d..23eb291d2656 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9644,7 +9644,7 @@ dependencies = [ [[package]] name = "sd-cloud-schema" version = "0.1.0" -source = "git+https://github.com/spacedriveapp/cloud-services-schema?branch=main#4b878e28c2e23d5e37ff827a97f11c5621ef0ded" +source = "git+https://github.com/spacedriveapp/cloud-services-schema?rev=25e4b92fdd#25e4b92fdd95eb07ea4bf75dd3a17d2a7adac068" dependencies = [ "argon2", "async-stream", diff --git a/Cargo.toml b/Cargo.toml index c6e543c94a9e..f3174c6b22ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ rust-version = "1.80" [workspace.dependencies] # First party dependencies -sd-cloud-schema = { git = "https://github.com/spacedriveapp/cloud-services-schema", branch = "main" } +sd-cloud-schema = { git = "https://github.com/spacedriveapp/cloud-services-schema", rev = "25e4b92fdd" } # Third party dependencies used by one or more of our crates async-channel = "2.3" diff --git a/core/crates/cloud-services/src/error.rs b/core/crates/cloud-services/src/error.rs index c2f23ff2b637..b4fa2b7de912 100644 --- a/core/crates/cloud-services/src/error.rs +++ b/core/crates/cloud-services/src/error.rs @@ -150,3 +150,9 @@ impl From for rspc::Error { Self::with_cause(rspc::ErrorCode::InternalServerError, e.to_string(), e) } } + +impl From for rspc::Error { + fn from(e: GetTokenError) -> Self { + Self::with_cause(rspc::ErrorCode::InternalServerError, e.to_string(), e) + } +} diff --git a/core/crates/cloud-services/src/lib.rs b/core/crates/cloud-services/src/lib.rs index d4fa13741f59..a0b1583a3c60 100644 --- a/core/crates/cloud-services/src/lib.rs +++ b/core/crates/cloud-services/src/lib.rs @@ -39,7 +39,9 @@ mod token_refresher; pub use client::CloudServices; pub use error::{Error, GetTokenError}; pub use key_manager::KeyManager; -pub use p2p::{CloudP2P, JoinSyncGroupResponse, NotifyUser, Ticket, UserResponse}; +pub use p2p::{ + CloudP2P, JoinSyncGroupResponse, JoinedLibraryCreateArgs, NotifyUser, Ticket, UserResponse, +}; pub use sync::{ declare_actors as declare_cloud_sync, SyncActors as CloudSyncActors, SyncActorsState as CloudSyncActorsState, diff --git a/core/crates/cloud-services/src/p2p/mod.rs b/core/crates/cloud-services/src/p2p/mod.rs index f050110ed756..cf266b174d52 100644 --- a/core/crates/cloud-services/src/p2p/mod.rs +++ b/core/crates/cloud-services/src/p2p/mod.rs @@ -3,6 +3,7 @@ use crate::{CloudServices, Error}; use sd_cloud_schema::{ cloud_p2p::{authorize_new_device_in_sync_group, CloudP2PALPN, CloudP2PError}, devices::{self, Device}, + libraries, sync::groups::GroupWithLibraryAndDevices, }; use sd_crypto::{CryptoRng, SeedableRng}; @@ -14,13 +15,20 @@ use iroh_net::{ Endpoint, NodeId, }; use serde::{Deserialize, Serialize}; -use tokio::spawn; +use tokio::{spawn, sync::oneshot}; use tracing::error; mod runner; use runner::Runner; +#[derive(Debug)] +pub struct JoinedLibraryCreateArgs { + pub pub_id: libraries::PubId, + pub name: String, + pub description: Option, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, specta::Type)] #[serde(transparent)] #[repr(transparent)] @@ -66,7 +74,13 @@ pub enum JoinSyncGroupResponse { #[derive(Debug, Deserialize, specta::Type)] #[serde(tag = "kind", content = "data")] pub enum UserResponse { - AcceptDeviceInSyncGroup { ticket: Ticket, accepted: bool }, + AcceptDeviceInSyncGroup { + ticket: Ticket, + accepted: bool, + library_pub_id: libraries::PubId, + library_name: String, + library_description: Option, + }, } #[derive(Debug, Clone)] pub struct CloudP2P { @@ -128,11 +142,13 @@ impl CloudP2P { &self, devices_in_group: Vec<(devices::PubId, NodeId)>, req: authorize_new_device_in_sync_group::Request, + tx: oneshot::Sender, ) { self.msgs_tx .send_async(runner::Message::Request(runner::Request::JoinSyncGroup { req, devices_in_group, + tx, })) .await .expect("Channel closed"); diff --git a/core/crates/cloud-services/src/p2p/runner.rs b/core/crates/cloud-services/src/p2p/runner.rs index d720f94e0d10..18fb2426b825 100644 --- a/core/crates/cloud-services/src/p2p/runner.rs +++ b/core/crates/cloud-services/src/p2p/runner.rs @@ -7,6 +7,7 @@ use sd_cloud_schema::{ self, authorize_new_device_in_sync_group, Client, CloudP2PALPN, CloudP2PError, Service, }, devices::{self, Device}, + libraries, sync::groups, }; use sd_crypto::{CryptoRng, SeedableRng}; @@ -32,14 +33,14 @@ use quic_rpc::{ }; use tokio::{ spawn, - sync::Mutex, + sync::{oneshot, Mutex}, task::JoinHandle, time::{interval, Instant, MissedTickBehavior}, }; use tokio_stream::wrappers::IntervalStream; use tracing::{debug, error, warn}; -use super::{JoinSyncGroupResponse, NotifyUser, Ticket, UserResponse}; +use super::{JoinSyncGroupResponse, JoinedLibraryCreateArgs, NotifyUser, Ticket, UserResponse}; const TEN_SECONDS: Duration = Duration::from_secs(10); const FIVE_MINUTES: Duration = Duration::from_secs(60 * 5); @@ -54,6 +55,7 @@ pub enum Request { JoinSyncGroup { req: authorize_new_device_in_sync_group::Request, devices_in_group: Vec<(devices::PubId, NodeId)>, + tx: oneshot::Sender, }, } @@ -177,13 +179,24 @@ impl Runner { StreamMessage::Message(Message::Request(Request::JoinSyncGroup { req, devices_in_group, - })) => self.dispatch_join_requests(req, devices_in_group, &mut rng), + tx, + })) => self.dispatch_join_requests(req, devices_in_group, &mut rng, tx), StreamMessage::UserResponse(UserResponse::AcceptDeviceInSyncGroup { ticket, accepted, + library_pub_id, + library_name, + library_description, }) => { - self.handle_join_response(ticket, accepted).await; + self.handle_join_response( + ticket, + accepted, + library_pub_id, + library_name, + library_description, + ) + .await; } StreamMessage::Tick => self.tick().await, @@ -201,6 +214,7 @@ impl Runner { req: authorize_new_device_in_sync_group::Request, devices_in_group: Vec<(devices::PubId, NodeId)>, rng: &mut CryptoRng, + tx: oneshot::Sender, ) { async fn inner( key_manager: Arc, @@ -208,6 +222,7 @@ impl Runner { mut rng: CryptoRng, req: authorize_new_device_in_sync_group::Request, devices_in_group: Vec<(devices::PubId, NodeId)>, + tx: oneshot::Sender, ) -> Result { let group_pub_id = req.sync_group.pub_id; loop { @@ -226,6 +241,9 @@ impl Runner { Ok(authorize_new_device_in_sync_group::Response { authorizor_device, keys, + library_pub_id, + library_name, + library_description, }) => { key_manager .add_many_keys( @@ -239,7 +257,17 @@ impl Runner { ) .await?; - // TODO(@fogodev): Figure out a way to dispatch sync related actors now that we have the keys + if tx + .send(JoinedLibraryCreateArgs { + pub_id: library_pub_id, + name: library_name, + description: library_description, + }) + .is_err() + { + error!("Failed to handle library creation locally from received library data"); + return Ok(JoinSyncGroupResponse::CriticalError); + } return Ok(JoinSyncGroupResponse::Accepted { authorizor_device }); } @@ -260,7 +288,7 @@ impl Runner { if let Err(SendError(response)) = notify_user_tx .send_async(NotifyUser::ReceivedJoinSyncGroupResponse { - response: inner(key_manager, endpoint, rng, req, devices_in_group) + response: inner(key_manager, endpoint, rng, req, devices_in_group, tx) .await .unwrap_or_else(|e| { error!( @@ -326,7 +354,14 @@ impl Runner { } } - async fn handle_join_response(&self, ticket: Ticket, accepted: bool) { + async fn handle_join_response( + &self, + ticket: Ticket, + accepted: bool, + library_pub_id: libraries::PubId, + library_name: String, + library_description: Option, + ) { let Some(PendingSyncGroupJoin { channel, request, @@ -355,6 +390,9 @@ impl Runner { .into_iter() .map(Into::into) .collect(), + library_pub_id, + library_name, + library_description, }) } else { Err(CloudP2PError::Rejected) diff --git a/core/src/api/cloud/devices.rs b/core/src/api/cloud/devices.rs index d8631acdb973..f4df3ec12998 100644 --- a/core/src/api/cloud/devices.rs +++ b/core/src/api/cloud/devices.rs @@ -218,10 +218,10 @@ pub struct DeviceRegisterData { pub pub_id: PubId, pub name: String, pub os: DeviceOS, - pub storage_size: u64, - pub connection_id: NodeId, pub hardware_model: HardwareModel, + pub storage_size: u64, pub used_storage: u64, + pub connection_id: NodeId, } pub async fn register( @@ -231,10 +231,10 @@ pub async fn register( pub_id, name, os, - storage_size, - connection_id, hardware_model, + storage_size, used_storage, + connection_id, }: DeviceRegisterData, hashed_pub_id: Hash, rng: &mut CryptoRng, diff --git a/core/src/api/cloud/mod.rs b/core/src/api/cloud/mod.rs index 4456d3c2bba5..873bfc2eb837 100644 --- a/core/src/api/cloud/mod.rs +++ b/core/src/api/cloud/mod.rs @@ -1,4 +1,5 @@ use crate::{ + library::LibraryManagerError, node::{config::NodeConfig, HardwareModel}, volume::get_volumes, Node, @@ -9,6 +10,7 @@ use sd_core_cloud_services::{CloudP2P, IrohSecretKey, KeyManager, QuinnConnectio use sd_cloud_schema::{ auth, error::{ClientSideError, Error}, + sync::groups, users, Client, Service, }; use sd_crypto::{CryptoRng, SeedableRng}; @@ -17,8 +19,9 @@ use std::pin::pin; use async_stream::stream; use futures::StreamExt; +use futures_concurrency::future::TryJoin; use rspc::alpha::AlphaRouter; -use tracing::{debug, error}; +use tracing::{debug, error, instrument}; use super::{Ctx, R}; @@ -51,7 +54,7 @@ pub(crate) fn mount() -> AlphaRouter { node.cloud_services .token_refresher - .init(access_token.clone(), refresh_token) + .init(access_token, refresh_token) .await?; let client = try_get_cloud_services_client(&node).await?; @@ -65,7 +68,11 @@ pub(crate) fn mount() -> AlphaRouter { client .users() .create(users::create::Request { - access_token: access_token.clone(), + access_token: node + .cloud_services + .token_refresher + .get_access_token() + .await?, }) .await, "Failed to create user;", @@ -82,7 +89,11 @@ pub(crate) fn mount() -> AlphaRouter { client .devices() .get(devices::get::Request { - access_token: access_token.clone(), + access_token: node + .cloud_services + .token_refresher + .get_access_token() + .await?, pub_id: device_pub_id, }) .await, @@ -92,7 +103,10 @@ pub(crate) fn mount() -> AlphaRouter { // Device registered, we execute a device hello flow let master_key = self::devices::hello( &client, - access_token, + node.cloud_services + .token_refresher + .get_access_token() + .await?, device_pub_id, hashed_pub_id, &mut rng, @@ -108,22 +122,31 @@ pub(crate) fn mount() -> AlphaRouter { HardwareModel::try_get().unwrap_or(HardwareModel::Other), ); + let (storage_size, used_storage) = get_volumes() + .await + .into_iter() + .fold((0, 0), |(storage_size, used_storage), volume| { + ( + storage_size + volume.total_capacity, + used_storage + + (volume.total_capacity - volume.available_capacity), + ) + }); + let master_key = self::devices::register( &client, - access_token, + node.cloud_services + .token_refresher + .get_access_token() + .await?, self::devices::DeviceRegisterData { pub_id: device_pub_id, name, os, - // TODO(@fogodev): We should use storage statistics from sqlite db - storage_size: get_volumes() - .await - .into_iter() - .map(|volume| volume.total_capacity) - .sum(), + storage_size, connection_id: iroh_secret_key.public(), hardware_model, - used_storage: 0, + used_storage, }, hashed_pub_id, &mut rng, @@ -156,7 +179,51 @@ pub(crate) fn mount() -> AlphaRouter { ) .await; - // TODO(@fogodev): Verify existing sync groups and dispatch sync related actors + let groups::list::Response(groups) = handle_comm_error( + client + .sync() + .groups() + .list(groups::list::Request { + access_token: node + .cloud_services + .token_refresher + .get_access_token() + .await?, + with_library: true, + }) + .await, + "Failed to list sync groups on bootstrap", + )??; + + groups + .into_iter() + .map( + |groups::Group { + pub_id, + library, + // TODO(@fogodev): We can use this latest key hash to check if we + // already have the latest key hash for this group locally + // issuing a ask for key hash request for other devices if we don't + latest_key_hash: _latest_key_hash, + .. + }| { + let node = &node; + + async move { + initialize_cloud_sync( + pub_id, + library.expect( + "we asked backend to receive a library, this is a bug and should crash" + ), + node, + ) + .await + } + }, + ) + .collect::>() + .try_join() + .await?; Ok(()) }, @@ -194,3 +261,21 @@ fn handle_comm_error Result<(), LibraryManagerError> { + let library = node + .libraries + .get_library(&library_pub_id) + .await + .ok_or(LibraryManagerError::LibraryNotFound)?; + + library.init_cloud_sync(node, group_pub_id).await +} diff --git a/core/src/api/cloud/sync_groups.rs b/core/src/api/cloud/sync_groups.rs index f471333ddfd4..27cf22002205 100644 --- a/core/src/api/cloud/sync_groups.rs +++ b/core/src/api/cloud/sync_groups.rs @@ -1,4 +1,10 @@ -use crate::api::{utils::library, Ctx, R}; +use crate::{ + api::{utils::library, Ctx, R}, + library::LibraryName, + Node, +}; + +use sd_core_cloud_services::JoinedLibraryCreateArgs; use sd_cloud_schema::{ auth::AccessToken, @@ -6,11 +12,14 @@ use sd_cloud_schema::{ sync::{groups, KeyHash}, }; +use std::sync::Arc; + use futures_concurrency::future::TryJoin; use rspc::alpha::AlphaRouter; use sd_crypto::{cloud::secret_key::SecretKey, CryptoRng, SeedableRng}; use serde::Deserialize; -use tracing::debug; +use tokio::{spawn, sync::oneshot}; +use tracing::{debug, error}; pub fn mount() -> AlphaRouter { R.router() @@ -66,7 +75,7 @@ pub fn mount() -> AlphaRouter { return Err(e.into()); } - // TODO(@fogodev): use the group_pub_id to dispatch actors for syncing to this group + library.init_cloud_sync(&node, group_pub_id).await?; debug!(%group_pub_id, "Created sync group"); @@ -271,6 +280,8 @@ pub fn mount() -> AlphaRouter { "Failed to update library;", )??; + let (tx, rx) = oneshot::channel(); + cloud_p2p .request_join_sync_group( existing_devices, @@ -278,9 +289,17 @@ pub fn mount() -> AlphaRouter { sync_group, asking_device, }, + tx, ) .await; + JoinedSyncGroupReceiver { + node, + group_pub_id, + rx, + } + .dispatch(); + debug!(%group_pub_id, "Requested to join sync group"); Ok(()) @@ -288,3 +307,49 @@ pub fn mount() -> AlphaRouter { ) }) } + +struct JoinedSyncGroupReceiver { + node: Arc, + group_pub_id: groups::PubId, + rx: oneshot::Receiver, +} + +impl JoinedSyncGroupReceiver { + fn dispatch(self) { + spawn(async move { + let Self { + node, + group_pub_id, + rx, + } = self; + + if let Ok(JoinedLibraryCreateArgs { + pub_id: libraries::PubId(pub_id), + name, + description, + }) = rx.await + { + let Ok(name) = + LibraryName::new(name).map_err(|e| error!(?e, "Invalid library name")) + else { + return; + }; + + let Ok(library) = node + .libraries + .create_with_uuid(pub_id, name, description, true, None, &node) + .await + .map_err(|e| { + error!(?e, "Failed to create library from sync group join response") + }) + else { + return; + }; + + if let Err(e) = library.init_cloud_sync(&node, group_pub_id).await { + error!(?e, "Failed to initialize cloud sync for library"); + } + } + }); + } +} diff --git a/core/src/library/config.rs b/core/src/library/config.rs index 53390fad8481..20c245d10edd 100644 --- a/core/src/library/config.rs +++ b/core/src/library/config.rs @@ -8,7 +8,7 @@ use sd_prisma::prisma::{file_path, indexer_rule, instance, location, PrismaClien use sd_utils::{db::maybe_missing, error::FileIOError}; use std::{ - path::Path, + path::{Path, PathBuf}, sync::{atomic::AtomicBool, Arc}, }; @@ -43,6 +43,9 @@ pub struct LibraryConfig { #[serde(default)] pub generate_sync_operations: Arc, version: LibraryConfigVersion, + + #[serde(skip, default)] + pub config_path: PathBuf, } #[derive( @@ -87,7 +90,6 @@ impl LibraryConfig { description: Option, instance_id: i32, path: impl AsRef, - generate_sync_operations: bool, ) -> Result { let this = Self { name, @@ -95,8 +97,8 @@ impl LibraryConfig { instance_id, version: Self::LATEST_VERSION, cloud_id: None, - // will always be `true` eventually - generate_sync_operations: Arc::new(AtomicBool::new(generate_sync_operations)), + generate_sync_operations: Arc::new(AtomicBool::new(false)), + config_path: path.as_ref().to_path_buf(), }; this.save(path).await.map(|()| this) @@ -109,7 +111,7 @@ impl LibraryConfig { ) -> Result { let path = path.as_ref(); - VersionManager::::migrate_and_load( + let mut loaded_config = VersionManager::::migrate_and_load( path, |current, next| async move { match (current, next) { @@ -407,7 +409,11 @@ impl LibraryConfig { Ok(()) }, ) - .await + .await?; + + loaded_config.config_path = path.to_path_buf(); + + Ok(loaded_config) } pub(crate) async fn save(&self, path: impl AsRef) -> Result<(), LibraryConfigError> { diff --git a/core/src/library/library.rs b/core/src/library/library.rs index 9b26367e2b23..bed66ea9343a 100644 --- a/core/src/library/library.rs +++ b/core/src/library/library.rs @@ -17,7 +17,7 @@ use std::{ collections::HashMap, fmt::{Debug, Formatter}, path::{Path, PathBuf}, - sync::Arc, + sync::{atomic::Ordering, Arc}, }; use futures_concurrency::future::Join; @@ -94,7 +94,7 @@ impl Library { &self, node: &Node, sync_group_pub_id: groups::PubId, - ) -> Result<(), sd_core_cloud_services::Error> { + ) -> Result<(), LibraryManagerError> { let rng = CryptoRng::from_seed(node.master_rng.lock().await.generate_fixed()); declare_cloud_sync( @@ -108,16 +108,21 @@ impl Library { ) .await?; - // TODO(@fogodev): Uncomment when they're ready - // ( - // self.cloud_sync_actors.start(CloudSyncActors::Sender), - // self.cloud_sync_actors.start(CloudSyncActors::Receiver), - // self.cloud_sync_actors.start(CloudSyncActors::Ingester), - // ) - // .join() - // .await; + ( + self.cloud_sync_actors.start(CloudSyncActors::Sender), + self.cloud_sync_actors.start(CloudSyncActors::Receiver), + self.cloud_sync_actors.start(CloudSyncActors::Ingester), + ) + .join() + .await; - Ok(()) + self.update_config(|config| { + config + .generate_sync_operations + .store(true, Ordering::Relaxed) + }) + .await + .map_err(Into::into) } pub async fn config(&self) -> LibraryConfig { @@ -127,13 +132,12 @@ impl Library { pub async fn update_config( &self, update_fn: impl FnOnce(&mut LibraryConfig), - config_path: impl AsRef, ) -> Result<(), LibraryManagerError> { let mut config = self.config.write().await; update_fn(&mut config); - config.save(config_path).await.map_err(Into::into) + config.save(&config.config_path).await.map_err(Into::into) } // TODO: Remove this once we replace the old invalidation system diff --git a/core/src/library/manager/error.rs b/core/src/library/manager/error.rs index 5a12ff221f01..4fc01dd4e7fa 100644 --- a/core/src/library/manager/error.rs +++ b/core/src/library/manager/error.rs @@ -47,6 +47,8 @@ pub enum LibraryManagerError { #[error(transparent)] LibraryConfig(#[from] LibraryConfigError), #[error(transparent)] + CloudServices(#[from] sd_core_cloud_services::Error), + #[error(transparent)] Sync(#[from] sd_core_sync::Error), } diff --git a/core/src/library/manager/mod.rs b/core/src/library/manager/mod.rs index 5a4d66d7b91c..a83368f406b8 100644 --- a/core/src/library/manager/mod.rs +++ b/core/src/library/manager/mod.rs @@ -154,12 +154,11 @@ impl Libraries { description: Option, node: &Arc, ) -> Result, LibraryManagerError> { - self.create_with_uuid(Uuid::now_v7(), name, description, true, None, node, false) + self.create_with_uuid(Uuid::now_v7(), name, description, true, None, node) .await } #[instrument(skip(self, instance, node), err)] - #[allow(clippy::too_many_arguments)] pub(crate) async fn create_with_uuid( self: &Arc, id: Uuid, @@ -169,7 +168,6 @@ impl Libraries { // `None` will fallback to default as library must be created with at least one instance instance: Option, node: &Arc, - generate_sync_operations: bool, ) -> Result, LibraryManagerError> { if name.as_ref().is_empty() || name.as_ref().chars().all(|x| x.is_whitespace()) { return Err(LibraryManagerError::InvalidConfig( @@ -185,7 +183,6 @@ impl Libraries { // First instance will be zero 0, &config_path, - generate_sync_operations, ) .await?; @@ -274,33 +271,28 @@ impl Libraries { ); library - .update_config( - |config| { - // update the library - if let Some(name) = name { - config.name = name; - } - match description { - MaybeUndefined::Undefined => {} - MaybeUndefined::Null => config.description = None, - MaybeUndefined::Value(description) => { - config.description = Some(description) - } - } - match cloud_id { - MaybeUndefined::Undefined => {} - MaybeUndefined::Null => config.cloud_id = None, - MaybeUndefined::Value(cloud_id) => config.cloud_id = Some(cloud_id), - } - match enable_sync { - None => {} - Some(value) => config - .generate_sync_operations - .store(value, Ordering::SeqCst), - } - }, - self.libraries_dir.join(format!("{id}.sdlibrary")), - ) + .update_config(|config| { + // update the library + if let Some(name) = name { + config.name = name; + } + match description { + MaybeUndefined::Undefined => {} + MaybeUndefined::Null => config.description = None, + MaybeUndefined::Value(description) => config.description = Some(description), + } + match cloud_id { + MaybeUndefined::Undefined => {} + MaybeUndefined::Null => config.cloud_id = None, + MaybeUndefined::Value(cloud_id) => config.cloud_id = Some(cloud_id), + } + match enable_sync { + None => {} + Some(value) => config + .generate_sync_operations + .store(value, Ordering::SeqCst), + } + }) .await?; self.tx @@ -429,6 +421,7 @@ impl Libraries { self.libraries.read().await.get(library_id).is_some() } + #[allow(clippy::too_many_arguments)] // TODO: remove this when we remove instance stuff #[instrument( skip_all, fields( diff --git a/core/src/util/debug_initializer.rs b/core/src/util/debug_initializer.rs index 8221aa77e524..562ca7b07813 100644 --- a/core/src/util/debug_initializer.rs +++ b/core/src/util/debug_initializer.rs @@ -130,7 +130,7 @@ impl InitConfig { lib } else { let library = library_manager - .create_with_uuid(lib.id, lib.name, lib.description, true, None, node, false) + .create_with_uuid(lib.id, lib.name, lib.description, true, None, node) .await?; let Some(lib) = library_manager.get_library(&library.id).await else {