From e9a22fbba0911a11bf55f20fe8a94cc264f8201e Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 30 May 2023 17:24:05 +0800 Subject: [PATCH] [ENG-546] Improve error handling (#802) * log to disk * remove some unwraps * panicless * some p2p error handling * clippy moment * Fix `` * open logs button * 39 to 0 * fix types * update deps and comment out broken tests * clippy * more clippy * upgrade rimraf - https://github.com/isaacs/rimraf/issues/259 * regen broken lockfile * update `notify` and update `commands.ts` * more clippy (pls work) * allow deprecated for p2p (hopefully temporary) * a * upgrade deps for p2p * do betterer * do it correctly * remove unused import * improve core startup error + bc keypair --------- Co-authored-by: Utku Bakir <74243531+utkubakir@users.noreply.github.com> Co-authored-by: brxken128 <77554505+brxken128@users.noreply.github.com> --- Cargo.lock | Bin 242263 -> 244843 bytes Cargo.toml | 2 +- apps/desktop/src-tauri/src/main.rs | 17 +- apps/desktop/src/App.tsx | 29 ++- apps/desktop/src/commands.ts | 4 + apps/mobile/crates/core/src/lib.rs | 2 + apps/server/src/main.rs | 2 + core/Cargo.toml | 11 +- core/src/api/files.rs | 6 +- core/src/api/keys.rs | 64 ++++--- core/src/api/libraries.rs | 2 +- core/src/api/nodes.rs | 13 +- core/src/api/p2p.rs | 16 +- core/src/api/tags.rs | 7 +- core/src/api/utils/invalidate.rs | 16 +- core/src/job/mod.rs | 13 +- core/src/lib.rs | 177 ++++++++++-------- core/src/library/manager.rs | 12 +- core/src/location/file_path_helper/mod.rs | 12 +- core/src/location/indexer/walk.rs | 8 +- core/src/location/manager/mod.rs | 17 +- core/src/location/manager/watcher/utils.rs | 24 ++- core/src/location/mod.rs | 9 +- core/src/node/mod.rs | 22 +-- core/src/node/peer_request.rs | 2 +- .../file_identifier/file_identifier_job.rs | 2 +- core/src/object/file_identifier/mod.rs | 5 +- .../shallow_file_identifier_job.rs | 2 +- core/src/object/fs/encrypt.rs | 7 +- core/src/object/preview/thumbnail/mod.rs | 6 +- core/src/object/validation/validator_job.rs | 5 +- core/src/p2p/p2p_manager.rs | 78 +++----- core/src/p2p/protocol.rs | 58 +++++- core/src/sync/manager.rs | 2 + core/src/util/debug_initializer.rs | 110 +++++++---- core/src/util/migrator.rs | 9 +- core/src/volume.rs | 1 + crates/p2p/Cargo.toml | 12 +- crates/p2p/src/manager.rs | 19 +- crates/p2p/src/manager_stream.rs | 26 +-- crates/p2p/src/spaceblock/mod.rs | 44 +++-- crates/p2p/src/spacetime/behaviour.rs | 32 ++-- crates/p2p/src/utils/keypair.rs | 31 ++- crates/p2p/src/utils/peer_id.rs | 1 + interface/ErrorFallback.tsx | 42 ++++- interface/app/$libraryId/debug.tsx | 2 - .../$libraryId/settings/client/general.tsx | 11 ++ interface/app/index.tsx | 2 + interface/package.json | 1 + interface/util/Platform.tsx | 1 + packages/client/src/core.ts | 82 ++++---- pnpm-lock.yaml | Bin 839165 -> 909242 bytes 52 files changed, 662 insertions(+), 416 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3dc41a6959c32ef10c8f84a5fc20301bd54c67a4..e7b0476eb941e340fa4333430973bb131c31aa2c 100644 GIT binary patch delta 32413 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 C=Gt}u delta 30493 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 -> Result<(), ()> { + opener::open(node.data_dir.join("logs")).ok(); + Ok(()) +} + pub fn tauri_error_plugin(err: NodeError) -> TauriPlugin { tauri::plugin::Builder::new("spacedrive") .js_init_script(format!(r#"window.__SD_ERROR__ = "{err}";"#)) @@ -55,6 +62,8 @@ async fn main() -> tauri::Result<()> { #[cfg(debug_assertions)] let data_dir = data_dir.join("dev"); + let _guard = Node::init_logger(&data_dir); + let result = Node::new(data_dir).await; let app = tauri::Builder::default(); @@ -76,7 +85,10 @@ async fn main() -> tauri::Result<()> { (Some(node), app) } - Err(err) => (None, app.plugin(tauri_error_plugin(err))), + Err(err) => { + tracing::error!("Error starting up the node: {err}"); + (None, app.plugin(tauri_error_plugin(err))) + } }; let app = app @@ -130,6 +142,7 @@ async fn main() -> tauri::Result<()> { .menu(menu::get_menu()) .invoke_handler(tauri_handlers![ app_ready, + open_logs_dir, file::open_file_path, file::get_file_path_open_with_apps, file::open_file_path_with diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 1e4f26d5f..b94f3033d 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -17,7 +17,13 @@ import { } from '@sd/interface'; import { getSpacedropState } from '@sd/interface/hooks/useSpacedropState'; import '@sd/ui/style'; -import { appReady, getFilePathOpenWithApps, openFilePath, openFilePathWith } from './commands'; +import { + appReady, + getFilePathOpenWithApps, + openFilePath, + openFilePathWith, + openLogsDir +} from './commands'; // TODO: Bring this back once upstream is fixed up. // const client = hooks.createClient({ @@ -71,6 +77,7 @@ const platform: Platform = { saveFilePickerDialog: () => dialog.save(), showDevtools: () => invoke('show_devtools'), openPath: (path) => shell.open(path), + openLogsDir, openFilePath, getFilePathOpenWithApps, openFilePathWith @@ -103,17 +110,27 @@ export default function App() { }; }, []); - if (startupError) { - return ; - } - return ( - + ); } + +// This is required because `ErrorPage` uses the OS which comes from `PlatformProvider` +function AppInner() { + if (startupError) { + return ( + + ); + } + + return ; +} diff --git a/apps/desktop/src/commands.ts b/apps/desktop/src/commands.ts index 37d4c4a2c..71e6dc417 100644 --- a/apps/desktop/src/commands.ts +++ b/apps/desktop/src/commands.ts @@ -14,6 +14,10 @@ export function appReady() { return invoke()("app_ready") } +export function openLogsDir() { + return invoke()("open_logs_dir") +} + export function openFilePath(library: string, id: number) { return invoke()("open_file_path", { library,id }) } diff --git a/apps/mobile/crates/core/src/lib.rs b/apps/mobile/crates/core/src/lib.rs index 7f2b76612..49631ecb4 100644 --- a/apps/mobile/crates/core/src/lib.rs +++ b/apps/mobile/crates/core/src/lib.rs @@ -67,6 +67,8 @@ pub fn handle_core_msg( match node { Some(node) => node.clone(), None => { + let _guard = Node::init_logger(&data_dir); + // TODO: probably don't unwrap let new_node = Node::new(data_dir).await.unwrap(); node.replace(new_node.clone()); diff --git a/apps/server/src/main.rs b/apps/server/src/main.rs index 678d5d6bd..17218f856 100644 --- a/apps/server/src/main.rs +++ b/apps/server/src/main.rs @@ -32,6 +32,8 @@ async fn main() { .map(|port| port.parse::().unwrap_or(8080)) .unwrap_or(8080); + let _guard = Node::init_logger(&data_dir); + let (node, router) = Node::new(data_dir).await.expect("Unable to create node"); let signal = utils::axum_shutdown_signal(node.clone()); diff --git a/core/Cargo.toml b/core/Cargo.toml index 10ef0385f..cea2c7cad 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -67,8 +67,10 @@ include_dir = { version = "0.7.2", features = ["glob"] } async-trait = "^0.1.57" image = "0.24.6" webp = "0.2.2" -tracing = "0.1.36" -tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } +tracing = { git = "https://github.com/tokio-rs/tracing", rev = "29146260fb4615d271d2e899ad95a753bb42915e" } # To work with tracing-appender +tracing-subscriber = { git = "https://github.com/tokio-rs/tracing", rev = "29146260fb4615d271d2e899ad95a753bb42915e", features = [ + "env-filter", +] } async-stream = "0.3.3" once_cell = "1.15.0" ctor = "0.1.23" @@ -80,12 +82,13 @@ http-range = "0.1.5" mini-moka = "0.10.0" serde_with = "2.2.0" dashmap = { version = "5.4.0", features = ["serde"] } -notify = { version = "5.0.0", default-features = false, features = [ +notify = { version = "5.2.0", default-features = false, features = [ "macos_fsevent", ], optional = true } static_assertions = "1.1.0" serde-hashkey = "0.4.5" normpath = { version = "1.1.1", features = ["localization"] } +tracing-appender = { git = "https://github.com/tokio-rs/tracing", rev = "29146260fb4615d271d2e899ad95a753bb42915e" } # Unreleased changes for log deletion strum = { version = "0.24", features = ["derive"] } strum_macros = "0.24" @@ -95,4 +98,4 @@ version = "0.1.5" [dev-dependencies] tempfile = "^3.3.0" -tracing-test = "^0.2.3" +tracing-test = "^0.2.4" diff --git a/core/src/api/files.rs b/core/src/api/files.rs index 49a85dd62..7bbfda2b1 100644 --- a/core/src/api/files.rs +++ b/core/src/api/files.rs @@ -9,7 +9,7 @@ use crate::{ prisma::{location, object}, }; -use chrono::{FixedOffset, Utc}; +use chrono::Utc; use rspc::{alpha::AlphaRouter, ErrorCode}; use serde::Deserialize; use specta::Type; @@ -108,9 +108,7 @@ pub(crate) fn mount() -> AlphaRouter { .object() .update( object::id::equals(id), - vec![object::date_accessed::set(Some( - Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()), - ))], + vec![object::date_accessed::set(Some(Utc::now().into()))], ) .exec() .await?; diff --git a/core/src/api/keys.rs b/core/src/api/keys.rs index 91064339d..d57405a23 100644 --- a/core/src/api/keys.rs +++ b/core/src/api/keys.rs @@ -1,4 +1,5 @@ use rspc::alpha::AlphaRouter; +use rspc::ErrorCode; use sd_crypto::keys::keymanager::{StoredKey, StoredKeyType}; use sd_crypto::primitives::SECRET_KEY_IDENTIFIER; use sd_crypto::types::{Algorithm, HashingAlgorithm, OnboardingConfig, SecretKeyString}; @@ -389,38 +390,45 @@ pub(crate) fn mount() -> AlphaRouter { invalidate_query!(library, "keys.list"); invalidate_query!(library, "keys.listMounted"); - Ok(TryInto::::try_into(updated_keys.len()).unwrap()) // We convert from `usize` (bigint type) to `u32` (number type) because rspc doesn't support bigints. + TryInto::::try_into(updated_keys.len()).map_err(|_| { + rspc::Error::new(ErrorCode::InternalServerError, "integer overflow".into()) + }) // We convert from `usize` (bigint type) to `u32` (number type) because rspc doesn't support bigints. }) }) - .procedure("changeMasterPassword", { - R.with2(library()) - .mutation(|(_, library), args: MasterPasswordChangeArgs| async move { - let verification_key = library - .key_manager - .change_master_password( - args.password, - args.algorithm, - args.hashing_algorithm, - library.id, - ) - .await?; + .procedure( + "changeMasterPassword", + #[allow(clippy::unwrap_used)] // TODO: Jake is fixing this in a Crypto PR + { + R.with2(library()).mutation( + |(_, library), args: MasterPasswordChangeArgs| async move { + let verification_key = library + .key_manager + .change_master_password( + args.password, + args.algorithm, + args.hashing_algorithm, + library.id, + ) + .await?; - invalidate_query!(library, "keys.getSecretKey"); + invalidate_query!(library, "keys.getSecretKey"); - // remove old root key if present - library - .db - .key() - .delete_many(vec![key::key_type::equals( - serde_json::to_string(&StoredKeyType::Root).unwrap(), - )]) - .exec() - .await?; + // remove old root key if present + library + .db + .key() + .delete_many(vec![key::key_type::equals( + serde_json::to_string(&StoredKeyType::Root).unwrap(), + )]) + .exec() + .await?; - // write the new verification key - write_storedkey_to_db(&library.db, &verification_key).await?; + // write the new verification key + write_storedkey_to_db(&library.db, &verification_key).await?; - Ok(()) - }) - }) + Ok(()) + }, + ) + }, + ) } diff --git a/core/src/api/libraries.rs b/core/src/api/libraries.rs index 2bd5ae0c3..21edbfc10 100644 --- a/core/src/api/libraries.rs +++ b/core/src/api/libraries.rs @@ -109,7 +109,7 @@ pub(crate) fn mount() -> AlphaRouter { ctx.library_manager .get_library(new_library.uuid) .await - .unwrap(), + .expect("We just created the library. Where do it be?"), "library.getStatistics" ); diff --git a/core/src/api/nodes.rs b/core/src/api/nodes.rs index 85a51a614..a94d1b551 100644 --- a/core/src/api/nodes.rs +++ b/core/src/api/nodes.rs @@ -1,6 +1,7 @@ -use rspc::alpha::AlphaRouter; +use rspc::{alpha::AlphaRouter, ErrorCode}; use serde::Deserialize; use specta::Type; +use tracing::error; use crate::api::R; @@ -20,9 +21,13 @@ pub(crate) fn mount() -> AlphaRouter { config.name = args.name; }) .await - .unwrap(); - - Ok(()) + .map_err(|err| { + error!("Failed to write config: {}", err); + rspc::Error::new( + ErrorCode::InternalServerError, + "error updating config".into(), + ) + }) }) }) } diff --git a/core/src/api/p2p.rs b/core/src/api/p2p.rs index 203ece62b..13109460a 100644 --- a/core/src/api/p2p.rs +++ b/core/src/api/p2p.rs @@ -1,4 +1,4 @@ -use rspc::alpha::AlphaRouter; +use rspc::{alpha::AlphaRouter, ErrorCode}; use sd_p2p::PeerId; use serde::Deserialize; use specta::Type; @@ -45,8 +45,18 @@ pub(crate) fn mount() -> AlphaRouter { R.mutation(|ctx, args: SpacedropArgs| async move { // TODO: Handle multiple files path and error if zero paths ctx.p2p - .big_bad_spacedrop(args.peer_id, PathBuf::from(args.file_path.first().unwrap())) - .await; + .big_bad_spacedrop( + args.peer_id, + PathBuf::from( + args.file_path + .first() + .expect("https://linear.app/spacedriveapp/issue/ENG-625/spacedrop-multiple-files"), + ), + ) + .await + .map_err(|_| { + rspc::Error::new(ErrorCode::InternalServerError, "todo".to_string()) + }) }) }) .procedure("acceptSpacedrop", { diff --git a/core/src/api/tags.rs b/core/src/api/tags.rs index 6922de7fa..1846593ca 100644 --- a/core/src/api/tags.rs +++ b/core/src/api/tags.rs @@ -1,4 +1,4 @@ -use rspc::alpha::AlphaRouter; +use rspc::{alpha::AlphaRouter, ErrorCode}; use serde::Deserialize; use specta::Type; @@ -151,7 +151,10 @@ pub(crate) fn mount() -> AlphaRouter { .select(tag::select!({ pub_id })) .exec() .await? - .unwrap(); + .ok_or(rspc::Error::new( + ErrorCode::NotFound, + "Error finding tag in db".into(), + ))?; sync.write_ops( db, diff --git a/core/src/api/utils/invalidate.rs b/core/src/api/utils/invalidate.rs index 38350445c..63df249c4 100644 --- a/core/src/api/utils/invalidate.rs +++ b/core/src/api/utils/invalidate.rs @@ -65,11 +65,13 @@ impl InvalidRequests { } } - #[allow(unused_variables)] + #[allow(unused_variables, clippy::panic)] pub(crate) fn validate(r: Arc) { #[cfg(debug_assertions)] { - let invalidate_requests = INVALIDATION_REQUESTS.lock().unwrap(); + let invalidate_requests = INVALIDATION_REQUESTS + .lock() + .expect("Failed to lock the mutex for invalidation requests"); let queries = r.queries(); for req in &invalidate_requests.queries { @@ -240,7 +242,15 @@ pub(crate) fn mount_invalidate() -> AlphaRouter { if let Ok(event) = event { if let CoreEvent::InvalidateOperation(op) = event { // Newer data replaces older data in the buffer - buf.insert(to_key(&(op.key, &op.arg)).unwrap(), op); + match to_key(&(op.key, &op.arg)) { + Ok(key) => { + buf.insert(key, op); + }, + Err(err) => { + warn!("Error deriving key for invalidate operation '{:?}': {:?}", op, err); + }, + } + } } else { warn!("Shutting down invalidation manager thread due to the core event bus being droppped!"); diff --git a/core/src/job/mod.rs b/core/src/job/mod.rs index 84a83da35..040e318cb 100644 --- a/core/src/job/mod.rs +++ b/core/src/job/mod.rs @@ -72,6 +72,10 @@ pub enum JobError { MatchingSrcDest(PathBuf), #[error("action would overwrite another file: {}", .0.display())] WouldOverwrite(PathBuf), + #[error("item of type '{0}' with id '{1}' is missing from the db")] + MissingFromDb(&'static str, String), + #[error("the cas id is not set on the path data")] + MissingCasId, // Not errors #[error("step completed with errors")] @@ -295,7 +299,10 @@ pub struct JobState { impl DynJob for Job { fn id(&self) -> Uuid { // SAFETY: This method is using during queueing, so we still have a report - self.report().as_ref().unwrap().id + self.report() + .as_ref() + .expect("This method is using during queueing, so we still have a report") + .id } fn parent_id(&self) -> Option { @@ -347,7 +354,9 @@ impl DynJob for Job { ) => { match step_result { Err(JobError::EarlyFinish { .. }) => { - info!("{}", step_result.unwrap_err()); + step_result.map_err(|err| { + warn!("{}", err); + }).ok(); break; }, Err(JobError::StepCompletedWithErrors(errors_text)) => { diff --git a/core/src/lib.rs b/core/src/lib.rs index 8ce04cbcf..3d1da1153 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,3 +1,5 @@ +#![warn(clippy::unwrap_used, clippy::panic)] + use crate::{ api::{CoreEvent, Router}, job::JobManager, @@ -9,11 +11,18 @@ use crate::{ pub use sd_prisma::*; -use std::{path::Path, sync::Arc}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use thiserror::Error; use tokio::{fs, sync::broadcast}; use tracing::{debug, error, info, warn}; -use tracing_subscriber::{prelude::*, EnvFilter}; +use tracing_appender::{ + non_blocking::{NonBlocking, WorkerGuard}, + rolling::{RollingFileAppender, Rotation}, +}; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; pub mod api; pub mod custom_uri; @@ -38,6 +47,7 @@ pub struct NodeContext { } pub struct Node { + pub data_dir: PathBuf, config: Arc, pub library_manager: Arc, location_manager: Arc, @@ -47,86 +57,16 @@ pub struct Node { // peer_request: tokio::sync::Mutex>, } -#[cfg(not(target_os = "android"))] -const CONSOLE_LOG_FILTER: tracing_subscriber::filter::LevelFilter = { - use tracing_subscriber::filter::LevelFilter; - - match cfg!(debug_assertions) { - true => LevelFilter::DEBUG, - false => LevelFilter::INFO, - } -}; - impl Node { pub async fn new(data_dir: impl AsRef) -> Result<(Arc, Arc), NodeError> { let data_dir = data_dir.as_ref(); #[cfg(debug_assertions)] - let init_data = util::debug_initializer::InitConfig::load(data_dir).await; + let init_data = util::debug_initializer::InitConfig::load(data_dir).await?; // This error is ignored because it's throwing on mobile despite the folder existing. let _ = fs::create_dir_all(&data_dir).await; - // dbg!(get_object_kind_from_extension("png")); - - // let (non_blocking, _guard) = tracing_appender::non_blocking(rolling::daily( - // Path::new(&data_dir).join("logs"), - // "log", - // )); - // TODO: Make logs automatically delete after x time https://github.com/tokio-rs/tracing/pull/2169 - - let subscriber = tracing_subscriber::registry().with( - EnvFilter::from_default_env() - .add_directive("warn".parse().expect("Error invalid tracing directive!")) - .add_directive( - "sd_core=debug" - .parse() - .expect("Error invalid tracing directive!"), - ) - .add_directive( - "sd_core::location::manager=info" - .parse() - .expect("Error invalid tracing directive!"), - ) - .add_directive( - "sd_core_mobile=debug" - .parse() - .expect("Error invalid tracing directive!"), - ) - // .add_directive( - // "sd_p2p=debug" - // .parse() - // .expect("Error invalid tracing directive!"), - // ) - .add_directive( - "server=debug" - .parse() - .expect("Error invalid tracing directive!"), - ) - .add_directive( - "desktop=debug" - .parse() - .expect("Error invalid tracing directive!"), - ), - // .add_directive( - // "rspc=debug" - // .parse() - // .expect("Error invalid tracing directive!"), - // ), - ); - #[cfg(not(target_os = "android"))] - let subscriber = subscriber.with(tracing_subscriber::fmt::layer().with_filter(CONSOLE_LOG_FILTER)); - // #[cfg(target_os = "android")] - // let subscriber = subscriber.with(tracing_android::layer("com.spacedrive.app").unwrap()); // TODO: This is not working - subscriber - // .with( - // Layer::default() - // .with_writer(non_blocking) - // .with_ansi(false) - // .with_filter(LevelFilter::DEBUG), - // ) - .init(); - let event_bus = broadcast::channel(1024); let config = NodeConfigManager::new(data_dir.to_path_buf()) .await @@ -134,7 +74,7 @@ impl Node { let jobs = JobManager::new(); let location_manager = LocationManager::new(); - let (p2p, mut p2p_rx) = P2PManager::new(config.clone()).await; + let (p2p, mut p2p_rx) = P2PManager::new(config.clone()).await?; let library_manager = LibraryManager::new( data_dir.join("libraries"), @@ -150,11 +90,9 @@ impl Node { #[cfg(debug_assertions)] if let Some(init_data) = init_data { - init_data.apply(&library_manager).await; + init_data.apply(&library_manager).await?; } - debug!("Watching locations"); - tokio::spawn({ let library_manager = library_manager.clone(); @@ -168,8 +106,12 @@ impl Node { }; for op in operations { - println!("ingest lib id: {}", library.id); - library.sync.ingest_op(op).await.unwrap(); + library.sync.ingest_op(op).await.unwrap_or_else(|err| { + error!( + "error ingesting operation for library '{}': {err:?}", + library.id + ); + }); } } } @@ -177,6 +119,7 @@ impl Node { let router = api::mount(); let node = Node { + data_dir: data_dir.to_path_buf(), config, library_manager, location_manager, @@ -190,6 +133,73 @@ impl Node { Ok((Arc::new(node), router)) } + pub fn init_logger(data_dir: impl AsRef) -> WorkerGuard { + let log_filter = match cfg!(debug_assertions) { + true => tracing::Level::DEBUG, + false => tracing::Level::INFO, + }; + + let (logfile, guard) = NonBlocking::new( + RollingFileAppender::builder() + .filename_prefix("sd.log") + .rotation(Rotation::DAILY) + .max_log_files(4) + .build(data_dir.as_ref().join("logs")) + .expect("Error setting up log file!"), + ); + + let collector = tracing_subscriber::registry() + .with(fmt::Subscriber::new().with_ansi(false).with_writer(logfile)) + .with( + fmt::Subscriber::new() + .with_writer(std::io::stdout.with_max_level(log_filter)) + .with_filter( + EnvFilter::from_default_env() + .add_directive( + "warn".parse().expect("Error invalid tracing directive!"), + ) + .add_directive( + "sd_core=debug" + .parse() + .expect("Error invalid tracing directive!"), + ) + .add_directive( + "sd_core::location::manager=info" + .parse() + .expect("Error invalid tracing directive!"), + ) + .add_directive( + "sd_core_mobile=debug" + .parse() + .expect("Error invalid tracing directive!"), + ) + // .add_directive( + // "sd_p2p=debug" + // .parse() + // .expect("Error invalid tracing directive!"), + // ) + .add_directive( + "server=debug" + .parse() + .expect("Error invalid tracing directive!"), + ) + .add_directive( + "spacedrive=debug" + .parse() + .expect("Error invalid tracing directive!"), + ), + ), + ); + + tracing::collect::set_global_default(collector) + .map_err(|err| { + println!("Error initializing global logger: {:?}", err); + }) + .ok(); + + guard + } + pub async fn shutdown(&self) { info!("Spacedrive shutting down..."); self.jobs.pause().await; @@ -216,12 +226,17 @@ impl Node { /// Error type for Node related errors. #[derive(Error, Debug)] pub enum NodeError { - #[error("failed to initialize config")] + #[error("failed to initialize config: {0}")] FailedToInitializeConfig(util::migrator::MigratorError), - #[error("failed to initialize library manager")] + #[error("failed to initialize library manager: {0}")] FailedToInitializeLibraryManager(#[from] library::LibraryManagerError), #[error(transparent)] LocationManager(#[from] LocationManagerError), + #[error("failed to initialize p2p manager: {0}")] + P2PManager(#[from] sd_p2p::ManagerError), #[error("invalid platform integer")] InvalidPlatformInt(i32), + #[cfg(debug_assertions)] + #[error("Init config error: {0}")] + InitConfig(#[from] util::debug_initializer::InitConfigError), } diff --git a/core/src/library/manager.rs b/core/src/library/manager.rs index 9b14356b6..6683325ca 100644 --- a/core/src/library/manager.rs +++ b/core/src/library/manager.rs @@ -5,7 +5,7 @@ use crate::{ prisma::{node, PrismaClient}, sync::{SyncManager, SyncMessage}, util::{ - db::load_and_migrate, + db::{load_and_migrate, MigrationError}, error::{FileIOError, NonUtf8PathError}, migrator::MigratorError, seeder::{indexer_rules_seeder, SeederError}, @@ -62,6 +62,8 @@ pub enum LibraryManagerError { KeyManager(#[from] sd_crypto::Error), #[error("failed to run library migrations")] MigratorError(#[from] MigratorError), + #[error("error migrating the library: {0}")] + MigrationError(#[from] MigrationError), #[error("invalid library configuration: {0}")] InvalidConfig(String), #[error(transparent)] @@ -93,7 +95,7 @@ pub async fn seed_keymanager( .iter() .map(|key| { let key = key.clone(); - let uuid = uuid::Uuid::from_str(&key.uuid).unwrap(); + let uuid = uuid::Uuid::from_str(&key.uuid).expect("invalid key id in the DB"); if key.default { default = Some(uuid); @@ -119,8 +121,7 @@ pub async fn seed_keymanager( automount: key.automount, }) }) - .collect::, sd_crypto::Error>>() - .unwrap(); + .collect::, sd_crypto::Error>>()?; // insert all keys from the DB into the keymanager's keystore km.populate_keystore(stored_keys).await?; @@ -368,8 +369,7 @@ impl LibraryManager { LibraryManagerError::NonUtf8Path(NonUtf8PathError(db_path.into())) })? )) - .await - .unwrap(), + .await?, ); let node_config = node_context.config.get().await; diff --git a/core/src/location/file_path_helper/mod.rs b/core/src/location/file_path_helper/mod.rs index 8aa416879..3048919f1 100644 --- a/core/src/location/file_path_helper/mod.rs +++ b/core/src/location/file_path_helper/mod.rs @@ -99,6 +99,8 @@ pub struct FilePathMetadata { pub enum FilePathError { #[error("file Path not found: ", .0.display())] NotFound(Box), + #[error("location '{0}' not found")] + LocationNotFound(i32), #[error("received an invalid sub path: ", .location_path.display(), .sub_path.display())] InvalidSubPath { location_path: Box, @@ -154,7 +156,7 @@ pub async fn create_file_path( .select(location::select!({ id pub_id })) .exec() .await? - .unwrap(); + .ok_or(FilePathError::LocationNotFound(location_id))?; let params = { use file_path::*; @@ -291,7 +293,9 @@ pub async fn ensure_sub_path_is_in_location( let mut sub_path = sub_path.as_ref(); if sub_path.starts_with("/") { // SAFETY: we just checked that it starts with the separator - sub_path = sub_path.strip_prefix("/").unwrap(); + sub_path = sub_path + .strip_prefix("/") + .expect("we just checked that it starts with the separator"); } let location_path = location_path.as_ref(); @@ -361,7 +365,9 @@ pub async fn ensure_sub_path_is_directory( Err(e) if e.kind() == io::ErrorKind::NotFound => { if sub_path.starts_with("/") { // SAFETY: we just checked that it starts with the separator - sub_path = sub_path.strip_prefix("/").unwrap(); + sub_path = sub_path + .strip_prefix("/") + .expect("we just checked that it starts with the separator"); } let location_path = location_path.as_ref(); diff --git a/core/src/location/indexer/walk.rs b/core/src/location/indexer/walk.rs index 50b54c3d0..4941f7e76 100644 --- a/core/src/location/indexer/walk.rs +++ b/core/src/location/indexer/walk.rs @@ -615,7 +615,7 @@ mod tests { use globset::{Glob, GlobSetBuilder}; use tempfile::{tempdir, TempDir}; use tokio::fs; - use tracing_test::traced_test; + // use tracing_test::traced_test; impl PartialEq for WalkedEntry { fn eq(&self, other: &Self) -> bool { @@ -758,7 +758,7 @@ mod tests { } #[tokio::test] - #[traced_test] + // #[traced_test] async fn test_only_photos() { let root = prepare_location().await; let root_path = root.path(); @@ -822,7 +822,7 @@ mod tests { } #[tokio::test] - #[traced_test] + // #[traced_test] async fn test_git_repos() { let root = prepare_location().await; let root_path = root.path(); @@ -895,7 +895,7 @@ mod tests { } #[tokio::test] - #[traced_test] + // #[traced_test] async fn git_repos_without_deps_or_build_dirs() { let root = prepare_location().await; let root_path = root.path(); diff --git a/core/src/location/manager/mod.rs b/core/src/location/manager/mod.rs index 565efc663..393cf6e8c 100644 --- a/core/src/location/manager/mod.rs +++ b/core/src/location/manager/mod.rs @@ -103,6 +103,11 @@ pub enum LocationManagerError { #[error("Job Manager error: (error: {0})")] JobManager(#[from] JobManagerError), + #[error("invalid inode")] + InvalidInode, + #[error("invalid device")] + InvalidDevice, + #[error(transparent)] FileIO(#[from] FileIOError), } @@ -554,10 +559,10 @@ impl Drop for StopWatcherGuard<'_> { fn drop(&mut self) { if cfg!(feature = "location-watcher") { // FIXME: change this Drop to async drop in the future - if let Err(e) = block_on( - self.manager - .reinit_watcher(self.location_id, self.library.take().unwrap()), - ) { + if let Err(e) = block_on(self.manager.reinit_watcher( + self.location_id, + self.library.take().expect("library should be set"), + )) { error!("Failed to reinit watcher on stop watcher guard drop: {e}"); } } @@ -578,9 +583,9 @@ impl Drop for IgnoreEventsForPathGuard<'_> { // FIXME: change this Drop to async drop in the future if let Err(e) = block_on(self.manager.watcher_management_message( self.location_id, - self.library.take().unwrap(), + self.library.take().expect("library should be set"), WatcherManagementMessageAction::IgnoreEventsForPath { - path: self.path.take().unwrap(), + path: self.path.take().expect("path should be set"), ignore: false, }, )) { diff --git a/core/src/location/manager/watcher/utils.rs b/core/src/location/manager/watcher/utils.rs index 064120c47..283078b4a 100644 --- a/core/src/location/manager/watcher/utils.rs +++ b/core/src/location/manager/watcher/utils.rs @@ -685,13 +685,23 @@ pub(super) async fn extract_inode_and_device_from_path( .select(file_path::select!( {inode device} )) .exec() .await? - .map(|file_path| { - ( - u64::from_le_bytes(file_path.inode[0..8].try_into().unwrap()), - u64::from_le_bytes(file_path.device[0..8].try_into().unwrap()), - ) - }) - .ok_or_else(|| FilePathError::NotFound(path.into()).into()) + .map_or( + Err(FilePathError::NotFound(path.into()).into()), + |file_path| { + Ok(( + u64::from_le_bytes( + file_path.inode[0..8] + .try_into() + .map_err(|_| LocationManagerError::InvalidInode)?, + ), + u64::from_le_bytes( + file_path.device[0..8] + .try_into() + .map_err(|_| LocationManagerError::InvalidDevice)?, + ), + )) + }, + ) } pub(super) async fn extract_location_path( diff --git a/core/src/location/mod.rs b/core/src/location/mod.rs index dc361070a..23bef9885 100644 --- a/core/src/location/mod.rs +++ b/core/src/location/mod.rs @@ -156,7 +156,10 @@ impl LocationCreateArgs { if metadata.has_library(library.id) { return Err(LocationError::NeedRelink { // SAFETY: This unwrap is ok as we checked that we have this library_id - old_path: metadata.location_path(library.id).unwrap().to_path_buf(), + old_path: metadata + .location_path(library.id) + .expect("This unwrap is ok as we checked that we have this library_id") + .to_path_buf(), new_path: self.path, }); } @@ -281,7 +284,9 @@ impl LocationUpdateArgs { if let Some(mut metadata) = SpacedriveLocationMetadataFile::try_load(&location.path).await? { - metadata.update(library.id, self.name.unwrap()).await?; + metadata + .update(library.id, self.name.expect("TODO")) + .await?; } } } diff --git a/core/src/node/mod.rs b/core/src/node/mod.rs index 04af1c7cd..af8967f6d 100644 --- a/core/src/node/mod.rs +++ b/core/src/node/mod.rs @@ -18,20 +18,16 @@ pub struct LibraryNode { pub last_seen: DateTime, } -impl From for LibraryNode { - fn from(data: node::Data) -> Self { - Self { - uuid: Uuid::from_slice(&data.pub_id).unwrap(), - name: data.name, - platform: Platform::try_from(data.platform).unwrap(), - last_seen: data.last_seen.into(), - } - } -} +impl TryFrom for LibraryNode { + type Error = String; -impl From> for LibraryNode { - fn from(data: Box) -> Self { - Self::from(*data) + fn try_from(data: node::Data) -> Result { + Ok(Self { + uuid: Uuid::from_slice(&data.pub_id).map_err(|_| "Invalid node pub_id")?, + name: data.name, + platform: Platform::try_from(data.platform).map_err(|_| "Invalid platform_id")?, + last_seen: data.last_seen.into(), + }) } } diff --git a/core/src/node/peer_request.rs b/core/src/node/peer_request.rs index 03762f2ab..83af4243c 100644 --- a/core/src/node/peer_request.rs +++ b/core/src/node/peer_request.rs @@ -1,4 +1,4 @@ -#![allow(dead_code, unused_variables)] // TODO: Reenable once this is working +#![allow(dead_code, unused_variables, clippy::panic, clippy::unwrap_used)] // TODO: Reenable once this is working use serde::{Deserialize, Serialize}; use specta::Type; diff --git a/core/src/object/file_identifier/file_identifier_job.rs b/core/src/object/file_identifier/file_identifier_job.rs index a5b3b18f8..43825cf79 100644 --- a/core/src/object/file_identifier/file_identifier_job.rs +++ b/core/src/object/file_identifier/file_identifier_job.rs @@ -149,7 +149,7 @@ impl StatefulJob for FileIdentifierJob { .select(file_path::select!({ id })) .exec() .await? - .unwrap(); // SAFETY: We already validated before that there are orphans `file_path`s + .expect("We already validated before that there are orphans `file_path`s"); // SAFETY: We already validated before that there are orphans `file_path`s data.cursor = first_path.id; diff --git a/core/src/object/file_identifier/mod.rs b/core/src/object/file_identifier/mod.rs index 3b4afb108..ab6618738 100644 --- a/core/src/object/file_identifier/mod.rs +++ b/core/src/object/file_identifier/mod.rs @@ -108,7 +108,8 @@ async fn identifier_job_step( .await .map(|params| { ( - Uuid::from_slice(&file_path.pub_id).unwrap(), + // SAFETY: This should never happen + Uuid::from_slice(&file_path.pub_id).expect("file_path.pub_id is invalid!"), (params, file_path), ) }) @@ -191,7 +192,7 @@ async fn identifier_job_step( let (crdt_op, db_op) = file_path_object_connect_ops( pub_id, // SAFETY: This pub_id is generated by the uuid lib, but we have to store bytes in sqlite - Uuid::from_slice(&object.pub_id).unwrap(), + Uuid::from_slice(&object.pub_id).expect("uuid bytes are invalid"), sync, db, ); diff --git a/core/src/object/file_identifier/shallow_file_identifier_job.rs b/core/src/object/file_identifier/shallow_file_identifier_job.rs index 4385407f5..2b36086fb 100644 --- a/core/src/object/file_identifier/shallow_file_identifier_job.rs +++ b/core/src/object/file_identifier/shallow_file_identifier_job.rs @@ -148,7 +148,7 @@ impl StatefulJob for ShallowFileIdentifierJob { .select(file_path::select!({ id })) .exec() .await? - .unwrap(); // SAFETY: We already validated before that there are orphans `file_path`s + .expect("We already validated before that there are orphans `file_path`s"); // SAFETY: We already validated before that there are orphans `file_path`s data.cursor = first_path.id; diff --git a/core/src/object/fs/encrypt.rs b/core/src/object/fs/encrypt.rs index ae72d6fb1..af30786ca 100644 --- a/core/src/object/fs/encrypt.rs +++ b/core/src/object/fs/encrypt.rs @@ -194,7 +194,12 @@ impl StatefulJob for FileEncryptorJob { .config() .data_directory() .join("thumbnails") - .join(info.path_data.cas_id.as_ref().unwrap()) + .join( + info.path_data + .cas_id + .as_ref() + .ok_or(JobError::MissingCasId)?, + ) .with_extension("wepb"); if tokio::fs::metadata(&pvm_path).await.is_ok() { diff --git a/core/src/object/preview/thumbnail/mod.rs b/core/src/object/preview/thumbnail/mod.rs index e92b4f1e1..9f94109f2 100644 --- a/core/src/object/preview/thumbnail/mod.rs +++ b/core/src/object/preview/thumbnail/mod.rs @@ -124,7 +124,11 @@ pub async fn generate_image_thumbnail>( let webp = block_in_place(|| -> Result, Box> { #[cfg(all(feature = "heif", target_os = "macos"))] let img = { - let ext = file_path.as_ref().extension().unwrap().to_ascii_lowercase(); + let ext = file_path + .as_ref() + .extension() + .unwrap_or_default() + .to_ascii_lowercase(); if HEIF_EXTENSIONS .iter() .any(|e| ext == std::ffi::OsStr::new(e)) diff --git a/core/src/object/validation/validator_job.rs b/core/src/object/validation/validator_job.rs index af1880e46..1579686d2 100644 --- a/core/src/object/validation/validator_job.rs +++ b/core/src/object/validation/validator_job.rs @@ -73,7 +73,10 @@ impl StatefulJob for ObjectValidatorJob { .find_unique(location::id::equals(state.init.location_id)) .exec() .await? - .unwrap(); + .ok_or(JobError::MissingFromDb( + "location", + format!("id={}", state.init.location_id), + ))?; state.data = Some(ObjectValidatorJobState { root_path: location.path.into(), diff --git a/core/src/p2p/p2p_manager.rs b/core/src/p2p/p2p_manager.rs index c7aac3904..6d41ea53c 100644 --- a/core/src/p2p/p2p_manager.rs +++ b/core/src/p2p/p2p_manager.rs @@ -1,4 +1,7 @@ +#![allow(clippy::unwrap_used)] // TODO: Remove once this is fully stablised + use std::{ + borrow::Cow, collections::HashMap, path::PathBuf, sync::Arc, @@ -8,7 +11,7 @@ use std::{ use sd_p2p::{ spaceblock::{self, BlockSize, SpacedropRequest}, spacetime::SpaceTimeStream, - Event, Manager, MetadataManager, PeerId, + Event, Manager, ManagerError, MetadataManager, PeerId, }; use sd_sync::CRDTOperation; use serde::Serialize; @@ -58,7 +61,7 @@ pub struct P2PManager { impl P2PManager { pub async fn new( node_config: Arc, - ) -> (Arc, broadcast::Receiver<(Uuid, Vec)>) { + ) -> Result<(Arc, broadcast::Receiver<(Uuid, Vec)>), ManagerError> { let (config, keypair) = { let config = node_config.get().await; (Self::config_to_metadata(&config), config.keypair) @@ -67,9 +70,7 @@ impl P2PManager { let metadata_manager = MetadataManager::new(config); let (manager, mut stream) = - Manager::new(SPACEDRIVE_APP_ID, &keypair, metadata_manager.clone()) - .await - .unwrap(); + Manager::new(SPACEDRIVE_APP_ID, &keypair, metadata_manager.clone()).await?; info!( "Node '{}' is now online listening at addresses: {:?}", @@ -226,41 +227,7 @@ impl P2PManager { } }); - // TODO(@Oscar): Remove this in the future once i'm done using it for testing - if std::env::var("SPACEDROP_DEMO").is_ok() { - // tokio::spawn({ - // let this = this.clone(); - // async move { - // tokio::time::sleep(std::time::Duration::from_secs(5)).await; - // let mut connected = this - // .manager - // .get_connected_peers() - // .await - // .unwrap() - // .into_iter(); - // if let Some(peer_id) = connected.next() { - // info!("Starting Spacedrop to peer '{}'", peer_id); - // this.broadcast_sync_events( - // Uuid::from_str("e4372586-d028-48f8-8be6-b4ff781a7dc2").unwrap(), - // vec![CRDTOperation { - // node: Uuid::new_v4(), - // timestamp: NTP64(1), - // id: Uuid::new_v4(), - // typ: CRDTOperationType::Owned(OwnedOperation { - // model: "TODO".to_owned(), - // items: Vec::new(), - // }), - // }], - // ) - // .await; - // } else { - // info!("No clients found so skipping Spacedrop demo!"); - // } - // } - // }); - } - - (this, rx2) + Ok((this, rx2)) } fn config_to_metadata(config: &NodeConfig) -> PeerMetadata { @@ -296,7 +263,13 @@ impl P2PManager { #[allow(unused)] // TODO: Remove `allow(unused)` once integrated pub async fn broadcast_sync_events(&self, library_id: Uuid, event: Vec) { - let mut buf = rmp_serde::to_vec_named(&event).unwrap(); // TODO: Error handling + let mut buf = match rmp_serde::to_vec_named(&event) { + Ok(buf) => buf, + Err(e) => { + error!("Failed to serialize sync event: {:?}", e); + return; + } + }; let mut head_buf = Header::Sync(library_id, buf.len() as u32).to_bytes(); // Max Sync payload is like 4GB head_buf.append(&mut buf); @@ -309,26 +282,31 @@ impl P2PManager { self.manager.broadcast(Header::Ping.to_bytes()).await; } - pub async fn big_bad_spacedrop(&self, peer_id: PeerId, path: PathBuf) { - let mut stream = self.manager.stream(peer_id).await.unwrap(); // TODO: handle providing incorrect peer id + // TODO: Proper error handling + pub async fn big_bad_spacedrop(&self, peer_id: PeerId, path: PathBuf) -> Result<(), ()> { + let mut stream = self.manager.stream(peer_id).await.map_err(|_| ())?; // TODO: handle providing incorrect peer id - let file = File::open(&path).await.unwrap(); - let metadata = file.metadata().await.unwrap(); + let file = File::open(&path).await.map_err(|_| ())?; + let metadata = file.metadata().await.map_err(|_| ())?; let header = Header::Spacedrop(SpacedropRequest { - name: path.file_name().unwrap().to_str().unwrap().to_string(), // TODO: Encode this as bytes instead + name: path + .file_name() + .map(|v| v.to_string_lossy()) + .unwrap_or(Cow::Borrowed("")) + .to_string(), size: metadata.len(), block_size: BlockSize::from_size(metadata.len()), // TODO: This should be dynamic }); - stream.write_all(&header.to_bytes()).await.unwrap(); + stream.write_all(&header.to_bytes()).await.map_err(|_| ())?; debug!("Waiting for Spacedrop to be accepted from peer '{peer_id}'"); let mut buf = [0; 1]; // TODO: Add timeout so the connection is dropped if they never response - stream.read_exact(&mut buf).await.unwrap(); + stream.read_exact(&mut buf).await.map_err(|_| ())?; if buf[0] != 1 { debug!("Spacedrop was rejected from peer '{peer_id}'"); - return; + return Ok(()); } debug!("Starting Spacedrop to peer '{peer_id}'"); @@ -349,6 +327,8 @@ impl P2PManager { "Finished Spacedrop to peer '{peer_id}' after '{:?}", i.elapsed() ); + + Ok(()) } pub async fn shutdown(&self) { diff --git a/core/src/p2p/protocol.rs b/core/src/p2p/protocol.rs index ca9e99aa9..d5eb26a0a 100644 --- a/core/src/p2p/protocol.rs +++ b/core/src/p2p/protocol.rs @@ -1,7 +1,11 @@ +use thiserror::Error; use tokio::io::AsyncReadExt; use uuid::Uuid; -use sd_p2p::{spaceblock::SpacedropRequest, spacetime::SpaceTimeStream}; +use sd_p2p::{ + spaceblock::{SpacedropRequest, SpacedropRequestError}, + spacetime::SpaceTimeStream, +}; /// TODO #[derive(Debug, PartialEq, Eq)] @@ -11,31 +15,65 @@ pub enum Header { Sync(Uuid, u32), } +#[derive(Debug, Error)] +pub enum SyncRequestError { + #[error("io error reading library id: {0}")] + LibraryIdIoError(std::io::Error), + #[error("io error decoding library id: {0}")] + ErrorDecodingLibraryId(uuid::Error), + #[error("io error reading sync payload len: {0}")] + PayloadLenIoError(std::io::Error), +} + +#[derive(Debug, Error)] +pub enum HeaderError { + #[error("io error reading discriminator: {0}")] + DiscriminatorIoError(std::io::Error), + #[error("invalid discriminator '{0}'")] + InvalidDiscriminator(u8), + #[error("error reading spacedrop request: {0}")] + SpacedropRequestError(#[from] SpacedropRequestError), + #[error("error reading sync request: {0}")] + SyncRequestError(#[from] SyncRequestError), + #[error("invalid request. Spacedrop requires a unicast stream!")] + SpacedropOverMulticastIsForbidden, +} + impl Header { - pub async fn from_stream(stream: &mut SpaceTimeStream) -> Result { - let discriminator = stream.read_u8().await.map_err(|e| { - dbg!(e); - })?; // TODO: Error handling + pub async fn from_stream(stream: &mut SpaceTimeStream) -> Result { + let discriminator = stream + .read_u8() + .await + .map_err(HeaderError::DiscriminatorIoError)?; match discriminator { 0 => match stream { SpaceTimeStream::Unicast(stream) => Ok(Self::Spacedrop( SpacedropRequest::from_stream(stream).await?, )), - _ => todo!(), + _ => Err(HeaderError::SpacedropOverMulticastIsForbidden), }, 1 => Ok(Self::Ping), 2 => { let mut uuid = [0u8; 16]; - stream.read_exact(&mut uuid).await.map_err(|_| ())?; // TODO: Error handling + stream + .read_exact(&mut uuid) + .await + .map_err(SyncRequestError::LibraryIdIoError)?; let mut len = [0; 4]; - stream.read_exact(&mut len).await.map_err(|_| ())?; // TODO: Error handling + stream + .read_exact(&mut len) + .await + .map_err(SyncRequestError::PayloadLenIoError)?; let len = u32::from_le_bytes(len); - Ok(Self::Sync(Uuid::from_slice(&uuid).unwrap(), len)) // TODO: Error handling + Ok(Self::Sync( + Uuid::from_slice(&uuid).map_err(SyncRequestError::ErrorDecodingLibraryId)?, + len, + )) } - _ => Err(()), + d => Err(HeaderError::InvalidDiscriminator(d)), } } diff --git a/core/src/sync/manager.rs b/core/src/sync/manager.rs index 885f4886d..7556cb0f9 100644 --- a/core/src/sync/manager.rs +++ b/core/src/sync/manager.rs @@ -1,3 +1,5 @@ +#![allow(clippy::unwrap_used, clippy::panic)] // TODO: Brendan remove this once you've got error handling here + use crate::prisma::*; use std::{collections::HashMap, sync::Arc}; diff --git a/core/src/util/debug_initializer.rs b/core/src/util/debug_initializer.rs index fc8a8fd80..591c79d8a 100644 --- a/core/src/util/debug_initializer.rs +++ b/core/src/util/debug_initializer.rs @@ -1,16 +1,22 @@ // ! A system for loading a default set of data on startup. This is ONLY enabled in development builds. use std::{ + io, path::{Path, PathBuf}, time::Duration, }; use crate::{ - library::LibraryConfig, - location::{delete_location, scan_location, LocationCreateArgs}, + job::JobManagerError, + library::{LibraryConfig, LibraryManagerError}, + location::{ + delete_location, scan_location, LocationCreateArgs, LocationError, LocationManagerError, + }, prisma::location, }; +use prisma_client_rust::QueryError; use serde::Deserialize; +use thiserror::Error; use tokio::{ fs::{self, metadata}, time::sleep, @@ -47,39 +53,56 @@ pub struct InitConfig { path: PathBuf, } +#[derive(Error, Debug)] +pub enum InitConfigError { + #[error("error loading the init data: {0}")] + Io(#[from] io::Error), + #[error("error parsing the init data: {0}")] + Json(#[from] serde_json::Error), + #[error("job manager: {0}")] + JobManager(#[from] JobManagerError), + #[error("location manager: {0}")] + LocationManager(#[from] LocationManagerError), + #[error("library manager: {0}")] + LibraryManager(#[from] LibraryManagerError), + #[error("query error: {0}")] + QueryError(#[from] QueryError), + #[error("location error: {0}")] + LocationError(#[from] LocationError), +} + impl InitConfig { - pub async fn load(data_dir: &Path) -> Option { - let path = std::env::current_dir() - .unwrap() + pub async fn load(data_dir: &Path) -> Result, InitConfigError> { + let path = std::env::current_dir()? .join(std::env::var("SD_INIT_DATA").unwrap_or("sd_init.json".to_string())); if metadata(&path).await.is_ok() { - let config = fs::read_to_string(&path).await.unwrap(); - let mut config: InitConfig = serde_json::from_str(&config).unwrap(); + let config = fs::read_to_string(&path).await?; + let mut config: InitConfig = serde_json::from_str(&config)?; config.path = path; if config.reset_on_startup && data_dir.exists() { warn!("previous 'SD_DATA_DIR' was removed on startup!"); - fs::remove_dir_all(&data_dir).await.unwrap(); + fs::remove_dir_all(&data_dir).await?; } - return Some(config); + return Ok(Some(config)); } - None + Ok(None) } - pub async fn apply(self, library_manager: &LibraryManager) { + pub async fn apply(self, library_manager: &LibraryManager) -> Result<(), InitConfigError> { info!("Initializing app from file: {:?}", self.path); for lib in self.libraries { let name = lib.name.clone(); - let handle = tokio::spawn(async move { + let _guard = AbortOnDrop(tokio::spawn(async move { loop { info!("Initializing library '{name}' from 'sd_init.json'..."); sleep(Duration::from_secs(1)).await; } - }); + })); let library = match library_manager.get_library(lib.id).await { Some(lib) => lib, @@ -92,25 +115,27 @@ impl InitConfig { description: lib.description.unwrap_or("".to_string()), }, ) - .await - .unwrap(); + .await?; - library_manager.get_library(library.uuid).await.unwrap() + match library_manager.get_library(library.uuid).await { + Some(lib) => lib, + None => { + warn!( + "Debug init error: library '{}' was not found after being created!", + library.config.name + ); + return Ok(()); + } + } } }; if lib.reset_locations_on_startup { - let locations = library - .db - .location() - .find_many(vec![]) - .exec() - .await - .unwrap(); + let locations = library.db.location().find_many(vec![]).exec().await?; for location in locations { warn!("deleting location: {:?}", location.path); - delete_location(&library, location.id).await.unwrap(); + delete_location(&library, location.id).await?; } } @@ -120,34 +145,47 @@ impl InitConfig { .location() .find_first(vec![location::path::equals(loc.path.clone())]) .exec() - .await - .unwrap() + .await? { warn!("deleting location: {:?}", location.path); - delete_location(&library, location.id).await.unwrap(); + delete_location(&library, location.id).await?; } let sd_file = PathBuf::from(&loc.path).join(".spacedrive"); if sd_file.exists() { - fs::remove_file(sd_file).await.unwrap(); + fs::remove_file(sd_file).await?; } let location = LocationCreateArgs { - path: loc.path.into(), + path: loc.path.clone().into(), dry_run: false, indexer_rules_ids: Vec::new(), } .create(&library) - .await - .unwrap() - .unwrap(); - - scan_location(&library, location).await.unwrap(); + .await?; + match location { + Some(location) => { + scan_location(&library, location).await?; + } + None => { + warn!( + "Debug init error: location '{}' was not found after being created!", + loc.path + ); + } + } } - - handle.abort(); } info!("Initialized app from file: {:?}", self.path); + Ok(()) + } +} + +pub struct AbortOnDrop(tokio::task::JoinHandle); + +impl Drop for AbortOnDrop { + fn drop(&mut self) { + self.0.abort(); } } diff --git a/core/src/util/migrator.rs b/core/src/util/migrator.rs index 3b249959f..cef8d2e4d 100644 --- a/core/src/util/migrator.rs +++ b/core/src/util/migrator.rs @@ -49,7 +49,7 @@ where // } pub fn load(&self, path: &Path) -> Result { - match path.try_exists().unwrap() { + match path.try_exists()? { true => { let mut file = File::options().read(true).write(true).open(path)?; let mut cfg: BaseConfig = serde_json::from_reader(BufReader::new(&mut file))?; @@ -89,10 +89,7 @@ where other: match serde_json::to_value(content)? { Value::Object(map) => map, _ => { - panic!( - "Type '{}' as generic `Migrator::T` must be serialiable to a Serde object!", - type_name::() - ); + return Err(MigratorError::InvalidType(type_name::())); } }, }; @@ -113,6 +110,8 @@ pub enum MigratorError { "the config file is for a newer version of the app. Please update to the latest version to load it!" )] YourAppIsOutdated, + #[error("Type '{0}' as generic `Migrator::T` must be serialiable to a Serde object!")] + InvalidType(&'static str), #[error("custom migration error: {0}")] Custom(String), } diff --git a/core/src/volume.rs b/core/src/volume.rs index 748959018..07ddadee2 100644 --- a/core/src/volume.rs +++ b/core/src/volume.rs @@ -11,6 +11,7 @@ use sysinfo::{DiskExt, System, SystemExt}; use thiserror::Error; #[derive(Serialize, Deserialize, Debug, Clone, Type)] +#[allow(clippy::upper_case_acronyms)] pub enum DiskType { SSD, HDD, diff --git a/crates/p2p/Cargo.toml b/crates/p2p/Cargo.toml index 957c03cb9..0d0516905 100644 --- a/crates/p2p/Cargo.toml +++ b/crates/p2p/Cargo.toml @@ -20,17 +20,19 @@ tokio = { workspace = true, features = [ "io-util", "fs", ] } -libp2p = { version = "0.51.0", features = ["tokio", "quic", "serde"] } +libp2p = { version = "0.51.3", features = ["tokio", "serde"] } +libp2p-quic = { version = "0.7.0-alpha.3", features = ["tokio"] } +if-watch = { version = "3.0.1", features = ["tokio"] } # Override the features of if-watch which is used by libp2p-quic mdns-sd = "0.6.1" -thiserror = "1.0.39" +thiserror = "1.0.40" tracing = "0.1.37" -serde = { version = "1.0.152", features = ["derive"] } +serde = { version = "1.0.163", features = ["derive"] } rmp-serde = "1.1.1" specta = { workspace = true } flume = "0.10.14" -tokio-util = { version = "0.7.7", features = ["compat"] } +tokio-util = { version = "0.7.8", features = ["compat"] } arc-swap = "1.6.0" [dev-dependencies] tokio = { workspace = true, features = ["rt-multi-thread"] } -tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } diff --git a/crates/p2p/src/manager.rs b/crates/p2p/src/manager.rs index 69b7dae98..e365caef9 100644 --- a/crates/p2p/src/manager.rs +++ b/crates/p2p/src/manager.rs @@ -7,7 +7,7 @@ use std::{ }, }; -use libp2p::{core::muxing::StreamMuxerBox, quic, Swarm, Transport}; +use libp2p::{core::muxing::StreamMuxerBox, swarm::SwarmBuilder, Transport}; use thiserror::Error; use tokio::sync::{mpsc, oneshot}; use tracing::{debug, error, warn}; @@ -41,7 +41,7 @@ impl Manager { .then_some(()) .ok_or(ManagerError::InvalidAppName)?; - let peer_id = PeerId(keypair.public().to_peer_id()); + let peer_id = PeerId(keypair.peer_id()); let (event_stream_tx, event_stream_rx) = mpsc::channel(1024); let (mdns, mdns_state) = Mdns::new(application_name, peer_id, metadata_manager) @@ -60,13 +60,16 @@ impl Manager { event_stream_tx, }); - let mut swarm = Swarm::with_tokio_executor( - quic::GenTransport::::new(quic::Config::new(keypair.inner())) - .map(|(p, c), _| (p, StreamMuxerBox::new(c))) - .boxed(), + let mut swarm = SwarmBuilder::with_tokio_executor( + libp2p_quic::GenTransport::::new( + libp2p_quic::Config::new(&keypair.inner()), + ) + .map(|(p, c), _| (p, StreamMuxerBox::new(c))) + .boxed(), SpaceTime::new(this.clone()), - keypair.public().to_peer_id(), - ); + keypair.peer_id(), + ) + .build(); { let listener_id = swarm .listen_on("/ip4/0.0.0.0/udp/0/quic-v1".parse().expect("Error passing libp2p multiaddr. This value is hardcoded so this should be impossible.")) diff --git a/crates/p2p/src/manager_stream.rs b/crates/p2p/src/manager_stream.rs index 1eb3b48a1..58da7e7ad 100644 --- a/crates/p2p/src/manager_stream.rs +++ b/crates/p2p/src/manager_stream.rs @@ -12,7 +12,7 @@ use libp2p::{ futures::StreamExt, swarm::{ dial_opts::{DialOpts, PeerCondition}, - NetworkBehaviourAction, NotifyHandler, SwarmEvent, + NotifyHandler, SwarmEvent, ToSwarm, }, Swarm, }; @@ -114,7 +114,6 @@ where SwarmEvent::IncomingConnection { local_addr, .. } => debug!("incoming connection from '{}'", local_addr), SwarmEvent::IncomingConnectionError { local_addr, error, .. } => warn!("handshake error with incoming connection from '{}': {}", local_addr, error), SwarmEvent::OutgoingConnectionError { peer_id, error } => warn!("error establishing connection with '{:?}': {}", peer_id, error), - SwarmEvent::BannedPeer { peer_id, .. } => warn!("banned peer '{}' attempted to connection and was rejected", peer_id), SwarmEvent::NewListenAddr { address, .. } => { match quic_multiaddr_to_socketaddr(address) { Ok(addr) => { @@ -162,6 +161,8 @@ where } SwarmEvent::ListenerError { listener_id, error } => warn!("listener '{:?}' reported a non-fatal error: {}", listener_id, error), SwarmEvent::Dialing(_peer_id) => {}, + #[allow(deprecated)] + SwarmEvent::BannedPeer { .. } => {}, } } } @@ -202,26 +203,25 @@ where } } ManagerStreamAction::StartStream(peer_id, rx) => { - self.swarm.behaviour_mut().pending_events.push_back( - NetworkBehaviourAction::NotifyHandler { + self.swarm + .behaviour_mut() + .pending_events + .push_back(ToSwarm::NotifyHandler { peer_id: peer_id.0, handler: NotifyHandler::Any, event: OutboundRequest::Unicast(rx), - }, - ); + }); } ManagerStreamAction::BroadcastData(data) => { let connected_peers = self.swarm.connected_peers().copied().collect::>(); let behaviour = self.swarm.behaviour_mut(); debug!("Broadcasting message to '{:?}'", connected_peers); for peer_id in connected_peers { - behaviour - .pending_events - .push_back(NetworkBehaviourAction::NotifyHandler { - peer_id, - handler: NotifyHandler::Any, - event: OutboundRequest::Broadcast(data.clone()), - }); + behaviour.pending_events.push_back(ToSwarm::NotifyHandler { + peer_id, + handler: NotifyHandler::Any, + event: OutboundRequest::Broadcast(data.clone()), + }); } } ManagerStreamAction::Shutdown(tx) => { diff --git a/crates/p2p/src/spaceblock/mod.rs b/crates/p2p/src/spaceblock/mod.rs index 823f7dd29..60535b299 100644 --- a/crates/p2p/src/spaceblock/mod.rs +++ b/crates/p2p/src/spaceblock/mod.rs @@ -6,8 +6,10 @@ use std::{ marker::PhantomData, path::{Path, PathBuf}, + string::FromUtf8Error, }; +use thiserror::Error; use tokio::{ fs::File, io::{AsyncBufRead, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader}, @@ -42,22 +44,38 @@ pub struct SpacedropRequest { pub block_size: BlockSize, } +#[derive(Debug, Error)] +pub enum SpacedropRequestError { + #[error("io error reading name len: {0}")] + NameLenIoError(std::io::Error), + #[error("io error reading name: {0}")] + NameIoError(std::io::Error), + #[error("error utf-8 decoding name: {0}")] + NameFormatError(FromUtf8Error), + #[error("io error reading file size: {0}")] + SizeIoError(std::io::Error), +} + impl SpacedropRequest { - pub async fn from_stream(stream: &mut (impl AsyncRead + Unpin)) -> Result { - let mut name_len = [0; 2]; - stream.read_exact(&mut name_len).await.map_err(|_| ())?; // TODO: Error handling - let name_len = u16::from_le_bytes(name_len); - + pub async fn from_stream( + stream: &mut (impl AsyncRead + Unpin), + ) -> Result { + let name_len = stream + .read_u16_le() + .await + .map_err(SpacedropRequestError::NameLenIoError)?; let mut name = vec![0u8; name_len as usize]; - stream.read_exact(&mut name).await.map_err(|_| ())?; // TODO: Error handling - let name = String::from_utf8(name).map_err(|_| ())?; // TODO: Error handling + stream + .read_exact(&mut name) + .await + .map_err(SpacedropRequestError::NameIoError)?; + let name = String::from_utf8(name).map_err(SpacedropRequestError::NameFormatError)?; - let mut size = [0; 8]; - stream.read_exact(&mut size).await.map_err(|_| ())?; // TODO: Error handling - let size = u64::from_le_bytes(size); - - // TODO: If we change `BlockSize` and both clients are running a different version this will not match up and everything will explode - let block_size = BlockSize::from_size(size); // TODO: Error handling + let size = stream + .read_u64_le() + .await + .map_err(SpacedropRequestError::SizeIoError)?; + let block_size = BlockSize::from_size(size); // TODO: Get from stream: stream.read_u8().await.map_err(|_| ())?; // TODO: Error handling Ok(Self { name, diff --git a/crates/p2p/src/spacetime/behaviour.rs b/crates/p2p/src/spacetime/behaviour.rs index e6c303724..7a7556925 100644 --- a/crates/p2p/src/spacetime/behaviour.rs +++ b/crates/p2p/src/spacetime/behaviour.rs @@ -8,8 +8,8 @@ use libp2p::{ core::{ConnectedPoint, Endpoint}, swarm::{ derive_prelude::{ConnectionEstablished, ConnectionId, FromSwarm}, - ConnectionClosed, ConnectionDenied, ConnectionHandler, NetworkBehaviour, - NetworkBehaviourAction, PollParameters, THandler, THandlerInEvent, + ConnectionClosed, ConnectionDenied, ConnectionHandler, NetworkBehaviour, PollParameters, + THandler, THandlerInEvent, ToSwarm, }, Multiaddr, }; @@ -34,9 +34,8 @@ pub enum OutboundFailure {} /// This protocol sits under the application to abstract many complexities of 2 way connections and deals with authentication, chucking, etc. pub struct SpaceTime { pub(crate) manager: Arc>, - pub(crate) pending_events: VecDeque< - NetworkBehaviourAction<::OutEvent, THandlerInEvent>, - >, + pub(crate) pending_events: + VecDeque::OutEvent, THandlerInEvent>>, // For future me's sake, DON't try and refactor this to use shared state (for the nth time), it doesn't fit into libp2p's synchronous trait and polling model!!! // pub(crate) connected_peers: HashMap, } @@ -116,12 +115,11 @@ impl NetworkBehaviour for SpaceTime { { debug!("sending establishment request to peer '{}'", peer_id); if other_established == 0 { - self.pending_events - .push_back(NetworkBehaviourAction::GenerateEvent( - ManagerStreamAction::Event(Event::PeerConnected(ConnectedPeer { - peer_id, - })), - )); + self.pending_events.push_back(ToSwarm::GenerateEvent( + ManagerStreamAction::Event(Event::PeerConnected(ConnectedPeer { + peer_id, + })), + )); } } } @@ -133,10 +131,9 @@ impl NetworkBehaviour for SpaceTime { let peer_id = PeerId(peer_id); if remaining_established == 0 { debug!("Disconnected from peer '{}'", peer_id); - self.pending_events - .push_back(NetworkBehaviourAction::GenerateEvent( - ManagerStreamAction::Event(Event::PeerDisconnected(peer_id)), - )); + self.pending_events.push_back(ToSwarm::GenerateEvent( + ManagerStreamAction::Event(Event::PeerDisconnected(peer_id)), + )); } } FromSwarm::AddressChange(event) => { @@ -187,15 +184,14 @@ impl NetworkBehaviour for SpaceTime { _connection: ConnectionId, event: as ConnectionHandler>::OutEvent, ) { - self.pending_events - .push_back(NetworkBehaviourAction::GenerateEvent(event)); + self.pending_events.push_back(ToSwarm::GenerateEvent(event)); } fn poll( &mut self, _: &mut Context<'_>, _: &mut impl PollParameters, - ) -> Poll>> { + ) -> Poll>> { if let Some(ev) = self.pending_events.pop_front() { return Poll::Ready(ev); } else if self.pending_events.capacity() > EMPTY_QUEUE_SHRINK_THRESHOLD { diff --git a/crates/p2p/src/utils/keypair.rs b/crates/p2p/src/utils/keypair.rs index 978b33f1f..62e4f6426 100644 --- a/crates/p2p/src/utils/keypair.rs +++ b/crates/p2p/src/utils/keypair.rs @@ -1,20 +1,22 @@ -use libp2p::identity::{ed25519, PublicKey}; +use libp2p::identity::ed25519::{self}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone)] -pub struct Keypair(libp2p::identity::Keypair); +pub struct Keypair(ed25519::Keypair); impl Keypair { pub fn generate() -> Self { - Self(libp2p::identity::Keypair::generate_ed25519()) + Self(ed25519::Keypair::generate()) } - pub fn public(&self) -> PublicKey { - self.0.public() + pub fn peer_id(&self) -> libp2p::PeerId { + let pk: libp2p::identity::PublicKey = self.0.public().into(); + + libp2p::PeerId::from_public_key(&pk) } - pub fn inner(&self) -> &libp2p::identity::Keypair { - &self.0 + pub fn inner(&self) -> libp2p::identity::Keypair { + self.0.clone().into() } } @@ -23,13 +25,7 @@ impl Serialize for Keypair { where S: serde::Serializer, { - match &self.0 { - libp2p::identity::Keypair::Ed25519(keypair) => { - serializer.serialize_bytes(&keypair.encode()) - } - #[allow(unreachable_patterns)] - _ => unreachable!(), - } + serializer.serialize_bytes(&self.0.to_bytes()) } } @@ -39,8 +35,9 @@ impl<'de> Deserialize<'de> for Keypair { D: serde::Deserializer<'de>, { let mut bytes = Vec::::deserialize(deserializer)?; - Ok(Self(libp2p::identity::Keypair::Ed25519( - ed25519::Keypair::decode(bytes.as_mut_slice()).map_err(serde::de::Error::custom)?, - ))) + Ok(Self( + ed25519::Keypair::try_from_bytes(bytes.as_mut_slice()) + .map_err(serde::de::Error::custom)?, + )) } } diff --git a/crates/p2p/src/utils/peer_id.rs b/crates/p2p/src/utils/peer_id.rs index 158aa67ef..e33b07b6d 100644 --- a/crates/p2p/src/utils/peer_id.rs +++ b/crates/p2p/src/utils/peer_id.rs @@ -10,6 +10,7 @@ pub struct PeerId( ); impl FromStr for PeerId { + #[allow(deprecated)] type Err = libp2p::core::ParseError; fn from_str(s: &str) -> Result { diff --git a/interface/ErrorFallback.tsx b/interface/ErrorFallback.tsx index 3074eea78..a4afdb89c 100644 --- a/interface/ErrorFallback.tsx +++ b/interface/ErrorFallback.tsx @@ -1,11 +1,29 @@ import { captureException } from '@sentry/browser'; import { FallbackProps } from 'react-error-boundary'; +import { useRouteError } from 'react-router'; import { useDebugState } from '@sd/client'; import { Button } from '@sd/ui'; +import { useOperatingSystem } from './hooks'; + +export function RouterErrorBoundary() { + const error = useRouteError(); + return ( + { + captureException(error); + location.reload(); + }} + reloadBtn={() => { + location.reload(); + }} + /> + ); +} export default ({ error, resetErrorBoundary }: FallbackProps) => ( { captureException(error); resetErrorBoundary(); @@ -17,28 +35,34 @@ export default ({ error, resetErrorBoundary }: FallbackProps) => ( export function ErrorPage({ reloadBtn, sendReportBtn, - message + message, + submessage }: { reloadBtn?: () => void; sendReportBtn?: () => void; message: string; + submessage?: string; }) { const debug = useDebugState(); + const os = useOperatingSystem(); + const isMacOS = os === 'macOS'; + + if (!submessage && debug.enabled) + submessage = 'Check the console (CMD/CTRL + OPTION + i) for stack trace.'; return (

APP CRASHED

We're past the event horizon...

-
Error: {message}
- {debug.enabled && ( -
-					Check the console (CMD/CTRL + OPTION + i) for stack trace.
-				
- )} +
{message}
+ {submessage &&
{submessage}
}
{reloadBtn && (
+
{ + platform?.openLogsDir?.(); + }} + className="text-sm font-medium text-ink-faint" + > + + Logs Folder + + {node.data?.data_path + '/logs'} +
diff --git a/interface/app/index.tsx b/interface/app/index.tsx index 807155c71..41991acad 100644 --- a/interface/app/index.tsx +++ b/interface/app/index.tsx @@ -1,6 +1,7 @@ import { Navigate, Outlet, RouteObject } from 'react-router-dom'; import { currentLibraryCache, useCachedLibraries, useInvalidateQuery } from '@sd/client'; import { Dialogs } from '@sd/ui'; +import { RouterErrorBoundary } from '~/ErrorFallback'; import { useKeybindHandler } from '~/hooks/useKeyboardHandler'; import libraryRoutes from './$libraryId'; import onboardingRoutes from './onboarding'; @@ -39,6 +40,7 @@ const Wrapper = () => { export const routes = [ { element: , + errorElement: , children: [ { index: true, diff --git a/interface/package.json b/interface/package.json index de6519dde..24782373d 100644 --- a/interface/package.json +++ b/interface/package.json @@ -19,6 +19,7 @@ "dependencies": { "@fontsource/inter": "^4.5.13", "@headlessui/react": "^1.7.3", + "@icons-pack/react-simple-icons": "^7.2.0", "@radix-ui/react-progress": "^1.0.1", "@radix-ui/react-slider": "^1.1.0", "@radix-ui/react-toast": "^1.1.2", diff --git a/interface/util/Platform.tsx b/interface/util/Platform.tsx index 2af08672b..682017247 100644 --- a/interface/util/Platform.tsx +++ b/interface/util/Platform.tsx @@ -21,6 +21,7 @@ export type Platform = { saveFilePickerDialog?(): Promise; showDevtools?(): void; openPath?(path: string): void; + openLogsDir?(): void; // Opens a file path with a given ID openFilePath?(library: string, id: number): any; getFilePathOpenWithApps?(library: string, id: number): any; diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 16f98fd1f..e5c8e7ceb 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -77,7 +77,7 @@ export type Procedures = { { key: "locations.quickRescan", input: LibraryArgs, result: null } | { key: "locations.relink", input: LibraryArgs, result: null } | { key: "locations.update", input: LibraryArgs, result: null } | - { key: "nodes.changeNodeName", input: ChangeNodeNameArgs, result: null } | + { key: "nodes.changeNodeName", input: ChangeNodeNameArgs, result: NodeConfig } | { key: "p2p.acceptSpacedrop", input: [string, string | null], result: null } | { key: "p2p.spacedrop", input: SpacedropArgs, result: null } | { key: "tags.assign", input: LibraryArgs, result: null } | @@ -96,10 +96,6 @@ export type FilePathSearchArgs = { take?: number | null; order?: FilePathSearchO export type PeerMetadata = { name: string; operating_system: OperatingSystem | null; version: string | null; email: string | null; img_url: string | null } -export type MasterPasswordChangeArgs = { password: Protected; algorithm: Algorithm; hashing_algorithm: HashingAlgorithm } - -export type Node = { id: number; pub_id: number[]; name: string; platform: number; version: string | null; last_seen: string; timezone: string | null; date_created: string } - /** * NodeConfig is the configuration for a node. This is shared between all libraries and is stored in a JSON file on disk. */ @@ -130,19 +126,11 @@ export type LibraryConfigWrapped = { uuid: string; config: LibraryConfig } */ export type Params = "Standard" | "Hardened" | "Paranoid" -/** - * `LocationUpdateArgs` is the argument received from the client using `rspc` to update a location. - * It contains the id of the location to be updated, possible a name to change the current location's name - * and a vector of indexer rules ids to add or remove from the location. - * - * It is important to note that only the indexer rule ids in this vector will be used from now on. - * Old rules that aren't in this vector will be purged. - */ -export type LocationUpdateArgs = { id: number; name: string | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; indexer_rules_ids: number[] } +export type Location = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string } export type SortOrder = "Asc" | "Desc" -export type MediaData = { id: number; pixel_width: number | null; pixel_height: number | null; longitude: number | null; latitude: number | null; fps: number | null; capture_device_make: string | null; capture_device_model: string | null; capture_device_software: string | null; duration_seconds: number | null; codecs: string | null; streams: number | null } +export type KeyAddArgs = { algorithm: Algorithm; hashing_algorithm: HashingAlgorithm; key: Protected; library_sync: boolean; automount: boolean } /** * Represents the operating system which the remote peer is running. @@ -163,8 +151,6 @@ export type OnboardingConfig = { password: Protected; algorithm: Algorit export type FileDecryptorJobInit = { location_id: number; path_id: number; mount_associated_key: boolean; output_path: string | null; password: string | null; save_to_library: boolean | null } -export type Statistics = { id: number; date_captured: string; total_object_count: number; library_db_size: string; total_bytes_used: string; total_bytes_capacity: string; total_unique_bytes: string; total_bytes_free: string; preview_media_bytes: string } - export type TagCreateArgs = { name: string; color: string } export type LightScanArgs = { location_id: number; sub_path: string } @@ -178,10 +164,20 @@ export type FileEraserJobInit = { location_id: number; path_id: number; passes: */ export type Nonce = { XChaCha20Poly1305: number[] } | { Aes256Gcm: number[] } -export type UnlockKeyManagerArgs = { password: Protected; secret_key: Protected } +export type AutomountUpdateArgs = { uuid: string; status: boolean } export type NodeState = ({ id: string; name: string; p2p_port: number | null; p2p_email: string | null; p2p_img_url: string | null }) & { data_path: string } +/** + * `LocationUpdateArgs` is the argument received from the client using `rspc` to update a location. + * It contains the id of the location to be updated, possible a name to change the current location's name + * and a vector of indexer rules ids to add or remove from the location. + * + * It is important to note that only the indexer rule ids in this vector will be used from now on. + * Old rules that aren't in this vector will be purged. + */ +export type LocationUpdateArgs = { id: number; name: string | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; indexer_rules_ids: number[] } + export type EditLibraryArgs = { id: string; name: string | null; description: string | null } export type SetNoteArgs = { id: number; note: string | null } @@ -204,20 +200,29 @@ export type Salt = number[] */ export type Category = "Recents" | "Favorites" | "Photos" | "Videos" | "Movies" | "Music" | "Documents" | "Downloads" | "Encrypted" | "Projects" | "Applications" | "Archives" | "Databases" | "Games" | "Books" | "Contacts" | "Trash" +/** + * TODO: P2P event for the frontend + */ +export type P2PEvent = { type: "DiscoveredPeer"; peer_id: PeerId; metadata: PeerMetadata } | { type: "SpacedropRequest"; id: string; peer_id: PeerId; name: string } + export type FileCopierJobInit = { source_location_id: number; source_path_id: number; target_location_id: number; target_path: string; target_file_name_suffix: string | null } export type DiskType = "SSD" | "HDD" | "Removable" +export type RestoreBackupArgs = { password: Protected; secret_key: Protected; path: string } + export type SetFavoriteArgs = { id: number; favorite: boolean } export type FilePathFilterArgs = { locationId?: number | null; search?: string; extension?: string | null; createdAt?: OptionalRange; path?: string | null; object?: ObjectFilterArgs | null } export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent" -export type Volume = { name: string; mount_point: string; total_capacity: string; available_capacity: string; is_removable: boolean; disk_type: DiskType | null; file_system: string | null; is_root_filesystem: boolean } +export type MediaData = { id: number; pixel_width: number | null; pixel_height: number | null; longitude: number | null; latitude: number | null; fps: number | null; capture_device_make: string | null; capture_device_model: string | null; capture_device_software: string | null; duration_seconds: number | null; codecs: string | null; streams: number | null } export type FilePathSearchOrdering = { name: SortOrder } | { sizeInBytes: SortOrder } | { dateCreated: SortOrder } | { dateModified: SortOrder } | { dateIndexed: SortOrder } | { object: ObjectSearchOrdering } +export type IndexerRule = { id: number; name: string; default: boolean; rules_per_kind: number[]; date_created: string; date_modified: string } + export type BuildInfo = { version: string; commit: string } export type IdentifyUniqueFilesArgs = { id: number; path: string } @@ -227,7 +232,7 @@ export type IdentifyUniqueFilesArgs = { id: number; path: string } */ export type Algorithm = "XChaCha20Poly1305" | "Aes256Gcm" -export type Location = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string } +export type Tag = { id: number; pub_id: number[]; name: string | null; color: string | null; total_objects: number | null; redundancy_goal: number | null; date_created: string; date_modified: string } export type OwnedOperationItem = { id: any; data: OwnedOperationData } @@ -235,21 +240,10 @@ export type ObjectSearchOrdering = { dateAccessed: SortOrder } export type CRDTOperationType = SharedOperation | RelationOperation | OwnedOperation -/** - * TODO: P2P event for the frontend - */ -export type P2PEvent = { type: "DiscoveredPeer"; peer_id: PeerId; metadata: PeerMetadata } | { type: "SpacedropRequest"; id: string; peer_id: PeerId; name: string } - -export type RenameFileArgs = { location_id: number; file_name: string; new_file_name: string } - export type MaybeNot = T | { not: T } export type SpacedropArgs = { peer_id: PeerId; file_path: string[] } -export type Object = { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; date_accessed: string | null } - -export type FilePath = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string } - export type JobReport = { id: string; name: string; action: string | null; data: number[] | null; metadata: any | null; is_background: boolean; errors_text: string[]; created_at: string | null; started_at: string | null; completed_at: string | null; parent_id: string | null; status: JobStatus; task_count: number; completed_task_count: number; message: string; estimated_completion: string } export type ObjectFilterArgs = { favorite?: boolean | null; hidden?: boolean | null; dateAccessed?: MaybeNot | null; kind?: number[]; tags?: number[] } @@ -280,12 +274,8 @@ export type IndexerRuleCreateArgs = { name: string; dry_run: boolean; rules: ([R export type SharedOperationCreateData = { u: { [key: string]: any } } | "a" -export type KeyAddArgs = { algorithm: Algorithm; hashing_algorithm: HashingAlgorithm; key: Protected; library_sync: boolean; automount: boolean } - export type OptionalRange = { from: T | null; to: T | null } -export type IndexerRule = { id: number; name: string; default: boolean; rules_per_kind: number[]; date_created: string; date_modified: string } - export type FileEncryptorJobInit = { location_id: number; path_id: number; key_uuid: string; algorithm: Algorithm; metadata: boolean; preview_media: boolean; output_path: string | null } /** @@ -302,22 +292,26 @@ export type ExplorerItem = { type: "Path"; has_thumbnail: boolean; item: FilePat */ export type LibraryArgs = { library_id: string; arg: T } -export type FileCutterJobInit = { source_location_id: number; source_path_id: number; target_location_id: number; target_path: string } +export type UnlockKeyManagerArgs = { password: Protected; secret_key: Protected } -export type Tag = { id: number; pub_id: number[]; name: string | null; color: string | null; total_objects: number | null; redundancy_goal: number | null; date_created: string; date_modified: string } +export type FileCutterJobInit = { source_location_id: number; source_path_id: number; target_location_id: number; target_path: string } export type OwnedOperationData = { Create: { [key: string]: any } } | { CreateMany: { values: ([any, { [key: string]: any }])[]; skip_duplicates: boolean } } | { Update: { [key: string]: any } } | "Delete" export type SharedOperationData = SharedOperationCreateData | { field: string; value: any } | null +export type Node = { id: number; pub_id: number[]; name: string; platform: number; version: string | null; last_seen: string; timezone: string | null; date_created: string } + +export type Volume = { name: string; mount_point: string; total_capacity: string; available_capacity: string; is_removable: boolean; disk_type: DiskType | null; file_system: string | null; is_root_filesystem: boolean } + export type TagUpdateArgs = { id: number; name: string | null; color: string | null } +export type MasterPasswordChangeArgs = { password: Protected; algorithm: Algorithm; hashing_algorithm: HashingAlgorithm } + export type ObjectValidatorArgs = { id: number; path: string } export type TagAssignArgs = { object_id: number; tag_id: number; unassign: boolean } -export type ChangeNodeNameArgs = { name: string } - /** * This defines all available password hashing algorithms. */ @@ -336,11 +330,17 @@ export type LibraryConfig = { name: string; description: string } export type SearchData = { cursor: number[] | null; items: T[] } -export type AutomountUpdateArgs = { uuid: string; status: boolean } +export type FilePath = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string } + +export type Statistics = { id: number; date_captured: string; total_object_count: number; library_db_size: string; total_bytes_used: string; total_bytes_capacity: string; total_unique_bytes: string; total_bytes_free: string; preview_media_bytes: string } export type Protected = T -export type RestoreBackupArgs = { password: Protected; secret_key: Protected; path: string } +export type ChangeNodeNameArgs = { name: string } + +export type Object = { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; date_accessed: string | null } + +export type RenameFileArgs = { location_id: number; file_name: string; new_file_name: string } export type RelationOperation = { relation_item: string; relation_group: string; relation: string; data: RelationOperationData } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92d9228c7191088da614a1cae1995ea9886eaa10..64b18b51fc2a3732b27c6804fefa44c14714426c 100644 GIT binary patch delta 37263 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 hovZ{cWtdx?q~2wiS~PYOf~pLCWc9MX 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