diff --git a/Cargo.lock b/Cargo.lock
index 314b5d6b6..35000242d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2683,6 +2683,18 @@ dependencies = [
"serde_json",
]
+[[package]]
+name = "dns-lookup"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5766087c2235fec47fafa4cfecc81e494ee679d0fd4a59887ea0919bfb0e4fc"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "socket2 0.5.5",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "dotenvy"
version = "0.15.7"
@@ -4255,7 +4267,7 @@ dependencies = [
"httpdate",
"itoa 1.0.10",
"pin-project-lite",
- "socket2 0.4.10",
+ "socket2 0.5.5",
"tokio",
"tower-service",
"tracing",
@@ -4429,7 +4441,7 @@ dependencies = [
[[package]]
name = "if-watch"
version = "3.2.0"
-source = "git+https://github.com/oscartbeaumont/if-watch.git?rev=a92c17d3f85c1c6fb0afeeaf6c2b24d0b147e8c3#a92c17d3f85c1c6fb0afeeaf6c2b24d0b147e8c3"
+source = "git+https://github.com/spacedriveapp/if-watch.git?rev=a92c17d3f85c1c6fb0afeeaf6c2b24d0b147e8c3#a92c17d3f85c1c6fb0afeeaf6c2b24d0b147e8c3"
dependencies = [
"async-io 2.3.0",
"core-foundation",
@@ -8554,9 +8566,9 @@ dependencies = [
[[package]]
name = "rmp"
-version = "0.8.12"
+version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20"
+checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
dependencies = [
"byteorder",
"num-traits",
@@ -8565,9 +8577,9 @@ dependencies = [
[[package]]
name = "rmp-serde"
-version = "1.1.2"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a"
+checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
dependencies = [
"byteorder",
"rmp",
@@ -9462,8 +9474,10 @@ version = "0.2.0"
dependencies = [
"base64 0.21.7",
"base91",
+ "dns-lookup",
"ed25519-dalek",
"flume 0.11.0",
+ "futures",
"futures-core",
"hash_map_diff",
"if-watch",
@@ -9473,6 +9487,7 @@ dependencies = [
"pin-project-lite",
"rand_core 0.6.4",
"reqwest 0.11.23",
+ "rmp-serde",
"serde",
"sha256",
"specta",
diff --git a/Cargo.toml b/Cargo.toml
index 572ca0e80..b03757327 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,17 +1,17 @@
[workspace]
resolver = "2"
members = [
- "core",
- "core/crates/*",
- "crates/*",
- "apps/cli",
- "apps/p2p-relay",
- "apps/desktop/src-tauri",
- "apps/desktop/crates/*",
- "apps/mobile/modules/sd-core/core",
- "apps/mobile/modules/sd-core/android/crate",
- "apps/mobile/modules/sd-core/ios/crate",
- "apps/server",
+ "core",
+ "core/crates/*",
+ "crates/*",
+ "apps/cli",
+ "apps/p2p-relay",
+ "apps/desktop/src-tauri",
+ "apps/desktop/crates/*",
+ "apps/mobile/modules/sd-core/core",
+ "apps/mobile/modules/sd-core/android/crate",
+ "apps/mobile/modules/sd-core/ios/crate",
+ "apps/server",
]
[workspace.package]
@@ -21,19 +21,19 @@ repository = "https://github.com/spacedriveapp/spacedrive"
[workspace.dependencies]
prisma-client-rust = { git = "https://github.com/brendonovich/prisma-client-rust", rev = "4f9ef9d38ca732162accff72b2eb684d2f120bab", features = [
- "migrations",
- "specta",
- "sqlite",
- "sqlite-create-many",
+ "migrations",
+ "specta",
+ "sqlite",
+ "sqlite-create-many",
], default-features = false }
prisma-client-rust-cli = { git = "https://github.com/brendonovich/prisma-client-rust", rev = "4f9ef9d38ca732162accff72b2eb684d2f120bab", features = [
- "migrations",
- "specta",
- "sqlite",
- "sqlite-create-many",
+ "migrations",
+ "specta",
+ "sqlite",
+ "sqlite-create-many",
], default-features = false }
prisma-client-rust-sdk = { git = "https://github.com/brendonovich/prisma-client-rust", rev = "4f9ef9d38ca732162accff72b2eb684d2f120bab", features = [
- "sqlite",
+ "sqlite",
], default-features = false }
rspc = { version = "0.1.4" }
@@ -89,7 +89,7 @@ webp = "0.3.0"
[patch.crates-io]
# Proper IOS Support
-if-watch = { git = "https://github.com/oscartbeaumont/if-watch.git", rev = "a92c17d3f85c1c6fb0afeeaf6c2b24d0b147e8c3" }
+if-watch = { git = "https://github.com/spacedriveapp/if-watch.git", rev = "a92c17d3f85c1c6fb0afeeaf6c2b24d0b147e8c3" }
# We hack it to the high heavens
rspc = { git = "https://github.com/spacedriveapp/rspc.git", rev = "ab12964b140991e0730c3423693533fba71efb03" }
diff --git a/apps/p2p-relay/deploy.sh b/apps/p2p-relay/deploy.sh
index 096394879..78dd7b558 100755
--- a/apps/p2p-relay/deploy.sh
+++ b/apps/p2p-relay/deploy.sh
@@ -3,10 +3,13 @@
set -e
-SERVER="54.176.132.155"
+SERVER=""
TARGET_DIR=$(cargo metadata | jq -r .target_directory)
cargo zigbuild --target aarch64-unknown-linux-musl --release
-echo "$TARGET_DIR/aarch64-unknown-linux-musl/release/sd-p2p-relay"
-
scp "$TARGET_DIR/aarch64-unknown-linux-musl/release/sd-p2p-relay" ec2-user@$SERVER:/home/ec2-user/sd-p2p-relay
+
+# ssh ec2-user@$SERVER
+# ./sd-p2p-relay init
+# Enter the `P2P_SECRET` secret env var from Vercel
+# ./sd-p2p-relay
diff --git a/apps/server/docker/Dockerfile b/apps/server/docker/Dockerfile
index 1d532e9e1..3c2d7393a 100644
--- a/apps/server/docker/Dockerfile
+++ b/apps/server/docker/Dockerfile
@@ -111,9 +111,15 @@ ADD --chmod=755 --checksum=sha256:1d127c69218f2cd14964036f2b057c4b2652cda3996c69
COPY --chmod=755 entrypoint.sh /usr/bin/
+# P2P config
+ENV SD_DOCKER=true
+
# Expose webserver
EXPOSE 8080
+# Expose P2P
+EXPOSE 7373
+
# Create the data directory to store the database
VOLUME [ "/data" ]
diff --git a/core/prisma/schema.prisma b/core/prisma/schema.prisma
index ac2571450..821a34a52 100644
--- a/core/prisma/schema.prisma
+++ b/core/prisma/schema.prisma
@@ -77,8 +77,11 @@ model Instance {
// Enum: sd_core::node::RemoteIdentity
remote_identity Bytes
- node_id Bytes
- metadata Bytes? // TODO: This should not be optional
+ // Enum: uuid::Uuid
+ node_id Bytes
+ // Enum: sd_core::node::RemoteIdentity
+ node_remote_identity Bytes? // TODO: This should not be optional
+ metadata Bytes? // TODO: This should not be optional
last_seen DateTime // Time core started for owner, last P2P message for P2P node
date_created DateTime
diff --git a/core/src/api/cloud.rs b/core/src/api/cloud.rs
index d13dc6a06..3bd40fb23 100644
--- a/core/src/api/cloud.rs
+++ b/core/src/api/cloud.rs
@@ -44,6 +44,10 @@ pub(crate) fn mount() -> AlphaRouter {
}
mod library {
+ use std::str::FromStr;
+
+ use sd_p2p::RemoteIdentity;
+
use crate::util::MaybeUndefined;
use super::*;
@@ -75,6 +79,7 @@ mod library {
library.instance_uuid,
library.identity.to_remote_identity(),
node_config.id,
+ node_config.identity.to_remote_identity(),
&node.p2p.peer_metadata(),
)
.await?;
@@ -139,6 +144,7 @@ mod library {
library.instance_uuid,
library.identity.to_remote_identity(),
node_config.id,
+ node_config.identity.to_remote_identity(),
node.p2p.peer_metadata(),
)
.await?;
@@ -152,7 +158,9 @@ mod library {
instance.uuid,
instance.identity,
instance.node_id,
- node.p2p.peer_metadata(),
+ RemoteIdentity::from_str(&instance.node_remote_identity)
+ .expect("malformed remote identity in the DB"),
+ instance.metadata,
)
.await?;
}
diff --git a/core/src/api/mod.rs b/core/src/api/mod.rs
index 3ad1625e1..eb5ab5fc9 100644
--- a/core/src/api/mod.rs
+++ b/core/src/api/mod.rs
@@ -1,7 +1,7 @@
use crate::{
invalidate_query,
node::{
- config::{NodeConfig, NodeConfigP2P, NodePreferences},
+ config::{is_in_docker, NodeConfig, NodeConfigP2P, NodePreferences},
get_hardware_model_name, HardwareModel,
},
old_job::JobProgressEvent,
@@ -116,6 +116,7 @@ struct NodeState {
config: SanitisedNodeConfig,
data_path: String,
device_model: Option,
+ is_in_docker: bool,
}
pub(crate) fn mount() -> Arc {
@@ -151,6 +152,7 @@ pub(crate) fn mount() -> Arc {
.expect("Found non-UTF-8 path")
.to_string(),
device_model: Some(device_model),
+ is_in_docker: is_in_docker(),
})
})
})
diff --git a/core/src/api/nodes.rs b/core/src/api/nodes.rs
index b5be20f3d..083055298 100644
--- a/core/src/api/nodes.rs
+++ b/core/src/api/nodes.rs
@@ -1,3 +1,5 @@
+use std::collections::HashSet;
+
use crate::{
invalidate_query,
node::config::{P2PDiscoveryState, Port},
@@ -20,10 +22,12 @@ pub(crate) fn mount() -> AlphaRouter {
pub struct ChangeNodeNameArgs {
pub name: Option,
pub p2p_port: Option,
- pub p2p_ipv4_enabled: Option,
- pub p2p_ipv6_enabled: Option,
+ pub p2p_disabled: Option,
+ pub p2p_ipv6_disabled: Option,
+ pub p2p_relay_disabled: Option,
pub p2p_discovery: Option,
pub p2p_remote_access: Option,
+ pub p2p_manual_peers: Option>,
pub image_labeler_version: Option,
}
R.mutation(|node, args: ChangeNodeNameArgs| async move {
@@ -48,17 +52,23 @@ pub(crate) fn mount() -> AlphaRouter {
if let Some(port) = args.p2p_port {
config.p2p.port = port;
};
- if let Some(enabled) = args.p2p_ipv4_enabled {
- config.p2p.ipv4 = enabled;
+ if let Some(enabled) = args.p2p_disabled {
+ config.p2p.disabled = enabled;
};
- if let Some(enabled) = args.p2p_ipv6_enabled {
- config.p2p.ipv6 = enabled;
+ if let Some(enabled) = args.p2p_ipv6_disabled {
+ config.p2p.disable_ipv6 = enabled;
+ };
+ if let Some(enabled) = args.p2p_relay_disabled {
+ config.p2p.disable_relay = enabled;
};
if let Some(discovery) = args.p2p_discovery {
config.p2p.discovery = discovery;
};
if let Some(remote_access) = args.p2p_remote_access {
- config.p2p.remote_access = remote_access;
+ config.p2p.enable_remote_access = remote_access;
+ };
+ if let Some(manual_peers) = args.p2p_manual_peers {
+ config.p2p.manual_peers = manual_peers;
};
#[cfg(feature = "ai")]
diff --git a/core/src/api/p2p.rs b/core/src/api/p2p.rs
index 04a007671..3d23d0374 100644
--- a/core/src/api/p2p.rs
+++ b/core/src/api/p2p.rs
@@ -3,7 +3,7 @@ use crate::p2p::{operations, ConnectionMethod, DiscoveryMethod, Header, P2PEvent
use sd_p2p::{PeerConnectionCandidate, RemoteIdentity};
use rspc::{alpha::AlphaRouter, ErrorCode};
-use serde::{Deserialize, Serialize};
+use serde::Deserialize;
use specta::Type;
use std::{path::PathBuf, sync::PoisonError};
use tokio::io::AsyncWriteExt;
@@ -26,21 +26,32 @@ pub(crate) fn mount() -> AlphaRouter {
}) {
queued.push(P2PEvent::PeerChange {
identity: peer.identity(),
- connection: if peer.is_connected_with_hook(node.p2p.libraries_hook_id) {
- ConnectionMethod::Relay
- } else if peer.is_connected() {
- ConnectionMethod::Local
+ connection: if peer.is_connected() {
+ if node.p2p.quic.is_relayed(peer.identity()) {
+ ConnectionMethod::Relay
+ } else {
+ ConnectionMethod::Local
+ }
} else {
ConnectionMethod::Disconnected
},
- discovery: match peer
+ discovery: if peer
.connection_candidates()
- .contains(&PeerConnectionCandidate::Relay)
+ .iter()
+ .any(|c| matches!(c, PeerConnectionCandidate::Manual(_)))
{
- true => DiscoveryMethod::Relay,
- false => DiscoveryMethod::Local,
+ DiscoveryMethod::Manual
+ } else if peer
+ .connection_candidates()
+ .iter()
+ .all(|c| *c == PeerConnectionCandidate::Relay)
+ {
+ DiscoveryMethod::Relay
+ } else {
+ DiscoveryMethod::Local
},
metadata,
+ addrs: peer.addrs(),
});
}
@@ -59,51 +70,13 @@ pub(crate) fn mount() -> AlphaRouter {
R.query(|node, _: ()| async move { Ok(node.p2p.state().await) })
})
.procedure("listeners", {
- #[derive(Serialize, Type)]
- #[serde(tag = "type")]
- pub enum ListenerState {
- Listening,
- Error { error: String },
- Disabled,
- }
-
- #[derive(Serialize, Type)]
- pub struct Listeners {
- ipv4: ListenerState,
- ipv6: ListenerState,
- }
-
R.query(|node, _: ()| async move {
- let addrs = node
+ Ok(node
.p2p
- .p2p
- .listeners()
- .iter()
- .flat_map(|l| l.addrs.clone())
- .collect::>();
-
- let errors = node
- .p2p
- .listener_errors
+ .listeners
.lock()
- .unwrap_or_else(PoisonError::into_inner);
-
- Ok(Listeners {
- ipv4: match errors.ipv4 {
- Some(ref err) => ListenerState::Error { error: err.clone() },
- None => match addrs.iter().any(|f| f.is_ipv4()) {
- true => ListenerState::Listening,
- false => ListenerState::Disabled,
- },
- },
- ipv6: match errors.ipv6 {
- Some(ref err) => ListenerState::Error { error: err.clone() },
- None => match addrs.iter().any(|f| f.is_ipv6()) {
- true => ListenerState::Listening,
- false => ListenerState::Disabled,
- },
- },
- })
+ .unwrap_or_else(PoisonError::into_inner)
+ .clone())
})
})
.procedure("debugConnect", {
diff --git a/core/src/cloud/sync/receive.rs b/core/src/cloud/sync/receive.rs
index b148f154a..8c62e018c 100644
--- a/core/src/cloud/sync/receive.rs
+++ b/core/src/cloud/sync/receive.rs
@@ -8,6 +8,7 @@ use sd_utils::uuid_to_bytes;
use std::{
collections::{hash_map::Entry, HashMap},
+ str::FromStr,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
@@ -167,6 +168,8 @@ pub async fn run_actor(
collection.instance_uuid,
instance.identity,
instance.node_id,
+ RemoteIdentity::from_str(&instance.node_remote_identity)
+ .expect("malformed remote identity in the DB"),
node.p2p.peer_metadata(),
)
.await
@@ -247,6 +250,7 @@ pub async fn upsert_instance(
uuid: Uuid,
identity: RemoteIdentity,
node_id: Uuid,
+ node_remote_identity: RemoteIdentity,
metadata: HashMap,
) -> prisma_client_rust::Result<()> {
db.instance()
@@ -258,9 +262,14 @@ pub async fn upsert_instance(
node_id.as_bytes().to_vec(),
Utc::now().into(),
Utc::now().into(),
- vec![instance::metadata::set(Some(
- serde_json::to_vec(&metadata).expect("unable to serialize metadata"),
- ))],
+ vec![
+ instance::node_remote_identity::set(Some(
+ node_remote_identity.get_bytes().to_vec(),
+ )),
+ instance::metadata::set(Some(
+ serde_json::to_vec(&metadata).expect("unable to serialize metadata"),
+ )),
+ ],
),
vec![],
)
diff --git a/core/src/library/config.rs b/core/src/library/config.rs
index 4a7bad016..f12a2711f 100644
--- a/core/src/library/config.rs
+++ b/core/src/library/config.rs
@@ -71,10 +71,11 @@ pub enum LibraryConfigVersion {
V8 = 8,
V9 = 9,
V10 = 10,
+ V11 = 11,
}
impl ManagedVersion for LibraryConfig {
- const LATEST_VERSION: LibraryConfigVersion = LibraryConfigVersion::V10;
+ const LATEST_VERSION: LibraryConfigVersion = LibraryConfigVersion::V11;
const KIND: Kind = Kind::Json("version");
@@ -446,6 +447,21 @@ impl LibraryConfig {
.await?;
}
+ (LibraryConfigVersion::V10, LibraryConfigVersion::V11) => {
+ db.instance()
+ .update_many(
+ vec![],
+ vec![instance::node_remote_identity::set(Some(
+ // This is a remote identity that doesn't exist. The expectation is that:
+ // - The current node will update it's own and notice the change causing it to push the updated id to the cloud
+ // - All other instances will be updated when the regular sync process with the cloud happens
+ "SaEhml9thV088ocsOXZ17BrNjFaROB0ojwBvnPHhztI".into(),
+ ))],
+ )
+ .exec()
+ .await?;
+ }
+
_ => {
error!("Library config version is not handled: {:?}", current);
return Err(VersionManagerError::UnexpectedMigration {
diff --git a/core/src/library/manager/mod.rs b/core/src/library/manager/mod.rs
index 49d3bc920..e81085921 100644
--- a/core/src/library/manager/mod.rs
+++ b/core/src/library/manager/mod.rs
@@ -9,7 +9,7 @@ use crate::{
};
use sd_core_sync::SyncMessage;
-use sd_p2p::Identity;
+use sd_p2p::{Identity, RemoteIdentity};
use sd_prisma::prisma::{crdt_operation, instance, location, SortOrder};
use sd_utils::{
db,
@@ -437,7 +437,14 @@ impl Libraries {
.as_ref()
.map(|metadata| serde_json::from_slice(metadata).expect("invalid metadata"));
let instance_node_id = Uuid::from_slice(&instance.node_id)?;
- if instance_node_id != node_config.id || curr_metadata != Some(node.p2p.peer_metadata()) {
+ let instance_node_remote_identity = instance
+ .node_remote_identity
+ .as_ref()
+ .and_then(|v| RemoteIdentity::from_bytes(v).ok());
+ if instance_node_id != node_config.id
+ || instance_node_remote_identity != Some(node_config.identity.to_remote_identity())
+ || curr_metadata != Some(node.p2p.peer_metadata())
+ {
info!(
"Detected that the library '{}' has changed node from '{}' to '{}'. Reconciling node data...",
id, instance_node_id, node_config.id
@@ -450,6 +457,13 @@ impl Libraries {
instance::id::equals(instance.id),
vec![
instance::node_id::set(node_config.id.as_bytes().to_vec()),
+ instance::node_remote_identity::set(Some(
+ node_config
+ .identity
+ .to_remote_identity()
+ .get_bytes()
+ .to_vec(),
+ )),
instance::metadata::set(Some(
serde_json::to_vec(&node.p2p.peer_metadata())
.expect("invalid peer metadata"),
@@ -581,7 +595,13 @@ impl Libraries {
.expect("invalid metadata")
});
let should_update = this_instance.node_id != node_config.id
- || curr_metadata != Some(node.p2p.peer_metadata());
+ || RemoteIdentity::from_str(
+ &this_instance.node_remote_identity,
+ )
+ .ok() != Some(
+ node_config.identity.to_remote_identity(),
+ ) || curr_metadata
+ != Some(node.p2p.peer_metadata());
if should_update {
warn!("Library instance on cloud is outdated. Updating...");
@@ -592,6 +612,7 @@ impl Libraries {
library.id,
this_instance.uuid,
Some(node_config.id),
+ Some(node_config.identity.to_remote_identity()),
Some(node.p2p.peer_metadata()),
)
.await
@@ -630,6 +651,10 @@ impl Libraries {
instance.uuid,
instance.identity,
instance.node_id,
+ RemoteIdentity::from_str(
+ &instance.node_remote_identity,
+ )
+ .expect("malformed remote identity from API"),
instance.metadata,
)
.await
diff --git a/core/src/node/config.rs b/core/src/node/config.rs
index 25e225af7..a908ee502 100644
--- a/core/src/node/config.rs
+++ b/core/src/node/config.rs
@@ -8,6 +8,7 @@ use sd_p2p::Identity;
use sd_utils::error::FileIOError;
use std::{
+ collections::HashSet,
path::{Path, PathBuf},
sync::Arc,
};
@@ -46,23 +47,23 @@ pub enum Port {
impl Port {
pub fn get(&self) -> u16 {
+ if is_in_docker() {
+ return 7373;
+ }
+
match self {
Port::Random => 0,
Port::Discrete(port) => *port,
}
}
- pub fn is_random(&self) -> bool {
+ pub fn is_default(&self) -> bool {
matches!(self, Port::Random)
}
}
-fn default_as_true() -> bool {
- true
-}
-
-fn skip_if_true(value: &bool) -> bool {
- *value
+pub fn is_in_docker() -> bool {
+ std::env::var("SD_DOCKER").as_deref() == Ok("true")
}
fn skip_if_false(value: &bool) -> bool {
@@ -73,14 +74,26 @@ fn skip_if_false(value: &bool) -> bool {
pub struct NodeConfigP2P {
#[serde(default)]
pub discovery: P2PDiscoveryState,
- #[serde(default, skip_serializing_if = "Port::is_random")]
+ #[serde(default, skip_serializing_if = "Port::is_default")]
pub port: Port,
- #[serde(default = "default_as_true", skip_serializing_if = "skip_if_true")]
- pub ipv4: bool,
- #[serde(default = "default_as_true", skip_serializing_if = "skip_if_true")]
- pub ipv6: bool,
#[serde(default, skip_serializing_if = "skip_if_false")]
- pub remote_access: bool,
+ pub disabled: bool,
+ #[serde(default, skip_serializing_if = "skip_if_false")]
+ pub disable_ipv6: bool,
+ #[serde(default, skip_serializing_if = "skip_if_false")]
+ pub disable_relay: bool,
+ #[serde(default, skip_serializing_if = "skip_if_false")]
+ pub enable_remote_access: bool,
+ /// A list of peer addresses to try and manually connect to, instead of relying on discovery.
+ ///
+ /// All of these are valid values:
+ /// - `localhost`
+ /// - `otbeaumont.me` or `otbeaumont.me:3000`
+ /// - `127.0.0.1` or `127.0.0.1:300`
+ /// - `[::1]` or `[::1]:3000`
+ /// which is why we use `String` not `SocketAddr`
+ #[serde(default)]
+ pub manual_peers: HashSet,
}
impl Default for NodeConfigP2P {
@@ -88,9 +101,11 @@ impl Default for NodeConfigP2P {
Self {
discovery: P2PDiscoveryState::Everyone,
port: Port::Random,
- ipv4: true,
- ipv6: true,
- remote_access: false,
+ disabled: true,
+ disable_ipv6: true,
+ disable_relay: true,
+ enable_remote_access: false,
+ manual_peers: Default::default(),
}
}
}
diff --git a/core/src/p2p/events.rs b/core/src/p2p/events.rs
index 179a3fdab..7523e64cb 100644
--- a/core/src/p2p/events.rs
+++ b/core/src/p2p/events.rs
@@ -1,6 +1,8 @@
-use std::sync::Arc;
+use std::{collections::HashSet, net::SocketAddr, sync::Arc};
-use sd_p2p::{flume::bounded, HookEvent, HookId, PeerConnectionCandidate, RemoteIdentity, P2P};
+use sd_p2p::{
+ flume::bounded, hooks::QuicHandle, HookEvent, PeerConnectionCandidate, RemoteIdentity, P2P,
+};
use serde::Serialize;
use specta::Type;
use tokio::sync::broadcast;
@@ -28,6 +30,8 @@ pub enum DiscoveryMethod {
Relay,
// Found via mDNS or a manual IP
Local,
+ // Found via manual entry on either node
+ Manual,
}
// This is used for synchronizing events between the backend and the frontend.
@@ -40,6 +44,7 @@ pub enum P2PEvent {
connection: ConnectionMethod,
discovery: DiscoveryMethod,
metadata: PeerMetadata,
+ addrs: HashSet,
},
// Delete a peer
PeerDelete {
@@ -69,7 +74,7 @@ pub struct P2PEvents {
}
impl P2PEvents {
- pub fn spawn(p2p: Arc, libraries_hook_id: HookId) -> Self {
+ pub fn spawn(p2p: Arc, quic: Arc) -> Self {
let events = broadcast::channel(15);
let (tx, rx) = bounded(15);
let _ = p2p.register_hook("sd-frontend-events", tx);
@@ -77,62 +82,66 @@ impl P2PEvents {
let events_tx = events.0.clone();
tokio::spawn(async move {
while let Ok(event) = rx.recv_async().await {
- let event = match event {
- // We use `HookEvent::PeerUnavailable`/`HookEvent::PeerAvailable` over `HookEvent::PeerExpiredBy`/`HookEvent::PeerDiscoveredBy` so that having an active connection is treated as "discovered".
- // It's possible to have an active connection without mDNS data (which is what Peer*By` are for)
- HookEvent::PeerConnectedWith(_, peer)
- | HookEvent::PeerAvailable(peer)
- // This will fire for updates to the mDNS metadata which are important for UX.
- | HookEvent::PeerDiscoveredBy(_, peer) => {
- let metadata = match PeerMetadata::from_hashmap(&peer.metadata()) {
- Ok(metadata) => metadata,
- Err(e) => {
- println!(
- "Invalid metadata for peer '{}': {:?}",
- peer.identity(),
- e
- );
+ let peer = match event {
+ HookEvent::PeerDisconnectedWith(_, identity)
+ | HookEvent::PeerExpiredBy(_, identity) => {
+ let peers = p2p.peers();
+ let Some(peer) = peers.get(&identity) else {
+ let _ = events_tx.send(P2PEvent::PeerDelete { identity });
continue;
- }
- };
+ };
- P2PEvent::PeerChange {
- identity: peer.identity(),
- connection: if peer.is_connected_with_hook(libraries_hook_id) {
- ConnectionMethod::Relay
- } else if peer.is_connected() {
- ConnectionMethod::Local
- } else {
- ConnectionMethod::Disconnected
- },
- discovery: match peer
- .connection_candidates()
- .contains(&PeerConnectionCandidate::Relay)
- {
- true => DiscoveryMethod::Relay,
- false => DiscoveryMethod::Local,
- },
- metadata,
- }
- }
- HookEvent::PeerUnavailable(identity) => P2PEvent::PeerDelete { identity },
- HookEvent::PeerDisconnectedWith(_, identity) => {
- let peers = p2p.peers();
- let Some(peer) = peers.get(&identity) else {
+ peer.clone()
+ },
+ // We use `HookEvent::PeerUnavailable`/`HookEvent::PeerAvailable` over `HookEvent::PeerExpiredBy`/`HookEvent::PeerDiscoveredBy` so that having an active connection is treated as "discovered".
+ // It's possible to have an active connection without mDNS data (which is what Peer*By` are for)
+ HookEvent::PeerConnectedWith(_, peer)
+ | HookEvent::PeerAvailable(peer)
+ // This will fire for updates to the mDNS metadata which are important for UX.
+ | HookEvent::PeerDiscoveredBy(_, peer) => peer,
+ HookEvent::PeerUnavailable(identity) => {
+ let _ = events_tx.send(P2PEvent::PeerDelete { identity });
continue;
- };
+ },
+ HookEvent::Shutdown { _guard } => break,
+ _ => continue,
+ };
- if !peer.is_connected() {
- P2PEvent::PeerDelete { identity }
- } else {
- continue;
- }
- }
- HookEvent::Shutdown { _guard } => break,
- _ => continue,
+ let Ok(metadata) = PeerMetadata::from_hashmap(&peer.metadata()).map_err(|err| {
+ println!("Invalid metadata for peer '{}': {err:?}", peer.identity())
+ }) else {
+ continue;
};
- let _ = events_tx.send(event);
+ let _ = events_tx.send(P2PEvent::PeerChange {
+ identity: peer.identity(),
+ connection: if peer.is_connected() {
+ if quic.is_relayed(peer.identity()) {
+ ConnectionMethod::Relay
+ } else {
+ ConnectionMethod::Local
+ }
+ } else {
+ ConnectionMethod::Disconnected
+ },
+ discovery: if peer
+ .connection_candidates()
+ .iter()
+ .any(|c| matches!(c, PeerConnectionCandidate::Manual(_)))
+ {
+ DiscoveryMethod::Manual
+ } else if peer
+ .connection_candidates()
+ .iter()
+ .all(|c| *c == PeerConnectionCandidate::Relay)
+ {
+ DiscoveryMethod::Relay
+ } else {
+ DiscoveryMethod::Local
+ },
+ metadata,
+ addrs: peer.addrs(),
+ });
}
});
diff --git a/core/src/p2p/libraries.rs b/core/src/p2p/libraries.rs
index 065b935d5..f594d30e5 100644
--- a/core/src/p2p/libraries.rs
+++ b/core/src/p2p/libraries.rs
@@ -1,6 +1,9 @@
-use std::{collections::HashMap, sync::Arc};
+use std::{
+ collections::HashMap,
+ sync::{Arc, Mutex, PoisonError},
+};
-use sd_p2p::{flume::bounded, HookEvent, HookId, PeerConnectionCandidate, RemoteIdentity, P2P};
+use sd_p2p::{hooks::QuicHandle, RemoteIdentity, P2P};
use tracing::error;
use crate::library::{Libraries, LibraryManagerEvent};
@@ -10,91 +13,127 @@ use crate::library::{Libraries, LibraryManagerEvent};
/// This hooks is responsible for:
/// - injecting library peers into the P2P system so we can connect to them over internet.
///
-pub fn libraries_hook(p2p: Arc, libraries: Arc) -> HookId {
- let (tx, rx) = bounded(15);
- let hook_id = p2p.register_hook("sd-libraries-hook", tx);
+pub fn libraries_hook(p2p: Arc, quic: Arc, libraries: Arc) {
+ let nodes_to_instance = Arc::new(Mutex::new(HashMap::new()));
- let handle = tokio::spawn(async move {
- if let Err(err) = libraries
- .rx
- .clone()
- .subscribe(|msg| {
- let p2p = p2p.clone();
- async move {
- match msg {
- LibraryManagerEvent::InstancesModified(library)
- | LibraryManagerEvent::Load(library) => {
- p2p.metadata_mut().insert(
- library.id.to_string(),
- library.identity.to_remote_identity().to_string(),
- );
+ let handle = tokio::spawn({
+ let quic = quic.clone();
- let Ok(instances) =
- library.db.instance().find_many(vec![]).exec().await
- else {
- return;
- };
+ async move {
+ if let Err(err) = libraries
+ .rx
+ .clone()
+ .subscribe(|msg| {
+ let p2p = p2p.clone();
+ let nodes_to_instance = nodes_to_instance.clone();
+ let quic = quic.clone();
- for i in instances.iter() {
- let identity = RemoteIdentity::from_bytes(&i.remote_identity)
- .expect("lol: invalid DB entry");
-
- // Skip self
- if identity == library.identity.to_remote_identity() {
- continue;
- }
-
- p2p.clone().discover_peer(
- hook_id,
- identity,
- HashMap::new(), // TODO: We should probs cache this so we have something
- [PeerConnectionCandidate::Relay].into_iter().collect(),
+ async move {
+ match msg {
+ LibraryManagerEvent::InstancesModified(library)
+ | LibraryManagerEvent::Load(library) => {
+ p2p.metadata_mut().insert(
+ library.id.to_string(),
+ library.identity.to_remote_identity().to_string(),
);
- }
- }
- LibraryManagerEvent::Edit(_library) => {
- // TODO: Send changes to all connected nodes or queue sending for when they are online!
- }
- LibraryManagerEvent::Delete(library) => {
- p2p.metadata_mut().remove(&library.id.to_string());
- let Ok(instances) =
- library.db.instance().find_many(vec![]).exec().await
- else {
- return;
- };
-
- for i in instances.iter() {
- let identity = RemoteIdentity::from_bytes(&i.remote_identity)
- .expect("lol: invalid DB entry");
-
- let peers = p2p.peers();
- let Some(peer) = peers.get(&identity) else {
- continue;
+ let Ok(instances) =
+ library.db.instance().find_many(vec![]).exec().await
+ else {
+ return;
};
- peer.undiscover_peer(hook_id);
+
+ let mut nodes_to_instance = nodes_to_instance
+ .lock()
+ .unwrap_or_else(PoisonError::into_inner);
+
+ for i in instances.iter() {
+ let identity = RemoteIdentity::from_bytes(&i.remote_identity)
+ .expect("invalid instance identity");
+ let node_identity = RemoteIdentity::from_bytes(
+ i.node_remote_identity
+ .as_ref()
+ .expect("node remote identity is required"),
+ )
+ .expect("invalid node remote identity");
+
+ // Skip self
+ if i.identity.is_some() {
+ continue;
+ }
+
+ nodes_to_instance
+ .entry(identity)
+ .or_insert(vec![])
+ .push(node_identity);
+
+ quic.track_peer(
+ node_identity,
+ serde_json::from_slice(
+ i.metadata.as_ref().expect("this is a required field"),
+ )
+ .expect("invalid metadata"),
+ );
+ }
+ }
+ LibraryManagerEvent::Edit(_library) => {
+ // TODO: Send changes to all connected nodes or queue sending for when they are online!
+ }
+ LibraryManagerEvent::Delete(library) => {
+ p2p.metadata_mut().remove(&library.id.to_string());
+
+ let Ok(instances) =
+ library.db.instance().find_many(vec![]).exec().await
+ else {
+ return;
+ };
+
+ let mut nodes_to_instance = nodes_to_instance
+ .lock()
+ .unwrap_or_else(PoisonError::into_inner);
+
+ for i in instances.iter() {
+ let identity = RemoteIdentity::from_bytes(&i.remote_identity)
+ .expect("invalid remote identity");
+ let node_identity = RemoteIdentity::from_bytes(
+ i.node_remote_identity
+ .as_ref()
+ .expect("node remote identity is required"),
+ )
+ .expect("invalid node remote identity");
+
+ // Skip self
+ if i.identity.is_some() {
+ continue;
+ }
+
+ // Only remove if all instances pointing to this node are removed
+ let Some(identities) = nodes_to_instance.get_mut(&identity)
+ else {
+ continue;
+ };
+ if let Some(i) =
+ identities.iter().position(|i| i == &node_identity)
+ {
+ identities.remove(i);
+ }
+ if identities.is_empty() {
+ quic.untrack_peer(node_identity);
+ }
+ }
}
}
}
- }
- })
- .await
- {
- error!("Core may become unstable! `LibraryServices::start` manager aborted with error: {err:?}");
- }
- });
-
- tokio::spawn(async move {
- while let Ok(event) = rx.recv_async().await {
- match event {
- HookEvent::Shutdown { _guard } => {
- handle.abort();
- break;
- }
- _ => continue,
+ })
+ .await
+ {
+ error!("Core may become unstable! `LibraryServices::start` manager aborted with error: {err:?}");
}
}
});
- hook_id
+ tokio::spawn(async move {
+ quic.shutdown().await;
+ handle.abort();
+ });
}
diff --git a/core/src/p2p/manager.rs b/core/src/p2p/manager.rs
index 3ce86a034..b14e2ea98 100644
--- a/core/src/p2p/manager.rs
+++ b/core/src/p2p/manager.rs
@@ -14,11 +14,13 @@ use axum::routing::IntoMakeService;
use sd_p2p::{
flume::{bounded, Receiver},
- HookId, Libp2pPeerId, Mdns, Peer, QuicTransport, RelayServerEntry, RemoteIdentity,
- UnicastStream, P2P,
+ hooks::{Libp2pPeerId, Mdns, QuicHandle, QuicTransport, RelayServerEntry},
+ Peer, RemoteIdentity, UnicastStream, P2P,
};
use sd_p2p_tunnel::Tunnel;
+use serde::Serialize;
use serde_json::json;
+use specta::Type;
use std::{
collections::HashMap,
convert::Infallible,
@@ -28,30 +30,44 @@ use std::{
use tower_service::Service;
use tracing::error;
-use tokio::sync::oneshot;
+use tokio::sync::{oneshot, Notify};
use tracing::info;
use uuid::Uuid;
use super::{P2PEvents, PeerMetadata};
-#[derive(Default)]
-pub struct ListenerErrors {
- pub ipv4: Option,
- pub ipv6: Option,
+#[derive(Default, Clone, Serialize, Type)]
+#[serde(tag = "type")]
+pub enum ListenerState {
+ Listening,
+ Error {
+ error: String,
+ },
+ #[default]
+ NotListening,
+}
+
+#[derive(Default, Clone, Serialize, Type)]
+pub struct Listeners {
+ ipv4: ListenerState,
+ ipv6: ListenerState,
+ relay: ListenerState,
}
pub struct P2PManager {
pub(crate) p2p: Arc,
mdns: Mutex
+ }
+ >
+ form.setValue('ipv6_disabled', !checked)}
+ />
+
+
+
+ {t('p2p_visibility_description')}
+
+ }
+ >
+
+
+
+
+
+ {t('enable_relay_description')}
+
+ >
+ }
+ >
+ form.setValue('relay_disabled', !checked)}
+ />
+
+
+ {isP2PWipFeatureEnabled && (
+ <>
+
+
+ {t('remote_access_description')}
+
+
+ WARNING: This protocol has no security at the moment and
+ effectively gives root access!
+
+ >
+ }
+ >
+
+ form.setValue('enable_remote_access', checked)
+ }
+ />
+
+ >
+ )}
+
+
+ {t('manual_peers_description')
+ .split('\n')
+ .map((line, index) => (
+
+ {line}
+
+ ))}
+ >
+ }
+ >
+
+
+ {form.watch('p2p_manual_peers')?.map((socket) => (
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ setNewSocket(e.currentTarget.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && !isNewSocketInvalid) {
+ form.setValue('p2p_manual_peers', [
+ ...(form.getValues('p2p_manual_peers') || []),
+ newSocket
+ ]);
+ setNewSocket('');
+ }
+ }}
+ />
+
+
+
+
+
+ >
+ ) : null}
+
+ );
+};
+
+function NodesPanel() {
+ const { t } = useLocale();
+ const navigate = useNavigate();
+ const peers = usePeers();
+ const platform = usePlatform();
+
+ const isP2PWipFeatureEnabled = useFeatureFlag('wipP2P');
+
+ const debugConnect = useBridgeMutation(['p2p.debugConnect'], {
+ onSuccess: () => toast.success('Connected!'),
+ onError: (e) => toast.error(`Error connecting '${e.message}'`)
+ });
+
+ return (
+
+
{t('nodes')}
+
+ {peers.size === 0 ? (
+
{t('no_nodes_found')}
+ ) : (
+
+ {[...peers.entries()].map(([id, peer]) => (
+
+
+
+
+
+ {peer.metadata.name}
+
+
+
+ Spacedrive {peer.metadata.version}{' '}
+ {peer.metadata.operating_system
+ ? `- ${peer.metadata.operating_system}`
+ : ''}
+
+
+
+
+
+ {isP2PWipFeatureEnabled && (
+
+ )}
+
+
+
+
+ {peer.discovery === 'Manual'
+ ? 'Manual'
+ : peer.discovery === 'Local'
+ ? 'LAN'
+ : 'Relay'}
+
+
+
+ {peer.connection}
+
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/interface/app/p2p/index.tsx b/interface/app/p2p/index.tsx
index 650141a5a..938935628 100644
--- a/interface/app/p2p/index.tsx
+++ b/interface/app/p2p/index.tsx
@@ -1,39 +1,60 @@
-import { useEffect, useRef, useState } from 'react';
+import { useEffect, useRef } from 'react';
import { useBridgeQuery } from '@sd/client';
import { toast } from '@sd/ui';
import { useLocale } from '~/hooks';
+const errorMessages = {
+ ipv4_ipv6: 'ipv4_ipv6_listeners_error',
+ ipv4: 'ipv4_listeners_error',
+ ipv6: 'ipv6_listeners_error',
+ relay: 'relay_listeners_error'
+};
+
export function useP2PErrorToast() {
const listeners = useBridgeQuery(['p2p.listeners']);
const didShowError = useRef(false);
const { t } = useLocale();
useEffect(() => {
- if (!listeners.data) return;
- if (didShowError.current) return;
+ if (!listeners.data || didShowError.current) return;
+
+ const getErrorBody = (type: keyof typeof errorMessages, error: string) => (
+
+
{t(errorMessages[type])}
+
{error}
+
+ );
let body: JSX.Element | undefined;
- if (listeners.data.ipv4.type === 'Error' && listeners.data.ipv6.type === 'Error') {
- body = (
-
-
{t('ipv4_ipv6_listeners_error')}
-
{listeners.data.ipv4.error}
-
- );
- } else if (listeners.data.ipv4.type === 'Error') {
- body = (
-
-
{t('ipv4_listeners_error')}
-
{listeners.data.ipv4.error}
-
- );
- } else if (listeners.data.ipv6.type === 'Error') {
- body = (
-
-
{t('ipv6_listeners_error')}
-
{listeners.data.ipv6.error}
-
+
+ switch (true) {
+ case listeners.data.ipv4.type === 'Error' && listeners.data.ipv6.type === 'Error':
+ body = getErrorBody('ipv4_ipv6', listeners.data.ipv4.error);
+ break;
+ case listeners.data.ipv4.type === 'Error':
+ body = getErrorBody('ipv4', listeners.data.ipv4.error);
+ break;
+ case listeners.data.ipv6.type === 'Error':
+ body = getErrorBody('ipv6', listeners.data.ipv6.error);
+ break;
+ case listeners.data.relay.type === 'Error':
+ body = getErrorBody('relay', listeners.data.relay.error);
+ break;
+ default:
+ break;
+ }
+
+ if (body) {
+ toast.error(
+ {
+ title: t('networking_error'),
+ body
+ },
+ {
+ id: 'p2p-listener-error'
+ }
);
+ didShowError.current = true;
}
if (body) {
@@ -49,7 +70,7 @@ export function useP2PErrorToast() {
didShowError.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [listeners.data]);
+ }, [listeners.data, t]);
return null;
}
diff --git a/interface/locales/en/common.json b/interface/locales/en/common.json
index 8cd4b92dc..c7a9ed308 100644
--- a/interface/locales/en/common.json
+++ b/interface/locales/en/common.json
@@ -123,6 +123,7 @@
"current_directory": "Current Directory",
"current_directory_with_descendants": "Current Directory With Descendants",
"custom": "Custom",
+ "docker": "Docker",
"cut": "Cut",
"cut_object": "Cut object",
"cut_success": "Items cut",
@@ -192,6 +193,8 @@
"enable_networking": "Enable Networking",
"enable_networking_description": "Allow your node to communicate with other Spacedrive nodes around you.",
"enable_networking_description_required": "Required for library sync or Spacedrop!",
+ "enable_relay": "Enable Relay",
+ "enable_relay_description": "Enable the relay server to allow your devices to communicate over the public internet.",
"enable_sync": "Enable Sync",
"enable_sync_description": "Generate sync operations for all the existing data in this library, and configure Spacedrive to generate sync operations when things happen in future.",
"enabled": "Enabled",
@@ -281,6 +284,10 @@
"general": "General",
"general_settings": "General Settings",
"general_settings_description": "General settings related to this client.",
+ "network_settings": "Network Settings",
+ "network_settings_description": "Settings related to networking and connectivity.",
+ "network_settings_advanced": "Advanced Network Overview",
+ "network_settings_advanced_description": "Advanced information about your current network setup.",
"general_shortcut_description": "General usage shortcuts",
"generatePreviewMedia_label": "Generate preview media for this Location",
"generate_checksums": "Generate Checksums",
@@ -335,6 +342,7 @@
"ipv6": "IPv6 networking",
"ipv6_description": "Allow peer-to-peer communication using IPv6 networking",
"ipv6_listeners_error": "Error creating the IPv6 listeners. Please check your firewall settings!",
+ "relay_listeners_error": "Error creating the relay listener. Please check your firewall settings!",
"is": "is",
"is_not": "is not",
"item": "item",
@@ -628,6 +636,8 @@
"spacedrop_disabled": "Disabled",
"spacedrop_everyone": "Everyone",
"spacedrop_rejected": "Spacedrop rejected",
+ "manual_peers": "Manually add peers",
+ "manual_peers_description": "Add peers manually by entering their IP address and port.\nThis is useful when automatic discovery is not possible.",
"square_thumbnails": "Square Thumbnails",
"star_on_github": "Star on GitHub",
"start": "Start",
diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts
index 5c9c249a7..645e55ff9 100644
--- a/packages/client/src/core.ts
+++ b/packages/client/src/core.ts
@@ -168,11 +168,11 @@ export type CRDTOperationData = { c: { [key in string]: JsonValue } } | { u: { f
export type CameraData = { device_make: string | null; device_model: string | null; color_space: string | null; color_profile: ColorProfile | null; focal_length: number | null; shutter_speed: number | null; flash: Flash | null; orientation: Orientation; lens_make: string | null; lens_model: string | null; bit_depth: number | null; zoom: number | null; iso: number | null; software: string | null; serial_number: string | null; lens_serial_number: string | null; contrast: number | null; saturation: number | null; sharpness: number | null; composite: Composite | null }
-export type ChangeNodeNameArgs = { name: string | null; p2p_port: Port | null; p2p_ipv4_enabled: boolean | null; p2p_ipv6_enabled: boolean | null; p2p_discovery: P2PDiscoveryState | null; p2p_remote_access: boolean | null; image_labeler_version: string | null }
+export type ChangeNodeNameArgs = { name: string | null; p2p_port: Port | null; p2p_disabled: boolean | null; p2p_ipv6_disabled: boolean | null; p2p_relay_disabled: boolean | null; p2p_discovery: P2PDiscoveryState | null; p2p_remote_access: boolean | null; p2p_manual_peers: string[] | null; image_labeler_version: string | null }
export type Chapter = { id: number; start: [number, number]; end: [number, number]; time_base_den: number; time_base_num: number; metadata: Metadata }
-export type CloudInstance = { id: string; uuid: string; identity: RemoteIdentity; nodeId: string; metadata: { [key in string]: string } }
+export type CloudInstance = { id: string; uuid: string; identity: RemoteIdentity; nodeId: string; nodeRemoteIdentity: string; metadata: { [key in string]: string } }
export type CloudLibrary = { id: string; uuid: string; name: string; instances: CloudInstance[]; ownerId: string }
@@ -228,7 +228,7 @@ export type DefaultLocations = { desktop: boolean; documents: boolean; downloads
* The method used for the discovery of this peer.
* *Technically* you can have multiple under the hood but this simplifies things for the UX.
*/
-export type DiscoveryMethod = "Relay" | "Local"
+export type DiscoveryMethod = "Relay" | "Local" | "Manual"
export type DiskType = "SSD" | "HDD" | "Removable"
@@ -421,7 +421,7 @@ instance_id: number;
*/
cloud_id?: string | null; generate_sync_operations?: boolean; version: LibraryConfigVersion }
-export type LibraryConfigVersion = "V0" | "V1" | "V2" | "V3" | "V4" | "V5" | "V6" | "V7" | "V8" | "V9" | "V10"
+export type LibraryConfigVersion = "V0" | "V1" | "V2" | "V3" | "V4" | "V5" | "V6" | "V7" | "V8" | "V9" | "V10" | "V11"
export type LibraryConfigWrapped = { uuid: string; instance_id: string; instance_public_key: RemoteIdentity; config: LibraryConfig }
@@ -431,9 +431,9 @@ export type LibraryPreferences = { location?: { [key in string]: LocationSetting
export type LightScanArgs = { location_id: number; sub_path: string }
-export type ListenerState = { type: "Listening" } | { type: "Error"; error: string } | { type: "Disabled" }
+export type ListenerState = { type: "Listening" } | { type: "Error"; error: string } | { type: "NotListening" }
-export type Listeners = { ipv4: ListenerState; ipv6: ListenerState }
+export type Listeners = { ipv4: ListenerState; ipv6: ListenerState; relay: ListenerState }
export type Location = { id: number; pub_id: number[]; name: string | null; path: string | null; total_capacity: number | null; available_capacity: number | null; size_in_bytes: number[] | null; is_archived: boolean | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; date_created: string | null; scan_state: number; instance_id: number | null }
@@ -472,7 +472,18 @@ export type MediaLocation = { latitude: number; longitude: number; pluscode: Plu
export type Metadata = { album: string | null; album_artist: string | null; artist: string | null; comment: string | null; composer: string | null; copyright: string | null; creation_time: string | null; date: string | null; disc: number | null; encoder: string | null; encoded_by: string | null; filename: string | null; genre: string | null; language: string | null; performer: string | null; publisher: string | null; service_name: string | null; service_provider: string | null; title: string | null; track: number | null; variant_bit_rate: number | null; custom: { [key in string]: string } }
-export type NodeConfigP2P = { discovery?: P2PDiscoveryState; port: Port; ipv4: boolean; ipv6: boolean; remote_access: boolean }
+export type NodeConfigP2P = { discovery?: P2PDiscoveryState; port: Port; disabled: boolean; disable_ipv6: boolean; disable_relay: boolean; enable_remote_access: boolean;
+/**
+ * A list of peer addresses to try and manually connect to, instead of relying on discovery.
+ *
+ * All of these are valid values:
+ * - `localhost`
+ * - `otbeaumont.me` or `otbeaumont.me:3000`
+ * - `127.0.0.1` or `127.0.0.1:300`
+ * - `[::1]` or `[::1]:3000`
+ * which is why we use `String` not `SocketAddr`
+ */
+manual_peers?: string[] }
export type NodePreferences = { thumbnailer: ThumbnailerPreferences }
@@ -484,7 +495,7 @@ id: string;
/**
* name is the display name of the current node. This is set by the user and is shown in the UI. // TODO: Length validation so it can fit in DNS record
*/
-name: string; identity: RemoteIdentity; p2p: NodeConfigP2P; features: BackendFeature[]; preferences: NodePreferences; image_labeler_version: string | null }) & { data_path: string; device_model: string | null }
+name: string; identity: RemoteIdentity; p2p: NodeConfigP2P; features: BackendFeature[]; preferences: NodePreferences; image_labeler_version: string | null }) & { data_path: string; device_model: string | null; is_in_docker: boolean }
export type NonIndexedPathItem = { path: string; name: string; extension: string; kind: number; is_dir: boolean; date_created: string; date_modified: string; size_in_bytes_bytes: number[]; hidden: boolean }
@@ -541,7 +552,7 @@ export type Orientation = "Normal" | "CW90" | "CW180" | "CW270" | "MirroredVerti
export type P2PDiscoveryState = "Everyone" | "ContactsOnly" | "Disabled"
-export type P2PEvent = { type: "PeerChange"; identity: RemoteIdentity; connection: ConnectionMethod; discovery: DiscoveryMethod; metadata: PeerMetadata } | { type: "PeerDelete"; identity: RemoteIdentity } | { type: "SpacedropRequest"; id: string; identity: RemoteIdentity; peer_name: string; files: string[] } | { type: "SpacedropProgress"; id: string; percent: number } | { type: "SpacedropTimedOut"; id: string } | { type: "SpacedropRejected"; id: string }
+export type P2PEvent = { type: "PeerChange"; identity: RemoteIdentity; connection: ConnectionMethod; discovery: DiscoveryMethod; metadata: PeerMetadata; addrs: string[] } | { type: "PeerDelete"; identity: RemoteIdentity } | { type: "SpacedropRequest"; id: string; identity: RemoteIdentity; peer_name: string; files: string[] } | { type: "SpacedropProgress"; id: string; percent: number } | { type: "SpacedropTimedOut"; id: string } | { type: "SpacedropRejected"; id: string }
export type PeerMetadata = { name: string; operating_system: OperatingSystem | null; device_model: HardwareModel | null; version: string | null }
diff --git a/packages/client/src/hooks/useP2PEvents.tsx b/packages/client/src/hooks/useP2PEvents.tsx
index 299823282..109d1913b 100644
--- a/packages/client/src/hooks/useP2PEvents.tsx
+++ b/packages/client/src/hooks/useP2PEvents.tsx
@@ -15,6 +15,7 @@ type Peer = {
connection: ConnectionMethod;
discovery: DiscoveryMethod;
metadata: PeerMetadata;
+ addrs: string[];
};
type Context = {
@@ -38,7 +39,8 @@ export function P2PContextProvider({ children }: PropsWithChildren) {
peers.set(data.identity, {
connection: data.connection,
discovery: data.discovery,
- metadata: data.metadata
+ metadata: data.metadata,
+ addrs: data.addrs
});
setPeers([peers]);
} else if (data.type === 'PeerDelete') {