From b43bb40d1a2c721d8ec3ee7ce5e599e87abfaeee Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Fri, 14 Nov 2025 18:47:46 +0800 Subject: [PATCH 01/42] Completed partial framework construction --- .gitignore | 7 +- Cargo.toml | 23 + src/gossip/message.rs | 205 ++++++++ src/gossip/mod.rs | 1 + src/identity/keypair.rs | 121 +++++ src/identity/mod.rs | 1 + src/lib.rs | 7 + src/main.rs | 142 ++++++ src/node/mod.rs | 3 + src/node/node.rs | 259 ++++++++++ src/node/node_id.rs | 192 ++++++++ src/node/node_manager.rs | 127 +++++ src/repo/mod.rs | 3 + src/repo/repo.rs | 113 +++++ src/repo/repo_id.rs | 156 ++++++ src/repo/repo_manager.rs | 107 ++++ src/storage/mod.rs | 74 +++ src/transport/cert.rs | 177 +++++++ src/transport/config.rs | 134 +++++ src/transport/mod.rs | 3 + src/transport/quic.rs | 429 ++++++++++++++++ src/util/mod.rs | 8 + test_output.txt | Bin 0 -> 14670 bytes ...21\347\273\234\350\256\276\350\256\241.md" | 462 ++++++++++++++++++ 24 files changed, 2753 insertions(+), 1 deletion(-) create mode 100644 Cargo.toml create mode 100644 src/gossip/message.rs create mode 100644 src/gossip/mod.rs create mode 100644 src/identity/keypair.rs create mode 100644 src/identity/mod.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/node/mod.rs create mode 100644 src/node/node.rs create mode 100644 src/node/node_id.rs create mode 100644 src/node/node_manager.rs create mode 100644 src/repo/mod.rs create mode 100644 src/repo/repo.rs create mode 100644 src/repo/repo_id.rs create mode 100644 src/repo/repo_manager.rs create mode 100644 src/storage/mod.rs create mode 100644 src/transport/cert.rs create mode 100644 src/transport/config.rs create mode 100644 src/transport/mod.rs create mode 100644 src/transport/quic.rs create mode 100644 src/util/mod.rs create mode 100644 test_output.txt create mode 100644 "\345\210\206\345\270\203\345\274\217p2p\347\275\221\347\273\234\350\256\276\350\256\241.md" diff --git a/.gitignore b/.gitignore index ad67955..9e3788d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,9 @@ target # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ + +.vscode/ + +cert/ +.megaengine/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e1b15bd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "megaengine" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.100" +ed25519-dalek = { version = "2.1.1", features = ["rand_core","serde"] } +rand_core = { version = "0.6.4", features = ["getrandom"] } +multibase = "0.9.2" +multihash = "0.9.2" +sha2 = "0.10" +hex = "0.4" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0" +quinn = "0.11" +tokio = { version = "1", features = ["full"] } +rustls = "0.23.34" +rustls-pemfile = "2.2" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +clap = { version = "4.3", features = ["derive"] } +rcgen = "0.13" diff --git a/src/gossip/message.rs b/src/gossip/message.rs new file mode 100644 index 0000000..31eaaa6 --- /dev/null +++ b/src/gossip/message.rs @@ -0,0 +1,205 @@ +use anyhow::{anyhow, Result}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::{net::SocketAddr, time}; + +use crate::{ + node::{ + node::{Node, NodeType}, + node_id::NodeId, + }, + repo::repo_id::RepoId, + util::timestamp_now, +}; + +/// Gossip 消息类型 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum GossipMessage { + /// 节点公告 + NodeAnnouncement(NodeAnnouncement), + /// 仓库公告 (库存公告) + RepoAnnouncement(RepoAnnouncement), +} + +/// 节点公告 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeAnnouncement { + pub node_id: NodeId, + pub version: u8, + pub alias: String, + pub node_type: NodeType, + pub addresses: Vec, +} + +impl From for NodeAnnouncement { + fn from(node: Node) -> Self { + Self { + node_id: node.node_id().clone(), + version: node.version(), + alias: node.alias().to_string(), + node_type: node.node_type(), + addresses: node.addresses().to_vec(), + } + } +} + +/// 仓库公告- 表示某个节点拥有的仓库列表 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RepoAnnouncement { + pub node_id: NodeId, + pub repos: Vec, +} + +/// 带签名的消息包装 +#[allow(dead_code)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignedMessage { + pub node_id: NodeId, + pub message: GossipMessage, + pub timestamp: u64, + pub signature: String, +} + +#[allow(dead_code)] +impl SignedMessage { + pub fn new_node_sign_message(node: Node) -> Result { + let message = GossipMessage::NodeAnnouncement(node.clone().into()); + + let mut sign_message = SignedMessage { + node_id: node.node_id().clone(), + message, + timestamp: timestamp_now(), + signature: "".to_string(), + }; + let self_hash = sign_message.self_hash(); + let sign = node.sign_message(self_hash.as_slice())?; + sign_message.signature = hex::encode(sign); + Ok(sign_message) + } + + pub fn new_repo_sign_message(repos: Vec, node: Node) -> Result { + let message = GossipMessage::RepoAnnouncement(RepoAnnouncement { + node_id: node.node_id().clone(), + repos: repos, + }); + + let mut sign_message = SignedMessage { + node_id: node.node_id().clone(), + message, + timestamp: timestamp_now(), + signature: "".to_string(), + }; + let self_hash = sign_message.self_hash(); + let sign = node.sign_message(self_hash.as_slice())?; + sign_message.signature = hex::encode(sign); + Ok(sign_message) + } + + pub fn self_hash(&self) -> Vec { + let mut hasher = Sha256::new(); + let message_bytes = serde_json::to_vec(&self.message).unwrap_or_default(); + + hasher.update(self.node_id.0.as_bytes()); + hasher.update(&message_bytes); + hasher.finalize().to_vec() + } + + /// 获取消息的时间戳 + pub fn timestamp(&self) -> u64 { + self.timestamp + } + + /// 获取消息类型 + pub fn message_type(&self) -> &'static str { + self.message.message_type() + } +} + +impl GossipMessage { + /// 获取消息类型 + pub fn message_type(&self) -> &'static str { + match self { + GossipMessage::NodeAnnouncement(_) => "node_announcement", + GossipMessage::RepoAnnouncement(_) => "inventory_announcement", + } + } + + /// 获取发送者 NodeId + pub fn sender(&self) -> &NodeId { + match self { + GossipMessage::NodeAnnouncement(na) => &na.node_id, + GossipMessage::RepoAnnouncement(ra) => &ra.node_id, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::identity::keypair::KeyPair; + use crate::node::node::Node; + + fn make_node() -> Node { + let keypair = KeyPair::generate().expect("generate keypair"); + let addresses = vec!["127.0.0.1:8080".parse().unwrap()]; + Node::from_keypair( + &keypair, + "test-node", + addresses, + crate::node::node::NodeType::Normal, + ) + } + + #[test] + fn test_new_node_sign_message() { + let node = make_node(); + let signed = SignedMessage::new_node_sign_message(node.clone()).expect("sign node message"); + + assert_eq!(signed.message_type(), "node_announcement"); + assert!(signed.timestamp() > 0); + + // signature should be a hex string that decodes to 64 bytes (ed25519) + let sig = hex::decode(&signed.signature).expect("decode hex"); + assert_eq!(sig.len(), 64); + + // self_hash is 32 bytes + let h = signed.self_hash(); + assert_eq!(h.len(), 32); + } + + #[test] + fn test_new_repo_sign_message() { + let keypair = KeyPair::generate().expect("generate keypair"); + let node = Node::from_keypair( + &keypair, + "repo-node", + vec!["127.0.0.1:9090".parse().unwrap()], + crate::node::node::NodeType::Relay, + ); + + // generate a repo id + let repo_id = crate::repo::repo_id::RepoId::generate( + b"root_commit", + node_keypair_bytes(&keypair).as_slice(), + ) + .expect("generate repo id"); + + let signed = SignedMessage::new_repo_sign_message(vec![repo_id.clone()], node.clone()) + .expect("sign repo message"); + + assert_eq!(signed.message_type(), "inventory_announcement"); + let sig = hex::decode(&signed.signature).expect("decode hex"); + assert_eq!(sig.len(), 64); + + // ensure the embedded repo id is present in message + if let GossipMessage::RepoAnnouncement(ra) = signed.message { + assert!(ra.repos.iter().any(|r| r.as_str() == repo_id.as_str())); + } else { + panic!("expected RepoAnnouncement"); + } + } + + fn node_keypair_bytes(kp: &KeyPair) -> Vec { + kp.verifying_key.as_bytes().to_vec() + } +} diff --git a/src/gossip/mod.rs b/src/gossip/mod.rs new file mode 100644 index 0000000..e935b02 --- /dev/null +++ b/src/gossip/mod.rs @@ -0,0 +1 @@ +mod message; diff --git a/src/identity/keypair.rs b/src/identity/keypair.rs new file mode 100644 index 0000000..c1ca0ff --- /dev/null +++ b/src/identity/keypair.rs @@ -0,0 +1,121 @@ +use anyhow::{anyhow, Result}; +use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; +use rand_core::OsRng; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct KeyPair { + pub signing_key: Option, + pub verifying_key: VerifyingKey, +} + +impl KeyPair { + pub fn generate() -> Result { + let mut rng = OsRng; + let signing_key = SigningKey::generate(&mut rng); + let verifying_key = signing_key.verifying_key(); + Ok(Self { + signing_key: Some(signing_key), + verifying_key, + }) + } + + pub fn from_signing_key_bytes(bytes: [u8; 32]) -> Result { + let signing_key = SigningKey::from_bytes(&bytes); + let verifying_key = signing_key.verifying_key(); + Ok(Self { + signing_key: Some(signing_key), + verifying_key, + }) + } + + pub fn from_verifying_key_bytes(verifying_key: [u8; 32]) -> Result { + let verifying_key = VerifyingKey::from_bytes(&verifying_key)?; + Ok(Self { + signing_key: None, + verifying_key, + }) + } + + pub fn sign(&self, msg: &[u8]) -> Result { + if let Some(signing_key) = &self.signing_key { + Ok(signing_key.sign(msg)) + } else { + Err(anyhow!("no signing key")) + } + } + + pub fn verify(&self, msg: &[u8], sig: &Signature) -> bool { + self.verifying_key.verify(msg, sig).is_ok() + } + + pub fn verifying_key_bytes(&self) -> [u8; 32] { + *self.verifying_key.as_bytes() + } + + pub fn signing_key_bytes(&self) -> Result<[u8; 32]> { + if let Some(signing_key) = &self.signing_key { + Ok(*signing_key.as_bytes()) + } else { + Err(anyhow!("no signing key")) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_and_sign_verify() { + let kp = KeyPair::generate().unwrap(); + let msg = b"hello world"; + let sig = kp.sign(msg).unwrap(); + assert!(kp.verify(msg, &sig)); + } + + #[test] + fn test_export_and_import_signing_key() { + let kp1 = KeyPair::generate().unwrap(); + let sk_bytes = kp1.signing_key_bytes().unwrap(); + let kp2 = KeyPair::from_signing_key_bytes(sk_bytes).unwrap(); + + let msg = b"test message"; + let sig1 = kp1.sign(msg).unwrap(); + let sig2 = kp2.sign(msg).unwrap(); + assert_eq!(sig1.to_bytes(), sig2.to_bytes()); + assert_eq!(kp1.verifying_key.as_bytes(), kp2.verifying_key.as_bytes()); + } + + #[test] + fn test_export_and_import_verifying_key() { + let kp1 = KeyPair::generate().unwrap(); + let vk_bytes = kp1.verifying_key.as_bytes().clone(); + let kp2 = KeyPair::from_verifying_key_bytes(vk_bytes).unwrap(); + + let msg = b"verify test"; + let sig = kp1.sign(msg).unwrap(); + + assert!(kp2.verify(msg, &sig)); + } + + #[test] + fn test_invalid_signature() { + let kp1 = KeyPair::generate().unwrap(); + let kp2 = KeyPair::generate().unwrap(); + + let msg = b"fake msg"; + let sig = kp1.sign(msg).unwrap(); + + assert!(!kp2.verify(msg, &sig)); + } + + #[test] + fn test_no_signing_key_error() { + let kp = + KeyPair::from_verifying_key_bytes(KeyPair::generate().unwrap().verifying_key_bytes()) + .unwrap(); + assert!(kp.sign(b"hi").is_err()); + } +} diff --git a/src/identity/mod.rs b/src/identity/mod.rs new file mode 100644 index 0000000..5af612b --- /dev/null +++ b/src/identity/mod.rs @@ -0,0 +1 @@ +pub mod keypair; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..69f0a7c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,7 @@ +pub mod gossip; +pub mod identity; +pub mod node; +pub mod repo; +pub mod storage; +pub mod transport; +pub mod util; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ae9bdd7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,142 @@ +use anyhow::Result; +use clap::{Parser, Subcommand}; +use std::net::SocketAddr; + +use megaengine::storage; + +#[derive(Parser)] +#[command(name = "megaengine")] +#[command(about = "MegaEngine P2P Git", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Identity related commands + Auth { + #[command(subcommand)] + action: AuthAction, + }, + /// Node related commands + Node { + #[command(subcommand)] + action: NodeAction, + }, +} + +#[derive(Subcommand)] +enum AuthAction { + /// Generate and save a new keypair + Init, +} + +#[derive(Subcommand)] +enum NodeAction { + /// Start node (initialization) + Start { + /// node alias + #[arg(long, default_value = "mega-node")] + alias: String, + /// one or more listen/announce addresses, e.g. 0.0.0.0:9000 + #[arg(short, long, default_value = "0.0.0.0:9000")] + addr: String, + + #[arg(short, long, default_value = "cert")] + cert_path: String, + }, +} + +#[tokio::main] +async fn main() -> Result<()> { + // Initialize rustls crypto provider + let _ = rustls::crypto::ring::default_provider().install_default(); + + // init logging + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + let cli = Cli::parse(); + + match cli.command { + Commands::Auth { action } => match action { + AuthAction::Init => { + tracing::info!("Generating new keypair..."); + let kp = megaengine::identity::keypair::KeyPair::generate()?; + storage::save_keypair(&kp)?; + tracing::info!("Keypair saved to {:?}", storage::keypair_path()); + } + }, + Commands::Node { action } => match action { + NodeAction::Start { + alias, + addr, + cert_path, + } => { + tracing::info!("Starting node..."); + + // Ensure certificates exist, generate if needed + let cert_dir = &cert_path; + megaengine::transport::cert::ensure_certificates( + &format!("{}/cert.pem", cert_dir), + &format!("{}/key.pem", cert_dir), + &format!("{}/ca-cert.pem", cert_dir), + )?; + + let kp = match storage::load_keypair() { + Ok(k) => k, + Err(e) => { + tracing::error!("failed to load keypair: {}", e); + tracing::info!("Run `auth init` first to generate keys"); + return Ok(()); + } + }; + + // parse addresses + let mut addrs: Vec = Vec::new(); + addrs.push(addr.parse()?); + + let mut node = megaengine::node::node::Node::from_keypair( + &kp, + alias, + addrs.clone(), + megaengine::node::node::NodeType::Normal, + ); + tracing::info!( + "Node initialized: alias={} id={}", + node.alias(), + node.node_id().0 + ); + + // Create QUIC config for this node + let quic_config = megaengine::transport::config::QuicConfig::new( + addr.parse()?, + format!("{}/cert.pem", cert_dir), + format!("{}/key.pem", cert_dir), + format!("{}/ca-cert.pem", cert_dir), + ); + + // Start QUIC server and keep it running + tracing::info!("Starting QUIC server on {}...", addr); + node.start_quic_server(quic_config).await?; + + println!( + "Node started successfully: {} ({})", + node.node_id().0, + node.alias() + ); + println!("Listening on: {}", addr); + println!("Press Ctrl+C to stop"); + + // Keep the node running indefinitely + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } + } + }, + } + + Ok(()) +} diff --git a/src/node/mod.rs b/src/node/mod.rs new file mode 100644 index 0000000..265e176 --- /dev/null +++ b/src/node/mod.rs @@ -0,0 +1,3 @@ +pub mod node; +pub mod node_id; +pub mod node_manager; diff --git a/src/node/node.rs b/src/node/node.rs new file mode 100644 index 0000000..23325a0 --- /dev/null +++ b/src/node/node.rs @@ -0,0 +1,259 @@ +use crate::identity::keypair::KeyPair; +use crate::node::node_id::NodeId; +use crate::transport::config::QuicConfig; +use crate::transport::quic::ConnectionManager; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; +use std::time::{Duration, SystemTime}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum NodeType { + Normal, + Relay, +} + +/// 节点信息(可序列化的部分) +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct NodeInfo { + pub node_id: NodeId, + pub alias: String, + pub addresses: Vec, + pub node_type: NodeType, + pub version: u8, + pub keypair: KeyPair, +} + +/// 运行时节点对象,包含网络管理器 +#[derive(Clone)] +pub struct Node { + pub info: NodeInfo, + pub connection_manager: Option>>, +} + +impl std::fmt::Debug for Node { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Node") + .field("info", &self.info) + .field("connection_manager", &"") + .finish() + } +} + +impl Node { + pub fn new( + node_id: NodeId, + alias: impl Into, + addresses: Vec, + node_type: NodeType, + keypair: KeyPair, + ) -> Self { + let info = NodeInfo { + node_id, + alias: alias.into(), + addresses, + node_type, + version: 1, + keypair, + }; + Self { + info, + connection_manager: None, + } + } + + pub fn from_keypair( + keypair: &KeyPair, + alias: impl Into, + addresses: Vec, + node_type: NodeType, + ) -> Self { + let node_id = NodeId::from_keypair(keypair); + Self::new(node_id, alias, addresses, node_type, keypair.clone()) + } + + pub fn sign_message(&self, msg: &[u8]) -> Result> { + self.info + .keypair + .sign(msg) + .map(|sig| sig.to_bytes().to_vec()) + } + + /// 启动 QUIC 服务端 + pub async fn start_quic_server(&mut self, config: QuicConfig) -> Result<()> { + let manager = ConnectionManager::run_server(config).await?; + self.connection_manager = Some(std::sync::Arc::new(tokio::sync::Mutex::new(manager))); + Ok(()) + } + + /// 获取节点信息的便捷访问器 + pub fn node_id(&self) -> &NodeId { + &self.info.node_id + } + + pub fn alias(&self) -> &str { + &self.info.alias + } + + pub fn addresses(&self) -> &[SocketAddr] { + &self.info.addresses + } + + pub fn node_type(&self) -> NodeType { + self.info.node_type.clone() + } + + pub fn version(&self) -> u8 { + self.info.version + } + + pub fn keypair(&self) -> &KeyPair { + &self.info.keypair + } +} + +#[derive(Clone, Debug)] +pub struct NodeRouting { + pub node_id: NodeId, + pub addresses: Vec, + pub last_seen: SystemTime, + pub ttl: Duration, + pub score: f32, +} + +impl NodeRouting { + pub fn new(node_id: NodeId, addresses: Vec) -> Self { + Self { + node_id, + addresses, + last_seen: SystemTime::now(), + ttl: Duration::from_secs(60 * 60 * 24), + score: 1.0, + } + } + + pub fn refresh(&mut self) { + self.last_seen = SystemTime::now(); + } + + pub fn expired(&self) -> bool { + self.last_seen + .elapsed() + .map(|e| e > self.ttl) + .unwrap_or(false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::{IpAddr, Ipv4Addr}; + use std::time::Duration; + + fn create_sample_node() -> Node { + let keypair = &KeyPair::generate().unwrap(); + let node_id = NodeId::from_keypair(keypair); + let alias = "Test Node"; + let addresses = vec![SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + 8080, + )]; + let node_type = NodeType::Normal; + + Node::new(node_id, alias, addresses, node_type, keypair.clone()) + } + + // Test creation of a Node + #[test] + fn test_create_node() { + let node = create_sample_node(); + + // Assert basic properties + assert_eq!(node.alias(), "Test Node"); + assert_eq!(node.node_type(), NodeType::Normal); + assert_eq!(node.version(), 1); + assert_eq!(node.addresses().len(), 1); + assert_eq!( + node.addresses()[0], + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080) + ); + } + + // Test `from_keypair` method for Node + #[test] + fn test_node_from_keypair() { + let keypair = KeyPair::generate().unwrap(); + let alias = "Keypair Node"; + let addresses = vec![SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + 8080, + )]; + let node_type = NodeType::Relay; + + let node = Node::from_keypair(&keypair, alias, addresses, node_type); + + // Assert properties based on keypair + assert_eq!(node.node_type(), NodeType::Relay); + assert_eq!(node.alias(), alias); + assert_eq!( + node.addresses()[0], + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080) + ); + } + + // Test `NodeRouting::new` method and expiration logic + #[test] + fn test_node_routing() { + let node_id = NodeId::from_keypair(&KeyPair::generate().unwrap()); + let addresses = vec![SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + 8080, + )]; + + let mut node_routing = NodeRouting::new(node_id.clone(), addresses); + + // Assert initial values + assert_eq!(node_routing.node_id, node_id); + assert_eq!(node_routing.addresses.len(), 1); + assert_eq!( + node_routing.addresses[0], + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080) + ); + assert_eq!(node_routing.score, 1.0); + + // Test refresh method + let initial_last_seen = node_routing.last_seen; + node_routing.refresh(); + assert_ne!(node_routing.last_seen, initial_last_seen); // last_seen should have changed + + // Test expiration logic + std::thread::sleep(Duration::from_secs(2)); // Sleep for 2 seconds to test expiration + assert_eq!(node_routing.expired(), false); // Not expired if TTL is 24 hours + + // Manually expire the node and check + node_routing.ttl = Duration::from_secs(1); // Set TTL to 1 second + std::thread::sleep(Duration::from_secs(2)); // Sleep for 2 seconds to make the node expire + assert_eq!(node_routing.expired(), true); // Should be expired now + } + + // Test the `NodeType` enum + #[test] + fn test_node_type_enum() { + // Normal node + let node = create_sample_node(); + assert_eq!(node.node_type(), NodeType::Normal); + + // Relay node + let node = Node::new( + NodeId::from_keypair(&KeyPair::generate().unwrap()), + "Relay Node", + vec![SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + 8080, + )], + NodeType::Relay, + KeyPair::generate().unwrap(), + ); + assert_eq!(node.node_type(), NodeType::Relay); + } +} diff --git a/src/node/node_id.rs b/src/node/node_id.rs new file mode 100644 index 0000000..ecdae35 --- /dev/null +++ b/src/node/node_id.rs @@ -0,0 +1,192 @@ +use crate::identity::keypair::KeyPair; +use anyhow::anyhow; +use anyhow::Result; +use multibase::Base; +use multibase::{decode, encode}; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] +pub struct NodeId(pub String); + +const DID_KEY_PREFIX: &str = "did:key:"; + +impl NodeId { + pub fn from_keypair(keypair: &KeyPair) -> Self { + let mut prefixed = vec![0xed]; + prefixed.extend_from_slice(keypair.verifying_key_bytes().as_slice()); + NodeId(format!( + "{}{}", + DID_KEY_PREFIX, + encode(Base::Base58Btc, prefixed) + )) + } + + pub fn from_string(node_id: &str) -> Result { + if !node_id.starts_with(DID_KEY_PREFIX) { + return Err(anyhow!("invalid NodeId prefix")); + } + + let encoded = &node_id[DID_KEY_PREFIX.len()..]; + if encoded.is_empty() { + return Err(anyhow!("empty encoded part")); + } + + let (base, data) = decode(encoded).map_err(|e| anyhow!("nodeId decode failed: {}", e))?; + + if base != Base::Base58Btc { + return Err(anyhow!("invalid base format")); + } + + if data.is_empty() || data[0] != 0xed { + return Err(anyhow!("invalid key prefix")); + } + + Ok(NodeId(node_id.to_string())) + } + + pub fn to_keypair(&self) -> Result { + if !self.0.starts_with(DID_KEY_PREFIX) { + return Err(anyhow!("invalid NodeId prefix")); + } + + let encoded = &self.0[DID_KEY_PREFIX.len()..]; + let (base, data) = decode(encoded).map_err(|e| anyhow!("nodeId decode failed: {}", e))?; + if base != Base::Base58Btc { + return Err(anyhow!("invalid base format")); + } + + if data.is_empty() || data[0] != 0xed { + return Err(anyhow!("invalid key prefix")); + } + + let pubkey_bytes = &data[1..]; + if pubkey_bytes.len() != 32 { + return Err(anyhow!("invalid key length")); + } + let keypair = KeyPair::from_verifying_key_bytes(<[u8; 32]>::try_from(pubkey_bytes)?)?; + Ok(keypair) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn to_string(&self) -> String { + self.0.clone() + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + pub fn as_vec(&self) -> Vec { + self.0.as_bytes().to_vec() + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ParseNodeIdError; + +impl FromStr for NodeId { + type Err = ParseNodeIdError; + + fn from_str(s: &str) -> Result { + NodeId::from_string(s).map_err(|_| ParseNodeIdError {}) + } +} + +impl fmt::Display for NodeId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_node_id_from_and_to_keypair() -> Result<()> { + let kp = KeyPair::generate()?; + + let node_id = NodeId::from_keypair(&kp); + assert!(node_id.0.starts_with(DID_KEY_PREFIX)); + assert!(node_id.0.len() > 20); + + let recovered = node_id.clone().to_keypair()?; + assert_eq!( + kp.verifying_key_bytes(), + recovered.verifying_key_bytes(), + "Recovered pubkey should match original" + ); + + Ok(()) + } + + #[test] + fn test_valid_from_string() -> Result<()> { + let node_id_str = "did:key:z2DXbAovGq5vNKpXVFyrhVLppMdUCmV1hCNjbUydLMEWasE"; + let node_id = NodeId::from_string(&node_id_str)?; + assert_eq!(node_id.0, node_id_str); + Ok(()) + } + + #[test] + fn test_invalid_prefix_from_string() { + let result = NodeId::from_string("invalid:zabcdef"); + assert!(result.is_err()); + } + + #[test] + fn test_invalid_base_from_string() { + let encoded = encode(Base::Base64, vec![0xed, 1, 2, 3]); + let node_id_str = format!("{}{}", DID_KEY_PREFIX, encoded); + let result = NodeId::from_string(&node_id_str); + assert!(result.is_err()); + assert!(format!("{}", result.unwrap_err()).contains("invalid base format")); + } + + #[test] + fn test_invalid_key_prefix_from_string() { + // 构造前缀错误 + let encoded = encode(Base::Base58Btc, vec![0xaa, 1, 2, 3]); + let node_id_str = format!("{}{}", DID_KEY_PREFIX, encoded); + let result = NodeId::from_string(&node_id_str); + assert!(result.is_err()); + assert!(format!("{}", result.unwrap_err()).contains("invalid key prefix")); + } + + #[test] + fn test_invalid_prefix() { + let node_id = NodeId("invalid:zabc".into()); + let result = node_id.to_keypair(); + assert!(result.is_err()); + } + + #[test] + fn test_invalid_encoding() { + let node_id = NodeId(format!("{}{}", DID_KEY_PREFIX, "notbase58!")); + let result = node_id.to_keypair(); + assert!(result.is_err()); + } + + #[test] + fn test_invalid_key_prefix() { + let bad_data = encode(Base::Base58Btc, vec![0xaa, 1, 2, 3]); + let node_id = NodeId(format!("{}{}", DID_KEY_PREFIX, bad_data)); + let result = node_id.to_keypair(); + assert!(result.is_err()); + } + + #[test] + fn test_invalid_key_length() { + let mut data = vec![0xed]; + data.extend_from_slice(&[1, 2, 3, 4, 5]); + let bad_data = encode(Base::Base58Btc, data); + let node_id = NodeId(format!("{}{}", DID_KEY_PREFIX, bad_data)); + let result = node_id.to_keypair(); + assert!(result.is_err()); + } +} diff --git a/src/node/node_manager.rs b/src/node/node_manager.rs new file mode 100644 index 0000000..13321c7 --- /dev/null +++ b/src/node/node_manager.rs @@ -0,0 +1,127 @@ +use crate::node::{ + node::{Node, NodeRouting}, + node_id::NodeId, +}; +use std::collections::HashMap; + +#[derive(Default)] +pub struct NodeManager { + pub nodes: HashMap, +} + +impl NodeManager { + pub fn new() -> Self { + Self { + nodes: HashMap::new(), + } + } + + pub fn insert_node(&mut self, node: &Node) { + let routing = NodeRouting::new(node.node_id().clone(), node.addresses().to_vec()); + self.nodes.insert(node.node_id().clone(), routing); + } + + pub fn mark_alive(&mut self, node_id: &NodeId) { + if let Some(n) = self.nodes.get_mut(node_id) { + n.refresh(); + } + } + + pub fn cleanup_expired(&mut self) { + self.nodes.retain(|_, v| !v.expired()); + } + + pub fn get_node(&self, node_id: &NodeId) -> Option<&NodeRouting> { + self.nodes.get(node_id) + } + + pub fn routing_print(&self) { + println!("Node routing table ({} entries):", self.nodes.len()); + for (id, info) in &self.nodes { + println!(" {:?} -> {:?}", id, info.addresses); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::identity::keypair::{self, KeyPair}; + use crate::node::node::NodeType; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + + fn create_sample_node() -> Node { + let keypair = &KeyPair::generate().unwrap(); + let node_id = NodeId::from_keypair(keypair); + let alias = "Test Node"; + let addresses = vec![SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + 8080, + )]; + let node_type = NodeType::Normal; + + Node::new(node_id, alias, addresses, node_type, keypair.clone()) + } + + #[test] + fn test_node_manager_insert_node() { + let mut manager = NodeManager::new(); + let node = create_sample_node(); + + // Insert the node + manager.insert_node(&node); + + // Assert that the node is in the manager + assert_eq!(manager.nodes.len(), 1); + let node_routing = manager.get_node(&node.node_id()); + assert!(node_routing.is_some()); + assert_eq!(node_routing.unwrap().node_id, *node.node_id()); + } + + #[test] + fn test_node_manager_mark_alive() { + let mut manager = NodeManager::new(); + let node = create_sample_node(); + + // Insert the node + manager.insert_node(&node); + + // Get the initial last_seen time + let initial_last_seen = manager.get_node(&node.node_id()).unwrap().last_seen; + + // Mark the node as alive (refresh) + manager.mark_alive(&node.node_id()); + + // Assert that the last_seen time was refreshed + let refreshed_last_seen = manager.get_node(&node.node_id()).unwrap().last_seen; + assert_ne!(initial_last_seen, refreshed_last_seen); + } + + #[test] + fn test_node_manager_cleanup_expired() { + let mut manager = NodeManager::new(); + let node = create_sample_node(); + + manager.insert_node(&node); + assert_eq!(manager.nodes.len(), 1); + + manager.nodes.get_mut(&node.node_id()).unwrap().ttl = std::time::Duration::from_secs(1); + std::thread::sleep(std::time::Duration::from_secs(2)); + manager.cleanup_expired(); + assert_eq!(manager.nodes.len(), 0); + } + + #[test] + fn test_node_manager_routing_print() { + let mut manager = NodeManager::new(); + let node1 = create_sample_node(); + let node2 = create_sample_node(); + + manager.insert_node(&node1); + manager.insert_node(&node2); + + let _ = std::panic::catch_unwind(|| { + manager.routing_print(); + }); + } +} diff --git a/src/repo/mod.rs b/src/repo/mod.rs new file mode 100644 index 0000000..994f7cc --- /dev/null +++ b/src/repo/mod.rs @@ -0,0 +1,3 @@ +pub mod repo; +pub mod repo_id; +pub mod repo_manager; diff --git a/src/repo/repo.rs b/src/repo/repo.rs new file mode 100644 index 0000000..2a65981 --- /dev/null +++ b/src/repo/repo.rs @@ -0,0 +1,113 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Map; +use std::collections::HashMap; +use std::path::PathBuf; + +/// P2P 仓库描述 +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct P2PDescription { + pub creator: String, // NodeId + pub name: String, + pub description: String, + pub timestamp: u64, // Unix timestamp +} + +/// P2P 仓库 +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Repo { + pub repo_id: String, + pub refs: HashMap, + pub p2p_description: P2PDescription, + pub path: PathBuf, // Git 仓库本地路径 +} + +impl Repo { + /// 创建新仓库 + pub fn new(repo_id: String, p2p_description: P2PDescription, path: PathBuf) -> Self { + Repo { + repo_id, + refs: HashMap::new(), + p2p_description, + path, + } + } + + /// 添加 ref + pub fn add_ref(&mut self, ref_name: String, commit_hash: String) { + self.refs.insert(ref_name, commit_hash); + } + + /// 获取 ref + pub fn get_ref(&self, ref_name: &str) -> Option<&String> { + self.refs.get(ref_name) + } + + /// 更新 ref + pub fn update_ref(&mut self, ref_name: String, commit_hash: String) -> bool { + self.refs.insert(ref_name, commit_hash).is_some() + } + + /// 删除 ref + pub fn remove_ref(&mut self, ref_name: &str) -> Option { + self.refs.remove(ref_name) + } + + /// 获取所有 refs + pub fn list_refs(&self) -> Vec<(String, String)> { + self.refs + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect() + } + + /// 获取仓库地址(P2P 格式) + pub fn p2p_address(&self) -> String { + format!("git+p2p://{}", self.repo_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_repo_creation() { + let desc = P2PDescription { + creator: "did:key:test".to_string(), + name: "test-repo".to_string(), + description: "A test repository".to_string(), + timestamp: 1000, + }; + + let repo = Repo::new( + "did:repo:test".to_string(), + desc, + PathBuf::from("/tmp/test-repo"), + ); + + assert_eq!(repo.repo_id, "did:repo:test"); + assert_eq!(repo.p2p_address(), "git+p2p://did:repo:test"); + } + + #[test] + fn test_repo_refs() { + let desc = P2PDescription { + creator: "did:key:test".to_string(), + name: "test-repo".to_string(), + description: "A test repository".to_string(), + timestamp: 1000, + }; + + let mut repo = Repo::new( + "did:repo:test".to_string(), + desc, + PathBuf::from("/tmp/test-repo"), + ); + + repo.add_ref("refs/heads/main".to_string(), "commit1".to_string()); + assert_eq!( + repo.get_ref("refs/heads/main"), + Some(&"commit1".to_string()) + ); + } +} diff --git a/src/repo/repo_id.rs b/src/repo/repo_id.rs new file mode 100644 index 0000000..9947b8b --- /dev/null +++ b/src/repo/repo_id.rs @@ -0,0 +1,156 @@ +use anyhow::anyhow; +use anyhow::Result; +use multibase::{decode, encode, Base}; +use multihash::Hash; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] +pub struct RepoId(pub String); + +const REPO_KEY_PREFIX: &str = "did:repo:"; + +impl RepoId { + pub fn generate(root_commit: &[u8], creator_public_key: &[u8]) -> Result { + let mut data = Vec::new(); + data.extend_from_slice(root_commit); + data.extend_from_slice(creator_public_key); + let hash = multihash::encode(Hash::SHA3256, &data)?; + Ok(RepoId(format!( + "{}{}", + REPO_KEY_PREFIX, + encode(Base::Base58Btc, hash) + ))) + } + + pub fn from_str(repo_id: &str) -> Result { + if !repo_id.starts_with(REPO_KEY_PREFIX) { + return Err(anyhow!("invalid NodeId prefix")); + } + + let encoded = &repo_id[REPO_KEY_PREFIX.len()..]; + if encoded.is_empty() { + return Err(anyhow!("empty encoded part")); + } + + let (base, data) = decode(encoded).map_err(|e| anyhow!("repoId decode failed: {}", e))?; + + if base != Base::Base58Btc { + return Err(anyhow!("invalid base format")); + } + + let _ = multihash::encode(Hash::SHA3256, &data)?; + + Ok(RepoId(repo_id.to_string())) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ParseNodeIdError; + +impl FromStr for RepoId { + type Err = ParseNodeIdError; + + fn from_str(s: &str) -> Result { + RepoId::from_str(s).map_err(|_| ParseNodeIdError) + } +} + +impl fmt::Display for RepoId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::identity::keypair::KeyPair; + use anyhow::Result; + + // 测试 RepoId 的生成 + #[test] + fn test_generate_repo_id() -> Result<()> { + let root_commit = b"root_commit_data"; + let keypair = KeyPair::generate()?; + + // 生成 RepoId + let repo_id = RepoId::generate(root_commit, keypair.verifying_key.as_bytes())?; + println!("Generated RepoId: {}", repo_id); + // 验证 RepoId 是否以 "did:repo:" 开头 + assert!(repo_id.0.starts_with(REPO_KEY_PREFIX)); + + Ok(()) + } + + // 测试 RepoId 的解析 + #[test] + fn test_from_string_valid() -> Result<()> { + let root_commit = b"root_commit_data"; // 示例 Git 根提交数据 + let keypair = KeyPair::generate()?; + + // 生成 RepoId + let repo_id = RepoId::generate(root_commit, keypair.verifying_key.as_bytes())?; + + // 从生成的字符串解析回 RepoId + let parsed_repo_id = RepoId::from_str(&repo_id.0)?; + + // 验证解析出来的 RepoId 与原始 RepoId 是否相同 + assert_eq!(repo_id, parsed_repo_id); + + Ok(()) + } + + // 测试 RepoId 解析的错误情况:无效的前缀 + #[test] + fn test_from_string_invalid_prefix() { + let invalid_repo_id = "invalid:repo_id"; + let result = RepoId::from_str(invalid_repo_id); + assert!(result.is_err()); + } + + // 测试 RepoId 解析的错误情况:空的编码部分 + #[test] + fn test_from_string_empty_encoded_part() { + let invalid_repo_id = "did:repo:"; + let result = RepoId::from_str(invalid_repo_id); + assert!(result.is_err()); + } + + // 测试 RepoId 解析的错误情况:无效的 Base58 格式 + #[test] + fn test_from_string_invalid_base_format() { + let invalid_repo_id = "did:repo:xyz123"; // 假设这是一个错误的 Base 格式 + let result = RepoId::from_str(invalid_repo_id); + assert!(result.is_err()); + } + + // 测试 RepoId 从字符串解析 + #[test] + fn test_repo_id_from_str() -> Result<()> { + let repo_id_str = "did:repo:z5fV2HmRQ3EzYYQ2smU2db1JgeWsxzPfYY9GBR1kFH8S5Zr"; + let repo_id: RepoId = repo_id_str.parse().unwrap(); + + // 验证解析出来的 RepoId 是否正确 + assert_eq!(repo_id.to_string(), repo_id_str); + + Ok(()) + } + + // 测试 RepoId 的 Display 格式化输出 + #[test] + fn test_repo_id_display() { + let repo_id = RepoId("z5fV2HmRQ3EzYYQ2smU2db1JgeWsxzPfYY9GBR1kFH8S5Zr".to_string()); + + // 验证 RepoId 的显示 + assert_eq!( + format!("{}", repo_id), + "z5fV2HmRQ3EzYYQ2smU2db1JgeWsxzPfYY9GBR1kFH8S5Zr" + ); + } +} diff --git a/src/repo/repo_manager.rs b/src/repo/repo_manager.rs new file mode 100644 index 0000000..93b1138 --- /dev/null +++ b/src/repo/repo_manager.rs @@ -0,0 +1,107 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use crate::repo::repo::Repo; + +/// 仓库管理器 +/// 管理本地仓库和 P2P 仓库的对应关系 +pub struct RepoManager { + // RepoId -> Repo 映射 + repos: HashMap, + // 本地路径 -> RepoId 映射 + path_to_repo_id: HashMap, +} + +impl RepoManager { + /// 创建新的仓库管理器 + pub fn new() -> Self { + RepoManager { + repos: HashMap::new(), + path_to_repo_id: HashMap::new(), + } + } + + /// 注册仓库 + pub fn register_repo(&mut self, repo: Repo) -> Result<(), String> { + let repo_id = repo.repo_id.clone(); + let path = repo.path.clone(); + + if self.repos.contains_key(&repo_id) { + return Err(format!("Repository {} already exists", repo_id)); + } + + self.repos.insert(repo_id.clone(), repo); + self.path_to_repo_id.insert(path, repo_id); + Ok(()) + } + + /// 根据 RepoId 获取仓库 + pub fn get_repo(&self, repo_id: &str) -> Option<&Repo> { + self.repos.get(repo_id) + } + + /// 根据 RepoId 获取仓库(可变) + pub fn get_repo_mut(&mut self, repo_id: &str) -> Option<&mut Repo> { + self.repos.get_mut(repo_id) + } + + /// 根据路径获取仓库 ID + pub fn get_repo_id_by_path(&self, path: &PathBuf) -> Option<&String> { + self.path_to_repo_id.get(path) + } + + /// 删除仓库 + pub fn remove_repo(&mut self, repo_id: &str) -> Option { + if let Some(repo) = self.repos.remove(repo_id) { + self.path_to_repo_id.remove(&repo.path); + Some(repo) + } else { + None + } + } + + /// 列出所有仓库 + pub fn list_repos(&self) -> Vec<&Repo> { + self.repos.values().collect() + } + + /// 获取仓库数量 + pub fn repo_count(&self) -> usize { + self.repos.len() + } +} + +impl Default for RepoManager { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use crate::repo::repo::P2PDescription; + + use super::*; + + #[test] + fn test_repo_manager() { + let mut manager = RepoManager::new(); + + let desc = P2PDescription { + creator: "did:key:test".to_string(), + name: "test-repo".to_string(), + description: "A test repository".to_string(), + timestamp: 1000, + }; + + let repo = Repo::new( + "did:repo:test".to_string(), + desc, + PathBuf::from("/tmp/test-repo"), + ); + + assert!(manager.register_repo(repo).is_ok()); + assert_eq!(manager.repo_count(), 1); + assert!(manager.get_repo("did:repo:test").is_some()); + } +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs new file mode 100644 index 0000000..dbc173b --- /dev/null +++ b/src/storage/mod.rs @@ -0,0 +1,74 @@ +use anyhow::Result; +use std::fs; +use std::path::PathBuf; + +use crate::identity::keypair::KeyPair; + +/// 数据目录:cwd/.megaengine +pub fn data_dir() -> PathBuf { + let mut p = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + p.push(".megaengine"); + p +} + +/// 密钥对文件路径 +pub fn keypair_path() -> PathBuf { + let mut p = data_dir(); + p.push("keypair.json"); + p +} + +/// 保存密钥对到文件 +pub fn save_keypair(kp: &KeyPair) -> Result<()> { + let dir = data_dir(); + fs::create_dir_all(&dir)?; + let path = keypair_path(); + let s = serde_json::to_string_pretty(kp)?; + fs::write(path, s)?; + Ok(()) +} + +/// 从文件加载密钥对 +pub fn load_keypair() -> Result { + let path = keypair_path(); + let s = fs::read_to_string(path)?; + let kp: KeyPair = serde_json::from_str(&s)?; + Ok(kp) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_data_dir() { + let dir = data_dir(); + assert!(dir.ends_with(".megaengine")); + } + + #[test] + fn test_keypair_path() { + let path = keypair_path(); + assert!(path.to_string_lossy().contains("keypair.json")); + } + + #[test] + fn test_save_and_load_keypair() -> Result<()> { + let kp = KeyPair::generate()?; + save_keypair(&kp)?; + + let loaded = load_keypair()?; + assert_eq!( + kp.verifying_key_bytes(), + loaded.verifying_key_bytes(), + "Loaded keypair should match saved keypair" + ); + + // cleanup + let path = keypair_path(); + if path.exists() { + fs::remove_file(path)?; + } + Ok(()) + } +} diff --git a/src/transport/cert.rs b/src/transport/cert.rs new file mode 100644 index 0000000..d721680 --- /dev/null +++ b/src/transport/cert.rs @@ -0,0 +1,177 @@ +use anyhow::{anyhow, Result}; +use rcgen::{Certificate, CertificateParams, DistinguishedName, DnType, KeyPair}; +use std::fs; +use std::path::Path; + +/// Generate a CA certificate and save to files. +pub fn generate_ca_cert(ca_cert_path: &str, ca_key_path: &str) -> Result { + // Check if CA certificate already exists + if Path::new(ca_cert_path).exists() && Path::new(ca_key_path).exists() { + tracing::info!( + "CA certificate already exists at {} and {}", + ca_cert_path, + ca_key_path + ); + // Return a dummy cert since we can't reconstruct it from PEM + // But files exist so they'll be used by other functions + return Err(anyhow!("CA cert exists, but cannot reconstruct from PEM")); + } + + // Create cert directory if needed + if let Some(parent) = Path::new(ca_cert_path).parent() { + if !parent.as_os_str().is_empty() && !parent.exists() { + fs::create_dir_all(parent)?; + } + } + + tracing::info!("Generating CA certificate..."); + + // Generate a keypair for CA + let keypair = + KeyPair::generate().map_err(|e| anyhow!("Failed to generate CA keypair: {}", e))?; + + // Create CA certificate parameters + let mut params = CertificateParams::new(vec![]) + .map_err(|e| anyhow!("Failed to create CA certificate params: {}", e))?; + + params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); + + // Set CA subject name + let mut dn = DistinguishedName::new(); + dn.push(DnType::CommonName, "MegaEngine CA"); + dn.push(DnType::OrganizationName, "MegaEngine"); + dn.push(DnType::CountryName, "CN"); + params.distinguished_name = dn; + + // Generate self-signed CA certificate + let ca_cert = params + .self_signed(&keypair) + .map_err(|e| anyhow!("Failed to generate CA certificate: {}", e))?; + + // Save CA certificate + let ca_cert_pem = ca_cert.pem(); + fs::write(ca_cert_path, ca_cert_pem)?; + tracing::info!("CA certificate written to {}", ca_cert_path); + + // Save CA private key + let ca_key_pem = keypair.serialize_pem(); + fs::write(ca_key_path, ca_key_pem)?; + tracing::info!("CA private key written to {}", ca_key_path); + + Ok(ca_cert) +} + +/// Generate a server certificate signed by CA. +pub fn generate_server_cert( + cert_path: &str, + key_path: &str, + ca_cert_obj: &Certificate, + ca_key_path: &str, +) -> Result<()> { + // Check if certificate already exists + if Path::new(cert_path).exists() && Path::new(key_path).exists() { + tracing::info!( + "Server certificate already exists at {} and {}", + cert_path, + key_path + ); + return Ok(()); + } + + // Create cert directory if needed + if let Some(parent) = Path::new(cert_path).parent() { + if !parent.as_os_str().is_empty() && !parent.exists() { + fs::create_dir_all(parent)?; + } + } + + tracing::info!("Generating server certificate signed by CA..."); + + // Read CA key + let ca_key_pem = fs::read_to_string(ca_key_path)?; + + // Parse CA key + let ca_keypair = + KeyPair::from_pem(&ca_key_pem).map_err(|e| anyhow!("Failed to parse CA key: {}", e))?; + + // Generate server keypair + let server_keypair = + KeyPair::generate().map_err(|e| anyhow!("Failed to generate server keypair: {}", e))?; + + // Create server certificate parameters with SANs + let mut params = CertificateParams::new(vec![ + "localhost".to_string(), + "127.0.0.1".to_string(), + "0.0.0.0".to_string(), + ]) + .map_err(|e| anyhow!("Failed to create server certificate params: {}", e))?; + + // Set server subject name + let mut dn = DistinguishedName::new(); + dn.push(DnType::CommonName, "localhost"); + dn.push(DnType::OrganizationName, "MegaEngine"); + dn.push(DnType::CountryName, "CN"); + params.distinguished_name = dn; + + // Sign server certificate with CA key + // signed_by expects (server_keypair, ca_cert_obj, ca_keypair) + let server_cert = params + .signed_by(&server_keypair, ca_cert_obj, &ca_keypair) + .map_err(|e| anyhow!("Failed to generate server certificate: {}", e))?; + + // Save server certificate + let cert_pem = server_cert.pem(); + fs::write(cert_path, cert_pem)?; + tracing::info!("Server certificate written to {}", cert_path); + + // Save server private key + let key_pem = server_keypair.serialize_pem(); + fs::write(key_path, key_pem)?; + tracing::info!("Server private key written to {}", key_path); + + Ok(()) +} + +/// Ensure certificates exist: generate CA once, then generate different server certs. +pub fn ensure_certificates(cert_path: &str, key_path: &str, ca_cert_path: &str) -> Result<()> { + // Derive CA key path from CA cert path + let ca_key_path = ca_cert_path.replace(".pem", "-key.pem"); + + // Check if both cert and key exist - if only one exists, something went wrong, regenerate both + let cert_exists = Path::new(cert_path).exists(); + let key_exists = Path::new(key_path).exists(); + + if cert_exists != key_exists { + // Mismatch - delete both and regenerate + let _ = std::fs::remove_file(cert_path); + let _ = std::fs::remove_file(key_path); + } + + // Generate CA certificate if needed (only once) + let ca_cert = match generate_ca_cert(ca_cert_path, &ca_key_path) { + Ok(cert) => cert, + Err(_) => { + // CA already exists - need to reconstruct it from files for signing + tracing::info!("CA certificate exists, reconstructing from files"); + + let ca_key_pem = fs::read_to_string(&ca_key_path)?; + let keypair = KeyPair::from_pem(&ca_key_pem)?; + let mut params = CertificateParams::new(vec![])?; + params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); + let mut dn = DistinguishedName::new(); + dn.push(DnType::CommonName, "MegaEngine CA"); + dn.push(DnType::OrganizationName, "MegaEngine"); + dn.push(DnType::CountryName, "CN"); + params.distinguished_name = dn; + params.self_signed(&keypair)? + } + }; + + // Generate server certificate signed by CA + // If server cert and key don't both exist, regenerate them + if !cert_exists || !key_exists { + generate_server_cert(cert_path, key_path, &ca_cert, &ca_key_path)?; + } + + Ok(()) +} diff --git a/src/transport/config.rs b/src/transport/config.rs new file mode 100644 index 0000000..24c784d --- /dev/null +++ b/src/transport/config.rs @@ -0,0 +1,134 @@ +use anyhow::{Context, Result}; +use quinn::crypto::rustls::{QuicClientConfig, QuicServerConfig}; +use quinn::{ClientConfig, IdleTimeout, ServerConfig, TransportConfig, VarInt}; +use rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use rustls::server::WebPkiClientVerifier; +use std::fs::File; +use std::io::BufReader; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; + +pub const ALPN_QUIC_HTTP: &[&[u8]] = &[b"h3"]; + +#[derive(Clone, Debug)] +pub struct QuicConfig { + pub bind_addr: SocketAddr, + pub cert_path: String, + pub key_path: String, + pub ca_cert_path: String, +} + +impl QuicConfig { + pub fn new( + bind_addr: SocketAddr, + cert_path: String, + key_path: String, + ca_cert_path: String, + ) -> Self { + QuicConfig { + bind_addr, + cert_path, + key_path, + ca_cert_path, + } + } + + /// 获取服务器配置 + pub fn get_server_config(&self) -> Result { + let (certs, key) = self.get_certificate_from_file()?; + + let mut roots = rustls::RootCertStore::empty(); + let ca_cert = self.get_ca_certificate_from_file()?; + roots.add(ca_cert)?; + + let client_verifier = WebPkiClientVerifier::builder(roots.into()) + .build() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + + let mut server_crypto = rustls::ServerConfig::builder() + .with_client_cert_verifier(client_verifier) + .with_single_cert(certs, key)?; + server_crypto.alpn_protocols = ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); + server_crypto.max_early_data_size = u32::MAX; + + let mut server_config = + ServerConfig::with_crypto(Arc::new(QuicServerConfig::try_from(server_crypto)?)); + + let mut transport_config = TransportConfig::default(); + transport_config.max_idle_timeout(Some(IdleTimeout::from(VarInt::from_u32(300_000)))); + transport_config.keep_alive_interval(Some(Duration::from_secs(30))); + server_config.transport_config(Arc::new(transport_config)); + + Ok(server_config) + } + + /// 获取客户端配置 + pub fn get_client_config(&self) -> Result { + let mut roots = rustls::RootCertStore::empty(); + let ca_cert = self.get_ca_certificate_from_file()?; + roots.add(ca_cert)?; + let (certs, key) = self.get_certificate_from_file()?; + + // let mut client_crypto = rustls::ClientConfig::builder() + // .with_root_certificates(roots) + // .with_no_client_auth(); + + let mut client_crypto = rustls::ClientConfig::builder() + .with_root_certificates(roots) + .with_client_auth_cert(certs, key)?; + + client_crypto.alpn_protocols = ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); + client_crypto.enable_early_data = false; + let client_config = ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?)); + Ok(client_config) + } + + /// 从文件读取证书和密钥 + pub fn get_certificate_from_file( + &self, + ) -> Result<(Vec>, PrivateKeyDer<'static>)> { + let cert_file = File::open(self.cert_path.as_str())?; + let mut cert_reader = BufReader::new(cert_file); + let certs = rustls_pemfile::certs(&mut cert_reader).collect::>>()?; + + if certs.is_empty() { + return Err(anyhow::anyhow!("No certificates found in PEM file")); + } + + let file = File::open(self.key_path.as_str())?; + let mut reader = BufReader::new(file); + + // 尝试读取PKCS8格式的私钥 + if let Some(key) = rustls_pemfile::private_key(&mut reader)? { + return Ok((certs, key)); + } + + // 如果PKCS8格式读取失败,重新读取文件尝试其他格式 + let file = File::open(self.key_path.as_str())?; + let mut reader = BufReader::new(file); + + // 尝试读取所有可能的私钥格式 + let keys = + rustls_pemfile::pkcs8_private_keys(&mut reader).collect::>>()?; + + if !keys.is_empty() { + return Ok((certs, PrivateKeyDer::Pkcs8(keys[0].clone_key()))); + } + Err(anyhow::anyhow!("No key found in PEM file")) + } + + /// 从文件读取 CA 证书 + pub fn get_ca_certificate_from_file(&self) -> Result> { + let file = File::open(self.ca_cert_path.as_str())?; + let mut reader = BufReader::new(file); + + let certs = rustls_pemfile::certs(&mut reader).collect::>>()?; + + if certs.is_empty() { + return Err(anyhow::anyhow!("No certificates found in CA PEM file")); + } + + Ok(certs[0].clone()) + } +} diff --git a/src/transport/mod.rs b/src/transport/mod.rs new file mode 100644 index 0000000..dc27750 --- /dev/null +++ b/src/transport/mod.rs @@ -0,0 +1,3 @@ +pub mod cert; +pub mod config; +pub mod quic; diff --git a/src/transport/quic.rs b/src/transport/quic.rs new file mode 100644 index 0000000..7804cd4 --- /dev/null +++ b/src/transport/quic.rs @@ -0,0 +1,429 @@ +use crate::node::node::Node; +use crate::node::node_id::NodeId; +use crate::transport::config::QuicConfig; +use anyhow::{Context, Result}; +use quinn::{ + Connection, ConnectionError, Endpoint, Incoming, RecvStream, ServerConfig, TransportConfig, + VarInt, +}; +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::sync::mpsc::Receiver; +use tokio::sync::{mpsc, Mutex}; +use tracing::{error, info}; + +const READ_BUF_SIZE: usize = 1024 * 1024; + +#[derive(Debug, Clone)] +pub struct ConnectionManager { + config: QuicConfig, + endpoint: Arc, + connection_tx: mpsc::Sender, + connections: Arc>>>, +} + +#[derive(Debug, Clone)] +pub struct QuicConnection { + pub connection: Connection, + pub peer_addr: SocketAddr, + pub node_id: NodeId, + pub connection_type: ConnectionType, + pub connection_state: ConnectionState, +} + +#[derive(Debug, Clone)] +pub enum ConnectionState { + Connecting, + Connected, + Disconnected, + Failed, +} + +#[derive(Debug, Clone)] +pub enum ConnectionType { + Client, + Server, +} + +impl ConnectionManager { + fn server(config: QuicConfig) -> Result<(Self, Receiver)> { + let server_config = config.get_server_config()?; + + let mut endpoint = Endpoint::server(server_config, config.bind_addr) + .context("Failed to create QUIC server endpoint")?; + let client_config = config.get_client_config()?; + endpoint.set_default_client_config(client_config); + + info!( + "The quic service starts on address {}", + endpoint.local_addr()? + ); + + let (connection_tx, connection_rx) = mpsc::channel(8); + + let transport = Self { + config, + endpoint: Arc::new(endpoint), + connection_tx, + connections: Arc::new(Mutex::new(HashMap::new())), + }; + Ok((transport, connection_rx)) + } + + pub async fn run_server(config: QuicConfig) -> Result { + let (manager, mut conn_rx) = ConnectionManager::server(config)?; + let endpoint = Arc::clone(&manager.endpoint); + let connection_tx = manager.connection_tx.clone(); + let connections = Arc::clone(&manager.connections); + let manager_clone = manager.clone(); + + tokio::spawn(async move { + while let Some(incoming) = endpoint.accept().await { + info!("Accepting connection from {}", incoming.remote_address()); + let tx = connection_tx.clone(); + let manager_clone = manager_clone.clone(); + tokio::spawn(async move { + // let manager_clone = manager_clone.clone(); + match Self::accept_connection(incoming).await { + Ok((conn, msg_rx)) => { + if let Err(e) = tx.send(conn).await { + error!("Failed to send connection: {}", e); + return; + } + manager_clone.spawn_message_handler(msg_rx).await; + } + Err(e) => { + error!("Connection failed: {}", e); + } + } + }); + } + }); + + // 保存连接 + tokio::spawn(async move { + while let Some(conn) = conn_rx.recv().await { + connections + .lock() + .await + .insert(conn.node_id.clone(), Arc::from(conn.clone())); + } + }); + + Ok(manager.clone()) + } + + pub async fn accept_connection( + incoming: Incoming, + ) -> Result<(QuicConnection, Receiver>)> { + let connection = incoming.await?; + let peer_addr = connection.remote_address(); + + // 等待客户端发来的身份流 + let mut recv = connection.accept_uni().await?; + let node_id_bytes = recv.read_to_end(READ_BUF_SIZE).await?; + let node_id_str = String::from_utf8(node_id_bytes)?; + let node_id: NodeId = node_id_str.parse().unwrap(); + + info!( + "Accepted connection from {}, NodeId = {}", + peer_addr, node_id + ); + + let (message_tx, message_rx) = mpsc::channel(32); + let connection_clone = connection.clone(); + tokio::spawn(async move { + while let Ok(mut recv) = connection_clone.accept_uni().await { + if let Ok(msg) = recv.read_to_end(READ_BUF_SIZE).await { + if message_tx.send(msg).await.is_err() { + break; + } + } + } + }); + Ok(( + QuicConnection { + connection, + peer_addr, + node_id, + connection_type: ConnectionType::Server, + connection_state: ConnectionState::Connected, + }, + message_rx, + )) + } + + /// 生成消息处理任务 + async fn spawn_message_handler(&self, mut receiver: Receiver>) { + tokio::spawn(async move { + while let Some(data) = receiver.recv().await { + let message = String::from_utf8(data).unwrap(); + info!("Received message: {}", message); + } + }); + } + + pub async fn connect( + &self, + self_node_id: NodeId, + target_node_id: NodeId, + addrs: Vec, + ) -> Result<()> { + let endpoint = self.endpoint.clone(); + let mut connection = None; + + info!("Trying to connect to node[{}]", target_node_id.to_string()); + for addr in addrs.iter() { + match endpoint.connect(*addr, "localhost")?.await { + Ok(c) => { + connection = Some(c); + break; + } + Err(_) => continue, + } + } + + let connection = match connection { + Some(c) => c, + None => { + return Err(anyhow::anyhow!( + "Failed to connect to node[{}], no address available", + target_node_id.to_string() + )) + } + }; + + let peer_addr = connection.remote_address(); + info!( + "Node[{}] connect to[[{}] successfully: {}", + self_node_id.to_string(), + target_node_id.to_string(), + peer_addr + ); + + //Send node_id + let mut send = connection.open_uni().await?; + send.write_all(self_node_id.as_bytes()).await?; + send.finish()?; + + let quic_conn = QuicConnection { + connection, + peer_addr, + node_id: target_node_id.clone(), + connection_type: ConnectionType::Client, + connection_state: ConnectionState::Connected, + }; + let connections = Arc::clone(&self.connections); + connections + .lock() + .await + .insert(target_node_id.clone(), Arc::from(quic_conn.clone())); + + Ok(()) + } + + pub async fn send_message(&self, node_id: NodeId, message: Vec) -> Result<()> { + let connections = self.connections.lock().await; + let conn = connections.get(&node_id).context(format!( + "Failed to send message to node[{}], connection not found", + node_id + ))?; + let mut sender = conn.connection.open_uni().await?; + sender.write_all(message.as_slice()).await?; + sender.finish()?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{identity::keypair::KeyPair, node::node::NodeType}; + use std::sync::Once; + use tokio::time::{sleep, Duration}; + + static RUSTLS_INIT: Once = Once::new(); + + fn init() { + // Install ring crypto provider only once per test process. + RUSTLS_INIT.call_once(|| { + let _ = rustls::crypto::ring::default_provider().install_default(); + }); + } + + // Mock configuration for the tests + fn mock_quic_config() -> QuicConfig { + // tracing subscriber may only be initialized once per process; ignore error if already set. + let _ = tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .with_test_writer() + .try_init(); + + // Ensure certificates exist for tests - generate separate cert/key for server 1 + let _ = crate::transport::cert::ensure_certificates( + "cert/cert.pem", + "cert/key.pem", + "cert/ca-cert.pem", + ); + + // Use mock configuration for the tests, ideally mock the actual methods + QuicConfig::new( + "0.0.0.0:0".parse().unwrap(), + "cert/cert.pem".to_string(), + "cert/key.pem".to_string(), + "cert/ca-cert.pem".to_string(), + ) + } + + fn mock_quic_config2() -> QuicConfig { + // Ensure certificates exist for tests - generate separate cert/key for server 2 + let _ = crate::transport::cert::ensure_certificates( + "cert/cert2.pem", + "cert/key2.pem", + "cert/ca-cert.pem", + ); + + // Use mock configuration for the tests, ideally mock the actual methods + QuicConfig::new( + "0.0.0.0:0".parse().unwrap(), + "cert/cert2.pem".to_string(), + "cert/key2.pem".to_string(), + "cert/ca-cert.pem".to_string(), + ) + } + + // Test the `server` method + #[tokio::test] + async fn test_server_creation() { + init(); + let config = mock_quic_config(); + + let manager = ConnectionManager::run_server(config).await; + assert!(manager.is_ok()); + + tokio::time::sleep(Duration::from_millis(500)).await; + let quic_transport = manager.unwrap(); + assert!(quic_transport.connections.lock().await.is_empty()); + } + + // Test the `connect` method + #[tokio::test] + async fn test_client_connection() { + init(); + let keypair1 = KeyPair::generate().expect("generate keypair"); + let keypair2 = KeyPair::generate().expect("generate keypair"); + + let config = mock_quic_config(); + let manager = ConnectionManager::run_server(config).await; + assert!(manager.is_ok()); + let manager = manager.unwrap(); + // give the server a moment to start and bind + tokio::time::sleep(Duration::from_millis(200)).await; + + let addr1 = manager.endpoint.local_addr().expect("get local addr"); + let addr1 = format!("127.0.0.1:{}", addr1.port()).parse().unwrap(); + let node1 = Node::new( + NodeId::from_keypair(&keypair1), + "", + vec![addr1], + NodeType::Normal, + keypair1.clone(), + ); + + let config2 = mock_quic_config2(); + let manager2 = ConnectionManager::run_server(config2).await; + assert!(manager2.is_ok()); + let manager2 = manager2.unwrap(); + tokio::time::sleep(Duration::from_millis(200)).await; + + let addr2 = manager2.endpoint.local_addr().expect("get local addr"); + let addr2 = format!("127.0.0.1:{}", addr2.port()).parse().unwrap(); + let node2 = Node::new( + NodeId::from_keypair(&keypair2), + "", + vec![addr2], + NodeType::Normal, + keypair2.clone(), + ); + + manager2 + .connect( + node2.node_id().clone(), + node1.node_id().clone(), + node1.addresses().to_vec(), + ) + .await + .unwrap(); + tokio::time::sleep(Duration::from_millis(500)).await; + + let connections1 = manager.connections.lock().await; + let connections2 = manager2.connections.lock().await; + assert!(!connections1.is_empty()); + assert!(!connections2.is_empty()); + + assert!(connections1.contains_key(&node2.node_id().clone())); + assert!(connections2.contains_key(&node1.node_id().clone())); + } + + #[tokio::test] + async fn test_send_message() { + init(); + let keypair1 = KeyPair::generate().expect("generate keypair"); + let keypair2 = KeyPair::generate().expect("generate keypair"); + + let config = mock_quic_config(); + let manager = ConnectionManager::run_server(config).await; + assert!(manager.is_ok()); + let manager = manager.unwrap(); + tokio::time::sleep(Duration::from_millis(200)).await; + + let addr1 = manager.endpoint.local_addr().expect("get local addr"); + let addr1 = format!("127.0.0.1:{}", addr1.port()).parse().unwrap(); + let node1 = Node::new( + NodeId::from_keypair(&keypair1), + "", + vec![addr1], + NodeType::Normal, + keypair1.clone(), + ); + + let config2 = mock_quic_config2(); + let manager2 = ConnectionManager::run_server(config2).await; + assert!(manager2.is_ok()); + let manager2 = manager2.unwrap(); + tokio::time::sleep(Duration::from_millis(200)).await; + + let addr2 = manager2.endpoint.local_addr().expect("get local addr"); + let node2 = Node::new( + NodeId::from_keypair(&keypair2), + "", + vec![addr2], + NodeType::Normal, + keypair2.clone(), + ); + + manager2 + .connect( + node2.node_id().clone(), + node1.node_id().clone(), + node1.addresses().to_vec(), + ) + .await + .unwrap(); + tokio::time::sleep(Duration::from_millis(200)).await; + + { + let connections1 = manager.connections.lock().await; + let connections2 = manager2.connections.lock().await; + assert!(!connections1.is_empty()); + assert!(!connections2.is_empty()); + } + + manager2 + .send_message(node1.node_id().clone(), b"hello".to_vec()) + .await + .unwrap(); + tokio::time::sleep(Duration::from_millis(500)).await; + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..b42c684 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,8 @@ +use std::time::SystemTime; + +pub fn timestamp_now() -> u64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() +} diff --git a/test_output.txt b/test_output.txt new file mode 100644 index 0000000000000000000000000000000000000000..e2b02e01b53844e5d57ef9e0c725efa37a19ebdc GIT binary patch literal 14670 zcmeI3TW=f3702hfK)(Zl0*;Kvf~3ed6%Fdja#O^PU0bbNSjp-_%4V%wNh-FQq|Q)yL~VSa%bmyZgXxk{LjA+!d@7J*WomD!iK)~+0^@SI1Bx7r0+z0!>Yb3dRq!xnq?;(Xna@gJB_1mhM$L@h2QKiy?pZY1w{7+5sr9Ma{;JYJ z+5b_*q!*qE{!w_No<3V4NYC|l7JrvjdarLMFxv~waCAA#vT$6!gzO(-+7GxoUBPKH z!T>E#Mfr>Hdr|UGW$%+ep~g3A8wr-P2uWXk`zm!Kx0cFLllWyYxAS5{bB_dx@%g4^ zd=Qt>m0|`d*bVRQL`r~M(TMgnCo~w5(SU+xd?wyX)kifVcpXF1C1I8+pcCd zyx>J&64Vn-khf!vI#GM-ZENuRPHh9V8kCGb*W0-y0$OxbkG!(ND9*kc=eiKDPc?S8 zgciy9QoV=jcOdD62Sa`O8f&u3+n(xs;`KmfB$My@CB8q#67q_S7zCZlY8x8yL#4*4 zIaL26LHJYnRW@JF@p}2YkUmavgMgmD*7rm>AkENZH~d1}b6PD`XjD{aGz>Sz8|qkVRvU-tT;W-;u5b3UFS59pce@M~R^SPS@)fW5TnwcL5x+tfWf-L%Y@P`6vrGEd^~yllp^bKgo0VLf ztZD4+OG+D^*T!VCr*Y7Fg~_!wpplik{lT4Og_&;waGv#>qU=vZTc ze^+&I8S6JFXER!P`}P=3ZOy!w*-K{oljg>{!mC-h^f&43zHo33-4TAP;ciVr@01ei zyRiv-uxhGLRH4U~AnZmdf!d=;3!pxboE-}id`stVopRJqq8e@O6#G2G$?=%{Y~Dv+ zJ9*rAi;u~l^vd7!a$B4ALuT)42JD%Ca{f1Cny)dxJ{ZdG;A1D<2L#jTgSw=x$>!Xa zwDANU$Ha9atyLV|`ivwiZ{war6T5A88{_aH5f3HZW~pq4R_#o6c3;2knP@nfg7`1Q z7}#HSCRPubXP@t?rmq^jwqCjS-G}UERH)N)IzJV!huyoD%><6Xx~k7Tt&7Zh2PE(2NH$Ku3l?Ym~0@3a~|dz(79sIHW{@xq=-n`C{vfy94;ua7V{hVdO0QW35a^Jq=ti8>7$jtfDF)Y6ylxc9ajbxS>01ujb(Z+Ron|DY?7EcCI*G+>LIR%eDbOX@@30V?IW zDY*S!wMdq)a7#9FQr3)q4#6-|8Jh-wcLbe0Ut`%CB+Ma$nx2I*ikSxrjq}xZGKj2vrg6=A#aj{f6>(-?KE-`K zyV|uql@A2tT|Jle_FiA3AXGGdCOFsQK2K26$KN{YZP6UQMqc(FLZ*`&j8v zkaq+FAMz(}BOkrDWpbcDbEK%9=sKUZzQ*_B4HU%A9BM`U=t3*8tKlL0X|^@nR!eZD zwW_iHB3C2NfHi_f#U)KzPDjJ2h&u)fqT$+BWq$K!knu?w5a)EAgU}R? znO-!W6=epc(7c{IZG#|}MlWBDcj93#FGWU+wl!TAW|P-Ct)ZJ~`T3;vYAow3I(N~G z^sWNMce~!hr+z2daf7wAUlUH8D&tm!Qxtk_44v0E#`fQb||N~Zq`+< zmgY%%i>MO6=Az`cdym-0bjmU7PG1E5nPVWGUX<3Fyk8q#t}ls+A8uJ@kwhPzbANum z)HU$FsDL!03y1@t4w1xU&U{#Sa{bf>MYF|QgnZz)5lxv5L|V;z`w&!dS{gqK`7T5c z#2tIuIc@m+$l0W#9$)2tF`iS~ip>yAT}+tW+CkFQ&!Hr(+;(FkVkSb37Y=|-KgCybC^@)?u0$MmySsbf~-!+QTj zq<(V&>p-p;9;W*Y$(L~LU{JPUJf5Y4mY%0;qbqvZV3_2$JPHq#PkpSjoQJgt_Ij)O zX;_;FF?sFjdCUzD>~Eqr`7X=UR{7sX;p@eGb&KdW8aqfftlc}f-rD2(*rm>)AOgf! zE4fQ0-VoI01j zAf7>Qp!qC|I$8VqoH!BF)|#_=XQ#=ReMizczU4RD0VKX z;3x2F%BFI9!ksgtC7EV&=f-EdRp2+tz=BNcyNKVJAi5dz$#y#nXvkG>snt#&Ez5PT zxj3`fR4wr_Husszr`q?nqwS=H{5yB!I42lL=2(>s7x66PU+K*KvCiaIMTbIGVN2yg zIpN#Nw-!2G+|)gpLUmuM?F*get;chNrZINfS07_D+1Y+tXEV-cznseyg8ixB_)COi zMXURo_3?!u+?3V&N>4Ap5Nt(wDT}tP`dP8?S*DqBIvl5&aZwa_F5WB-`^mG-gZcPu zvtn(xSl>!d7Ruq8F5~pTB=^rzPT9Tpqg3Me97xtpW(KN9?!4Bdms*?EEoHLTxy?ia z&|)Vx7V{I)V^Pse$0ZamwdTI0m)i_4qF#4TkiGsud%ix3^G~!2`oQoot=uW$!aL^- z$tQ-t|7P8HA{+M=hG8|?t*Ci)dw@=n`Ry$L*u+!;N8#AlcOxAW9OEjC!pJm4BaGYkw zMNz;t&tiOq$+OLa`S@(JV*SZlezKNzOZfj|Es0@?JK&Mu!FgADQ$#q=!yn}fY|9t; zt^B4<{l8#ab(FUBFPYUC6A{xB-{1+J$a-6h&wtoBo9XL!KSo)^tK7z^=f(F_M<$u* z<Pf5#_d=XLBs#`ZBw|h*$;&~4{H(?xY)tgYfcjR&K@5B|& j=D08#Dd3+eaNF+dRN=!8njEl_%ba6{%%$62cH{oPw~l9H literal 0 HcmV?d00001 diff --git "a/\345\210\206\345\270\203\345\274\217p2p\347\275\221\347\273\234\350\256\276\350\256\241.md" "b/\345\210\206\345\270\203\345\274\217p2p\347\275\221\347\273\234\350\256\276\350\256\241.md" new file mode 100644 index 0000000..bfb97bc --- /dev/null +++ "b/\345\210\206\345\270\203\345\274\217p2p\347\275\221\347\273\234\350\256\276\350\256\241.md" @@ -0,0 +1,462 @@ +# 基于QUIC协议构建P2P的Git网络 + +核心:发现节点,发现仓库,共享仓库,复制仓库,开源协作 + +尽量与git本身无关,只需要一个仓库标识,就可以下载仓库,并验证,无论上传者是否在线 + +## 架构设计 + +- **Identity层:**每个节点有一对长期密钥(Ed25519)。签名用于消息 refs、repo、node messages 签名验证。 +- **Transport层**:QUIC(使用 Rust 的 `quinn` 实现),或者Noise协议(Rust的`snow`实现,Noise协议只需要验证公钥就可以建立加密通信)。利用 QUIC 的 multiplexed streams 传输 git 数据,用 datagram 承载低价值的 gossip。 +- **Gossip 层**:实现 RepoMessage/ NodeMessage/EventMessage (三类),负责节点表和仓库路由表构造与更新。 + - Messages会缓存一段时间,重复消息被丢弃 + - **Repo Routing**:根据NodeMessage和RepoMessages创建仓库与节点的对应表。 + - 可以记录仓库的id,节点关系等 + - **Node Routing**:根据NodeMessage创建节点路由表 + - 可以记录节点的在线状态,优先级,地址等信息 +- **Git层**:实现基于 Git 协议的对象传输,或支持 `git packfile` 的二进制流传输。 +- **Relay层**:缓存RepoMessage/ RefsMessage / NodeMessage,缓存Repo仓库,推导Routing表。 +- **应用层**:连接Relay,下载Relay的Routing表,实现分享仓库,下载仓库,更新仓库,开源协作等功能。 + +image-20250922084407689 + +## 使用Gossip传播信息 + +1. 节点会把消息传播至邻居节点 +2. 邻居节点收到消息会再次转发 +3. 为避免无限传播,节点会丢弃重复收到的消息 +4. 节点通常会缓存一定量的消息,用于判断消息是否已经收到 + +## 一、节点 + +### 节点id + +`did:key:zQmW8QYFL8YRxq0QNqSLCVJvEoDuCgZQpL9FxT3p2ZbwU9o` + +**NodeId 表示形式**:采用 **Multibase Base58-btc** 编码: + +- 把 Ed25519 公钥原始字节前置 multicodec 标识(`0xED`,multicodec name: `ed25519-pub`)。 +- 对这个字节串做 multibase(base58btc) 编码,并在前面加上 `did:key:` 。 +- 示例最终形式: `did:key:z` + +即`"did:key:z" + base58(multicodec_prefix || pubkey_bytes)` + +节点id和公钥可以直接**互转** + +### 节点 + +``` +Node{ + NodeId, //节点ID,与其公钥可以互相转化 + Alias, //别名 + Vec
, //节点地址,可以有多个 + NodeType, //节点类型,normal/relay + Version, //版本u8 +} +``` + +一般来说,节点需要缓存收到的节点消息一定时间 + +一方面是为了不重复发送消息,另一方面是为了大致了解网络节点的状态 + +### 节点发现 + +当一个新节点第一次加入网络时,需要「引导(bootstrap)节点(也可以称之为seed/relay节点)」: + +1. 连接到Relay节点,接受Relay节点缓存的`Repo Routing Table`和`Node Routing Table` +2. 节点可以自己选择是否连接到其他Relay节点 +3. 可以周期广播自己的`NodeMessage`以便其他节点更新自己的在线状态 + +## 二、仓库 + +### P2P仓库 + +仓库id生成: + +`did:repo:zXXXXXXXXXX` + +``` +Repo{ + RepoId, + GitRootCommit, + Refs, + P2PDescription, + Path, //git仓库地址 +} + +//git仓库关联 +//git的首次提交记录 + 创建者公钥 -> p2p仓库id +RepoId = Multibase(Multihash( + GitRootCommit || CreatorPublicKey //一次性使用 +)) + +P2PDescription{ + "creator":"NodeId", + "name":"aaa", + "description":"bbb", + "timestamp":123 +} +``` + +### 本地仓库 + +需要建立本地仓库和P2P仓库的对应关系 + +RepoId -> .git的文件路径 + +这个关系存在本地 + +### P2P仓库地址 + +1.`git+p2p://{RepoId}` + +这个地址算是短地址,但是由于同一个仓库,可能有多个拷贝。 + +默认从Relay节点克隆仓库,默认选择长期在线节点,Relay需要跟原始地址保持一致数据 + +2.`git+p2p://{RepoId}/peer/{NodeId}` + +从某个节点下载某个仓库,但是一般节点可能无法直连,可以特指某个fork仓库 + +3. + +`mega://{RepoId}/peer/{NodeId}/refs/{RefName}` + +`mega://{RepoId}/peer/{NodeId}/path/{FilePath}` + +`mega://{RepoId}/peer/{NodeId}/raw/{GitHash}` + +找某个节点的Repo的文件路径 ,或者历史,或者提交信息等 + +### MonoRepo的情况 + +P2P设计是基于通用的Git来设计,还是根据Mega的Mono来设计? + +几个问题? + +1. 是否全网共用一个MonoRepo树?不是,每个client有自己的mono,其实跟本地git一样 +2. 还有RepoId的概念吗,是不是用地址就可以?有RepoId,也有path +3. 多个人提交同一个地址,会冲突吗?不会 + +回答: + +这个其实不用太考虑,看git用的哪个服务就行,如果是Mega,让Mega自己处理,如果是普通的git服务,让git服务自己处理。 + +## 三、消息Gossip Messages + +### 节点消息NodeMessage + +``` +NodeMessage{ + NodeId, //节点ID,与其公钥可以互相转化 + Alias, //别名 + Vec
, //节点地址,可以有多个 + NodeType, //节点类型,normal/relay + Version, //版本u8 +} +``` + +### 仓库消息RepoMessage + +``` +RepoMessage{ + NodeId, + RepoId, +} +``` + +### 协作消息ActivityMessage + +``` +ActivityMessage{ + NodeId, + RepoId, + Activity, +} +``` + +把消息包起来,并签名 + +``` +Message{ + NodeId, + Sign, + Enum +} +``` + + + +## 四、路由表 + +1. 将收到的RepoMessage和NodeMessage转化为: + +``` +struct NodeRouting{ + node_id: NodeId, + addresses: Vec
, + last_seen: SystemTime, + ttl: Duration, //超过 TTL(比如 24 小时)未刷新则删除 + score: f32 // 用于优先级 +} + +//Map存下Repo和node的对应关系 +RepoRouting: Map> +``` + +Client是否需要这个路由表? + +其实还有个功能,就是 本地 查询仓库和节点,我就可以本地直接查,就不用再去问relay了 + +## 五、传输层协议 + +### 如果是QUIC连接 + +#### 需要证书管理 + +Ca?Relay?如何管理根证书? + +#### Multiplexed Streams + +可靠传输机制,用于传输git pack流 + +#### Datagram + +不可靠传播机制,用于Gossip广播 + +#### 连接保持 + +Relay和普通节点需要 将NodeId 与 QUIC connection 绑定,方便广播和nat穿透 + +### 如果是Noise + +TLS协议过于笨重,证书管理过程需要自己处理 + +`Noise`协议基于DH 算法,创建会话密钥,是基于TCP的可靠传输 + +radicle,libp2p(可选)用的也是Noise + +### 如果公钥已知的情况下是否可以建立连接 + +可以自定义验证流程吗,因为双方公钥可以看作已知 + +## 六、Git数据传输 + +### 模仿git fetch + +1. 客户端连接Relay获得`NodeRouting`、`RepoRouting`和`Repo`的数据 +2. 客户端请求 Repo加Refs,默认从活跃度高的Relay请求 +3. Relay 通过git打包 package 传输到client +4. 校验package并暂存本地,等待后端自己处理 + +## 七、NAT穿透 + +目前**不直接提供**NAT穿透功能,Message由Relay进行转发 + +仓库复制,如果是内网节点,需要等Relay先复制,才能给其他节点复制 + +如果是公网节点,可以直接提供服务,但不需要像Relay一样缓存大量的仓库 + +## 八、仓库一致性 + +如果仓库被多次复制,和提交,仓库可能会分叉 + +类似 **Radicle 的设计**:`refs/nodes//heads`,**每个节点独立维护自己的 refs,不覆盖别人,最终由协作策略patch协作收敛。** + +如果Relay,收到同一个RepoId的不同refs,都保存,但是需要标明哪些是仓库代表`delegates`的refs。 + +思考: + +开源协作场景下,PR怎么提交,仓库怎么更新 + +Radicle有,去看一下 + +回答: + +Radicle使用的patch,类似pr,需要提交patch协作请求,由`delegate`下载到本地分支,合并到自己的分支并上传 + +## 九、开源协作 + +通过ActivityMessage去实现,全网广播 + +``` +ActivityMessage{ + NodeId, + RepoId, + Activity, +} +``` + +### 协作对象类型 + +| 类型名 | 含义 | github类比 | +| ------ | ---- | ---------- | +| Issue | 问题 | issue | +| Patch | 补丁 | PR | + +... + +### 协作对象事件 + +``` +Activity { + id: Hash, // 事件哈希 + parent: Hash, // 上一个事件id,根是repoId + type: Type // Issue/Patch/Comment + payload: Payload, // 事件内容 + author: PeerId, // 签名者 + signature: Signature, // Ed25519 签名 + timestamp: u64, +} +``` + +### 协作对象负载 + +#### Issue + +``` +enum IssueEventPayload { + Create { title: String, body: String }, + Comment { body: String }, + Close, + Reopen, + Edit { new_body: String }, +} +``` + +#### Patch + +``` +enum PatchEventPayload { + Create { base: CommitHash, head: CommitHash, title: String }, + Review { decision: ReviewDecision }, + Merge, + Close, +} +``` + +### 对象存储 + +目前只需要存储Event即可 + +### 分叉与合并 + +冲突自由复制数据类型(Conflict-free Replicated Data Type, CRDT),在网络延迟和离线的情况下,保证最终结果一致性 + +![image-20251011163655788](C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20251011163655788.png) + + + +在去中心化环境中,不同节点(Peers)对同一个 Event的操作可能是**异步传播**的,因此: + +当节点收到另一个分支时,它不会直接覆盖,而是: + +1. 验证操作签名; +2. 将该操作插入本地 DAG; +3. 调用合并逻辑来生成一个统一的 view(视图)。 + +### Patch(PR)的流程 + +#### 本地分支提交 + +``` +git checkout -b fix-bug +git commit -am "fix: solve race condition" +``` + +``` +git push rad HEAD:refs/patches +``` + +git提交的时候,会自动生成radicle patch对象并广播?怎么实现的?我在安装的时候,radicle改了git配置吗? + +#### 其他节点更新 + +``` +rad sync //同步seed的数据 +``` + +查看patch + +``` +rad patch list +``` + +更新patch到本地,会在本地更新一个branch + +``` +rad patch checkout e5f0a5a +``` + +更新master + +``` +git merge patch/e5f0a5a +``` + +推送 + +``` +git push rad master +``` + +### 节点和仓库的关系 + +一个仓库是对应多个节点(用户的),master分支应该是Delegate来维护 + +image-20251011173327160 + +## 十、本地存储 + +目前本地存储方案待定,Client和Relay + +信息存SQLITE + +Vercel 了解下 + +答: + +Vercel 更多的是托管页面,如果是后台,优先考虑docker部署 + +## 十一、开发计划 + +## 总结:实现建议阶段划分 + +| 模块/阶段 | 模块名称 | 功能 | 内容 | +| ---------------------- | ------------------ | ----------------- | ------------------------- | +| 身份认证+网络传输 | Identity+Transport | Identity + QUIC | 建立节点通信、公钥验证 | +| 节点管理+仓库管理+存储 | Storage | Node+Repo+Storage | 建立并保存 Node/Repo 信息 | +| 消息广播+仓库路由 | Gossip | Gossip + Routing | 广播消息,建立路由表 | +| git兼容 | Git | Git Sync | 支持git fetch操作 | +| 开源协作 | Collaboration | Issue+Patch | 支持 Issue/Patch 协作 | +| 部署 | Docker | Docker | 支持docker部署 | + +看一下RustVault简易版能不能用->libvault + +# 🦀 Rust 模块结构总览 + +``` +p2p-git/ +│ +├── main.rs +│ +├── identity/ # 身份与密钥层 +│ +├── transport/ # 网络传输层 (QUIC) +│ +├── storage/ # 本地持久化 (通用) SQLite +│ +├── gossip/ # Gossip 消息传播层 +│ +├── node/ # 节点服务 +│ +├── git/ # Git 对象传输层 +│ +├── activity/ # 协作层 (Issue / Patch ) +│ +├── cli/ # 应用层命令行 +│ +└── utils/ # 工具模块 + +``` + From 8cc9340b7421fe60788c64e56a36aa69724468f4 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Mon, 17 Nov 2025 19:40:42 +0800 Subject: [PATCH 02/42] Implement basic P2P functionality --- Cargo.lock | 4008 +++++++++++++++++ Cargo.toml | 4 + README.md | 233 + src/git/mod.rs | 36 + src/gossip/message.rs | 10 +- src/gossip/mod.rs | 6 +- src/gossip/service.rs | 218 + src/lib.rs | 1 + src/main.rs | 161 +- src/node/mod.rs | 1 + src/node/node.rs | 15 +- src/node/node_id.rs | 4 - src/node/node_manager.rs | 33 +- src/repo/mod.rs | 1 + src/repo/repo.rs | 7 +- src/repo/repo_id.rs | 16 +- src/repo/repo_manager.rs | 133 +- src/storage/mod.rs | 132 +- src/storage/node_model.rs | 117 + src/storage/repo_model.rs | 204 + src/transport/config.rs | 2 +- src/transport/quic.rs | 56 +- src/util/mod.rs | 9 +- test_output.txt | Bin 14670 -> 0 bytes tests/gossip_three_nodes.rs | 137 + ...21\347\273\234\350\256\276\350\256\241.md" | 462 -- 26 files changed, 5411 insertions(+), 595 deletions(-) create mode 100644 Cargo.lock create mode 100644 README.md create mode 100644 src/git/mod.rs create mode 100644 src/gossip/service.rs create mode 100644 src/storage/node_model.rs create mode 100644 src/storage/repo_model.rs delete mode 100644 test_output.txt create mode 100644 tests/gossip_three_nodes.rs delete mode 100644 "\345\210\206\345\270\203\345\274\217p2p\347\275\221\347\273\234\350\256\276\350\256\241.md" diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b69ac00 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4008 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b8ff6c09cd57b16da53641caa860168b88c172a5ee163b0288d3d6eea12786" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e44d16778acaf6a9ec9899b92cebd65580b83f685446bf2e1f5d3d732f99dcd" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base256emoji" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" +dependencies = [ + "const-str", + "match-lookup", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bigdecimal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.108", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec 0.5.2", + "constant_time_eq", +] + +[[package]] +name = "blake2s_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e461a7034e85b211a4acb57ee2e6730b32912b06c08cc242243c39fc21ae6a2" +dependencies = [ + "arrayref", + "arrayvec 0.5.2", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a076c298b9ecdb530ed9d967e74a6027d6a7478924520acddcddc24c1c8ab3ab" +dependencies = [ + "arrayref", + "byte-tools", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.9", +] + +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byte-tools" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes 1.10.1", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-str" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array 0.14.9", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "data-encoding-macro" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +dependencies = [ + "data-encoding", + "syn 2.0.108", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b072242a8cbaf9c145665af9d250c59af3b958f83ed6824e13533cf76d5b90" +dependencies = [ + "generic-array 0.9.1", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "fastbloom" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18c1ddb9231d8554c2d6bdf4cfaabf0c59251658c68b6c95cd52dd0c513a912a" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand 0.9.2", + "siphasher", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d00328cedcac5e81c683e5620ca6a30756fc23027ebf9bff405c0e8da1fbb7e" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "git2" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc" +dependencies = [ + "bitflags 1.3.2", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", +] + +[[package]] +name = "inherent" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libgit2-sys" +version = "0.14.2+1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libssh2-sys" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "match-lookup" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "megaengine" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "ed25519-dalek", + "git2", + "hex", + "multibase", + "multihash", + "quinn", + "rand_core 0.6.4", + "rcgen", + "rustls", + "rustls-pemfile", + "sea-orm", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multibase" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" +dependencies = [ + "base-x", + "base256emoji", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multihash" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26aea85740a1d3014a2a4fe75fc70c5061c96eadb9d0c934cd5cb6178a3dc810" +dependencies = [ + "blake2b_simd", + "blake2s_simd", + "bytes 0.5.6", + "sha1 0.5.0", + "sha2 0.7.1", + "tiny-keccak", + "unsigned-varint", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "3.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ouroboros" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ba07320d39dfea882faa70554b4bd342a5f273ed59ba7c1c6b4c840492c954" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.108", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes 1.10.1", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes 1.10.1", + "fastbloom", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes 1.10.1", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rsa" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust_decimal" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +dependencies = [ + "arrayvec 0.7.6", + "borsh", + "bytes 1.10.1", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "sea-orm" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8814e37dc25de54398ee62228323657520b7f29713b8e238649385dbe473ee0" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "futures", + "log", + "ouroboros", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-binder", + "serde", + "serde_json", + "sqlx", + "strum", + "thiserror 1.0.69", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-macros" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e115c6b078e013aa963cc2d38c196c2c40b05f03d0ac872fe06b6e0d5265603" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.108", + "unicode-ident", +] + +[[package]] +name = "sea-query" +version = "0.30.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4166a1e072292d46dc91f31617c2a1cdaf55a8be4b5c9f4bf2ba248e3ac4999b" +dependencies = [ + "bigdecimal", + "chrono", + "derivative", + "inherent", + "ordered-float", + "rust_decimal", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-binder" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36bbb68df92e820e4d5aeb17b4acd5cc8b5d18b2c36a4dd6f4626aabfa7ab1b9" +dependencies = [ + "bigdecimal", + "chrono", + "rust_decimal", + "sea-query", + "serde_json", + "sqlx", + "time", + "uuid", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "sha1" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171698ce4ec7cbb93babeb3190021b4d72e96ccb98e33d277ae4ea959d6f2d9e" + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb6be24e4c23a84d7184280d2722f7f2731fcdd4a9d886efbfe4413e4847ea0" +dependencies = [ + "block-buffer 0.3.3", + "byte-tools", + "digest 0.7.6", + "fake-simd", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +dependencies = [ + "ahash 0.8.12", + "atoi", + "bigdecimal", + "byteorder", + "bytes 1.10.1", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "native-tls", + "once_cell", + "paste", + "percent-encoding", + "rust_decimal", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "sqlformat", + "thiserror 1.0.69", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +dependencies = [ + "atoi", + "base64 0.21.7", + "bigdecimal", + "bitflags 2.10.0", + "byteorder", + "bytes 1.10.1", + "chrono", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array 0.14.9", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "rust_decimal", + "serde", + "sha1 0.10.6", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +dependencies = [ + "atoi", + "base64 0.21.7", + "bigdecimal", + "bitflags 2.10.0", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "num-bigint", + "once_cell", + "rand 0.8.5", + "rust_decimal", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "time", + "tracing", + "url", + "urlencoding", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8a021c69bb74a44ccedb824a046447e2c84a01df9e5c20779750acb38e11b2" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes 1.10.1", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "unsigned-varint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67332660eb59a6f1eb24ff1220c9e8d01738a8503c6002e30bcfe4bd9f2b4a9" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.108", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] diff --git a/Cargo.toml b/Cargo.toml index e1b15bd..cd858f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,7 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } clap = { version = "4.3", features = ["derive"] } rcgen = "0.13" +chrono = { version = "0.4", features = ["serde"] } +sea-orm = { version = "0.12", features = ["runtime-tokio-native-tls", "sqlx-sqlite"] } +sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "sqlite"] } +git2 = "0.16" diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a7c7f5 --- /dev/null +++ b/README.md @@ -0,0 +1,233 @@ +# MegaEngine - P2P Git Network + +MegaEngine is a distributed peer-to-peer (P2P) network for Git repositories. It enables nodes to discover, announce, and synchronize Git repositories across a decentralized network using the gossip protocol over QUIC transport. + +## 🎯 Features + +- **Decentralized Node Discovery**: Nodes automatically discover each other and exchange node information via gossip protocol +- **Repository Synchronization**: Nodes announce and sync repository inventory across the network +- **QUIC Transport**: Uses QUIC protocol for reliable, low-latency peer-to-peer communication +- **Gossip Protocol**: Implements epidemic message propagation with TTL and deduplication +- **Cryptographic Identity**: Each node has a unique EdDSA-based identity (`did:key` format) +- **SQLite Persistence**: Stores repositories and node information persistently +- **CLI Interface**: Easy-to-use command-line tool for managing nodes and repositories + +## 📦 Architecture + +``` +┌─────────────────────────────────────┐ +│ CLI Interface │ +│ (node start, repo add, auth init) │ +└──────────────┬──────────────────────┘ + │ +┌──────────────▼──────────────────────┐ +│ Node / Repository Manager │ +│ (NodeManager, RepoManager) │ +└──────────────┬──────────────────────┘ + │ +┌──────────────▼──────────────────────┐ +│ Gossip Protocol Service │ +│ (message relay, dedup, TTL) │ +└──────────────┬──────────────────────┘ + │ +┌──────────────▼──────────────────────┐ +│ QUIC Connection Manager │ +│ (peer connections, message send) │ +└──────────────┬──────────────────────┘ + │ +┌──────────────▼──────────────────────┐ +│ SQLite Storage / Sea-ORM │ +│ (repos, nodes persistence) │ +└─────────────────────────────────────┘ +``` + +## 🔧 Build & Setup + +### Prerequisites + +- Rust 1.70+ (2021 edition) +- Git +- OpenSSL development libraries (for TLS) + +### Build + +```bash +cargo build --release +``` + +### Configure Environment + +Set the root directory for MegaEngine data (default: `~/.megaengine`): + +```bash +export MEGAENGINE_ROOT=/path/to/megaengine-data +``` + +## 🚀 Usage + +### 1. Initialize Keypair + +Generate a new cryptographic keypair (EdDSA): + +```bash +cargo run -- auth init +``` + +Output: +``` +Keypair saved to /.megaengine/keypair.json +``` + +### 2. Start a Node + +Start a MegaEngine node that listens on a QUIC endpoint: + +```bash +cargo run -- node start \ + --alias my-node \ + --addr 0.0.0.0:9000 \ + --cert-path cert +``` + +The node will: +- Initialize QUIC server on the specified address +- Start gossip protocol for peer discovery +- Periodically announce node and repository information +- Listen indefinitely until Ctrl+C + +### 3. Get Node ID + +Display the node ID (based on your keypair): + +```bash +cargo run -- node id +``` + +Output: +``` +did:key:z2DXbAovGq5vNKpXVFyrhVLppMdUCmV1hCNjbUydLMEWasE +``` + +### 4. Register a Repository + +Add a local Git repository to the network: + +```bash +cargo run -- repo add \ + --path /path/to/git/repo \ + --description "My awesome repository" +``` + +The repo ID is automatically generated from the Git root commit hash and the node's public key. + +## 🧪 Testing + +Run the integration test for three-node gossip propagation: + +```bash +cargo test --test gossip_three_nodes -- --nocapture --test-threads=1 +``` + +This test: +1. Starts three nodes with QUIC servers +2. Connects them in a chain: node1 ↔ node2 ↔ node3 +3. Initiates gossip protocol on all nodes +4. Sends NodeAnnouncement and RepoAnnouncement messages +5. Verifies message propagation with TTL and deduplication + +Example output: +``` +Gossip: NodeAnnouncement from did:key:z2DZe... (alias: node1) +Gossip: RepoAnnouncement from did:key:z2DW... with 1 repos: ["did:repo:zW1i..."] +``` + +## 📁 Project Structure + +``` +src/ +├── main.rs # CLI entry point +├── lib.rs # Library root +├── identity/ +│ └── keypair.rs # EdDSA keypair generation and signing +├── node/ +│ ├── node.rs # Node runtime with QUIC manager +│ ├── node_id.rs # Node identifier (did:key format) +│ ├── node_manager.rs # Node routing and lifecycle +│ └── node.rs +├── repo/ +│ ├── repo.rs # Repository metadata and refs +│ ├── repo_id.rs # Repository identifier (did:repo format) +│ └── repo_manager.rs # Repository persistence and query +├── storage/ +│ ├── mod.rs # Database initialization and connection +│ ├── repo_model.rs # Sea-ORM models and CRUD for repos +│ └── node_model.rs # Sea-ORM models and CRUD for nodes +├── transport/ +│ ├── quic.rs # QUIC connection manager +│ ├── config.rs # QUIC configuration +│ └── cert.rs # TLS certificate generation +├── gossip/ +│ ├── mod.rs # Gossip service exports +│ ├── message.rs # Gossip message types and signing +│ └── service.rs # Gossip protocol implementation +├── git/ +│ └── mod.rs # Git repository utilities +└── util/ + └── mod.rs # Timestamp and utility functions + +tests/ +└── gossip_three_nodes.rs # Integration test for gossip relay +``` + +## 🔐 Data Formats + +### Node ID (did:key) + +``` +did:key:z2DSQWVWxVg2Dq8qvq7TqJG75gY2hh9cT6RkzzgYpf7YptF + ↑ ↑ ↑ + | | Ed25519 public key (base58 encoded) + | Multibase encoding + DID scheme +``` + +### Repository ID (did:repo) + +``` +did:repo:zW1iF5iwCChifAcjZUrDbwD9o8LS76kFsz6bTZFEJhEqVCU + ↑ ↑ + | SHA3-256(root_commit + creator_pubkey) + Multibase encoding +``` + +## 📊 Gossip Protocol + +- **Message Types**: + - `NodeAnnouncement`: Advertises node metadata (alias, addresses, type) + - `RepoAnnouncement`: Lists repositories owned by a node + +- **TTL (Time-to-Live)**: Default 4 hops, decremented on each relay +- **Deduplication**: Tracks seen message hashes in a 5-minute sliding window +- **Broadcast Interval**: 10 seconds + +## 💾 Storage + +Data is persisted in SQLite at `$MEGAENGINE_ROOT/megaengine.db`: + +### Tables + +- **repos**: Repository metadata (id, name, creator, description, path, refs, timestamps) +- **nodes**: Node information (id, alias, addresses, node_type, version, timestamps) + +## 🔧 Configuration + +### Environment Variables + +- `MEGAENGINE_ROOT`: Root directory for data storage (default: `~/.megaengine`) +- `RUST_LOG`: Logging level (e.g., `megaengine=debug`) + +### Default Ports + +- QUIC Server: `0.0.0.0:9000` (configurable via `--addr`) + + diff --git a/src/git/mod.rs b/src/git/mod.rs new file mode 100644 index 0000000..ed7d752 --- /dev/null +++ b/src/git/mod.rs @@ -0,0 +1,36 @@ +use anyhow::Result; +use git2::{Repository, Sort}; + +pub fn repo_root_commit_bytes(path: &str) -> Result> { + let repo = + Repository::open(path).map_err(|e| anyhow::anyhow!("failed to open git repo: {}", e))?; + let mut revwalk = repo + .revwalk() + .map_err(|e| anyhow::anyhow!("revwalk error: {}", e))?; + revwalk + .push_head() + .map_err(|e| anyhow::anyhow!("push_head failed: {}", e))?; + let _ = revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::REVERSE); + + if let Some(entry) = revwalk.next() { + let oid = entry.map_err(|e| anyhow::anyhow!("revwalk entry error: {}", e))?; + return Ok(oid.as_bytes().to_vec()); + } + + Err(anyhow::anyhow!("no commits found in repo")) +} + +pub fn repo_name_space(path: &str) -> String { + let repo = match Repository::open(path) { + Ok(repo) => repo, + Err(_) => { + return "".to_string(); + } + }; + let path = repo.path(); + + if let Some(name) = path.parent().and_then(|p| p.file_name()) { + return name.to_string_lossy().to_string(); + } + "".to_string() +} diff --git a/src/gossip/message.rs b/src/gossip/message.rs index 31eaaa6..829fed9 100644 --- a/src/gossip/message.rs +++ b/src/gossip/message.rs @@ -1,7 +1,7 @@ -use anyhow::{anyhow, Result}; +use anyhow::Result; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use std::{net::SocketAddr, time}; +use std::net::SocketAddr; use crate::{ node::{ @@ -56,7 +56,7 @@ pub struct RepoAnnouncement { pub struct SignedMessage { pub node_id: NodeId, pub message: GossipMessage, - pub timestamp: u64, + pub timestamp: i64, pub signature: String, } @@ -80,7 +80,7 @@ impl SignedMessage { pub fn new_repo_sign_message(repos: Vec, node: Node) -> Result { let message = GossipMessage::RepoAnnouncement(RepoAnnouncement { node_id: node.node_id().clone(), - repos: repos, + repos, }); let mut sign_message = SignedMessage { @@ -105,7 +105,7 @@ impl SignedMessage { } /// 获取消息的时间戳 - pub fn timestamp(&self) -> u64 { + pub fn timestamp(&self) -> i64 { self.timestamp } diff --git a/src/gossip/mod.rs b/src/gossip/mod.rs index e935b02..6d0c358 100644 --- a/src/gossip/mod.rs +++ b/src/gossip/mod.rs @@ -1 +1,5 @@ -mod message; +pub mod message; +mod service; + +pub use message::SignedMessage; +pub use service::GossipService; diff --git a/src/gossip/service.rs b/src/gossip/service.rs new file mode 100644 index 0000000..01fb4ba --- /dev/null +++ b/src/gossip/service.rs @@ -0,0 +1,218 @@ +use crate::gossip::message::{GossipMessage, SignedMessage}; +use crate::node::node::Node; +use crate::node::node_id::NodeId; +use crate::repo::repo_manager::RepoManager; +use crate::transport::quic::ConnectionManager; +use anyhow::Result; +use ed25519_dalek::Signature; +use hex; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::convert::TryInto; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::sync::{mpsc, Mutex}; + +const DEFAULT_TTL: u8 = 16; + +/// 简单的 gossip 服务:接收来自 QUIC 的消息,去重、验签、处理并转发给邻居 +#[allow(dead_code)] +pub struct GossipService { + manager: Arc>, + node: Node, + repo_manager: Option>>, + seen: Arc>>, +} + +impl GossipService { + pub fn new( + manager: Arc>, + node: Node, + repo_manager: Option>>, + ) -> Self { + Self { + manager, + node, + repo_manager, + seen: Arc::new(Mutex::new(HashMap::new())), + } + } + + /// Start the gossip service: register incoming channel and spawn handler + periodic broadcaster + pub async fn start(self: Arc) -> Result<()> { + let (tx, mut rx) = mpsc::channel::<(NodeId, Vec)>(256); + + // register incoming sender with connection manager + { + let mgr = self.manager.lock().await; + mgr.register_incoming_sender(tx).await; + } + + // clone for handler task + let s = Arc::clone(&self); + tokio::spawn(async move { + while let Some((from, data)) = rx.recv().await { + let _ = s.handle_incoming(from, data).await; + } + }); + + // periodic broadcaster: node announcement (and repo announcement if available) + let s2 = Arc::clone(&self); + tokio::spawn(async move { + loop { + #[derive(Serialize, Deserialize, Clone)] + struct Envelope { + payload: SignedMessage, + ttl: u8, + } + + // 1. 发送 NodeAnnouncement + if let Ok(signed) = SignedMessage::new_node_sign_message(s2.node.clone()) { + let env = Envelope { + payload: signed, + ttl: DEFAULT_TTL, + }; + let data = serde_json::to_vec(&env).unwrap_or_default(); + let mgr = s2.manager.lock().await; + let peers = mgr.list_peers().await; + for peer in peers { + let _ = mgr.send_message(peer.clone(), data.clone()).await; + } + } + + // 2. 发送 RepoAnnouncement(从本地 storage 加载 repo 列表) + if let Ok(repos) = crate::storage::repo_model::list_repos().await { + let repo_ids: Vec<_> = repos + .iter() + .filter_map(|r| { + crate::repo::repo_id::RepoId::parse_from_str(&r.repo_id).ok() + }) + .collect(); + if !repo_ids.is_empty() { + if let Ok(signed) = + SignedMessage::new_repo_sign_message(repo_ids, s2.node.clone()) + { + let env = Envelope { + payload: signed, + ttl: DEFAULT_TTL, + }; + let data = serde_json::to_vec(&env).unwrap_or_default(); + let mgr = s2.manager.lock().await; + let peers = mgr.list_peers().await; + for peer in peers { + let _ = mgr.send_message(peer.clone(), data.clone()).await; + } + } + } + } + + tokio::time::sleep(Duration::from_secs(10)).await; + } + }); + + // spawn a cleanup task for seen map + let seen = Arc::clone(&self.seen); + tokio::spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(30)).await; + let mut guard = seen.lock().await; + let now = Instant::now(); + guard.retain(|_, &mut v| v + Duration::from_secs(300) > now); + } + }); + + Ok(()) + } + + async fn handle_incoming(&self, from: NodeId, data: Vec) -> Result<()> { + // Try parse as Envelope (with ttl). If not, fall back to raw SignedMessage. + #[derive(Serialize, Deserialize, Clone)] + struct Envelope { + payload: SignedMessage, + ttl: u8, + } + + let (signed, mut ttl) = if let Ok(env) = serde_json::from_slice::(&data) { + (env.payload, env.ttl) + } else if let Ok(s) = serde_json::from_slice::(&data) { + (s, DEFAULT_TTL) + } else { + return Ok(()); + }; + + let id = hex::encode(signed.self_hash()); + + // dedup + { + let mut seen = self.seen.lock().await; + if seen.contains_key(&id) { + return Ok(()); + } + seen.insert(id.clone(), Instant::now()); + } + + // verify signature using sender's NodeId -> verifying key + if let Ok(kp) = signed.node_id.to_keypair() { + let sig_bytes = hex::decode(&signed.signature).unwrap_or_default(); + let arr: [u8; 64] = match sig_bytes.as_slice().try_into() { + Ok(a) => a, + Err(_) => return Ok(()), + }; + let sig = Signature::from_bytes(&arr); + if !kp.verify(&signed.self_hash(), &sig) { + tracing::warn!( + "signature verification failed for message from {}", + signed.node_id + ); + return Ok(()); + } + } + + // process message (borrow the inner message to avoid moving) + match &signed.message { + GossipMessage::NodeAnnouncement(na) => { + tracing::info!( + "Gossip: NodeAnnouncement from {} (alias: {})", + na.node_id, + na.alias + ); + // TODO: update NodeManager or Node routing table + } + GossipMessage::RepoAnnouncement(ra) => { + tracing::info!( + "Gossip: RepoAnnouncement from {} with {} repos: {:?}", + ra.node_id, + ra.repos.len(), + ra.repos.iter().map(|r| r.as_str()).collect::>() + ); + // 可选:记录该节点拥有的 repo 信息到本地数据库(用于搜索/发现) + // 这里可以保存到一个 peer_repos 表用于跟踪哪个节点有哪些 repo + } + } + + // forward if ttl > 0 + if ttl > 0 { + ttl -= 1; + #[derive(Serialize, Deserialize, Clone)] + struct Envelope2 { + payload: SignedMessage, + ttl: u8, + } + let fwd = Envelope2 { + payload: signed.clone(), + ttl, + }; + let data = serde_json::to_vec(&fwd).unwrap_or_default(); + let mgr = self.manager.lock().await; + let peers = mgr.list_peers().await; + for peer in peers { + if peer == from { + continue; + } + let _ = mgr.send_message(peer.clone(), data.clone()).await; + } + } + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 69f0a7c..47f8c9f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod git; pub mod gossip; pub mod identity; pub mod node; diff --git a/src/main.rs b/src/main.rs index ae9bdd7..0743d96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,12 +2,23 @@ use anyhow::Result; use clap::{Parser, Subcommand}; use std::net::SocketAddr; -use megaengine::storage; +use megaengine::gossip::GossipService; +use megaengine::{ + git::{repo_name_space, repo_root_commit_bytes}, + node::node_id::NodeId, + repo::{self, repo_id::RepoId}, + storage, + util::timestamp_now, +}; #[derive(Parser)] #[command(name = "megaengine")] #[command(about = "MegaEngine P2P Git", long_about = None)] struct Cli { + /// Root data directory (overrides $MEGAENGINE_ROOT). Defaults to ~/.megaengine + #[arg(long, global = true, default_value = "~/.megaengine")] + root: String, + #[command(subcommand)] command: Commands, } @@ -24,6 +35,11 @@ enum Commands { #[command(subcommand)] action: NodeAction, }, + /// Repo related commands + Repo { + #[command(subcommand)] + action: RepoAction, + }, } #[derive(Subcommand)] @@ -46,27 +62,66 @@ enum NodeAction { #[arg(short, long, default_value = "cert")] cert_path: String, }, + /// Print node id using stored keypair + Id, +} + +#[derive(Subcommand)] +enum RepoAction { + /// Add a repository record to the manager and database + Add { + /// Local path to the repository + #[arg(long)] + path: String, + + /// Description + #[arg(long, default_value = "")] + description: String, + }, } #[tokio::main] async fn main() -> Result<()> { - // Initialize rustls crypto provider let _ = rustls::crypto::ring::default_provider().install_default(); - - // init logging tracing_subscriber::fmt() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive("megaengine=info".parse().unwrap()), + ) .init(); let cli = Cli::parse(); + let root_path = if let Ok(env_root) = std::env::var("MEGAENGINE_ROOT") { + env_root + } else { + let path = if cli.root.starts_with("~/") { + let home = std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) + .unwrap_or_else(|_| ".".to_string()); + cli.root.replace("~", &home) + } else { + cli.root.clone() + }; + std::env::set_var("MEGAENGINE_ROOT", &path); + path + }; + match cli.command { Commands::Auth { action } => match action { AuthAction::Init => { - tracing::info!("Generating new keypair..."); - let kp = megaengine::identity::keypair::KeyPair::generate()?; - storage::save_keypair(&kp)?; - tracing::info!("Keypair saved to {:?}", storage::keypair_path()); + let kp_path = storage::keypair_path(); + if kp_path.exists() { + tracing::info!( + "Keypair already exists at {:?}; skipping generation", + kp_path + ); + } else { + tracing::info!("Generating new keypair..."); + let kp = megaengine::identity::keypair::KeyPair::generate()?; + storage::save_keypair(&kp)?; + tracing::info!("Keypair saved to {:?}", storage::keypair_path()); + } } }, Commands::Node { action } => match action { @@ -76,9 +131,7 @@ async fn main() -> Result<()> { cert_path, } => { tracing::info!("Starting node..."); - - // Ensure certificates exist, generate if needed - let cert_dir = &cert_path; + let cert_dir = format!("{}/{}", &root_path, cert_path); megaengine::transport::cert::ensure_certificates( &format!("{}/cert.pem", cert_dir), &format!("{}/key.pem", cert_dir), @@ -94,9 +147,7 @@ async fn main() -> Result<()> { } }; - // parse addresses - let mut addrs: Vec = Vec::new(); - addrs.push(addr.parse()?); + let addrs: Vec = vec![addr.parse()?]; let mut node = megaengine::node::node::Node::from_keypair( &kp, @@ -110,7 +161,6 @@ async fn main() -> Result<()> { node.node_id().0 ); - // Create QUIC config for this node let quic_config = megaengine::transport::config::QuicConfig::new( addr.parse()?, format!("{}/cert.pem", cert_dir), @@ -118,9 +168,19 @@ async fn main() -> Result<()> { format!("{}/ca-cert.pem", cert_dir), ); - // Start QUIC server and keep it running tracing::info!("Starting QUIC server on {}...", addr); node.start_quic_server(quic_config).await?; + if let Some(conn_mgr) = &node.connection_manager { + let gossip = std::sync::Arc::new(GossipService::new( + std::sync::Arc::clone(conn_mgr), + node.clone(), + None, + )); + tokio::spawn(gossip.start()); + tracing::info!("Gossip protocol started"); + } else { + tracing::warn!("No connection manager found, gossip not started"); + } println!( "Node started successfully: {} ({})", @@ -130,12 +190,77 @@ async fn main() -> Result<()> { println!("Listening on: {}", addr); println!("Press Ctrl+C to stop"); - // Keep the node running indefinitely loop { tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; } } + NodeAction::Id => { + let kp = match storage::load_keypair() { + Ok(k) => k, + Err(e) => { + tracing::error!("failed to load keypair: {}", e); + tracing::info!("Run `auth init` first to generate keys"); + return Ok(()); + } + }; + + let node_id = NodeId::from_keypair(&kp); + println!("{}", node_id); + } }, + Commands::Repo { action } => { + match action { + RepoAction::Add { path, description } => { + let kp = match storage::load_keypair() { + Ok(k) => k, + Err(e) => { + tracing::error!("failed to load keypair: {}", e); + tracing::info!("Run `auth init` first to generate keys"); + return Ok(()); + } + }; + let node_id = NodeId::from_keypair(&kp); + + let root_bytes = match repo_root_commit_bytes(&path) { + Ok(b) => b, + Err(e) => { + tracing::error!("failed to read repo root commit: {}", e); + println!("Ensure the provided path is a git repository with at least one commit"); + return Ok(()); + } + }; + + let repo_id = + match RepoId::generate(root_bytes.as_slice(), &kp.verifying_key_bytes()) { + Ok(id) => id, + Err(e) => { + tracing::error!("Failed to generate RepoId: {}", e); + return Ok(()); + } + }; + + let name = repo_name_space(&path); + let desc = repo::repo::P2PDescription { + creator: node_id.to_string(), + name: name.clone(), + description: description.clone(), + timestamp: timestamp_now(), + }; + + let repo = repo::repo::Repo::new( + repo_id.to_string(), + desc, + std::path::PathBuf::from(path), + ); + + let mut manager = repo::repo_manager::RepoManager::new(); + match manager.register_repo(repo).await { + Ok(_) => tracing::info!("Repo {} added", repo_id), + Err(e) => tracing::info!("Failed to add repo: {}", e), + } + } + } + } } Ok(()) diff --git a/src/node/mod.rs b/src/node/mod.rs index 265e176..4cd3336 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -1,3 +1,4 @@ +#![allow(clippy::module_inception)] pub mod node; pub mod node_id; pub mod node_manager; diff --git a/src/node/node.rs b/src/node/node.rs index 23325a0..665298c 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -5,7 +5,9 @@ use crate::transport::quic::ConnectionManager; use anyhow::Result; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; +use std::sync::Arc; use std::time::{Duration, SystemTime}; +use tokio::sync::Mutex; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum NodeType { @@ -21,14 +23,14 @@ pub struct NodeInfo { pub addresses: Vec, pub node_type: NodeType, pub version: u8, - pub keypair: KeyPair, } /// 运行时节点对象,包含网络管理器 #[derive(Clone)] pub struct Node { pub info: NodeInfo, - pub connection_manager: Option>>, + pub connection_manager: Option>>, + pub keypair: KeyPair, } impl std::fmt::Debug for Node { @@ -54,11 +56,11 @@ impl Node { addresses, node_type, version: 1, - keypair, }; Self { info, connection_manager: None, + keypair, } } @@ -73,10 +75,7 @@ impl Node { } pub fn sign_message(&self, msg: &[u8]) -> Result> { - self.info - .keypair - .sign(msg) - .map(|sig| sig.to_bytes().to_vec()) + self.keypair.sign(msg).map(|sig| sig.to_bytes().to_vec()) } /// 启动 QUIC 服务端 @@ -108,7 +107,7 @@ impl Node { } pub fn keypair(&self) -> &KeyPair { - &self.info.keypair + &self.keypair } } diff --git a/src/node/node_id.rs b/src/node/node_id.rs index ecdae35..87d21b0 100644 --- a/src/node/node_id.rs +++ b/src/node/node_id.rs @@ -73,10 +73,6 @@ impl NodeId { &self.0 } - pub fn to_string(&self) -> String { - self.0.clone() - } - pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() } diff --git a/src/node/node_manager.rs b/src/node/node_manager.rs index 13321c7..de11595 100644 --- a/src/node/node_manager.rs +++ b/src/node/node_manager.rs @@ -16,9 +16,12 @@ impl NodeManager { } } - pub fn insert_node(&mut self, node: &Node) { + pub async fn insert_node(&mut self, node: &Node) { let routing = NodeRouting::new(node.node_id().clone(), node.addresses().to_vec()); self.nodes.insert(node.node_id().clone(), routing); + + // 持久化 NodeInfo + let _ = crate::storage::node_model::save_node_info_to_db(&node.info).await; } pub fn mark_alive(&mut self, node_id: &NodeId) { @@ -46,7 +49,7 @@ impl NodeManager { #[cfg(test)] mod tests { use super::*; - use crate::identity::keypair::{self, KeyPair}; + use crate::identity::keypair::KeyPair; use crate::node::node::NodeType; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; @@ -63,13 +66,13 @@ mod tests { Node::new(node_id, alias, addresses, node_type, keypair.clone()) } - #[test] - fn test_node_manager_insert_node() { + #[tokio::test] + async fn test_node_manager_insert_node() { let mut manager = NodeManager::new(); let node = create_sample_node(); // Insert the node - manager.insert_node(&node); + manager.insert_node(&node).await; // Assert that the node is in the manager assert_eq!(manager.nodes.len(), 1); @@ -78,13 +81,13 @@ mod tests { assert_eq!(node_routing.unwrap().node_id, *node.node_id()); } - #[test] - fn test_node_manager_mark_alive() { + #[tokio::test] + async fn test_node_manager_mark_alive() { let mut manager = NodeManager::new(); let node = create_sample_node(); // Insert the node - manager.insert_node(&node); + manager.insert_node(&node).await; // Get the initial last_seen time let initial_last_seen = manager.get_node(&node.node_id()).unwrap().last_seen; @@ -97,12 +100,12 @@ mod tests { assert_ne!(initial_last_seen, refreshed_last_seen); } - #[test] - fn test_node_manager_cleanup_expired() { + #[tokio::test] + async fn test_node_manager_cleanup_expired() { let mut manager = NodeManager::new(); let node = create_sample_node(); - manager.insert_node(&node); + manager.insert_node(&node).await; assert_eq!(manager.nodes.len(), 1); manager.nodes.get_mut(&node.node_id()).unwrap().ttl = std::time::Duration::from_secs(1); @@ -111,14 +114,14 @@ mod tests { assert_eq!(manager.nodes.len(), 0); } - #[test] - fn test_node_manager_routing_print() { + #[tokio::test] + async fn test_node_manager_routing_print() { let mut manager = NodeManager::new(); let node1 = create_sample_node(); let node2 = create_sample_node(); - manager.insert_node(&node1); - manager.insert_node(&node2); + manager.insert_node(&node1).await; + manager.insert_node(&node2).await; let _ = std::panic::catch_unwind(|| { manager.routing_print(); diff --git a/src/repo/mod.rs b/src/repo/mod.rs index 994f7cc..ade3bd0 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -1,3 +1,4 @@ +#![allow(clippy::module_inception)] pub mod repo; pub mod repo_id; pub mod repo_manager; diff --git a/src/repo/repo.rs b/src/repo/repo.rs index 2a65981..826b4e2 100644 --- a/src/repo/repo.rs +++ b/src/repo/repo.rs @@ -1,15 +1,14 @@ use serde::{Deserialize, Serialize}; -use serde_json::Map; use std::collections::HashMap; use std::path::PathBuf; /// P2P 仓库描述 #[derive(Clone, Debug, Serialize, Deserialize)] pub struct P2PDescription { - pub creator: String, // NodeId + pub creator: String, pub name: String, pub description: String, - pub timestamp: u64, // Unix timestamp + pub timestamp: i64, } /// P2P 仓库 @@ -18,7 +17,7 @@ pub struct Repo { pub repo_id: String, pub refs: HashMap, pub p2p_description: P2PDescription, - pub path: PathBuf, // Git 仓库本地路径 + pub path: PathBuf, } impl Repo { diff --git a/src/repo/repo_id.rs b/src/repo/repo_id.rs index 9947b8b..bd7cf34 100644 --- a/src/repo/repo_id.rs +++ b/src/repo/repo_id.rs @@ -24,7 +24,7 @@ impl RepoId { ))) } - pub fn from_str(repo_id: &str) -> Result { + pub fn parse_from_str(repo_id: &str) -> Result { if !repo_id.starts_with(REPO_KEY_PREFIX) { return Err(anyhow!("invalid NodeId prefix")); } @@ -57,7 +57,7 @@ impl FromStr for RepoId { type Err = ParseNodeIdError; fn from_str(s: &str) -> Result { - RepoId::from_str(s).map_err(|_| ParseNodeIdError) + RepoId::parse_from_str(s).map_err(|_| ParseNodeIdError) } } @@ -98,7 +98,7 @@ mod tests { let repo_id = RepoId::generate(root_commit, keypair.verifying_key.as_bytes())?; // 从生成的字符串解析回 RepoId - let parsed_repo_id = RepoId::from_str(&repo_id.0)?; + let parsed_repo_id = RepoId::parse_from_str(&repo_id.0)?; // 验证解析出来的 RepoId 与原始 RepoId 是否相同 assert_eq!(repo_id, parsed_repo_id); @@ -110,23 +110,21 @@ mod tests { #[test] fn test_from_string_invalid_prefix() { let invalid_repo_id = "invalid:repo_id"; - let result = RepoId::from_str(invalid_repo_id); + let result = RepoId::parse_from_str(invalid_repo_id); assert!(result.is_err()); } - // 测试 RepoId 解析的错误情况:空的编码部分 #[test] fn test_from_string_empty_encoded_part() { let invalid_repo_id = "did:repo:"; - let result = RepoId::from_str(invalid_repo_id); + let result = RepoId::parse_from_str(invalid_repo_id); assert!(result.is_err()); } - // 测试 RepoId 解析的错误情况:无效的 Base58 格式 #[test] fn test_from_string_invalid_base_format() { - let invalid_repo_id = "did:repo:xyz123"; // 假设这是一个错误的 Base 格式 - let result = RepoId::from_str(invalid_repo_id); + let invalid_repo_id = "did:repo:xyz123"; + let result = RepoId::parse_from_str(invalid_repo_id); assert!(result.is_err()); } diff --git a/src/repo/repo_manager.rs b/src/repo/repo_manager.rs index 93b1138..aba3d98 100644 --- a/src/repo/repo_manager.rs +++ b/src/repo/repo_manager.rs @@ -1,73 +1,79 @@ -use std::collections::HashMap; use std::path::PathBuf; use crate::repo::repo::Repo; +use crate::storage::repo_model::{ + delete_repo_from_db, list_repos, load_repo_from_db, save_repo_to_db, +}; +use anyhow::Result; /// 仓库管理器 -/// 管理本地仓库和 P2P 仓库的对应关系 -pub struct RepoManager { - // RepoId -> Repo 映射 - repos: HashMap, - // 本地路径 -> RepoId 映射 - path_to_repo_id: HashMap, -} +/// 管理本地仓库和 P2P 仓库的对应关系,并支持数据库持久化 +pub struct RepoManager {} impl RepoManager { /// 创建新的仓库管理器 pub fn new() -> Self { - RepoManager { - repos: HashMap::new(), - path_to_repo_id: HashMap::new(), - } + RepoManager {} } /// 注册仓库 - pub fn register_repo(&mut self, repo: Repo) -> Result<(), String> { - let repo_id = repo.repo_id.clone(); - let path = repo.path.clone(); - - if self.repos.contains_key(&repo_id) { - return Err(format!("Repository {} already exists", repo_id)); - } + pub async fn register_repo(&mut self, repo: Repo) -> Result<(), String> { + save_repo_to_db(&repo).await.map_err(|e| e.to_string())?; - self.repos.insert(repo_id.clone(), repo); - self.path_to_repo_id.insert(path, repo_id); Ok(()) } /// 根据 RepoId 获取仓库 - pub fn get_repo(&self, repo_id: &str) -> Option<&Repo> { - self.repos.get(repo_id) - } - - /// 根据 RepoId 获取仓库(可变) - pub fn get_repo_mut(&mut self, repo_id: &str) -> Option<&mut Repo> { - self.repos.get_mut(repo_id) + pub async fn get_repo(&self, repo_id: &str) -> Result> { + let repo = load_repo_from_db(repo_id).await?; + Ok(repo) } /// 根据路径获取仓库 ID - pub fn get_repo_id_by_path(&self, path: &PathBuf) -> Option<&String> { - self.path_to_repo_id.get(path) + pub async fn get_repo_id_by_path(&self, path: &PathBuf) -> Result> { + // 回退到数据库查询 + let repos = list_repos().await?; + for repo in repos { + if &repo.path == path { + return Ok(Some(repo.repo_id)); + } + } + Ok(None) } /// 删除仓库 - pub fn remove_repo(&mut self, repo_id: &str) -> Option { - if let Some(repo) = self.repos.remove(repo_id) { - self.path_to_repo_id.remove(&repo.path); - Some(repo) + pub async fn remove_repo(&mut self, repo_id: &str) -> Result> { + // 先从数据库加载 repo,返回给调用方;再删除数据库记录 + if let Some(repo) = load_repo_from_db(repo_id).await? { + // 删除数据库记录 + delete_repo_from_db(repo_id).await?; + + Ok(Some(repo)) } else { - None + Ok(None) } } /// 列出所有仓库 - pub fn list_repos(&self) -> Vec<&Repo> { - self.repos.values().collect() + pub async fn list_repos(&self) -> Result> { + let repos = list_repos().await?; + Ok(repos) } /// 获取仓库数量 - pub fn repo_count(&self) -> usize { - self.repos.len() + pub async fn repo_count(&self) -> Result { + let repos = list_repos().await?; + Ok(repos.len()) + } + + /// 更新 Repo 的 refs(会自动持久化到数据库) + pub async fn update_repo(&mut self, repo: Repo) -> Result<()> { + if (load_repo_from_db(repo.repo_id.as_str()).await?).is_some() { + save_repo_to_db(&repo).await?; + Ok(()) + } else { + Err(anyhow::anyhow!("Repository {} not found", repo.repo_id)) + } } } @@ -83,8 +89,8 @@ mod tests { use super::*; - #[test] - fn test_repo_manager() { + #[tokio::test] + async fn test_repo_manager() -> Result<()> { let mut manager = RepoManager::new(); let desc = P2PDescription { @@ -100,8 +106,47 @@ mod tests { PathBuf::from("/tmp/test-repo"), ); - assert!(manager.register_repo(repo).is_ok()); - assert_eq!(manager.repo_count(), 1); - assert!(manager.get_repo("did:repo:test").is_some()); + let before = manager.repo_count().await?; + assert!(manager.register_repo(repo).await.is_ok()); + let count = manager.repo_count().await?; + // 可能存在旧数据,确保数量不减少并且能通过 id 读取到刚注册的 repo + assert!(count >= before); + let loaded = manager.get_repo("did:repo:test").await?; + assert!(loaded.is_some()); + Ok(()) + } + + #[tokio::test] + async fn test_repo_manager_with_persistence() -> Result<()> { + // 持久化现在为默认行为 + let mut manager = RepoManager::new(); + + let desc = P2PDescription { + creator: "did:key:test".to_string(), + name: "test-repo-persist".to_string(), + description: "A test repository with persistence".to_string(), + timestamp: 2000, + }; + + let repo = Repo::new( + "did:repo:test-persist".to_string(), + desc, + PathBuf::from("/tmp/test-repo-persist"), + ); + + let before = manager.repo_count().await?; + assert!(manager.register_repo(repo).await.is_ok()); + let count = manager.repo_count().await?; + assert!(count >= before); + + // 删除仓库 + let result = manager.remove_repo("did:repo:test-persist").await; + assert!(result.is_ok()); + assert!(result.unwrap().is_some()); + // 验证数据库中已删除该 repo + let loaded_after = manager.get_repo("did:repo:test-persist").await?; + assert!(loaded_after.is_none()); + + Ok(()) } } diff --git a/src/storage/mod.rs b/src/storage/mod.rs index dbc173b..a4b3314 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,24 +1,149 @@ +pub mod node_model; +pub mod repo_model; + use anyhow::Result; +use sea_orm::{ConnectOptions, ConnectionTrait, Database, DatabaseConnection}; use std::fs; use std::path::PathBuf; +use std::time::Duration; +use tokio::sync::OnceCell; use crate::identity::keypair::KeyPair; -/// 数据目录:cwd/.megaengine +/// 默认根目录:`~/.megaengine`,可由 `MEGAENGINE_ROOT` 环境变量覆盖 pub fn data_dir() -> PathBuf { + if let Some(dir) = std::env::var_os("MEGAENGINE_ROOT") { + return PathBuf::from(dir); + } + + if let Some(home) = std::env::var_os("HOME") { + let mut p = PathBuf::from(home); + p.push(".megaengine"); + return p; + } + + // Windows fallback + if let Some(profile) = std::env::var_os("USERPROFILE") { + let mut p = PathBuf::from(profile); + p.push(".megaengine"); + return p; + } + + // As a last resort fall back to cwd/.megaengine let mut p = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); p.push(".megaengine"); p } -/// 密钥对文件路径 +/// keypair 存放到根目录下 pub fn keypair_path() -> PathBuf { let mut p = data_dir(); + fs::create_dir_all(&p).ok(); p.push("keypair.json"); p } -/// 保存密钥对到文件 +/// 证书路径(默认放到根目录) +pub fn cert_path() -> PathBuf { + let mut p = data_dir(); + fs::create_dir_all(&p).ok(); + p.push("cert.pem"); + p +} + +pub fn key_path() -> PathBuf { + let mut p = data_dir(); + fs::create_dir_all(&p).ok(); + p.push("key.pem"); + p +} + +pub fn ca_cert_path() -> PathBuf { + let mut p = data_dir(); + fs::create_dir_all(&p).ok(); + p.push("ca-cert.pem"); + p +} + +/// SQLite DB 路径 +pub fn db_path() -> PathBuf { + let mut p = data_dir(); + fs::create_dir_all(&p).ok(); + p.push("megaengine.db"); + p +} + +/// 初始化数据库连接并创建表 +pub async fn init_db() -> Result { + static DB: OnceCell = OnceCell::const_new(); + + // 如果已经初始化,直接返回 clone + if let Some(db) = DB.get() { + return Ok(db.clone()); + } + + // 延迟初始化并缓存全局连接(仅第一次会执行创建表操作) + let db_conn = DB + .get_or_init(|| async { + let db_path = db_path(); + + // 确保目录存在 + if let Some(parent) = db_path.parent() { + fs::create_dir_all(parent).ok(); + } + + // 使用合适的 SQLite URL 格式 + let db_url = format!("sqlite://{}?mode=rwc", db_path.display()); + + let mut opt = ConnectOptions::new(db_url); + opt.max_connections(5) + .min_connections(1) + .connect_timeout(Duration::from_secs(8)); + + let db = Database::connect(opt) + .await + .expect("failed to connect to db"); + + // 运行迁移或创建表(只在初始化时执行) + let _ = db + .execute_unprepared( + "CREATE TABLE IF NOT EXISTS repos ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + creator TEXT NOT NULL, + description TEXT NOT NULL, + timestamp INTEGER NOT NULL, + refs TEXT NOT NULL, + path TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )", + ) + .await; + + // 节点表 + let _ = db + .execute_unprepared( + "CREATE TABLE IF NOT EXISTS nodes ( + id TEXT PRIMARY KEY, + alias TEXT NOT NULL, + addresses TEXT NOT NULL, + node_type INTEGER NOT NULL, + version INTEGER NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )", + ) + .await; + + db + }) + .await; + + Ok(db_conn.clone()) +} + +/// 保存密钥对到文件(JSON) pub fn save_keypair(kp: &KeyPair) -> Result<()> { let dir = data_dir(); fs::create_dir_all(&dir)?; @@ -38,6 +163,7 @@ pub fn load_keypair() -> Result { #[cfg(test)] mod tests { + use super::*; #[test] diff --git a/src/storage/node_model.rs b/src/storage/node_model.rs new file mode 100644 index 0000000..fb0947b --- /dev/null +++ b/src/storage/node_model.rs @@ -0,0 +1,117 @@ +use std::net::SocketAddr; + +use anyhow::Result; +use sea_orm::entity::prelude::*; +use sea_orm::Set; + +use crate::node::node::{NodeInfo, NodeType}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "nodes")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: String, + pub alias: String, + pub addresses: String, + pub node_type: i32, + pub version: i32, + pub created_at: i64, + pub updated_at: i64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +/// 将 NodeInfo 保存到数据库 +pub async fn save_node_info_to_db(info: &NodeInfo) -> Result<()> { + let db = crate::storage::init_db().await?; + + let addresses_json = serde_json::to_string(&info.addresses)?; + let now = chrono::Local::now().timestamp(); + + // 删除旧记录(如果存在) + let _ = Entity::delete_by_id(info.node_id.to_string()) + .exec(&db) + .await; + + let node_type_int = match info.node_type { + NodeType::Normal => 0, + NodeType::Relay => 1, + }; + + let active = ActiveModel { + id: Set(info.node_id.to_string()), + alias: Set(info.alias.clone()), + addresses: Set(addresses_json), + node_type: Set(node_type_int), + version: Set(info.version as i32), + created_at: Set(now), + updated_at: Set(now), + }; + + Entity::insert(active).exec(&db).await?; + Ok(()) +} + +/// 从数据库加载 NodeInfo +pub async fn load_node_info_from_db(node_id: &str) -> Result> { + let db = crate::storage::init_db().await?; + + let models = Entity::find().all(&db).await?; + for m in models { + if m.id == node_id { + let addresses: Vec = serde_json::from_str(&m.addresses)?; + let node_type = match m.node_type { + 0 => NodeType::Normal, + _ => NodeType::Relay, + }; + + let info = NodeInfo { + node_id: crate::node::node_id::NodeId::from_string(&m.id) + .unwrap_or_else(|_| crate::node::node_id::NodeId::from_string("").unwrap()), + alias: m.alias, + addresses, + node_type, + version: m.version as u8, + }; + + return Ok(Some(info)); + } + } + + Ok(None) +} + +/// 删除节点记录 +pub async fn delete_node_from_db(node_id: &str) -> Result<()> { + let db = crate::storage::init_db().await?; + Entity::delete_by_id(node_id).exec(&db).await?; + Ok(()) +} + +/// 列出所有节点 +pub async fn list_nodes() -> Result> { + let db = crate::storage::init_db().await?; + let models = Entity::find().all(&db).await?; + + let mut out = Vec::new(); + for m in models { + let addresses: Vec = serde_json::from_str(&m.addresses).unwrap_or_default(); + let node_type = match m.node_type { + 0 => NodeType::Normal, + _ => NodeType::Relay, + }; + let info = NodeInfo { + node_id: crate::node::node_id::NodeId::from_string(&m.id) + .unwrap_or_else(|_| crate::node::node_id::NodeId::from_string("").unwrap()), + alias: m.alias, + addresses, + node_type, + version: m.version as u8, + }; + out.push(info); + } + Ok(out) +} diff --git a/src/storage/repo_model.rs b/src/storage/repo_model.rs new file mode 100644 index 0000000..7671f6b --- /dev/null +++ b/src/storage/repo_model.rs @@ -0,0 +1,204 @@ +use std::path::PathBuf; + +use anyhow::Result; +use sea_orm::entity::prelude::*; +use sea_orm::{Set, Unchanged}; + +use crate::{repo::repo::Repo, storage::init_db}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "repos")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: String, + pub name: String, + pub creator: String, + pub description: String, + pub timestamp: i64, + pub refs: String, + pub path: String, + pub created_at: i64, + pub updated_at: i64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +/// 保存或更新 Repo 到数据库 +pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { + let db = init_db().await?; + let refs_json = serde_json::to_string(&repo.refs)?; + let now = chrono::Local::now().timestamp(); + + // 查询是否已存在 + let existing = Entity::find_by_id(repo.repo_id.clone()).one(&db).await?; + + if let Some(existing_model) = existing { + // 记录已存在,更新 + let active_model = ActiveModel { + id: Unchanged(repo.repo_id.clone()), + name: Set(repo.p2p_description.name.clone()), + creator: Set(repo.p2p_description.creator.clone()), + description: Set(repo.p2p_description.description.clone()), + timestamp: Set(repo.p2p_description.timestamp), + refs: Set(refs_json), + path: Set(repo.path.to_string_lossy().to_string()), + created_at: Unchanged(existing_model.created_at), + updated_at: Set(now), + }; + Entity::update(active_model).exec(&db).await?; + } else { + // 记录不存在,插入 + let active_model = ActiveModel { + id: Set(repo.repo_id.clone()), + name: Set(repo.p2p_description.name.clone()), + creator: Set(repo.p2p_description.creator.clone()), + description: Set(repo.p2p_description.description.clone()), + timestamp: Set(repo.p2p_description.timestamp), + refs: Set(refs_json), + path: Set(repo.path.to_string_lossy().to_string()), + created_at: Set(now), + updated_at: Set(now), + }; + Entity::insert(active_model).exec(&db).await?; + } + + Ok(()) +} + +/// 从数据库加载 Repo +pub async fn load_repo_from_db(repo_id: &str) -> Result> { + let db = init_db().await?; + + // 使用 find() 来查询所有,然后筛选 + let models = Entity::find().all(&db).await?; + + for model in models { + if model.id == repo_id { + let refs: std::collections::HashMap = + serde_json::from_str(&model.refs)?; + + let repo = Repo { + repo_id: model.id, + refs, + p2p_description: crate::repo::repo::P2PDescription { + creator: model.creator, + name: model.name, + description: model.description, + timestamp: model.timestamp, + }, + path: PathBuf::from(model.path), + }; + return Ok(Some(repo)); + } + } + + Ok(None) +} + +/// 删除 Repo 从数据库 +pub async fn delete_repo_from_db(repo_id: &str) -> Result<()> { + let db = init_db().await?; + Entity::delete_by_id(repo_id).exec(&db).await?; + Ok(()) +} + +/// 列出所有 Repos +pub async fn list_repos() -> Result> { + let db = init_db().await?; + let models = Entity::find().all(&db).await?; + + let mut repos = Vec::new(); + for model in models { + let refs: std::collections::HashMap = + serde_json::from_str(&model.refs).unwrap_or_default(); + + repos.push(Repo { + repo_id: model.id, + refs, + p2p_description: crate::repo::repo::P2PDescription { + creator: model.creator, + name: model.name, + description: model.description, + timestamp: model.timestamp, + }, + path: PathBuf::from(model.path), + }); + } + Ok(repos) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_save_and_load_repo() -> Result<()> { + // 创建测试 Repo + let desc = crate::repo::repo::P2PDescription { + creator: "did:node:test".to_string(), + name: "test-repo".to_string(), + description: "A test repository".to_string(), + timestamp: 1000, + }; + + let mut repo = Repo::new( + "did:repo:test".to_string(), + desc, + PathBuf::from("/tmp/test-repo"), + ); + repo.add_ref("refs/heads/main".to_string(), "abc123".to_string()); + + // 保存到数据库 + save_repo_to_db(&repo).await?; + + // 从数据库加载 + let loaded = load_repo_from_db("did:repo:test").await?; + assert!(loaded.is_some()); + + let loaded_repo = loaded.unwrap(); + assert_eq!(loaded_repo.repo_id, repo.repo_id); + assert_eq!(loaded_repo.p2p_description.name, repo.p2p_description.name); + assert_eq!( + loaded_repo.get_ref("refs/heads/main"), + Some(&"abc123".to_string()) + ); + + // 清理 + delete_repo_from_db("did:repo:test").await?; + Ok(()) + } + + #[tokio::test] + async fn test_list_repos() -> Result<()> { + // 创建多个测试 Repos + for i in 0..3 { + let desc = crate::repo::repo::P2PDescription { + creator: "did:node:test".to_string(), + name: format!("test-repo-{}", i), + description: format!("Test repository {}", i), + timestamp: 1000 + i, + }; + + let repo = Repo::new( + format!("did:repo:test-{}", i), + desc, + PathBuf::from(format!("/tmp/test-repo-{}", i)), + ); + + save_repo_to_db(&repo).await?; + } + + // 列出所有 Repos + let repos = list_repos().await?; + assert!(repos.len() >= 3); + + // 清理 + for i in 0..3 { + delete_repo_from_db(&format!("did:repo:test-{}", i)).await?; + } + Ok(()) + } +} diff --git a/src/transport/config.rs b/src/transport/config.rs index 24c784d..c810a32 100644 --- a/src/transport/config.rs +++ b/src/transport/config.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::Result; use quinn::crypto::rustls::{QuicClientConfig, QuicServerConfig}; use quinn::{ClientConfig, IdleTimeout, ServerConfig, TransportConfig, VarInt}; use rustls::pki_types::{CertificateDer, PrivateKeyDer}; diff --git a/src/transport/quic.rs b/src/transport/quic.rs index 7804cd4..da597c2 100644 --- a/src/transport/quic.rs +++ b/src/transport/quic.rs @@ -1,11 +1,7 @@ -use crate::node::node::Node; use crate::node::node_id::NodeId; use crate::transport::config::QuicConfig; use anyhow::{Context, Result}; -use quinn::{ - Connection, ConnectionError, Endpoint, Incoming, RecvStream, ServerConfig, TransportConfig, - VarInt, -}; +use quinn::{Connection, Endpoint, Incoming}; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; @@ -13,14 +9,21 @@ use tokio::sync::mpsc::Receiver; use tokio::sync::{mpsc, Mutex}; use tracing::{error, info}; +use tokio::sync::mpsc::Sender as TokioSender; + const READ_BUF_SIZE: usize = 1024 * 1024; +// Type alias for incoming message sender to reduce type complexity +type IncomingMessageSender = Arc)>>>>; + #[derive(Debug, Clone)] pub struct ConnectionManager { + #[allow(dead_code)] config: QuicConfig, endpoint: Arc, connection_tx: mpsc::Sender, connections: Arc>>>, + incoming_sender: IncomingMessageSender, } #[derive(Debug, Clone)] @@ -67,6 +70,7 @@ impl ConnectionManager { endpoint: Arc::new(endpoint), connection_tx, connections: Arc::new(Mutex::new(HashMap::new())), + incoming_sender: Arc::new(Mutex::new(None)), }; Ok((transport, connection_rx)) } @@ -87,11 +91,13 @@ impl ConnectionManager { // let manager_clone = manager_clone.clone(); match Self::accept_connection(incoming).await { Ok((conn, msg_rx)) => { - if let Err(e) = tx.send(conn).await { + if let Err(e) = tx.send(conn.clone()).await { error!("Failed to send connection: {}", e); return; } - manager_clone.spawn_message_handler(msg_rx).await; + manager_clone + .spawn_message_handler(conn.node_id.clone(), msg_rx) + .await; } Err(e) => { error!("Connection failed: {}", e); @@ -154,16 +160,35 @@ impl ConnectionManager { )) } - /// 生成消息处理任务 - async fn spawn_message_handler(&self, mut receiver: Receiver>) { + /// 生成消息处理任务,将接收到的消息转发到注册的 incoming_sender(包含发送者 NodeId) + async fn spawn_message_handler(&self, peer_id: NodeId, mut receiver: Receiver>) { + let incoming = Arc::clone(&self.incoming_sender); tokio::spawn(async move { while let Some(data) = receiver.recv().await { - let message = String::from_utf8(data).unwrap(); - info!("Received message: {}", message); + // forward to registered gossip handler if present + let maybe = incoming.lock().await; + if let Some(tx) = maybe.as_ref() { + let _ = tx.send((peer_id.clone(), data)).await; + } else { + let message = String::from_utf8(data).unwrap_or_default(); + info!("Received message from {}: {}", peer_id, message); + } } }); } + /// Register a channel to receive incoming messages from peers: (peer_id, bytes) + pub async fn register_incoming_sender(&self, tx: TokioSender<(NodeId, Vec)>) { + let mut guard = self.incoming_sender.lock().await; + *guard = Some(tx); + } + + /// Return list of connected peer NodeIds + pub async fn list_peers(&self) -> Vec { + let connections = self.connections.lock().await; + connections.keys().cloned().collect() + } + pub async fn connect( &self, self_node_id: NodeId, @@ -189,7 +214,7 @@ impl ConnectionManager { None => { return Err(anyhow::anyhow!( "Failed to connect to node[{}], no address available", - target_node_id.to_string() + target_node_id )) } }; @@ -239,9 +264,12 @@ impl ConnectionManager { #[cfg(test)] mod tests { use super::*; - use crate::{identity::keypair::KeyPair, node::node::NodeType}; + use crate::{ + identity::keypair::KeyPair, + node::node::{Node, NodeType}, + }; use std::sync::Once; - use tokio::time::{sleep, Duration}; + use tokio::time::Duration; static RUSTLS_INIT: Once = Once::new(); diff --git a/src/util/mod.rs b/src/util/mod.rs index b42c684..49a0191 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,8 +1,3 @@ -use std::time::SystemTime; - -pub fn timestamp_now() -> u64 { - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs() +pub fn timestamp_now() -> i64 { + chrono::Local::now().timestamp() } diff --git a/test_output.txt b/test_output.txt deleted file mode 100644 index e2b02e01b53844e5d57ef9e0c725efa37a19ebdc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14670 zcmeI3TW=f3702hfK)(Zl0*;Kvf~3ed6%Fdja#O^PU0bbNSjp-_%4V%wNh-FQq|Q)yL~VSa%bmyZgXxk{LjA+!d@7J*WomD!iK)~+0^@SI1Bx7r0+z0!>Yb3dRq!xnq?;(Xna@gJB_1mhM$L@h2QKiy?pZY1w{7+5sr9Ma{;JYJ z+5b_*q!*qE{!w_No<3V4NYC|l7JrvjdarLMFxv~waCAA#vT$6!gzO(-+7GxoUBPKH z!T>E#Mfr>Hdr|UGW$%+ep~g3A8wr-P2uWXk`zm!Kx0cFLllWyYxAS5{bB_dx@%g4^ zd=Qt>m0|`d*bVRQL`r~M(TMgnCo~w5(SU+xd?wyX)kifVcpXF1C1I8+pcCd zyx>J&64Vn-khf!vI#GM-ZENuRPHh9V8kCGb*W0-y0$OxbkG!(ND9*kc=eiKDPc?S8 zgciy9QoV=jcOdD62Sa`O8f&u3+n(xs;`KmfB$My@CB8q#67q_S7zCZlY8x8yL#4*4 zIaL26LHJYnRW@JF@p}2YkUmavgMgmD*7rm>AkENZH~d1}b6PD`XjD{aGz>Sz8|qkVRvU-tT;W-;u5b3UFS59pce@M~R^SPS@)fW5TnwcL5x+tfWf-L%Y@P`6vrGEd^~yllp^bKgo0VLf ztZD4+OG+D^*T!VCr*Y7Fg~_!wpplik{lT4Og_&;waGv#>qU=vZTc ze^+&I8S6JFXER!P`}P=3ZOy!w*-K{oljg>{!mC-h^f&43zHo33-4TAP;ciVr@01ei zyRiv-uxhGLRH4U~AnZmdf!d=;3!pxboE-}id`stVopRJqq8e@O6#G2G$?=%{Y~Dv+ zJ9*rAi;u~l^vd7!a$B4ALuT)42JD%Ca{f1Cny)dxJ{ZdG;A1D<2L#jTgSw=x$>!Xa zwDANU$Ha9atyLV|`ivwiZ{war6T5A88{_aH5f3HZW~pq4R_#o6c3;2knP@nfg7`1Q z7}#HSCRPubXP@t?rmq^jwqCjS-G}UERH)N)IzJV!huyoD%><6Xx~k7Tt&7Zh2PE(2NH$Ku3l?Ym~0@3a~|dz(79sIHW{@xq=-n`C{vfy94;ua7V{hVdO0QW35a^Jq=ti8>7$jtfDF)Y6ylxc9ajbxS>01ujb(Z+Ron|DY?7EcCI*G+>LIR%eDbOX@@30V?IW zDY*S!wMdq)a7#9FQr3)q4#6-|8Jh-wcLbe0Ut`%CB+Ma$nx2I*ikSxrjq}xZGKj2vrg6=A#aj{f6>(-?KE-`K zyV|uql@A2tT|Jle_FiA3AXGGdCOFsQK2K26$KN{YZP6UQMqc(FLZ*`&j8v zkaq+FAMz(}BOkrDWpbcDbEK%9=sKUZzQ*_B4HU%A9BM`U=t3*8tKlL0X|^@nR!eZD zwW_iHB3C2NfHi_f#U)KzPDjJ2h&u)fqT$+BWq$K!knu?w5a)EAgU}R? znO-!W6=epc(7c{IZG#|}MlWBDcj93#FGWU+wl!TAW|P-Ct)ZJ~`T3;vYAow3I(N~G z^sWNMce~!hr+z2daf7wAUlUH8D&tm!Qxtk_44v0E#`fQb||N~Zq`+< zmgY%%i>MO6=Az`cdym-0bjmU7PG1E5nPVWGUX<3Fyk8q#t}ls+A8uJ@kwhPzbANum z)HU$FsDL!03y1@t4w1xU&U{#Sa{bf>MYF|QgnZz)5lxv5L|V;z`w&!dS{gqK`7T5c z#2tIuIc@m+$l0W#9$)2tF`iS~ip>yAT}+tW+CkFQ&!Hr(+;(FkVkSb37Y=|-KgCybC^@)?u0$MmySsbf~-!+QTj zq<(V&>p-p;9;W*Y$(L~LU{JPUJf5Y4mY%0;qbqvZV3_2$JPHq#PkpSjoQJgt_Ij)O zX;_;FF?sFjdCUzD>~Eqr`7X=UR{7sX;p@eGb&KdW8aqfftlc}f-rD2(*rm>)AOgf! zE4fQ0-VoI01j zAf7>Qp!qC|I$8VqoH!BF)|#_=XQ#=ReMizczU4RD0VKX z;3x2F%BFI9!ksgtC7EV&=f-EdRp2+tz=BNcyNKVJAi5dz$#y#nXvkG>snt#&Ez5PT zxj3`fR4wr_Husszr`q?nqwS=H{5yB!I42lL=2(>s7x66PU+K*KvCiaIMTbIGVN2yg zIpN#Nw-!2G+|)gpLUmuM?F*get;chNrZINfS07_D+1Y+tXEV-cznseyg8ixB_)COi zMXURo_3?!u+?3V&N>4Ap5Nt(wDT}tP`dP8?S*DqBIvl5&aZwa_F5WB-`^mG-gZcPu zvtn(xSl>!d7Ruq8F5~pTB=^rzPT9Tpqg3Me97xtpW(KN9?!4Bdms*?EEoHLTxy?ia z&|)Vx7V{I)V^Pse$0ZamwdTI0m)i_4qF#4TkiGsud%ix3^G~!2`oQoot=uW$!aL^- z$tQ-t|7P8HA{+M=hG8|?t*Ci)dw@=n`Ry$L*u+!;N8#AlcOxAW9OEjC!pJm4BaGYkw zMNz;t&tiOq$+OLa`S@(JV*SZlezKNzOZfj|Es0@?JK&Mu!FgADQ$#q=!yn}fY|9t; zt^B4<{l8#ab(FUBFPYUC6A{xB-{1+J$a-6h&wtoBo9XL!KSo)^tK7z^=f(F_M<$u* z<Pf5#_d=XLBs#`ZBw|h*$;&~4{H(?xY)tgYfcjR&K@5B|& j=D08#Dd3+eaNF+dRN=!8njEl_%ba6{%%$62cH{oPw~l9H diff --git a/tests/gossip_three_nodes.rs b/tests/gossip_three_nodes.rs new file mode 100644 index 0000000..160ad69 --- /dev/null +++ b/tests/gossip_three_nodes.rs @@ -0,0 +1,137 @@ +//! 集成测试:启动三个节点,gossip 传递消息 +use megaengine::gossip::{GossipService, SignedMessage}; +use megaengine::identity::keypair::KeyPair; +use megaengine::node::node::{Node, NodeType}; +use megaengine::transport::config::QuicConfig; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::time::{sleep, Duration}; + +#[tokio::test] +async fn test_gossip_three_nodes_message_relay() { + // 初始化 rustls crypto provider + let _ = rustls::crypto::ring::default_provider().install_default(); + + // 初始化日志 + let _ = tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_test_writer() + .try_init(); + + // 生成或确保证书存在 + megaengine::transport::cert::ensure_certificates( + "cert/cert.pem", + "cert/key.pem", + "cert/ca-cert.pem", + ) + .expect("ensure certificates"); + + megaengine::transport::cert::ensure_certificates( + "cert/cert2.pem", + "cert/key2.pem", + "cert/ca-cert.pem", + ) + .expect("ensure certificates 2"); + + megaengine::transport::cert::ensure_certificates( + "cert/cert3.pem", + "cert/key3.pem", + "cert/ca-cert.pem", + ) + .expect("ensure certificates 3"); + + // 1. 生成三对密钥 + let kp1 = KeyPair::generate().unwrap(); + let kp2 = KeyPair::generate().unwrap(); + let kp3 = KeyPair::generate().unwrap(); + + // 2. 分配三个端口 + let addr1: SocketAddr = "127.0.0.1:19001".parse().unwrap(); + let addr2: SocketAddr = "127.0.0.1:19002".parse().unwrap(); + let addr3: SocketAddr = "127.0.0.1:19003".parse().unwrap(); + + // 3. 创建节点 + let mut node1 = Node::from_keypair(&kp1, "node1", vec![addr1], NodeType::Normal); + let mut node2 = Node::from_keypair(&kp2, "node2", vec![addr2], NodeType::Normal); + let mut node3 = Node::from_keypair(&kp3, "node3", vec![addr3], NodeType::Normal); + + // 4. 启动 QUIC server + let config1 = QuicConfig::new( + addr1, + "cert/cert.pem".to_string(), + "cert/key.pem".to_string(), + "cert/ca-cert.pem".to_string(), + ); + let config2 = QuicConfig::new( + addr2, + "cert/cert2.pem".to_string(), + "cert/key2.pem".to_string(), + "cert/ca-cert.pem".to_string(), + ); + let config3 = QuicConfig::new( + addr3, + "cert/cert3.pem".to_string(), + "cert/key3.pem".to_string(), + "cert/ca-cert.pem".to_string(), + ); + node1.start_quic_server(config1).await.unwrap(); + node2.start_quic_server(config2).await.unwrap(); + node3.start_quic_server(config3).await.unwrap(); + + // 5. 启动 gossip + let gossip1 = Arc::new(GossipService::new( + Arc::clone(node1.connection_manager.as_ref().unwrap()), + node1.clone(), + None, + )); + let gossip2 = Arc::new(GossipService::new( + Arc::clone(node2.connection_manager.as_ref().unwrap()), + node2.clone(), + None, + )); + let gossip3 = Arc::new(GossipService::new( + Arc::clone(node3.connection_manager.as_ref().unwrap()), + node3.clone(), + None, + )); + gossip1.start().await.unwrap(); + gossip2.start().await.unwrap(); + gossip3.start().await.unwrap(); + + // 6. 连接成链 node1 <-> node2 <-> node3 + let mgr1 = node1.connection_manager.as_ref().unwrap().clone(); + let mgr2 = node2.connection_manager.as_ref().unwrap().clone(); + mgr1.lock() + .await + .connect( + node1.node_id().clone(), + node2.node_id().clone(), + vec![addr2], + ) + .await + .unwrap(); + mgr2.lock() + .await + .connect( + node2.node_id().clone(), + node3.node_id().clone(), + vec![addr3], + ) + .await + .unwrap(); + // 等待连接建立 + sleep(Duration::from_millis(500)).await; + + // 7. node1 发送 gossip 消息(NodeAnnouncement) + let signed = SignedMessage::new_node_sign_message(node1.clone()).unwrap(); + let env = serde_json::to_vec(&serde_json::json!({"payload": signed, "ttl": 3})).unwrap(); + mgr1.lock() + .await + .send_message(node2.node_id().clone(), env) + .await + .unwrap(); + + // 8. 等待消息传播 + sleep(Duration::from_secs(2)).await; + // 这里只能通过日志人工观察传播效果,或后续扩展 GossipService 提供 hook/回调收集消息 +} diff --git "a/\345\210\206\345\270\203\345\274\217p2p\347\275\221\347\273\234\350\256\276\350\256\241.md" "b/\345\210\206\345\270\203\345\274\217p2p\347\275\221\347\273\234\350\256\276\350\256\241.md" deleted file mode 100644 index bfb97bc..0000000 --- "a/\345\210\206\345\270\203\345\274\217p2p\347\275\221\347\273\234\350\256\276\350\256\241.md" +++ /dev/null @@ -1,462 +0,0 @@ -# 基于QUIC协议构建P2P的Git网络 - -核心:发现节点,发现仓库,共享仓库,复制仓库,开源协作 - -尽量与git本身无关,只需要一个仓库标识,就可以下载仓库,并验证,无论上传者是否在线 - -## 架构设计 - -- **Identity层:**每个节点有一对长期密钥(Ed25519)。签名用于消息 refs、repo、node messages 签名验证。 -- **Transport层**:QUIC(使用 Rust 的 `quinn` 实现),或者Noise协议(Rust的`snow`实现,Noise协议只需要验证公钥就可以建立加密通信)。利用 QUIC 的 multiplexed streams 传输 git 数据,用 datagram 承载低价值的 gossip。 -- **Gossip 层**:实现 RepoMessage/ NodeMessage/EventMessage (三类),负责节点表和仓库路由表构造与更新。 - - Messages会缓存一段时间,重复消息被丢弃 - - **Repo Routing**:根据NodeMessage和RepoMessages创建仓库与节点的对应表。 - - 可以记录仓库的id,节点关系等 - - **Node Routing**:根据NodeMessage创建节点路由表 - - 可以记录节点的在线状态,优先级,地址等信息 -- **Git层**:实现基于 Git 协议的对象传输,或支持 `git packfile` 的二进制流传输。 -- **Relay层**:缓存RepoMessage/ RefsMessage / NodeMessage,缓存Repo仓库,推导Routing表。 -- **应用层**:连接Relay,下载Relay的Routing表,实现分享仓库,下载仓库,更新仓库,开源协作等功能。 - -image-20250922084407689 - -## 使用Gossip传播信息 - -1. 节点会把消息传播至邻居节点 -2. 邻居节点收到消息会再次转发 -3. 为避免无限传播,节点会丢弃重复收到的消息 -4. 节点通常会缓存一定量的消息,用于判断消息是否已经收到 - -## 一、节点 - -### 节点id - -`did:key:zQmW8QYFL8YRxq0QNqSLCVJvEoDuCgZQpL9FxT3p2ZbwU9o` - -**NodeId 表示形式**:采用 **Multibase Base58-btc** 编码: - -- 把 Ed25519 公钥原始字节前置 multicodec 标识(`0xED`,multicodec name: `ed25519-pub`)。 -- 对这个字节串做 multibase(base58btc) 编码,并在前面加上 `did:key:` 。 -- 示例最终形式: `did:key:z` - -即`"did:key:z" + base58(multicodec_prefix || pubkey_bytes)` - -节点id和公钥可以直接**互转** - -### 节点 - -``` -Node{ - NodeId, //节点ID,与其公钥可以互相转化 - Alias, //别名 - Vec
, //节点地址,可以有多个 - NodeType, //节点类型,normal/relay - Version, //版本u8 -} -``` - -一般来说,节点需要缓存收到的节点消息一定时间 - -一方面是为了不重复发送消息,另一方面是为了大致了解网络节点的状态 - -### 节点发现 - -当一个新节点第一次加入网络时,需要「引导(bootstrap)节点(也可以称之为seed/relay节点)」: - -1. 连接到Relay节点,接受Relay节点缓存的`Repo Routing Table`和`Node Routing Table` -2. 节点可以自己选择是否连接到其他Relay节点 -3. 可以周期广播自己的`NodeMessage`以便其他节点更新自己的在线状态 - -## 二、仓库 - -### P2P仓库 - -仓库id生成: - -`did:repo:zXXXXXXXXXX` - -``` -Repo{ - RepoId, - GitRootCommit, - Refs, - P2PDescription, - Path, //git仓库地址 -} - -//git仓库关联 -//git的首次提交记录 + 创建者公钥 -> p2p仓库id -RepoId = Multibase(Multihash( - GitRootCommit || CreatorPublicKey //一次性使用 -)) - -P2PDescription{ - "creator":"NodeId", - "name":"aaa", - "description":"bbb", - "timestamp":123 -} -``` - -### 本地仓库 - -需要建立本地仓库和P2P仓库的对应关系 - -RepoId -> .git的文件路径 - -这个关系存在本地 - -### P2P仓库地址 - -1.`git+p2p://{RepoId}` - -这个地址算是短地址,但是由于同一个仓库,可能有多个拷贝。 - -默认从Relay节点克隆仓库,默认选择长期在线节点,Relay需要跟原始地址保持一致数据 - -2.`git+p2p://{RepoId}/peer/{NodeId}` - -从某个节点下载某个仓库,但是一般节点可能无法直连,可以特指某个fork仓库 - -3. - -`mega://{RepoId}/peer/{NodeId}/refs/{RefName}` - -`mega://{RepoId}/peer/{NodeId}/path/{FilePath}` - -`mega://{RepoId}/peer/{NodeId}/raw/{GitHash}` - -找某个节点的Repo的文件路径 ,或者历史,或者提交信息等 - -### MonoRepo的情况 - -P2P设计是基于通用的Git来设计,还是根据Mega的Mono来设计? - -几个问题? - -1. 是否全网共用一个MonoRepo树?不是,每个client有自己的mono,其实跟本地git一样 -2. 还有RepoId的概念吗,是不是用地址就可以?有RepoId,也有path -3. 多个人提交同一个地址,会冲突吗?不会 - -回答: - -这个其实不用太考虑,看git用的哪个服务就行,如果是Mega,让Mega自己处理,如果是普通的git服务,让git服务自己处理。 - -## 三、消息Gossip Messages - -### 节点消息NodeMessage - -``` -NodeMessage{ - NodeId, //节点ID,与其公钥可以互相转化 - Alias, //别名 - Vec
, //节点地址,可以有多个 - NodeType, //节点类型,normal/relay - Version, //版本u8 -} -``` - -### 仓库消息RepoMessage - -``` -RepoMessage{ - NodeId, - RepoId, -} -``` - -### 协作消息ActivityMessage - -``` -ActivityMessage{ - NodeId, - RepoId, - Activity, -} -``` - -把消息包起来,并签名 - -``` -Message{ - NodeId, - Sign, - Enum -} -``` - - - -## 四、路由表 - -1. 将收到的RepoMessage和NodeMessage转化为: - -``` -struct NodeRouting{ - node_id: NodeId, - addresses: Vec
, - last_seen: SystemTime, - ttl: Duration, //超过 TTL(比如 24 小时)未刷新则删除 - score: f32 // 用于优先级 -} - -//Map存下Repo和node的对应关系 -RepoRouting: Map> -``` - -Client是否需要这个路由表? - -其实还有个功能,就是 本地 查询仓库和节点,我就可以本地直接查,就不用再去问relay了 - -## 五、传输层协议 - -### 如果是QUIC连接 - -#### 需要证书管理 - -Ca?Relay?如何管理根证书? - -#### Multiplexed Streams - -可靠传输机制,用于传输git pack流 - -#### Datagram - -不可靠传播机制,用于Gossip广播 - -#### 连接保持 - -Relay和普通节点需要 将NodeId 与 QUIC connection 绑定,方便广播和nat穿透 - -### 如果是Noise - -TLS协议过于笨重,证书管理过程需要自己处理 - -`Noise`协议基于DH 算法,创建会话密钥,是基于TCP的可靠传输 - -radicle,libp2p(可选)用的也是Noise - -### 如果公钥已知的情况下是否可以建立连接 - -可以自定义验证流程吗,因为双方公钥可以看作已知 - -## 六、Git数据传输 - -### 模仿git fetch - -1. 客户端连接Relay获得`NodeRouting`、`RepoRouting`和`Repo`的数据 -2. 客户端请求 Repo加Refs,默认从活跃度高的Relay请求 -3. Relay 通过git打包 package 传输到client -4. 校验package并暂存本地,等待后端自己处理 - -## 七、NAT穿透 - -目前**不直接提供**NAT穿透功能,Message由Relay进行转发 - -仓库复制,如果是内网节点,需要等Relay先复制,才能给其他节点复制 - -如果是公网节点,可以直接提供服务,但不需要像Relay一样缓存大量的仓库 - -## 八、仓库一致性 - -如果仓库被多次复制,和提交,仓库可能会分叉 - -类似 **Radicle 的设计**:`refs/nodes//heads`,**每个节点独立维护自己的 refs,不覆盖别人,最终由协作策略patch协作收敛。** - -如果Relay,收到同一个RepoId的不同refs,都保存,但是需要标明哪些是仓库代表`delegates`的refs。 - -思考: - -开源协作场景下,PR怎么提交,仓库怎么更新 - -Radicle有,去看一下 - -回答: - -Radicle使用的patch,类似pr,需要提交patch协作请求,由`delegate`下载到本地分支,合并到自己的分支并上传 - -## 九、开源协作 - -通过ActivityMessage去实现,全网广播 - -``` -ActivityMessage{ - NodeId, - RepoId, - Activity, -} -``` - -### 协作对象类型 - -| 类型名 | 含义 | github类比 | -| ------ | ---- | ---------- | -| Issue | 问题 | issue | -| Patch | 补丁 | PR | - -... - -### 协作对象事件 - -``` -Activity { - id: Hash, // 事件哈希 - parent: Hash, // 上一个事件id,根是repoId - type: Type // Issue/Patch/Comment - payload: Payload, // 事件内容 - author: PeerId, // 签名者 - signature: Signature, // Ed25519 签名 - timestamp: u64, -} -``` - -### 协作对象负载 - -#### Issue - -``` -enum IssueEventPayload { - Create { title: String, body: String }, - Comment { body: String }, - Close, - Reopen, - Edit { new_body: String }, -} -``` - -#### Patch - -``` -enum PatchEventPayload { - Create { base: CommitHash, head: CommitHash, title: String }, - Review { decision: ReviewDecision }, - Merge, - Close, -} -``` - -### 对象存储 - -目前只需要存储Event即可 - -### 分叉与合并 - -冲突自由复制数据类型(Conflict-free Replicated Data Type, CRDT),在网络延迟和离线的情况下,保证最终结果一致性 - -![image-20251011163655788](C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20251011163655788.png) - - - -在去中心化环境中,不同节点(Peers)对同一个 Event的操作可能是**异步传播**的,因此: - -当节点收到另一个分支时,它不会直接覆盖,而是: - -1. 验证操作签名; -2. 将该操作插入本地 DAG; -3. 调用合并逻辑来生成一个统一的 view(视图)。 - -### Patch(PR)的流程 - -#### 本地分支提交 - -``` -git checkout -b fix-bug -git commit -am "fix: solve race condition" -``` - -``` -git push rad HEAD:refs/patches -``` - -git提交的时候,会自动生成radicle patch对象并广播?怎么实现的?我在安装的时候,radicle改了git配置吗? - -#### 其他节点更新 - -``` -rad sync //同步seed的数据 -``` - -查看patch - -``` -rad patch list -``` - -更新patch到本地,会在本地更新一个branch - -``` -rad patch checkout e5f0a5a -``` - -更新master - -``` -git merge patch/e5f0a5a -``` - -推送 - -``` -git push rad master -``` - -### 节点和仓库的关系 - -一个仓库是对应多个节点(用户的),master分支应该是Delegate来维护 - -image-20251011173327160 - -## 十、本地存储 - -目前本地存储方案待定,Client和Relay - -信息存SQLITE - -Vercel 了解下 - -答: - -Vercel 更多的是托管页面,如果是后台,优先考虑docker部署 - -## 十一、开发计划 - -## 总结:实现建议阶段划分 - -| 模块/阶段 | 模块名称 | 功能 | 内容 | -| ---------------------- | ------------------ | ----------------- | ------------------------- | -| 身份认证+网络传输 | Identity+Transport | Identity + QUIC | 建立节点通信、公钥验证 | -| 节点管理+仓库管理+存储 | Storage | Node+Repo+Storage | 建立并保存 Node/Repo 信息 | -| 消息广播+仓库路由 | Gossip | Gossip + Routing | 广播消息,建立路由表 | -| git兼容 | Git | Git Sync | 支持git fetch操作 | -| 开源协作 | Collaboration | Issue+Patch | 支持 Issue/Patch 协作 | -| 部署 | Docker | Docker | 支持docker部署 | - -看一下RustVault简易版能不能用->libvault - -# 🦀 Rust 模块结构总览 - -``` -p2p-git/ -│ -├── main.rs -│ -├── identity/ # 身份与密钥层 -│ -├── transport/ # 网络传输层 (QUIC) -│ -├── storage/ # 本地持久化 (通用) SQLite -│ -├── gossip/ # Gossip 消息传播层 -│ -├── node/ # 节点服务 -│ -├── git/ # Git 对象传输层 -│ -├── activity/ # 协作层 (Issue / Patch ) -│ -├── cli/ # 应用层命令行 -│ -└── utils/ # 工具模块 - -``` - From f0cb345588552803ad5a5ef4ba70c5d33feeeee8 Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Mon, 17 Nov 2025 19:48:49 +0800 Subject: [PATCH 03/42] Update src/repo/repo_id.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/repo/repo_id.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repo/repo_id.rs b/src/repo/repo_id.rs index bd7cf34..46a8d2d 100644 --- a/src/repo/repo_id.rs +++ b/src/repo/repo_id.rs @@ -26,7 +26,7 @@ impl RepoId { pub fn parse_from_str(repo_id: &str) -> Result { if !repo_id.starts_with(REPO_KEY_PREFIX) { - return Err(anyhow!("invalid NodeId prefix")); + return Err(anyhow!("invalid RepoId prefix")); } let encoded = &repo_id[REPO_KEY_PREFIX.len()..]; From d9ff13f69615af8fddb2d96bea18b4a7f264be37 Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Mon, 17 Nov 2025 19:48:59 +0800 Subject: [PATCH 04/42] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a7c7f5..59fe909 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ did:repo:zW1iF5iwCChifAcjZUrDbwD9o8LS76kFsz6bTZFEJhEqVCU - `NodeAnnouncement`: Advertises node metadata (alias, addresses, type) - `RepoAnnouncement`: Lists repositories owned by a node -- **TTL (Time-to-Live)**: Default 4 hops, decremented on each relay +- **TTL (Time-to-Live)**: Default 16 hops, decremented on each relay - **Deduplication**: Tracks seen message hashes in a 5-minute sliding window - **Broadcast Interval**: 10 seconds From 29e26cb55354add3077ee85f294723961630fd91 Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Mon, 17 Nov 2025 19:49:06 +0800 Subject: [PATCH 05/42] Update src/transport/quic.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/transport/quic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transport/quic.rs b/src/transport/quic.rs index da597c2..9284427 100644 --- a/src/transport/quic.rs +++ b/src/transport/quic.rs @@ -88,7 +88,7 @@ impl ConnectionManager { let tx = connection_tx.clone(); let manager_clone = manager_clone.clone(); tokio::spawn(async move { - // let manager_clone = manager_clone.clone(); + match Self::accept_connection(incoming).await { Ok((conn, msg_rx)) => { if let Err(e) = tx.send(conn.clone()).await { From d7d632c246e6aa2caece7dbb4bdabe0c7e51746c Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Mon, 17 Nov 2025 19:49:12 +0800 Subject: [PATCH 06/42] Update src/transport/quic.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/transport/quic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transport/quic.rs b/src/transport/quic.rs index 9284427..e3f2b57 100644 --- a/src/transport/quic.rs +++ b/src/transport/quic.rs @@ -250,7 +250,7 @@ impl ConnectionManager { pub async fn send_message(&self, node_id: NodeId, message: Vec) -> Result<()> { let connections = self.connections.lock().await; - let conn = connections.get(&node_id).context(format!( + let conn = connections.get(&node_id).with_context(|| format!( "Failed to send message to node[{}], connection not found", node_id ))?; From 90b1893b9613476be7b21dc0e48f4c3598adb1f0 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Wed, 26 Nov 2025 11:45:56 +0800 Subject: [PATCH 07/42] update gossip --- README.md | 2 +- src/gossip/service.rs | 19 ++++-- src/main.rs | 118 ++++++++++++++++++++++++++++++++++-- src/node/mod.rs | 1 + src/node/node_addr.rs | 50 +++++++++++++++ src/transport/config.rs | 72 ++++++++++++++++------ src/transport/quic.rs | 19 +++++- tests/gossip_three_nodes.rs | 14 ++++- 8 files changed, 265 insertions(+), 30 deletions(-) create mode 100644 src/node/node_addr.rs diff --git a/README.md b/README.md index 2a7c7f5..7012715 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ Add a local Git repository to the network: ```bash cargo run -- repo add \ --path /path/to/git/repo \ - --description "My awesome repository" + --description "My repository" ``` The repo ID is automatically generated from the Git root commit hash and the node's public key. diff --git a/src/gossip/service.rs b/src/gossip/service.rs index 01fb4ba..53bccf0 100644 --- a/src/gossip/service.rs +++ b/src/gossip/service.rs @@ -12,6 +12,7 @@ use std::convert::TryInto; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::{mpsc, Mutex}; +use tracing::info; const DEFAULT_TTL: u8 = 16; @@ -65,6 +66,12 @@ impl GossipService { payload: SignedMessage, ttl: u8, } + { + let mgr = s2.manager.lock().await; + mgr.list_peers().await.into_iter().for_each(|peer| { + info!("Connected peer: {}", peer); + }); + } // 1. 发送 NodeAnnouncement if let Ok(signed) = SignedMessage::new_node_sign_message(s2.node.clone()) { @@ -156,11 +163,14 @@ impl GossipService { let sig_bytes = hex::decode(&signed.signature).unwrap_or_default(); let arr: [u8; 64] = match sig_bytes.as_slice().try_into() { Ok(a) => a, - Err(_) => return Ok(()), + Err(e) => { + tracing::error!("Failed to convert signature bytes: {}", e); + return Ok(()); + } }; let sig = Signature::from_bytes(&arr); if !kp.verify(&signed.self_hash(), &sig) { - tracing::warn!( + tracing::error!( "signature verification failed for message from {}", signed.node_id ); @@ -172,9 +182,10 @@ impl GossipService { match &signed.message { GossipMessage::NodeAnnouncement(na) => { tracing::info!( - "Gossip: NodeAnnouncement from {} (alias: {})", + "Gossip: NodeAnnouncement from {} (alias: {}, addresses: {:?})", na.node_id, - na.alias + na.alias, + na.addresses, ); // TODO: update NodeManager or Node routing table } diff --git a/src/main.rs b/src/main.rs index 0743d96..941fc85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use anyhow::Result; use clap::{Parser, Subcommand}; +use megaengine::node::node_addr::NodeAddr; use std::net::SocketAddr; use megaengine::gossip::GossipService; @@ -61,9 +62,23 @@ enum NodeAction { #[arg(short, long, default_value = "cert")] cert_path: String, + + /// Bootstrap node address to connect to on startup (e.g., 127.0.0.1:9000) + #[arg(long)] + bootstrap_node: Option, }, /// Print node id using stored keypair Id, + /// Connect to another node + Connect { + /// Target node's address (e.g., 127.0.0.1:9001) + #[arg(long)] + peer_addr: String, + + /// Target node's ID + #[arg(long)] + peer_id: String, + }, } #[derive(Subcommand)] @@ -78,16 +93,22 @@ enum RepoAction { #[arg(long, default_value = "")] description: String, }, + /// List all repositories + List, } #[tokio::main] async fn main() -> Result<()> { let _ = rustls::crypto::ring::default_provider().install_default(); + + // 初始化 tracing 日志 + let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("error,megaengine=info")); + tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::from_default_env() - .add_directive("megaengine=info".parse().unwrap()), - ) + .with_env_filter(env_filter) + .with_target(true) + .with_level(true) .init(); let cli = Cli::parse(); @@ -129,6 +150,7 @@ async fn main() -> Result<()> { alias, addr, cert_path, + bootstrap_node, } => { tracing::info!("Starting node..."); let cert_dir = format!("{}/{}", &root_path, cert_path); @@ -151,7 +173,7 @@ async fn main() -> Result<()> { let mut node = megaengine::node::node::Node::from_keypair( &kp, - alias, + &alias, addrs.clone(), megaengine::node::node::NodeType::Normal, ); @@ -170,6 +192,7 @@ async fn main() -> Result<()> { tracing::info!("Starting QUIC server on {}...", addr); node.start_quic_server(quic_config).await?; + if let Some(conn_mgr) = &node.connection_manager { let gossip = std::sync::Arc::new(GossipService::new( std::sync::Arc::clone(conn_mgr), @@ -182,12 +205,69 @@ async fn main() -> Result<()> { tracing::warn!("No connection manager found, gossip not started"); } + // 如果提供了 bootstrap_node,尝试连接到它 + if let Some(bootstrap_addr_str) = bootstrap_node { + if let Some(conn_mgr) = &node.connection_manager { + tracing::info!( + "Attempting to connect to bootstrap node: {}", + bootstrap_addr_str + ); + + match NodeAddr::parse(&bootstrap_addr_str) { + Ok(bootstrap_info) => { + match conn_mgr + .lock() + .await + .connect( + node.node_id().clone(), + bootstrap_info.peer_id.clone(), + vec![bootstrap_info.address], + ) + .await + { + Ok(_) => { + tracing::info!( + "Successfully connected to bootstrap node {} at {}", + bootstrap_info.peer_id, + bootstrap_info.address + ); + println!( + "Connected to bootstrap node: {} at {}", + bootstrap_info.peer_id, bootstrap_info.address + ); + } + Err(e) => { + tracing::warn!( + "Failed to connect to bootstrap node: {}", + e + ); + eprintln!( + "Warning: Failed to connect to bootstrap node: {}", + e + ); + } + } + } + Err(e) => { + tracing::error!("Failed to parse bootstrap node address: {}", e); + eprintln!("Error: {}", e); + return Err(e); + } + } + } + } + println!( "Node started successfully: {} ({})", node.node_id().0, node.alias() ); println!("Listening on: {}", addr); + + // 打印 node 地址 + let addr = NodeAddr::new(node.node_id().clone(), addr.parse()?); + println!("Node address: {}", addr.to_string()); + println!("Press Ctrl+C to stop"); loop { @@ -207,6 +287,11 @@ async fn main() -> Result<()> { let node_id = NodeId::from_keypair(&kp); println!("{}", node_id); } + NodeAction::Connect { peer_addr, peer_id } => { + // TODO: implement node connect + println!("Connect not yet implemented"); + println!("Would connect to {} with peer_id {}", peer_addr, peer_id); + } }, Commands::Repo { action } => { match action { @@ -259,6 +344,29 @@ async fn main() -> Result<()> { Err(e) => tracing::info!("Failed to add repo: {}", e), } } + RepoAction::List => match storage::repo_model::list_repos().await { + Ok(repos) => { + if repos.is_empty() { + println!("No repositories found"); + } else { + println!("Repositories:"); + println!("{}", "─".repeat(100)); + for repo in repos { + println!(" ID: {}", repo.repo_id); + println!(" Name: {}", repo.p2p_description.name); + println!(" Creator: {}", repo.p2p_description.creator); + println!(" Description: {}", repo.p2p_description.description); + println!(" Path: {}", repo.path.display()); + println!(" Timestamp: {}", repo.p2p_description.timestamp); + println!("{}", "─".repeat(100)); + } + } + } + Err(e) => { + tracing::error!("Failed to list repos: {}", e); + println!("Failed to list repositories: {}", e); + } + }, } } } diff --git a/src/node/mod.rs b/src/node/mod.rs index 4cd3336..fd5c9b9 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -1,4 +1,5 @@ #![allow(clippy::module_inception)] pub mod node; +pub mod node_addr; pub mod node_id; pub mod node_manager; diff --git a/src/node/node_addr.rs b/src/node/node_addr.rs new file mode 100644 index 0000000..943dd85 --- /dev/null +++ b/src/node/node_addr.rs @@ -0,0 +1,50 @@ +use crate::node::node_id::NodeId; +use anyhow::{anyhow, Result}; +use std::net::SocketAddr; + +/// Represents a node address in the format: peer_id@address +/// Example: did:key:z2DeZG8TuHkTvrJ7jijysNsQTpTiu9tRQkxcPmmem1tHvVP@127.0.0.1:9000 +#[derive(Debug, Clone)] +pub struct NodeAddr { + pub peer_id: NodeId, + pub address: SocketAddr, +} + +impl NodeAddr { + /// Parse node address from string format: "peer_id@address" + pub fn parse(s: &str) -> Result { + let parts: Vec<&str> = s.split('@').collect(); + if parts.len() != 2 { + return Err(anyhow!( + "Invalid node address format. Expected 'peer_id@address', got '{}'", + s + )); + } + + let peer_id_str = parts[0]; + let address_str = parts[1]; + + let peer_id = peer_id_str + .parse::() + .map_err(|_| anyhow!("Invalid peer_id: {}", peer_id_str))?; + + let address = address_str + .parse::() + .map_err(|_| anyhow!("Invalid address: {}", address_str))?; + + Ok(NodeAddr { peer_id, address }) + } + + /// Create node address from peer_id and address + pub fn new(peer_id: NodeId, address: SocketAddr) -> Self { + NodeAddr { peer_id, address } + } + + /// Format node address as string + pub fn to_string(&self) -> String { + format!("{}@{}", self.peer_id, self.address) + } +} + +#[cfg(test)] +mod tests {} diff --git a/src/transport/config.rs b/src/transport/config.rs index c810a32..97e336b 100644 --- a/src/transport/config.rs +++ b/src/transport/config.rs @@ -2,7 +2,6 @@ use anyhow::Result; use quinn::crypto::rustls::{QuicClientConfig, QuicServerConfig}; use quinn::{ClientConfig, IdleTimeout, ServerConfig, TransportConfig, VarInt}; use rustls::pki_types::{CertificateDer, PrivateKeyDer}; -use rustls::server::WebPkiClientVerifier; use std::fs::File; use std::io::BufReader; use std::net::SocketAddr; @@ -11,6 +10,51 @@ use std::time::Duration; pub const ALPN_QUIC_HTTP: &[&[u8]] = &[b"h3"]; +/// 用于开发/测试环境的服务器证书验证器 +/// 跳过所有服务器证书验证,允许自签名证书和不同的 CA +#[derive(Debug)] +struct NoServerCertificateVerification; + +impl rustls::client::danger::ServerCertVerifier for NoServerCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + // 跳过验证,允许任何证书 + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + rustls::SignatureScheme::RSA_PKCS1_SHA256, + rustls::SignatureScheme::ECDSA_NISTP256_SHA256, + rustls::SignatureScheme::ED25519, + ] + } +} + #[derive(Clone, Debug)] pub struct QuicConfig { pub bind_addr: SocketAddr, @@ -35,19 +79,13 @@ impl QuicConfig { } /// 获取服务器配置 + /// 注意:不验证客户端证书,仅适用于开发/测试环境 + /// 生产环境应该使用正确的 CA 证书验证 pub fn get_server_config(&self) -> Result { let (certs, key) = self.get_certificate_from_file()?; - let mut roots = rustls::RootCertStore::empty(); - let ca_cert = self.get_ca_certificate_from_file()?; - roots.add(ca_cert)?; - - let client_verifier = WebPkiClientVerifier::builder(roots.into()) - .build() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; - let mut server_crypto = rustls::ServerConfig::builder() - .with_client_cert_verifier(client_verifier) + .with_no_client_auth() // 不验证客户端证书 .with_single_cert(certs, key)?; server_crypto.alpn_protocols = ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); server_crypto.max_early_data_size = u32::MAX; @@ -64,18 +102,16 @@ impl QuicConfig { } /// 获取客户端配置 + /// 注意:使用不验证服务器证书的配置,仅适用于开发/测试环境 + /// 生产环境应该使用正确的 CA 证书验证 pub fn get_client_config(&self) -> Result { - let mut roots = rustls::RootCertStore::empty(); - let ca_cert = self.get_ca_certificate_from_file()?; - roots.add(ca_cert)?; let (certs, key) = self.get_certificate_from_file()?; - // let mut client_crypto = rustls::ClientConfig::builder() - // .with_root_certificates(roots) - // .with_no_client_auth(); - + // 创建一个不验证服务器证书的客户端配置 + // 这对于开发/测试环境很有用,当每个节点都有独立的证书时 let mut client_crypto = rustls::ClientConfig::builder() - .with_root_certificates(roots) + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoServerCertificateVerification)) .with_client_auth_cert(certs, key)?; client_crypto.alpn_protocols = ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); diff --git a/src/transport/quic.rs b/src/transport/quic.rs index da597c2..437626c 100644 --- a/src/transport/quic.rs +++ b/src/transport/quic.rs @@ -233,7 +233,7 @@ impl ConnectionManager { send.finish()?; let quic_conn = QuicConnection { - connection, + connection: connection.clone(), peer_addr, node_id: target_node_id.clone(), connection_type: ConnectionType::Client, @@ -245,6 +245,22 @@ impl ConnectionManager { .await .insert(target_node_id.clone(), Arc::from(quic_conn.clone())); + // 启动消息接收任务,用于接收服务端发来的消息 + let peer_id = target_node_id.clone(); + let connection_clone = connection.clone(); + let incoming_sender = Arc::clone(&self.incoming_sender); + tokio::spawn(async move { + while let Ok(mut recv) = connection_clone.accept_uni().await { + if let Ok(msg) = recv.read_to_end(READ_BUF_SIZE).await { + // 转发给注册的 incoming_sender(如 GossipService) + let maybe = incoming_sender.lock().await; + if let Some(tx) = maybe.as_ref() { + let _ = tx.send((peer_id.clone(), msg)).await; + } + } + } + }); + Ok(()) } @@ -254,6 +270,7 @@ impl ConnectionManager { "Failed to send message to node[{}], connection not found", node_id ))?; + let mut sender = conn.connection.open_uni().await?; sender.write_all(message.as_slice()).await?; sender.finish()?; diff --git a/tests/gossip_three_nodes.rs b/tests/gossip_three_nodes.rs index 160ad69..435021d 100644 --- a/tests/gossip_three_nodes.rs +++ b/tests/gossip_three_nodes.rs @@ -101,6 +101,7 @@ async fn test_gossip_three_nodes_message_relay() { // 6. 连接成链 node1 <-> node2 <-> node3 let mgr1 = node1.connection_manager.as_ref().unwrap().clone(); let mgr2 = node2.connection_manager.as_ref().unwrap().clone(); + let mgr3 = node3.connection_manager.as_ref().unwrap().clone(); mgr1.lock() .await .connect( @@ -132,6 +133,17 @@ async fn test_gossip_three_nodes_message_relay() { .unwrap(); // 8. 等待消息传播 - sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(1)).await; + + // 8. node1 发送 gossip 消息(NodeAnnouncement) + let signed = SignedMessage::new_node_sign_message(node3.clone()).unwrap(); + let env = serde_json::to_vec(&serde_json::json!({"payload": signed, "ttl": 3})).unwrap(); + mgr3.lock() + .await + .send_message(node2.node_id().clone(), env) + .await + .unwrap(); + // 这里只能通过日志人工观察传播效果,或后续扩展 GossipService 提供 hook/回调收集消息 + sleep(Duration::from_secs(1)).await; } From 91811ea38ee08a34f576ecbf08660d2a6b29d9cb Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Wed, 26 Nov 2025 14:59:33 +0800 Subject: [PATCH 08/42] connection clear and git pack --- .gitignore | 3 +- README.md | 59 +-------- src/git/git_repo.rs | 36 ++++++ src/git/mod.rs | 36 +----- src/git/pack.rs | 68 +++++++++++ src/gossip/message.rs | 42 +++++-- src/gossip/service.rs | 44 +++---- src/main.rs | 177 +++++++++++++++------------ src/repo/repo_manager.rs | 1 - src/transport/quic.rs | 41 +++++-- tests/git_pack.rs | 253 +++++++++++++++++++++++++++++++++++++++ 11 files changed, 551 insertions(+), 209 deletions(-) create mode 100644 src/git/git_repo.rs create mode 100644 src/git/pack.rs create mode 100644 tests/git_pack.rs diff --git a/.gitignore b/.gitignore index 9e3788d..65e7997 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ target .vscode/ cert/ -.megaengine/ \ No newline at end of file +.megaengine/ +tmp/* \ No newline at end of file diff --git a/README.md b/README.md index 7012715..7e2ef88 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ MegaEngine is a distributed peer-to-peer (P2P) network for Git repositories. It - **Decentralized Node Discovery**: Nodes automatically discover each other and exchange node information via gossip protocol - **Repository Synchronization**: Nodes announce and sync repository inventory across the network +- **Repository Packing**: Pack Git repositories into bundle - **QUIC Transport**: Uses QUIC protocol for reliable, low-latency peer-to-peer communication - **Gossip Protocol**: Implements epidemic message propagation with TTL and deduplication - **Cryptographic Identity**: Each node has a unique EdDSA-based identity (`did:key` format) @@ -46,7 +47,7 @@ MegaEngine is a distributed peer-to-peer (P2P) network for Git repositories. It ### Prerequisites - Rust 1.70+ (2021 edition) -- Git +- Git (for git operations and bundle/tar packing) - OpenSSL development libraries (for TLS) ### Build @@ -120,64 +121,10 @@ cargo run -- repo add \ The repo ID is automatically generated from the Git root commit hash and the node's public key. + ## 🧪 Testing -Run the integration test for three-node gossip propagation: -```bash -cargo test --test gossip_three_nodes -- --nocapture --test-threads=1 -``` - -This test: -1. Starts three nodes with QUIC servers -2. Connects them in a chain: node1 ↔ node2 ↔ node3 -3. Initiates gossip protocol on all nodes -4. Sends NodeAnnouncement and RepoAnnouncement messages -5. Verifies message propagation with TTL and deduplication - -Example output: -``` -Gossip: NodeAnnouncement from did:key:z2DZe... (alias: node1) -Gossip: RepoAnnouncement from did:key:z2DW... with 1 repos: ["did:repo:zW1i..."] -``` - -## 📁 Project Structure - -``` -src/ -├── main.rs # CLI entry point -├── lib.rs # Library root -├── identity/ -│ └── keypair.rs # EdDSA keypair generation and signing -├── node/ -│ ├── node.rs # Node runtime with QUIC manager -│ ├── node_id.rs # Node identifier (did:key format) -│ ├── node_manager.rs # Node routing and lifecycle -│ └── node.rs -├── repo/ -│ ├── repo.rs # Repository metadata and refs -│ ├── repo_id.rs # Repository identifier (did:repo format) -│ └── repo_manager.rs # Repository persistence and query -├── storage/ -│ ├── mod.rs # Database initialization and connection -│ ├── repo_model.rs # Sea-ORM models and CRUD for repos -│ └── node_model.rs # Sea-ORM models and CRUD for nodes -├── transport/ -│ ├── quic.rs # QUIC connection manager -│ ├── config.rs # QUIC configuration -│ └── cert.rs # TLS certificate generation -├── gossip/ -│ ├── mod.rs # Gossip service exports -│ ├── message.rs # Gossip message types and signing -│ └── service.rs # Gossip protocol implementation -├── git/ -│ └── mod.rs # Git repository utilities -└── util/ - └── mod.rs # Timestamp and utility functions - -tests/ -└── gossip_three_nodes.rs # Integration test for gossip relay -``` ## 🔐 Data Formats diff --git a/src/git/git_repo.rs b/src/git/git_repo.rs new file mode 100644 index 0000000..ed7d752 --- /dev/null +++ b/src/git/git_repo.rs @@ -0,0 +1,36 @@ +use anyhow::Result; +use git2::{Repository, Sort}; + +pub fn repo_root_commit_bytes(path: &str) -> Result> { + let repo = + Repository::open(path).map_err(|e| anyhow::anyhow!("failed to open git repo: {}", e))?; + let mut revwalk = repo + .revwalk() + .map_err(|e| anyhow::anyhow!("revwalk error: {}", e))?; + revwalk + .push_head() + .map_err(|e| anyhow::anyhow!("push_head failed: {}", e))?; + let _ = revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::REVERSE); + + if let Some(entry) = revwalk.next() { + let oid = entry.map_err(|e| anyhow::anyhow!("revwalk entry error: {}", e))?; + return Ok(oid.as_bytes().to_vec()); + } + + Err(anyhow::anyhow!("no commits found in repo")) +} + +pub fn repo_name_space(path: &str) -> String { + let repo = match Repository::open(path) { + Ok(repo) => repo, + Err(_) => { + return "".to_string(); + } + }; + let path = repo.path(); + + if let Some(name) = path.parent().and_then(|p| p.file_name()) { + return name.to_string_lossy().to_string(); + } + "".to_string() +} diff --git a/src/git/mod.rs b/src/git/mod.rs index ed7d752..910b431 100644 --- a/src/git/mod.rs +++ b/src/git/mod.rs @@ -1,36 +1,4 @@ -use anyhow::Result; -use git2::{Repository, Sort}; -pub fn repo_root_commit_bytes(path: &str) -> Result> { - let repo = - Repository::open(path).map_err(|e| anyhow::anyhow!("failed to open git repo: {}", e))?; - let mut revwalk = repo - .revwalk() - .map_err(|e| anyhow::anyhow!("revwalk error: {}", e))?; - revwalk - .push_head() - .map_err(|e| anyhow::anyhow!("push_head failed: {}", e))?; - let _ = revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::REVERSE); - if let Some(entry) = revwalk.next() { - let oid = entry.map_err(|e| anyhow::anyhow!("revwalk entry error: {}", e))?; - return Ok(oid.as_bytes().to_vec()); - } - - Err(anyhow::anyhow!("no commits found in repo")) -} - -pub fn repo_name_space(path: &str) -> String { - let repo = match Repository::open(path) { - Ok(repo) => repo, - Err(_) => { - return "".to_string(); - } - }; - let path = repo.path(); - - if let Some(name) = path.parent().and_then(|p| p.file_name()) { - return name.to_string_lossy().to_string(); - } - "".to_string() -} +pub mod git_repo; +pub mod pack; diff --git a/src/git/pack.rs b/src/git/pack.rs new file mode 100644 index 0000000..361b7ac --- /dev/null +++ b/src/git/pack.rs @@ -0,0 +1,68 @@ +use anyhow::Result; +use git2::Repository; +use std::process::Command; + +/// Pack a git repository into a single file using git bundle +/// This creates a bundle file that contains all branches and commits +/// +/// # Arguments +/// * `repo_path` - Path to the git repository to pack +/// * `output_path` - Path where the bundle file will be created +/// +/// # Example +/// ```ignore +/// pack_repo_bundle("/path/to/repo", "/tmp/repo.bundle")?; +/// ``` +pub fn pack_repo_bundle(repo_path: &str, output_path: &str) -> Result<()> { + let repo = Repository::open(repo_path) + .map_err(|e| anyhow::anyhow!("failed to open git repo: {}", e))?; + + // Get all branches to include in the bundle + let mut branch_refs = Vec::new(); + let mut branches = repo + .branches(None) + .map_err(|e| anyhow::anyhow!("failed to list branches: {}", e))?; + + while let Some(branch_result) = branches.next() { + let (branch, _) = + branch_result.map_err(|e| anyhow::anyhow!("failed to get branch: {}", e))?; + if let Ok(name) = branch.name() { + if let Some(name_str) = name { + branch_refs.push(name_str.to_string()); + } + } + } + + // If no branches found, try to get HEAD + if branch_refs.is_empty() { + if repo.head().is_ok() { + branch_refs.push("HEAD".to_string()); + } + } + + if branch_refs.is_empty() { + return Err(anyhow::anyhow!("no branches found to bundle")); + } + + // Use git bundle command to create the bundle + let mut cmd = Command::new("git"); + cmd.current_dir(repo_path) + .arg("bundle") + .arg("create") + .arg(output_path); + + for branch_ref in &branch_refs { + cmd.arg(branch_ref); + } + + let output = cmd + .output() + .map_err(|e| anyhow::anyhow!("failed to execute git bundle: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow::anyhow!("git bundle failed: {}", stderr)); + } + + Ok(()) +} diff --git a/src/gossip/message.rs b/src/gossip/message.rs index 829fed9..bc68813 100644 --- a/src/gossip/message.rs +++ b/src/gossip/message.rs @@ -8,7 +8,7 @@ use crate::{ node::{Node, NodeType}, node_id::NodeId, }, - repo::repo_id::RepoId, + repo::repo::Repo, util::timestamp_now, }; @@ -43,11 +43,11 @@ impl From for NodeAnnouncement { } } -/// 仓库公告- 表示某个节点拥有的仓库列表 +/// 仓库公告- 表示某个节点拥有的仓库列表(path 为空) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RepoAnnouncement { pub node_id: NodeId, - pub repos: Vec, + pub repos: Vec, } /// 带签名的消息包装 @@ -77,10 +77,19 @@ impl SignedMessage { Ok(sign_message) } - pub fn new_repo_sign_message(repos: Vec, node: Node) -> Result { + pub fn new_repo_sign_message(repos: Vec, node: Node) -> Result { + // 转换 repos,清空 path + let repos_with_empty_path = repos + .into_iter() + .map(|mut repo| { + repo.path = std::path::PathBuf::new(); + repo + }) + .collect(); + let message = GossipMessage::RepoAnnouncement(RepoAnnouncement { node_id: node.node_id().clone(), - repos, + repos: repos_with_empty_path, }); let mut sign_message = SignedMessage { @@ -177,23 +186,38 @@ mod tests { crate::node::node::NodeType::Relay, ); - // generate a repo id + // generate a repo let repo_id = crate::repo::repo_id::RepoId::generate( b"root_commit", node_keypair_bytes(&keypair).as_slice(), ) .expect("generate repo id"); - let signed = SignedMessage::new_repo_sign_message(vec![repo_id.clone()], node.clone()) + let desc = crate::repo::repo::P2PDescription { + creator: "did:key:test".to_string(), + name: "test-repo".to_string(), + description: "A test repository".to_string(), + timestamp: 1000, + }; + + let repo = Repo::new( + repo_id.to_string(), + desc, + std::path::PathBuf::from("/tmp/test-repo"), + ); + + let signed = SignedMessage::new_repo_sign_message(vec![repo.clone()], node.clone()) .expect("sign repo message"); assert_eq!(signed.message_type(), "inventory_announcement"); let sig = hex::decode(&signed.signature).expect("decode hex"); assert_eq!(sig.len(), 64); - // ensure the embedded repo id is present in message + // ensure the embedded repo is present in message and path is empty if let GossipMessage::RepoAnnouncement(ra) = signed.message { - assert!(ra.repos.iter().any(|r| r.as_str() == repo_id.as_str())); + assert!(ra.repos.iter().any(|r| r.repo_id == repo_id.to_string())); + // verify path is cleared + assert!(ra.repos.iter().all(|r| r.path.as_os_str().is_empty())); } else { panic!("expected RepoAnnouncement"); } diff --git a/src/gossip/service.rs b/src/gossip/service.rs index 53bccf0..739d731 100644 --- a/src/gossip/service.rs +++ b/src/gossip/service.rs @@ -1,7 +1,8 @@ use crate::gossip::message::{GossipMessage, SignedMessage}; -use crate::node::node::Node; +use crate::node::node::{Node, NodeInfo}; use crate::node::node_id::NodeId; use crate::repo::repo_manager::RepoManager; +use crate::storage::node_model; use crate::transport::quic::ConnectionManager; use anyhow::Result; use ed25519_dalek::Signature; @@ -12,7 +13,6 @@ use std::convert::TryInto; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::{mpsc, Mutex}; -use tracing::info; const DEFAULT_TTL: u8 = 16; @@ -66,12 +66,6 @@ impl GossipService { payload: SignedMessage, ttl: u8, } - { - let mgr = s2.manager.lock().await; - mgr.list_peers().await.into_iter().for_each(|peer| { - info!("Connected peer: {}", peer); - }); - } // 1. 发送 NodeAnnouncement if let Ok(signed) = SignedMessage::new_node_sign_message(s2.node.clone()) { @@ -89,15 +83,9 @@ impl GossipService { // 2. 发送 RepoAnnouncement(从本地 storage 加载 repo 列表) if let Ok(repos) = crate::storage::repo_model::list_repos().await { - let repo_ids: Vec<_> = repos - .iter() - .filter_map(|r| { - crate::repo::repo_id::RepoId::parse_from_str(&r.repo_id).ok() - }) - .collect(); - if !repo_ids.is_empty() { + if !repos.is_empty() { if let Ok(signed) = - SignedMessage::new_repo_sign_message(repo_ids, s2.node.clone()) + SignedMessage::new_repo_sign_message(repos, s2.node.clone()) { let env = Envelope { payload: signed, @@ -187,17 +175,33 @@ impl GossipService { na.alias, na.addresses, ); - // TODO: update NodeManager or Node routing table + + // 将节点信息保存到数据库 + let node_info = NodeInfo { + node_id: na.node_id.clone(), + alias: na.alias.clone(), + addresses: na.addresses.clone(), + node_type: na.node_type.clone(), + version: na.version, + }; + + if let Err(e) = node_model::save_node_info_to_db(&node_info).await { + tracing::warn!("Failed to save node info to db: {}", e); + } } GossipMessage::RepoAnnouncement(ra) => { tracing::info!( "Gossip: RepoAnnouncement from {} with {} repos: {:?}", ra.node_id, ra.repos.len(), - ra.repos.iter().map(|r| r.as_str()).collect::>() + ra.repos.iter().map(|r| &r.repo_id).collect::>() ); - // 可选:记录该节点拥有的 repo 信息到本地数据库(用于搜索/发现) - // 这里可以保存到一个 peer_repos 表用于跟踪哪个节点有哪些 repo + // 将每个 repo 保存到数据库(带空路径表示远程 repo) + for repo in &ra.repos { + if let Err(e) = crate::storage::repo_model::save_repo_to_db(repo).await { + tracing::warn!("Failed to save remote repo {} to db: {}", &repo.repo_id, e); + } + } } } diff --git a/src/main.rs b/src/main.rs index 941fc85..b7a868e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,12 @@ use anyhow::Result; use clap::{Parser, Subcommand}; +use megaengine::git::git_repo::{repo_name_space, repo_root_commit_bytes}; +use megaengine::git::pack::pack_repo_bundle; use megaengine::node::node_addr::NodeAddr; use std::net::SocketAddr; use megaengine::gossip::GossipService; use megaengine::{ - git::{repo_name_space, repo_root_commit_bytes}, node::node_id::NodeId, repo::{self, repo_id::RepoId}, storage, @@ -69,16 +70,6 @@ enum NodeAction { }, /// Print node id using stored keypair Id, - /// Connect to another node - Connect { - /// Target node's address (e.g., 127.0.0.1:9001) - #[arg(long)] - peer_addr: String, - - /// Target node's ID - #[arg(long)] - peer_id: String, - }, } #[derive(Subcommand)] @@ -95,6 +86,20 @@ enum RepoAction { }, /// List all repositories List, + /// Pack a repository into a distributable format + Pack { + /// Repository path to pack + #[arg(long)] + path: String, + + /// Output file path for the bundle + #[arg(long)] + output: String, + + /// Pack format: bundle, tar, or metadata + #[arg(long, default_value = "bundle")] + format: String, + }, } #[tokio::main] @@ -287,88 +292,106 @@ async fn main() -> Result<()> { let node_id = NodeId::from_keypair(&kp); println!("{}", node_id); } - NodeAction::Connect { peer_addr, peer_id } => { - // TODO: implement node connect - println!("Connect not yet implemented"); - println!("Would connect to {} with peer_id {}", peer_addr, peer_id); - } }, - Commands::Repo { action } => { - match action { - RepoAction::Add { path, description } => { - let kp = match storage::load_keypair() { - Ok(k) => k, - Err(e) => { - tracing::error!("failed to load keypair: {}", e); - tracing::info!("Run `auth init` first to generate keys"); - return Ok(()); - } - }; - let node_id = NodeId::from_keypair(&kp); + Commands::Repo { action } => match action { + RepoAction::Add { path, description } => { + let kp = match storage::load_keypair() { + Ok(k) => k, + Err(e) => { + tracing::error!("failed to load keypair: {}", e); + tracing::info!("Run `auth init` first to generate keys"); + return Ok(()); + } + }; + let node_id = NodeId::from_keypair(&kp); + + let root_bytes = match repo_root_commit_bytes(&path) { + Ok(b) => b, + Err(e) => { + tracing::error!("failed to read repo root commit: {}", e); + println!( + "Ensure the provided path is a git repository with at least one commit" + ); + return Ok(()); + } + }; - let root_bytes = match repo_root_commit_bytes(&path) { - Ok(b) => b, + let repo_id = + match RepoId::generate(root_bytes.as_slice(), &kp.verifying_key_bytes()) { + Ok(id) => id, Err(e) => { - tracing::error!("failed to read repo root commit: {}", e); - println!("Ensure the provided path is a git repository with at least one commit"); + tracing::error!("Failed to generate RepoId: {}", e); return Ok(()); } }; - let repo_id = - match RepoId::generate(root_bytes.as_slice(), &kp.verifying_key_bytes()) { - Ok(id) => id, - Err(e) => { - tracing::error!("Failed to generate RepoId: {}", e); - return Ok(()); - } - }; - - let name = repo_name_space(&path); - let desc = repo::repo::P2PDescription { - creator: node_id.to_string(), - name: name.clone(), - description: description.clone(), - timestamp: timestamp_now(), - }; + let name = repo_name_space(&path); + let desc = repo::repo::P2PDescription { + creator: node_id.to_string(), + name: name.clone(), + description: description.clone(), + timestamp: timestamp_now(), + }; - let repo = repo::repo::Repo::new( - repo_id.to_string(), - desc, - std::path::PathBuf::from(path), - ); + let repo = repo::repo::Repo::new( + repo_id.to_string(), + desc, + std::path::PathBuf::from(path), + ); - let mut manager = repo::repo_manager::RepoManager::new(); - match manager.register_repo(repo).await { - Ok(_) => tracing::info!("Repo {} added", repo_id), - Err(e) => tracing::info!("Failed to add repo: {}", e), - } + let mut manager = repo::repo_manager::RepoManager::new(); + match manager.register_repo(repo).await { + Ok(_) => tracing::info!("Repo {} added", repo_id), + Err(e) => tracing::info!("Failed to add repo: {}", e), } - RepoAction::List => match storage::repo_model::list_repos().await { - Ok(repos) => { - if repos.is_empty() { - println!("No repositories found"); - } else { - println!("Repositories:"); + } + RepoAction::List => match storage::repo_model::list_repos().await { + Ok(repos) => { + if repos.is_empty() { + println!("No repositories found"); + } else { + println!("Repositories:"); + println!("{}", "─".repeat(100)); + for repo in repos { + println!(" ID: {}", repo.repo_id); + println!(" Name: {}", repo.p2p_description.name); + println!(" Creator: {}", repo.p2p_description.creator); + println!(" Description: {}", repo.p2p_description.description); + println!(" Path: {}", repo.path.display()); + println!(" Timestamp: {}", repo.p2p_description.timestamp); println!("{}", "─".repeat(100)); - for repo in repos { - println!(" ID: {}", repo.repo_id); - println!(" Name: {}", repo.p2p_description.name); - println!(" Creator: {}", repo.p2p_description.creator); - println!(" Description: {}", repo.p2p_description.description); - println!(" Path: {}", repo.path.display()); - println!(" Timestamp: {}", repo.p2p_description.timestamp); - println!("{}", "─".repeat(100)); - } } } + } + Err(e) => { + tracing::error!("Failed to list repos: {}", e); + println!("Failed to list repositories: {}", e); + } + }, + RepoAction::Pack { + path, + output, + format, + } => match format.as_str() { + "bundle" => match pack_repo_bundle(&path, &output) { + Ok(_) => { + let file_size = std::fs::metadata(&output).map(|m| m.len()).unwrap_or(0); + println!("✓ Bundle created successfully"); + println!(" Output: {}", output); + println!(" Size: {} bytes", file_size); + } Err(e) => { - tracing::error!("Failed to list repos: {}", e); - println!("Failed to list repositories: {}", e); + eprintln!("✗ Failed to create bundle: {}", e); } }, - } - } + _ => { + eprintln!( + "✗ Unknown format: {}. Use 'bundle', 'tar', or 'metadata'", + format + ); + } + }, + }, } Ok(()) diff --git a/src/repo/repo_manager.rs b/src/repo/repo_manager.rs index aba3d98..45c0732 100644 --- a/src/repo/repo_manager.rs +++ b/src/repo/repo_manager.rs @@ -19,7 +19,6 @@ impl RepoManager { /// 注册仓库 pub async fn register_repo(&mut self, repo: Repo) -> Result<(), String> { save_repo_to_db(&repo).await.map_err(|e| e.to_string())?; - Ok(()) } diff --git a/src/transport/quic.rs b/src/transport/quic.rs index 437626c..34a146e 100644 --- a/src/transport/quic.rs +++ b/src/transport/quic.rs @@ -9,9 +9,11 @@ use tokio::sync::mpsc::Receiver; use tokio::sync::{mpsc, Mutex}; use tracing::{error, info}; +use std::time::Duration; use tokio::sync::mpsc::Sender as TokioSender; const READ_BUF_SIZE: usize = 1024 * 1024; +const CONNECTION_CLEANUP_INTERVAL: Duration = Duration::from_secs(30); // Type alias for incoming message sender to reduce type complexity type IncomingMessageSender = Arc)>>>>; @@ -32,15 +34,6 @@ pub struct QuicConnection { pub peer_addr: SocketAddr, pub node_id: NodeId, pub connection_type: ConnectionType, - pub connection_state: ConnectionState, -} - -#[derive(Debug, Clone)] -pub enum ConnectionState { - Connecting, - Connected, - Disconnected, - Failed, } #[derive(Debug, Clone)] @@ -82,6 +75,8 @@ impl ConnectionManager { let connections = Arc::clone(&manager.connections); let manager_clone = manager.clone(); + manager.start_connection_cleanup(); + tokio::spawn(async move { while let Some(incoming) = endpoint.accept().await { info!("Accepting connection from {}", incoming.remote_address()); @@ -154,7 +149,6 @@ impl ConnectionManager { peer_addr, node_id, connection_type: ConnectionType::Server, - connection_state: ConnectionState::Connected, }, message_rx, )) @@ -189,6 +183,32 @@ impl ConnectionManager { connections.keys().cloned().collect() } + /// Start background task to periodically clean up stale connections + pub fn start_connection_cleanup(&self) { + let connections = Arc::clone(&self.connections); + + tokio::spawn(async move { + let mut interval = tokio::time::interval(CONNECTION_CLEANUP_INTERVAL); + loop { + interval.tick().await; + let mut conns = connections.lock().await; + let mut dead_nodes = Vec::new(); + + for (node_id, conn) in conns.iter() { + if conn.connection.close_reason().is_some() { + info!("Connection to node[{}] closed", node_id); + dead_nodes.push(node_id.clone()); + } + } + + for node_id in dead_nodes { + conns.remove(&node_id); + info!("Cleaned up stale connection for node: {}", node_id); + } + } + }); + } + pub async fn connect( &self, self_node_id: NodeId, @@ -237,7 +257,6 @@ impl ConnectionManager { peer_addr, node_id: target_node_id.clone(), connection_type: ConnectionType::Client, - connection_state: ConnectionState::Connected, }; let connections = Arc::clone(&self.connections); connections diff --git a/tests/git_pack.rs b/tests/git_pack.rs new file mode 100644 index 0000000..59eb3f2 --- /dev/null +++ b/tests/git_pack.rs @@ -0,0 +1,253 @@ +use megaengine::git::pack::pack_repo_bundle; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +/// Ensure tmp directory exists +fn ensure_tmp_dir() -> PathBuf { + let tmp_path = PathBuf::from("tmp"); + if !tmp_path.exists() { + fs::create_dir(&tmp_path).expect("Failed to create tmp directory"); + } + tmp_path +} + +/// Helper function to run git commands +fn run_git_command(cwd: &str, args: &[&str]) -> bool { + let output = Command::new("git") + .current_dir(cwd) + .args(args) + .output() + .expect("Failed to execute git command"); + + output.status.success() +} + +/// Test pack_repo_bundle with complete workflow: +/// 1. Create a git repository +/// 2. Commit some files +/// 3. Pack into bundle +/// 4. Restore bundle into a new repository +#[test] +fn test_pack_repo_bundle() { + let tmp_dir = ensure_tmp_dir(); + + // Step 1: Create git repository 1 + let repo1_path = tmp_dir.join("repo1"); + if repo1_path.exists() { + fs::remove_dir_all(&repo1_path).ok(); + } + fs::create_dir(&repo1_path).expect("Failed to create repo1 directory"); + let repo1_str = repo1_path.to_str().unwrap(); + + println!("📁 Step 1: Creating git repository at {}", repo1_str); + + // Initialize git repo + assert!( + run_git_command(repo1_str, &["init"]), + "Failed to init git repo" + ); + assert!( + run_git_command(repo1_str, &["config", "user.email", "test@example.com"]), + "Failed to config user email" + ); + assert!( + run_git_command(repo1_str, &["config", "user.name", "Test User"]), + "Failed to config user name" + ); + + // Step 2: Create and commit files + println!("📝 Step 2: Creating and committing files"); + + // Create multiple files + fs::write( + repo1_path.join("README.md"), + "# Test Project\n\nThis is a test repository for bundle packing.", + ) + .expect("Failed to write README.md"); + + fs::write( + repo1_path.join("main.rs"), + "fn main() {\n println!(\"Hello!\");\n}\n", + ) + .expect("Failed to write main.rs"); + + fs::write(repo1_path.join("data.txt"), "Some data here\n").expect("Failed to write data.txt"); + + // Commit the files + assert!( + run_git_command(repo1_str, &["add", "."]), + "Failed to add files" + ); + assert!( + run_git_command( + repo1_str, + &["commit", "-m", "Initial commit with multiple files"] + ), + "Failed to commit" + ); + + // Create another commit + fs::write(repo1_path.join("second.txt"), "Second file\n").expect("Failed to write second.txt"); + assert!( + run_git_command(repo1_str, &["add", "."]), + "Failed to add second file" + ); + assert!( + run_git_command(repo1_str, &["commit", "-m", "Add second file"]), + "Failed to commit second file" + ); + + // Create a branch + assert!( + run_git_command(repo1_str, &["branch", "feature/dev"]), + "Failed to create branch" + ); + assert!( + run_git_command(repo1_str, &["checkout", "feature/dev"]), + "Failed to checkout branch" + ); + + fs::write(repo1_path.join("feature.txt"), "Feature branch file\n") + .expect("Failed to write feature.txt"); + assert!( + run_git_command(repo1_str, &["add", "."]), + "Failed to add feature file" + ); + assert!( + run_git_command(repo1_str, &["commit", "-m", "Add feature in dev branch"]), + "Failed to commit feature" + ); + + // Go back to main + if !run_git_command(repo1_str, &["checkout", "main"]) { + run_git_command(repo1_str, &["checkout", "master"]); + } + + // Create a tag + assert!( + run_git_command(repo1_str, &["tag", "v1.0"]), + "Failed to create tag" + ); + + println!("✅ Repository initialized with commits and branches"); + + // Step 3: Pack into bundle + let bundle_path = tmp_dir.join("repo1.bundle"); + let bundle_path_abs = std::env::current_dir().unwrap().join(&bundle_path); + println!( + "📦 Step 3: Packing repository into bundle: {}", + bundle_path_abs.display() + ); + + match pack_repo_bundle(repo1_str, bundle_path_abs.to_str().unwrap()) { + Ok(_) => { + assert!( + bundle_path_abs.exists(), + "Bundle file was not created at {}", + bundle_path_abs.display() + ); + let bundle_size = fs::metadata(&bundle_path_abs) + .expect("Failed to read bundle metadata") + .len(); + println!("✅ Bundle created successfully: {} bytes", bundle_size); + assert!(bundle_size > 0, "Bundle file is empty"); + } + Err(e) => { + panic!("Failed to create bundle: {}", e); + } + } + + // Step 4: Restore bundle into new repository + let repo2_path = tmp_dir.join("repo2_restored"); + if repo2_path.exists() { + fs::remove_dir_all(&repo2_path).ok(); + } + + let repo2_path_abs = std::env::current_dir().unwrap().join(&repo2_path); + + println!( + "🔄 Step 4: Restoring bundle to new repository at {}", + repo2_path_abs.display() + ); + + let repo2_str = repo2_path_abs.to_str().unwrap(); + + // Clone from bundle + assert!( + run_git_command( + tmp_dir.to_str().unwrap(), + &["clone", bundle_path_abs.to_str().unwrap(), repo2_str] + ), + "Failed to clone from bundle" + ); + + println!("✅ Repository restored from bundle"); + + // Step 5: Verify restored repository + println!("✓ Step 5: Verifying restored repository"); + + // Check that main branch exists + let output = Command::new("git") + .current_dir(repo2_str) + .args(&["branch", "-a"]) + .output() + .expect("Failed to list branches"); + let branches = String::from_utf8_lossy(&output.stdout); + println!("Branches in restored repo:\n{}", branches); + + // Check that files exist + assert!( + repo2_path_abs.join("README.md").exists(), + "README.md not restored" + ); + assert!( + repo2_path_abs.join("main.rs").exists(), + "main.rs not restored" + ); + assert!( + repo2_path_abs.join("data.txt").exists(), + "data.txt not restored" + ); + + // Check commit history + let output = Command::new("git") + .current_dir(repo2_str) + .args(&["log", "--oneline"]) + .output() + .expect("Failed to get log"); + let log = String::from_utf8_lossy(&output.stdout); + println!("Commit history in restored repo:\n{}", log); + assert!( + log.contains("Initial commit"), + "Initial commit not found in restored repo" + ); + + // Check tags + let output = Command::new("git") + .current_dir(repo2_str) + .args(&["tag"]) + .output() + .expect("Failed to list tags"); + let tags = String::from_utf8_lossy(&output.stdout); + println!("Tags in restored repo:\n{}", tags); + // Note: Tags might not be included in the bundle by default + + println!("✅ All verifications passed!"); + println!("\n📊 Summary:"); + println!(" Original repo: {}", repo1_path.display()); + println!( + " Bundle file: {} ({} bytes)", + bundle_path_abs.display(), + fs::metadata(&bundle_path_abs).unwrap().len() + ); + println!(" Restored repo: {}", repo2_path_abs.display()); + println!("✅ Bundle pack/restore test completed successfully!"); + + // Cleanup: Remove test directories and bundle + println!("\n🧹 Cleaning up temporary directories..."); + fs::remove_dir_all(&repo1_path).ok(); + fs::remove_dir_all(&repo2_path_abs).ok(); + fs::remove_file(&bundle_path_abs).ok(); + println!("✅ Cleanup completed!"); +} From 13b526f97f059108e9f50ed3c9a40e18801ee400 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Wed, 26 Nov 2025 15:19:55 +0800 Subject: [PATCH 09/42] fix gossip signed msg --- src/gossip/message.rs | 1 + src/gossip/service.rs | 25 +++++++++++-------------- src/storage/mod.rs | 5 ----- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/gossip/message.rs b/src/gossip/message.rs index bc68813..c24bec3 100644 --- a/src/gossip/message.rs +++ b/src/gossip/message.rs @@ -110,6 +110,7 @@ impl SignedMessage { hasher.update(self.node_id.0.as_bytes()); hasher.update(&message_bytes); + hasher.update(self.timestamp.to_le_bytes()); hasher.finalize().to_vec() } diff --git a/src/gossip/service.rs b/src/gossip/service.rs index 739d731..dabd3ac 100644 --- a/src/gossip/service.rs +++ b/src/gossip/service.rs @@ -25,6 +25,12 @@ pub struct GossipService { seen: Arc>>, } +#[derive(Serialize, Deserialize, Clone)] +struct Envelope { + payload: SignedMessage, + ttl: u8, +} + impl GossipService { pub fn new( manager: Arc>, @@ -61,12 +67,6 @@ impl GossipService { let s2 = Arc::clone(&self); tokio::spawn(async move { loop { - #[derive(Serialize, Deserialize, Clone)] - struct Envelope { - payload: SignedMessage, - ttl: u8, - } - // 1. 发送 NodeAnnouncement if let Ok(signed) = SignedMessage::new_node_sign_message(s2.node.clone()) { let env = Envelope { @@ -76,6 +76,7 @@ impl GossipService { let data = serde_json::to_vec(&env).unwrap_or_default(); let mgr = s2.manager.lock().await; let peers = mgr.list_peers().await; + tracing::debug!("Send NodeAnnouncement to {} peers", peers.len()); for peer in peers { let _ = mgr.send_message(peer.clone(), data.clone()).await; } @@ -121,11 +122,6 @@ impl GossipService { async fn handle_incoming(&self, from: NodeId, data: Vec) -> Result<()> { // Try parse as Envelope (with ttl). If not, fall back to raw SignedMessage. - #[derive(Serialize, Deserialize, Clone)] - struct Envelope { - payload: SignedMessage, - ttl: u8, - } let (signed, mut ttl) = if let Ok(env) = serde_json::from_slice::(&data) { (env.payload, env.ttl) @@ -169,11 +165,12 @@ impl GossipService { // process message (borrow the inner message to avoid moving) match &signed.message { GossipMessage::NodeAnnouncement(na) => { - tracing::info!( - "Gossip: NodeAnnouncement from {} (alias: {}, addresses: {:?})", + tracing::debug!( + "Gossip: NodeAnnouncement from {} (alias: {}, addresses: {:?}, timestamp: {})", na.node_id, na.alias, na.addresses, + signed.timestamp(), ); // 将节点信息保存到数据库 @@ -190,7 +187,7 @@ impl GossipService { } } GossipMessage::RepoAnnouncement(ra) => { - tracing::info!( + tracing::debug!( "Gossip: RepoAnnouncement from {} with {} repos: {:?}", ra.node_id, ra.repos.len(), diff --git a/src/storage/mod.rs b/src/storage/mod.rs index a4b3314..b321ad8 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -190,11 +190,6 @@ mod tests { "Loaded keypair should match saved keypair" ); - // cleanup - let path = keypair_path(); - if path.exists() { - fs::remove_file(path)?; - } Ok(()) } } From 6c6932c18420963135d126ab664da9ac0ae4cec3 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Wed, 26 Nov 2025 16:04:54 +0800 Subject: [PATCH 10/42] update test --- src/gossip/message.rs | 1 + src/gossip/service.rs | 18 ++++++++++--- src/main.rs | 7 ++--- src/node/node_manager.rs | 18 +++++++++++++ src/repo/repo.rs | 4 +++ src/repo/repo_manager.rs | 21 ++++++++------- src/storage/mod.rs | 2 ++ src/storage/repo_model.rs | 53 ++++++++++++++++++++----------------- tests/gossip_three_nodes.rs | 9 +++++++ 9 files changed, 90 insertions(+), 43 deletions(-) diff --git a/src/gossip/message.rs b/src/gossip/message.rs index c24bec3..17930f1 100644 --- a/src/gossip/message.rs +++ b/src/gossip/message.rs @@ -83,6 +83,7 @@ impl SignedMessage { .into_iter() .map(|mut repo| { repo.path = std::path::PathBuf::new(); + repo.bundle = std::path::PathBuf::new(); repo }) .collect(); diff --git a/src/gossip/service.rs b/src/gossip/service.rs index dabd3ac..03f5a37 100644 --- a/src/gossip/service.rs +++ b/src/gossip/service.rs @@ -165,7 +165,7 @@ impl GossipService { // process message (borrow the inner message to avoid moving) match &signed.message { GossipMessage::NodeAnnouncement(na) => { - tracing::debug!( + tracing::info!( "Gossip: NodeAnnouncement from {} (alias: {}, addresses: {:?}, timestamp: {})", na.node_id, na.alias, @@ -187,15 +187,25 @@ impl GossipService { } } GossipMessage::RepoAnnouncement(ra) => { - tracing::debug!( + tracing::info!( "Gossip: RepoAnnouncement from {} with {} repos: {:?}", ra.node_id, ra.repos.len(), ra.repos.iter().map(|r| &r.repo_id).collect::>() ); - // 将每个 repo 保存到数据库(带空路径表示远程 repo) + // 将每个 repo 保存到数据库 for repo in &ra.repos { - if let Err(e) = crate::storage::repo_model::save_repo_to_db(repo).await { + // 检查仓库是否已存在,如果存在则跳过 + if let Ok(Some(_)) = + crate::storage::repo_model::load_repo_from_db(&repo.repo_id).await + { + tracing::debug!("Repo {} already exists, skipping", &repo.repo_id); + continue; + } + + let mut repo = repo.clone(); + repo.is_external = true; + if let Err(e) = crate::storage::repo_model::save_repo_to_db(&repo).await { tracing::warn!("Failed to save remote repo {} to db: {}", &repo.repo_id, e); } } diff --git a/src/main.rs b/src/main.rs index b7a868e..911607b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use megaengine::git::git_repo::{repo_name_space, repo_root_commit_bytes}; use megaengine::git::pack::pack_repo_bundle; use megaengine::node::node_addr::NodeAddr; use std::net::SocketAddr; +use std::path::PathBuf; use megaengine::gossip::GossipService; use megaengine::{ @@ -333,11 +334,7 @@ async fn main() -> Result<()> { timestamp: timestamp_now(), }; - let repo = repo::repo::Repo::new( - repo_id.to_string(), - desc, - std::path::PathBuf::from(path), - ); + let repo = repo::repo::Repo::new(repo_id.to_string(), desc, PathBuf::from(path)); let mut manager = repo::repo_manager::RepoManager::new(); match manager.register_repo(repo).await { diff --git a/src/node/node_manager.rs b/src/node/node_manager.rs index de11595..65ee83f 100644 --- a/src/node/node_manager.rs +++ b/src/node/node_manager.rs @@ -70,6 +70,7 @@ mod tests { async fn test_node_manager_insert_node() { let mut manager = NodeManager::new(); let node = create_sample_node(); + let node_id = node.node_id().clone(); // Insert the node manager.insert_node(&node).await; @@ -79,12 +80,16 @@ mod tests { let node_routing = manager.get_node(&node.node_id()); assert!(node_routing.is_some()); assert_eq!(node_routing.unwrap().node_id, *node.node_id()); + + // Cleanup: Remove from database + let _ = crate::storage::node_model::delete_node_from_db(&node_id.to_string()).await; } #[tokio::test] async fn test_node_manager_mark_alive() { let mut manager = NodeManager::new(); let node = create_sample_node(); + let node_id = node.node_id().clone(); // Insert the node manager.insert_node(&node).await; @@ -98,12 +103,16 @@ mod tests { // Assert that the last_seen time was refreshed let refreshed_last_seen = manager.get_node(&node.node_id()).unwrap().last_seen; assert_ne!(initial_last_seen, refreshed_last_seen); + + // Cleanup: Remove from database + let _ = crate::storage::node_model::delete_node_from_db(&node_id.to_string()).await; } #[tokio::test] async fn test_node_manager_cleanup_expired() { let mut manager = NodeManager::new(); let node = create_sample_node(); + let node_id = node.node_id().clone(); manager.insert_node(&node).await; assert_eq!(manager.nodes.len(), 1); @@ -112,6 +121,9 @@ mod tests { std::thread::sleep(std::time::Duration::from_secs(2)); manager.cleanup_expired(); assert_eq!(manager.nodes.len(), 0); + + // Cleanup: Remove from database + let _ = crate::storage::node_model::delete_node_from_db(&node_id.to_string()).await; } #[tokio::test] @@ -119,6 +131,8 @@ mod tests { let mut manager = NodeManager::new(); let node1 = create_sample_node(); let node2 = create_sample_node(); + let node_id_1 = node1.node_id().clone(); + let node_id_2 = node2.node_id().clone(); manager.insert_node(&node1).await; manager.insert_node(&node2).await; @@ -126,5 +140,9 @@ mod tests { let _ = std::panic::catch_unwind(|| { manager.routing_print(); }); + + // Cleanup: Remove from database + let _ = crate::storage::node_model::delete_node_from_db(&node_id_1.to_string()).await; + let _ = crate::storage::node_model::delete_node_from_db(&node_id_2.to_string()).await; } } diff --git a/src/repo/repo.rs b/src/repo/repo.rs index 826b4e2..fc5ab66 100644 --- a/src/repo/repo.rs +++ b/src/repo/repo.rs @@ -18,6 +18,8 @@ pub struct Repo { pub refs: HashMap, pub p2p_description: P2PDescription, pub path: PathBuf, + pub is_external: bool, + pub bundle: PathBuf, } impl Repo { @@ -28,6 +30,8 @@ impl Repo { refs: HashMap::new(), p2p_description, path, + is_external: false, + bundle: PathBuf::new(), } } diff --git a/src/repo/repo_manager.rs b/src/repo/repo_manager.rs index 45c0732..7b52bd0 100644 --- a/src/repo/repo_manager.rs +++ b/src/repo/repo_manager.rs @@ -92,6 +92,7 @@ mod tests { async fn test_repo_manager() -> Result<()> { let mut manager = RepoManager::new(); + let repo_id = "did:repo:test"; let desc = P2PDescription { creator: "did:key:test".to_string(), name: "test-repo".to_string(), @@ -99,19 +100,18 @@ mod tests { timestamp: 1000, }; - let repo = Repo::new( - "did:repo:test".to_string(), - desc, - PathBuf::from("/tmp/test-repo"), - ); + let repo = Repo::new(repo_id.to_string(), desc, PathBuf::from("/tmp/test-repo")); let before = manager.repo_count().await?; assert!(manager.register_repo(repo).await.is_ok()); let count = manager.repo_count().await?; - // 可能存在旧数据,确保数量不减少并且能通过 id 读取到刚注册的 repo assert!(count >= before); - let loaded = manager.get_repo("did:repo:test").await?; + let loaded = manager.get_repo(repo_id).await?; assert!(loaded.is_some()); + + // 清理测试数据 + manager.remove_repo(repo_id).await?; + Ok(()) } @@ -120,6 +120,7 @@ mod tests { // 持久化现在为默认行为 let mut manager = RepoManager::new(); + let repo_id = "did:repo:test-persist"; let desc = P2PDescription { creator: "did:key:test".to_string(), name: "test-repo-persist".to_string(), @@ -128,7 +129,7 @@ mod tests { }; let repo = Repo::new( - "did:repo:test-persist".to_string(), + repo_id.to_string(), desc, PathBuf::from("/tmp/test-repo-persist"), ); @@ -139,11 +140,11 @@ mod tests { assert!(count >= before); // 删除仓库 - let result = manager.remove_repo("did:repo:test-persist").await; + let result = manager.remove_repo(repo_id).await; assert!(result.is_ok()); assert!(result.unwrap().is_some()); // 验证数据库中已删除该 repo - let loaded_after = manager.get_repo("did:repo:test-persist").await?; + let loaded_after = manager.get_repo(repo_id).await?; assert!(loaded_after.is_none()); Ok(()) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index b321ad8..5c53db0 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -115,6 +115,8 @@ pub async fn init_db() -> Result { timestamp INTEGER NOT NULL, refs TEXT NOT NULL, path TEXT NOT NULL, + bundle TEXT NOT NULL DEFAULT '', + is_external INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL )", diff --git a/src/storage/repo_model.rs b/src/storage/repo_model.rs index 7671f6b..6315862 100644 --- a/src/storage/repo_model.rs +++ b/src/storage/repo_model.rs @@ -17,6 +17,8 @@ pub struct Model { pub timestamp: i64, pub refs: String, pub path: String, + pub bundle: String, + pub is_external: bool, pub created_at: i64, pub updated_at: i64, } @@ -45,6 +47,8 @@ pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { timestamp: Set(repo.p2p_description.timestamp), refs: Set(refs_json), path: Set(repo.path.to_string_lossy().to_string()), + bundle: Set(repo.bundle.to_string_lossy().to_string()), + is_external: Set(repo.is_external), created_at: Unchanged(existing_model.created_at), updated_at: Set(now), }; @@ -59,6 +63,8 @@ pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { timestamp: Set(repo.p2p_description.timestamp), refs: Set(refs_json), path: Set(repo.path.to_string_lossy().to_string()), + bundle: Set(repo.bundle.to_string_lossy().to_string()), + is_external: Set(repo.is_external), created_at: Set(now), updated_at: Set(now), }; @@ -72,27 +78,24 @@ pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { pub async fn load_repo_from_db(repo_id: &str) -> Result> { let db = init_db().await?; - // 使用 find() 来查询所有,然后筛选 - let models = Entity::find().all(&db).await?; + // 使用 find_by_id 直接查询 + if let Some(model) = Entity::find_by_id(repo_id).one(&db).await? { + let refs: std::collections::HashMap = serde_json::from_str(&model.refs)?; - for model in models { - if model.id == repo_id { - let refs: std::collections::HashMap = - serde_json::from_str(&model.refs)?; - - let repo = Repo { - repo_id: model.id, - refs, - p2p_description: crate::repo::repo::P2PDescription { - creator: model.creator, - name: model.name, - description: model.description, - timestamp: model.timestamp, - }, - path: PathBuf::from(model.path), - }; - return Ok(Some(repo)); - } + let repo = Repo { + repo_id: model.id, + refs, + p2p_description: crate::repo::repo::P2PDescription { + creator: model.creator, + name: model.name, + description: model.description, + timestamp: model.timestamp, + }, + path: PathBuf::from(model.path), + bundle: PathBuf::from(model.bundle), + is_external: model.is_external, + }; + return Ok(Some(repo)); } Ok(None) @@ -125,6 +128,8 @@ pub async fn list_repos() -> Result> { timestamp: model.timestamp, }, path: PathBuf::from(model.path), + bundle: PathBuf::from(model.bundle), + is_external: model.is_external, }); } Ok(repos) @@ -138,14 +143,14 @@ mod tests { async fn test_save_and_load_repo() -> Result<()> { // 创建测试 Repo let desc = crate::repo::repo::P2PDescription { - creator: "did:node:test".to_string(), + creator: "did:node:test333".to_string(), name: "test-repo".to_string(), description: "A test repository".to_string(), timestamp: 1000, }; let mut repo = Repo::new( - "did:repo:test".to_string(), + "did:repo:test333".to_string(), desc, PathBuf::from("/tmp/test-repo"), ); @@ -155,7 +160,7 @@ mod tests { save_repo_to_db(&repo).await?; // 从数据库加载 - let loaded = load_repo_from_db("did:repo:test").await?; + let loaded = load_repo_from_db("did:repo:test333").await?; assert!(loaded.is_some()); let loaded_repo = loaded.unwrap(); @@ -167,7 +172,7 @@ mod tests { ); // 清理 - delete_repo_from_db("did:repo:test").await?; + delete_repo_from_db("did:repo:test333").await?; Ok(()) } diff --git a/tests/gossip_three_nodes.rs b/tests/gossip_three_nodes.rs index 435021d..2584165 100644 --- a/tests/gossip_three_nodes.rs +++ b/tests/gossip_three_nodes.rs @@ -2,6 +2,7 @@ use megaengine::gossip::{GossipService, SignedMessage}; use megaengine::identity::keypair::KeyPair; use megaengine::node::node::{Node, NodeType}; +use megaengine::storage::node_model; use megaengine::transport::config::QuicConfig; use std::net::SocketAddr; use std::sync::Arc; @@ -146,4 +147,12 @@ async fn test_gossip_three_nodes_message_relay() { // 这里只能通过日志人工观察传播效果,或后续扩展 GossipService 提供 hook/回调收集消息 sleep(Duration::from_secs(1)).await; + + // Cleanup: Remove nodes from database + let node_id_1 = node1.node_id().to_string(); + let node_id_2 = node2.node_id().to_string(); + let node_id_3 = node3.node_id().to_string(); + let _ = node_model::delete_node_from_db(&node_id_1).await; + let _ = node_model::delete_node_from_db(&node_id_2).await; + let _ = node_model::delete_node_from_db(&node_id_3).await; } From b3d1c6a7542cebcc68706a33d896fc457c25248a Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Wed, 26 Nov 2025 17:06:29 +0800 Subject: [PATCH 11/42] transfer bundle --- src/bundle/mod.rs | 5 + src/bundle/service.rs | 71 +++++++ src/bundle/transfer.rs | 292 ++++++++++++++++++++++++++ src/git/mod.rs | 2 - src/gossip/service.rs | 20 +- src/lib.rs | 1 + src/main.rs | 13 +- src/repo/repo_manager.rs | 42 +++- src/transport/quic.rs | 140 ++++++++++--- tests/bundle_two_nodes.rs | 407 ++++++++++++++++++++++++++++++++++++ tests/gossip_three_nodes.rs | 3 +- 11 files changed, 949 insertions(+), 47 deletions(-) create mode 100644 src/bundle/mod.rs create mode 100644 src/bundle/service.rs create mode 100644 src/bundle/transfer.rs create mode 100644 tests/bundle_two_nodes.rs diff --git a/src/bundle/mod.rs b/src/bundle/mod.rs new file mode 100644 index 0000000..3dce96a --- /dev/null +++ b/src/bundle/mod.rs @@ -0,0 +1,5 @@ +pub mod service; +pub mod transfer; + +pub use service::BundleService; +pub use transfer::BundleTransferManager; diff --git a/src/bundle/service.rs b/src/bundle/service.rs new file mode 100644 index 0000000..792e96e --- /dev/null +++ b/src/bundle/service.rs @@ -0,0 +1,71 @@ +use crate::bundle::transfer::BundleTransferManager; +use crate::node::node_id::NodeId; +use crate::transport::quic::ConnectionManager; +use anyhow::Result; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::mpsc; +use tokio::sync::Mutex; + +/// Bundle 传输服务 +/// +/// 负责处理 bundle 文件的接收和发送, +pub struct BundleService { + connection_manager: Arc>, + bundle_manager: Arc, +} + +impl BundleService { + /// 创建新的 BundleService + pub fn new(connection_manager: Arc>, storage_dir: PathBuf) -> Self { + let bundle_manager = Arc::new(BundleTransferManager::new( + connection_manager.clone(), + storage_dir, + )); + + Self { + connection_manager, + bundle_manager, + } + } + + /// 启动 Bundle 服务:注册 data_sender 并处理接收的 bundle 消息 + pub async fn start(self: Arc) -> Result<()> { + // 注册数据传输接收器 + let (data_tx, mut data_rx) = mpsc::channel::<(NodeId, Vec)>(256); + + { + let mgr = self.connection_manager.lock().await; + mgr.register_data_sender(data_tx).await; + } + + // Bundle 数据处理任务 + let s = Arc::clone(&self); + tokio::spawn(async move { + while let Some((from, data)) = data_rx.recv().await { + if let Err(e) = s.bundle_manager.handle_bundle_message(from, data).await { + tracing::warn!("Failed to handle bundle message: {}", e); + } + } + }); + + Ok(()) + } + + /// 发送 bundle 文件到指定节点 + pub async fn send_bundle( + &self, + target_node_id: NodeId, + repo_id: String, + bundle_path: &str, + ) -> Result<()> { + self.bundle_manager + .send_bundle(target_node_id, repo_id, bundle_path) + .await + } + + /// 获取接收的 bundle 文件路径 + pub fn get_bundle_path(&self, from: &NodeId, repo_id: &str) -> PathBuf { + self.bundle_manager.get_bundle_path(from, repo_id) + } +} diff --git a/src/bundle/transfer.rs b/src/bundle/transfer.rs new file mode 100644 index 0000000..072efae --- /dev/null +++ b/src/bundle/transfer.rs @@ -0,0 +1,292 @@ +use crate::node::node_id::NodeId; +use crate::transport::quic::ConnectionManager; +use anyhow::Context; +use anyhow::Result; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::fs; +use tokio::sync::Mutex; +use tracing::{debug, info, warn}; + +/// Bundle 消息类型(用于多帧传输) +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +enum BundleMessageType { + /// 开始传输:包含文件元数据 + Start { + repo_id: String, + file_name: String, + total_size: u64, + }, + /// 数据块:包含分块数据 + Chunk { + repo_id: String, + chunk_idx: u32, + data: Vec, + }, + /// 传输完成 + Done { repo_id: String }, +} + +/// Bundle 文件传输管理器 +pub struct BundleTransferManager { + connection_manager: Arc>, + storage_dir: PathBuf, +} + +impl BundleTransferManager { + /// 创建新的 BundleTransferManager + pub fn new(connection_manager: Arc>, storage_dir: PathBuf) -> Self { + Self { + connection_manager, + storage_dir, + } + } + + /// 发送 bundle 文件到指定节点 + /// + /// # Arguments + /// * `target_node_id` - 目标节点 ID + /// * `repo_id` - 仓库 ID + /// * `bundle_path` - bundle 文件路径 + /// + /// # Example + /// ```ignore + /// manager.send_bundle(peer_id, "repo123", "path/to/bundle").await?; + /// ``` + pub async fn send_bundle( + &self, + target_node_id: NodeId, + repo_id: String, + bundle_path: &str, + ) -> Result<()> { + // 读取 bundle 文件 + let path = Path::new(bundle_path); + let bundle_data = fs::read(path).await.context("Failed to read bundle file")?; + + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("repo.bundle") + .to_string(); + + let total_size = bundle_data.len() as u64; + + info!( + "Sending bundle {} ({} bytes) to node {}", + file_name, total_size, target_node_id + ); + + let mgr = self.connection_manager.lock().await; + + // 1. 发送 START 消息 + let start_msg = BundleMessageType::Start { + repo_id: repo_id.clone(), + file_name: file_name.clone(), + total_size, + }; + let start_payload = serde_json::to_vec(&start_msg).context("Failed to serialize START")?; + mgr.send_data_message(target_node_id.clone(), start_payload) + .await + .context("Failed to send START message")?; + + // 2. 分块发送数据 + const CHUNK_SIZE: usize = 64 * 1024; // 64KB per chunk + for (chunk_idx, chunk) in bundle_data.chunks(CHUNK_SIZE).enumerate() { + let chunk_msg = BundleMessageType::Chunk { + repo_id: repo_id.clone(), + chunk_idx: chunk_idx as u32, + data: chunk.to_vec(), + }; + let chunk_payload = + serde_json::to_vec(&chunk_msg).context("Failed to serialize CHUNK")?; + + mgr.send_data_message(target_node_id.clone(), chunk_payload) + .await + .context("Failed to send CHUNK message")?; + + debug!( + "Sent chunk {} ({} bytes) for repo {}", + chunk_idx, + chunk.len(), + repo_id + ); + } + + // 3. 发送 DONE 消息 + let done_msg = BundleMessageType::Done { + repo_id: repo_id.clone(), + }; + let done_payload = serde_json::to_vec(&done_msg).context("Failed to serialize DONE")?; + mgr.send_data_message(target_node_id.clone(), done_payload) + .await + .context("Failed to send DONE message")?; + + info!( + "Bundle {} sent successfully to node {} ({} chunks)", + file_name, + target_node_id, + bundle_data.chunks(CHUNK_SIZE).count() + ); + + Ok(()) + } + + /// 处理接收的 bundle 消息流 + /// + /// 这个方法应该由接收 data_sender 的处理器调用 + pub async fn handle_bundle_message(&self, from: NodeId, data: Vec) -> Result<()> { + // 反序列化消息 + let msg: BundleMessageType = + serde_json::from_slice(&data).context("Failed to deserialize bundle message")?; + + match msg { + BundleMessageType::Start { + repo_id, + file_name, + total_size, + } => { + self.handle_bundle_start(&from, &repo_id, &file_name, total_size) + .await + } + BundleMessageType::Chunk { + repo_id, + chunk_idx, + data, + } => { + self.handle_bundle_chunk(&from, &repo_id, chunk_idx, data) + .await + } + BundleMessageType::Done { repo_id } => self.handle_bundle_done(&from, &repo_id).await, + } + } + + /// 将 NodeId 编码为合法的目录名(替换非法字符) + fn encode_node_id(node_id: &NodeId) -> String { + let id_str = node_id.to_string(); + // 将 : 替换为 _,其他非法字符也替换 + id_str.replace(':', "_").replace('/', "_") + } + + /// 处理 START 消息 + async fn handle_bundle_start( + &self, + from: &NodeId, + repo_id: &str, + file_name: &str, + total_size: u64, + ) -> Result<()> { + let encoded_id = Self::encode_node_id(from); + let dir = self.storage_dir.join(&encoded_id); + fs::create_dir_all(&dir) + .await + .context("Failed to create bundle storage directory")?; + + info!( + "Bundle transfer START from {}: repo={}, file={}, size={} bytes", + from, repo_id, file_name, total_size + ); + + Ok(()) + } + + /// 处理 CHUNK 消息 + async fn handle_bundle_chunk( + &self, + from: &NodeId, + repo_id: &str, + chunk_idx: u32, + data: Vec, + ) -> Result<()> { + let encoded_id = Self::encode_node_id(from); + let dir = self.storage_dir.join(&encoded_id); + let file_path = dir.join(format!("{}.bundle", repo_id)); + + // 追加写入到文件 + let mut file = fs::OpenOptions::new() + .create(true) + .append(true) + .open(&file_path) + .await + .context("Failed to open bundle file for appending")?; + + use tokio::io::AsyncWriteExt; + file.write_all(&data) + .await + .context("Failed to write chunk data")?; + + debug!( + "Received chunk {} ({} bytes) for repo {} from {}", + chunk_idx, + data.len(), + repo_id, + from + ); + + Ok(()) + } + + /// 处理 DONE 消息 + async fn handle_bundle_done(&self, from: &NodeId, repo_id: &str) -> Result<()> { + let encoded_id = Self::encode_node_id(from); + let dir = self.storage_dir.join(&encoded_id); + let file_path = dir.join(format!("{}.bundle", repo_id)); + + if file_path.exists() { + let metadata = fs::metadata(&file_path) + .await + .context("Failed to get bundle file metadata")?; + + info!( + "Bundle transfer completed from {}: repo={}, file_size={} bytes", + from, + repo_id, + metadata.len() + ); + } else { + warn!( + "Bundle transfer DONE message received but file not found for repo {} from {}", + repo_id, from + ); + } + + Ok(()) + } + + /// 获取从指定节点接收的 bundle 文件路径 + pub fn get_bundle_path(&self, from: &NodeId, repo_id: &str) -> PathBuf { + let encoded_id = Self::encode_node_id(from); + self.storage_dir + .join(&encoded_id) + .join(format!("{}.bundle", repo_id)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bundle_message_serialization() { + let msg = BundleMessageType::Start { + repo_id: "repo123".to_string(), + file_name: "repo.bundle".to_string(), + total_size: 1024, + }; + + let serialized = serde_json::to_vec(&msg).unwrap(); + let deserialized: BundleMessageType = serde_json::from_slice(&serialized).unwrap(); + + match deserialized { + BundleMessageType::Start { + repo_id, + file_name, + total_size, + } => { + assert_eq!(repo_id, "repo123"); + assert_eq!(file_name, "repo.bundle"); + assert_eq!(total_size, 1024); + } + _ => panic!("Wrong message type"), + } + } +} diff --git a/src/git/mod.rs b/src/git/mod.rs index 910b431..4775df0 100644 --- a/src/git/mod.rs +++ b/src/git/mod.rs @@ -1,4 +1,2 @@ - - pub mod git_repo; pub mod pack; diff --git a/src/gossip/service.rs b/src/gossip/service.rs index 03f5a37..b085947 100644 --- a/src/gossip/service.rs +++ b/src/gossip/service.rs @@ -16,7 +16,7 @@ use tokio::sync::{mpsc, Mutex}; const DEFAULT_TTL: u8 = 16; -/// 简单的 gossip 服务:接收来自 QUIC 的消息,去重、验签、处理并转发给邻居 +/// 简单的 gossip 服务:接收来自 QUIC 的 Gossip 控制消息,去重、验签、处理并转发给邻居 #[allow(dead_code)] pub struct GossipService { manager: Arc>, @@ -45,20 +45,20 @@ impl GossipService { } } - /// Start the gossip service: register incoming channel and spawn handler + periodic broadcaster + /// Start the gossip service: register gossip channel and spawn handler + periodic broadcaster pub async fn start(self: Arc) -> Result<()> { - let (tx, mut rx) = mpsc::channel::<(NodeId, Vec)>(256); + // 注册 Gossip 控制消息接收器 + let (gossip_tx, mut gossip_rx) = mpsc::channel::<(NodeId, Vec)>(256); - // register incoming sender with connection manager { let mgr = self.manager.lock().await; - mgr.register_incoming_sender(tx).await; + mgr.register_gossip_sender(gossip_tx).await; } - // clone for handler task + // Gossip 消息处理任务 let s = Arc::clone(&self); tokio::spawn(async move { - while let Some((from, data)) = rx.recv().await { + while let Some((from, data)) = gossip_rx.recv().await { let _ = s.handle_incoming(from, data).await; } }); @@ -78,7 +78,7 @@ impl GossipService { let peers = mgr.list_peers().await; tracing::debug!("Send NodeAnnouncement to {} peers", peers.len()); for peer in peers { - let _ = mgr.send_message(peer.clone(), data.clone()).await; + let _ = mgr.send_gossip_message(peer.clone(), data.clone()).await; } } @@ -96,7 +96,7 @@ impl GossipService { let mgr = s2.manager.lock().await; let peers = mgr.list_peers().await; for peer in peers { - let _ = mgr.send_message(peer.clone(), data.clone()).await; + let _ = mgr.send_gossip_message(peer.clone(), data.clone()).await; } } } @@ -231,7 +231,7 @@ impl GossipService { if peer == from { continue; } - let _ = mgr.send_message(peer.clone(), data.clone()).await; + let _ = mgr.send_gossip_message(peer.clone(), data.clone()).await; } } diff --git a/src/lib.rs b/src/lib.rs index 47f8c9f..ef483b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod bundle; pub mod git; pub mod gossip; pub mod identity; diff --git a/src/main.rs b/src/main.rs index 911607b..cd17eab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use anyhow::Result; use clap::{Parser, Subcommand}; +use megaengine::bundle::BundleService; use megaengine::git::git_repo::{repo_name_space, repo_root_commit_bytes}; use megaengine::git::pack::pack_repo_bundle; use megaengine::node::node_addr::NodeAddr; @@ -200,6 +201,7 @@ async fn main() -> Result<()> { node.start_quic_server(quic_config).await?; if let Some(conn_mgr) = &node.connection_manager { + // 启动 Gossip 服务 let gossip = std::sync::Arc::new(GossipService::new( std::sync::Arc::clone(conn_mgr), node.clone(), @@ -207,8 +209,17 @@ async fn main() -> Result<()> { )); tokio::spawn(gossip.start()); tracing::info!("Gossip protocol started"); + + // 启动 Bundle 传输服务 + let bundle_storage = std::path::PathBuf::from("./data/bundles"); + let bundle_service = std::sync::Arc::new(BundleService::new( + std::sync::Arc::clone(conn_mgr), + bundle_storage, + )); + tokio::spawn(bundle_service.start()); + tracing::info!("Bundle transfer service started"); } else { - tracing::warn!("No connection manager found, gossip not started"); + tracing::warn!("No connection manager found, services not started"); } // 如果提供了 bootstrap_node,尝试连接到它 diff --git a/src/repo/repo_manager.rs b/src/repo/repo_manager.rs index 7b52bd0..29c867c 100644 --- a/src/repo/repo_manager.rs +++ b/src/repo/repo_manager.rs @@ -102,12 +102,22 @@ mod tests { let repo = Repo::new(repo_id.to_string(), desc, PathBuf::from("/tmp/test-repo")); - let before = manager.repo_count().await?; + // 清理之前可能存在的测试数据 + let _ = manager.remove_repo(repo_id).await; + + // 注册前,确保 repo 不存在 + let before = manager.get_repo(repo_id).await?; + assert!( + before.is_none(), + "repo should not exist before registration" + ); + + // 注册 repo assert!(manager.register_repo(repo).await.is_ok()); - let count = manager.repo_count().await?; - assert!(count >= before); + + // 验证 repo 已注册 let loaded = manager.get_repo(repo_id).await?; - assert!(loaded.is_some()); + assert!(loaded.is_some(), "repo should exist after registration"); // 清理测试数据 manager.remove_repo(repo_id).await?; @@ -134,18 +144,34 @@ mod tests { PathBuf::from("/tmp/test-repo-persist"), ); - let before = manager.repo_count().await?; + // 清理之前可能存在的测试数据 + let _ = manager.remove_repo(repo_id).await; + + // 注册前,确保 repo 不存在 + let before = manager.get_repo(repo_id).await?; + assert!( + before.is_none(), + "repo should not exist before registration" + ); + + // 注册 repo assert!(manager.register_repo(repo).await.is_ok()); - let count = manager.repo_count().await?; - assert!(count >= before); + + // 验证 repo 已注册 + let loaded = manager.get_repo(repo_id).await?; + assert!(loaded.is_some(), "repo should exist after registration"); // 删除仓库 let result = manager.remove_repo(repo_id).await; assert!(result.is_ok()); assert!(result.unwrap().is_some()); + // 验证数据库中已删除该 repo let loaded_after = manager.get_repo(repo_id).await?; - assert!(loaded_after.is_none()); + assert!( + loaded_after.is_none(), + "repo should not exist after deletion" + ); Ok(()) } diff --git a/src/transport/quic.rs b/src/transport/quic.rs index bb3eb76..4d5b397 100644 --- a/src/transport/quic.rs +++ b/src/transport/quic.rs @@ -15,8 +15,14 @@ use tokio::sync::mpsc::Sender as TokioSender; const READ_BUF_SIZE: usize = 1024 * 1024; const CONNECTION_CLEANUP_INTERVAL: Duration = Duration::from_secs(30); -// Type alias for incoming message sender to reduce type complexity -type IncomingMessageSender = Arc)>>>>; +// 消息前缀:用于区分 Gossip 控制消息和数据传输 +const GOSSIP_MESSAGE_PREFIX: &[u8] = b"GOSSIP:"; +const DATA_MESSAGE_PREFIX: &[u8] = b"DATA:"; + +// Type alias for Gossip 消息发送端(控制流) +type GossipMessageSender = Arc)>>>>; +// Type alias for 数据传输发送端(数据流) +type DataMessageSender = Arc)>>>>; #[derive(Debug, Clone)] pub struct ConnectionManager { @@ -25,7 +31,9 @@ pub struct ConnectionManager { endpoint: Arc, connection_tx: mpsc::Sender, connections: Arc>>>, - incoming_sender: IncomingMessageSender, + // 区分 Gossip 消息(控制流)和数据传输流 + gossip_sender: GossipMessageSender, + data_sender: DataMessageSender, } #[derive(Debug, Clone)] @@ -63,7 +71,8 @@ impl ConnectionManager { endpoint: Arc::new(endpoint), connection_tx, connections: Arc::new(Mutex::new(HashMap::new())), - incoming_sender: Arc::new(Mutex::new(None)), + gossip_sender: Arc::new(Mutex::new(None)), + data_sender: Arc::new(Mutex::new(None)), }; Ok((transport, connection_rx)) } @@ -83,7 +92,6 @@ impl ConnectionManager { let tx = connection_tx.clone(); let manager_clone = manager_clone.clone(); tokio::spawn(async move { - match Self::accept_connection(incoming).await { Ok((conn, msg_rx)) => { if let Err(e) = tx.send(conn.clone()).await { @@ -154,29 +162,71 @@ impl ConnectionManager { )) } - /// 生成消息处理任务,将接收到的消息转发到注册的 incoming_sender(包含发送者 NodeId) + /// 生成消息处理任务,将接收到的消息路由到对应的处理器(Gossip 或数据传输) + /// + /// 路由策略基于消息前缀: + /// - b"GOSSIP:" 前缀:路由到 gossip_sender(控制流消息) + /// - b"DATA:" 前缀:路由到 data_sender(数据传输) + /// - 无前缀:默认路由到 gossip_sender(向后兼容) async fn spawn_message_handler(&self, peer_id: NodeId, mut receiver: Receiver>) { - let incoming = Arc::clone(&self.incoming_sender); + let gossip = Arc::clone(&self.gossip_sender); + let data = Arc::clone(&self.data_sender); + tokio::spawn(async move { - while let Some(data) = receiver.recv().await { - // forward to registered gossip handler if present - let maybe = incoming.lock().await; - if let Some(tx) = maybe.as_ref() { - let _ = tx.send((peer_id.clone(), data)).await; + while let Some(bytes) = receiver.recv().await { + // 检查消息前缀来路由 + let is_data_transfer = bytes.starts_with(DATA_MESSAGE_PREFIX); + + if is_data_transfer { + // 移除前缀并转发到 data_sender + let payload = bytes[DATA_MESSAGE_PREFIX.len()..].to_vec(); + let maybe_data = data.lock().await; + if let Some(tx) = maybe_data.as_ref() { + let _ = tx.send((peer_id.clone(), payload)).await; + continue; + } + } + + // 检查并移除 GOSSIP 前缀(如果存在) + let payload = if bytes.starts_with(GOSSIP_MESSAGE_PREFIX) { + bytes[GOSSIP_MESSAGE_PREFIX.len()..].to_vec() + } else { + bytes.clone() + }; + + // 路由到 gossip_sender + let maybe_gossip = gossip.lock().await; + if let Some(tx) = maybe_gossip.as_ref() { + let _ = tx.send((peer_id.clone(), payload)).await; } else { - let message = String::from_utf8(data).unwrap_or_default(); + let message = String::from_utf8(payload).unwrap_or_default(); info!("Received message from {}: {}", peer_id, message); } } }); } - /// Register a channel to receive incoming messages from peers: (peer_id, bytes) - pub async fn register_incoming_sender(&self, tx: TokioSender<(NodeId, Vec)>) { - let mut guard = self.incoming_sender.lock().await; + /// 注册 Gossip 消息接收器(用于控制流消息) + pub async fn register_gossip_sender(&self, tx: TokioSender<(NodeId, Vec)>) { + let mut guard = self.gossip_sender.lock().await; + *guard = Some(tx); + } + + /// 注册数据传输接收器(用于大文件/二进制数据) + pub async fn register_data_sender(&self, tx: TokioSender<(NodeId, Vec)>) { + let mut guard = self.data_sender.lock().await; *guard = Some(tx); } + /// 向后兼容:注册 incoming 消息接收器(废弃,改用 register_gossip_sender) + #[deprecated( + since = "0.2.0", + note = "Use `register_gossip_sender()` instead for Gossip messages" + )] + pub async fn register_incoming_sender(&self, tx: TokioSender<(NodeId, Vec)>) { + self.register_gossip_sender(tx).await; + } + /// Return list of connected peer NodeIds pub async fn list_peers(&self) -> Vec { let connections = self.connections.lock().await; @@ -267,14 +317,36 @@ impl ConnectionManager { // 启动消息接收任务,用于接收服务端发来的消息 let peer_id = target_node_id.clone(); let connection_clone = connection.clone(); - let incoming_sender = Arc::clone(&self.incoming_sender); + let gossip_sender = Arc::clone(&self.gossip_sender); + let data_sender = Arc::clone(&self.data_sender); + tokio::spawn(async move { while let Ok(mut recv) = connection_clone.accept_uni().await { if let Ok(msg) = recv.read_to_end(READ_BUF_SIZE).await { - // 转发给注册的 incoming_sender(如 GossipService) - let maybe = incoming_sender.lock().await; - if let Some(tx) = maybe.as_ref() { - let _ = tx.send((peer_id.clone(), msg)).await; + // 基于前缀路由消息 + let is_data_transfer = msg.starts_with(DATA_MESSAGE_PREFIX); + + if is_data_transfer { + // 移除前缀并路由到 data_sender + let payload = msg[DATA_MESSAGE_PREFIX.len()..].to_vec(); + let maybe_data = data_sender.lock().await; + if let Some(tx) = maybe_data.as_ref() { + let _ = tx.send((peer_id.clone(), payload)).await; + continue; + } + } + + // 检查并移除 GOSSIP 前缀(如果存在) + let payload = if msg.starts_with(GOSSIP_MESSAGE_PREFIX) { + msg[GOSSIP_MESSAGE_PREFIX.len()..].to_vec() + } else { + msg.clone() + }; + + // 路由到 gossip_sender + let maybe_gossip = gossip_sender.lock().await; + if let Some(tx) = maybe_gossip.as_ref() { + let _ = tx.send((peer_id.clone(), payload)).await; } } } @@ -285,16 +357,34 @@ impl ConnectionManager { pub async fn send_message(&self, node_id: NodeId, message: Vec) -> Result<()> { let connections = self.connections.lock().await; - let conn = connections.get(&node_id).with_context(|| format!( - "Failed to send message to node[{}], connection not found", - node_id - ))?; + let conn = connections.get(&node_id).with_context(|| { + format!( + "Failed to send message to node[{}], connection not found", + node_id + ) + })?; let mut sender = conn.connection.open_uni().await?; sender.write_all(message.as_slice()).await?; sender.finish()?; Ok(()) } + + /// 发送 Gossip 消息(会自动添加 GOSSIP: 前缀) + pub async fn send_gossip_message(&self, node_id: NodeId, message: Vec) -> Result<()> { + let mut prefixed = Vec::with_capacity(GOSSIP_MESSAGE_PREFIX.len() + message.len()); + prefixed.extend_from_slice(GOSSIP_MESSAGE_PREFIX); + prefixed.extend_from_slice(&message); + self.send_message(node_id, prefixed).await + } + + /// 发送数据消息(会自动添加 DATA: 前缀,用于大文件传输) + pub async fn send_data_message(&self, node_id: NodeId, message: Vec) -> Result<()> { + let mut prefixed = Vec::with_capacity(DATA_MESSAGE_PREFIX.len() + message.len()); + prefixed.extend_from_slice(DATA_MESSAGE_PREFIX); + prefixed.extend_from_slice(&message); + self.send_message(node_id, prefixed).await + } } #[cfg(test)] diff --git a/tests/bundle_two_nodes.rs b/tests/bundle_two_nodes.rs new file mode 100644 index 0000000..e8c3bfb --- /dev/null +++ b/tests/bundle_two_nodes.rs @@ -0,0 +1,407 @@ +//! 集成测试:两个节点之间通过网络传输 bundle +use megaengine::bundle::BundleService; +use megaengine::git::pack::pack_repo_bundle; +use megaengine::gossip::GossipService; +use megaengine::identity::keypair::KeyPair; +use megaengine::node::node::{Node, NodeType}; +use megaengine::transport::config::QuicConfig; +use std::fs; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::process::Command; +use std::sync::Arc; +use tokio::time::{sleep, Duration}; + +/// Helper function to run git commands +fn run_git_command(cwd: &str, args: &[&str]) -> bool { + let output = Command::new("git") + .current_dir(cwd) + .args(args) + .output() + .expect("Failed to execute git command"); + + output.status.success() +} + +/// Create a test git repository +fn create_test_repo(repo_path: &str) -> Result<(), Box> { + fs::create_dir_all(repo_path)?; + + run_git_command(repo_path, &["init"]); + run_git_command(repo_path, &["config", "user.email", "test@example.com"]); + run_git_command(repo_path, &["config", "user.name", "Test User"]); + + let repo_dir = PathBuf::from(repo_path); + fs::write( + repo_dir.join("README.md"), + "# Test Repository for Node Transfer\n", + )?; + fs::write( + repo_dir.join("data.txt"), + "Important data to transfer between nodes\n", + )?; + + run_git_command(repo_path, &["add", "."]); + run_git_command( + repo_path, + &["commit", "-m", "Initial commit for transfer test"], + ); + + Ok(()) +} + +#[tokio::test] +async fn test_bundle_transfer_between_two_nodes() { + println!("\n========================================"); + println!("🔄 Bundle Transfer Between Two Nodes Test"); + println!("========================================\n"); + + // Initialize rustls crypto provider + let _ = rustls::crypto::ring::default_provider().install_default(); + + // Initialize logging + let _ = tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_test_writer() + .try_init(); + + println!("📋 Step 1: Setting up certificates"); + // Ensure certificates exist + megaengine::transport::cert::ensure_certificates( + "cert/cert_sender.pem", + "cert/key_sender.pem", + "cert/ca-cert.pem", + ) + .expect("Failed to ensure sender certificates"); + + megaengine::transport::cert::ensure_certificates( + "cert/cert_receiver.pem", + "cert/key_receiver.pem", + "cert/ca-cert.pem", + ) + .expect("Failed to ensure receiver certificates"); + + println!("✅ Certificates ready"); + + println!("\n📋 Step 2: Generating node keys and identities"); + // Generate keypairs for both nodes + let sender_kp = KeyPair::generate().expect("Failed to generate sender keypair"); + let receiver_kp = KeyPair::generate().expect("Failed to generate receiver keypair"); + + let sender_addr: SocketAddr = "127.0.0.1:19010".parse().unwrap(); + let receiver_addr: SocketAddr = "127.0.0.1:19011".parse().unwrap(); + + let mut sender_node = Node::from_keypair( + &sender_kp, + "sender_node", + vec![sender_addr], + NodeType::Normal, + ); + let mut receiver_node = Node::from_keypair( + &receiver_kp, + "receiver_node", + vec![receiver_addr], + NodeType::Normal, + ); + + println!("✅ Nodes created"); + println!(" - Sender: {} at {}", sender_node.node_id(), sender_addr); + println!( + " - Receiver: {} at {}", + receiver_node.node_id(), + receiver_addr + ); + + println!("\n📋 Step 3: Starting QUIC servers"); + // Start QUIC servers + let sender_config = QuicConfig::new( + sender_addr, + "cert/cert_sender.pem".to_string(), + "cert/key_sender.pem".to_string(), + "cert/ca-cert.pem".to_string(), + ); + let receiver_config = QuicConfig::new( + receiver_addr, + "cert/cert_receiver.pem".to_string(), + "cert/key_receiver.pem".to_string(), + "cert/ca-cert.pem".to_string(), + ); + + sender_node + .start_quic_server(sender_config) + .await + .expect("Failed to start sender QUIC server"); + receiver_node + .start_quic_server(receiver_config) + .await + .expect("Failed to start receiver QUIC server"); + + println!("✅ QUIC servers started"); + + println!("\n📋 Step 4: Starting Gossip and Bundle services"); + // Create and start gossip services + let sender_gossip = Arc::new(GossipService::new( + Arc::clone(sender_node.connection_manager.as_ref().unwrap()), + sender_node.clone(), + None, + )); + let receiver_gossip = Arc::new(GossipService::new( + Arc::clone(receiver_node.connection_manager.as_ref().unwrap()), + receiver_node.clone(), + None, + )); + + sender_gossip + .start() + .await + .expect("Failed to start sender gossip"); + receiver_gossip + .start() + .await + .expect("Failed to start receiver gossip"); + + // Create and start bundle services with absolute paths + let sender_bundle_storage = std::env::current_dir() + .unwrap() + .join("tmp/sender_bundle_storage"); + let receiver_bundle_storage = std::env::current_dir() + .unwrap() + .join("tmp/receiver_bundle_storage"); + + fs::create_dir_all(&sender_bundle_storage).ok(); + fs::create_dir_all(&receiver_bundle_storage).ok(); + + let sender_bundle = Arc::new(BundleService::new( + Arc::clone(sender_node.connection_manager.as_ref().unwrap()), + sender_bundle_storage.clone(), + )); + let receiver_bundle = Arc::new(BundleService::new( + Arc::clone(receiver_node.connection_manager.as_ref().unwrap()), + receiver_bundle_storage.clone(), + )); + + sender_bundle + .clone() + .start() + .await + .expect("Failed to start sender bundle service"); + receiver_bundle + .clone() + .start() + .await + .expect("Failed to start receiver bundle service"); + + println!("✅ Services started"); + println!( + " - Sender bundle storage: {}", + sender_bundle_storage.display() + ); + println!( + " - Receiver bundle storage: {}", + receiver_bundle_storage.display() + ); + + println!("\n📋 Step 5: Creating test repository and packing bundle"); + // Create test repository + let repo_path = std::env::current_dir() + .unwrap() + .join("tmp/test_repo_for_transfer"); + + fs::remove_dir_all(&repo_path).ok(); + create_test_repo(repo_path.to_str().unwrap()).expect("Failed to create test repository"); + println!("✅ Test repository created at {}", repo_path.display()); + + // Pack repository into bundle + let bundle_path = std::env::current_dir() + .unwrap() + .join("./tmp/transfer_test.bundle"); + + pack_repo_bundle(repo_path.to_str().unwrap(), bundle_path.to_str().unwrap()) + .expect("Failed to pack repository"); + + let bundle_size = fs::metadata(&bundle_path) + .expect("Failed to read bundle metadata") + .len(); + + println!("✅ Bundle created"); + println!(" - Path: {}", bundle_path.display()); + println!(" - Size: {} bytes", bundle_size); + + println!("\n📋 Step 6: Connecting nodes"); + // Connect sender to receiver + let sender_mgr = sender_node.connection_manager.as_ref().unwrap().clone(); + sender_mgr + .lock() + .await + .connect( + sender_node.node_id().clone(), + receiver_node.node_id().clone(), + vec![receiver_addr], + ) + .await + .expect("Failed to connect sender to receiver"); + + println!("✅ Nodes connected"); + sleep(Duration::from_millis(500)).await; + + println!("\n📋 Step 7: Sender transmitting bundle to receiver"); + println!(" - Repo ID: test_transfer_repo"); + println!(" - Bundle path: {}", bundle_path.display()); + + // Sender sends bundle to receiver + sender_bundle + .send_bundle( + receiver_node.node_id().clone(), + "test_transfer_repo".to_string(), + bundle_path.to_str().unwrap(), + ) + .await + .expect("Failed to send bundle"); + + println!("✅ Bundle transmission initiated"); + + // Wait for transfer to complete + sleep(Duration::from_secs(2)).await; + + println!("\n📋 Step 8: Verifying bundle reception"); + // Check if bundle was received + // The bundle is stored in the receiver's storage with encoded sender_node_id directory + let encoded_sender_id = sender_node + .node_id() + .to_string() + .replace(':', "_") + .replace('/', "_"); + let received_bundle_path = + receiver_bundle_storage.join(format!("{}/test_transfer_repo.bundle", encoded_sender_id)); + + if received_bundle_path.exists() { + let received_size = fs::metadata(&received_bundle_path) + .expect("Failed to read received bundle metadata") + .len(); + + println!("✅ Bundle received!"); + println!(" - Path: {}", received_bundle_path.display()); + println!(" - Size: {} bytes", received_size); + + if received_size == bundle_size as u64 { + println!("✅ Bundle size matches (integrity verified)"); + } else { + println!( + "⚠️ Bundle size mismatch: expected {} bytes, got {} bytes", + bundle_size, received_size + ); + } + + println!("\n📋 Step 9: Verifying bundle content by restoration"); + // Verify bundle by restoring it + let restored_repo_path = "./tmp/restored_repo_from_transfer"; + fs::remove_dir_all(restored_repo_path).ok(); + fs::create_dir_all(restored_repo_path).ok(); + + let restore_success = Command::new("git") + .args(&[ + "clone", + received_bundle_path.to_str().unwrap(), + restored_repo_path, + ]) + .output() + .map(|output| output.status.success()) + .unwrap_or(false); + + if restore_success { + println!("✅ Bundle successfully restored to new repository"); + + // Verify files exist + let readme_path = PathBuf::from(restored_repo_path).join("README.md"); + let data_path = PathBuf::from(restored_repo_path).join("data.txt"); + + if readme_path.exists() && data_path.exists() { + println!("✅ All expected files found in restored repository"); + + // Check commit history + let output = Command::new("git") + .current_dir(restored_repo_path) + .args(&["log", "--oneline"]) + .output() + .expect("Failed to get git log"); + + let log = String::from_utf8_lossy(&output.stdout); + if log.contains("Initial commit for transfer test") { + println!("✅ Commit history preserved in restored repository"); + } else { + println!("⚠️ Original commit message not found in restored repo"); + } + } else { + println!("❌ Some expected files not found in restored repository"); + } + } else { + println!("❌ Failed to restore bundle"); + } + + println!("\n📋 Step 10: Cleanup"); + // Cleanup + fs::remove_dir_all( + std::env::current_dir() + .unwrap() + .join("tmp/test_repo_for_transfer"), + ) + .ok(); + fs::remove_dir_all( + std::env::current_dir() + .unwrap() + .join("tmp/restored_repo_from_transfer"), + ) + .ok(); + fs::remove_dir_all(&sender_bundle_storage).ok(); + fs::remove_dir_all(&receiver_bundle_storage).ok(); + fs::remove_file(&bundle_path).ok(); + println!("✅ Cleanup completed"); + + println!("\n========================================"); + println!("✨ Bundle transfer test completed successfully!"); + println!("========================================\n"); + } else { + println!( + "❌ Bundle not received at expected path: {}", + received_bundle_path.display() + ); + println!("\n📊 Debug information:"); + println!( + " - Receiver storage: {}", + receiver_bundle_storage.display() + ); + + // List files in receiver storage + if receiver_bundle_storage.exists() { + println!(" - Contents of receiver storage:"); + if let Ok(entries) = fs::read_dir(&receiver_bundle_storage) { + for entry in entries { + if let Ok(entry) = entry { + println!(" - {}", entry.path().display()); + } + } + } + } + + // Cleanup anyway + fs::remove_dir_all( + std::env::current_dir() + .unwrap() + .join("tmp/test_repo_for_transfer"), + ) + .ok(); + fs::remove_dir_all(&sender_bundle_storage).ok(); + fs::remove_dir_all(&receiver_bundle_storage).ok(); + fs::remove_file(&bundle_path).ok(); + + panic!("Bundle reception failed"); + } + + // Cleanup database records + let _ = + megaengine::storage::node_model::delete_node_from_db(&sender_node.node_id().to_string()) + .await; + let _ = + megaengine::storage::node_model::delete_node_from_db(&receiver_node.node_id().to_string()) + .await; +} diff --git a/tests/gossip_three_nodes.rs b/tests/gossip_three_nodes.rs index 2584165..b8b2cb8 100644 --- a/tests/gossip_three_nodes.rs +++ b/tests/gossip_three_nodes.rs @@ -79,7 +79,8 @@ async fn test_gossip_three_nodes_message_relay() { node2.start_quic_server(config2).await.unwrap(); node3.start_quic_server(config3).await.unwrap(); - // 5. 启动 gossip + // 5. 启动 gossip 和 bundle 服务 + let bundle_storage = std::path::PathBuf::from("./data/test_bundles"); let gossip1 = Arc::new(GossipService::new( Arc::clone(node1.connection_manager.as_ref().unwrap()), node1.clone(), From cd96c0eb0bda9e16863bfe5f45883d7f20d05d94 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Wed, 26 Nov 2025 19:03:48 +0800 Subject: [PATCH 12/42] git clone via bundle --- README.md | 44 ++++++++++++++- src/bundle/bundle_sync.rs | 87 ++++++++++++++++++++++++++++++ src/bundle/mod.rs | 2 + src/bundle/service.rs | 29 ++++++++++ src/bundle/transfer.rs | 102 +++++++++++++++++++++++++++++++---- src/git/pack.rs | 67 +++++++++++++++++++++++ src/main.rs | 100 +++++++++++++++++++++++----------- src/storage/repo_model.rs | 27 ++++++++++ src/transport/quic.rs | 9 ---- src/util/mod.rs | 10 ++++ tests/bundle_two_nodes.rs | 109 +++++++++++++++++++++----------------- 11 files changed, 487 insertions(+), 99 deletions(-) create mode 100644 src/bundle/bundle_sync.rs diff --git a/README.md b/README.md index d51b52b..01c8420 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ MegaEngine is a distributed peer-to-peer (P2P) network for Git repositories. It - **Decentralized Node Discovery**: Nodes automatically discover each other and exchange node information via gossip protocol - **Repository Synchronization**: Nodes announce and sync repository inventory across the network -- **Repository Packing**: Pack Git repositories into bundle +- **Bundle Transfer**: P2P transfer of Git bundle files between nodes with integrity verification +- **Automatic Bundle Sync**: Periodic background task that automatically downloads bundles for external repositories +- **Repository Cloning**: Clone repositories from bundles using the `repo clone` command - **QUIC Transport**: Uses QUIC protocol for reliable, low-latency peer-to-peer communication - **Gossip Protocol**: Implements epidemic message propagation with TTL and deduplication - **Cryptographic Identity**: Each node has a unique EdDSA-based identity (`did:key` format) @@ -121,6 +123,20 @@ cargo run -- repo add \ The repo ID is automatically generated from the Git root commit hash and the node's public key. +### 5. Clone a Repository + +Clone a repository from bundle using its repo ID: + +```bash +cargo run -- repo clone \ + --repo-id \ + --output /path/to/clone +``` + +Requirements: +- Repository must exist in the database +- Bundle file must be downloaded (via automatic sync or transfer) + ## 🧪 Testing @@ -157,6 +173,32 @@ did:repo:zW1iF5iwCChifAcjZUrDbwD9o8LS76kFsz6bTZFEJhEqVCU - **Deduplication**: Tracks seen message hashes in a 5-minute sliding window - **Broadcast Interval**: 10 seconds +## 📦 Bundle Transfer Protocol + +MegaEngine implements a multi-frame bundle transfer protocol for P2P repository synchronization: + +### Message Types + +- **Request**: Request a bundle for a repository from a peer +- **Start**: Initiates bundle transfer with metadata (file_name, total_size) +- **Chunk**: Transfers data in 64KB chunks +- **Done**: Signals transfer completion + +### Workflow + +1. **Discovery**: Node learns about external repository via gossip +2. **Request**: Background task periodically requests missing bundles from repo owner +3. **Generation**: Owner generates bundle from local repository +4. **Transfer**: Bundle is sent to requester in multiple frames +5. **Storage**: Received bundle is stored locally and marked in database +6. **Restoration**: User can clone repository from stored bundle + +### Automatic Synchronization + +- Runs every 60 seconds by default +- Checks for external repositories with empty bundle field +- Automatically requests missing bundles from repository owners + ## 💾 Storage Data is persisted in SQLite at `$MEGAENGINE_ROOT/megaengine.db`: diff --git a/src/bundle/bundle_sync.rs b/src/bundle/bundle_sync.rs new file mode 100644 index 0000000..7bd1927 --- /dev/null +++ b/src/bundle/bundle_sync.rs @@ -0,0 +1,87 @@ +use crate::node::node_id::NodeId; +use crate::repo::repo::Repo; +use crate::storage::repo_model; +use anyhow::Result; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::Mutex; +use tokio::time::interval; +use tracing::{debug, info, warn}; + +use super::BundleService; + +const SYNC_INTERVAL: Duration = Duration::from_secs(30); + +/// 后台任务:定时检查和同步 external repos 的 bundle +pub async fn start_bundle_sync_task(bundle_service: Arc>) { + tokio::spawn(async move { + let mut tick = interval(SYNC_INTERVAL); + + loop { + tick.tick().await; + + debug!("Starting bundle sync check for external repos"); + + // 查询所有 external repos + match repo_model::list_repos().await { + Ok(repos) => { + for repo in repos { + if repo.is_external && repo.bundle.as_os_str().is_empty() { + debug!( + "Found external repo without bundle: {} (creator: {})", + repo.repo_id, repo.p2p_description.creator + ); + + // 从creator节点请求bundle + if let Err(e) = request_bundle_from_owner( + &bundle_service, + &repo, + &repo.p2p_description.creator, + ) + .await + { + warn!("Failed to request bundle for repo {}: {}", repo.repo_id, e); + } + } else if repo.is_external && !repo.bundle.as_os_str().is_empty() { + // Bundle 已存在,确保数据库已更新 + debug!( + "External repo {} already has bundle: {}", + repo.repo_id, + repo.bundle.display() + ); + } + } + } + Err(e) => { + warn!("Failed to list repos during sync check: {}", e); + } + } + } + }); +} + +/// 从仓库所有者请求 bundle +async fn request_bundle_from_owner( + bundle_service: &Arc>, + repo: &Repo, + owner_node_id_str: &str, +) -> Result<()> { + info!( + "Requesting bundle for repo {} from node {}", + repo.repo_id, owner_node_id_str + ); + + // 解析所有者的 NodeId + let owner_node_id = NodeId::from_string(owner_node_id_str)?; + + // 通过 BundleService 的 request_bundle 发送 Request 消息 + let service = bundle_service.lock().await; + service + .request_bundle(&owner_node_id, &repo.repo_id) + .await?; + + Ok(()) +} + +#[cfg(test)] +mod tests {} diff --git a/src/bundle/mod.rs b/src/bundle/mod.rs index 3dce96a..06bc4fe 100644 --- a/src/bundle/mod.rs +++ b/src/bundle/mod.rs @@ -1,5 +1,7 @@ +pub mod bundle_sync; pub mod service; pub mod transfer; +pub use bundle_sync::start_bundle_sync_task; pub use service::BundleService; pub use transfer::BundleTransferManager; diff --git a/src/bundle/service.rs b/src/bundle/service.rs index 792e96e..d74c063 100644 --- a/src/bundle/service.rs +++ b/src/bundle/service.rs @@ -1,3 +1,4 @@ +use crate::bundle::transfer::BundleMessageType; use crate::bundle::transfer::BundleTransferManager; use crate::node::node_id::NodeId; use crate::transport::quic::ConnectionManager; @@ -14,6 +15,10 @@ pub struct BundleService { connection_manager: Arc>, bundle_manager: Arc, } +pub struct BundleRequest { + pub requester_id: NodeId, + pub repo_id: String, +} impl BundleService { /// 创建新的 BundleService @@ -68,4 +73,28 @@ impl BundleService { pub fn get_bundle_path(&self, from: &NodeId, repo_id: &str) -> PathBuf { self.bundle_manager.get_bundle_path(from, repo_id) } + + /// 向指定节点请求 bundle(发送 Request 消息) + pub async fn request_bundle(&self, target_node_id: &NodeId, repo_id: &str) -> Result<()> { + // 构造 Request 消息 + let start_msg = BundleMessageType::Request { + repo_id: repo_id.to_string(), + }; + + let payload = serde_json::to_vec(&start_msg)?; + + let mgr = self.connection_manager.lock().await; + let peers = mgr.list_peers().await; + tracing::info!("Sent bundle peers: {:?}", peers); + mgr.send_data_message(target_node_id.clone(), payload) + .await?; + + tracing::info!( + "Bundle request sent to {} for repo {}", + target_node_id, + repo_id + ); + + Ok(()) + } } diff --git a/src/bundle/transfer.rs b/src/bundle/transfer.rs index 072efae..026af9b 100644 --- a/src/bundle/transfer.rs +++ b/src/bundle/transfer.rs @@ -1,5 +1,8 @@ use crate::node::node_id::NodeId; +use crate::storage::repo_model; use crate::transport::quic::ConnectionManager; +use crate::util::get_node_id_last_part; +use crate::util::get_repo_id_last_part; use anyhow::Context; use anyhow::Result; use std::path::{Path, PathBuf}; @@ -10,7 +13,10 @@ use tracing::{debug, info, warn}; /// Bundle 消息类型(用于多帧传输) #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -enum BundleMessageType { +pub enum BundleMessageType { + Request { + repo_id: String, + }, /// 开始传输:包含文件元数据 Start { repo_id: String, @@ -24,7 +30,9 @@ enum BundleMessageType { data: Vec, }, /// 传输完成 - Done { repo_id: String }, + Done { + repo_id: String, + }, } /// Bundle 文件传输管理器 @@ -138,8 +146,10 @@ impl BundleTransferManager { // 反序列化消息 let msg: BundleMessageType = serde_json::from_slice(&data).context("Failed to deserialize bundle message")?; - match msg { + BundleMessageType::Request { repo_id } => { + self.handle_bundle_request(&from, &repo_id).await + } BundleMessageType::Start { repo_id, file_name, @@ -163,8 +173,78 @@ impl BundleTransferManager { /// 将 NodeId 编码为合法的目录名(替换非法字符) fn encode_node_id(node_id: &NodeId) -> String { let id_str = node_id.to_string(); - // 将 : 替换为 _,其他非法字符也替换 - id_str.replace(':', "_").replace('/', "_") + get_node_id_last_part(&id_str) + } + + /// 处理 Request 消息:检查本地 repo 是否存在,如果存在则生成 bundle 并发送 + async fn handle_bundle_request(&self, from: &NodeId, repo_id: &str) -> Result<()> { + info!("Received bundle request from {} for repo {}", from, repo_id); + + // 检查本地是否有该 repo + match crate::storage::repo_model::load_repo_from_db(repo_id).await { + Ok(Some(repo)) => { + // repo 存在,检查是否是本地 repo(不是 external) + if repo.is_external { + warn!( + "Cannot send bundle for external repo {} to {}", + repo_id, from + ); + return Ok(()); + } + + let repo_path = repo.path.to_string_lossy().to_string(); + let bundle_dir = self.storage_dir.clone(); + let bundle_file_name = format!("{}.bundle", get_repo_id_last_part(repo_id)); + let bundle_path = bundle_dir.join(&bundle_file_name); + + info!( + "Found local repo {} at {}, generating bundle for request from {}", + repo_id, repo_path, from + ); + + // 生成 bundle 文件(同步操作,需要在线程中运行) + let repo_path_clone = repo_path.clone(); + let bundle_path_clone = bundle_path.clone(); + + tokio::task::spawn_blocking(move || { + crate::git::pack::pack_repo_bundle( + &repo_path_clone, + bundle_path_clone.to_str().unwrap_or(""), + ) + }) + .await + .context("Failed to spawn bundle packing task")??; + + info!("Bundle generated successfully for repo {}", repo_id); + + // 发送 bundle 给请求者 + self.send_bundle( + from.clone(), + repo_id.to_string(), + bundle_path.to_str().unwrap_or(""), + ) + .await + .context("Failed to send bundle in response to request")?; + + info!("Bundle for repo {} sent successfully to {}", repo_id, from); + + Ok(()) + } + Ok(None) => { + warn!( + "Received bundle request for non-existent repo {} from {}", + repo_id, from + ); + Ok(()) + } + Err(e) => { + warn!( + "Error checking repo {} for bundle request from {}: {}", + repo_id, from, e + ); + Ok(()) + } + } } /// 处理 START 消息 @@ -199,7 +279,8 @@ impl BundleTransferManager { ) -> Result<()> { let encoded_id = Self::encode_node_id(from); let dir = self.storage_dir.join(&encoded_id); - let file_path = dir.join(format!("{}.bundle", repo_id)); + let encoded_repo_id = get_repo_id_last_part(repo_id); + let file_path = dir.join(format!("{}.bundle", encoded_repo_id)); // 追加写入到文件 let mut file = fs::OpenOptions::new() @@ -214,7 +295,7 @@ impl BundleTransferManager { .await .context("Failed to write chunk data")?; - debug!( + info!( "Received chunk {} ({} bytes) for repo {} from {}", chunk_idx, data.len(), @@ -229,13 +310,16 @@ impl BundleTransferManager { async fn handle_bundle_done(&self, from: &NodeId, repo_id: &str) -> Result<()> { let encoded_id = Self::encode_node_id(from); let dir = self.storage_dir.join(&encoded_id); - let file_path = dir.join(format!("{}.bundle", repo_id)); + let encoded_repo_id = get_repo_id_last_part(repo_id); + let file_path = dir.join(format!("{}.bundle", encoded_repo_id)); if file_path.exists() { let metadata = fs::metadata(&file_path) .await .context("Failed to get bundle file metadata")?; - + // 标记 bundle 已接收 + let bundle_path = file_path.to_string_lossy().to_string(); + repo_model::update_repo_bundle(repo_id, &bundle_path).await?; info!( "Bundle transfer completed from {}: repo={}, file_size={} bytes", from, diff --git a/src/git/pack.rs b/src/git/pack.rs index 361b7ac..04fea33 100644 --- a/src/git/pack.rs +++ b/src/git/pack.rs @@ -1,5 +1,6 @@ use anyhow::Result; use git2::Repository; +use std::path::Path; use std::process::Command; /// Pack a git repository into a single file using git bundle @@ -14,6 +15,14 @@ use std::process::Command; /// pack_repo_bundle("/path/to/repo", "/tmp/repo.bundle")?; /// ``` pub fn pack_repo_bundle(repo_path: &str, output_path: &str) -> Result<()> { + // 检查并创建 output_path 的目录 + if let Some(parent_dir) = Path::new(output_path).parent() { + if !parent_dir.as_os_str().is_empty() { + std::fs::create_dir_all(parent_dir) + .map_err(|e| anyhow::anyhow!("failed to create output directory: {}", e))?; + } + } + let repo = Repository::open(repo_path) .map_err(|e| anyhow::anyhow!("failed to open git repo: {}", e))?; @@ -66,3 +75,61 @@ pub fn pack_repo_bundle(repo_path: &str, output_path: &str) -> Result<()> { Ok(()) } + +/// Restore a git repository from a bundle file +/// This creates a new repository by cloning from the bundle +/// +/// # Arguments +/// * `bundle_path` - Path to the bundle file +/// * `output_path` - Path where the new repository will be created +/// +/// # Example +/// ```ignore +/// restore_repo_from_bundle("/tmp/repo.bundle", "/path/to/new/repo").await?; +/// ``` +pub async fn restore_repo_from_bundle(bundle_path: &str, output_path: &str) -> Result<()> { + // 检查 bundle 文件是否存在 + if !Path::new(bundle_path).exists() { + return Err(anyhow::anyhow!("bundle file not found: {}", bundle_path)); + } + + // 检查输出目录是否已存在 + let output_dir = Path::new(output_path); + if output_dir.exists() { + return Err(anyhow::anyhow!( + "output directory already exists: {}", + output_path + )); + } + + // 创建输出目录的父目录 + if let Some(parent_dir) = output_dir.parent() { + if !parent_dir.as_os_str().is_empty() { + std::fs::create_dir_all(parent_dir) + .map_err(|e| anyhow::anyhow!("failed to create output directory: {}", e))?; + } + } + + // 在线程中执行 git clone,避免阻塞 async 运行时 + let bundle_path = bundle_path.to_string(); + let output_path = output_path.to_string(); + + tokio::task::spawn_blocking(move || { + // 使用 git clone 从 bundle 恢复仓库 + let output = Command::new("git") + .arg("clone") + .arg(&bundle_path) + .arg(&output_path) + .output() + .map_err(|e| anyhow::anyhow!("failed to execute git clone: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow::anyhow!("git clone from bundle failed: {}", stderr)); + } + + Ok(()) + }) + .await + .map_err(|e| anyhow::anyhow!("failed to spawn bundle restore task: {}", e))? +} diff --git a/src/main.rs b/src/main.rs index cd17eab..cb3ee52 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use anyhow::Result; use clap::{Parser, Subcommand}; use megaengine::bundle::BundleService; use megaengine::git::git_repo::{repo_name_space, repo_root_commit_bytes}; -use megaengine::git::pack::pack_repo_bundle; +use megaengine::git::pack::restore_repo_from_bundle; use megaengine::node::node_addr::NodeAddr; use std::net::SocketAddr; use std::path::PathBuf; @@ -88,19 +88,12 @@ enum RepoAction { }, /// List all repositories List, - /// Pack a repository into a distributable format - Pack { - /// Repository path to pack - #[arg(long)] - path: String, - - /// Output file path for the bundle + Clone { #[arg(long)] output: String, - /// Pack format: bundle, tar, or metadata - #[arg(long, default_value = "bundle")] - format: String, + #[arg(long)] + repo_id: String, }, } @@ -211,13 +204,20 @@ async fn main() -> Result<()> { tracing::info!("Gossip protocol started"); // 启动 Bundle 传输服务 - let bundle_storage = std::path::PathBuf::from("./data/bundles"); + let bundles_dir = PathBuf::from(format!("{}/bundles", &root_path)); + let bundle_storage = bundles_dir.clone(); let bundle_service = std::sync::Arc::new(BundleService::new( std::sync::Arc::clone(conn_mgr), bundle_storage, )); - tokio::spawn(bundle_service.start()); + tokio::spawn(bundle_service.clone().start()); tracing::info!("Bundle transfer service started"); + + // 启动 Bundle 同步后台任务:定时检查 external repos 并请求 bundle + let bundle_service_for_sync = std::sync::Arc::new(tokio::sync::Mutex::new( + BundleService::new(std::sync::Arc::clone(conn_mgr), bundles_dir), + )); + megaengine::bundle::start_bundle_sync_task(bundle_service_for_sync).await; } else { tracing::warn!("No connection manager found, services not started"); } @@ -376,29 +376,65 @@ async fn main() -> Result<()> { println!("Failed to list repositories: {}", e); } }, - RepoAction::Pack { - path, - output, - format, - } => match format.as_str() { - "bundle" => match pack_repo_bundle(&path, &output) { - Ok(_) => { - let file_size = std::fs::metadata(&output).map(|m| m.len()).unwrap_or(0); - println!("✓ Bundle created successfully"); - println!(" Output: {}", output); - println!(" Size: {} bytes", file_size); + RepoAction::Clone { output, repo_id } => { + // 查询数据库获取 repo 信息 + match storage::repo_model::load_repo_from_db(&repo_id).await { + Ok(Some(repo)) => { + // 检查 bundle 是否存在 + if repo.bundle.as_os_str().is_empty() + || repo.bundle.to_string_lossy().is_empty() + { + tracing::error!( + "Repository {} has no bundle available for cloning", + repo_id + ); + println!("Error: Repository {} has no bundle available", repo_id); + return Ok(()); + } + + let bundle_path = repo.bundle.to_string_lossy().to_string(); + if !std::path::Path::new(&bundle_path).exists() { + tracing::error!("Bundle file not found at path: {}", bundle_path); + println!("Error: Bundle file not found at {}", bundle_path); + return Ok(()); + } + + // 使用 git pack 模块中的函数恢复仓库 + tracing::info!( + "Cloning repository {} from bundle {} to {}", + repo_id, + bundle_path, + output + ); + + match restore_repo_from_bundle(&bundle_path, &output).await { + Ok(_) => { + tracing::info!( + "Repository {} cloned successfully to {}", + repo_id, + output + ); + println!("✅ Repository cloned successfully to {}", output); + println!(" Repository: {}", repo.p2p_description.name); + println!(" Creator: {}", repo.p2p_description.creator); + println!(" Description: {}", repo.p2p_description.description); + } + Err(e) => { + tracing::error!("Failed to clone repository: {}", e); + println!("Error: Failed to clone repository: {}", e); + } + } + } + Ok(None) => { + tracing::error!("Repository {} not found in database", repo_id); + println!("Error: Repository {} not found", repo_id); } Err(e) => { - eprintln!("✗ Failed to create bundle: {}", e); + tracing::error!("Failed to query repository {}: {}", repo_id, e); + println!("Error: Failed to query repository: {}", e); } - }, - _ => { - eprintln!( - "✗ Unknown format: {}. Use 'bundle', 'tar', or 'metadata'", - format - ); } - }, + } }, } diff --git a/src/storage/repo_model.rs b/src/storage/repo_model.rs index 6315862..2c6f085 100644 --- a/src/storage/repo_model.rs +++ b/src/storage/repo_model.rs @@ -135,6 +135,33 @@ pub async fn list_repos() -> Result> { Ok(repos) } +/// 更新 Repo 的 bundle 路径 +pub async fn update_repo_bundle(repo_id: &str, bundle_path: &str) -> Result<()> { + let db = init_db().await?; + let now = chrono::Local::now().timestamp(); + + // 查询是否存在 + if let Some(model) = Entity::find_by_id(repo_id).one(&db).await? { + let active_model = ActiveModel { + id: Unchanged(model.id), + bundle: Set(bundle_path.to_string()), + updated_at: Set(now), + // Keep other fields unchanged + name: Unchanged(model.name), + creator: Unchanged(model.creator), + description: Unchanged(model.description), + timestamp: Unchanged(model.timestamp), + refs: Unchanged(model.refs), + path: Unchanged(model.path), + is_external: Unchanged(model.is_external), + created_at: Unchanged(model.created_at), + }; + Entity::update(active_model).exec(&db).await?; + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/transport/quic.rs b/src/transport/quic.rs index 4d5b397..5694752 100644 --- a/src/transport/quic.rs +++ b/src/transport/quic.rs @@ -218,15 +218,6 @@ impl ConnectionManager { *guard = Some(tx); } - /// 向后兼容:注册 incoming 消息接收器(废弃,改用 register_gossip_sender) - #[deprecated( - since = "0.2.0", - note = "Use `register_gossip_sender()` instead for Gossip messages" - )] - pub async fn register_incoming_sender(&self, tx: TokioSender<(NodeId, Vec)>) { - self.register_gossip_sender(tx).await; - } - /// Return list of connected peer NodeIds pub async fn list_peers(&self) -> Vec { let connections = self.connections.lock().await; diff --git a/src/util/mod.rs b/src/util/mod.rs index 49a0191..da8e3d6 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,3 +1,13 @@ pub fn timestamp_now() -> i64 { chrono::Local::now().timestamp() } + +/// 获取 repo_id 的最后一段字符串(用 : 分割) +pub fn get_repo_id_last_part(repo_id: &str) -> String { + repo_id.split(':').last().unwrap_or(repo_id).to_string() +} + +/// 获取 node_id 的最后一段字符串(用 : 分割) +pub fn get_node_id_last_part(node_id: &str) -> String { + node_id.split(':').last().unwrap_or(node_id).to_string() +} diff --git a/tests/bundle_two_nodes.rs b/tests/bundle_two_nodes.rs index e8c3bfb..a561f06 100644 --- a/tests/bundle_two_nodes.rs +++ b/tests/bundle_two_nodes.rs @@ -1,6 +1,6 @@ //! 集成测试:两个节点之间通过网络传输 bundle use megaengine::bundle::BundleService; -use megaengine::git::pack::pack_repo_bundle; +use megaengine::git::pack::{pack_repo_bundle, restore_repo_from_bundle}; use megaengine::gossip::GossipService; use megaengine::identity::keypair::KeyPair; use megaengine::node::node::{Node, NodeType}; @@ -266,13 +266,26 @@ async fn test_bundle_transfer_between_two_nodes() { println!("\n📋 Step 8: Verifying bundle reception"); // Check if bundle was received // The bundle is stored in the receiver's storage with encoded sender_node_id directory - let encoded_sender_id = sender_node - .node_id() - .to_string() - .replace(':', "_") - .replace('/', "_"); - let received_bundle_path = - receiver_bundle_storage.join(format!("{}/test_transfer_repo.bundle", encoded_sender_id)); + // encode_node_id 函数提取 NodeId 的最后一段并用 _ 替换 : + let sender_node_id_str = sender_node.node_id().to_string(); + // 提取最后一段(NodeId 格式是 "did:key:xxx") + let last_segment = sender_node_id_str + .split(':') + .last() + .unwrap_or(&sender_node_id_str); + let encoded_sender_id = last_segment.replace(':', "_").replace('/', "_"); + + // repo_id 会被处理为最后一段(用 : 分割) + let repo_id_last_part = "test_transfer_repo" + .split(':') + .last() + .unwrap_or("test_transfer_repo") + .to_string(); + + let received_bundle_path = receiver_bundle_storage.join(format!( + "{}/{}.bundle", + encoded_sender_id, repo_id_last_part + )); if received_bundle_path.exists() { let received_size = fs::metadata(&received_bundle_path) @@ -293,49 +306,49 @@ async fn test_bundle_transfer_between_two_nodes() { } println!("\n📋 Step 9: Verifying bundle content by restoration"); - // Verify bundle by restoring it - let restored_repo_path = "./tmp/restored_repo_from_transfer"; - fs::remove_dir_all(restored_repo_path).ok(); - fs::create_dir_all(restored_repo_path).ok(); - - let restore_success = Command::new("git") - .args(&[ - "clone", - received_bundle_path.to_str().unwrap(), - restored_repo_path, - ]) - .output() - .map(|output| output.status.success()) - .unwrap_or(false); - - if restore_success { - println!("✅ Bundle successfully restored to new repository"); - - // Verify files exist - let readme_path = PathBuf::from(restored_repo_path).join("README.md"); - let data_path = PathBuf::from(restored_repo_path).join("data.txt"); - - if readme_path.exists() && data_path.exists() { - println!("✅ All expected files found in restored repository"); - - // Check commit history - let output = Command::new("git") - .current_dir(restored_repo_path) - .args(&["log", "--oneline"]) - .output() - .expect("Failed to get git log"); - - let log = String::from_utf8_lossy(&output.stdout); - if log.contains("Initial commit for transfer test") { - println!("✅ Commit history preserved in restored repository"); + // Verify bundle by restoring it using the provided utility function + let restored_repo_path = std::env::current_dir() + .unwrap() + .join("tmp/restored_repo_from_transfer"); + + fs::remove_dir_all(&restored_repo_path).ok(); + + match restore_repo_from_bundle( + received_bundle_path.to_str().unwrap(), + restored_repo_path.to_str().unwrap(), + ) + .await + { + Ok(_) => { + println!("✅ Bundle successfully restored to new repository"); + + // Verify files exist + let readme_path = restored_repo_path.join("README.md"); + let data_path = restored_repo_path.join("data.txt"); + + if readme_path.exists() && data_path.exists() { + println!("✅ All expected files found in restored repository"); + + // Check commit history + let output = Command::new("git") + .current_dir(restored_repo_path.to_str().unwrap()) + .args(&["log", "--oneline"]) + .output() + .expect("Failed to get git log"); + + let log = String::from_utf8_lossy(&output.stdout); + if log.contains("Initial commit for transfer test") { + println!("✅ Commit history preserved in restored repository"); + } else { + println!("⚠️ Original commit message not found in restored repo"); + } } else { - println!("⚠️ Original commit message not found in restored repo"); + println!("❌ Some expected files not found in restored repository"); } - } else { - println!("❌ Some expected files not found in restored repository"); } - } else { - println!("❌ Failed to restore bundle"); + Err(e) => { + println!("❌ Failed to restore bundle: {}", e); + } } println!("\n📋 Step 10: Cleanup"); From 90a65d8457cc8866cd36867b8bc1b147671f4cc6 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Wed, 26 Nov 2025 19:05:57 +0800 Subject: [PATCH 13/42] fix clippy --- src/git/pack.rs | 15 ++++++--------- src/main.rs | 2 +- src/node/node_addr.rs | 7 ++++--- src/util/mod.rs | 4 ++-- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/git/pack.rs b/src/git/pack.rs index 04fea33..baca1bf 100644 --- a/src/git/pack.rs +++ b/src/git/pack.rs @@ -28,26 +28,23 @@ pub fn pack_repo_bundle(repo_path: &str, output_path: &str) -> Result<()> { // Get all branches to include in the bundle let mut branch_refs = Vec::new(); - let mut branches = repo + let branches = repo .branches(None) .map_err(|e| anyhow::anyhow!("failed to list branches: {}", e))?; - while let Some(branch_result) = branches.next() { + for branch_result in branches { let (branch, _) = branch_result.map_err(|e| anyhow::anyhow!("failed to get branch: {}", e))?; - if let Ok(name) = branch.name() { - if let Some(name_str) = name { - branch_refs.push(name_str.to_string()); - } + if let Ok(Some(name_str)) = branch.name() { + branch_refs.push(name_str.to_string()); } } // If no branches found, try to get HEAD - if branch_refs.is_empty() { - if repo.head().is_ok() { + if branch_refs.is_empty() + && repo.head().is_ok() { branch_refs.push("HEAD".to_string()); } - } if branch_refs.is_empty() { return Err(anyhow::anyhow!("no branches found to bundle")); diff --git a/src/main.rs b/src/main.rs index cb3ee52..ee37157 100644 --- a/src/main.rs +++ b/src/main.rs @@ -283,7 +283,7 @@ async fn main() -> Result<()> { // 打印 node 地址 let addr = NodeAddr::new(node.node_id().clone(), addr.parse()?); - println!("Node address: {}", addr.to_string()); + println!("Node address: {}", addr); println!("Press Ctrl+C to stop"); diff --git a/src/node/node_addr.rs b/src/node/node_addr.rs index 943dd85..70a470f 100644 --- a/src/node/node_addr.rs +++ b/src/node/node_addr.rs @@ -39,10 +39,11 @@ impl NodeAddr { pub fn new(peer_id: NodeId, address: SocketAddr) -> Self { NodeAddr { peer_id, address } } +} - /// Format node address as string - pub fn to_string(&self) -> String { - format!("{}@{}", self.peer_id, self.address) +impl std::fmt::Display for NodeAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}@{}", self.peer_id, self.address) } } diff --git a/src/util/mod.rs b/src/util/mod.rs index da8e3d6..d31aa80 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -4,10 +4,10 @@ pub fn timestamp_now() -> i64 { /// 获取 repo_id 的最后一段字符串(用 : 分割) pub fn get_repo_id_last_part(repo_id: &str) -> String { - repo_id.split(':').last().unwrap_or(repo_id).to_string() + repo_id.split(':').next_back().unwrap_or(repo_id).to_string() } /// 获取 node_id 的最后一段字符串(用 : 分割) pub fn get_node_id_last_part(node_id: &str) -> String { - node_id.split(':').last().unwrap_or(node_id).to_string() + node_id.split(':').next_back().unwrap_or(node_id).to_string() } From 1c6ee0ece1daffcdf83fba15af5bc92ebb7756b5 Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Wed, 26 Nov 2025 19:12:02 +0800 Subject: [PATCH 14/42] Update src/git/pack.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/git/pack.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/git/pack.rs b/src/git/pack.rs index baca1bf..c62c235 100644 --- a/src/git/pack.rs +++ b/src/git/pack.rs @@ -41,10 +41,9 @@ pub fn pack_repo_bundle(repo_path: &str, output_path: &str) -> Result<()> { } // If no branches found, try to get HEAD - if branch_refs.is_empty() - && repo.head().is_ok() { - branch_refs.push("HEAD".to_string()); - } + if branch_refs.is_empty() && repo.head().is_ok() { + branch_refs.push("HEAD".to_string()); + } if branch_refs.is_empty() { return Err(anyhow::anyhow!("no branches found to bundle")); From 512d310d8a8c2d8ea30c5d1d67170b25af43ad04 Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Wed, 26 Nov 2025 19:13:05 +0800 Subject: [PATCH 15/42] Update src/repo/repo_id.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/repo/repo_id.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/repo/repo_id.rs b/src/repo/repo_id.rs index 46a8d2d..61a7c5c 100644 --- a/src/repo/repo_id.rs +++ b/src/repo/repo_id.rs @@ -51,13 +51,13 @@ impl RepoId { } #[derive(Debug, PartialEq, Eq)] -pub struct ParseNodeIdError; +pub struct ParseRepoIdError; impl FromStr for RepoId { - type Err = ParseNodeIdError; + type Err = ParseRepoIdError; fn from_str(s: &str) -> Result { - RepoId::parse_from_str(s).map_err(|_| ParseNodeIdError) + RepoId::parse_from_str(s).map_err(|_| ParseRepoIdError) } } From 9ebcb084a7ccb5a2bad93477424273a18065f876 Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Wed, 26 Nov 2025 19:14:12 +0800 Subject: [PATCH 16/42] Update src/storage/node_model.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/storage/node_model.rs | 40 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/storage/node_model.rs b/src/storage/node_model.rs index fb0947b..854d172 100644 --- a/src/storage/node_model.rs +++ b/src/storage/node_model.rs @@ -59,29 +59,25 @@ pub async fn save_node_info_to_db(info: &NodeInfo) -> Result<()> { pub async fn load_node_info_from_db(node_id: &str) -> Result> { let db = crate::storage::init_db().await?; - let models = Entity::find().all(&db).await?; - for m in models { - if m.id == node_id { - let addresses: Vec = serde_json::from_str(&m.addresses)?; - let node_type = match m.node_type { - 0 => NodeType::Normal, - _ => NodeType::Relay, - }; - - let info = NodeInfo { - node_id: crate::node::node_id::NodeId::from_string(&m.id) - .unwrap_or_else(|_| crate::node::node_id::NodeId::from_string("").unwrap()), - alias: m.alias, - addresses, - node_type, - version: m.version as u8, - }; - - return Ok(Some(info)); - } - } + if let Some(m) = Entity::find_by_id(node_id).one(&db).await? { + let addresses: Vec = serde_json::from_str(&m.addresses)?; + let node_type = match m.node_type { + 0 => NodeType::Normal, + _ => NodeType::Relay, + }; - Ok(None) + let info = NodeInfo { + node_id: crate::node::node_id::NodeId::from_string(&m.id) + .unwrap_or_else(|_| crate::node::node_id::NodeId::from_string("").unwrap()), + alias: m.alias, + addresses, + node_type, + version: m.version as u8, + }; + Ok(Some(info)) + } else { + Ok(None) + } } /// 删除节点记录 From 80935105c49877168468a009643af442a700109c Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Fri, 2 Jan 2026 15:37:43 +0800 Subject: [PATCH 17/42] add refs table --- README.md | 96 ++++++++------- src/git/git_repo.rs | 40 +++++++ src/git/pack.rs | 7 +- src/main.rs | 35 +++++- src/storage/mod.rs | 16 ++- src/storage/ref_model.rs | 244 ++++++++++++++++++++++++++++++++++++++ src/storage/repo_model.rs | 17 +-- 7 files changed, 399 insertions(+), 56 deletions(-) create mode 100644 src/storage/ref_model.rs diff --git a/README.md b/README.md index 01c8420..3c9151d 100644 --- a/README.md +++ b/README.md @@ -66,79 +66,93 @@ Set the root directory for MegaEngine data (default: `~/.megaengine`): export MEGAENGINE_ROOT=/path/to/megaengine-data ``` -## 🚀 Usage -### 1. Initialize Keypair +## Example: Two-Node Network with Repository Synchronization -Generate a new cryptographic keypair (EdDSA): +This example demonstrates how to set up a two-node network where the first node adds a repository, and the second node automatically synchronizes and clones it. +### Prerequisites + +- Create a test Git repository (or use an existing one): + ```bash + mkdir -p E:\git_test\tiny + cd E:\git_test\tiny + git init + # Add some content + git add . + git commit -m "Initial commit" + ``` + +### Step 1: Initialize Keypairs for Both Nodes + +**Terminal 1** - Initialize the first node's keypair: ```bash cargo run -- auth init ``` -Output: -``` -Keypair saved to /.megaengine/keypair.json +**Terminal 2** - Initialize the second node's keypair with a custom root directory: +```bash +cargo run -- --root ~/.megaengine2 auth init ``` -### 2. Start a Node +Output will show the keypair location and the DID key for each node. -Start a MegaEngine node that listens on a QUIC endpoint: +### Step 2: Start Both Nodes +**Terminal 1** - Start the first node (node1): ```bash -cargo run -- node start \ - --alias my-node \ - --addr 0.0.0.0:9000 \ - --cert-path cert +cargo run -- node start --alias node1 --addr 127.0.0.1:9000 --cert-path cert ``` -The node will: -- Initialize QUIC server on the specified address -- Start gossip protocol for peer discovery -- Periodically announce node and repository information -- Listen indefinitely until Ctrl+C - -### 3. Get Node ID - -Display the node ID (based on your keypair): +Keep this terminal running. +**Terminal 2** - Start the second node (node2) with node1 as bootstrap node: ```bash -cargo run -- node id +cargo run -- --root ~/.megaengine2 node start --alias node2 --cert-path cert --bootstrap-node did:key:z2DUYGZos3YrXrD4pQ9aAku2g7btumKcfTiMSyBC8btqFDJ@127.0.0.1:9000 --addr 127.0.0.1:9001 ``` -Output: -``` -did:key:z2DXbAovGq5vNKpXVFyrhVLppMdUCmV1hCNjbUydLMEWasE -``` +Keep this terminal running as well. -### 4. Register a Repository +**Note**: Replace `did:key:z2DUYGZos3YrXrD4pQ9aAku2g7btumKcfTiMSyBC8btqFDJ` with the actual DID key from the first node's auth init output. -Add a local Git repository to the network: +### Step 3: Add Repository to Node1 +**Terminal 3** - Add a repository on node1: ```bash -cargo run -- repo add \ - --path /path/to/git/repo \ - --description "My repository" +cargo run -- repo add --path E:\git_test\tiny --description "Tiny" ``` -The repo ID is automatically generated from the Git root commit hash and the node's public key. +The output will display the repo ID. Save this ID for later use. + +### Step 4: Node2 Automatically Synchronizes + +The second node will automatically: +1. Discover the repository announcement via gossip protocol +2. Periodically request the bundle from node1 (every 60 seconds by default) +3. Download the bundle file +4. Store it locally -### 5. Clone a Repository +Monitor the output from Terminal 2 to see the synchronization progress. -Clone a repository from bundle using its repo ID: +### Step 5: Query Repository on Node2 +**Terminal 3** - List repositories on node2: ```bash -cargo run -- repo clone \ - --repo-id \ - --output /path/to/clone +cargo run -- --root ~/.megaengine2 repo list ``` -Requirements: -- Repository must exist in the database -- Bundle file must be downloaded (via automatic sync or transfer) +You should see the "Tiny" repository announced by node1. + +### Step 6: Clone Repository from Node2 + +**Terminal 3** - Clone the repository on node2: +```bash +cargo run -- --root ~/.megaengine2 repo clone --repo-id --output ./tiny +``` +Replace `` with the ID from Step 3. -## 🧪 Testing +The cloned repository will be available at `./tiny` on node2. diff --git a/src/git/git_repo.rs b/src/git/git_repo.rs index ed7d752..bb9eaeb 100644 --- a/src/git/git_repo.rs +++ b/src/git/git_repo.rs @@ -34,3 +34,43 @@ pub fn repo_name_space(path: &str) -> String { } "".to_string() } + +/// Read all refs (branches and tags) from a git repository +pub fn read_repo_refs(path: &str) -> Result> { + let repo = + Repository::open(path).map_err(|e| anyhow::anyhow!("failed to open git repo: {}", e))?; + + let mut refs = std::collections::HashMap::new(); + + // Read branches (refs/heads/*) + let branches = repo + .branches(None) + .map_err(|e| anyhow::anyhow!("failed to read branches: {}", e))?; + + for branch_result in branches { + let (branch, _) = + branch_result.map_err(|e| anyhow::anyhow!("failed to read branch: {}", e))?; + if let Ok(name) = branch.name() { + if let Some(name) = name { + if let Some(oid) = branch.get().target() { + refs.insert(format!("refs/heads/{}", name), oid.to_string()); + } + } + } + } + + // Read tags (refs/tags/*) + let tag_names = repo + .tag_names(None) + .map_err(|e| anyhow::anyhow!("failed to read tags: {}", e))?; + + for tag_name in tag_names.iter().flatten() { + if let Ok(reference) = repo.find_reference(&format!("refs/tags/{}", tag_name)) { + if let Some(oid) = reference.target() { + refs.insert(format!("refs/tags/{}", tag_name), oid.to_string()); + } + } + } + + Ok(refs) +} diff --git a/src/git/pack.rs b/src/git/pack.rs index baca1bf..c62c235 100644 --- a/src/git/pack.rs +++ b/src/git/pack.rs @@ -41,10 +41,9 @@ pub fn pack_repo_bundle(repo_path: &str, output_path: &str) -> Result<()> { } // If no branches found, try to get HEAD - if branch_refs.is_empty() - && repo.head().is_ok() { - branch_refs.push("HEAD".to_string()); - } + if branch_refs.is_empty() && repo.head().is_ok() { + branch_refs.push("HEAD".to_string()); + } if branch_refs.is_empty() { return Err(anyhow::anyhow!("no branches found to bundle")); diff --git a/src/main.rs b/src/main.rs index ee37157..d9cff45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -345,10 +345,21 @@ async fn main() -> Result<()> { timestamp: timestamp_now(), }; - let repo = repo::repo::Repo::new(repo_id.to_string(), desc, PathBuf::from(path)); + let mut repo_obj = repo::repo::Repo::new(repo_id.to_string(), desc, PathBuf::from(path.clone())); + + // Read and populate refs from the git repository + match megaengine::git::git_repo::read_repo_refs(&path) { + Ok(refs) => { + repo_obj.refs = refs; + tracing::info!("Loaded {} refs from repository", repo_obj.refs.len()); + } + Err(e) => { + tracing::warn!("Failed to read refs from repository: {}", e); + } + } let mut manager = repo::repo_manager::RepoManager::new(); - match manager.register_repo(repo).await { + match manager.register_repo(repo_obj).await { Ok(_) => tracing::info!("Repo {} added", repo_id), Err(e) => tracing::info!("Failed to add repo: {}", e), } @@ -418,6 +429,26 @@ async fn main() -> Result<()> { println!(" Repository: {}", repo.p2p_description.name); println!(" Creator: {}", repo.p2p_description.creator); println!(" Description: {}", repo.p2p_description.description); + + // Read and save refs from the cloned repository + match megaengine::git::git_repo::read_repo_refs(&output) { + Ok(refs) => { + tracing::info!("Loaded {} refs from cloned repository", refs.len()); + // Save refs to the database + match storage::ref_model::batch_save_refs(&repo_id, &refs).await { + Ok(_) => { + tracing::info!("Refs saved to database for repository {}", repo_id); + println!(" Refs: {} branches/tags", refs.len()); + } + Err(e) => { + tracing::warn!("Failed to save refs to database: {}", e); + } + } + } + Err(e) => { + tracing::warn!("Failed to read refs from cloned repository: {}", e); + } + } } Err(e) => { tracing::error!("Failed to clone repository: {}", e); diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 5c53db0..9f918e1 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,5 +1,6 @@ pub mod node_model; pub mod repo_model; +pub mod ref_model; use anyhow::Result; use sea_orm::{ConnectOptions, ConnectionTrait, Database, DatabaseConnection}; @@ -113,7 +114,6 @@ pub async fn init_db() -> Result { creator TEXT NOT NULL, description TEXT NOT NULL, timestamp INTEGER NOT NULL, - refs TEXT NOT NULL, path TEXT NOT NULL, bundle TEXT NOT NULL DEFAULT '', is_external INTEGER NOT NULL DEFAULT 0, @@ -138,6 +138,20 @@ pub async fn init_db() -> Result { ) .await; + // Refs 表:存储分支和标签的最新 commit + let _ = db + .execute_unprepared( + "CREATE TABLE IF NOT EXISTS refs ( + id TEXT PRIMARY KEY, + repo_id TEXT NOT NULL, + ref_name TEXT NOT NULL, + commit_hash TEXT NOT NULL, + updated_at INTEGER NOT NULL, + UNIQUE(repo_id, ref_name) + )", + ) + .await; + db }) .await; diff --git a/src/storage/ref_model.rs b/src/storage/ref_model.rs new file mode 100644 index 0000000..4cebb45 --- /dev/null +++ b/src/storage/ref_model.rs @@ -0,0 +1,244 @@ +use anyhow::Result; +use sea_orm::entity::prelude::*; +use sea_orm::{Set, Unchanged}; + +use crate::storage::init_db; + +/// Refs table entity for tracking branch and tag commits +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "refs")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: String, + pub repo_id: String, + pub ref_name: String, + pub commit_hash: String, + pub updated_at: i64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +/// Save or update a ref in the database +pub async fn save_ref(repo_id: &str, ref_name: &str, commit_hash: &str) -> Result<()> { + let db = init_db().await?; + let now = chrono::Local::now().timestamp(); + + // Generate a unique ID for this ref record (repo_id + ref_name) + let id = format!("{}:{}", repo_id, ref_name); + + // Check if ref already exists + let existing = Entity::find_by_id(id.clone()).one(&db).await?; + + if let Some(_) = existing { + // Update existing ref + let active_model = ActiveModel { + id: Unchanged(id), + repo_id: Unchanged(repo_id.to_string()), + ref_name: Unchanged(ref_name.to_string()), + commit_hash: Set(commit_hash.to_string()), + updated_at: Set(now), + }; + Entity::update(active_model).exec(&db).await?; + } else { + // Insert new ref + let active_model = ActiveModel { + id: Set(id), + repo_id: Set(repo_id.to_string()), + ref_name: Set(ref_name.to_string()), + commit_hash: Set(commit_hash.to_string()), + updated_at: Set(now), + }; + Entity::insert(active_model).exec(&db).await?; + } + + Ok(()) +} + +/// Batch save multiple refs for a repository +pub async fn batch_save_refs( + repo_id: &str, + refs: &std::collections::HashMap, +) -> Result<()> { + let db = init_db().await?; + let now = chrono::Local::now().timestamp(); + + for (ref_name, commit_hash) in refs { + let id = format!("{}:{}", repo_id, ref_name); + + let existing = Entity::find_by_id(id.clone()).one(&db).await?; + + if let Some(_) = existing { + let active_model = ActiveModel { + id: Unchanged(id), + repo_id: Unchanged(repo_id.to_string()), + ref_name: Unchanged(ref_name.clone()), + commit_hash: Set(commit_hash.clone()), + updated_at: Set(now), + }; + Entity::update(active_model).exec(&db).await?; + } else { + let active_model = ActiveModel { + id: Set(id), + repo_id: Set(repo_id.to_string()), + ref_name: Set(ref_name.clone()), + commit_hash: Set(commit_hash.clone()), + updated_at: Set(now), + }; + Entity::insert(active_model).exec(&db).await?; + } + } + + Ok(()) +} + +/// Load all refs for a repository +pub async fn load_refs_for_repo(repo_id: &str) -> Result> { + let db = init_db().await?; + + let refs = Entity::find() + .filter(Column::RepoId.eq(repo_id)) + .all(&db) + .await?; + + let mut result = std::collections::HashMap::new(); + for ref_record in refs { + result.insert(ref_record.ref_name, ref_record.commit_hash); + } + + Ok(result) +} + +/// Get a specific ref by repo_id and ref_name +pub async fn get_ref(repo_id: &str, ref_name: &str) -> Result> { + let db = init_db().await?; + let id = format!("{}:{}", repo_id, ref_name); + + if let Some(model) = Entity::find_by_id(id).one(&db).await? { + return Ok(Some(model.commit_hash)); + } + + Ok(None) +} + +/// Delete all refs for a repository +pub async fn delete_refs_for_repo(repo_id: &str) -> Result<()> { + let db = init_db().await?; + Entity::delete_many() + .filter(Column::RepoId.eq(repo_id)) + .exec(&db) + .await?; + Ok(()) +} + +/// Delete a specific ref +pub async fn delete_ref(repo_id: &str, ref_name: &str) -> Result<()> { + let db = init_db().await?; + let id = format!("{}:{}", repo_id, ref_name); + Entity::delete_by_id(id).exec(&db).await?; + Ok(()) +} + +/// Check if any ref in the repository has been updated +pub async fn has_refs_changed( + repo_id: &str, + old_refs: &std::collections::HashMap, +) -> Result { + let db = init_db().await?; + + let current_refs = Entity::find() + .filter(Column::RepoId.eq(repo_id)) + .all(&db) + .await?; + + // If counts don't match, something has changed + if current_refs.len() != old_refs.len() { + return Ok(true); + } + + // Check if any ref's commit hash has changed + for ref_record in current_refs { + if let Some(old_commit) = old_refs.get(&ref_record.ref_name) { + if old_commit != &ref_record.commit_hash { + return Ok(true); + } + } else { + // New ref found + return Ok(true); + } + } + + Ok(false) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_save_and_load_ref() -> Result<()> { + let repo_id = "did:repo:test-ref-001"; + let ref_name = "refs/heads/main"; + let commit_hash = "abc123def456"; + + // Save ref + save_ref(repo_id, ref_name, commit_hash).await?; + + // Load ref + let loaded = get_ref(repo_id, ref_name).await?; + assert!(loaded.is_some()); + assert_eq!(loaded.unwrap(), commit_hash); + + // Cleanup + delete_ref(repo_id, ref_name).await?; + Ok(()) + } + + #[tokio::test] + async fn test_batch_save_refs() -> Result<()> { + let repo_id = "did:repo:test-ref-002"; + let mut refs = std::collections::HashMap::new(); + refs.insert("refs/heads/main".to_string(), "abc123".to_string()); + refs.insert("refs/heads/develop".to_string(), "def456".to_string()); + refs.insert("refs/tags/v1.0".to_string(), "ghi789".to_string()); + + // Save all refs + batch_save_refs(repo_id, &refs).await?; + + // Load and verify + let loaded = load_refs_for_repo(repo_id).await?; + assert_eq!(loaded.len(), 3); + assert_eq!(loaded.get("refs/heads/main"), Some(&"abc123".to_string())); + assert_eq!(loaded.get("refs/heads/develop"), Some(&"def456".to_string())); + assert_eq!(loaded.get("refs/tags/v1.0"), Some(&"ghi789".to_string())); + + // Cleanup + delete_refs_for_repo(repo_id).await?; + Ok(()) + } + + #[tokio::test] + async fn test_has_refs_changed() -> Result<()> { + let repo_id = "did:repo:test-ref-003"; + let mut refs = std::collections::HashMap::new(); + refs.insert("refs/heads/main".to_string(), "abc123".to_string()); + + // Save initial refs + batch_save_refs(repo_id, &refs).await?; + + // No change + let changed = has_refs_changed(repo_id, &refs).await?; + assert!(!changed); + + // Change commit hash + refs.insert("refs/heads/main".to_string(), "def456".to_string()); + let changed = has_refs_changed(repo_id, &refs).await?; + assert!(changed); + + // Cleanup + delete_refs_for_repo(repo_id).await?; + Ok(()) + } +} diff --git a/src/storage/repo_model.rs b/src/storage/repo_model.rs index 2c6f085..fa4847b 100644 --- a/src/storage/repo_model.rs +++ b/src/storage/repo_model.rs @@ -15,7 +15,6 @@ pub struct Model { pub creator: String, pub description: String, pub timestamp: i64, - pub refs: String, pub path: String, pub bundle: String, pub is_external: bool, @@ -31,7 +30,6 @@ impl ActiveModelBehavior for ActiveModel {} /// 保存或更新 Repo 到数据库 pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { let db = init_db().await?; - let refs_json = serde_json::to_string(&repo.refs)?; let now = chrono::Local::now().timestamp(); // 查询是否已存在 @@ -45,7 +43,6 @@ pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { creator: Set(repo.p2p_description.creator.clone()), description: Set(repo.p2p_description.description.clone()), timestamp: Set(repo.p2p_description.timestamp), - refs: Set(refs_json), path: Set(repo.path.to_string_lossy().to_string()), bundle: Set(repo.bundle.to_string_lossy().to_string()), is_external: Set(repo.is_external), @@ -61,7 +58,6 @@ pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { creator: Set(repo.p2p_description.creator.clone()), description: Set(repo.p2p_description.description.clone()), timestamp: Set(repo.p2p_description.timestamp), - refs: Set(refs_json), path: Set(repo.path.to_string_lossy().to_string()), bundle: Set(repo.bundle.to_string_lossy().to_string()), is_external: Set(repo.is_external), @@ -71,6 +67,9 @@ pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { Entity::insert(active_model).exec(&db).await?; } + // 保存 refs 到 refs 表 + crate::storage::ref_model::batch_save_refs(&repo.repo_id, &repo.refs).await?; + Ok(()) } @@ -80,7 +79,8 @@ pub async fn load_repo_from_db(repo_id: &str) -> Result> { // 使用 find_by_id 直接查询 if let Some(model) = Entity::find_by_id(repo_id).one(&db).await? { - let refs: std::collections::HashMap = serde_json::from_str(&model.refs)?; + // Load refs from ref_model table + let refs = crate::storage::ref_model::load_refs_for_repo(&model.id).await?; let repo = Repo { repo_id: model.id, @@ -105,6 +105,8 @@ pub async fn load_repo_from_db(repo_id: &str) -> Result> { pub async fn delete_repo_from_db(repo_id: &str) -> Result<()> { let db = init_db().await?; Entity::delete_by_id(repo_id).exec(&db).await?; + // Delete associated refs + crate::storage::ref_model::delete_refs_for_repo(repo_id).await?; Ok(()) } @@ -115,8 +117,8 @@ pub async fn list_repos() -> Result> { let mut repos = Vec::new(); for model in models { - let refs: std::collections::HashMap = - serde_json::from_str(&model.refs).unwrap_or_default(); + // Load refs from ref_model table + let refs = crate::storage::ref_model::load_refs_for_repo(&model.id).await?; repos.push(Repo { repo_id: model.id, @@ -151,7 +153,6 @@ pub async fn update_repo_bundle(repo_id: &str, bundle_path: &str) -> Result<()> creator: Unchanged(model.creator), description: Unchanged(model.description), timestamp: Unchanged(model.timestamp), - refs: Unchanged(model.refs), path: Unchanged(model.path), is_external: Unchanged(model.is_external), created_at: Unchanged(model.created_at), From 7e5e48f311e3c375cb9405fdc64f1c52dd86f2b3 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Fri, 2 Jan 2026 17:27:15 +0800 Subject: [PATCH 18/42] new repo update function --- README.md | 43 +++++ src/cli/auth.rs | 18 ++ src/cli/mod.rs | 7 + src/cli/node.rs | 188 +++++++++++++++++++ src/cli/repo.rs | 290 ++++++++++++++++++++++++++++ src/git/pack.rs | 100 ++++++++++ src/gossip/service.rs | 187 ++++++++++++++++-- src/main.rs | 396 ++++----------------------------------- src/repo/mod.rs | 3 + src/repo/repo_sync.rs | 85 +++++++++ src/storage/mod.rs | 2 +- src/storage/ref_model.rs | 9 +- 12 files changed, 951 insertions(+), 377 deletions(-) create mode 100644 src/cli/auth.rs create mode 100644 src/cli/mod.rs create mode 100644 src/cli/node.rs create mode 100644 src/cli/repo.rs create mode 100644 src/repo/repo_sync.rs diff --git a/README.md b/README.md index 3c9151d..838c026 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,49 @@ Replace `` with the ID from Step 3. The cloned repository will be available at `./tiny` on node2. +### Step 7: Repository Update Synchronization + +When the repository creator (node1) pushes new commits, node2 will automatically synchronize them. + +**Terminal 3** - Make a change in the original repository on node1: +```bash +cd E:\git_test\tiny +# Add or modify some files +echo "Updated content" >> README.md +git add README.md +git commit -m "Update repository" +``` + +**Terminal 3** - Update the repository bundle on node1: +```bash +cargo run -- repo list +``` + +You'll see the status indicator changes to `⚠️ HAS UPDATES`, showing new commits are available. + +**Terminal 3** - Node2 will automatically discover and download the updated bundle + +Monitor Terminal 2 output - you should see automatic bundle sync activity. The background task runs every 60 seconds and will: +1. Detect the repository update announcement via gossip protocol +2. Request the updated bundle from node1 +3. Download and store the new bundle + +**Terminal 3** - Check repository status on node2: +```bash +cargo run -- --root ~/.megaengine2 repo list +``` + +You should see the repository status has changed, indicating updates are available. + +**Terminal 3** - Pull the latest updates to the cloned repository on node2: +```bash +cargo run -- --root ~/.megaengine2 repo pull --repo-id +``` + +Replace `` with the repository ID from Step 3. + +The cloned repository at `./tiny` will be updated with the latest commits from the bundle. + ## 🔐 Data Formats diff --git a/src/cli/auth.rs b/src/cli/auth.rs new file mode 100644 index 0000000..4f0f7c7 --- /dev/null +++ b/src/cli/auth.rs @@ -0,0 +1,18 @@ +use anyhow::Result; +use megaengine::storage; + +pub async fn handle_auth() -> Result<()> { + let kp_path = storage::keypair_path(); + if kp_path.exists() { + tracing::info!( + "Keypair already exists at {:?}; skipping generation", + kp_path + ); + } else { + tracing::info!("Generating new keypair..."); + let kp = megaengine::identity::keypair::KeyPair::generate()?; + storage::save_keypair(&kp)?; + tracing::info!("Keypair saved to {:?}", storage::keypair_path()); + } + Ok(()) +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..19ea821 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,7 @@ +pub mod auth; +pub mod node; +pub mod repo; + +pub use auth::handle_auth; +pub use node::handle_node; +pub use repo::handle_repo; diff --git a/src/cli/node.rs b/src/cli/node.rs new file mode 100644 index 0000000..4a4c0df --- /dev/null +++ b/src/cli/node.rs @@ -0,0 +1,188 @@ +use anyhow::Result; +use megaengine::{ + bundle::BundleService, node::node_addr::NodeAddr, storage, transport::config::QuicConfig, +}; +use std::path::PathBuf; +use std::sync::Arc; + +pub async fn handle_node_start( + root_path: &str, + alias: String, + addr: String, + cert_path: String, + bootstrap_node: Option, +) -> Result<()> { + tracing::info!("Starting node..."); + let cert_dir = format!("{}/{}", root_path, cert_path); + megaengine::transport::cert::ensure_certificates( + &format!("{}/cert.pem", cert_dir), + &format!("{}/key.pem", cert_dir), + &format!("{}/ca-cert.pem", cert_dir), + )?; + + let kp = match storage::load_keypair() { + Ok(k) => k, + Err(e) => { + tracing::error!("failed to load keypair: {}", e); + tracing::info!("Run `auth init` first to generate keys"); + return Ok(()); + } + }; + + let addrs: Vec = vec![addr.parse()?]; + + let mut node = megaengine::node::node::Node::from_keypair( + &kp, + &alias, + addrs.clone(), + megaengine::node::node::NodeType::Normal, + ); + tracing::info!( + "Node initialized: alias={} id={}", + node.alias(), + node.node_id().0 + ); + + let quic_config = QuicConfig::new( + addr.parse()?, + format!("{}/cert.pem", cert_dir), + format!("{}/key.pem", cert_dir), + format!("{}/ca-cert.pem", cert_dir), + ); + + tracing::info!("Starting QUIC server on {}...", addr); + node.start_quic_server(quic_config).await?; + + if let Some(conn_mgr) = &node.connection_manager { + // 启动 Gossip 服务 + let gossip = Arc::new(megaengine::gossip::GossipService::new( + Arc::clone(conn_mgr), + node.clone(), + None, + )); + tokio::spawn(gossip.start()); + tracing::info!("Gossip protocol started"); + + // 启动 Bundle 传输服务 + let bundles_dir = PathBuf::from(format!("{}/bundles", root_path)); + let bundle_storage = bundles_dir.clone(); + let bundle_service = Arc::new(BundleService::new(Arc::clone(conn_mgr), bundle_storage)); + tokio::spawn(bundle_service.clone().start()); + tracing::info!("Bundle transfer service started"); + + // 启动 Bundle 同步后台任务 + let bundle_service_for_sync = Arc::new(tokio::sync::Mutex::new(BundleService::new( + Arc::clone(conn_mgr), + bundles_dir, + ))); + megaengine::bundle::start_bundle_sync_task(bundle_service_for_sync).await; + tracing::info!("Bundle sync task started"); + + // 启动 Repo 同步后台任务 + megaengine::repo::start_repo_sync_task().await; + tracing::info!("Repo sync task started"); + } else { + tracing::warn!("No connection manager found, services not started"); + } + + // 连接到 bootstrap node + if let Some(bootstrap_addr_str) = bootstrap_node { + connect_to_bootstrap_node(&node, bootstrap_addr_str).await; + } + + println!( + "Node started successfully: {} ({})", + node.node_id().0, + node.alias() + ); + println!("Listening on: {}", addr); + + let node_addr = NodeAddr::new(node.node_id().clone(), addr.parse()?); + println!("Node address: {}", node_addr); + println!("Press Ctrl+C to stop"); + + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } +} + +async fn connect_to_bootstrap_node( + node: &megaengine::node::node::Node, + bootstrap_addr_str: String, +) { + if let Some(conn_mgr) = &node.connection_manager { + tracing::info!( + "Attempting to connect to bootstrap node: {}", + bootstrap_addr_str + ); + + match NodeAddr::parse(&bootstrap_addr_str) { + Ok(bootstrap_info) => { + match conn_mgr + .lock() + .await + .connect( + node.node_id().clone(), + bootstrap_info.peer_id.clone(), + vec![bootstrap_info.address], + ) + .await + { + Ok(_) => { + tracing::info!( + "Successfully connected to bootstrap node {} at {}", + bootstrap_info.peer_id, + bootstrap_info.address + ); + println!( + "Connected to bootstrap node: {} at {}", + bootstrap_info.peer_id, bootstrap_info.address + ); + } + Err(e) => { + tracing::warn!("Failed to connect to bootstrap node: {}", e); + eprintln!("Warning: Failed to connect to bootstrap node: {}", e); + } + } + } + Err(e) => { + tracing::error!("Failed to parse bootstrap node address: {}", e); + eprintln!("Error: {}", e); + } + } + } +} + +pub async fn handle_node_id() -> Result<()> { + let kp = match storage::load_keypair() { + Ok(k) => k, + Err(e) => { + tracing::error!("failed to load keypair: {}", e); + tracing::info!("Run `auth init` first to generate keys"); + return Ok(()); + } + }; + + let node_id = megaengine::node::node_id::NodeId::from_keypair(&kp); + println!("{}", node_id); + Ok(()) +} + +pub async fn handle_node( + root_path: String, + _alias: String, + _addr: String, + _cert_path: String, + _bootstrap_node: Option, + action: crate::NodeAction, +) -> Result<()> { + match action { + crate::NodeAction::Start { + alias, + addr, + cert_path, + bootstrap_node, + } => handle_node_start(&root_path, alias, addr, cert_path, bootstrap_node).await, + crate::NodeAction::Id => handle_node_id().await, + } +} diff --git a/src/cli/repo.rs b/src/cli/repo.rs new file mode 100644 index 0000000..df211a4 --- /dev/null +++ b/src/cli/repo.rs @@ -0,0 +1,290 @@ +use anyhow::Result; +use megaengine::{ + git::pack::{pull_repo_from_bundle, restore_repo_from_bundle}, + node::node_id::NodeId, + repo::{self, repo::Repo, repo_id::RepoId}, + storage, + util::timestamp_now, +}; +use std::path::PathBuf; + +pub async fn handle_repo_add(path: String, description: String) -> Result<()> { + let kp = match storage::load_keypair() { + Ok(k) => k, + Err(e) => { + tracing::error!("failed to load keypair: {}", e); + tracing::info!("Run `auth init` first to generate keys"); + return Ok(()); + } + }; + let node_id = NodeId::from_keypair(&kp); + + let root_bytes = match megaengine::git::git_repo::repo_root_commit_bytes(&path) { + Ok(b) => b, + Err(e) => { + tracing::error!("failed to read repo root commit: {}", e); + println!("Ensure the provided path is a git repository with at least one commit"); + return Ok(()); + } + }; + + let repo_id = match RepoId::generate(root_bytes.as_slice(), &kp.verifying_key_bytes()) { + Ok(id) => id, + Err(e) => { + tracing::error!("Failed to generate RepoId: {}", e); + return Ok(()); + } + }; + + let name = megaengine::git::git_repo::repo_name_space(&path); + let desc = repo::repo::P2PDescription { + creator: node_id.to_string(), + name: name.clone(), + description: description.clone(), + timestamp: timestamp_now(), + }; + + let mut repo_obj = + repo::repo::Repo::new(repo_id.to_string(), desc, PathBuf::from(path.clone())); + + // Read and populate refs from the git repository + match megaengine::git::git_repo::read_repo_refs(&path) { + Ok(refs) => { + repo_obj.refs = refs; + tracing::info!("Loaded {} refs from repository", repo_obj.refs.len()); + } + Err(e) => { + tracing::warn!("Failed to read refs from repository: {}", e); + } + } + + let mut manager = repo::repo_manager::RepoManager::new(); + match manager.register_repo(repo_obj).await { + Ok(_) => tracing::info!("Repo {} added", repo_id), + Err(e) => tracing::info!("Failed to add repo: {}", e), + } + Ok(()) +} + +pub async fn handle_repo_list() -> Result<()> { + match storage::repo_model::list_repos().await { + Ok(repos) => { + if repos.is_empty() { + println!("No repositories found"); + } else { + println!("Repositories:"); + println!("{}", "─".repeat(120)); + for repo in repos { + print_repo_info(&repo).await; + } + } + } + Err(e) => { + tracing::error!("Failed to list repos: {}", e); + println!("Failed to list repositories: {}", e); + } + } + Ok(()) +} + +async fn print_repo_info(repo: &Repo) { + println!(" ID: {}", repo.repo_id); + println!(" Name: {}", repo.p2p_description.name); + println!(" Creator: {}", repo.p2p_description.creator); + println!(" Description: {}", repo.p2p_description.description); + println!(" Path: {}", repo.path.display()); + println!(" Bundle: {}", repo.bundle.display()); + println!(" Timestamp: {}", repo.p2p_description.timestamp); + + match megaengine::git::pack::extract_bundle_refs(&repo.bundle.to_string_lossy()) { + Ok(local_refs) => { + if local_refs.is_empty() { + println!(" Refs: (none)"); + } else { + println!(" Refs: ({} total)", local_refs.len()); + for (ref_name, commit) in &local_refs { + println!(" - {}: {}", ref_name, commit); + } + } + + // Check for updates if this is a local repo + if !repo.path.as_os_str().is_empty() && repo.path.exists() { + match megaengine::git::git_repo::read_repo_refs(repo.path.to_str().unwrap_or("")) { + Ok(current_refs) => { + // Compare current refs with local refs + if current_refs != local_refs { + println!(" Status: ⚠️ HAS UPDATES"); + println!(" Updated Refs: ({} total)", current_refs.len()); + for (ref_name, commit) in ¤t_refs { + let local_commit = local_refs.get(ref_name); + if local_commit != Some(commit) { + let indicator = if local_commit.is_none() { + "NEW" + } else { + "CHANGED" + }; + println!(" - {} {} : {}", indicator, ref_name, commit); + } + } + } else { + println!(" Status: ✅ Up-to-date"); + } + } + Err(e) => { + tracing::warn!("Failed to check for updates: {}", e); + println!(" Status: (failed to check: {})", e); + } + } + } + } + Err(e) => { + println!(" Refs: (failed to load: {})", e); + } + } + + println!("{}", "─".repeat(120)); +} + +pub async fn handle_repo_pull(repo_id: String) -> Result<()> { + match storage::repo_model::load_repo_from_db(&repo_id).await { + Ok(Some(repo)) => { + // Check if repo has a local path + if repo.path.as_os_str().is_empty() { + tracing::error!("Repository {} has no local path", repo_id); + println!("Error: Repository {} has no local path", repo_id); + return Ok(()); + } + // Check if bundle exists + if repo.bundle.as_os_str().is_empty() { + tracing::error!("Repository {} has no bundle available", repo_id); + println!("Error: Repository {} has no bundle available", repo_id); + return Ok(()); + } + + let result = pull_repo_from_bundle( + repo.path.as_os_str().to_str().unwrap(), + repo.bundle.as_os_str().to_str().unwrap(), + "master", + ); + + match result { + Ok(()) => { + tracing::info!("Repository {} fetched successfully from bundle", repo_id); + println!("✅ Repository updated successfully!"); + println!(" Repository: {}", repo.p2p_description.name); + println!(" Path: {}", repo.path.display()); + } + Err(e) => { + tracing::error!("Failed to spawn fetch task: {}", e); + println!("Error: Failed to spawn fetch task: {}", e); + } + } + } + Ok(None) => { + tracing::error!("Repository {} not found in database", repo_id); + println!("Error: Repository {} not found", repo_id); + } + Err(e) => { + tracing::error!("Failed to query repository {}: {}", repo_id, e); + println!("Error: Failed to query repository: {}", e); + } + } + Ok(()) +} + +pub async fn handle_repo_clone(output: String, repo_id: String) -> Result<()> { + match storage::repo_model::load_repo_from_db(&repo_id).await { + Ok(Some(mut repo)) => { + // Check if bundle exists + if repo.bundle.as_os_str().is_empty() || repo.bundle.to_string_lossy().is_empty() { + tracing::error!("Repository {} has no bundle available for cloning", repo_id); + println!("Error: Repository {} has no bundle available", repo_id); + return Ok(()); + } + + let bundle_path = repo.bundle.to_string_lossy().to_string(); + if !std::path::Path::new(&bundle_path).exists() { + tracing::error!("Bundle file not found at path: {}", bundle_path); + println!("Error: Bundle file not found at {}", bundle_path); + return Ok(()); + } + + tracing::info!( + "Cloning repository {} from bundle {} to {}", + repo_id, + bundle_path, + output + ); + + match restore_repo_from_bundle(&bundle_path, &output).await { + Ok(_) => { + tracing::info!("Repository {} cloned successfully to {}", repo_id, output); + println!("✅ Repository cloned successfully to {}", output); + println!(" Repository: {}", repo.p2p_description.name); + println!(" Creator: {}", repo.p2p_description.creator); + println!(" Description: {}", repo.p2p_description.description); + + // Read and save refs from the cloned repository + match megaengine::git::git_repo::read_repo_refs(&output) { + Ok(refs) => { + tracing::info!("Loaded {} refs from cloned repository", refs.len()); + // Save refs to the database + match storage::ref_model::batch_save_refs(&repo_id, &refs).await { + Ok(_) => { + tracing::info!( + "Refs saved to database for repository {}", + repo_id + ); + println!(" Refs: {} branches/tags", refs.len()); + } + Err(e) => { + tracing::warn!("Failed to save refs to database: {}", e); + } + } + } + Err(e) => { + tracing::warn!("Failed to read refs from cloned repository: {}", e); + } + } + + // Update repo path to the cloned location + repo.path = PathBuf::from(&output); + match storage::repo_model::save_repo_to_db(&repo).await { + Ok(_) => { + tracing::info!( + "Updated repo path to {} for repository {}", + output, + repo_id + ); + } + Err(e) => { + tracing::warn!("Failed to update repo path to database: {}", e); + } + } + } + Err(e) => { + tracing::error!("Failed to clone repository: {}", e); + println!("Error: Failed to clone repository: {}", e); + } + } + } + Ok(None) => { + tracing::error!("Repository {} not found in database", repo_id); + println!("Error: Repository {} not found", repo_id); + } + Err(e) => { + tracing::error!("Failed to query repository {}: {}", repo_id, e); + println!("Error: Failed to query repository: {}", e); + } + } + Ok(()) +} + +pub async fn handle_repo(action: crate::RepoAction) -> Result<()> { + match action { + crate::RepoAction::Add { path, description } => handle_repo_add(path, description).await, + crate::RepoAction::List => handle_repo_list().await, + crate::RepoAction::Pull { repo_id } => handle_repo_pull(repo_id).await, + crate::RepoAction::Clone { output, repo_id } => handle_repo_clone(output, repo_id).await, + } +} diff --git a/src/git/pack.rs b/src/git/pack.rs index c62c235..ba3b290 100644 --- a/src/git/pack.rs +++ b/src/git/pack.rs @@ -129,3 +129,103 @@ pub async fn restore_repo_from_bundle(bundle_path: &str, output_path: &str) -> R .await .map_err(|e| anyhow::anyhow!("failed to spawn bundle restore task: {}", e))? } + +/// Extract refs information from a git bundle file +/// Uses `git bundle list-heads` to get the refs and their commit hashes +/// +/// # Arguments +/// * `bundle_path` - Path to the bundle file +/// +/// # Returns +/// A HashMap of ref_name -> commit_hash +/// +/// # Example +/// ```ignore +/// let refs = extract_bundle_refs("/tmp/repo.bundle")?; +/// ``` +pub fn extract_bundle_refs(bundle_path: &str) -> Result> { + // 检查 bundle 文件是否存在 + if !Path::new(bundle_path).exists() { + return Err(anyhow::anyhow!("bundle file not found: {}", bundle_path)); + } + + // 使用 git bundle list-heads 获取 bundle 中的 refs + let output = Command::new("git") + .arg("bundle") + .arg("list-heads") + .arg(bundle_path) + .output() + .map_err(|e| anyhow::anyhow!("failed to execute git bundle list-heads: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow::anyhow!("git bundle list-heads failed: {}", stderr)); + } + + // 解析输出,格式为: + let stdout = String::from_utf8_lossy(&output.stdout); + let mut refs = std::collections::HashMap::new(); + + for line in stdout.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + let commit_hash = parts[0].to_string(); + let ref_name = parts[1].to_string(); + refs.insert(ref_name, commit_hash); + } + } + + Ok(refs) +} + +/// Pull updates from a git bundle file into an existing repository +/// This updates the existing repository with commits from the bundle +/// +/// # Arguments +/// * `repo_path` - Path to the existing git repository +/// * `bundle_path` - Path to the bundle file +/// * `branch` - Branch name to pull from the bundle (e.g., "master" or "refs/heads/master") +/// +/// # Example +/// ```ignore +/// pull_repo_from_bundle("/path/to/repo", "/tmp/repo.bundle", "master")?; +/// ``` +pub fn pull_repo_from_bundle(repo_path: &str, bundle_path: &str, branch: &str) -> Result<()> { + // 检查 bundle 文件是否存在 + if !Path::new(bundle_path).exists() { + return Err(anyhow::anyhow!("bundle file not found: {}", bundle_path)); + } + + // 检查仓库是否存在 + if !Path::new(repo_path).exists() { + return Err(anyhow::anyhow!("repository not found: {}", repo_path)); + } + + let _repo = Repository::open(repo_path) + .map_err(|e| anyhow::anyhow!("failed to open git repo: {}", e))?; + + // 构建分支引用名称,确保格式正确 + let _ref_spec = if branch.starts_with("refs/") { + branch.to_string() + } else if branch == "HEAD" { + "HEAD".to_string() + } else { + format!("refs/heads/{}", branch) + }; + + // 使用 git pull 从 bundle 拉取更新 + let output = Command::new("git") + .current_dir(repo_path) + .arg("pull") + .arg(bundle_path) + .arg(branch) + .output() + .map_err(|e| anyhow::anyhow!("failed to execute git pull: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow::anyhow!("git pull from bundle failed: {}", stderr)); + } + + Ok(()) +} diff --git a/src/gossip/service.rs b/src/gossip/service.rs index b085947..ba70983 100644 --- a/src/gossip/service.rs +++ b/src/gossip/service.rs @@ -25,7 +25,7 @@ pub struct GossipService { seen: Arc>>, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug)] struct Envelope { payload: SignedMessage, ttl: u8, @@ -73,6 +73,7 @@ impl GossipService { payload: signed, ttl: DEFAULT_TTL, }; + tracing::debug!("Broadcasting NodeAnnouncement: {:?}", env); let data = serde_json::to_vec(&env).unwrap_or_default(); let mgr = s2.manager.lock().await; let peers = mgr.list_peers().await; @@ -92,6 +93,7 @@ impl GossipService { payload: signed, ttl: DEFAULT_TTL, }; + tracing::debug!("Broadcasting RepoAnnouncement: {:?}", env); let data = serde_json::to_vec(&env).unwrap_or_default(); let mgr = s2.manager.lock().await; let peers = mgr.list_peers().await; @@ -102,7 +104,7 @@ impl GossipService { } } - tokio::time::sleep(Duration::from_secs(10)).await; + tokio::time::sleep(Duration::from_secs(30)).await; } }); @@ -195,18 +197,177 @@ impl GossipService { ); // 将每个 repo 保存到数据库 for repo in &ra.repos { - // 检查仓库是否已存在,如果存在则跳过 - if let Ok(Some(_)) = - crate::storage::repo_model::load_repo_from_db(&repo.repo_id).await - { - tracing::debug!("Repo {} already exists, skipping", &repo.repo_id); - continue; - } + // 检查仓库是否已存在 + match crate::storage::repo_model::load_repo_from_db(&repo.repo_id).await { + Ok(Some(local_repo)) => { + // 如果是本地仓库,不更新 + if !local_repo.is_external { + tracing::debug!( + "Repo {} is a local repository, skipping update", + &repo.repo_id + ); + continue; + } + + // Repo 已存在,检查是否需要更新 + tracing::debug!( + "Repo {} already exists, checking if update needed", + &repo.repo_id + ); + + // 比较 refs:从 bundle 中提取本地 refs + let local_refs = if !local_repo.bundle.as_os_str().is_empty() { + // Bundle 存在,从 bundle 中提取 refs + let bundle_path = local_repo.bundle.to_string_lossy().to_string(); + match crate::git::pack::extract_bundle_refs(&bundle_path) { + Ok(refs) => { + tracing::debug!( + "Extracted {} refs from bundle for repo {}", + refs.len(), + &repo.repo_id + ); + refs + } + Err(e) => { + tracing::warn!( + "Failed to extract refs from bundle for repo {}: {}", + &repo.repo_id, + e + ); + // 如果 bundle 提取失败,从数据库读取 refs 作为备份 + match crate::storage::ref_model::load_refs_for_repo( + &repo.repo_id, + ) + .await + { + Ok(refs) => refs, + Err(e2) => { + tracing::warn!( + "Failed to load local refs for repo {}: {}", + &repo.repo_id, + e2 + ); + continue; + } + } + } + } + } else { + // Bundle 不存在,从数据库读取 refs + match crate::storage::ref_model::load_refs_for_repo(&repo.repo_id) + .await + { + Ok(refs) => refs, + Err(e) => { + tracing::warn!( + "Failed to load local refs for repo {}: {}", + &repo.repo_id, + e + ); + continue; + } + } + }; + + // 检查 2:如果远端 refs 与本地相同,不更新 + if local_refs == repo.refs { + tracing::debug!("Repo {} refs are up-to-date", &repo.repo_id); + continue; + } + + // 有新的 refs 更新,清空 bundle 等待重新同步 + tracing::info!( + "Detected ref updates for repo {} from node {}", + &repo.repo_id, + ra.node_id + ); + + // 删除旧的 bundle 文件 + if !local_repo.bundle.as_os_str().is_empty() { + let bundle_path = local_repo.bundle.to_string_lossy().to_string(); + match tokio::fs::remove_file(&bundle_path).await { + Ok(_) => { + tracing::info!( + "Deleted outdated bundle for repo {}", + &repo.repo_id + ); + } + Err(e) => { + tracing::warn!( + "Failed to delete bundle file {}: {}", + bundle_path, + e + ); + } + } + } - let mut repo = repo.clone(); - repo.is_external = true; - if let Err(e) = crate::storage::repo_model::save_repo_to_db(&repo).await { - tracing::warn!("Failed to save remote repo {} to db: {}", &repo.repo_id, e); + // 清空旧的 refs 并添加最新的 refs + if let Err(e) = + crate::storage::ref_model::delete_refs_for_repo(&repo.repo_id).await + { + tracing::warn!( + "Failed to delete refs for repo {}: {}", + &repo.repo_id, + e + ); + } + + // 添加最新的 refs + if let Err(e) = crate::storage::ref_model::batch_save_refs( + &repo.repo_id, + &repo.refs, + ) + .await + { + tracing::warn!( + "Failed to save new refs for repo {}: {}", + &repo.repo_id, + e + ); + } else { + tracing::info!( + "Updated refs for repo {} with {} new refs", + &repo.repo_id, + repo.refs.len() + ); + } + + // 更新 repo 表:清空 bundle 字段 + if let Err(e) = + crate::storage::repo_model::update_repo_bundle(&repo.repo_id, "") + .await + { + tracing::warn!( + "Failed to clear bundle for repo {}: {}", + &repo.repo_id, + e + ); + } + + tracing::info!( + "Cleared bundle and refs for repo {}, waiting for automatic sync", + &repo.repo_id + ); + } + Ok(None) => { + // Repo 不存在,插入为 external repo + tracing::debug!("Repo {} is new, adding as external", &repo.repo_id); + let mut new_repo = repo.clone(); + new_repo.is_external = true; + if let Err(e) = + crate::storage::repo_model::save_repo_to_db(&new_repo).await + { + tracing::warn!( + "Failed to save remote repo {} to db: {}", + &repo.repo_id, + e + ); + } + } + Err(e) => { + tracing::warn!("Failed to load repo {}: {}", &repo.repo_id, e); + } } } } diff --git a/src/main.rs b/src/main.rs index d9cff45..0065742 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,8 @@ use anyhow::Result; use clap::{Parser, Subcommand}; -use megaengine::bundle::BundleService; -use megaengine::git::git_repo::{repo_name_space, repo_root_commit_bytes}; -use megaengine::git::pack::restore_repo_from_bundle; -use megaengine::node::node_addr::NodeAddr; -use std::net::SocketAddr; -use std::path::PathBuf; -use megaengine::gossip::GossipService; -use megaengine::{ - node::node_id::NodeId, - repo::{self, repo_id::RepoId}, - storage, - util::timestamp_now, -}; +mod cli; +use cli::{handle_auth, handle_node, handle_repo}; #[derive(Parser)] #[command(name = "megaengine")] @@ -88,6 +77,12 @@ enum RepoAction { }, /// List all repositories List, + /// Update repository from bundle (like git pull) + Pull { + /// Repository ID + #[arg(long)] + repo_id: String, + }, Clone { #[arg(long)] output: String, @@ -103,7 +98,7 @@ async fn main() -> Result<()> { // 初始化 tracing 日志 let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("error,megaengine=info")); + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("error,megaengine=debug")); tracing_subscriber::fmt() .with_env_filter(env_filter) @@ -113,361 +108,40 @@ async fn main() -> Result<()> { let cli = Cli::parse(); - let root_path = if let Ok(env_root) = std::env::var("MEGAENGINE_ROOT") { - env_root - } else { - let path = if cli.root.starts_with("~/") { - let home = std::env::var("HOME") - .or_else(|_| std::env::var("USERPROFILE")) - .unwrap_or_else(|_| ".".to_string()); - cli.root.replace("~", &home) - } else { - cli.root.clone() - }; - std::env::set_var("MEGAENGINE_ROOT", &path); - path - }; + let root_path = resolve_root_path(&cli.root)?; match cli.command { Commands::Auth { action } => match action { AuthAction::Init => { - let kp_path = storage::keypair_path(); - if kp_path.exists() { - tracing::info!( - "Keypair already exists at {:?}; skipping generation", - kp_path - ); - } else { - tracing::info!("Generating new keypair..."); - let kp = megaengine::identity::keypair::KeyPair::generate()?; - storage::save_keypair(&kp)?; - tracing::info!("Keypair saved to {:?}", storage::keypair_path()); - } + handle_auth().await?; } }, - Commands::Node { action } => match action { - NodeAction::Start { - alias, - addr, - cert_path, - bootstrap_node, - } => { - tracing::info!("Starting node..."); - let cert_dir = format!("{}/{}", &root_path, cert_path); - megaengine::transport::cert::ensure_certificates( - &format!("{}/cert.pem", cert_dir), - &format!("{}/key.pem", cert_dir), - &format!("{}/ca-cert.pem", cert_dir), - )?; - - let kp = match storage::load_keypair() { - Ok(k) => k, - Err(e) => { - tracing::error!("failed to load keypair: {}", e); - tracing::info!("Run `auth init` first to generate keys"); - return Ok(()); - } - }; - - let addrs: Vec = vec![addr.parse()?]; - - let mut node = megaengine::node::node::Node::from_keypair( - &kp, - &alias, - addrs.clone(), - megaengine::node::node::NodeType::Normal, - ); - tracing::info!( - "Node initialized: alias={} id={}", - node.alias(), - node.node_id().0 - ); - - let quic_config = megaengine::transport::config::QuicConfig::new( - addr.parse()?, - format!("{}/cert.pem", cert_dir), - format!("{}/key.pem", cert_dir), - format!("{}/ca-cert.pem", cert_dir), - ); - - tracing::info!("Starting QUIC server on {}...", addr); - node.start_quic_server(quic_config).await?; - - if let Some(conn_mgr) = &node.connection_manager { - // 启动 Gossip 服务 - let gossip = std::sync::Arc::new(GossipService::new( - std::sync::Arc::clone(conn_mgr), - node.clone(), - None, - )); - tokio::spawn(gossip.start()); - tracing::info!("Gossip protocol started"); - - // 启动 Bundle 传输服务 - let bundles_dir = PathBuf::from(format!("{}/bundles", &root_path)); - let bundle_storage = bundles_dir.clone(); - let bundle_service = std::sync::Arc::new(BundleService::new( - std::sync::Arc::clone(conn_mgr), - bundle_storage, - )); - tokio::spawn(bundle_service.clone().start()); - tracing::info!("Bundle transfer service started"); - - // 启动 Bundle 同步后台任务:定时检查 external repos 并请求 bundle - let bundle_service_for_sync = std::sync::Arc::new(tokio::sync::Mutex::new( - BundleService::new(std::sync::Arc::clone(conn_mgr), bundles_dir), - )); - megaengine::bundle::start_bundle_sync_task(bundle_service_for_sync).await; - } else { - tracing::warn!("No connection manager found, services not started"); - } - - // 如果提供了 bootstrap_node,尝试连接到它 - if let Some(bootstrap_addr_str) = bootstrap_node { - if let Some(conn_mgr) = &node.connection_manager { - tracing::info!( - "Attempting to connect to bootstrap node: {}", - bootstrap_addr_str - ); - - match NodeAddr::parse(&bootstrap_addr_str) { - Ok(bootstrap_info) => { - match conn_mgr - .lock() - .await - .connect( - node.node_id().clone(), - bootstrap_info.peer_id.clone(), - vec![bootstrap_info.address], - ) - .await - { - Ok(_) => { - tracing::info!( - "Successfully connected to bootstrap node {} at {}", - bootstrap_info.peer_id, - bootstrap_info.address - ); - println!( - "Connected to bootstrap node: {} at {}", - bootstrap_info.peer_id, bootstrap_info.address - ); - } - Err(e) => { - tracing::warn!( - "Failed to connect to bootstrap node: {}", - e - ); - eprintln!( - "Warning: Failed to connect to bootstrap node: {}", - e - ); - } - } - } - Err(e) => { - tracing::error!("Failed to parse bootstrap node address: {}", e); - eprintln!("Error: {}", e); - return Err(e); - } - } - } - } - - println!( - "Node started successfully: {} ({})", - node.node_id().0, - node.alias() - ); - println!("Listening on: {}", addr); - - // 打印 node 地址 - let addr = NodeAddr::new(node.node_id().clone(), addr.parse()?); - println!("Node address: {}", addr); - - println!("Press Ctrl+C to stop"); - - loop { - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - } - } - NodeAction::Id => { - let kp = match storage::load_keypair() { - Ok(k) => k, - Err(e) => { - tracing::error!("failed to load keypair: {}", e); - tracing::info!("Run `auth init` first to generate keys"); - return Ok(()); - } - }; - - let node_id = NodeId::from_keypair(&kp); - println!("{}", node_id); - } - }, - Commands::Repo { action } => match action { - RepoAction::Add { path, description } => { - let kp = match storage::load_keypair() { - Ok(k) => k, - Err(e) => { - tracing::error!("failed to load keypair: {}", e); - tracing::info!("Run `auth init` first to generate keys"); - return Ok(()); - } - }; - let node_id = NodeId::from_keypair(&kp); - - let root_bytes = match repo_root_commit_bytes(&path) { - Ok(b) => b, - Err(e) => { - tracing::error!("failed to read repo root commit: {}", e); - println!( - "Ensure the provided path is a git repository with at least one commit" - ); - return Ok(()); - } - }; - - let repo_id = - match RepoId::generate(root_bytes.as_slice(), &kp.verifying_key_bytes()) { - Ok(id) => id, - Err(e) => { - tracing::error!("Failed to generate RepoId: {}", e); - return Ok(()); - } - }; - - let name = repo_name_space(&path); - let desc = repo::repo::P2PDescription { - creator: node_id.to_string(), - name: name.clone(), - description: description.clone(), - timestamp: timestamp_now(), - }; - - let mut repo_obj = repo::repo::Repo::new(repo_id.to_string(), desc, PathBuf::from(path.clone())); - - // Read and populate refs from the git repository - match megaengine::git::git_repo::read_repo_refs(&path) { - Ok(refs) => { - repo_obj.refs = refs; - tracing::info!("Loaded {} refs from repository", repo_obj.refs.len()); - } - Err(e) => { - tracing::warn!("Failed to read refs from repository: {}", e); - } - } - - let mut manager = repo::repo_manager::RepoManager::new(); - match manager.register_repo(repo_obj).await { - Ok(_) => tracing::info!("Repo {} added", repo_id), - Err(e) => tracing::info!("Failed to add repo: {}", e), - } - } - RepoAction::List => match storage::repo_model::list_repos().await { - Ok(repos) => { - if repos.is_empty() { - println!("No repositories found"); - } else { - println!("Repositories:"); - println!("{}", "─".repeat(100)); - for repo in repos { - println!(" ID: {}", repo.repo_id); - println!(" Name: {}", repo.p2p_description.name); - println!(" Creator: {}", repo.p2p_description.creator); - println!(" Description: {}", repo.p2p_description.description); - println!(" Path: {}", repo.path.display()); - println!(" Timestamp: {}", repo.p2p_description.timestamp); - println!("{}", "─".repeat(100)); - } - } - } - Err(e) => { - tracing::error!("Failed to list repos: {}", e); - println!("Failed to list repositories: {}", e); - } - }, - RepoAction::Clone { output, repo_id } => { - // 查询数据库获取 repo 信息 - match storage::repo_model::load_repo_from_db(&repo_id).await { - Ok(Some(repo)) => { - // 检查 bundle 是否存在 - if repo.bundle.as_os_str().is_empty() - || repo.bundle.to_string_lossy().is_empty() - { - tracing::error!( - "Repository {} has no bundle available for cloning", - repo_id - ); - println!("Error: Repository {} has no bundle available", repo_id); - return Ok(()); - } - - let bundle_path = repo.bundle.to_string_lossy().to_string(); - if !std::path::Path::new(&bundle_path).exists() { - tracing::error!("Bundle file not found at path: {}", bundle_path); - println!("Error: Bundle file not found at {}", bundle_path); - return Ok(()); - } - - // 使用 git pack 模块中的函数恢复仓库 - tracing::info!( - "Cloning repository {} from bundle {} to {}", - repo_id, - bundle_path, - output - ); + Commands::Node { action } => { + handle_node(root_path, String::new(), String::new(), String::new(), None, action) + .await?; + } + Commands::Repo { action } => { + handle_repo(action).await?; + } + } - match restore_repo_from_bundle(&bundle_path, &output).await { - Ok(_) => { - tracing::info!( - "Repository {} cloned successfully to {}", - repo_id, - output - ); - println!("✅ Repository cloned successfully to {}", output); - println!(" Repository: {}", repo.p2p_description.name); - println!(" Creator: {}", repo.p2p_description.creator); - println!(" Description: {}", repo.p2p_description.description); + Ok(()) +} - // Read and save refs from the cloned repository - match megaengine::git::git_repo::read_repo_refs(&output) { - Ok(refs) => { - tracing::info!("Loaded {} refs from cloned repository", refs.len()); - // Save refs to the database - match storage::ref_model::batch_save_refs(&repo_id, &refs).await { - Ok(_) => { - tracing::info!("Refs saved to database for repository {}", repo_id); - println!(" Refs: {} branches/tags", refs.len()); - } - Err(e) => { - tracing::warn!("Failed to save refs to database: {}", e); - } - } - } - Err(e) => { - tracing::warn!("Failed to read refs from cloned repository: {}", e); - } - } - } - Err(e) => { - tracing::error!("Failed to clone repository: {}", e); - println!("Error: Failed to clone repository: {}", e); - } - } - } - Ok(None) => { - tracing::error!("Repository {} not found in database", repo_id); - println!("Error: Repository {} not found", repo_id); - } - Err(e) => { - tracing::error!("Failed to query repository {}: {}", repo_id, e); - println!("Error: Failed to query repository: {}", e); - } - } - } - }, +fn resolve_root_path(root_arg: &str) -> Result { + if let Ok(env_root) = std::env::var("MEGAENGINE_ROOT") { + return Ok(env_root); } - Ok(()) + let path = if root_arg.starts_with("~/") { + let home = std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) + .unwrap_or_else(|_| ".".to_string()); + root_arg.replace("~", &home) + } else { + root_arg.to_string() + }; + + std::env::set_var("MEGAENGINE_ROOT", &path); + Ok(path) } diff --git a/src/repo/mod.rs b/src/repo/mod.rs index ade3bd0..8b6b7a4 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -2,3 +2,6 @@ pub mod repo; pub mod repo_id; pub mod repo_manager; +pub mod repo_sync; + +pub use repo_sync::start_repo_sync_task; diff --git a/src/repo/repo_sync.rs b/src/repo/repo_sync.rs new file mode 100644 index 0000000..d983503 --- /dev/null +++ b/src/repo/repo_sync.rs @@ -0,0 +1,85 @@ +use crate::git::git_repo::read_repo_refs; +use crate::storage::{ref_model, repo_model}; +use anyhow::Result; +use std::time::Duration; +use tokio::time::interval; +use tracing::{debug, info, warn}; + +const REPO_CHECK_INTERVAL: Duration = Duration::from_secs(60); + +/// 后台任务:定时检查本地 repos 的 refs 是否有更新 +pub async fn start_repo_sync_task() { + tokio::spawn(async move { + let mut tick = interval(REPO_CHECK_INTERVAL); + + loop { + tick.tick().await; + + debug!("Starting repo refs check"); + + // 查询所有 repos + match repo_model::list_repos().await { + Ok(repos) => { + for repo in repos { + // 只检查本地 repos (is_external=false) + if !repo.is_external { + if let Err(e) = check_and_update_repo_refs(&repo).await { + warn!("Failed to check refs for repo {}: {}", repo.repo_id, e); + } + } + } + } + Err(e) => { + warn!("Failed to list repos during sync check: {}", e); + } + } + } + }); +} + +/// 检查仓库的 refs 是否有更新,如果有则更新数据库 +async fn check_and_update_repo_refs(repo: &crate::repo::repo::Repo) -> Result<()> { + let repo_path = repo.path.to_string_lossy().to_string(); + + // 从 git 仓库读取最新的 refs + let current_refs = read_repo_refs(&repo_path)?; + + // 检查是否有变化 + if ref_model::has_refs_changed(&repo.repo_id, ¤t_refs).await? { + info!( + "Detected refs change in local repo {}, updating database", + repo.repo_id + ); + + // 更新 refs 到数据库 + ref_model::batch_save_refs(&repo.repo_id, ¤t_refs).await?; + + // 这里后续可以扩展: + // 1. 重新生成 bundle + // 2. 广播 repo announcement 告知其他节点有更新 + // TODO: trigger bundle generation and gossip announcement + + debug!( + "Successfully updated refs for repo {} ({} refs)", + repo.repo_id, + current_refs.len() + ); + } else { + debug!("No changes detected in repo {}", repo.repo_id); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_repo_sync_task_spawns() { + // 只测试任务能否正常启动,不测试实际功能 + start_repo_sync_task().await; + // 任务已在后台运行,测试通过 + tokio::time::sleep(Duration::from_millis(100)).await; + } +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 9f918e1..9390569 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,6 +1,6 @@ pub mod node_model; -pub mod repo_model; pub mod ref_model; +pub mod repo_model; use anyhow::Result; use sea_orm::{ConnectOptions, ConnectionTrait, Database, DatabaseConnection}; diff --git a/src/storage/ref_model.rs b/src/storage/ref_model.rs index 4cebb45..15fbce1 100644 --- a/src/storage/ref_model.rs +++ b/src/storage/ref_model.rs @@ -95,7 +95,9 @@ pub async fn batch_save_refs( } /// Load all refs for a repository -pub async fn load_refs_for_repo(repo_id: &str) -> Result> { +pub async fn load_refs_for_repo( + repo_id: &str, +) -> Result> { let db = init_db().await?; let refs = Entity::find() @@ -211,7 +213,10 @@ mod tests { let loaded = load_refs_for_repo(repo_id).await?; assert_eq!(loaded.len(), 3); assert_eq!(loaded.get("refs/heads/main"), Some(&"abc123".to_string())); - assert_eq!(loaded.get("refs/heads/develop"), Some(&"def456".to_string())); + assert_eq!( + loaded.get("refs/heads/develop"), + Some(&"def456".to_string()) + ); assert_eq!(loaded.get("refs/tags/v1.0"), Some(&"ghi789".to_string())); // Cleanup From 56ffe4cba8f48da62414723b4087104fe77584b5 Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Fri, 2 Jan 2026 19:11:50 +0800 Subject: [PATCH 19/42] Update src/cli/repo.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/cli/repo.rs | 75 ++++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/src/cli/repo.rs b/src/cli/repo.rs index df211a4..12c95aa 100644 --- a/src/cli/repo.rs +++ b/src/cli/repo.rs @@ -96,49 +96,54 @@ async fn print_repo_info(repo: &Repo) { println!(" Bundle: {}", repo.bundle.display()); println!(" Timestamp: {}", repo.p2p_description.timestamp); - match megaengine::git::pack::extract_bundle_refs(&repo.bundle.to_string_lossy()) { - Ok(local_refs) => { - if local_refs.is_empty() { - println!(" Refs: (none)"); - } else { - println!(" Refs: ({} total)", local_refs.len()); - for (ref_name, commit) in &local_refs { - println!(" - {}: {}", ref_name, commit); + if repo.bundle.as_os_str().is_empty() { + // No bundle path configured; avoid calling extract_bundle_refs on an empty path. + println!(" Refs: (bundle path not set)"); + } else { + match megaengine::git::pack::extract_bundle_refs(&repo.bundle.to_string_lossy()) { + Ok(local_refs) => { + if local_refs.is_empty() { + println!(" Refs: (none)"); + } else { + println!(" Refs: ({} total)", local_refs.len()); + for (ref_name, commit) in &local_refs { + println!(" - {}: {}", ref_name, commit); + } } - } - // Check for updates if this is a local repo - if !repo.path.as_os_str().is_empty() && repo.path.exists() { - match megaengine::git::git_repo::read_repo_refs(repo.path.to_str().unwrap_or("")) { - Ok(current_refs) => { - // Compare current refs with local refs - if current_refs != local_refs { - println!(" Status: ⚠️ HAS UPDATES"); - println!(" Updated Refs: ({} total)", current_refs.len()); - for (ref_name, commit) in ¤t_refs { - let local_commit = local_refs.get(ref_name); - if local_commit != Some(commit) { - let indicator = if local_commit.is_none() { - "NEW" - } else { - "CHANGED" - }; - println!(" - {} {} : {}", indicator, ref_name, commit); + // Check for updates if this is a local repo + if !repo.path.as_os_str().is_empty() && repo.path.exists() { + match megaengine::git::git_repo::read_repo_refs(repo.path.to_str().unwrap_or("")) { + Ok(current_refs) => { + // Compare current refs with local refs + if current_refs != local_refs { + println!(" Status: ⚠️ HAS UPDATES"); + println!(" Updated Refs: ({} total)", current_refs.len()); + for (ref_name, commit) in ¤t_refs { + let local_commit = local_refs.get(ref_name); + if local_commit != Some(commit) { + let indicator = if local_commit.is_none() { + "NEW" + } else { + "CHANGED" + }; + println!(" - {} {} : {}", indicator, ref_name, commit); + } } + } else { + println!(" Status: ✅ Up-to-date"); } - } else { - println!(" Status: ✅ Up-to-date"); } - } - Err(e) => { - tracing::warn!("Failed to check for updates: {}", e); - println!(" Status: (failed to check: {})", e); + Err(e) => { + tracing::warn!("Failed to check for updates: {}", e); + println!(" Status: (failed to check: {})", e); + } } } } - } - Err(e) => { - println!(" Refs: (failed to load: {})", e); + Err(e) => { + println!(" Refs: (failed to load: {})", e); + } } } From d155e6c0858016cb7e643bd192e5e18135370da6 Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Fri, 2 Jan 2026 19:12:14 +0800 Subject: [PATCH 20/42] Update src/storage/mod.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/storage/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 9390569..f576987 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -147,7 +147,7 @@ pub async fn init_db() -> Result { ref_name TEXT NOT NULL, commit_hash TEXT NOT NULL, updated_at INTEGER NOT NULL, - UNIQUE(repo_id, ref_name) + UNIQUE(repo_id, ref_name) ON CONFLICT REPLACE )", ) .await; From 469423026c09e01ae66667371694d06bfdafa638 Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Fri, 2 Jan 2026 19:12:30 +0800 Subject: [PATCH 21/42] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 838c026..a5ac920 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Keep this terminal running as well. **Terminal 3** - Add a repository on node1: ```bash -cargo run -- repo add --path E:\git_test\tiny --description "Tiny" +cargo run -- repo add --path /path/to/git_test/tiny --description "Tiny" ``` The output will display the repo ID. Save this ID for later use. From 541395d3335f1524a22c870cab1ed3fd66ad4ab4 Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Fri, 2 Jan 2026 19:12:50 +0800 Subject: [PATCH 22/42] Update src/cli/repo.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/cli/repo.rs | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/cli/repo.rs b/src/cli/repo.rs index 12c95aa..99efdc0 100644 --- a/src/cli/repo.rs +++ b/src/cli/repo.rs @@ -166,9 +166,41 @@ pub async fn handle_repo_pull(repo_id: String) -> Result<()> { return Ok(()); } + let path_str = match repo.path.as_os_str().to_str() { + Some(s) => s, + None => { + tracing::error!( + "Repository {} has a local path that is not valid UTF-8: {}", + repo_id, + repo.path.display() + ); + println!( + "Error: Repository {} has a local path that is not valid UTF-8", + repo_id + ); + return Ok(()); + } + }; + + let bundle_str = match repo.bundle.as_os_str().to_str() { + Some(s) => s, + None => { + tracing::error!( + "Repository {} has a bundle path that is not valid UTF-8: {}", + repo_id, + repo.bundle.display() + ); + println!( + "Error: Repository {} has a bundle path that is not valid UTF-8", + repo_id + ); + return Ok(()); + } + }; + let result = pull_repo_from_bundle( - repo.path.as_os_str().to_str().unwrap(), - repo.bundle.as_os_str().to_str().unwrap(), + path_str, + bundle_str, "master", ); From a415011e518758eb2f68685cd7922f7645830e0a Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Fri, 2 Jan 2026 19:13:13 +0800 Subject: [PATCH 23/42] Update src/git/pack.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/git/pack.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git/pack.rs b/src/git/pack.rs index ba3b290..805f395 100644 --- a/src/git/pack.rs +++ b/src/git/pack.rs @@ -201,7 +201,7 @@ pub fn pull_repo_from_bundle(repo_path: &str, bundle_path: &str, branch: &str) - return Err(anyhow::anyhow!("repository not found: {}", repo_path)); } - let _repo = Repository::open(repo_path) + Repository::open(repo_path) .map_err(|e| anyhow::anyhow!("failed to open git repo: {}", e))?; // 构建分支引用名称,确保格式正确 From 673f9334118171d09e1c724ab66d7f9eef7acd03 Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Fri, 2 Jan 2026 19:16:30 +0800 Subject: [PATCH 24/42] Update src/cli/node.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/cli/node.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/cli/node.rs b/src/cli/node.rs index 4a4c0df..18b62a8 100644 --- a/src/cli/node.rs +++ b/src/cli/node.rs @@ -170,10 +170,6 @@ pub async fn handle_node_id() -> Result<()> { pub async fn handle_node( root_path: String, - _alias: String, - _addr: String, - _cert_path: String, - _bootstrap_node: Option, action: crate::NodeAction, ) -> Result<()> { match action { From d7f7418132a75b87f1e751e0f6a4a1c7af09c910 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Sun, 4 Jan 2026 12:47:44 +0800 Subject: [PATCH 25/42] fix --- src/cli/node.rs | 5 +---- src/main.rs | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/cli/node.rs b/src/cli/node.rs index 18b62a8..c445ea8 100644 --- a/src/cli/node.rs +++ b/src/cli/node.rs @@ -168,10 +168,7 @@ pub async fn handle_node_id() -> Result<()> { Ok(()) } -pub async fn handle_node( - root_path: String, - action: crate::NodeAction, -) -> Result<()> { +pub async fn handle_node(root_path: String, action: crate::NodeAction) -> Result<()> { match action { crate::NodeAction::Start { alias, diff --git a/src/main.rs b/src/main.rs index 0065742..0bdd3bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -117,8 +117,7 @@ async fn main() -> Result<()> { } }, Commands::Node { action } => { - handle_node(root_path, String::new(), String::new(), String::new(), None, action) - .await?; + handle_node(root_path, action).await?; } Commands::Repo { action } => { handle_repo(action).await?; From 93e14d33f45b2ba3c95423fc3a8be6fe6145e11d Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Fri, 23 Jan 2026 17:23:55 +0800 Subject: [PATCH 26/42] mcp --- Cargo.lock | 251 ++++++++++++++++++++++++++++++++++++++- Cargo.toml | 5 + src/cli/node.rs | 40 ++++++- src/lib.rs | 1 + src/main.rs | 14 +++ src/mcp/mcp_server.rs | 267 ++++++++++++++++++++++++++++++++++++++++++ src/mcp/mod.rs | 5 + src/mcp/sse_server.rs | 208 ++++++++++++++++++++++++++++++++ 8 files changed, 788 insertions(+), 3 deletions(-) create mode 100644 src/mcp/mcp_server.rs create mode 100644 src/mcp/mod.rs create mode 100644 src/mcp/sse_server.rs diff --git a/Cargo.lock b/Cargo.lock index b69ac00..dfa4de8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,6 +172,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -201,6 +207,61 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes 1.10.1", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes 1.10.1", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base-x" version = "0.2.11" @@ -925,6 +986,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -975,6 +1037,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -996,6 +1069,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1153,6 +1227,88 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes 1.10.1", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes 1.10.1", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes 1.10.1", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes 1.10.1", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "bytes 1.10.1", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -1509,6 +1665,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "md-5" version = "0.10.6" @@ -1524,9 +1686,11 @@ name = "megaengine" version = "0.1.0" dependencies = [ "anyhow", + "axum", "chrono", "clap", "ed25519-dalek", + "futures", "git2", "hex", "multibase", @@ -1542,8 +1706,11 @@ dependencies = [ "sha2 0.10.9", "sqlx", "tokio", + "tokio-stream", + "tower-http", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -1552,6 +1719,12 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2599,6 +2772,29 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.5.0" @@ -3013,6 +3209,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "synstructure" version = "0.13.2" @@ -3187,9 +3389,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -3226,6 +3428,50 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.10.0", + "bytes 1.10.1", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" @@ -3381,6 +3627,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ + "getrandom 0.3.4", "js-sys", "serde", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index cd858f1..fde4dc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,8 @@ chrono = { version = "0.4", features = ["serde"] } sea-orm = { version = "0.12", features = ["runtime-tokio-native-tls", "sqlx-sqlite"] } sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "sqlite"] } git2 = "0.16" +axum = "0.7" +tower-http = { version = "0.5", features = ["cors"] } +uuid = { version = "1.0", features = ["v4"] } +futures = "0.3" +tokio-stream = "0.1.18" diff --git a/src/cli/node.rs b/src/cli/node.rs index c445ea8..1d97d86 100644 --- a/src/cli/node.rs +++ b/src/cli/node.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use megaengine::mcp::{start_mcp_server, start_sse_server}; use megaengine::{ bundle::BundleService, node::node_addr::NodeAddr, storage, transport::config::QuicConfig, }; @@ -11,6 +12,8 @@ pub async fn handle_node_start( addr: String, cert_path: String, bootstrap_node: Option, + enable_mcp: bool, + mcp_sse_port: Option, ) -> Result<()> { tracing::info!("Starting node..."); let cert_dir = format!("{}/{}", root_path, cert_path); @@ -101,6 +104,28 @@ pub async fn handle_node_start( println!("Node address: {}", node_addr); println!("Press Ctrl+C to stop"); + if enable_mcp { + tracing::info!("MCP server enabled, starting alongside node"); + println!("MCP server is enabled"); + std::thread::spawn(|| { + let rt = tokio::runtime::Runtime::new().unwrap(); + if let Err(e) = rt.block_on(start_mcp_server()) { + tracing::error!("MCP server error: {}", e); + } + }); + } + + if let Some(port) = mcp_sse_port { + tracing::info!("MCP SSE server enabled on port {}", port); + println!("MCP SSE server enabled on port {}", port); + tokio::spawn(async move { + let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port)); + if let Err(e) = start_sse_server(addr).await { + tracing::error!("MCP SSE server error: {}", e); + } + }); + } + loop { tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; } @@ -175,7 +200,20 @@ pub async fn handle_node(root_path: String, action: crate::NodeAction) -> Result addr, cert_path, bootstrap_node, - } => handle_node_start(&root_path, alias, addr, cert_path, bootstrap_node).await, + mcp, + mcp_sse_port, + } => { + handle_node_start( + &root_path, + alias, + addr, + cert_path, + bootstrap_node, + mcp, + mcp_sse_port, + ) + .await + } crate::NodeAction::Id => handle_node_id().await, } } diff --git a/src/lib.rs b/src/lib.rs index ef483b6..a0b4593 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod bundle; pub mod git; pub mod gossip; pub mod identity; +pub mod mcp; pub mod node; pub mod repo; pub mod storage; diff --git a/src/main.rs b/src/main.rs index 0bdd3bc..021218c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,8 @@ enum Commands { #[command(subcommand)] action: RepoAction, }, + /// Start MCP server (Stdio mode) + Mcp, } #[derive(Subcommand)] @@ -58,6 +60,14 @@ enum NodeAction { /// Bootstrap node address to connect to on startup (e.g., 127.0.0.1:9000) #[arg(long)] bootstrap_node: Option, + + /// Start MCP server alongside the node + #[arg(long, default_value = "false")] + mcp: bool, + + /// Start MCP SSE server on the specified port (e.g., 3001) + #[arg(long)] + mcp_sse_port: Option, }, /// Print node id using stored keypair Id, @@ -104,6 +114,7 @@ async fn main() -> Result<()> { .with_env_filter(env_filter) .with_target(true) .with_level(true) + .with_writer(std::io::stderr) .init(); let cli = Cli::parse(); @@ -122,6 +133,9 @@ async fn main() -> Result<()> { Commands::Repo { action } => { handle_repo(action).await?; } + Commands::Mcp => { + megaengine::mcp::start_mcp_server().await?; + } } Ok(()) diff --git a/src/mcp/mcp_server.rs b/src/mcp/mcp_server.rs new file mode 100644 index 0000000..34b2e2d --- /dev/null +++ b/src/mcp/mcp_server.rs @@ -0,0 +1,267 @@ +use crate::git::pack; +use crate::storage; +use anyhow::Result; +use serde_json::{json, Value}; +use std::io::{self, BufRead, BufReader, BufWriter, Write}; + +/// MCP Server implementation for repository operations +pub struct RepoMcpServer; + +impl RepoMcpServer { + /// Get list of available tools + pub fn get_tools() -> Vec { + vec![ + json!({ + "name": "list_repos", + "description": "List all repositories with their details and refs", + "inputSchema": { + "type": "object", + "properties": {}, + "required": [] + } + }), + json!({ + "name": "get_repo_details", + "description": "Get detailed information about a specific repository", + "inputSchema": { + "type": "object", + "properties": { + "repo_id": { + "type": "string", + "description": "The ID of the repository" + } + }, + "required": ["repo_id"] + } + }), + ] + } + + /// Execute a tool with the given arguments + pub async fn execute_tool(name: &str, args: Value) -> Result { + match name { + "list_repos" => Self::list_repos().await, + "get_repo_details" => { + let repo_id = args + .get("repo_id") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing repo_id parameter"))?; + Self::get_repo_details(repo_id).await + } + _ => Err(anyhow::anyhow!("Unknown tool: {}", name)), + } + } + + /// List all repositories + async fn list_repos() -> Result { + match storage::repo_model::list_repos().await { + Ok(repos) => { + let repo_list: Vec = repos + .iter() + .map(|repo| { + let mut repo_info = json!({ + "repo_id": repo.repo_id, + "name": repo.p2p_description.name, + "creator": repo.p2p_description.creator, + "description": repo.p2p_description.description, + "path": repo.path.display().to_string(), + "bundle": repo.bundle.display().to_string(), + "timestamp": repo.p2p_description.timestamp, + }); + + // Extract refs information + if !repo.bundle.as_os_str().is_empty() { + if let Ok(local_refs) = + pack::extract_bundle_refs(&repo.bundle.to_string_lossy()) + { + let refs: Vec = local_refs + .iter() + .map(|(ref_name, commit)| { + json!({ + "name": ref_name, + "commit": commit + }) + }) + .collect(); + repo_info["refs"] = Value::Array(refs); + } + } + + repo_info + }) + .collect(); + + Ok(json!({ + "status": "success", + "repositories": repo_list, + "count": repo_list.len() + })) + } + Err(e) => Ok(json!({ + "status": "error", + "error": e.to_string() + })), + } + } + + /// Get details of a specific repository + async fn get_repo_details(repo_id: &str) -> Result { + match storage::repo_model::load_repo_from_db(repo_id).await { + Ok(Some(repo)) => { + let mut repo_info = json!({ + "repo_id": repo.repo_id, + "name": repo.p2p_description.name, + "creator": repo.p2p_description.creator, + "description": repo.p2p_description.description, + "path": repo.path.display().to_string(), + "bundle": repo.bundle.display().to_string(), + "timestamp": repo.p2p_description.timestamp, + }); + + // Check for updates if this is a local repo + if !repo.path.as_os_str().is_empty() && repo.path.exists() { + if let Ok(current_refs) = + crate::git::git_repo::read_repo_refs(repo.path.to_str().unwrap_or("")) + { + if let Ok(local_refs) = + pack::extract_bundle_refs(&repo.bundle.to_string_lossy()) + { + repo_info["has_updates"] = Value::Bool(current_refs != local_refs); + + let local_ref_list: Vec = local_refs + .iter() + .map(|(ref_name, commit)| { + json!({ + "name": ref_name, + "commit": commit + }) + }) + .collect(); + repo_info["local_refs"] = Value::Array(local_ref_list); + + let current_ref_list: Vec = current_refs + .iter() + .map(|(ref_name, commit)| { + json!({ + "name": ref_name, + "commit": commit + }) + }) + .collect(); + repo_info["current_refs"] = Value::Array(current_ref_list); + } + } + } + + Ok(json!({ + "status": "success", + "repository": repo_info + })) + } + Ok(None) => Ok(json!({ + "status": "error", + "error": format!("Repository {} not found", repo_id) + })), + Err(e) => Ok(json!({ + "status": "error", + "error": e.to_string() + })), + } + } +} + +pub async fn start_mcp_server() -> Result<()> { + let stdin = io::stdin(); + let stdout = io::stdout(); + let mut reader = BufReader::new(stdin.lock()); + let mut writer = BufWriter::new(stdout.lock()); + + tracing::info!("MCP Repository Server started"); + + // Send initialization message + let init_response = json!({ + "version": "1.0", + "name": "megaengine-repo-mcp", + "capabilities": { + "tools": {} + } + }); + writeln!(writer, "{}", init_response.to_string())?; + writer.flush()?; + + // Main request loop + let mut line = String::new(); + while reader.read_line(&mut line)? > 0 { + if let Err(e) = handle_mcp_request(&line, &mut writer).await { + tracing::error!("Error handling MCP request: {}", e); + let error_response = json!({ + "error": e.to_string() + }); + writeln!(writer, "{}", error_response.to_string())?; + writer.flush()?; + } + line.clear(); + } + + Ok(()) +} + +async fn handle_mcp_request( + request: &str, + writer: &mut BufWriter>, +) -> Result<()> { + use anyhow::anyhow; + + let request_data: Value = serde_json::from_str(request)?; + + match request_data.get("method").and_then(|v| v.as_str()) { + Some("tools/list") => { + let tools = RepoMcpServer::get_tools(); + let response = json!({ + "tools": tools + }); + writeln!(writer, "{}", response.to_string())?; + writer.flush()?; + } + Some("tools/call") => { + let tool_name = request_data + .get("params") + .and_then(|p| p.get("name")) + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow!("Missing tool name"))?; + + let tool_args = request_data + .get("params") + .and_then(|p| p.get("arguments")) + .cloned() + .unwrap_or_else(|| json!({})); + + let result = RepoMcpServer::execute_tool(tool_name, tool_args).await?; + + let response = json!({ + "result": result + }); + writeln!(writer, "{}", response.to_string())?; + writer.flush()?; + } + Some(method) => { + return Err(anyhow!("Unknown method: {}", method)); + } + None => { + return Err(anyhow!("Missing method field")); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_tools() { + let tools = RepoMcpServer::get_tools(); + assert_eq!(tools.len(), 2); + } +} diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs new file mode 100644 index 0000000..25385ff --- /dev/null +++ b/src/mcp/mod.rs @@ -0,0 +1,5 @@ +pub mod mcp_server; +pub mod sse_server; + +pub use mcp_server::start_mcp_server; +pub use sse_server::start_sse_server; diff --git a/src/mcp/sse_server.rs b/src/mcp/sse_server.rs new file mode 100644 index 0000000..6f81b19 --- /dev/null +++ b/src/mcp/sse_server.rs @@ -0,0 +1,208 @@ +use crate::mcp::mcp_server::RepoMcpServer; +use axum::{ + extract::{Query, State}, + response::{ + sse::{Event, Sse}, + IntoResponse, + }, + routing::{get, post}, + Json, Router, +}; +use futures::stream::Stream; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; +use tokio::sync::{mpsc, RwLock}; +use tokio_stream::wrappers::UnboundedReceiverStream; +use tower_http::cors::CorsLayer; +use uuid::Uuid; + +// App state to hold active sessions +struct AppState { + sessions: RwLock>>>, +} + +#[derive(Deserialize)] +struct SessionParam { + session_id: String, +} + +pub async fn start_sse_server(addr: SocketAddr) -> anyhow::Result<()> { + let state = Arc::new(AppState { + sessions: RwLock::new(HashMap::new()), + }); + + let app = Router::new() + .route("/sse", get(sse_handler)) + .route("/messages", post(message_handler)) + .layer(CorsLayer::permissive()) + .with_state(state); + + tracing::info!("MCP SSE Server listening on {}", addr); + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await?; + + Ok(()) +} + +async fn sse_handler( + State(state): State>, +) -> Sse>> { + let session_id = Uuid::new_v4().to_string(); + let (tx, rx) = mpsc::unbounded_channel(); + + // Store the sender + state + .sessions + .write() + .await + .insert(session_id.clone(), tx.clone()); + + let stream = UnboundedReceiverStream::new(rx); + + // Send the endpoint event immediately + let endpoint_url = format!("/messages?session_id={}", session_id); + let _ = tx.send(Ok(Event::default().event("endpoint").data(endpoint_url))); + + tracing::info!("New SSE session connected: {}", session_id); + + Sse::new(stream) + .keep_alive(axum::response::sse::KeepAlive::new().interval(Duration::from_secs(15))) +} + +async fn message_handler( + State(state): State>, + Query(params): Query, + Json(request): Json, +) -> impl IntoResponse { + let session_id = params.session_id; + + let tx = { + let sessions = state.sessions.read().await; + sessions.get(&session_id).cloned() + }; + + if let Some(tx) = tx { + // Handle the MCP request (JSON-RPC) + // We spawn a task to process it so we don't block + tokio::spawn(async move { + if let Some(method) = request.get("method").and_then(|v| v.as_str()) { + let response = match method { + "initialize" => Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "result": { + "protocolVersion": "2024-11-05", + "capabilities": { + "tools": {} + }, + "serverInfo": { + "name": "megaengine", + "version": "0.1.0" + } + } + })), + "notifications/initialized" => { + // Client initialized, no response needed for notification + None + } + "ping" => Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "result": {} + })), + "tools/list" => { + let tools = RepoMcpServer::get_tools(); + Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "result": { + "tools": tools + } + })) + } + "tools/call" => { + if let Some(params) = request.get("params") { + let name = params.get("name").and_then(|v| v.as_str()); + let args = params.get("arguments").cloned().unwrap_or(json!({})); + + if let Some(name) = name { + match RepoMcpServer::execute_tool(name, args).await { + Ok(result_value) => { + // Format result as MCP CallToolResult with text content + let content_text = result_value.to_string(); + Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "result": { + "content": [{ + "type": "text", + "text": content_text + }], + "isError": false + } + })) + }, + Err(e) => Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "result": { + "content": [{ + "type": "text", + "text": e.to_string() + }], + "isError": true + } + })), + } + } else { + Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "error": { + "code": -32602, + "message": "Missing 'name' in params" + } + })) + } + } else { + Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "error": { + "code": -32602, + "message": "Missing 'params'" + } + })) + } + } + // Handle other JSON-RPC methods or notifications if needed + _ => None, + }; + + if let Some(response) = response { + if let Ok(data) = serde_json::to_string(&response) { + let _ = tx.send(Ok(Event::default().event("message").data(data))); + } + } + } else { + tracing::warn!("Received invalid JSON-RPC request: missing method"); + let error_response = json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "error": { + "code": -32600, + "message": "Invalid Request: Method missing" + } + }); + if let Ok(data) = serde_json::to_string(&error_response) { + let _ = tx.send(Ok(Event::default().event("message").data(data))); + } + } + }); + + axum::http::StatusCode::ACCEPTED + } else { + axum::http::StatusCode::NOT_FOUND + } +} From f83fcb20b923b07a474fefe6f936438c0b5b8598 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Thu, 29 Jan 2026 13:59:43 +0800 Subject: [PATCH 27/42] remove node_manager --- .gitignore | 3 +- src/cli/repo.rs | 10 +- src/gossip/message.rs | 1 - src/mcp/mcp_server.rs | 321 +++++++++++++++++++++++++++------------ src/node/mod.rs | 1 - src/node/node_manager.rs | 148 ------------------ src/transport/quic.rs | 1 - 7 files changed, 226 insertions(+), 259 deletions(-) delete mode 100644 src/node/node_manager.rs diff --git a/.gitignore b/.gitignore index 65e7997..a8bf005 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ target cert/ .megaengine/ -tmp/* \ No newline at end of file +tmp/* +dist/ \ No newline at end of file diff --git a/src/cli/repo.rs b/src/cli/repo.rs index 99efdc0..bde8f99 100644 --- a/src/cli/repo.rs +++ b/src/cli/repo.rs @@ -113,7 +113,9 @@ async fn print_repo_info(repo: &Repo) { // Check for updates if this is a local repo if !repo.path.as_os_str().is_empty() && repo.path.exists() { - match megaengine::git::git_repo::read_repo_refs(repo.path.to_str().unwrap_or("")) { + match megaengine::git::git_repo::read_repo_refs( + repo.path.to_str().unwrap_or(""), + ) { Ok(current_refs) => { // Compare current refs with local refs if current_refs != local_refs { @@ -198,11 +200,7 @@ pub async fn handle_repo_pull(repo_id: String) -> Result<()> { } }; - let result = pull_repo_from_bundle( - path_str, - bundle_str, - "master", - ); + let result = pull_repo_from_bundle(path_str, bundle_str, "master"); match result { Ok(()) => { diff --git a/src/gossip/message.rs b/src/gossip/message.rs index 17930f1..cde500d 100644 --- a/src/gossip/message.rs +++ b/src/gossip/message.rs @@ -60,7 +60,6 @@ pub struct SignedMessage { pub signature: String, } -#[allow(dead_code)] impl SignedMessage { pub fn new_node_sign_message(node: Node) -> Result { let message = GossipMessage::NodeAnnouncement(node.clone().into()); diff --git a/src/mcp/mcp_server.rs b/src/mcp/mcp_server.rs index 34b2e2d..41c49b8 100644 --- a/src/mcp/mcp_server.rs +++ b/src/mcp/mcp_server.rs @@ -1,14 +1,41 @@ -use crate::git::pack; -use crate::storage; +use crate::{git::pack, storage}; use anyhow::Result; +use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use std::io::{self, BufRead, BufReader, BufWriter, Write}; +use std::io::{self, BufRead, BufReader, Write}; + +// --- 1. 定义符合 JSON-RPC 2.0 标准的结构 --- + +#[derive(Deserialize, Debug)] +struct JsonRpcRequest { + jsonrpc: String, + method: String, + params: Option, + id: Option, +} + +#[derive(Serialize, Debug)] +struct JsonRpcResponse { + jsonrpc: String, + #[serde(skip_serializing_if = "Option::is_none")] + result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, + id: Option, +} + +#[derive(Serialize, Debug)] +struct JsonRpcError { + code: i32, + message: String, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option, +} /// MCP Server implementation for repository operations pub struct RepoMcpServer; impl RepoMcpServer { - /// Get list of available tools pub fn get_tools() -> Vec { vec![ json!({ @@ -34,10 +61,27 @@ impl RepoMcpServer { "required": ["repo_id"] } }), + json!({ + "name": "clone_repo", + "description": "Clone a repository from its bundle to a local directory", + "inputSchema": { + "type": "object", + "properties": { + "repo_id": { + "type": "string", + "description": "The ID of the repository to clone" + }, + "output_path": { + "type": "string", + "description": "The local path where the repository should be cloned" + } + }, + "required": ["repo_id", "output_path"] + } + }), ] } - /// Execute a tool with the given arguments pub async fn execute_tool(name: &str, args: Value) -> Result { match name { "list_repos" => Self::list_repos().await, @@ -48,11 +92,21 @@ impl RepoMcpServer { .ok_or_else(|| anyhow::anyhow!("Missing repo_id parameter"))?; Self::get_repo_details(repo_id).await } + "clone_repo" => { + let repo_id = args + .get("repo_id") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing repo_id parameter"))?; + let output_path = args + .get("output_path") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing output_path parameter"))?; + Self::clone_repo(repo_id, output_path).await + } _ => Err(anyhow::anyhow!("Unknown tool: {}", name)), } } - /// List all repositories async fn list_repos() -> Result { match storage::repo_model::list_repos().await { Ok(repos) => { @@ -69,7 +123,7 @@ impl RepoMcpServer { "timestamp": repo.p2p_description.timestamp, }); - // Extract refs information + // 恢复 refs 处理逻辑 if !repo.bundle.as_os_str().is_empty() { if let Ok(local_refs) = pack::extract_bundle_refs(&repo.bundle.to_string_lossy()) @@ -90,21 +144,17 @@ impl RepoMcpServer { repo_info }) .collect(); - Ok(json!({ - "status": "success", - "repositories": repo_list, - "count": repo_list.len() + "content": [{ + "type": "text", + "text": serde_json::to_string(&repo_list)? + }] })) } - Err(e) => Ok(json!({ - "status": "error", - "error": e.to_string() - })), + Err(e) => Err(e), } } - /// Get details of a specific repository async fn get_repo_details(repo_id: &str) -> Result { match storage::repo_model::load_repo_from_db(repo_id).await { Ok(Some(repo)) => { @@ -152,116 +202,185 @@ impl RepoMcpServer { } } } + Ok(json!({ + "content": [{ + "type": "text", + "text": serde_json::to_string_pretty(&repo_info)? + }] + })) + } + Ok(None) => Err(anyhow::anyhow!("Repository not found")), + Err(e) => Err(e), + } + } + + async fn clone_repo(repo_id: &str, output: &str) -> Result { + use std::path::PathBuf; + match storage::repo_model::load_repo_from_db(repo_id).await { + Ok(Some(mut repo)) => { + let bundle_path = repo.bundle.to_string_lossy().to_string(); + if bundle_path.is_empty() || !std::path::Path::new(&bundle_path).exists() { + return Err(anyhow::anyhow!("Bundle file not found for repository")); + } + + pack::restore_repo_from_bundle(&bundle_path, output).await?; + + // Read and save refs from the cloned repository + if let Ok(refs) = crate::git::git_repo::read_repo_refs(output) { + let _ = storage::ref_model::batch_save_refs(repo_id, &refs).await; + } + + // Update repo path to the cloned location + repo.path = PathBuf::from(output); + let _ = storage::repo_model::save_repo_to_db(&repo).await; Ok(json!({ - "status": "success", - "repository": repo_info + "content": [{ + "type": "text", + "text": format!("Successfully cloned repository {} to {}", repo_id, output) + }] })) } - Ok(None) => Ok(json!({ - "status": "error", - "error": format!("Repository {} not found", repo_id) - })), - Err(e) => Ok(json!({ - "status": "error", - "error": e.to_string() - })), + Ok(None) => Err(anyhow::anyhow!("Repository not found")), + Err(e) => Err(e), } } } pub async fn start_mcp_server() -> Result<()> { + eprintln!("MCP Repository Server started"); + let stdin = io::stdin(); - let stdout = io::stdout(); + let mut stdout = io::stdout(); let mut reader = BufReader::new(stdin.lock()); - let mut writer = BufWriter::new(stdout.lock()); - tracing::info!("MCP Repository Server started"); - - // Send initialization message - let init_response = json!({ - "version": "1.0", - "name": "megaengine-repo-mcp", - "capabilities": { - "tools": {} - } - }); - writeln!(writer, "{}", init_response.to_string())?; - writer.flush()?; - - // Main request loop let mut line = String::new(); while reader.read_line(&mut line)? > 0 { - if let Err(e) = handle_mcp_request(&line, &mut writer).await { - tracing::error!("Error handling MCP request: {}", e); - let error_response = json!({ - "error": e.to_string() - }); - writeln!(writer, "{}", error_response.to_string())?; - writer.flush()?; + if line.trim().is_empty() { + line.clear(); + continue; } + + // 1. 解析请求 + let req: JsonRpcRequest = match serde_json::from_str(&line) { + Ok(r) => r, + Err(e) => { + eprintln!("Failed to parse JSON: {}", e); + line.clear(); + continue; + } + }; + + eprintln!("Received method: {}", req.method); + + // 2. 处理请求并获取 Result 或 Error + // 注意:这里返回元组 (Option, Option) + let (result, error) = match req.method.as_str() { + // A. 初始化握手 (必须响应 initialize) + "initialize" => ( + Some(json!({ + "protocolVersion": "2024-11-05", + "capabilities": { "tools": {} }, + "serverInfo": { + "name": "megaengine-repo-mcp", + "version": "1.0" + } + })), + None, + ), + + // B. 初始化完成通知 (不需要响应) + "notifications/initialized" => { + eprintln!("Client initialized."); + line.clear(); + continue; + } + + // C. 列出工具 + "tools/list" => { + let tools = RepoMcpServer::get_tools(); + (Some(json!({ "tools": tools })), None) + } + + // D. 调用工具 + "tools/call" => handle_tool_call(&req.params).await, + + // E. 心跳 + "ping" => (Some(json!({})), None), + + // F. 未知方法 + _ => ( + None, + Some(JsonRpcError { + code: -32601, + message: format!("Method not found: {}", req.method), + data: None, + }), + ), + }; + + // 3. 构建并发送响应 + if let Some(req_id) = req.id { + let resp = JsonRpcResponse { + jsonrpc: "2.0".to_string(), + result, + error, + id: Some(req_id), + }; + + let resp_str = serde_json::to_string(&resp)?; + writeln!(stdout, "{}", resp_str)?; + stdout.flush()?; + } + line.clear(); } Ok(()) } -async fn handle_mcp_request( - request: &str, - writer: &mut BufWriter>, -) -> Result<()> { - use anyhow::anyhow; - - let request_data: Value = serde_json::from_str(request)?; - - match request_data.get("method").and_then(|v| v.as_str()) { - Some("tools/list") => { - let tools = RepoMcpServer::get_tools(); - let response = json!({ - "tools": tools - }); - writeln!(writer, "{}", response.to_string())?; - writer.flush()?; - } - Some("tools/call") => { - let tool_name = request_data - .get("params") - .and_then(|p| p.get("name")) - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow!("Missing tool name"))?; - - let tool_args = request_data - .get("params") - .and_then(|p| p.get("arguments")) - .cloned() - .unwrap_or_else(|| json!({})); - - let result = RepoMcpServer::execute_tool(tool_name, tool_args).await?; - - let response = json!({ - "result": result - }); - writeln!(writer, "{}", response.to_string())?; - writer.flush()?; - } - Some(method) => { - return Err(anyhow!("Unknown method: {}", method)); - } +// 辅助函数:处理工具调用 +async fn handle_tool_call(params: &Option) -> (Option, Option) { + let params = match params { + Some(p) => p, None => { - return Err(anyhow!("Missing method field")); + return ( + None, + Some(JsonRpcError { + code: -32602, + message: "Missing params".into(), + data: None, + }), + ) } - } + }; - Ok(()) -} + let name = match params.get("name").and_then(|n| n.as_str()) { + Some(n) => n, + None => { + return ( + None, + Some(JsonRpcError { + code: -32602, + message: "Missing tool name".into(), + data: None, + }), + ) + } + }; -#[cfg(test)] -mod tests { - use super::*; + let args = params.get("arguments").cloned().unwrap_or(json!({})); - #[test] - fn test_get_tools() { - let tools = RepoMcpServer::get_tools(); - assert_eq!(tools.len(), 2); + // 调用业务逻辑 + match RepoMcpServer::execute_tool(name, args).await { + Ok(res) => (Some(res), None), // 这里的 res 必须符合 CallToolResult 结构 + Err(e) => ( + None, + Some(JsonRpcError { + code: -32000, // 应用级错误 + message: e.to_string(), + data: None, + }), + ), } } diff --git a/src/node/mod.rs b/src/node/mod.rs index fd5c9b9..295c983 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -2,4 +2,3 @@ pub mod node; pub mod node_addr; pub mod node_id; -pub mod node_manager; diff --git a/src/node/node_manager.rs b/src/node/node_manager.rs deleted file mode 100644 index 65ee83f..0000000 --- a/src/node/node_manager.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::node::{ - node::{Node, NodeRouting}, - node_id::NodeId, -}; -use std::collections::HashMap; - -#[derive(Default)] -pub struct NodeManager { - pub nodes: HashMap, -} - -impl NodeManager { - pub fn new() -> Self { - Self { - nodes: HashMap::new(), - } - } - - pub async fn insert_node(&mut self, node: &Node) { - let routing = NodeRouting::new(node.node_id().clone(), node.addresses().to_vec()); - self.nodes.insert(node.node_id().clone(), routing); - - // 持久化 NodeInfo - let _ = crate::storage::node_model::save_node_info_to_db(&node.info).await; - } - - pub fn mark_alive(&mut self, node_id: &NodeId) { - if let Some(n) = self.nodes.get_mut(node_id) { - n.refresh(); - } - } - - pub fn cleanup_expired(&mut self) { - self.nodes.retain(|_, v| !v.expired()); - } - - pub fn get_node(&self, node_id: &NodeId) -> Option<&NodeRouting> { - self.nodes.get(node_id) - } - - pub fn routing_print(&self) { - println!("Node routing table ({} entries):", self.nodes.len()); - for (id, info) in &self.nodes { - println!(" {:?} -> {:?}", id, info.addresses); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::identity::keypair::KeyPair; - use crate::node::node::NodeType; - use std::net::{IpAddr, Ipv4Addr, SocketAddr}; - - fn create_sample_node() -> Node { - let keypair = &KeyPair::generate().unwrap(); - let node_id = NodeId::from_keypair(keypair); - let alias = "Test Node"; - let addresses = vec![SocketAddr::new( - IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - 8080, - )]; - let node_type = NodeType::Normal; - - Node::new(node_id, alias, addresses, node_type, keypair.clone()) - } - - #[tokio::test] - async fn test_node_manager_insert_node() { - let mut manager = NodeManager::new(); - let node = create_sample_node(); - let node_id = node.node_id().clone(); - - // Insert the node - manager.insert_node(&node).await; - - // Assert that the node is in the manager - assert_eq!(manager.nodes.len(), 1); - let node_routing = manager.get_node(&node.node_id()); - assert!(node_routing.is_some()); - assert_eq!(node_routing.unwrap().node_id, *node.node_id()); - - // Cleanup: Remove from database - let _ = crate::storage::node_model::delete_node_from_db(&node_id.to_string()).await; - } - - #[tokio::test] - async fn test_node_manager_mark_alive() { - let mut manager = NodeManager::new(); - let node = create_sample_node(); - let node_id = node.node_id().clone(); - - // Insert the node - manager.insert_node(&node).await; - - // Get the initial last_seen time - let initial_last_seen = manager.get_node(&node.node_id()).unwrap().last_seen; - - // Mark the node as alive (refresh) - manager.mark_alive(&node.node_id()); - - // Assert that the last_seen time was refreshed - let refreshed_last_seen = manager.get_node(&node.node_id()).unwrap().last_seen; - assert_ne!(initial_last_seen, refreshed_last_seen); - - // Cleanup: Remove from database - let _ = crate::storage::node_model::delete_node_from_db(&node_id.to_string()).await; - } - - #[tokio::test] - async fn test_node_manager_cleanup_expired() { - let mut manager = NodeManager::new(); - let node = create_sample_node(); - let node_id = node.node_id().clone(); - - manager.insert_node(&node).await; - assert_eq!(manager.nodes.len(), 1); - - manager.nodes.get_mut(&node.node_id()).unwrap().ttl = std::time::Duration::from_secs(1); - std::thread::sleep(std::time::Duration::from_secs(2)); - manager.cleanup_expired(); - assert_eq!(manager.nodes.len(), 0); - - // Cleanup: Remove from database - let _ = crate::storage::node_model::delete_node_from_db(&node_id.to_string()).await; - } - - #[tokio::test] - async fn test_node_manager_routing_print() { - let mut manager = NodeManager::new(); - let node1 = create_sample_node(); - let node2 = create_sample_node(); - let node_id_1 = node1.node_id().clone(); - let node_id_2 = node2.node_id().clone(); - - manager.insert_node(&node1).await; - manager.insert_node(&node2).await; - - let _ = std::panic::catch_unwind(|| { - manager.routing_print(); - }); - - // Cleanup: Remove from database - let _ = crate::storage::node_model::delete_node_from_db(&node_id_1.to_string()).await; - let _ = crate::storage::node_model::delete_node_from_db(&node_id_2.to_string()).await; - } -} diff --git a/src/transport/quic.rs b/src/transport/quic.rs index 5694752..7c05059 100644 --- a/src/transport/quic.rs +++ b/src/transport/quic.rs @@ -31,7 +31,6 @@ pub struct ConnectionManager { endpoint: Arc, connection_tx: mpsc::Sender, connections: Arc>>>, - // 区分 Gossip 消息(控制流)和数据传输流 gossip_sender: GossipMessageSender, data_sender: DataMessageSender, } From 78c0e81a01514fa911200b3bf040ee8106591d60 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Thu, 29 Jan 2026 14:56:59 +0800 Subject: [PATCH 28/42] =?UTF-8?q?=E4=BC=98=E5=8C=96bundle=E4=BC=A0?= =?UTF-8?q?=E8=BE=93=E5=92=8C=E5=85=8B=E9=9A=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bundle/transfer.rs | 39 ++++++++-- src/cli/repo.rs | 170 +++++++++++++++++++---------------------- src/git/git_repo.rs | 10 ++- src/git/pack.rs | 23 +++++- src/gossip/message.rs | 4 +- src/gossip/service.rs | 6 +- src/main.rs | 3 +- 7 files changed, 148 insertions(+), 107 deletions(-) diff --git a/src/bundle/transfer.rs b/src/bundle/transfer.rs index 026af9b..6728419 100644 --- a/src/bundle/transfer.rs +++ b/src/bundle/transfer.rs @@ -5,12 +5,16 @@ use crate::util::get_node_id_last_part; use crate::util::get_repo_id_last_part; use anyhow::Context; use anyhow::Result; +use std::io::SeekFrom; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::fs; +use tokio::io::{AsyncSeekExt, AsyncWriteExt}; use tokio::sync::Mutex; use tracing::{debug, info, warn}; +const TRANSFER_CHUNK_SIZE: usize = 64 * 1024; // 64KB per chunk + /// Bundle 消息类型(用于多帧传输) #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum BundleMessageType { @@ -98,8 +102,7 @@ impl BundleTransferManager { .context("Failed to send START message")?; // 2. 分块发送数据 - const CHUNK_SIZE: usize = 64 * 1024; // 64KB per chunk - for (chunk_idx, chunk) in bundle_data.chunks(CHUNK_SIZE).enumerate() { + for (chunk_idx, chunk) in bundle_data.chunks(TRANSFER_CHUNK_SIZE).enumerate() { let chunk_msg = BundleMessageType::Chunk { repo_id: repo_id.clone(), chunk_idx: chunk_idx as u32, @@ -133,7 +136,7 @@ impl BundleTransferManager { "Bundle {} sent successfully to node {} ({} chunks)", file_name, target_node_id, - bundle_data.chunks(CHUNK_SIZE).count() + bundle_data.chunks(TRANSFER_CHUNK_SIZE).count() ); Ok(()) @@ -261,6 +264,14 @@ impl BundleTransferManager { .await .context("Failed to create bundle storage directory")?; + // 确保文件从头开始:如果存在则清空,如果不存在则创建 + let encoded_repo_id = get_repo_id_last_part(repo_id); + let file_path = dir.join(format!("{}.bundle", encoded_repo_id)); + + let _ = fs::File::create(&file_path) + .await + .context("Failed to create/truncate bundle file")?; + info!( "Bundle transfer START from {}: repo={}, file={}, size={} bytes", from, repo_id, file_name, total_size @@ -282,22 +293,34 @@ impl BundleTransferManager { let encoded_repo_id = get_repo_id_last_part(repo_id); let file_path = dir.join(format!("{}.bundle", encoded_repo_id)); - // 追加写入到文件 + // 如果文件不存在(可能是 Start 消息丢失),先创建 + if !file_path.exists() { + if let Some(parent) = file_path.parent() { + fs::create_dir_all(parent).await?; + } + } + + // 使用 Write 模式打开,不追加,而是使用 Seek let mut file = fs::OpenOptions::new() .create(true) - .append(true) + .write(true) .open(&file_path) .await - .context("Failed to open bundle file for appending")?; + .context("Failed to open bundle file")?; + + let offset = (chunk_idx as u64) * (TRANSFER_CHUNK_SIZE as u64); + file.seek(SeekFrom::Start(offset)) + .await + .context("Failed to seek to chunk position")?; - use tokio::io::AsyncWriteExt; file.write_all(&data) .await .context("Failed to write chunk data")?; info!( - "Received chunk {} ({} bytes) for repo {} from {}", + "Received chunk {} (offset {}) ({} bytes) for repo {} from {}", chunk_idx, + offset, data.len(), repo_id, from diff --git a/src/cli/repo.rs b/src/cli/repo.rs index bde8f99..7e0ddea 100644 --- a/src/cli/repo.rs +++ b/src/cli/repo.rs @@ -60,8 +60,16 @@ pub async fn handle_repo_add(path: String, description: String) -> Result<()> { let mut manager = repo::repo_manager::RepoManager::new(); match manager.register_repo(repo_obj).await { - Ok(_) => tracing::info!("Repo {} added", repo_id), - Err(e) => tracing::info!("Failed to add repo: {}", e), + Ok(_) => { + tracing::info!("Repo {} added", repo_id); + println!("✅ Repository added successfully!"); + println!(" ID: {}", repo_id); + println!(" Name: {}", name); + } + Err(e) => { + tracing::error!("Failed to add repo: {}", e); + eprintln!("❌ Failed to add repository: {}", e); + } } Ok(()) } @@ -70,10 +78,10 @@ pub async fn handle_repo_list() -> Result<()> { match storage::repo_model::list_repos().await { Ok(repos) => { if repos.is_empty() { - println!("No repositories found"); + println!("No repositories found."); } else { - println!("Repositories:"); - println!("{}", "─".repeat(120)); + println!("Found {} repositories:", repos.len()); + println!("{}", "─".repeat(60)); for repo in repos { print_repo_info(&repo).await; } @@ -81,105 +89,99 @@ pub async fn handle_repo_list() -> Result<()> { } Err(e) => { tracing::error!("Failed to list repos: {}", e); - println!("Failed to list repositories: {}", e); + eprintln!("❌ Failed to list repositories: {}", e); } } Ok(()) } async fn print_repo_info(repo: &Repo) { - println!(" ID: {}", repo.repo_id); - println!(" Name: {}", repo.p2p_description.name); - println!(" Creator: {}", repo.p2p_description.creator); - println!(" Description: {}", repo.p2p_description.description); - println!(" Path: {}", repo.path.display()); - println!(" Bundle: {}", repo.bundle.display()); - println!(" Timestamp: {}", repo.p2p_description.timestamp); + println!("📦 Repo: {}", repo.p2p_description.name); + println!(" ID: {}", repo.repo_id); + println!(" Creator: {}", repo.p2p_description.creator); + if !repo.p2p_description.description.is_empty() { + println!(" Description: {}", repo.p2p_description.description); + } + println!(" Path: {}", repo.path.display()); + // Bundle path only shown if it exists, to reduce clutter + if !repo.bundle.as_os_str().is_empty() { + println!(" Bundle: {}", repo.bundle.display()); + } + // Status check logic... if repo.bundle.as_os_str().is_empty() { - // No bundle path configured; avoid calling extract_bundle_refs on an empty path. - println!(" Refs: (bundle path not set)"); + println!(" Refs: (bundle not set)"); } else { match megaengine::git::pack::extract_bundle_refs(&repo.bundle.to_string_lossy()) { Ok(local_refs) => { - if local_refs.is_empty() { - println!(" Refs: (none)"); - } else { - println!(" Refs: ({} total)", local_refs.len()); - for (ref_name, commit) in &local_refs { - println!(" - {}: {}", ref_name, commit); - } - } + let ref_count = local_refs.len(); + + // Check if up-to-date with local path + let mut status_msg = "✅ Synced".to_string(); + let mut updates = Vec::new(); - // Check for updates if this is a local repo if !repo.path.as_os_str().is_empty() && repo.path.exists() { match megaengine::git::git_repo::read_repo_refs( repo.path.to_str().unwrap_or(""), ) { Ok(current_refs) => { - // Compare current refs with local refs if current_refs != local_refs { - println!(" Status: ⚠️ HAS UPDATES"); - println!(" Updated Refs: ({} total)", current_refs.len()); + status_msg = "⚠️ Out of Sync".to_string(); for (ref_name, commit) in ¤t_refs { - let local_commit = local_refs.get(ref_name); - if local_commit != Some(commit) { - let indicator = if local_commit.is_none() { - "NEW" - } else { - "CHANGED" - }; - println!(" - {} {} : {}", indicator, ref_name, commit); + if local_refs.get(ref_name) != Some(commit) { + updates.push(format!("{} -> {}", ref_name, &commit[0..7])); } } - } else { - println!(" Status: ✅ Up-to-date"); } } - Err(e) => { - tracing::warn!("Failed to check for updates: {}", e); - println!(" Status: (failed to check: {})", e); - } + Err(_) => status_msg = "❓ Unknown (Check Failed)".to_string(), + } + } + + println!(" Refs: {} branches/tags", ref_count); + println!(" Status: {}", status_msg); + + if !updates.is_empty() { + println!(" Updates: {} pending changes", updates.len()); + for update in updates.iter().take(3) { + println!(" - {}", update); + } + if updates.len() > 3 { + println!(" - ... and {} more", updates.len() - 3); } } } - Err(e) => { - println!(" Refs: (failed to load: {})", e); - } + Err(_) => println!(" Refs: (error loading bundle)"), } } - println!("{}", "─".repeat(120)); + println!("{}", "─".repeat(60)); } pub async fn handle_repo_pull(repo_id: String) -> Result<()> { + println!("🔄 Pulling repository {}...", repo_id); match storage::repo_model::load_repo_from_db(&repo_id).await { Ok(Some(repo)) => { // Check if repo has a local path if repo.path.as_os_str().is_empty() { tracing::error!("Repository {} has no local path", repo_id); - println!("Error: Repository {} has no local path", repo_id); + eprintln!( + "❌ Error: Repository {} has no local path configured.", + repo_id + ); return Ok(()); } // Check if bundle exists if repo.bundle.as_os_str().is_empty() { tracing::error!("Repository {} has no bundle available", repo_id); - println!("Error: Repository {} has no bundle available", repo_id); + eprintln!("❌ Error: Repository {} has no bundle available.", repo_id); return Ok(()); } let path_str = match repo.path.as_os_str().to_str() { Some(s) => s, None => { - tracing::error!( - "Repository {} has a local path that is not valid UTF-8: {}", - repo_id, - repo.path.display() - ); - println!( - "Error: Repository {} has a local path that is not valid UTF-8", - repo_id - ); + eprintln!("❌ Error: Local path is not valid UTF-8."); return Ok(()); } }; @@ -187,15 +189,7 @@ pub async fn handle_repo_pull(repo_id: String) -> Result<()> { let bundle_str = match repo.bundle.as_os_str().to_str() { Some(s) => s, None => { - tracing::error!( - "Repository {} has a bundle path that is not valid UTF-8: {}", - repo_id, - repo.bundle.display() - ); - println!( - "Error: Repository {} has a bundle path that is not valid UTF-8", - repo_id - ); + eprintln!("❌ Error: Bundle path is not valid UTF-8."); return Ok(()); } }; @@ -206,41 +200,42 @@ pub async fn handle_repo_pull(repo_id: String) -> Result<()> { Ok(()) => { tracing::info!("Repository {} fetched successfully from bundle", repo_id); println!("✅ Repository updated successfully!"); - println!(" Repository: {}", repo.p2p_description.name); - println!(" Path: {}", repo.path.display()); + println!(" Name: {}", repo.p2p_description.name); + println!(" Path: {}", repo.path.display()); } Err(e) => { tracing::error!("Failed to spawn fetch task: {}", e); - println!("Error: Failed to spawn fetch task: {}", e); + eprintln!("❌ Failed to update repository: {}", e); } } } Ok(None) => { tracing::error!("Repository {} not found in database", repo_id); - println!("Error: Repository {} not found", repo_id); + eprintln!("❌ Error: Repository {} not found.", repo_id); } Err(e) => { tracing::error!("Failed to query repository {}: {}", repo_id, e); - println!("Error: Failed to query repository: {}", e); + eprintln!("❌ Database error: {}", e); } } Ok(()) } pub async fn handle_repo_clone(output: String, repo_id: String) -> Result<()> { + println!("📥 Cloning repository {}...", repo_id); match storage::repo_model::load_repo_from_db(&repo_id).await { Ok(Some(mut repo)) => { // Check if bundle exists if repo.bundle.as_os_str().is_empty() || repo.bundle.to_string_lossy().is_empty() { tracing::error!("Repository {} has no bundle available for cloning", repo_id); - println!("Error: Repository {} has no bundle available", repo_id); + eprintln!("❌ Error: Repository {} has no bundle available.", repo_id); return Ok(()); } let bundle_path = repo.bundle.to_string_lossy().to_string(); if !std::path::Path::new(&bundle_path).exists() { tracing::error!("Bundle file not found at path: {}", bundle_path); - println!("Error: Bundle file not found at {}", bundle_path); + eprintln!("❌ Error: Bundle file not found at {}", bundle_path); return Ok(()); } @@ -254,10 +249,11 @@ pub async fn handle_repo_clone(output: String, repo_id: String) -> Result<()> { match restore_repo_from_bundle(&bundle_path, &output).await { Ok(_) => { tracing::info!("Repository {} cloned successfully to {}", repo_id, output); - println!("✅ Repository cloned successfully to {}", output); - println!(" Repository: {}", repo.p2p_description.name); - println!(" Creator: {}", repo.p2p_description.creator); - println!(" Description: {}", repo.p2p_description.description); + println!("✅ Repository cloned successfully!"); + println!(" Name: {}", repo.p2p_description.name); + println!(" Creator: {}", repo.p2p_description.creator); + println!(" Description: {}", repo.p2p_description.description); + println!(" Path: {}", output); // Read and save refs from the cloned repository match megaengine::git::git_repo::read_repo_refs(&output) { @@ -266,11 +262,10 @@ pub async fn handle_repo_clone(output: String, repo_id: String) -> Result<()> { // Save refs to the database match storage::ref_model::batch_save_refs(&repo_id, &refs).await { Ok(_) => { - tracing::info!( - "Refs saved to database for repository {}", - repo_id + println!( + " Refs: {} branches/tags imported", + refs.len() ); - println!(" Refs: {} branches/tags", refs.len()); } Err(e) => { tracing::warn!("Failed to save refs to database: {}", e); @@ -279,19 +274,14 @@ pub async fn handle_repo_clone(output: String, repo_id: String) -> Result<()> { } Err(e) => { tracing::warn!("Failed to read refs from cloned repository: {}", e); + println!(" ⚠️ Warning: Failed to read refs from cloned repo: {}", e); } } // Update repo path to the cloned location repo.path = PathBuf::from(&output); match storage::repo_model::save_repo_to_db(&repo).await { - Ok(_) => { - tracing::info!( - "Updated repo path to {} for repository {}", - output, - repo_id - ); - } + Ok(_) => {} Err(e) => { tracing::warn!("Failed to update repo path to database: {}", e); } @@ -299,17 +289,17 @@ pub async fn handle_repo_clone(output: String, repo_id: String) -> Result<()> { } Err(e) => { tracing::error!("Failed to clone repository: {}", e); - println!("Error: Failed to clone repository: {}", e); + eprintln!("❌ Failed to clone repository: {}", e); } } } Ok(None) => { tracing::error!("Repository {} not found in database", repo_id); - println!("Error: Repository {} not found", repo_id); + eprintln!("❌ Error: Repository {} not found.", repo_id); } Err(e) => { tracing::error!("Failed to query repository {}: {}", repo_id, e); - println!("Error: Failed to query repository: {}", e); + eprintln!("❌ Database error: {}", e); } } Ok(()) diff --git a/src/git/git_repo.rs b/src/git/git_repo.rs index bb9eaeb..8bb77bd 100644 --- a/src/git/git_repo.rs +++ b/src/git/git_repo.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use git2::{Repository, Sort}; +use git2::{BranchType, Repository, Sort}; pub fn repo_root_commit_bytes(path: &str) -> Result> { let repo = @@ -48,12 +48,16 @@ pub fn read_repo_refs(path: &str) -> Result format!("refs/heads/{}", name), + BranchType::Remote => format!("refs/remotes/{}", name), + }; + refs.insert(ref_name, oid.to_string()); } } } diff --git a/src/git/pack.rs b/src/git/pack.rs index 805f395..96f3c35 100644 --- a/src/git/pack.rs +++ b/src/git/pack.rs @@ -112,6 +112,9 @@ pub async fn restore_repo_from_bundle(bundle_path: &str, output_path: &str) -> R tokio::task::spawn_blocking(move || { // 使用 git clone 从 bundle 恢复仓库 + // 注意:从 bundle 克隆时,git clone 可能不会自动 checkout 到 HEAD, + // 特别是当 bundle 包含多个 heads 时。 + // 所以我们需要显式 clone,然后如果目录为空,尝试 checkout。 let output = Command::new("git") .arg("clone") .arg(&bundle_path) @@ -124,6 +127,23 @@ pub async fn restore_repo_from_bundle(bundle_path: &str, output_path: &str) -> R return Err(anyhow::anyhow!("git clone from bundle failed: {}", stderr)); } + // 尝试自动检出分支 (clone bundle 有时不会自动检出工作区) + // 尝试常见分支名,忽略错误(可能分支不存在) + let _ = Command::new("git") + .current_dir(&output_path) + .args(["checkout", "main"]) + .output(); + let _ = Command::new("git") + .current_dir(&output_path) + .args(["checkout", "master"]) + .output(); + + // 强制重置工作区到当前 HEAD,确保文件被检出 + let _ = Command::new("git") + .current_dir(&output_path) + .args(["reset", "--hard", "HEAD"]) + .output(); + Ok(()) }) .await @@ -201,8 +221,7 @@ pub fn pull_repo_from_bundle(repo_path: &str, bundle_path: &str, branch: &str) - return Err(anyhow::anyhow!("repository not found: {}", repo_path)); } - Repository::open(repo_path) - .map_err(|e| anyhow::anyhow!("failed to open git repo: {}", e))?; + Repository::open(repo_path).map_err(|e| anyhow::anyhow!("failed to open git repo: {}", e))?; // 构建分支引用名称,确保格式正确 let _ref_spec = if branch.starts_with("refs/") { diff --git a/src/gossip/message.rs b/src/gossip/message.rs index cde500d..3cf8b8f 100644 --- a/src/gossip/message.rs +++ b/src/gossip/message.rs @@ -106,7 +106,9 @@ impl SignedMessage { pub fn self_hash(&self) -> Vec { let mut hasher = Sha256::new(); - let message_bytes = serde_json::to_vec(&self.message).unwrap_or_default(); + // Canonicalize JSON serialization by converting to Value first (which sorts map keys) + let message_value = serde_json::to_value(&self.message).unwrap_or(serde_json::Value::Null); + let message_bytes = serde_json::to_vec(&message_value).unwrap_or_default(); hasher.update(self.node_id.0.as_bytes()); hasher.update(&message_bytes); diff --git a/src/gossip/service.rs b/src/gossip/service.rs index ba70983..0e3da79 100644 --- a/src/gossip/service.rs +++ b/src/gossip/service.rs @@ -277,9 +277,11 @@ impl GossipService { // 有新的 refs 更新,清空 bundle 等待重新同步 tracing::info!( - "Detected ref updates for repo {} from node {}", + "Detected ref updates for repo {} from node {}. local refs: {:?}, remote refs: {:?}", &repo.repo_id, - ra.node_id + ra.node_id, + local_refs, + repo.refs ); // 删除旧的 bundle 文件 diff --git a/src/main.rs b/src/main.rs index 021218c..891420a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use clap::{Parser, Subcommand}; mod cli; use cli::{handle_auth, handle_node, handle_repo}; +use megaengine::mcp::start_mcp_server; #[derive(Parser)] #[command(name = "megaengine")] @@ -134,7 +135,7 @@ async fn main() -> Result<()> { handle_repo(action).await?; } Commands::Mcp => { - megaengine::mcp::start_mcp_server().await?; + start_mcp_server().await?; } } From 71002400a5942863c38b304efbef29cda2c54737 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Thu, 29 Jan 2026 15:04:33 +0800 Subject: [PATCH 29/42] remove test certs --- tests/bundle_two_nodes.rs | 6 ++++++ tests/gossip_three_nodes.rs | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/bundle_two_nodes.rs b/tests/bundle_two_nodes.rs index a561f06..5c96709 100644 --- a/tests/bundle_two_nodes.rs +++ b/tests/bundle_two_nodes.rs @@ -417,4 +417,10 @@ async fn test_bundle_transfer_between_two_nodes() { let _ = megaengine::storage::node_model::delete_node_from_db(&receiver_node.node_id().to_string()) .await; + + // 清理生成的证书文件 + let _ = std::fs::remove_file("cert/cert_sender.pem"); + let _ = std::fs::remove_file("cert/key_sender.pem"); + let _ = std::fs::remove_file("cert/cert_receiver.pem"); + let _ = std::fs::remove_file("cert/key_receiver.pem"); } diff --git a/tests/gossip_three_nodes.rs b/tests/gossip_three_nodes.rs index b8b2cb8..23c3fab 100644 --- a/tests/gossip_three_nodes.rs +++ b/tests/gossip_three_nodes.rs @@ -21,8 +21,8 @@ async fn test_gossip_three_nodes_message_relay() { // 生成或确保证书存在 megaengine::transport::cert::ensure_certificates( - "cert/cert.pem", - "cert/key.pem", + "cert/cert1.pem", + "cert/key1.pem", "cert/ca-cert.pem", ) .expect("ensure certificates"); @@ -59,8 +59,8 @@ async fn test_gossip_three_nodes_message_relay() { // 4. 启动 QUIC server let config1 = QuicConfig::new( addr1, - "cert/cert.pem".to_string(), - "cert/key.pem".to_string(), + "cert/cert1.pem".to_string(), + "cert/key1.pem".to_string(), "cert/ca-cert.pem".to_string(), ); let config2 = QuicConfig::new( @@ -156,4 +156,12 @@ async fn test_gossip_three_nodes_message_relay() { let _ = node_model::delete_node_from_db(&node_id_1).await; let _ = node_model::delete_node_from_db(&node_id_2).await; let _ = node_model::delete_node_from_db(&node_id_3).await; + + // 清理生成的证书文件 + let _ = std::fs::remove_file("cert/cert1.pem"); + let _ = std::fs::remove_file("cert/key1.pem"); + let _ = std::fs::remove_file("cert/cert2.pem"); + let _ = std::fs::remove_file("cert/key2.pem"); + let _ = std::fs::remove_file("cert/cert3.pem"); + let _ = std::fs::remove_file("cert/key3.pem"); } From 403178647161e323d589270109ef39981c07ba01 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Thu, 29 Jan 2026 15:56:54 +0800 Subject: [PATCH 30/42] =?UTF-8?q?=E6=9B=B4=E6=96=B0size,language,last=5Fco?= =?UTF-8?q?mmit=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli/repo.rs | 144 +++++++++++++++++++++++++++++++++++++- src/git/git_repo.rs | 12 ++++ src/gossip/message.rs | 4 +- src/mcp/mcp_server.rs | 5 +- src/repo/repo.rs | 12 +++- src/repo/repo_manager.rs | 9 ++- src/storage/mod.rs | 4 +- src/storage/repo_model.rs | 35 ++++++--- 8 files changed, 204 insertions(+), 21 deletions(-) diff --git a/src/cli/repo.rs b/src/cli/repo.rs index 7e0ddea..a79864d 100644 --- a/src/cli/repo.rs +++ b/src/cli/repo.rs @@ -37,11 +37,30 @@ pub async fn handle_repo_add(path: String, description: String) -> Result<()> { }; let name = megaengine::git::git_repo::repo_name_space(&path); + let language = detect_language(&path); + + // Calculate size: prefer .git directory size (repository data) over working tree size + let path_p = std::path::Path::new(&path); + let git_dir = path_p.join(".git"); + let size = if git_dir.exists() { + calculate_directory_size(&git_dir) + } else { + 0 + }; + + // Try to get latest git commit time, fallback to now if failed (e.g. empty repo) + let latest_commit_at = match megaengine::git::git_repo::get_latest_commit_time(&path) { + Ok(t) => t, + Err(_) => timestamp_now(), + }; + let desc = repo::repo::P2PDescription { creator: node_id.to_string(), name: name.clone(), description: description.clone(), - timestamp: timestamp_now(), + language: language.clone(), + latest_commit_at, + size, }; let mut repo_obj = @@ -74,6 +93,115 @@ pub async fn handle_repo_add(path: String, description: String) -> Result<()> { Ok(()) } +fn detect_language(path: &str) -> String { + use std::collections::HashMap; + use std::fs; + + let mut ext_counts: HashMap = HashMap::new(); + let mut stack = vec![PathBuf::from(path)]; + let mut files_scanned = 0; + + while let Some(dir) = stack.pop() { + if files_scanned > 2000 { + break; + } // limit scanning + + if let Ok(entries) = fs::read_dir(&dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if name.starts_with('.') + || name == "target" + || name == "node_modules" + || name == "dist" + || name == "build" + { + continue; + } + if stack.len() < 50 { + stack.push(path); + } + } else { + if let Some(ext) = path.extension().and_then(|s| s.to_str()) { + *ext_counts.entry(ext.to_lowercase()).or_insert(0) += 1; + files_scanned += 1; + } + } + } + } + } + + let mut lang_stats = HashMap::new(); + for (ext, count) in ext_counts { + let lang = match ext.as_str() { + "rs" => "Rust", + "go" => "Go", + "py" => "Python", + "js" => "JavaScript", + "ts" | "tsx" => "TypeScript", + "java" => "Java", + "c" | "h" => "C", + "cpp" | "hpp" | "cc" | "cxx" => "C++", + "cs" => "C#", + "rb" => "Ruby", + "php" => "PHP", + "html" => "HTML", + "css" | "scss" | "less" => "CSS", + "swift" => "Swift", + "kt" | "kts" => "Kotlin", + "scala" => "Scala", + "lua" => "Lua", + "sh" | "bash" | "zsh" => "Shell", + "sql" => "SQL", + "md" => "Markdown", + "json" | "yaml" | "yml" | "toml" | "xml" => "Config/Data", + _ => continue, + }; + *lang_stats.entry(lang).or_insert(0) += count; + } + + // 找出数量最多的语言,排除配置类 + lang_stats + .into_iter() + .filter(|(l, _)| *l != "Config/Data" && *l != "Markdown") + .max_by_key(|&(_, count)| count) + .map(|(lang, _)| lang.to_string()) + .unwrap_or_else(|| "Unknown".to_string()) +} + +fn calculate_directory_size(path: &std::path::Path) -> u64 { + use std::fs; + let mut size = 0; + if let Ok(entries) = fs::read_dir(path) { + for entry in entries.flatten() { + let p = entry.path(); + if p.is_file() { + size += fs::metadata(&p).map(|m| m.len()).unwrap_or(0); + } else if p.is_dir() { + size += calculate_directory_size(&p); + } + } + } + size +} + +fn format_bytes(bytes: u64) -> String { + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + + if bytes >= GB { + format!("{:.2} GB", bytes as f64 / GB as f64) + } else if bytes >= MB { + format!("{:.2} MB", bytes as f64 / MB as f64) + } else if bytes >= KB { + format!("{:.2} KB", bytes as f64 / KB as f64) + } else { + format!("{} B", bytes) + } +} + pub async fn handle_repo_list() -> Result<()> { match storage::repo_model::list_repos().await { Ok(repos) => { @@ -99,6 +227,20 @@ async fn print_repo_info(repo: &Repo) { println!("📦 Repo: {}", repo.p2p_description.name); println!(" ID: {}", repo.repo_id); println!(" Creator: {}", repo.p2p_description.creator); + println!(" Language: {}", repo.p2p_description.language); + if repo.p2p_description.latest_commit_at > 0 { + if let Some(dt) = chrono::DateTime::from_timestamp(repo.p2p_description.latest_commit_at, 0) + { + let local = dt.with_timezone(&chrono::Local); + println!(" Updated: {}", local.format("%Y-%m-%d %H:%M:%S")); + } + } + if repo.p2p_description.size > 0 { + println!( + " Size: {}", + format_bytes(repo.p2p_description.size) + ); + } if !repo.p2p_description.description.is_empty() { println!(" Description: {}", repo.p2p_description.description); } diff --git a/src/git/git_repo.rs b/src/git/git_repo.rs index 8bb77bd..e5d0e07 100644 --- a/src/git/git_repo.rs +++ b/src/git/git_repo.rs @@ -78,3 +78,15 @@ pub fn read_repo_refs(path: &str) -> Result Result { + let repo = + Repository::open(path).map_err(|e| anyhow::anyhow!("failed to open git repo: {}", e))?; + let head = repo + .head() + .map_err(|e| anyhow::anyhow!("failed to get HEAD: {}", e))?; + let commit = head + .peel_to_commit() + .map_err(|e| anyhow::anyhow!("failed to peel to commit: {}", e))?; + Ok(commit.time().seconds()) +} diff --git a/src/gossip/message.rs b/src/gossip/message.rs index 3cf8b8f..2130cf2 100644 --- a/src/gossip/message.rs +++ b/src/gossip/message.rs @@ -200,7 +200,9 @@ mod tests { creator: "did:key:test".to_string(), name: "test-repo".to_string(), description: "A test repository".to_string(), - timestamp: 1000, + language: "Rust".to_string(), + latest_commit_at: 1000, + size: 0, }; let repo = Repo::new( diff --git a/src/mcp/mcp_server.rs b/src/mcp/mcp_server.rs index 41c49b8..ca13a42 100644 --- a/src/mcp/mcp_server.rs +++ b/src/mcp/mcp_server.rs @@ -8,7 +8,6 @@ use std::io::{self, BufRead, BufReader, Write}; #[derive(Deserialize, Debug)] struct JsonRpcRequest { - jsonrpc: String, method: String, params: Option, id: Option, @@ -120,7 +119,7 @@ impl RepoMcpServer { "description": repo.p2p_description.description, "path": repo.path.display().to_string(), "bundle": repo.bundle.display().to_string(), - "timestamp": repo.p2p_description.timestamp, + "latest_commit_at": repo.p2p_description.latest_commit_at, }); // 恢复 refs 处理逻辑 @@ -165,7 +164,7 @@ impl RepoMcpServer { "description": repo.p2p_description.description, "path": repo.path.display().to_string(), "bundle": repo.bundle.display().to_string(), - "timestamp": repo.p2p_description.timestamp, + "latest_commit_at": repo.p2p_description.latest_commit_at, }); // Check for updates if this is a local repo diff --git a/src/repo/repo.rs b/src/repo/repo.rs index fc5ab66..64f60f6 100644 --- a/src/repo/repo.rs +++ b/src/repo/repo.rs @@ -8,7 +8,9 @@ pub struct P2PDescription { pub creator: String, pub name: String, pub description: String, - pub timestamp: i64, + pub language: String, + pub latest_commit_at: i64, + pub size: u64, } /// P2P 仓库 @@ -79,7 +81,9 @@ mod tests { creator: "did:key:test".to_string(), name: "test-repo".to_string(), description: "A test repository".to_string(), - timestamp: 1000, + language: "Rust".to_string(), + latest_commit_at: 1000, + size: 0, }; let repo = Repo::new( @@ -98,7 +102,9 @@ mod tests { creator: "did:key:test".to_string(), name: "test-repo".to_string(), description: "A test repository".to_string(), - timestamp: 1000, + language: "Rust".to_string(), + latest_commit_at: 1000, + size: 0, }; let mut repo = Repo::new( diff --git a/src/repo/repo_manager.rs b/src/repo/repo_manager.rs index 29c867c..c125cfe 100644 --- a/src/repo/repo_manager.rs +++ b/src/repo/repo_manager.rs @@ -97,7 +97,9 @@ mod tests { creator: "did:key:test".to_string(), name: "test-repo".to_string(), description: "A test repository".to_string(), - timestamp: 1000, + language: "Rust".to_string(), + latest_commit_at: 2000, + size: 0, }; let repo = Repo::new(repo_id.to_string(), desc, PathBuf::from("/tmp/test-repo")); @@ -135,7 +137,10 @@ mod tests { creator: "did:key:test".to_string(), name: "test-repo-persist".to_string(), description: "A test repository with persistence".to_string(), - timestamp: 2000, + language: "Rust".to_string(), + + latest_commit_at: 2000, + size: 0, }; let repo = Repo::new( diff --git a/src/storage/mod.rs b/src/storage/mod.rs index f576987..84b18ea 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -113,7 +113,9 @@ pub async fn init_db() -> Result { name TEXT NOT NULL, creator TEXT NOT NULL, description TEXT NOT NULL, - timestamp INTEGER NOT NULL, + language TEXT NOT NULL DEFAULT '', + size INTEGER NOT NULL DEFAULT 0, + latest_commit_at INTEGER NOT NULL DEFAULT 0, path TEXT NOT NULL, bundle TEXT NOT NULL DEFAULT '', is_external INTEGER NOT NULL DEFAULT 0, diff --git a/src/storage/repo_model.rs b/src/storage/repo_model.rs index fa4847b..ff69e36 100644 --- a/src/storage/repo_model.rs +++ b/src/storage/repo_model.rs @@ -14,10 +14,12 @@ pub struct Model { pub name: String, pub creator: String, pub description: String, - pub timestamp: i64, + pub language: String, pub path: String, pub bundle: String, pub is_external: bool, + pub size: i64, + pub latest_commit_at: i64, pub created_at: i64, pub updated_at: i64, } @@ -42,10 +44,12 @@ pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { name: Set(repo.p2p_description.name.clone()), creator: Set(repo.p2p_description.creator.clone()), description: Set(repo.p2p_description.description.clone()), - timestamp: Set(repo.p2p_description.timestamp), + language: Set(repo.p2p_description.language.clone()), path: Set(repo.path.to_string_lossy().to_string()), bundle: Set(repo.bundle.to_string_lossy().to_string()), is_external: Set(repo.is_external), + size: Set(repo.p2p_description.size as i64), + latest_commit_at: Set(repo.p2p_description.latest_commit_at), created_at: Unchanged(existing_model.created_at), updated_at: Set(now), }; @@ -57,10 +61,12 @@ pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { name: Set(repo.p2p_description.name.clone()), creator: Set(repo.p2p_description.creator.clone()), description: Set(repo.p2p_description.description.clone()), - timestamp: Set(repo.p2p_description.timestamp), + language: Set(repo.p2p_description.language.clone()), path: Set(repo.path.to_string_lossy().to_string()), bundle: Set(repo.bundle.to_string_lossy().to_string()), is_external: Set(repo.is_external), + size: Set(repo.p2p_description.size as i64), + latest_commit_at: Set(repo.p2p_description.latest_commit_at), created_at: Set(now), updated_at: Set(now), }; @@ -89,7 +95,9 @@ pub async fn load_repo_from_db(repo_id: &str) -> Result> { creator: model.creator, name: model.name, description: model.description, - timestamp: model.timestamp, + language: model.language, + latest_commit_at: model.latest_commit_at, + size: model.size as u64, }, path: PathBuf::from(model.path), bundle: PathBuf::from(model.bundle), @@ -127,7 +135,9 @@ pub async fn list_repos() -> Result> { creator: model.creator, name: model.name, description: model.description, - timestamp: model.timestamp, + language: model.language, + latest_commit_at: model.latest_commit_at, + size: model.size as u64, }, path: PathBuf::from(model.path), bundle: PathBuf::from(model.bundle), @@ -140,21 +150,22 @@ pub async fn list_repos() -> Result> { /// 更新 Repo 的 bundle 路径 pub async fn update_repo_bundle(repo_id: &str, bundle_path: &str) -> Result<()> { let db = init_db().await?; - let now = chrono::Local::now().timestamp(); // 查询是否存在 if let Some(model) = Entity::find_by_id(repo_id).one(&db).await? { let active_model = ActiveModel { id: Unchanged(model.id), bundle: Set(bundle_path.to_string()), - updated_at: Set(now), + updated_at: Unchanged(model.updated_at), // Keep other fields unchanged name: Unchanged(model.name), creator: Unchanged(model.creator), description: Unchanged(model.description), - timestamp: Unchanged(model.timestamp), + language: Unchanged(model.language), path: Unchanged(model.path), is_external: Unchanged(model.is_external), + size: Unchanged(model.size), + latest_commit_at: Unchanged(model.latest_commit_at), created_at: Unchanged(model.created_at), }; Entity::update(active_model).exec(&db).await?; @@ -174,7 +185,9 @@ mod tests { creator: "did:node:test333".to_string(), name: "test-repo".to_string(), description: "A test repository".to_string(), - timestamp: 1000, + language: "Rust".to_string(), + latest_commit_at: 1000, + size: 0, }; let mut repo = Repo::new( @@ -212,7 +225,9 @@ mod tests { creator: "did:node:test".to_string(), name: format!("test-repo-{}", i), description: format!("Test repository {}", i), - timestamp: 1000 + i, + language: "Rust".to_string(), + latest_commit_at: 1000 + i, + size: 0, }; let repo = Repo::new( From a1d6749c39c80ed5b7e00cad962181627f522cb9 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Thu, 29 Jan 2026 15:58:47 +0800 Subject: [PATCH 31/42] upload test certs --- .gitignore | 2 +- cert/ca-cert-key.pem | 5 +++++ cert/ca-cert.pem | 12 ++++++++++++ cert/cert.pem | 11 +++++++++++ cert/key.pem | 5 +++++ 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 cert/ca-cert-key.pem create mode 100644 cert/ca-cert.pem create mode 100644 cert/cert.pem create mode 100644 cert/key.pem diff --git a/.gitignore b/.gitignore index a8bf005..fa768ae 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,7 @@ target .vscode/ -cert/ +# cert/ .megaengine/ tmp/* dist/ \ No newline at end of file diff --git a/cert/ca-cert-key.pem b/cert/ca-cert-key.pem new file mode 100644 index 0000000..5dbd471 --- /dev/null +++ b/cert/ca-cert-key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgwjcRYNwbWnz3oU4d +WVQP2+kO5rWIa/Yp/isw9wWKpAKhRANCAAQKqIDIm/IWAi2YWlBcr1FhR7/LiuBz +ieorlLJ0PCiQwTT0rqvsPY/yP9T+fuatqbBMRQ9O9eBa9DYt/vpf0yrF +-----END PRIVATE KEY----- diff --git a/cert/ca-cert.pem b/cert/ca-cert.pem new file mode 100644 index 0000000..5bf54e4 --- /dev/null +++ b/cert/ca-cert.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBqTCCAVCgAwIBAgIUEjtJHYOEW2t8KW4M+D6SE63tIpswCgYIKoZIzj0EAwIw +OjEWMBQGA1UEAwwNTWVnYUVuZ2luZSBDQTETMBEGA1UECgwKTWVnYUVuZ2luZTEL +MAkGA1UEBgwCQ04wIBcNNzUwMTAxMDAwMDAwWhgPNDA5NjAxMDEwMDAwMDBaMDox +FjAUBgNVBAMMDU1lZ2FFbmdpbmUgQ0ExEzARBgNVBAoMCk1lZ2FFbmdpbmUxCzAJ +BgNVBAYMAkNOMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECqiAyJvyFgItmFpQ +XK9RYUe/y4rgc4nqK5SydDwokME09K6r7D2P8j/U/n7mramwTEUPTvXgWvQ2Lf76 +X9MqxaMyMDAwHQYDVR0OBBYEFMUTNaN009HlhwbwOAGZ83JEJFXjMA8GA1UdEwEB +/wQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgVs5IaHqpW3QHZc83js5wQhslKhFo +ky1i1MCoAN6fdOACIF0fbglxEteZ4h9A76qKiSuai1DQ5iCdQW7goko/9SX4 +-----END CERTIFICATE----- +-- diff --git a/cert/cert.pem b/cert/cert.pem new file mode 100644 index 0000000..5f3607a --- /dev/null +++ b/cert/cert.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBmDCCAT6gAwIBAgIUXoB8kOpCVx6expYfOB8rKzEuDsEwCgYIKoZIzj0EAwIw +OjEWMBQGA1UEAwwNTWVnYUVuZ2luZSBDQTETMBEGA1UECgwKTWVnYUVuZ2luZTEL +MAkGA1UEBgwCQ04wIBcNNzUwMTAxMDAwMDAwWhgPNDA5NjAxMDEwMDAwMDBaMDYx +EjAQBgNVBAMMCWxvY2FsaG9zdDETMBEGA1UECgwKTWVnYUVuZ2luZTELMAkGA1UE +BgwCQ04wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARxLOHbYPV/sW6ZlZamzZp7 +wa3EnqMuz9aRZj3pxU2YA0Qrku1Qi2pF7gvn1COn1byBqJeSDu4RCOASBgny7lrW +oyQwIjAgBgNVHREEGTAXgglsb2NhbGhvc3SHBH8AAAGHBAAAAAAwCgYIKoZIzj0E +AwIDSAAwRQIhAPJ4rCN9Jt+eSNZyOy4FK8UUI3OTWAE3dYdGXW+m5cC9AiBBRy5E +v6ak88bHExo6hbaMNfrWsuiFgQqcSlTnQOqGNQ== +-----END CERTIFICATE----- diff --git a/cert/key.pem b/cert/key.pem new file mode 100644 index 0000000..b039a5b --- /dev/null +++ b/cert/key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgRCRp5Jovne1DDTtJ +lSGamrZNrcpG42tEih09TDxD3WuhRANCAARxLOHbYPV/sW6ZlZamzZp7wa3EnqMu +z9aRZj3pxU2YA0Qrku1Qi2pF7gvn1COn1byBqJeSDu4RCOASBgny7lrW +-----END PRIVATE KEY----- From 1cd9f8a02e793df36c8132721e7cf249332ee14e Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Fri, 13 Mar 2026 11:47:56 +0800 Subject: [PATCH 32/42] enable mcp, chat, libvault --- .github/workflows/claude-review.yml | 117 ++ Cargo.lock | 2213 +++++++++++++++++++++++++-- Cargo.toml | 5 +- cert/ca-cert-key.pem | 5 - cert/ca-cert.pem | 12 - cert/cert.pem | 11 - cert/key.pem | 5 - src/chat/mod.rs | 1 + src/chat/service.rs | 324 ++++ src/cli/chat.rs | 69 + src/cli/mod.rs | 2 + src/cli/node.rs | 8 + src/gossip/message.rs | 37 + src/gossip/service.rs | 38 +- src/identity/keypair.rs | 120 ++ src/lib.rs | 1 + src/main.rs | 8 + src/mcp/mcp_server.rs | 12 + src/storage/chat_message.rs | 67 + src/storage/mod.rs | 181 ++- src/storage/node_model.rs | 8 +- src/storage/ref_model.rs | 16 +- src/storage/repo_model.rs | 12 +- src/transport/cert.rs | 248 ++- src/transport/config.rs | 11 +- src/transport/quic.rs | 116 ++ 26 files changed, 3320 insertions(+), 327 deletions(-) create mode 100644 .github/workflows/claude-review.yml delete mode 100644 cert/ca-cert-key.pem delete mode 100644 cert/ca-cert.pem delete mode 100644 cert/cert.pem delete mode 100644 cert/key.pem create mode 100644 src/chat/mod.rs create mode 100644 src/chat/service.rs create mode 100644 src/cli/chat.rs create mode 100644 src/storage/chat_message.rs diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml new file mode 100644 index 0000000..b6643b6 --- /dev/null +++ b/.github/workflows/claude-review.yml @@ -0,0 +1,117 @@ +name: Claude Code Review with Progress Tracking + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created, edited, deleted] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + pull_request_target: + types: [opened, synchronize, closed] + +permissions: + contents: read + pull-requests: write + issues: write + actions: write + +jobs: + claude-review-with-tracking: + runs-on: ubuntu-latest + + # Only run for: + # 1. PRs from trusted users (OWNER/MEMBER/COLLABORATOR) + # 2. Comments mentioning @claude from trusted users + # 3. PR reviews mentioning @claude from trusted users + if: | + ( + github.event_name == 'pull_request_target' && + ( + github.event.pull_request.author_association == 'OWNER' || + github.event.pull_request.author_association == 'MEMBER' || + github.event.pull_request.author_association == 'COLLABORATOR' + ) + ) || + ( + (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && + contains(github.event.comment.body, '@claude') && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + ) + ) || + ( + github.event_name == 'pull_request_review' && + contains(github.event.review.body, '@claude') && + ( + github.event.review.author_association == 'OWNER' || + github.event.review.author_association == 'MEMBER' || + github.event.review.author_association == 'COLLABORATOR' + ) + ) + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} + + - name: Checkout PR Branch (for comments) + if: ${{ github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_review' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr checkout ${{ github.event.issue.number || github.event.pull_request.number }} + + - name: PR Review with Progress Tracking + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + # Enable progress tracking + track_progress: true + show_full_output: true + + # Your custom review instructions + prompt: | + REPO: ${{ github.repository }} + + Perform a comprehensive code review with the following focus areas: + + 1. **Code Quality** + - Clean code principles and best practices + - Proper error handling and edge cases + - Code readability and maintainability + + 2. **Security** + - Check for potential security vulnerabilities + - Validate input sanitization + - Review authentication/authorization logic + + 3. **Performance** + - Identify potential performance bottlenecks + - Review database queries for efficiency + - Check for memory leaks or resource issues + + 4. **Testing** + - Verify adequate test coverage + - Review test quality and edge cases + - Check for missing test scenarios + + 5. **Documentation** + - Ensure code is properly documented + - Verify README updates for new features + - Check API documentation accuracy + + Provide detailed feedback using inline comments for specific issues. + Use top-level comments for general observations or praise. + + # Tools for comprehensive PR review + claude_args: | + --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" diff --git a/Cargo.lock b/Cargo.lock index dfa4de8..b616a85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,57 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "bytes 1.10.1", + "crypto-common", + "generic-array 0.14.9", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-kw" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fa2b352dcefb5f7f3a5fb840e02665d311d878955380515e4fd50095dd3d8c" +dependencies = [ + "aes", +] + [[package]] name = "ahash" version = "0.7.8" @@ -112,6 +163,28 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arc-swap" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", + "zeroize", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -130,6 +203,51 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "as-any" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f477b951e452a0b6b4a10b53ccd569042d1d01729b519e02074a9c0958a063" + +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -172,6 +290,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -214,7 +341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.4.5", "bytes 1.10.1", "futures-util", "http", @@ -223,7 +350,7 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit", + "matchit 0.7.3", "memchr", "mime", "percent-encoding", @@ -241,6 +368,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core 0.5.6", + "bytes 1.10.1", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit 0.8.4", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "axum-core" version = "0.4.5" @@ -262,12 +414,36 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes 1.10.1", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "base-x" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base256emoji" version = "1.0.2" @@ -296,6 +472,30 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +[[package]] +name = "bcrypt" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abaf6da45c74385272ddf00e1ac074c7d8a6c1a1dda376902bd6a427522a8b2c" +dependencies = [ + "base64 0.22.1", + "blowfish", + "getrandom 0.3.4", + "subtle", + "zeroize", +] + +[[package]] +name = "better_default" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b9a5040dce49a7642c97ccb1ae59567098967b5d52c29773f1299a42d23bb39" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "bigdecimal" version = "0.3.1" @@ -316,7 +516,7 @@ dependencies = [ "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -327,6 +527,27 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "bitfields" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d866f92dc1574aa8da443eacb06ad8fbe4056dbc1b7c3aae508cbccd46c7e706" +dependencies = [ + "bitfields-impl", +] + +[[package]] +name = "bitfields-impl" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c09459e6af3016ea58af8332e31d5da117d33a621bad7019355eefccc4a567d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", + "thiserror 2.0.17", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -354,6 +575,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "blake2b_simd" version = "0.5.11" @@ -362,7 +592,18 @@ checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" dependencies = [ "arrayref", "arrayvec 0.5.2", - "constant_time_eq", + "constant_time_eq 0.1.5", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "constant_time_eq 0.4.2", ] [[package]] @@ -373,7 +614,21 @@ checksum = "9e461a7034e85b211a4acb57ee2e6730b32912b06c08cc242243c39fc21ae6a2" dependencies = [ "arrayref", "arrayvec 0.5.2", - "constant_time_eq", + "constant_time_eq 0.1.5", +] + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "cc", + "cfg-if", + "constant_time_eq 0.4.2", + "cpufeatures", ] [[package]] @@ -395,6 +650,25 @@ dependencies = [ "generic-array 0.14.9", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array 0.14.9", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "borsh" version = "1.5.7" @@ -418,6 +692,37 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "buffer-redux" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "431a9cc8d7efa49bc326729264537f5e60affce816c66edf434350778c9f4f54" +dependencies = [ + "memchr", +] + +[[package]] +name = "builder-pattern" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85376b93d8efe18dd819f56505e33e7a9c0f93fb02bd761f8690026178ed6e5" +dependencies = [ + "builder-pattern-macro", + "futures", +] + +[[package]] +name = "builder-pattern-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d624ef88b39588d113f807ffb38ee968aafc388ca57dd7a9a7b82d3de1f5f4" +dependencies = [ + "bitflags 1.3.2", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -452,6 +757,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" @@ -470,6 +781,34 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "camellia" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3264e2574e9ef2b53ce6f536dea83a69ac0bc600b762d1523ff83fe07230ce30" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "cast5" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b07d673db1ccf000e90f54b819db9e75a8348d6eb056e9b8ab53231b7a9911" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.44" @@ -494,7 +833,16 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", +] + +[[package]] +name = "cfb-mode" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330" +dependencies = [ + "cipher", ] [[package]] @@ -509,6 +857,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.42" @@ -523,6 +895,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -574,6 +957,17 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "cmac" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" +dependencies = [ + "cipher", + "dbl", + "digest 0.10.7", +] + [[package]] name = "cmake" version = "0.1.54" @@ -599,6 +993,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", + "portable-atomic", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -617,6 +1021,27 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -667,6 +1092,30 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc24" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -688,6 +1137,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.9", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -695,92 +1156,276 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array 0.14.9", + "rand_core 0.6.4", "typenum", ] [[package]] -name = "curve25519-dalek" -version = "4.1.3" +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "cx448" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c0cf476284b03eb6c10e78787b21c7abb7d7d43cb2f02532ba6b831ed892fa" +dependencies = [ + "crypto-bigint", + "elliptic-curve", + "pkcs8", + "rand_core 0.6.4", + "serdect 0.3.0", + "sha3", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "daemonize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e" +dependencies = [ + "libc", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.108", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "data-encoding-macro" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +dependencies = [ + "data-encoding", + "syn 2.0.108", +] + +[[package]] +name = "dbl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" +dependencies = [ + "generic-array 0.14.9", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest 0.10.7", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", + "derive_builder_macro", ] [[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" +name = "derive_builder_core" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ + "darling", "proc-macro2", "quote", "syn 2.0.108", ] [[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "data-encoding-macro" -version = "0.1.18" +name = "derive_builder_macro" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ - "data-encoding", - "data-encoding-macro-internal", + "derive_builder_core", + "syn 2.0.108", ] [[package]] -name = "data-encoding-macro-internal" -version = "0.1.16" +name = "derive_more" +version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "data-encoding", + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", "syn 2.0.108", ] [[package]] -name = "der" -version = "0.7.10" +name = "derive_more" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", + "derive_more-impl", ] [[package]] -name = "deranged" -version = "0.5.5" +name = "derive_more-impl" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "powerfmt", - "serde_core", + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.108", + "unicode-xid", ] [[package]] -name = "derivative" -version = "2.2.0" +name = "des" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "cipher", ] [[package]] @@ -821,12 +1466,55 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dsa" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689" +dependencies = [ + "digest 0.10.7", + "num-bigint-dig", + "num-traits", + "pkcs8", + "rfc6979", + "sha2 0.10.9", + "signature", + "zeroize", +] + [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "eax" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28" +dependencies = [ + "aead", + "cipher", + "cmac", + "ctr", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -862,6 +1550,66 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "base64ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array 0.14.9", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "serde_json", + "serdect 0.2.0", + "subtle", + "tap", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -895,6 +1643,19 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", + "portable-atomic", + "portable-atomic-util", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -919,6 +1680,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -931,6 +1703,17 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + [[package]] name = "flume" version = "0.11.1" @@ -942,6 +1725,12 @@ dependencies = [ "spin", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1095,6 +1884,7 @@ checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1124,6 +1914,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "git2" version = "0.16.1" @@ -1145,6 +1945,42 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "go-defer" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4053f727e8be72cb6afd9f05955676f4df66b2f6a56c5b29362403df8810062b" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes 1.10.1", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1179,6 +2015,45 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "hcl-edit" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88489f7cdf733b4c7798403f72d2c16fdc2b720e82c5151055f618a9b49afc1c" +dependencies = [ + "fnv", + "hcl-primitives", + "vecmap-rs", + "winnow", +] + +[[package]] +name = "hcl-primitives" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829a11d304c89e2cfe0dbb494a686bbe2b48ade17705c62cd1957b04aa4630f6" +dependencies = [ + "itoa", + "kstring", + "ryu", + "serde", + "unicode-ident", +] + +[[package]] +name = "hcl-rs" +version = "0.18.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48af7144c49a8db969e8a9d00cd470e1a446a3a73f6fa5eafc1eeb3d44d61ff4" +dependencies = [ + "hcl-edit", + "hcl-primitives", + "indexmap", + "itoa", + "serde", + "vecmap-rs", +] + [[package]] name = "heck" version = "0.4.1" @@ -1272,6 +2147,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + [[package]] name = "hyper" version = "1.8.1" @@ -1282,6 +2163,7 @@ dependencies = [ "bytes 1.10.1", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1291,6 +2173,53 @@ dependencies = [ "pin-utils", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.6", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes 1.10.1", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] @@ -1299,14 +2228,24 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ + "base64 0.22.1", "bytes 1.10.1", + "futures-channel", "futures-core", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", + "system-configuration", "tokio", "tower-service", + "tracing", + "windows-registry", ] [[package]] @@ -1414,6 +2353,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "idea" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075557004419d7f2031b8bb7f44bb43e55a83ca7b63076a8fb8fe75753836477" +dependencies = [ + "cipher", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1443,6 +2397,8 @@ checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", + "serde", + "serde_core", ] [[package]] @@ -1456,6 +2412,40 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array 0.14.9", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "ipnetwork" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c3eaab3ac0ede60ffa41add21970a7df7d91772c03383aac6c2c3d53cc716b" +dependencies = [ + "serde", +] + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1471,6 +2461,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1519,6 +2518,39 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2 0.10.9", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "serde", + "static_assertions", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1528,6 +2560,12 @@ dependencies = [ "spin", ] +[[package]] +name = "libbz2-rs-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + [[package]] name = "libc" version = "0.2.177" @@ -1600,6 +2638,77 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libvault" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14728802093baa700a22a6906e12fe97fcee8a3edadfa30f02ce9d590e94aee4" +dependencies = [ + "anyhow", + "arc-swap", + "as-any", + "async-trait", + "base64 0.22.1", + "bcrypt", + "better_default", + "blake2b_simd 1.0.4", + "blake3", + "builder-pattern", + "chrono", + "crossbeam-channel", + "daemonize", + "dashmap", + "derivative", + "derive_more 0.99.20", + "enum-map", + "foreign-types", + "glob", + "go-defer", + "hcl-rs", + "hex", + "humantime", + "ipnetwork", + "itertools 0.14.0", + "lazy_static", + "libc", + "lockfile", + "log", + "openssl", + "openssl-sys", + "pem", + "pgp", + "priority-queue", + "radix_trie", + "rand 0.9.2", + "rand_chacha 0.3.1", + "regex", + "reqwest", + "rustls", + "rustls-pemfile", + "rustls-webpki", + "serde", + "serde_bytes", + "serde_derive", + "serde_json", + "serde_yaml", + "smallvec", + "ssh-key", + "stretto", + "strum", + "strum_macros", + "tempfile", + "thiserror 2.0.17", + "tokio", + "toml", + "tonic", + "tracing", + "ureq", + "url", + "webpki-roots 0.26.11", + "x509-parser", + "zeroize", +] + [[package]] name = "libz-sys" version = "1.1.23" @@ -1633,6 +2742,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfile" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be1cf190319c74ba3e45923624626ae2e43fe42ad7e60ff38ded81044c37630" + [[package]] name = "log" version = "0.4.28" @@ -1671,6 +2786,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -1686,18 +2807,21 @@ name = "megaengine" version = "0.1.0" dependencies = [ "anyhow", - "axum", + "axum 0.7.9", + "chacha20poly1305", "chrono", "clap", + "curve25519-dalek", "ed25519-dalek", "futures", "git2", "hex", + "libvault", "multibase", "multihash", + "openssl", "quinn", "rand_core 0.6.4", - "rcgen", "rustls", "rustls-pemfile", "sea-orm", @@ -1707,7 +2831,7 @@ dependencies = [ "sqlx", "tokio", "tokio-stream", - "tower-http", + "tower-http 0.5.2", "tracing", "tracing-subscriber", "uuid", @@ -1731,6 +2855,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.0" @@ -1760,7 +2894,7 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26aea85740a1d3014a2a4fe75fc70c5061c96eadb9d0c934cd5cb6178a3dc810" dependencies = [ - "blake2b_simd", + "blake2b_simd 0.5.11", "blake2s_simd", "bytes 0.5.6", "sha1 0.5.0", @@ -1786,6 +2920,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + [[package]] name = "nom" version = "7.1.3" @@ -1796,6 +2939,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1827,6 +2979,7 @@ dependencies = [ "num-iter", "num-traits", "rand 0.8.5", + "serde", "smallvec", "zeroize", ] @@ -1867,6 +3020,49 @@ dependencies = [ "libm", ] +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "ocb3" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c196e0276c471c843dd5777e7543a36a298a4be942a2a688d8111cd43390dedb" +dependencies = [ + "aead", + "cipher", + "ctr", + "subtle", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1879,6 +3075,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl" version = "0.10.75" @@ -1956,6 +3158,50 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core 0.6.4", + "sha2 0.10.9", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1979,6 +3225,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -1986,30 +3243,119 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] -name = "pem" -version = "3.0.6" +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pgp" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaffe1ec22db286599c30ae6be75b37493b558735d86c8e59ec5c38794415fe4" +dependencies = [ + "aead", + "aes", + "aes-gcm", + "aes-kw", + "argon2", + "base64 0.22.1", + "bitfields", + "block-padding", + "blowfish", + "buffer-redux", + "byteorder", + "bytes 1.10.1", + "bzip2", + "camellia", + "cast5", + "cfb-mode", + "cipher", + "const-oid", + "crc24", + "curve25519-dalek", + "cx448", + "derive_builder", + "derive_more 2.1.1", + "des", + "digest 0.10.7", + "dsa", + "eax", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "flate2", + "generic-array 0.14.9", + "hex", + "hkdf", + "idea", + "k256", + "log", + "md-5", + "nom 8.0.0", + "num-bigint-dig", + "num-traits", + "num_enum", + "ocb3", + "p256", + "p384", + "p521", + "rand 0.8.5", + "regex", + "replace_with", + "ripemd", + "rsa", + "sha1 0.10.6", + "sha1-checked", + "sha2 0.10.9", + "sha3", + "signature", + "smallvec", + "snafu", + "twofish", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "pin-project" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ - "base64 0.22.1", - "serde_core", + "pin-project-internal", ] [[package]] -name = "pem-rfc7468" -version = "0.7.0" +name = "pin-project-internal" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ - "base64ct", + "proc-macro2", + "quote", + "syn 2.0.108", ] -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2049,6 +3395,44 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -2083,13 +3467,33 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "priority-queue" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" +dependencies = [ + "equivalent", + "indexmap", + "serde", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.7", ] [[package]] @@ -2245,6 +3649,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.8.5" @@ -2304,19 +3718,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "rcgen" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" -dependencies = [ - "pem", - "ring", - "rustls-pki-types", - "time", - "yasna", -] - [[package]] name = "redox_syscall" version = "0.5.18" @@ -2328,9 +3729,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2340,9 +3741,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2364,6 +3765,66 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "replace_with" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes 1.10.1", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower", + "tower-http 0.6.8", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.6", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -2378,6 +3839,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "rkyv" version = "0.7.45" @@ -2409,9 +3879,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", "digest 0.10.7", @@ -2421,6 +3891,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core 0.6.4", + "sha2 0.10.9", "signature", "spki", "subtle", @@ -2458,6 +3929,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "rustix" version = "1.1.2" @@ -2687,6 +4167,21 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array 0.14.9", + "pkcs8", + "serdect 0.2.0", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -2739,6 +4234,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -2783,6 +4288,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2795,6 +4309,39 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "serdect" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.5.0" @@ -2812,6 +4359,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1-checked" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" +dependencies = [ + "digest 0.10.7", + "sha1 0.10.6", + "zeroize", +] + [[package]] name = "sha2" version = "0.7.1" @@ -2835,6 +4393,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2869,6 +4437,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "simdutf8" version = "0.1.5" @@ -2893,6 +4467,27 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "socket2" version = "0.6.1" @@ -2928,7 +4523,7 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" dependencies = [ - "nom", + "nom 7.1.3", "unicode_categories", ] @@ -2960,7 +4555,7 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener", + "event-listener 2.5.3", "futures-channel", "futures-core", "futures-intrusive", @@ -3146,6 +4741,49 @@ dependencies = [ "uuid", ] +[[package]] +name = "ssh-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "cipher", + "ssh-encoding", +] + +[[package]] +name = "ssh-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2 0.10.9", +] + +[[package]] +name = "ssh-key" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" +dependencies = [ + "ed25519-dalek", + "num-bigint-dig", + "p256", + "p384", + "p521", + "rand_core 0.6.4", + "rsa", + "sec1", + "sha2 0.10.9", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3158,6 +4796,24 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stretto" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a313e115c2cd9a88d99d60386bc88641c853d468b2c3bc454c294f385fc084" +dependencies = [ + "atomic", + "crossbeam-channel", + "getrandom 0.2.16", + "parking_lot", + "rand 0.8.5", + "seahash", + "thiserror 1.0.69", + "tracing", + "wg", + "xxhash-rust", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -3176,10 +4832,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "strum" -version = "0.25.0" +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.108", +] [[package]] name = "subtle" @@ -3214,6 +4886,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3226,6 +4901,27 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -3387,6 +5083,26 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -3398,6 +5114,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes 1.10.1", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.3" @@ -3407,6 +5157,20 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + [[package]] name = "toml_edit" version = "0.23.7" @@ -3414,7 +5178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 0.7.3", "toml_parser", "winnow", ] @@ -3428,6 +5192,41 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tonic" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +dependencies = [ + "async-trait", + "axum 0.8.8", + "base64 0.22.1", + "bytes 1.10.1", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.3" @@ -3436,9 +5235,12 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -3460,6 +5262,24 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes 1.10.1", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -3534,6 +5354,31 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "triomphe" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" +dependencies = [ + "serde", + "stable_deref_trait", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "twofish" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78e83a30223c757c3947cd144a31014ff04298d8719ae10d03c31c0448c8013" +dependencies = [ + "cipher", +] + [[package]] name = "typenum" version = "1.19.0" @@ -3573,12 +5418,34 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unicode_categories" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "unsigned-varint" version = "0.3.3" @@ -3591,6 +5458,24 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + [[package]] name = "url" version = "2.5.7" @@ -3645,6 +5530,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vecmap-rs" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9758649b51083aa8008666f41c23f05abca1766aad4cc447b195dd83ef1297b" +dependencies = [ + "serde", +] + [[package]] name = "version_check" version = "0.9.5" @@ -3661,6 +5555,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3695,6 +5598,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.105" @@ -3727,6 +5643,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -3746,6 +5672,36 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wg" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aafc5e81e847f05d6770e074faf7b1cd4a5dec9a0e88eac5d55e20fdfebee9a" +dependencies = [ + "event-listener 5.4.1", + "futures-core", + "parking_lot", + "triomphe", +] + [[package]] name = "whoami" version = "1.6.1" @@ -3806,6 +5762,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -4143,14 +6110,40 @@ dependencies = [ ] [[package]] -name = "yasna" -version = "0.5.2" +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.17", "time", ] +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yoke" version = "0.8.1" @@ -4220,6 +6213,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] [[package]] name = "zerotrie" @@ -4253,3 +6260,9 @@ dependencies = [ "quote", "syn 2.0.108", ] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" diff --git a/Cargo.toml b/Cargo.toml index fde4dc4..a8828b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,8 @@ rustls-pemfile = "2.2" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } clap = { version = "4.3", features = ["derive"] } -rcgen = "0.13" +libvault = "0.2.2" +openssl = "0.10" chrono = { version = "0.4", features = ["serde"] } sea-orm = { version = "0.12", features = ["runtime-tokio-native-tls", "sqlx-sqlite"] } sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "sqlite"] } @@ -30,3 +31,5 @@ tower-http = { version = "0.5", features = ["cors"] } uuid = { version = "1.0", features = ["v4"] } futures = "0.3" tokio-stream = "0.1.18" +chacha20poly1305 = "0.10.1" +curve25519-dalek = { version = "4.1.3", features = ["legacy_compatibility"] } diff --git a/cert/ca-cert-key.pem b/cert/ca-cert-key.pem deleted file mode 100644 index 5dbd471..0000000 --- a/cert/ca-cert-key.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgwjcRYNwbWnz3oU4d -WVQP2+kO5rWIa/Yp/isw9wWKpAKhRANCAAQKqIDIm/IWAi2YWlBcr1FhR7/LiuBz -ieorlLJ0PCiQwTT0rqvsPY/yP9T+fuatqbBMRQ9O9eBa9DYt/vpf0yrF ------END PRIVATE KEY----- diff --git a/cert/ca-cert.pem b/cert/ca-cert.pem deleted file mode 100644 index 5bf54e4..0000000 --- a/cert/ca-cert.pem +++ /dev/null @@ -1,12 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIBqTCCAVCgAwIBAgIUEjtJHYOEW2t8KW4M+D6SE63tIpswCgYIKoZIzj0EAwIw -OjEWMBQGA1UEAwwNTWVnYUVuZ2luZSBDQTETMBEGA1UECgwKTWVnYUVuZ2luZTEL -MAkGA1UEBgwCQ04wIBcNNzUwMTAxMDAwMDAwWhgPNDA5NjAxMDEwMDAwMDBaMDox -FjAUBgNVBAMMDU1lZ2FFbmdpbmUgQ0ExEzARBgNVBAoMCk1lZ2FFbmdpbmUxCzAJ -BgNVBAYMAkNOMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECqiAyJvyFgItmFpQ -XK9RYUe/y4rgc4nqK5SydDwokME09K6r7D2P8j/U/n7mramwTEUPTvXgWvQ2Lf76 -X9MqxaMyMDAwHQYDVR0OBBYEFMUTNaN009HlhwbwOAGZ83JEJFXjMA8GA1UdEwEB -/wQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgVs5IaHqpW3QHZc83js5wQhslKhFo -ky1i1MCoAN6fdOACIF0fbglxEteZ4h9A76qKiSuai1DQ5iCdQW7goko/9SX4 ------END CERTIFICATE----- --- diff --git a/cert/cert.pem b/cert/cert.pem deleted file mode 100644 index 5f3607a..0000000 --- a/cert/cert.pem +++ /dev/null @@ -1,11 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIBmDCCAT6gAwIBAgIUXoB8kOpCVx6expYfOB8rKzEuDsEwCgYIKoZIzj0EAwIw -OjEWMBQGA1UEAwwNTWVnYUVuZ2luZSBDQTETMBEGA1UECgwKTWVnYUVuZ2luZTEL -MAkGA1UEBgwCQ04wIBcNNzUwMTAxMDAwMDAwWhgPNDA5NjAxMDEwMDAwMDBaMDYx -EjAQBgNVBAMMCWxvY2FsaG9zdDETMBEGA1UECgwKTWVnYUVuZ2luZTELMAkGA1UE -BgwCQ04wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARxLOHbYPV/sW6ZlZamzZp7 -wa3EnqMuz9aRZj3pxU2YA0Qrku1Qi2pF7gvn1COn1byBqJeSDu4RCOASBgny7lrW -oyQwIjAgBgNVHREEGTAXgglsb2NhbGhvc3SHBH8AAAGHBAAAAAAwCgYIKoZIzj0E -AwIDSAAwRQIhAPJ4rCN9Jt+eSNZyOy4FK8UUI3OTWAE3dYdGXW+m5cC9AiBBRy5E -v6ak88bHExo6hbaMNfrWsuiFgQqcSlTnQOqGNQ== ------END CERTIFICATE----- diff --git a/cert/key.pem b/cert/key.pem deleted file mode 100644 index b039a5b..0000000 --- a/cert/key.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgRCRp5Jovne1DDTtJ -lSGamrZNrcpG42tEih09TDxD3WuhRANCAARxLOHbYPV/sW6ZlZamzZp7wa3EnqMu -z9aRZj3pxU2YA0Qrku1Qi2pF7gvn1COn1byBqJeSDu4RCOASBgny7lrW ------END PRIVATE KEY----- diff --git a/src/chat/mod.rs b/src/chat/mod.rs new file mode 100644 index 0000000..1f278a4 --- /dev/null +++ b/src/chat/mod.rs @@ -0,0 +1 @@ +pub mod service; diff --git a/src/chat/service.rs b/src/chat/service.rs new file mode 100644 index 0000000..c9d8c14 --- /dev/null +++ b/src/chat/service.rs @@ -0,0 +1,324 @@ +use crate::gossip::message::{ + ChatAckMessage, EncryptedChatMessage, Envelope, GossipMessage, SignedMessage, +}; +use crate::node::node::Node; +use crate::node::node_id::NodeId; +use crate::storage::chat_message::MessageStatus; +use crate::transport::quic::ConnectionManager; +use crate::util::timestamp_now; +use anyhow::{anyhow, Result}; +use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; +use std::sync::Arc; +use tokio::sync::Mutex; +use uuid::Uuid; + +const TTL: u8 = 16; + +pub async fn start_chat_sender_task( + manager: Arc>, + my_node: Node, +) -> Result<()> { + loop { + if let Err(e) = process_pending_messages(manager.clone(), my_node.clone()).await { + tracing::error!("Failed to process pending messages: {}", e); + } + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; + } +} + +async fn process_pending_messages( + manager: Arc>, + my_node: Node, +) -> Result<()> { + // 1. Find all messages with status 'Sending' + let db = crate::storage::get_db_conn().await?; + let pending_msgs = crate::storage::chat_message::Entity::find() + .filter(crate::storage::chat_message::Column::Status.eq(MessageStatus::Sending)) + .all(&db) + .await?; + + for msg in pending_msgs { + tracing::info!("Processing pending message: {}", msg.id); + + let receiver_node_id = match NodeId::from_string(&msg.to) { + Ok(id) => id, + Err(_) => { + tracing::error!("Invalid receiver node id: {}, marking failed", msg.to); + crate::storage::chat_message::update_message_status(&msg.id, MessageStatus::Failed).await?; + continue; + } + }; + + match try_send_pending_msg(manager.clone(), my_node.clone(), receiver_node_id, msg.content.clone(), msg.id.clone()).await { + Ok(_) => { + crate::storage::chat_message::update_message_status(&msg.id, MessageStatus::Sent).await?; + tracing::info!("Message {} sent successfully", msg.id); + }, + Err(e) => { + tracing::error!("Failed to send message {}: {}", msg.id, e); + // We can keep it as 'Sending' to retry later, or make a 'Failed' logic + // For now, retry indefinitely + } + } + } + Ok(()) +} + +async fn try_send_pending_msg( + manager: Arc>, + my_node: Node, + receiver_node_id: NodeId, + content: String, + msg_id: String, +) -> Result<()> { + // 1. Get Receiver Public Key + let receiver_keypair = receiver_node_id.to_keypair() + .map_err(|_| anyhow!("Could not decode receiver NodeId (did:key)"))?; + let receiver_pk = receiver_keypair.verifying_key; + + // 2. Encrypt + let my_keypair = &my_node.keypair; + let encrypted_bytes = my_keypair.encrypt_to_node(&receiver_pk, content.as_bytes())?; + + // 3. Construct Message + let encrypted_chat = EncryptedChatMessage { + sender_id: my_node.node_id().clone(), + receiver_id: receiver_node_id.clone(), + msg_id: msg_id.clone(), + ciphertext: encrypted_bytes, + }; + + let message = GossipMessage::Chat(encrypted_chat); + + // 4. Sign & Broadcast/Send + let mut signed_msg = SignedMessage { + node_id: my_node.node_id().clone(), + message, + timestamp: timestamp_now(), + signature: "".to_string(), + }; + let self_hash = signed_msg.self_hash(); + let sign = my_node.sign_message(self_hash.as_slice())?; + signed_msg.signature = hex::encode(sign); + + let envelope = Envelope { + payload: signed_msg, + ttl: TTL, + }; + let data = serde_json::to_vec(&envelope)?; + + let mgr = manager.lock().await; + + // Try to find if we are connected or have a known route? + // Gossip usually just floods peers if not knowing better. + // If we have direct connection or routing table, use it. + // For now: broadcast to all connected peers. + + let peers = mgr.list_peers().await; + if peers.is_empty() { + return Err(anyhow!("No peers connected to send message")); + } + + if peers.contains(&receiver_node_id) { + let _ = mgr.send_gossip_message(receiver_node_id.clone(), data.clone()).await; + } else { + for peer in peers { + let _ = mgr.send_gossip_message(peer.clone(), data.clone()).await; + } + } + + Ok(()) +} + +pub async fn send_chat_message( + _manager: Arc>, + my_node: Node, + receiver_node_id: NodeId, + content: String, +) -> Result<()> { + // Backward compatibility or direct call wrapper + // Now just saves to DB and calls try_send immediately for responsiveness, or let scheduler handle it? + // Let's make it just save to DB. + + let msg_id = Uuid::new_v4().to_string(); + + crate::storage::chat_message::save_message( + msg_id.clone(), + my_node.node_id().to_string(), + receiver_node_id.to_string(), + content.clone(), + timestamp_now(), + MessageStatus::Sending, + ) + .await?; + + // Maybe trigger one round of processing immediately? + // For now rely on background task. + Ok(()) +} + +pub async fn process_incoming_chat( + msg: EncryptedChatMessage, + manager: Arc>, + my_node: Node, +) -> Result<()> { + // 1. Check if it's for me + if msg.receiver_id != *my_node.node_id() { + tracing::info!("Message not for me, forwarding to {}", msg.receiver_id); + + // Construct the payload to forward + let gossip_msg = GossipMessage::Chat(msg.clone()); + + let mut signed_msg = SignedMessage { + node_id: my_node.node_id().clone(), + message: gossip_msg, + timestamp: timestamp_now(), + signature: "".to_string(), + }; + let self_hash = signed_msg.self_hash(); + let sign = my_node.sign_message(self_hash.as_slice())?; + signed_msg.signature = hex::encode(sign); + + let envelope = Envelope { + payload: signed_msg, + ttl: TTL, + }; + let data = serde_json::to_vec(&envelope)?; + + let mgr = manager.lock().await; + let peers = mgr.list_peers().await; + + if peers.contains(&msg.receiver_id) { + tracing::info!("Found target {} in neighbors, sending directly", msg.receiver_id); + let _ = mgr.send_gossip_message(msg.receiver_id.clone(), data).await; + } else { + tracing::info!("Target {} not in neighbors, broadcasting", msg.receiver_id); + for peer in peers { + if peer != msg.sender_id { + let _ = mgr.send_gossip_message(peer.clone(), data.clone()).await; + } + } + } + + return Ok(()); + } + + // 2. Decrypt + let my_keypair = &my_node.keypair; + let plaintext_bytes = my_keypair.decrypt_message(&msg.ciphertext)?; + let content = String::from_utf8(plaintext_bytes)?; + + tracing::info!("Received Chat from {}: {}", msg.sender_id.0, content); + + // 3. Store + let db = crate::storage::get_db_conn().await.unwrap(); + if (crate::storage::chat_message::Entity::find_by_id(msg.msg_id.clone()).one(&db).await?).is_none() { + crate::storage::chat_message::save_message( + msg.msg_id.clone(), + msg.sender_id.to_string(), + my_node.node_id().to_string(), + content, + timestamp_now(), + MessageStatus::Delivered, + ) + .await?; + } + + // 4. Send ACK + let ack_msg = ChatAckMessage { + sender_id: my_node.node_id().clone(), + target_id: msg.sender_id.clone(), + msg_id: msg.msg_id.clone(), + timestamp: timestamp_now(), + signature: "".to_string(), + }; + + let ack_sig = my_node.sign_message(msg.msg_id.as_bytes())?; + let ack_msg = ChatAckMessage { + signature: hex::encode(ack_sig), + ..ack_msg + }; + + let gossip_msg = GossipMessage::ChatAck(ack_msg); + + let mut signed_ack = SignedMessage { + node_id: my_node.node_id().clone(), + message: gossip_msg, + timestamp: timestamp_now(), + signature: "".to_string(), + }; + let self_hash = signed_ack.self_hash(); + let sign = my_node.sign_message(self_hash.as_slice())?; + signed_ack.signature = hex::encode(sign); + + let envelope = Envelope { + payload: signed_ack, + ttl: TTL, + }; + let data = serde_json::to_vec(&envelope)?; + + let mgr = manager.lock().await; + let peers = mgr.list_peers().await; + for peer in peers { + let _ = mgr.send_gossip_message(peer.clone(), data.clone()).await; + } + + Ok(()) +} + +pub async fn process_ack( + ack: ChatAckMessage, + manager: Arc>, + my_node: Node, +) -> Result<()> { + // 1. Check if it's for me + if ack.target_id != *my_node.node_id() { + tracing::info!("ACK not for me (for {}), forwarding", ack.target_id); + + // Construct the payload to forward + let gossip_msg = GossipMessage::ChatAck(ack.clone()); + + let mut signed_msg = SignedMessage { + node_id: my_node.node_id().clone(), + message: gossip_msg, + timestamp: timestamp_now(), + signature: "".to_string(), + }; + let self_hash = signed_msg.self_hash(); + let sign = my_node.sign_message(self_hash.as_slice())?; + signed_msg.signature = hex::encode(sign); + + let envelope = Envelope { + payload: signed_msg, + ttl: TTL, + }; + let data = serde_json::to_vec(&envelope)?; + + let mgr = manager.lock().await; + let peers = mgr.list_peers().await; + + if peers.contains(&ack.target_id) { + tracing::info!("Found target {} in neighbors, sending ACK directly", ack.target_id); + let _ = mgr.send_gossip_message(ack.target_id.clone(), data).await; + } else { + tracing::info!("Target {} not in neighbors, broadcasting ACK", ack.target_id); + for peer in peers { + if peer != ack.sender_id { + let _ = mgr.send_gossip_message(peer.clone(), data.clone()).await; + } + } + } + return Ok(()); + } + + tracing::info!("Received ACK for msg {}", ack.msg_id); + + crate::storage::chat_message::update_message_status( + &ack.msg_id, + MessageStatus::Delivered, + ) + .await?; + + Ok(()) +} + diff --git a/src/cli/chat.rs b/src/cli/chat.rs new file mode 100644 index 0000000..0c221dc --- /dev/null +++ b/src/cli/chat.rs @@ -0,0 +1,69 @@ +use anyhow::Result; +use clap::Subcommand; +use megaengine::node::node_id::NodeId; +use megaengine::storage::chat_message::{Entity as ChatMessage, MessageStatus}; +use megaengine::util::timestamp_now; +use sea_orm::{EntityTrait, QueryOrder}; +use uuid::Uuid; + +#[derive(Clone, Debug, Subcommand)] +pub enum ChatCommand { + /// Send a message to a node + Send { + /// Target Node ID (did:key:...) + #[arg(long)] + to: String, + /// Message content + #[arg(long)] + msg: String, + }, + /// List messages + List, +} + +pub async fn run_chat_command(cmd: ChatCommand) -> Result<()> { + match cmd { + ChatCommand::Send { to, msg } => { + // Load identity + let keypair = megaengine::storage::load_keypair()?; + let my_node_id = NodeId::from_keypair(&keypair); + + let msg_id = Uuid::new_v4().to_string(); + + // Save to DB (Queue it) + // The background node process (if running) will pick this up and send it. + // If it's not running, it will be sent next time it runs. + megaengine::storage::chat_message::save_message( + msg_id.clone(), + my_node_id.to_string(), + to.clone(), + msg, + timestamp_now(), + MessageStatus::Sending, + ) + .await?; + + println!("Message queued (ID: {}).", msg_id); + println!("It will be delivered automatically when the node service is active."); + } + ChatCommand::List => { + let db = megaengine::storage::get_db_conn().await?; + let messages = ChatMessage::find() + .order_by_desc(megaengine::storage::chat_message::Column::CreatedAt) + .all(&db) + .await?; + + println!("--- Chat History ---"); + for m in messages { + let time = chrono::DateTime::from_timestamp(m.created_at, 0) + .unwrap_or_default() + .format("%Y-%m-%d %H:%M:%S"); + println!( + "[{}] From: {} To: {} : {} ({:?})", + time, m.from, m.to, m.content, m.status + ); + } + } + } + Ok(()) +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 19ea821..62e48c3 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,7 +1,9 @@ pub mod auth; pub mod node; pub mod repo; +pub mod chat; pub use auth::handle_auth; pub use node::handle_node; pub use repo::handle_repo; +pub use chat::run_chat_command as handle_chat; diff --git a/src/cli/node.rs b/src/cli/node.rs index 1d97d86..0cf88b5 100644 --- a/src/cli/node.rs +++ b/src/cli/node.rs @@ -84,6 +84,14 @@ pub async fn handle_node_start( // 启动 Repo 同步后台任务 megaengine::repo::start_repo_sync_task().await; tracing::info!("Repo sync task started"); + + // Start Chat Sender Task + let chat_node = node.clone(); + let chat_mgr = Arc::clone(conn_mgr); + tokio::spawn(async move { + let _ = megaengine::chat::service::start_chat_sender_task(chat_mgr, chat_node).await; + }); + tracing::info!("Chat sender task started"); } else { tracing::warn!("No connection manager found, services not started"); } diff --git a/src/gossip/message.rs b/src/gossip/message.rs index 2130cf2..7b11c39 100644 --- a/src/gossip/message.rs +++ b/src/gossip/message.rs @@ -19,6 +19,33 @@ pub enum GossipMessage { NodeAnnouncement(NodeAnnouncement), /// 仓库公告 (库存公告) RepoAnnouncement(RepoAnnouncement), + /// P2P 聊天消息 + Chat(EncryptedChatMessage), + /// 聊天消息送达确认 + ChatAck(ChatAckMessage), +} + +/// 聊天消息 (加密) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EncryptedChatMessage { + /// 真正的发送者 + pub sender_id: NodeId, + /// 目标接收者 + pub receiver_id: NodeId, + /// 消息 ID (用于去重) + pub msg_id: String, + /// 密文数据 (包含 ephemeral public key) + pub ciphertext: Vec, +} + +/// 聊天回执 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatAckMessage { + pub sender_id: NodeId, + pub target_id: NodeId, + pub msg_id: String, + pub timestamp: i64, + pub signature: String, } /// 节点公告 @@ -31,6 +58,12 @@ pub struct NodeAnnouncement { pub addresses: Vec, } +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Envelope { + pub payload: SignedMessage, + pub ttl: u8, +} + impl From for NodeAnnouncement { fn from(node: Node) -> Self { Self { @@ -133,6 +166,8 @@ impl GossipMessage { match self { GossipMessage::NodeAnnouncement(_) => "node_announcement", GossipMessage::RepoAnnouncement(_) => "inventory_announcement", + GossipMessage::Chat(_) => "chat", + GossipMessage::ChatAck(_ack) => "chat_ack", } } @@ -141,6 +176,8 @@ impl GossipMessage { match self { GossipMessage::NodeAnnouncement(na) => &na.node_id, GossipMessage::RepoAnnouncement(ra) => &ra.node_id, + GossipMessage::Chat(c) => &c.sender_id, + GossipMessage::ChatAck(ack) => &ack.sender_id, } } } diff --git a/src/gossip/service.rs b/src/gossip/service.rs index 0e3da79..ee3a278 100644 --- a/src/gossip/service.rs +++ b/src/gossip/service.rs @@ -1,4 +1,4 @@ -use crate::gossip::message::{GossipMessage, SignedMessage}; +use crate::gossip::message::{Envelope, GossipMessage, SignedMessage}; use crate::node::node::{Node, NodeInfo}; use crate::node::node_id::NodeId; use crate::repo::repo_manager::RepoManager; @@ -7,7 +7,6 @@ use crate::transport::quic::ConnectionManager; use anyhow::Result; use ed25519_dalek::Signature; use hex; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::convert::TryInto; use std::sync::Arc; @@ -25,12 +24,6 @@ pub struct GossipService { seen: Arc>>, } -#[derive(Serialize, Deserialize, Clone, Debug)] -struct Envelope { - payload: SignedMessage, - ttl: u8, -} - impl GossipService { pub fn new( manager: Arc>, @@ -373,17 +366,34 @@ impl GossipService { } } } + GossipMessage::Chat(c) => { + if let Err(e) = crate::chat::service::process_incoming_chat( + c.clone(), + self.manager.clone(), + self.node.clone(), + ) + .await + { + tracing::error!("Error processing chat message: {}", e); + } + } + GossipMessage::ChatAck(ack) => { + if let Err(e) = crate::chat::service::process_ack( + ack.clone(), + self.manager.clone(), + self.node.clone(), + ) + .await + { + tracing::error!("Error processing chat ack: {}", e); + } + } } // forward if ttl > 0 if ttl > 0 { ttl -= 1; - #[derive(Serialize, Deserialize, Clone)] - struct Envelope2 { - payload: SignedMessage, - ttl: u8, - } - let fwd = Envelope2 { + let fwd = Envelope { payload: signed.clone(), ttl, }; diff --git a/src/identity/keypair.rs b/src/identity/keypair.rs index c1ca0ff..8b65f64 100644 --- a/src/identity/keypair.rs +++ b/src/identity/keypair.rs @@ -1,8 +1,16 @@ +#![allow(deprecated)] use anyhow::{anyhow, Result}; +use chacha20poly1305::{ + aead::{Aead, KeyInit}, + ChaCha20Poly1305, Nonce, +}; +use curve25519_dalek::{edwards::CompressedEdwardsY, montgomery::MontgomeryPoint, scalar::Scalar}; use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; use rand_core::OsRng; +use rand_core::RngCore; use serde::Deserialize; use serde::Serialize; +use sha2::{Digest, Sha256}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct KeyPair { @@ -50,6 +58,118 @@ impl KeyPair { self.verifying_key.verify(msg, sig).is_ok() } + /// Encrypt a message for a specific recipient (identified by their Ed25519 VerifyingKey) + /// Returns: Ephemeral_PK (32) + Nonce (12) + Ciphertext (N) + pub fn encrypt_to_node(&self, recipient_vk: &VerifyingKey, message: &[u8]) -> Result> { + let _rng = OsRng; + + // 1. Convert Recipient Ed25519 PK -> X25519 PK (Montgomery) + let recipient_ed_y = CompressedEdwardsY::from_slice(recipient_vk.as_bytes())?; + let recipient_ed_point = recipient_ed_y + .decompress() + .ok_or(anyhow!("Invalid Public Key Point"))?; + let recipient_mont_point = recipient_ed_point.to_montgomery(); + + // 2. Generate Ephemeral Keypair + let mut scalar_bytes = [0u8; 32]; + OsRng.fill_bytes(&mut scalar_bytes); + let ephemeral_scalar = Scalar::from_bytes_mod_order(scalar_bytes); + let ephemeral_point = MontgomeryPoint::mul_base(&ephemeral_scalar); + + // 3. Keep Ephemeral Public Key + let ephemeral_pk_bytes = ephemeral_point.to_bytes(); + + // 4. Calculate Shared Secret: ephemeral_secret * recipient_public + let shared_secret_point = ephemeral_scalar * recipient_mont_point; + let shared_secret_bytes = shared_secret_point.to_bytes(); + + // 5. Derive Encryption Key (Hash) + let mut hasher = Sha256::new(); + hasher.update(&shared_secret_bytes); + hasher.update(&ephemeral_pk_bytes); + hasher.update(recipient_mont_point.to_bytes()); + let key_hash = hasher.finalize(); + + let key = chacha20poly1305::Key::from_slice(&key_hash); + let cipher = ChaCha20Poly1305::new(key); + + // 6. Encrypt + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = chacha20poly1305::Nonce::from_slice(&nonce_bytes); + let ciphertext = cipher + .encrypt(nonce, message) + .map_err(|e| anyhow!("Encryption failed: {}", e))?; + + // 7. Pack: EphemeralPK (32) + Nonce (12) + Ciphertext + let mut result = Vec::with_capacity(32 + 12 + ciphertext.len()); + result.extend_from_slice(&ephemeral_pk_bytes); + result.extend_from_slice(&nonce); + result.extend_from_slice(&ciphertext); + + Ok(result) + } + + /// Decrypt a message addressed to this keypair + pub fn decrypt_message(&self, payload: &[u8]) -> Result> { + if payload.len() < 32 + 12 { + return Err(anyhow!("Message too short")); + } + + let signing_key = self + .signing_key + .as_ref() + .ok_or(anyhow!("No private key available for decryption"))?; + + // 1. My Secret Key Conversion + let mut hasher = sha2::Sha512::new(); + hasher.update(signing_key.as_bytes()); + let h = hasher.finalize(); + + let mut clamped = [0u8; 32]; + clamped.copy_from_slice(&h[0..32]); + clamped[0] &= 248; + clamped[31] &= 127; + clamped[31] |= 64; + + let my_scalar = Scalar::from_bits(clamped); + + // 2. Parse Payload + let ephemeral_pk_bytes = &payload[0..32]; + let nonce_bytes = &payload[32..44]; + let ciphertext = &payload[44..]; + + let ephemeral_point = MontgomeryPoint(ephemeral_pk_bytes.try_into()?); + + // 3. Calculate Shared Secret: my_secret * ephemeral_public + let shared_secret_point = my_scalar * ephemeral_point; + let shared_secret_bytes = shared_secret_point.to_bytes(); + + // 4. Derive Key + let my_ed_y = CompressedEdwardsY::from_slice(self.verifying_key.as_bytes())?; + let my_ed_point = my_ed_y + .decompress() + .ok_or(anyhow!("Invalid My Public Key"))?; + let my_mont_point = my_ed_point.to_montgomery(); + + let mut hasher = Sha256::new(); + hasher.update(&shared_secret_bytes); + hasher.update(ephemeral_pk_bytes); + hasher.update(my_mont_point.to_bytes()); + let key_hash = hasher.finalize(); + + let key = chacha20poly1305::Key::from_slice(&key_hash); + let cipher = ChaCha20Poly1305::new(key); + let nonce = Nonce::from_slice(nonce_bytes); + + // 5. Decrypt + let plaintext = cipher + .decrypt(nonce, ciphertext) + .map_err(|e| anyhow!("Decryption failed: {}", e))?; + + Ok(plaintext) + } + pub fn verifying_key_bytes(&self) -> [u8; 32] { *self.verifying_key.as_bytes() } diff --git a/src/lib.rs b/src/lib.rs index a0b4593..d407846 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod bundle; +pub mod chat; pub mod git; pub mod gossip; pub mod identity; diff --git a/src/main.rs b/src/main.rs index 891420a..bee4339 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,6 +34,11 @@ enum Commands { #[command(subcommand)] action: RepoAction, }, + /// Chat P2P commands + Chat { + #[command(subcommand)] + action: crate::cli::chat::ChatCommand, + }, /// Start MCP server (Stdio mode) Mcp, } @@ -134,6 +139,9 @@ async fn main() -> Result<()> { Commands::Repo { action } => { handle_repo(action).await?; } + Commands::Chat { action } => { + crate::cli::handle_chat(action).await?; + } Commands::Mcp => { start_mcp_server().await?; } diff --git a/src/mcp/mcp_server.rs b/src/mcp/mcp_server.rs index ca13a42..7269213 100644 --- a/src/mcp/mcp_server.rs +++ b/src/mcp/mcp_server.rs @@ -116,6 +116,8 @@ impl RepoMcpServer { "repo_id": repo.repo_id, "name": repo.p2p_description.name, "creator": repo.p2p_description.creator, + "language": repo.p2p_description.language, + "size": repo.p2p_description.size, "description": repo.p2p_description.description, "path": repo.path.display().to_string(), "bundle": repo.bundle.display().to_string(), @@ -137,6 +139,16 @@ impl RepoMcpServer { }) .collect(); repo_info["refs"] = Value::Array(refs); + + let mut has_updates = false; + if !repo.path.as_os_str().is_empty() && repo.path.exists() { + if let Ok(current_refs) = crate::git::git_repo::read_repo_refs( + repo.path.to_str().unwrap_or(""), + ) { + has_updates = current_refs != local_refs; + } + } + repo_info["has_updates"] = Value::Bool(has_updates); } } diff --git a/src/storage/chat_message.rs b/src/storage/chat_message.rs new file mode 100644 index 0000000..64c146d --- /dev/null +++ b/src/storage/chat_message.rs @@ -0,0 +1,67 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] +#[sea_orm(rs_type = "String", db_type = "String(None)")] +pub enum MessageStatus { + #[sea_orm(string_value = "Sending")] + Sending, + #[sea_orm(string_value = "Sent")] + Sent, + #[sea_orm(string_value = "Delivered")] + Delivered, + #[sea_orm(string_value = "Failed")] + Failed, +} + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "chat_messages")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: String, // UUID + pub from: String, // Sender NodeId + pub to: String, // Receiver NodeId + pub content: String, // Plaintext content (local storage is trusted for now) + pub created_at: i64, // Timestamp + pub status: MessageStatus, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +use anyhow::Result; +use sea_orm::{ActiveModelTrait, Set}; + +pub async fn save_message( + id: String, + from: String, + to: String, + content: String, + created_at: i64, + status: MessageStatus, +) -> Result<()> { + let db = crate::storage::get_db_conn().await?; + let model = ActiveModel { + id: Set(id), + from: Set(from), + to: Set(to), + content: Set(content), + created_at: Set(created_at), + status: Set(status), + }; + model.insert(&db).await?; + Ok(()) +} + +pub async fn update_message_status(msg_id: &str, status: MessageStatus) -> Result<()> { + let db = crate::storage::get_db_conn().await?; + let msg = Entity::find_by_id(msg_id).one(&db).await?; + if let Some(m) = msg { + let mut active: ActiveModel = m.into(); + active.status = Set(status); + active.update(&db).await?; + } + Ok(()) +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 84b18ea..956267e 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,3 +1,4 @@ +pub mod chat_message; pub mod node_model; pub mod ref_model; pub mod repo_model; @@ -75,90 +76,114 @@ pub fn db_path() -> PathBuf { } /// 初始化数据库连接并创建表 -pub async fn init_db() -> Result { - static DB: OnceCell = OnceCell::const_new(); +pub async fn get_db_conn() -> Result { + use std::collections::HashMap; + use tokio::sync::Mutex; - // 如果已经初始化,直接返回 clone - if let Some(db) = DB.get() { - return Ok(db.clone()); + static DB_POOL: OnceCell>> = OnceCell::const_new(); + + let pool = DB_POOL + .get_or_init(|| async { Mutex::new(HashMap::new()) }) + .await; + + let path = db_path(); + + { + let map = pool.lock().await; + if let Some(db) = map.get(&path) { + return Ok(db.clone()); + } } // 延迟初始化并缓存全局连接(仅第一次会执行创建表操作) - let db_conn = DB - .get_or_init(|| async { - let db_path = db_path(); - - // 确保目录存在 - if let Some(parent) = db_path.parent() { - fs::create_dir_all(parent).ok(); - } - - // 使用合适的 SQLite URL 格式 - let db_url = format!("sqlite://{}?mode=rwc", db_path.display()); - - let mut opt = ConnectOptions::new(db_url); - opt.max_connections(5) - .min_connections(1) - .connect_timeout(Duration::from_secs(8)); - - let db = Database::connect(opt) - .await - .expect("failed to connect to db"); - - // 运行迁移或创建表(只在初始化时执行) - let _ = db - .execute_unprepared( - "CREATE TABLE IF NOT EXISTS repos ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - creator TEXT NOT NULL, - description TEXT NOT NULL, - language TEXT NOT NULL DEFAULT '', - size INTEGER NOT NULL DEFAULT 0, - latest_commit_at INTEGER NOT NULL DEFAULT 0, - path TEXT NOT NULL, - bundle TEXT NOT NULL DEFAULT '', - is_external INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - )", - ) - .await; - - // 节点表 - let _ = db - .execute_unprepared( - "CREATE TABLE IF NOT EXISTS nodes ( - id TEXT PRIMARY KEY, - alias TEXT NOT NULL, - addresses TEXT NOT NULL, - node_type INTEGER NOT NULL, - version INTEGER NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - )", - ) - .await; - - // Refs 表:存储分支和标签的最新 commit - let _ = db - .execute_unprepared( - "CREATE TABLE IF NOT EXISTS refs ( - id TEXT PRIMARY KEY, - repo_id TEXT NOT NULL, - ref_name TEXT NOT NULL, - commit_hash TEXT NOT NULL, - updated_at INTEGER NOT NULL, - UNIQUE(repo_id, ref_name) ON CONFLICT REPLACE - )", - ) - .await; - - db - }) + let db_path = path.clone(); + + // 确保目录存在 + if let Some(parent) = db_path.parent() { + fs::create_dir_all(parent).ok(); + } + + // 使用合适的 SQLite URL 格式 + let db_url = format!("sqlite://{}?mode=rwc", db_path.display()); + + let mut opt = ConnectOptions::new(db_url); + opt.max_connections(100) + .min_connections(1) + .connect_timeout(Duration::from_secs(8)) + .idle_timeout(Duration::from_secs(8)) + .sqlx_logging(false); + + let db = Database::connect(opt).await?; + + // 运行迁移或创建表(只在初始化时执行) + let _ = db + .execute_unprepared( + "CREATE TABLE IF NOT EXISTS repos ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + creator TEXT NOT NULL, + description TEXT NOT NULL, + language TEXT NOT NULL DEFAULT '', + size INTEGER NOT NULL DEFAULT 0, + latest_commit_at INTEGER NOT NULL DEFAULT 0, + path TEXT NOT NULL, + bundle TEXT NOT NULL DEFAULT '', + is_external INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )", + ) .await; - Ok(db_conn.clone()) + // 节点表 + let _ = db + .execute_unprepared( + "CREATE TABLE IF NOT EXISTS nodes ( + id TEXT PRIMARY KEY, + alias TEXT NOT NULL, + addresses TEXT NOT NULL, + node_type INTEGER NOT NULL, + version INTEGER NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )", + ) + .await; + + // Refs 表 + let _ = db + .execute_unprepared( + "CREATE TABLE IF NOT EXISTS refs ( + repo_id TEXT NOT NULL, + ref_name TEXT NOT NULL, + commit_hash TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (repo_id, ref_name) + )", + ) + .await; + + // Chat Messages 表 + let _ = db + .execute_unprepared( + "CREATE TABLE IF NOT EXISTS chat_messages ( + id TEXT PRIMARY KEY, + \"from\" TEXT NOT NULL, + \"to\" TEXT NOT NULL, + content TEXT NOT NULL, + created_at INTEGER NOT NULL, + status TEXT NOT NULL + )", + ) + .await; + + { + let mut map = pool.lock().await; + map.insert(path, db.clone()); + } + + Ok(db) } /// 保存密钥对到文件(JSON) diff --git a/src/storage/node_model.rs b/src/storage/node_model.rs index 854d172..7cd8d54 100644 --- a/src/storage/node_model.rs +++ b/src/storage/node_model.rs @@ -26,7 +26,7 @@ impl ActiveModelBehavior for ActiveModel {} /// 将 NodeInfo 保存到数据库 pub async fn save_node_info_to_db(info: &NodeInfo) -> Result<()> { - let db = crate::storage::init_db().await?; + let db = crate::storage::get_db_conn().await?; let addresses_json = serde_json::to_string(&info.addresses)?; let now = chrono::Local::now().timestamp(); @@ -57,7 +57,7 @@ pub async fn save_node_info_to_db(info: &NodeInfo) -> Result<()> { /// 从数据库加载 NodeInfo pub async fn load_node_info_from_db(node_id: &str) -> Result> { - let db = crate::storage::init_db().await?; + let db = crate::storage::get_db_conn().await?; if let Some(m) = Entity::find_by_id(node_id).one(&db).await? { let addresses: Vec = serde_json::from_str(&m.addresses)?; @@ -82,14 +82,14 @@ pub async fn load_node_info_from_db(node_id: &str) -> Result> { /// 删除节点记录 pub async fn delete_node_from_db(node_id: &str) -> Result<()> { - let db = crate::storage::init_db().await?; + let db = crate::storage::get_db_conn().await?; Entity::delete_by_id(node_id).exec(&db).await?; Ok(()) } /// 列出所有节点 pub async fn list_nodes() -> Result> { - let db = crate::storage::init_db().await?; + let db = crate::storage::get_db_conn().await?; let models = Entity::find().all(&db).await?; let mut out = Vec::new(); diff --git a/src/storage/ref_model.rs b/src/storage/ref_model.rs index 15fbce1..d833b61 100644 --- a/src/storage/ref_model.rs +++ b/src/storage/ref_model.rs @@ -2,7 +2,7 @@ use anyhow::Result; use sea_orm::entity::prelude::*; use sea_orm::{Set, Unchanged}; -use crate::storage::init_db; +use crate::storage::get_db_conn; /// Refs table entity for tracking branch and tag commits #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] @@ -23,7 +23,7 @@ impl ActiveModelBehavior for ActiveModel {} /// Save or update a ref in the database pub async fn save_ref(repo_id: &str, ref_name: &str, commit_hash: &str) -> Result<()> { - let db = init_db().await?; + let db = get_db_conn().await?; let now = chrono::Local::now().timestamp(); // Generate a unique ID for this ref record (repo_id + ref_name) @@ -62,7 +62,7 @@ pub async fn batch_save_refs( repo_id: &str, refs: &std::collections::HashMap, ) -> Result<()> { - let db = init_db().await?; + let db = get_db_conn().await?; let now = chrono::Local::now().timestamp(); for (ref_name, commit_hash) in refs { @@ -98,7 +98,7 @@ pub async fn batch_save_refs( pub async fn load_refs_for_repo( repo_id: &str, ) -> Result> { - let db = init_db().await?; + let db = get_db_conn().await?; let refs = Entity::find() .filter(Column::RepoId.eq(repo_id)) @@ -115,7 +115,7 @@ pub async fn load_refs_for_repo( /// Get a specific ref by repo_id and ref_name pub async fn get_ref(repo_id: &str, ref_name: &str) -> Result> { - let db = init_db().await?; + let db = get_db_conn().await?; let id = format!("{}:{}", repo_id, ref_name); if let Some(model) = Entity::find_by_id(id).one(&db).await? { @@ -127,7 +127,7 @@ pub async fn get_ref(repo_id: &str, ref_name: &str) -> Result> { /// Delete all refs for a repository pub async fn delete_refs_for_repo(repo_id: &str) -> Result<()> { - let db = init_db().await?; + let db = get_db_conn().await?; Entity::delete_many() .filter(Column::RepoId.eq(repo_id)) .exec(&db) @@ -137,7 +137,7 @@ pub async fn delete_refs_for_repo(repo_id: &str) -> Result<()> { /// Delete a specific ref pub async fn delete_ref(repo_id: &str, ref_name: &str) -> Result<()> { - let db = init_db().await?; + let db = get_db_conn().await?; let id = format!("{}:{}", repo_id, ref_name); Entity::delete_by_id(id).exec(&db).await?; Ok(()) @@ -148,7 +148,7 @@ pub async fn has_refs_changed( repo_id: &str, old_refs: &std::collections::HashMap, ) -> Result { - let db = init_db().await?; + let db = get_db_conn().await?; let current_refs = Entity::find() .filter(Column::RepoId.eq(repo_id)) diff --git a/src/storage/repo_model.rs b/src/storage/repo_model.rs index ff69e36..5b58722 100644 --- a/src/storage/repo_model.rs +++ b/src/storage/repo_model.rs @@ -4,7 +4,7 @@ use anyhow::Result; use sea_orm::entity::prelude::*; use sea_orm::{Set, Unchanged}; -use crate::{repo::repo::Repo, storage::init_db}; +use crate::{repo::repo::Repo, storage::get_db_conn}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[sea_orm(table_name = "repos")] @@ -31,7 +31,7 @@ impl ActiveModelBehavior for ActiveModel {} /// 保存或更新 Repo 到数据库 pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { - let db = init_db().await?; + let db = get_db_conn().await?; let now = chrono::Local::now().timestamp(); // 查询是否已存在 @@ -81,7 +81,7 @@ pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { /// 从数据库加载 Repo pub async fn load_repo_from_db(repo_id: &str) -> Result> { - let db = init_db().await?; + let db = get_db_conn().await?; // 使用 find_by_id 直接查询 if let Some(model) = Entity::find_by_id(repo_id).one(&db).await? { @@ -111,7 +111,7 @@ pub async fn load_repo_from_db(repo_id: &str) -> Result> { /// 删除 Repo 从数据库 pub async fn delete_repo_from_db(repo_id: &str) -> Result<()> { - let db = init_db().await?; + let db = get_db_conn().await?; Entity::delete_by_id(repo_id).exec(&db).await?; // Delete associated refs crate::storage::ref_model::delete_refs_for_repo(repo_id).await?; @@ -120,7 +120,7 @@ pub async fn delete_repo_from_db(repo_id: &str) -> Result<()> { /// 列出所有 Repos pub async fn list_repos() -> Result> { - let db = init_db().await?; + let db = get_db_conn().await?; let models = Entity::find().all(&db).await?; let mut repos = Vec::new(); @@ -149,7 +149,7 @@ pub async fn list_repos() -> Result> { /// 更新 Repo 的 bundle 路径 pub async fn update_repo_bundle(repo_id: &str, bundle_path: &str) -> Result<()> { - let db = init_db().await?; + let db = get_db_conn().await?; // 查询是否存在 if let Some(model) = Entity::find_by_id(repo_id).one(&db).await? { diff --git a/src/transport/cert.rs b/src/transport/cert.rs index d721680..7ce7dcf 100644 --- a/src/transport/cert.rs +++ b/src/transport/cert.rs @@ -1,10 +1,41 @@ use anyhow::{anyhow, Result}; -use rcgen::{Certificate, CertificateParams, DistinguishedName, DnType, KeyPair}; +use libvault::utils::cert::Certificate; +use openssl::pkey::{PKey, Private}; +use openssl::x509::X509; use std::fs; use std::path::Path; +use std::time::{Duration, SystemTime}; + +fn build_ca_certificate() -> Result { + let mut ca = Certificate { + is_ca: true, + key_type: "rsa".to_string(), + key_bits: 2048, + not_after: SystemTime::now() + Duration::from_secs(60 * 60 * 24 * 3650), + ..Default::default() + }; + + ca.to_cert_bundle(None, None) + .map_err(|e| anyhow!("Failed to generate CA certificate: {}", e)) +} + +fn build_server_certificate(ca_cert: &X509, ca_key: &PKey) -> Result { + let mut cert = Certificate { + dns_sans: vec!["localhost".to_string()], + ip_sans: vec!["127.0.0.1".to_string(), "0.0.0.0".to_string()], + is_ca: false, + key_type: "rsa".to_string(), + key_bits: 2048, + not_after: SystemTime::now() + Duration::from_secs(60 * 60 * 24 * 3650), + ..Default::default() + }; + + cert.to_cert_bundle(Some(ca_cert), Some(ca_key)) + .map_err(|e| anyhow!("Failed to generate server certificate: {}", e)) +} /// Generate a CA certificate and save to files. -pub fn generate_ca_cert(ca_cert_path: &str, ca_key_path: &str) -> Result { +pub fn generate_ca_cert(ca_cert_path: &str, ca_key_path: &str) -> Result<()> { // Check if CA certificate already exists if Path::new(ca_cert_path).exists() && Path::new(ca_key_path).exists() { tracing::info!( @@ -12,9 +43,7 @@ pub fn generate_ca_cert(ca_cert_path: &str, ca_key_path: &str) -> Result Result Result<()> { // Check if certificate already exists @@ -88,44 +97,21 @@ pub fn generate_server_cert( tracing::info!("Generating server certificate signed by CA..."); // Read CA key - let ca_key_pem = fs::read_to_string(ca_key_path)?; + let ca_key_pem = fs::read(ca_key_path)?; // Parse CA key - let ca_keypair = - KeyPair::from_pem(&ca_key_pem).map_err(|e| anyhow!("Failed to parse CA key: {}", e))?; - - // Generate server keypair - let server_keypair = - KeyPair::generate().map_err(|e| anyhow!("Failed to generate server keypair: {}", e))?; - - // Create server certificate parameters with SANs - let mut params = CertificateParams::new(vec![ - "localhost".to_string(), - "127.0.0.1".to_string(), - "0.0.0.0".to_string(), - ]) - .map_err(|e| anyhow!("Failed to create server certificate params: {}", e))?; - - // Set server subject name - let mut dn = DistinguishedName::new(); - dn.push(DnType::CommonName, "localhost"); - dn.push(DnType::OrganizationName, "MegaEngine"); - dn.push(DnType::CountryName, "CN"); - params.distinguished_name = dn; - - // Sign server certificate with CA key - // signed_by expects (server_keypair, ca_cert_obj, ca_keypair) - let server_cert = params - .signed_by(&server_keypair, ca_cert_obj, &ca_keypair) - .map_err(|e| anyhow!("Failed to generate server certificate: {}", e))?; + let ca_key = PKey::private_key_from_pem(&ca_key_pem) + .map_err(|e| anyhow!("Failed to parse CA key: {}", e))?; + + let server_cert = build_server_certificate(ca_cert_obj, &ca_key)?; // Save server certificate - let cert_pem = server_cert.pem(); + let cert_pem = server_cert.certificate.to_pem()?; fs::write(cert_path, cert_pem)?; tracing::info!("Server certificate written to {}", cert_path); // Save server private key - let key_pem = server_keypair.serialize_pem(); + let key_pem = server_cert.private_key.private_key_to_pem_pkcs8()?; fs::write(key_path, key_pem)?; tracing::info!("Server private key written to {}", key_path); @@ -148,24 +134,10 @@ pub fn ensure_certificates(cert_path: &str, key_path: &str, ca_cert_path: &str) } // Generate CA certificate if needed (only once) - let ca_cert = match generate_ca_cert(ca_cert_path, &ca_key_path) { - Ok(cert) => cert, - Err(_) => { - // CA already exists - need to reconstruct it from files for signing - tracing::info!("CA certificate exists, reconstructing from files"); - - let ca_key_pem = fs::read_to_string(&ca_key_path)?; - let keypair = KeyPair::from_pem(&ca_key_pem)?; - let mut params = CertificateParams::new(vec![])?; - params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); - let mut dn = DistinguishedName::new(); - dn.push(DnType::CommonName, "MegaEngine CA"); - dn.push(DnType::OrganizationName, "MegaEngine"); - dn.push(DnType::CountryName, "CN"); - params.distinguished_name = dn; - params.self_signed(&keypair)? - } - }; + generate_ca_cert(ca_cert_path, &ca_key_path)?; + + let ca_cert_pem = fs::read(ca_cert_path)?; + let ca_cert = X509::from_pem(&ca_cert_pem).map_err(|e| anyhow!("Failed to parse CA cert: {}", e))?; // Generate server certificate signed by CA // If server cert and key don't both exist, regenerate them @@ -175,3 +147,123 @@ pub fn ensure_certificates(cert_path: &str, key_path: &str, ca_cert_path: &str) Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn test_dir(prefix: &str) -> std::path::PathBuf { + let ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time before unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!("megaengine-{}-{}", prefix, ts)) + } + + #[test] + fn test_ensure_certificates_generates_files() { + let dir = test_dir("cert-generate"); + std::fs::create_dir_all(&dir).expect("create temp dir"); + + let cert_path = dir.join("cert.pem"); + let key_path = dir.join("key.pem"); + let ca_cert_path = dir.join("ca-cert.pem"); + let ca_key_path = dir.join("ca-cert-key.pem"); + + let result = ensure_certificates( + cert_path.to_str().expect("cert path utf8"), + key_path.to_str().expect("key path utf8"), + ca_cert_path.to_str().expect("ca cert path utf8"), + ); + + assert!(result.is_ok()); + assert!(cert_path.exists()); + assert!(key_path.exists()); + assert!(ca_cert_path.exists()); + assert!(ca_key_path.exists()); + + let cert_pem = std::fs::read(&cert_path).expect("read cert pem"); + let key_pem = std::fs::read(&key_path).expect("read key pem"); + let ca_cert_pem = std::fs::read(&ca_cert_path).expect("read ca cert pem"); + + assert!(X509::from_pem(&cert_pem).is_ok()); + assert!(PKey::private_key_from_pem(&key_pem).is_ok()); + assert!(X509::from_pem(&ca_cert_pem).is_ok()); + + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn test_ensure_certificates_is_idempotent_when_files_exist() { + let dir = test_dir("cert-idempotent"); + std::fs::create_dir_all(&dir).expect("create temp dir"); + + let cert_path = dir.join("cert.pem"); + let key_path = dir.join("key.pem"); + let ca_cert_path = dir.join("ca-cert.pem"); + + ensure_certificates( + cert_path.to_str().expect("cert path utf8"), + key_path.to_str().expect("key path utf8"), + ca_cert_path.to_str().expect("ca cert path utf8"), + ) + .expect("first ensure"); + + let cert_before = std::fs::read(&cert_path).expect("read cert before"); + let key_before = std::fs::read(&key_path).expect("read key before"); + + ensure_certificates( + cert_path.to_str().expect("cert path utf8"), + key_path.to_str().expect("key path utf8"), + ca_cert_path.to_str().expect("ca cert path utf8"), + ) + .expect("second ensure"); + + let cert_after = std::fs::read(&cert_path).expect("read cert after"); + let key_after = std::fs::read(&key_path).expect("read key after"); + + assert_eq!(cert_before, cert_after); + assert_eq!(key_before, key_after); + + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn test_ensure_certificates_recovers_from_missing_key() { + let dir = test_dir("cert-recover"); + std::fs::create_dir_all(&dir).expect("create temp dir"); + + let cert_path = dir.join("cert.pem"); + let key_path = dir.join("key.pem"); + let ca_cert_path = dir.join("ca-cert.pem"); + + ensure_certificates( + cert_path.to_str().expect("cert path utf8"), + key_path.to_str().expect("key path utf8"), + ca_cert_path.to_str().expect("ca cert path utf8"), + ) + .expect("first ensure"); + + std::fs::remove_file(&key_path).expect("remove key file"); + assert!(cert_path.exists()); + assert!(!key_path.exists()); + + ensure_certificates( + cert_path.to_str().expect("cert path utf8"), + key_path.to_str().expect("key path utf8"), + ca_cert_path.to_str().expect("ca cert path utf8"), + ) + .expect("recovery ensure"); + + assert!(cert_path.exists()); + assert!(key_path.exists()); + + let cert_pem = std::fs::read(&cert_path).expect("read cert pem"); + let key_pem = std::fs::read(&key_path).expect("read key pem"); + assert!(X509::from_pem(&cert_pem).is_ok()); + assert!(PKey::private_key_from_pem(&key_pem).is_ok()); + + let _ = std::fs::remove_dir_all(&dir); + } +} diff --git a/src/transport/config.rs b/src/transport/config.rs index 97e336b..78e96c7 100644 --- a/src/transport/config.rs +++ b/src/transport/config.rs @@ -47,11 +47,12 @@ impl rustls::client::danger::ServerCertVerifier for NoServerCertificateVerificat } fn supported_verify_schemes(&self) -> Vec { - vec![ - rustls::SignatureScheme::RSA_PKCS1_SHA256, - rustls::SignatureScheme::ECDSA_NISTP256_SHA256, - rustls::SignatureScheme::ED25519, - ] + let provider = rustls::crypto::CryptoProvider::get_default() + .cloned() + .unwrap_or(Arc::new(rustls::crypto::ring::default_provider())); + provider + .signature_verification_algorithms + .supported_schemes() } } diff --git a/src/transport/quic.rs b/src/transport/quic.rs index 7c05059..7603c80 100644 --- a/src/transport/quic.rs +++ b/src/transport/quic.rs @@ -396,6 +396,29 @@ mod tests { }); } + fn cleanup_test_certs() { + let files = [ + "cert/cert.pem", + "cert/key.pem", + "cert/cert2.pem", + "cert/key2.pem", + "cert/ca-cert.pem", + "cert/ca-cert-key.pem", + "cert/no-shared-ca-cert1.pem", + "cert/no-shared-ca-key1.pem", + "cert/no-shared-ca-cert2.pem", + "cert/no-shared-ca-key2.pem", + "cert/no-shared-ca1.pem", + "cert/no-shared-ca1-key.pem", + "cert/no-shared-ca2.pem", + "cert/no-shared-ca2-key.pem", + ]; + + for file in files { + let _ = std::fs::remove_file(file); + } + } + // Mock configuration for the tests fn mock_quic_config() -> QuicConfig { // tracing subscriber may only be initialized once per process; ignore error if already set. @@ -437,10 +460,41 @@ mod tests { ) } + fn mock_quic_config_no_shared_ca_1() -> QuicConfig { + let _ = crate::transport::cert::ensure_certificates( + "cert/no-shared-ca-cert1.pem", + "cert/no-shared-ca-key1.pem", + "cert/no-shared-ca1.pem", + ); + + QuicConfig::new( + "0.0.0.0:0".parse().unwrap(), + "cert/no-shared-ca-cert1.pem".to_string(), + "cert/no-shared-ca-key1.pem".to_string(), + "cert/no-shared-ca1.pem".to_string(), + ) + } + + fn mock_quic_config_no_shared_ca_2() -> QuicConfig { + let _ = crate::transport::cert::ensure_certificates( + "cert/no-shared-ca-cert2.pem", + "cert/no-shared-ca-key2.pem", + "cert/no-shared-ca2.pem", + ); + + QuicConfig::new( + "0.0.0.0:0".parse().unwrap(), + "cert/no-shared-ca-cert2.pem".to_string(), + "cert/no-shared-ca-key2.pem".to_string(), + "cert/no-shared-ca2.pem".to_string(), + ) + } + // Test the `server` method #[tokio::test] async fn test_server_creation() { init(); + cleanup_test_certs(); let config = mock_quic_config(); let manager = ConnectionManager::run_server(config).await; @@ -449,12 +503,14 @@ mod tests { tokio::time::sleep(Duration::from_millis(500)).await; let quic_transport = manager.unwrap(); assert!(quic_transport.connections.lock().await.is_empty()); + cleanup_test_certs(); } // Test the `connect` method #[tokio::test] async fn test_client_connection() { init(); + cleanup_test_certs(); let keypair1 = KeyPair::generate().expect("generate keypair"); let keypair2 = KeyPair::generate().expect("generate keypair"); @@ -508,11 +564,13 @@ mod tests { assert!(connections1.contains_key(&node2.node_id().clone())); assert!(connections2.contains_key(&node1.node_id().clone())); + cleanup_test_certs(); } #[tokio::test] async fn test_send_message() { init(); + cleanup_test_certs(); let keypair1 = KeyPair::generate().expect("generate keypair"); let keypair2 = KeyPair::generate().expect("generate keypair"); @@ -569,5 +627,63 @@ mod tests { .await .unwrap(); tokio::time::sleep(Duration::from_millis(500)).await; + cleanup_test_certs(); + } + + #[tokio::test] + async fn test_client_connection_without_shared_ca() { + init(); + cleanup_test_certs(); + let keypair1 = KeyPair::generate().expect("generate keypair"); + let keypair2 = KeyPair::generate().expect("generate keypair"); + + let config1 = mock_quic_config_no_shared_ca_1(); + let manager1 = ConnectionManager::run_server(config1).await; + assert!(manager1.is_ok()); + let manager1 = manager1.unwrap(); + tokio::time::sleep(Duration::from_millis(200)).await; + + let addr1 = manager1.endpoint.local_addr().expect("get local addr"); + let addr1 = format!("127.0.0.1:{}", addr1.port()).parse().unwrap(); + let node1 = Node::new( + NodeId::from_keypair(&keypair1), + "", + vec![addr1], + NodeType::Normal, + keypair1.clone(), + ); + + let config2 = mock_quic_config_no_shared_ca_2(); + let manager2 = ConnectionManager::run_server(config2).await; + assert!(manager2.is_ok()); + let manager2 = manager2.unwrap(); + tokio::time::sleep(Duration::from_millis(200)).await; + + let addr2 = manager2.endpoint.local_addr().expect("get local addr"); + let addr2 = format!("127.0.0.1:{}", addr2.port()).parse().unwrap(); + let node2 = Node::new( + NodeId::from_keypair(&keypair2), + "", + vec![addr2], + NodeType::Normal, + keypair2.clone(), + ); + + let result = manager2 + .connect( + node2.node_id().clone(), + node1.node_id().clone(), + node1.addresses().to_vec(), + ) + .await; + assert!(result.is_ok()); + + tokio::time::sleep(Duration::from_millis(500)).await; + + let connections1 = manager1.connections.lock().await; + let connections2 = manager2.connections.lock().await; + assert!(connections1.contains_key(&node2.node_id().clone())); + assert!(connections2.contains_key(&node1.node_id().clone())); + cleanup_test_certs(); } } From 89a3c784f2a10331cbc798512339d3c75d9dab22 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Fri, 13 Mar 2026 11:48:38 +0800 Subject: [PATCH 33/42] update ignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fa768ae..a8bf005 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,7 @@ target .vscode/ -# cert/ +cert/ .megaengine/ tmp/* dist/ \ No newline at end of file From 41aed54b0c8c1527f45e41e4bd808baa86fbe29a Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Fri, 13 Mar 2026 15:38:10 +0800 Subject: [PATCH 34/42] fix clippy --- src/bundle/transfer.rs | 1 + src/cli/repo.rs | 8 ++-- src/git/git_repo.rs | 16 ++++---- src/gossip/service.rs | 77 ------------------------------------- src/identity/keypair.rs | 10 ++--- src/main.rs | 8 ---- src/node/node.rs | 4 +- src/node/node_id.rs | 2 +- src/storage/mod.rs | 1 - src/storage/ref_model.rs | 4 +- src/storage/repo_model.rs | 9 ----- src/transport/quic.rs | 11 +++++- tests/bundle_two_nodes.rs | 14 +++---- tests/git_pack.rs | 6 +-- tests/gossip_three_nodes.rs | 1 - 15 files changed, 40 insertions(+), 132 deletions(-) diff --git a/src/bundle/transfer.rs b/src/bundle/transfer.rs index 6728419..7cf0649 100644 --- a/src/bundle/transfer.rs +++ b/src/bundle/transfer.rs @@ -304,6 +304,7 @@ impl BundleTransferManager { let mut file = fs::OpenOptions::new() .create(true) .write(true) + .truncate(false) .open(&file_path) .await .context("Failed to open bundle file")?; diff --git a/src/cli/repo.rs b/src/cli/repo.rs index a79864d..94cafdd 100644 --- a/src/cli/repo.rs +++ b/src/cli/repo.rs @@ -122,11 +122,9 @@ fn detect_language(path: &str) -> String { if stack.len() < 50 { stack.push(path); } - } else { - if let Some(ext) = path.extension().and_then(|s| s.to_str()) { - *ext_counts.entry(ext.to_lowercase()).or_insert(0) += 1; - files_scanned += 1; - } + } else if let Some(ext) = path.extension().and_then(|s| s.to_str()) { + *ext_counts.entry(ext.to_lowercase()).or_insert(0) += 1; + files_scanned += 1; } } } diff --git a/src/git/git_repo.rs b/src/git/git_repo.rs index e5d0e07..73609d4 100644 --- a/src/git/git_repo.rs +++ b/src/git/git_repo.rs @@ -50,15 +50,13 @@ pub fn read_repo_refs(path: &str) -> Result format!("refs/heads/{}", name), - BranchType::Remote => format!("refs/remotes/{}", name), - }; - refs.insert(ref_name, oid.to_string()); - } + if let Ok(Some(name)) = branch.name() { + if let Some(oid) = branch.get().target() { + let ref_name = match branch_type { + BranchType::Local => format!("refs/heads/{}", name), + BranchType::Remote => format!("refs/remotes/{}", name), + }; + refs.insert(ref_name, oid.to_string()); } } } diff --git a/src/gossip/service.rs b/src/gossip/service.rs index 107f01b..f181287 100644 --- a/src/gossip/service.rs +++ b/src/gossip/service.rs @@ -270,83 +270,6 @@ impl GossipService { tracing::debug!("Repo {} refs are up-to-date", &repo.repo_id); continue; } - // 检查仓库是否已存在 - match crate::storage::repo_model::load_repo_from_db(&repo.repo_id).await { - Ok(Some(local_repo)) => { - // 如果是本地仓库,不更新 - if !local_repo.is_external { - tracing::debug!( - "Repo {} is a local repository, skipping update", - &repo.repo_id - ); - continue; - } - - // Repo 已存在,检查是否需要更新 - tracing::debug!( - "Repo {} already exists, checking if update needed", - &repo.repo_id - ); - - // 比较 refs:从 bundle 中提取本地 refs - let local_refs = if !local_repo.bundle.as_os_str().is_empty() { - // Bundle 存在,从 bundle 中提取 refs - let bundle_path = local_repo.bundle.to_string_lossy().to_string(); - match crate::git::pack::extract_bundle_refs(&bundle_path) { - Ok(refs) => { - tracing::debug!( - "Extracted {} refs from bundle for repo {}", - refs.len(), - &repo.repo_id - ); - refs - } - Err(e) => { - tracing::warn!( - "Failed to extract refs from bundle for repo {}: {}", - &repo.repo_id, - e - ); - // 如果 bundle 提取失败,从数据库读取 refs 作为备份 - match crate::storage::ref_model::load_refs_for_repo( - &repo.repo_id, - ) - .await - { - Ok(refs) => refs, - Err(e2) => { - tracing::warn!( - "Failed to load local refs for repo {}: {}", - &repo.repo_id, - e2 - ); - continue; - } - } - } - } - } else { - // Bundle 不存在,从数据库读取 refs - match crate::storage::ref_model::load_refs_for_repo(&repo.repo_id) - .await - { - Ok(refs) => refs, - Err(e) => { - tracing::warn!( - "Failed to load local refs for repo {}: {}", - &repo.repo_id, - e - ); - continue; - } - } - }; - - // 检查 2:如果远端 refs 与本地相同,不更新 - if local_refs == repo.refs { - tracing::debug!("Repo {} refs are up-to-date", &repo.repo_id); - continue; - } // 有新的 refs 更新,清空 bundle 等待重新同步 tracing::info!( diff --git a/src/identity/keypair.rs b/src/identity/keypair.rs index 8b65f64..99715b6 100644 --- a/src/identity/keypair.rs +++ b/src/identity/keypair.rs @@ -85,8 +85,8 @@ impl KeyPair { // 5. Derive Encryption Key (Hash) let mut hasher = Sha256::new(); - hasher.update(&shared_secret_bytes); - hasher.update(&ephemeral_pk_bytes); + hasher.update(shared_secret_bytes); + hasher.update(ephemeral_pk_bytes); hasher.update(recipient_mont_point.to_bytes()); let key_hash = hasher.finalize(); @@ -104,7 +104,7 @@ impl KeyPair { // 7. Pack: EphemeralPK (32) + Nonce (12) + Ciphertext let mut result = Vec::with_capacity(32 + 12 + ciphertext.len()); result.extend_from_slice(&ephemeral_pk_bytes); - result.extend_from_slice(&nonce); + result.extend_from_slice(nonce); result.extend_from_slice(&ciphertext); Ok(result) @@ -153,7 +153,7 @@ impl KeyPair { let my_mont_point = my_ed_point.to_montgomery(); let mut hasher = Sha256::new(); - hasher.update(&shared_secret_bytes); + hasher.update(shared_secret_bytes); hasher.update(ephemeral_pk_bytes); hasher.update(my_mont_point.to_bytes()); let key_hash = hasher.finalize(); @@ -211,7 +211,7 @@ mod tests { #[test] fn test_export_and_import_verifying_key() { let kp1 = KeyPair::generate().unwrap(); - let vk_bytes = kp1.verifying_key.as_bytes().clone(); + let vk_bytes = *kp1.verifying_key.as_bytes(); let kp2 = KeyPair::from_verifying_key_bytes(vk_bytes).unwrap(); let msg = b"verify test"; diff --git a/src/main.rs b/src/main.rs index 7a31965..bee4339 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,12 +99,6 @@ enum RepoAction { #[arg(long)] repo_id: String, }, - /// Update repository from bundle (like git pull) - Pull { - /// Repository ID - #[arg(long)] - repo_id: String, - }, Clone { #[arg(long)] output: String, @@ -121,7 +115,6 @@ async fn main() -> Result<()> { // 初始化 tracing 日志 let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("error,megaengine=debug")); - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("error,megaengine=debug")); tracing_subscriber::fmt() .with_env_filter(env_filter) @@ -132,7 +125,6 @@ async fn main() -> Result<()> { let cli = Cli::parse(); - let root_path = resolve_root_path(&cli.root)?; let root_path = resolve_root_path(&cli.root)?; match cli.command { diff --git a/src/node/node.rs b/src/node/node.rs index 665298c..67fea1a 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -227,12 +227,12 @@ mod tests { // Test expiration logic std::thread::sleep(Duration::from_secs(2)); // Sleep for 2 seconds to test expiration - assert_eq!(node_routing.expired(), false); // Not expired if TTL is 24 hours + assert!(!node_routing.expired()); // Not expired if TTL is 24 hours // Manually expire the node and check node_routing.ttl = Duration::from_secs(1); // Set TTL to 1 second std::thread::sleep(Duration::from_secs(2)); // Sleep for 2 seconds to make the node expire - assert_eq!(node_routing.expired(), true); // Should be expired now + assert!(node_routing.expired()); // Should be expired now } // Test the `NodeType` enum diff --git a/src/node/node_id.rs b/src/node/node_id.rs index 87d21b0..3ed67a6 100644 --- a/src/node/node_id.rs +++ b/src/node/node_id.rs @@ -124,7 +124,7 @@ mod tests { #[test] fn test_valid_from_string() -> Result<()> { let node_id_str = "did:key:z2DXbAovGq5vNKpXVFyrhVLppMdUCmV1hCNjbUydLMEWasE"; - let node_id = NodeId::from_string(&node_id_str)?; + let node_id = NodeId::from_string(node_id_str)?; assert_eq!(node_id.0, node_id_str); Ok(()) } diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 68625f6..956267e 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,7 +1,6 @@ pub mod chat_message; pub mod node_model; pub mod ref_model; -pub mod ref_model; pub mod repo_model; use anyhow::Result; diff --git a/src/storage/ref_model.rs b/src/storage/ref_model.rs index d833b61..a0b765e 100644 --- a/src/storage/ref_model.rs +++ b/src/storage/ref_model.rs @@ -32,7 +32,7 @@ pub async fn save_ref(repo_id: &str, ref_name: &str, commit_hash: &str) -> Resul // Check if ref already exists let existing = Entity::find_by_id(id.clone()).one(&db).await?; - if let Some(_) = existing { + if existing.is_some() { // Update existing ref let active_model = ActiveModel { id: Unchanged(id), @@ -70,7 +70,7 @@ pub async fn batch_save_refs( let existing = Entity::find_by_id(id.clone()).one(&db).await?; - if let Some(_) = existing { + if existing.is_some() { let active_model = ActiveModel { id: Unchanged(id), repo_id: Unchanged(repo_id.to_string()), diff --git a/src/storage/repo_model.rs b/src/storage/repo_model.rs index 7cc5d40..5b58722 100644 --- a/src/storage/repo_model.rs +++ b/src/storage/repo_model.rs @@ -76,9 +76,6 @@ pub async fn save_repo_to_db(repo: &Repo) -> Result<()> { // 保存 refs 到 refs 表 crate::storage::ref_model::batch_save_refs(&repo.repo_id, &repo.refs).await?; - // 保存 refs 到 refs 表 - crate::storage::ref_model::batch_save_refs(&repo.repo_id, &repo.refs).await?; - Ok(()) } @@ -90,8 +87,6 @@ pub async fn load_repo_from_db(repo_id: &str) -> Result> { if let Some(model) = Entity::find_by_id(repo_id).one(&db).await? { // Load refs from ref_model table let refs = crate::storage::ref_model::load_refs_for_repo(&model.id).await?; - // Load refs from ref_model table - let refs = crate::storage::ref_model::load_refs_for_repo(&model.id).await?; let repo = Repo { repo_id: model.id, @@ -120,8 +115,6 @@ pub async fn delete_repo_from_db(repo_id: &str) -> Result<()> { Entity::delete_by_id(repo_id).exec(&db).await?; // Delete associated refs crate::storage::ref_model::delete_refs_for_repo(repo_id).await?; - // Delete associated refs - crate::storage::ref_model::delete_refs_for_repo(repo_id).await?; Ok(()) } @@ -134,8 +127,6 @@ pub async fn list_repos() -> Result> { for model in models { // Load refs from ref_model table let refs = crate::storage::ref_model::load_refs_for_repo(&model.id).await?; - // Load refs from ref_model table - let refs = crate::storage::ref_model::load_refs_for_repo(&model.id).await?; repos.push(Repo { repo_id: model.id, diff --git a/src/transport/quic.rs b/src/transport/quic.rs index 7603c80..8dbc8c5 100644 --- a/src/transport/quic.rs +++ b/src/transport/quic.rs @@ -384,10 +384,15 @@ mod tests { identity::keypair::KeyPair, node::node::{Node, NodeType}, }; - use std::sync::Once; + use std::sync::{Once, OnceLock}; use tokio::time::Duration; static RUSTLS_INIT: Once = Once::new(); + static TEST_SERIAL_LOCK: OnceLock> = OnceLock::new(); + + fn serial_lock() -> &'static tokio::sync::Mutex<()> { + TEST_SERIAL_LOCK.get_or_init(|| tokio::sync::Mutex::new(())) + } fn init() { // Install ring crypto provider only once per test process. @@ -493,6 +498,7 @@ mod tests { // Test the `server` method #[tokio::test] async fn test_server_creation() { + let _guard = serial_lock().lock().await; init(); cleanup_test_certs(); let config = mock_quic_config(); @@ -509,6 +515,7 @@ mod tests { // Test the `connect` method #[tokio::test] async fn test_client_connection() { + let _guard = serial_lock().lock().await; init(); cleanup_test_certs(); let keypair1 = KeyPair::generate().expect("generate keypair"); @@ -569,6 +576,7 @@ mod tests { #[tokio::test] async fn test_send_message() { + let _guard = serial_lock().lock().await; init(); cleanup_test_certs(); let keypair1 = KeyPair::generate().expect("generate keypair"); @@ -632,6 +640,7 @@ mod tests { #[tokio::test] async fn test_client_connection_without_shared_ca() { + let _guard = serial_lock().lock().await; init(); cleanup_test_certs(); let keypair1 = KeyPair::generate().expect("generate keypair"); diff --git a/tests/bundle_two_nodes.rs b/tests/bundle_two_nodes.rs index 5c96709..6fb9d26 100644 --- a/tests/bundle_two_nodes.rs +++ b/tests/bundle_two_nodes.rs @@ -271,14 +271,14 @@ async fn test_bundle_transfer_between_two_nodes() { // 提取最后一段(NodeId 格式是 "did:key:xxx") let last_segment = sender_node_id_str .split(':') - .last() + .next_back() .unwrap_or(&sender_node_id_str); - let encoded_sender_id = last_segment.replace(':', "_").replace('/', "_"); + let encoded_sender_id = last_segment.replace([':', '/'], "_"); // repo_id 会被处理为最后一段(用 : 分割) let repo_id_last_part = "test_transfer_repo" .split(':') - .last() + .next_back() .unwrap_or("test_transfer_repo") .to_string(); @@ -332,7 +332,7 @@ async fn test_bundle_transfer_between_two_nodes() { // Check commit history let output = Command::new("git") .current_dir(restored_repo_path.to_str().unwrap()) - .args(&["log", "--oneline"]) + .args(["log", "--oneline"]) .output() .expect("Failed to get git log"); @@ -388,10 +388,8 @@ async fn test_bundle_transfer_between_two_nodes() { if receiver_bundle_storage.exists() { println!(" - Contents of receiver storage:"); if let Ok(entries) = fs::read_dir(&receiver_bundle_storage) { - for entry in entries { - if let Ok(entry) = entry { - println!(" - {}", entry.path().display()); - } + for entry in entries.flatten() { + println!(" - {}", entry.path().display()); } } } diff --git a/tests/git_pack.rs b/tests/git_pack.rs index 59eb3f2..1c42c5c 100644 --- a/tests/git_pack.rs +++ b/tests/git_pack.rs @@ -190,7 +190,7 @@ fn test_pack_repo_bundle() { // Check that main branch exists let output = Command::new("git") .current_dir(repo2_str) - .args(&["branch", "-a"]) + .args(["branch", "-a"]) .output() .expect("Failed to list branches"); let branches = String::from_utf8_lossy(&output.stdout); @@ -213,7 +213,7 @@ fn test_pack_repo_bundle() { // Check commit history let output = Command::new("git") .current_dir(repo2_str) - .args(&["log", "--oneline"]) + .args(["log", "--oneline"]) .output() .expect("Failed to get log"); let log = String::from_utf8_lossy(&output.stdout); @@ -226,7 +226,7 @@ fn test_pack_repo_bundle() { // Check tags let output = Command::new("git") .current_dir(repo2_str) - .args(&["tag"]) + .args(["tag"]) .output() .expect("Failed to list tags"); let tags = String::from_utf8_lossy(&output.stdout); diff --git a/tests/gossip_three_nodes.rs b/tests/gossip_three_nodes.rs index 23c3fab..3c0cc6b 100644 --- a/tests/gossip_three_nodes.rs +++ b/tests/gossip_three_nodes.rs @@ -80,7 +80,6 @@ async fn test_gossip_three_nodes_message_relay() { node3.start_quic_server(config3).await.unwrap(); // 5. 启动 gossip 和 bundle 服务 - let bundle_storage = std::path::PathBuf::from("./data/test_bundles"); let gossip1 = Arc::new(GossipService::new( Arc::clone(node1.connection_manager.as_ref().unwrap()), node1.clone(), From e95d8f6d308c13ffd35bb53ff2fec5f505e6ac30 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Fri, 13 Mar 2026 16:00:12 +0800 Subject: [PATCH 35/42] fix --- README.md | 19 ++++++++++++++ src/storage/ref_model.rs | 57 ++++++++++++++++++++++++---------------- src/transport/config.rs | 9 ++++++- src/transport/quic.rs | 7 +++-- 4 files changed, 66 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index a5ac920..eb36ffa 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ MegaEngine is a distributed peer-to-peer (P2P) network for Git repositories. It - **Bundle Transfer**: P2P transfer of Git bundle files between nodes with integrity verification - **Automatic Bundle Sync**: Periodic background task that automatically downloads bundles for external repositories - **Repository Cloning**: Clone repositories from bundles using the `repo clone` command +- **Peer-to-Peer Chat**: Send direct encrypted chat messages between nodes using the `chat send` command - **QUIC Transport**: Uses QUIC protocol for reliable, low-latency peer-to-peer communication - **Gossip Protocol**: Implements epidemic message propagation with TTL and deduplication - **Cryptographic Identity**: Each node has a unique EdDSA-based identity (`did:key` format) @@ -197,6 +198,24 @@ Replace `` with the repository ID from Step 3. The cloned repository at `./tiny` will be updated with the latest commits from the bundle. +### Step 8: Node-to-Node Chat Messaging + +After both nodes are running and connected, you can send chat messages directly by Node ID. + +**Terminal 3** - Send a message from node2 to node1: +```bash +cargo run -- --root ~/.megaengine2 chat send --to --msg "hello from node2" +``` + +Replace `` with node1's DID key from Step 1/Step 2 output. + +Example: +```bash +cargo run -- --root ~/.megaengine2 chat send --to did:key:z2DUYGZos3YrXrD4pQ9aAku2g7btumKcfTiMSyBC8btqFDJ --msg "hello" +``` + +You should see the message reception log on node1's terminal. + ## 🔐 Data Formats diff --git a/src/storage/ref_model.rs b/src/storage/ref_model.rs index a0b765e..7a2d662 100644 --- a/src/storage/ref_model.rs +++ b/src/storage/ref_model.rs @@ -8,11 +8,12 @@ use crate::storage::get_db_conn; #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[sea_orm(table_name = "refs")] pub struct Model { - #[sea_orm(primary_key)] - pub id: String, + #[sea_orm(primary_key, auto_increment = false)] pub repo_id: String, + #[sea_orm(primary_key, auto_increment = false)] pub ref_name: String, pub commit_hash: String, + pub created_at: i64, pub updated_at: i64, } @@ -26,29 +27,30 @@ pub async fn save_ref(repo_id: &str, ref_name: &str, commit_hash: &str) -> Resul let db = get_db_conn().await?; let now = chrono::Local::now().timestamp(); - // Generate a unique ID for this ref record (repo_id + ref_name) - let id = format!("{}:{}", repo_id, ref_name); - // Check if ref already exists - let existing = Entity::find_by_id(id.clone()).one(&db).await?; + let existing = Entity::find() + .filter(Column::RepoId.eq(repo_id)) + .filter(Column::RefName.eq(ref_name)) + .one(&db) + .await?; - if existing.is_some() { + if let Some(existing_model) = existing { // Update existing ref let active_model = ActiveModel { - id: Unchanged(id), - repo_id: Unchanged(repo_id.to_string()), - ref_name: Unchanged(ref_name.to_string()), + repo_id: Unchanged(existing_model.repo_id), + ref_name: Unchanged(existing_model.ref_name), commit_hash: Set(commit_hash.to_string()), + created_at: Unchanged(existing_model.created_at), updated_at: Set(now), }; Entity::update(active_model).exec(&db).await?; } else { // Insert new ref let active_model = ActiveModel { - id: Set(id), repo_id: Set(repo_id.to_string()), ref_name: Set(ref_name.to_string()), commit_hash: Set(commit_hash.to_string()), + created_at: Set(now), updated_at: Set(now), }; Entity::insert(active_model).exec(&db).await?; @@ -66,25 +68,27 @@ pub async fn batch_save_refs( let now = chrono::Local::now().timestamp(); for (ref_name, commit_hash) in refs { - let id = format!("{}:{}", repo_id, ref_name); + let existing = Entity::find() + .filter(Column::RepoId.eq(repo_id)) + .filter(Column::RefName.eq(ref_name.as_str())) + .one(&db) + .await?; - let existing = Entity::find_by_id(id.clone()).one(&db).await?; - - if existing.is_some() { + if let Some(existing_model) = existing { let active_model = ActiveModel { - id: Unchanged(id), - repo_id: Unchanged(repo_id.to_string()), - ref_name: Unchanged(ref_name.clone()), + repo_id: Unchanged(existing_model.repo_id), + ref_name: Unchanged(existing_model.ref_name), commit_hash: Set(commit_hash.clone()), + created_at: Unchanged(existing_model.created_at), updated_at: Set(now), }; Entity::update(active_model).exec(&db).await?; } else { let active_model = ActiveModel { - id: Set(id), repo_id: Set(repo_id.to_string()), ref_name: Set(ref_name.clone()), commit_hash: Set(commit_hash.clone()), + created_at: Set(now), updated_at: Set(now), }; Entity::insert(active_model).exec(&db).await?; @@ -116,9 +120,13 @@ pub async fn load_refs_for_repo( /// Get a specific ref by repo_id and ref_name pub async fn get_ref(repo_id: &str, ref_name: &str) -> Result> { let db = get_db_conn().await?; - let id = format!("{}:{}", repo_id, ref_name); - if let Some(model) = Entity::find_by_id(id).one(&db).await? { + if let Some(model) = Entity::find() + .filter(Column::RepoId.eq(repo_id)) + .filter(Column::RefName.eq(ref_name)) + .one(&db) + .await? + { return Ok(Some(model.commit_hash)); } @@ -138,8 +146,11 @@ pub async fn delete_refs_for_repo(repo_id: &str) -> Result<()> { /// Delete a specific ref pub async fn delete_ref(repo_id: &str, ref_name: &str) -> Result<()> { let db = get_db_conn().await?; - let id = format!("{}:{}", repo_id, ref_name); - Entity::delete_by_id(id).exec(&db).await?; + Entity::delete_many() + .filter(Column::RepoId.eq(repo_id)) + .filter(Column::RefName.eq(ref_name)) + .exec(&db) + .await?; Ok(()) } diff --git a/src/transport/config.rs b/src/transport/config.rs index 78e96c7..7524ab6 100644 --- a/src/transport/config.rs +++ b/src/transport/config.rs @@ -117,7 +117,14 @@ impl QuicConfig { client_crypto.alpn_protocols = ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); client_crypto.enable_early_data = false; - let client_config = ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?)); + let mut client_config = + ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?)); + + let mut transport_config = TransportConfig::default(); + transport_config.max_idle_timeout(Some(IdleTimeout::from(VarInt::from_u32(300_000)))); + transport_config.keep_alive_interval(Some(Duration::from_secs(30))); + client_config.transport_config(Arc::new(transport_config)); + Ok(client_config) } diff --git a/src/transport/quic.rs b/src/transport/quic.rs index 8dbc8c5..1ba4aa9 100644 --- a/src/transport/quic.rs +++ b/src/transport/quic.rs @@ -235,8 +235,11 @@ impl ConnectionManager { let mut dead_nodes = Vec::new(); for (node_id, conn) in conns.iter() { - if conn.connection.close_reason().is_some() { - info!("Connection to node[{}] closed", node_id); + if let Some(reason) = conn.connection.close_reason() { + info!( + "Connection to node[{}] closed, reason: {:?}", + node_id, reason + ); dead_nodes.push(node_id.clone()); } } From 92c21d6a6da8e1573cf9ff83dd76afaff432c4cd Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Fri, 13 Mar 2026 16:21:27 +0800 Subject: [PATCH 36/42] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/chat/service.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/chat/service.rs b/src/chat/service.rs index c9d8c14..45f273b 100644 --- a/src/chat/service.rs +++ b/src/chat/service.rs @@ -120,10 +120,33 @@ async fn try_send_pending_msg( } if peers.contains(&receiver_node_id) { - let _ = mgr.send_gossip_message(receiver_node_id.clone(), data.clone()).await; + // Direct send to receiver; propagate any error to the caller. + mgr.send_gossip_message(receiver_node_id.clone(), data.clone()) + .await + .map_err(|e| anyhow!("Failed to send gossip message to receiver {}: {}", receiver_node_id, e))?; } else { + // Broadcast to all peers; require at least one successful send. + let mut at_least_one_success = false; + let mut last_err: Option = None; + for peer in peers { - let _ = mgr.send_gossip_message(peer.clone(), data.clone()).await; + match mgr.send_gossip_message(peer.clone(), data.clone()).await { + Ok(()) => { + at_least_one_success = true; + } + Err(e) => { + last_err = Some(anyhow!("Failed to send gossip message to peer {}: {}", peer, e)); + } + } + } + + if !at_least_one_success { + // If all sends failed, return the last error (or a generic one if none captured). + if let Some(err) = last_err { + return Err(err); + } else { + return Err(anyhow!("Failed to send gossip message to any peer (unknown error)")); + } } } From 00cd2745c04ad0d10d155196f34405b8d3c5b6ed Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Fri, 13 Mar 2026 16:44:56 +0800 Subject: [PATCH 37/42] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/storage/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 956267e..8f25b99 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -107,7 +107,7 @@ pub async fn get_db_conn() -> Result { let db_url = format!("sqlite://{}?mode=rwc", db_path.display()); let mut opt = ConnectOptions::new(db_url); - opt.max_connections(100) + opt.max_connections(10) .min_connections(1) .connect_timeout(Duration::from_secs(8)) .idle_timeout(Duration::from_secs(8)) From aa4b3334ed9c25a3ad71a1541367b5959de08a9d Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Fri, 13 Mar 2026 17:07:55 +0800 Subject: [PATCH 38/42] fix --- src/chat/service.rs | 80 ++---------- src/cli/node.rs | 18 ++- src/gossip/service.rs | 14 ++- src/identity/keypair.rs | 10 +- src/main.rs | 2 +- src/mcp/sse_server.rs | 64 ++++++++-- src/storage/mod.rs | 257 ++++++++++++++++++++++++++++++-------- src/storage/repo_model.rs | 3 +- 8 files changed, 297 insertions(+), 151 deletions(-) diff --git a/src/chat/service.rs b/src/chat/service.rs index 45f273b..1a475e0 100644 --- a/src/chat/service.rs +++ b/src/chat/service.rs @@ -187,41 +187,10 @@ pub async fn process_incoming_chat( ) -> Result<()> { // 1. Check if it's for me if msg.receiver_id != *my_node.node_id() { - tracing::info!("Message not for me, forwarding to {}", msg.receiver_id); - - // Construct the payload to forward - let gossip_msg = GossipMessage::Chat(msg.clone()); - - let mut signed_msg = SignedMessage { - node_id: my_node.node_id().clone(), - message: gossip_msg, - timestamp: timestamp_now(), - signature: "".to_string(), - }; - let self_hash = signed_msg.self_hash(); - let sign = my_node.sign_message(self_hash.as_slice())?; - signed_msg.signature = hex::encode(sign); - - let envelope = Envelope { - payload: signed_msg, - ttl: TTL, - }; - let data = serde_json::to_vec(&envelope)?; - - let mgr = manager.lock().await; - let peers = mgr.list_peers().await; - - if peers.contains(&msg.receiver_id) { - tracing::info!("Found target {} in neighbors, sending directly", msg.receiver_id); - let _ = mgr.send_gossip_message(msg.receiver_id.clone(), data).await; - } else { - tracing::info!("Target {} not in neighbors, broadcasting", msg.receiver_id); - for peer in peers { - if peer != msg.sender_id { - let _ = mgr.send_gossip_message(peer.clone(), data.clone()).await; - } - } - } + tracing::info!( + "Message not for me (target: {}), skip local forwarding and let gossip handle it", + msg.receiver_id + ); return Ok(()); } @@ -291,46 +260,15 @@ pub async fn process_incoming_chat( pub async fn process_ack( ack: ChatAckMessage, - manager: Arc>, + _manager: Arc>, my_node: Node, ) -> Result<()> { // 1. Check if it's for me if ack.target_id != *my_node.node_id() { - tracing::info!("ACK not for me (for {}), forwarding", ack.target_id); - - // Construct the payload to forward - let gossip_msg = GossipMessage::ChatAck(ack.clone()); - - let mut signed_msg = SignedMessage { - node_id: my_node.node_id().clone(), - message: gossip_msg, - timestamp: timestamp_now(), - signature: "".to_string(), - }; - let self_hash = signed_msg.self_hash(); - let sign = my_node.sign_message(self_hash.as_slice())?; - signed_msg.signature = hex::encode(sign); - - let envelope = Envelope { - payload: signed_msg, - ttl: TTL, - }; - let data = serde_json::to_vec(&envelope)?; - - let mgr = manager.lock().await; - let peers = mgr.list_peers().await; - - if peers.contains(&ack.target_id) { - tracing::info!("Found target {} in neighbors, sending ACK directly", ack.target_id); - let _ = mgr.send_gossip_message(ack.target_id.clone(), data).await; - } else { - tracing::info!("Target {} not in neighbors, broadcasting ACK", ack.target_id); - for peer in peers { - if peer != ack.sender_id { - let _ = mgr.send_gossip_message(peer.clone(), data.clone()).await; - } - } - } + tracing::info!( + "ACK not for me (target: {}), skip local forwarding and let gossip handle it", + ack.target_id + ); return Ok(()); } diff --git a/src/cli/node.rs b/src/cli/node.rs index 0cf88b5..e57680c 100644 --- a/src/cli/node.rs +++ b/src/cli/node.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use megaengine::mcp::{start_mcp_server, start_sse_server}; +use megaengine::mcp::start_sse_server; use megaengine::{ bundle::BundleService, node::node_addr::NodeAddr, storage, transport::config::QuicConfig, }; @@ -113,21 +113,19 @@ pub async fn handle_node_start( println!("Press Ctrl+C to stop"); if enable_mcp { - tracing::info!("MCP server enabled, starting alongside node"); - println!("MCP server is enabled"); - std::thread::spawn(|| { - let rt = tokio::runtime::Runtime::new().unwrap(); - if let Err(e) = rt.block_on(start_mcp_server()) { - tracing::error!("MCP server error: {}", e); - } - }); + tracing::warn!( + "--mcp (stdio) is ignored during node start to avoid stdin/stdout contention; use `megaengine mcp` in a separate process" + ); + eprintln!( + "Warning: --mcp (stdio) is not started with `node start`. Run `megaengine mcp` in a separate process." + ); } if let Some(port) = mcp_sse_port { tracing::info!("MCP SSE server enabled on port {}", port); println!("MCP SSE server enabled on port {}", port); tokio::spawn(async move { - let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port)); + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port)); if let Err(e) = start_sse_server(addr).await { tracing::error!("MCP SSE server error: {}", e); } diff --git a/src/gossip/service.rs b/src/gossip/service.rs index f181287..dc84378 100644 --- a/src/gossip/service.rs +++ b/src/gossip/service.rs @@ -67,7 +67,6 @@ impl GossipService { ttl: DEFAULT_TTL, }; tracing::debug!("Broadcasting NodeAnnouncement: {:?}", env); - tracing::debug!("Broadcasting NodeAnnouncement: {:?}", env); let data = serde_json::to_vec(&env).unwrap_or_default(); let mgr = s2.manager.lock().await; let peers = mgr.list_peers().await; @@ -88,7 +87,6 @@ impl GossipService { ttl: DEFAULT_TTL, }; tracing::debug!("Broadcasting RepoAnnouncement: {:?}", env); - tracing::debug!("Broadcasting RepoAnnouncement: {:?}", env); let data = serde_json::to_vec(&env).unwrap_or_default(); let mgr = s2.manager.lock().await; let peers = mgr.list_peers().await; @@ -100,7 +98,6 @@ impl GossipService { } tokio::time::sleep(Duration::from_secs(30)).await; - tokio::time::sleep(Duration::from_secs(30)).await; } }); @@ -160,6 +157,17 @@ impl GossipService { } } + // Ensure outer signer identity matches the embedded payload sender identity. + // This prevents payload-level sender_id spoofing. + if signed.node_id != *signed.message.sender() { + tracing::error!( + "message sender mismatch: signed node {} != payload sender {}", + signed.node_id, + signed.message.sender() + ); + return Ok(()); + } + // process message (borrow the inner message to avoid moving) match &signed.message { GossipMessage::NodeAnnouncement(na) => { diff --git a/src/identity/keypair.rs b/src/identity/keypair.rs index 99715b6..4b5ad37 100644 --- a/src/identity/keypair.rs +++ b/src/identity/keypair.rs @@ -61,8 +61,6 @@ impl KeyPair { /// Encrypt a message for a specific recipient (identified by their Ed25519 VerifyingKey) /// Returns: Ephemeral_PK (32) + Nonce (12) + Ciphertext (N) pub fn encrypt_to_node(&self, recipient_vk: &VerifyingKey, message: &[u8]) -> Result> { - let _rng = OsRng; - // 1. Convert Recipient Ed25519 PK -> X25519 PK (Montgomery) let recipient_ed_y = CompressedEdwardsY::from_slice(recipient_vk.as_bytes())?; let recipient_ed_point = recipient_ed_y @@ -73,7 +71,13 @@ impl KeyPair { // 2. Generate Ephemeral Keypair let mut scalar_bytes = [0u8; 32]; OsRng.fill_bytes(&mut scalar_bytes); - let ephemeral_scalar = Scalar::from_bytes_mod_order(scalar_bytes); + + // Clamp scalar bytes to align with X25519 scalar requirements. + scalar_bytes[0] &= 248; + scalar_bytes[31] &= 127; + scalar_bytes[31] |= 64; + + let ephemeral_scalar = Scalar::from_bits(scalar_bytes); let ephemeral_point = MontgomeryPoint::mul_base(&ephemeral_scalar); // 3. Keep Ephemeral Public Key diff --git a/src/main.rs b/src/main.rs index bee4339..e8271b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -67,7 +67,7 @@ enum NodeAction { #[arg(long)] bootstrap_node: Option, - /// Start MCP server alongside the node + /// Deprecated for node start: stdio MCP must run as a separate process via `megaengine mcp` #[arg(long, default_value = "false")] mcp: bool, diff --git a/src/mcp/sse_server.rs b/src/mcp/sse_server.rs index 6f81b19..5000400 100644 --- a/src/mcp/sse_server.rs +++ b/src/mcp/sse_server.rs @@ -13,7 +13,6 @@ use serde::Deserialize; use serde_json::{json, Value}; use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; use tokio::sync::{mpsc, RwLock}; -use tokio_stream::wrappers::UnboundedReceiverStream; use tower_http::cors::CorsLayer; use uuid::Uuid; @@ -27,6 +26,31 @@ struct SessionParam { session_id: String, } +struct SessionCleanup { + state: Arc, + session_id: String, +} + +impl SessionCleanup { + fn new(state: Arc, session_id: String) -> Self { + Self { state, session_id } + } +} + +impl Drop for SessionCleanup { + fn drop(&mut self) { + let state = Arc::clone(&self.state); + let session_id = self.session_id.clone(); + + tokio::spawn(async move { + let removed = state.sessions.write().await.remove(&session_id); + if removed.is_some() { + tracing::info!("SSE session cleaned up: {}", session_id); + } + }); + } +} + pub async fn start_sse_server(addr: SocketAddr) -> anyhow::Result<()> { let state = Arc::new(AppState { sessions: RwLock::new(HashMap::new()), @@ -58,7 +82,10 @@ async fn sse_handler( .await .insert(session_id.clone(), tx.clone()); - let stream = UnboundedReceiverStream::new(rx); + let stream = futures::stream::unfold( + (rx, SessionCleanup::new(state.clone(), session_id.clone())), + |(mut rx, cleanup)| async move { rx.recv().await.map(|event| (event, (rx, cleanup))) }, + ); // Send the endpoint event immediately let endpoint_url = format!("/messages?session_id={}", session_id); @@ -142,7 +169,7 @@ async fn message_handler( "isError": false } })) - }, + } Err(e) => Some(json!({ "jsonrpc": "2.0", "id": request.get("id"), @@ -176,13 +203,31 @@ async fn message_handler( })) } } - // Handle other JSON-RPC methods or notifications if needed - _ => None, + // For unknown methods, reply only when this is a request (has id). + _ => { + if request.get("id").is_some() { + Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "error": { + "code": -32601, + "message": "Method not found" + } + })) + } else { + None + } + } }; if let Some(response) = response { if let Ok(data) = serde_json::to_string(&response) { - let _ = tx.send(Ok(Event::default().event("message").data(data))); + if tx + .send(Ok(Event::default().event("message").data(data))) + .is_err() + { + state.sessions.write().await.remove(&session_id); + } } } } else { @@ -196,7 +241,12 @@ async fn message_handler( } }); if let Ok(data) = serde_json::to_string(&error_response) { - let _ = tx.send(Ok(Event::default().event("message").data(data))); + if tx + .send(Ok(Event::default().event("message").data(data))) + .is_err() + { + state.sessions.write().await.remove(&session_id); + } } } }); diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 956267e..fb44398 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -3,8 +3,10 @@ pub mod node_model; pub mod ref_model; pub mod repo_model; -use anyhow::Result; -use sea_orm::{ConnectOptions, ConnectionTrait, Database, DatabaseConnection}; +use anyhow::{anyhow, Result}; +use sea_orm::{ + ConnectOptions, ConnectionTrait, Database, DatabaseConnection, DbBackend, Statement, +}; use std::fs; use std::path::PathBuf; use std::time::Duration; @@ -75,50 +77,144 @@ pub fn db_path() -> PathBuf { p } -/// 初始化数据库连接并创建表 -pub async fn get_db_conn() -> Result { - use std::collections::HashMap; - use tokio::sync::Mutex; +async fn execute_sql_ignore_duplicate_column(db: &DatabaseConnection, sql: &str) -> Result<()> { + match db.execute_unprepared(sql).await { + Ok(_) => Ok(()), + Err(err) => { + let msg = err.to_string().to_lowercase(); + if msg.contains("duplicate column name") { + return Ok(()); + } + Err(err.into()) + } + } +} - static DB_POOL: OnceCell>> = OnceCell::const_new(); +fn escape_sqlite_literal(value: &str) -> String { + value.replace('\'', "''") +} - let pool = DB_POOL - .get_or_init(|| async { Mutex::new(HashMap::new()) }) - .await; +async fn sqlite_query_one_i64(db: &DatabaseConnection, sql: String) -> Result { + let row = db + .query_one(Statement::from_string(DbBackend::Sqlite, sql)) + .await? + .ok_or_else(|| anyhow!("sqlite query returned no rows"))?; + Ok(row.try_get_by_index(0)?) +} - let path = db_path(); +async fn sqlite_query_one_string_opt( + db: &DatabaseConnection, + sql: String, +) -> Result> { + let Some(row) = db + .query_one(Statement::from_string(DbBackend::Sqlite, sql)) + .await? + else { + return Ok(None); + }; + + let value: Option = row.try_get_by_index(0)?; + Ok(value) +} - { - let map = pool.lock().await; - if let Some(db) = map.get(&path) { - return Ok(db.clone()); - } - } +async fn sqlite_has_column(db: &DatabaseConnection, table: &str, column: &str) -> Result { + let sql = format!( + "SELECT COUNT(*) FROM pragma_table_info('{}') WHERE name = '{}'", + escape_sqlite_literal(table), + escape_sqlite_literal(column) + ); + Ok(sqlite_query_one_i64(db, sql).await? > 0) +} - // 延迟初始化并缓存全局连接(仅第一次会执行创建表操作) - let db_path = path.clone(); +async fn migrate_repos_table(db: &DatabaseConnection) -> Result<()> { + execute_sql_ignore_duplicate_column( + db, + "ALTER TABLE repos ADD COLUMN language TEXT NOT NULL DEFAULT ''", + ) + .await?; + execute_sql_ignore_duplicate_column( + db, + "ALTER TABLE repos ADD COLUMN size INTEGER NOT NULL DEFAULT 0", + ) + .await?; + execute_sql_ignore_duplicate_column( + db, + "ALTER TABLE repos ADD COLUMN latest_commit_at INTEGER NOT NULL DEFAULT 0", + ) + .await?; + Ok(()) +} - // 确保目录存在 - if let Some(parent) = db_path.parent() { - fs::create_dir_all(parent).ok(); - } +async fn refs_table_needs_rebuild(db: &DatabaseConnection) -> Result { + let pk_sql = format!( + "SELECT group_concat(name, ',') FROM (\ + SELECT name FROM pragma_table_info('{}') WHERE pk > 0 ORDER BY pk\ + )", + escape_sqlite_literal("refs") + ); + let pk = sqlite_query_one_string_opt(db, pk_sql).await?; + let has_created_at = sqlite_has_column(db, "refs", "created_at").await?; + Ok(pk.as_deref() != Some("repo_id,ref_name") || !has_created_at) +} - // 使用合适的 SQLite URL 格式 - let db_url = format!("sqlite://{}?mode=rwc", db_path.display()); +async fn rebuild_refs_table(db: &DatabaseConnection) -> Result<()> { + let refs_has_created_at = sqlite_has_column(db, "refs", "created_at").await?; + let refs_has_updated_at = sqlite_has_column(db, "refs", "updated_at").await?; + + let created_expr = if refs_has_created_at { + "COALESCE(created_at, CAST(strftime('%s','now') AS INTEGER))" + } else if refs_has_updated_at { + "COALESCE(updated_at, CAST(strftime('%s','now') AS INTEGER))" + } else { + "CAST(strftime('%s','now') AS INTEGER)" + }; + + let updated_expr = if refs_has_updated_at { + "COALESCE(updated_at, CAST(strftime('%s','now') AS INTEGER))" + } else if refs_has_created_at { + "COALESCE(created_at, CAST(strftime('%s','now') AS INTEGER))" + } else { + "CAST(strftime('%s','now') AS INTEGER)" + }; + + let sql = format!( + "BEGIN IMMEDIATE; + CREATE TABLE refs_new ( + repo_id TEXT NOT NULL, + ref_name TEXT NOT NULL, + commit_hash TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (repo_id, ref_name) + ); + INSERT OR REPLACE INTO refs_new (repo_id, ref_name, commit_hash, created_at, updated_at) + SELECT repo_id, ref_name, commit_hash, {created_expr}, {updated_expr} + FROM refs + WHERE repo_id IS NOT NULL AND ref_name IS NOT NULL; + DROP TABLE refs; + ALTER TABLE refs_new RENAME TO refs; + COMMIT;" + ); + + if let Err(err) = db.execute_unprepared(&sql).await { + let rollback = "ROLLBACK;"; + let _ = db.execute_unprepared(rollback).await; + return Err(err.into()); + } - let mut opt = ConnectOptions::new(db_url); - opt.max_connections(100) - .min_connections(1) - .connect_timeout(Duration::from_secs(8)) - .idle_timeout(Duration::from_secs(8)) - .sqlx_logging(false); + Ok(()) +} - let db = Database::connect(opt).await?; +async fn migrate_refs_table(db: &DatabaseConnection) -> Result<()> { + if !refs_table_needs_rebuild(db).await? { + return Ok(()); + } + rebuild_refs_table(db).await +} - // 运行迁移或创建表(只在初始化时执行) - let _ = db - .execute_unprepared( - "CREATE TABLE IF NOT EXISTS repos ( +async fn ensure_schema(db: &DatabaseConnection) -> Result<()> { + db.execute_unprepared( + "CREATE TABLE IF NOT EXISTS repos ( id TEXT PRIMARY KEY, name TEXT NOT NULL, creator TEXT NOT NULL, @@ -132,13 +228,11 @@ pub async fn get_db_conn() -> Result { created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL )", - ) - .await; + ) + .await?; - // 节点表 - let _ = db - .execute_unprepared( - "CREATE TABLE IF NOT EXISTS nodes ( + db.execute_unprepared( + "CREATE TABLE IF NOT EXISTS nodes ( id TEXT PRIMARY KEY, alias TEXT NOT NULL, addresses TEXT NOT NULL, @@ -147,13 +241,11 @@ pub async fn get_db_conn() -> Result { created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL )", - ) - .await; + ) + .await?; - // Refs 表 - let _ = db - .execute_unprepared( - "CREATE TABLE IF NOT EXISTS refs ( + db.execute_unprepared( + "CREATE TABLE IF NOT EXISTS refs ( repo_id TEXT NOT NULL, ref_name TEXT NOT NULL, commit_hash TEXT NOT NULL, @@ -161,13 +253,11 @@ pub async fn get_db_conn() -> Result { updated_at INTEGER NOT NULL, PRIMARY KEY (repo_id, ref_name) )", - ) - .await; + ) + .await?; - // Chat Messages 表 - let _ = db - .execute_unprepared( - "CREATE TABLE IF NOT EXISTS chat_messages ( + db.execute_unprepared( + "CREATE TABLE IF NOT EXISTS chat_messages ( id TEXT PRIMARY KEY, \"from\" TEXT NOT NULL, \"to\" TEXT NOT NULL, @@ -175,9 +265,66 @@ pub async fn get_db_conn() -> Result { created_at INTEGER NOT NULL, status TEXT NOT NULL )", - ) + ) + .await?; + + migrate_repos_table(db).await?; + migrate_refs_table(db).await?; + + // Align old refs rows that may have default timestamps after ALTER/rebuild. + db.execute_unprepared( + "UPDATE refs + SET created_at = updated_at + WHERE created_at = 0 AND updated_at > 0", + ) + .await?; + + Ok(()) +} + +/// 初始化数据库连接并创建表 +pub async fn get_db_conn() -> Result { + use std::collections::HashMap; + use tokio::sync::Mutex; + + static DB_POOL: OnceCell>> = OnceCell::const_new(); + + let pool = DB_POOL + .get_or_init(|| async { Mutex::new(HashMap::new()) }) .await; + let path = db_path(); + + { + let map = pool.lock().await; + if let Some(db) = map.get(&path) { + return Ok(db.clone()); + } + } + + // 延迟初始化并缓存全局连接(仅第一次会执行创建表操作) + let db_path = path.clone(); + + // 确保目录存在 + if let Some(parent) = db_path.parent() { + fs::create_dir_all(parent).ok(); + } + + // 使用合适的 SQLite URL 格式 + let db_url = format!("sqlite://{}?mode=rwc", db_path.display()); + + let mut opt = ConnectOptions::new(db_url); + opt.max_connections(8) + .min_connections(1) + .connect_timeout(Duration::from_secs(8)) + .idle_timeout(Duration::from_secs(8)) + .sqlx_logging(false); + + let db = Database::connect(opt).await?; + + // 运行迁移/建表,兼容已有数据库结构升级 + ensure_schema(&db).await?; + { let mut map = pool.lock().await; map.insert(path, db.clone()); diff --git a/src/storage/repo_model.rs b/src/storage/repo_model.rs index 5b58722..85c3bf9 100644 --- a/src/storage/repo_model.rs +++ b/src/storage/repo_model.rs @@ -150,13 +150,14 @@ pub async fn list_repos() -> Result> { /// 更新 Repo 的 bundle 路径 pub async fn update_repo_bundle(repo_id: &str, bundle_path: &str) -> Result<()> { let db = get_db_conn().await?; + let now = chrono::Local::now().timestamp(); // 查询是否存在 if let Some(model) = Entity::find_by_id(repo_id).one(&db).await? { let active_model = ActiveModel { id: Unchanged(model.id), bundle: Set(bundle_path.to_string()), - updated_at: Unchanged(model.updated_at), + updated_at: Set(now), // Keep other fields unchanged name: Unchanged(model.name), creator: Unchanged(model.creator), From 0cb39f2611e96996e7b92eba8759d23ee660429b Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Fri, 13 Mar 2026 17:35:16 +0800 Subject: [PATCH 39/42] fix --- src/chat/service.rs | 2 +- src/cli/chat.rs | 8 ++- src/cli/repo.rs | 46 +++++++++++-- src/mcp/sse_server.rs | 11 +-- src/storage/mod.rs | 131 ++++++++++++++++++++++++++++++++++++ tests/bundle_two_nodes.rs | 51 +++++++++----- tests/gossip_three_nodes.rs | 2 + 7 files changed, 213 insertions(+), 38 deletions(-) diff --git a/src/chat/service.rs b/src/chat/service.rs index 65bb9b9..195f4f7 100644 --- a/src/chat/service.rs +++ b/src/chat/service.rs @@ -226,7 +226,7 @@ pub async fn process_incoming_chat( tracing::info!("Received Chat from {}: {}", msg.sender_id.0, content); // 3. Store - let db = crate::storage::get_db_conn().await.unwrap(); + let db = crate::storage::get_db_conn().await?; if (crate::storage::chat_message::Entity::find_by_id(msg.msg_id.clone()) .one(&db) .await?) diff --git a/src/cli/chat.rs b/src/cli/chat.rs index 0c221dc..44db5c6 100644 --- a/src/cli/chat.rs +++ b/src/cli/chat.rs @@ -55,9 +55,11 @@ pub async fn run_chat_command(cmd: ChatCommand) -> Result<()> { println!("--- Chat History ---"); for m in messages { - let time = chrono::DateTime::from_timestamp(m.created_at, 0) - .unwrap_or_default() - .format("%Y-%m-%d %H:%M:%S"); + let time = if let Some(dt) = chrono::DateTime::from_timestamp(m.created_at, 0) { + dt.format("%Y-%m-%d %H:%M:%S").to_string() + } else { + m.created_at.to_string() + }; println!( "[{}] From: {} To: {} : {} ({:?})", time, m.from, m.to, m.content, m.status diff --git a/src/cli/repo.rs b/src/cli/repo.rs index 94cafdd..77a1e8a 100644 --- a/src/cli/repo.rs +++ b/src/cli/repo.rs @@ -170,18 +170,50 @@ fn detect_language(path: &str) -> String { fn calculate_directory_size(path: &std::path::Path) -> u64 { use std::fs; - let mut size = 0; - if let Ok(entries) = fs::read_dir(path) { + + const MAX_DEPTH: usize = 64; + const MAX_ENTRIES: u64 = 200_000; + const MAX_TOTAL_SIZE: u64 = 20 * 1024 * 1024 * 1024; // 20 GiB + + fn walk(path: &std::path::Path, depth: usize, entries_seen: &mut u64, total: &mut u64) { + if depth > MAX_DEPTH || *entries_seen >= MAX_ENTRIES || *total >= MAX_TOTAL_SIZE { + return; + } + + let Ok(entries) = fs::read_dir(path) else { + return; + }; + for entry in entries.flatten() { + if *entries_seen >= MAX_ENTRIES || *total >= MAX_TOTAL_SIZE { + break; + } + + *entries_seen += 1; let p = entry.path(); - if p.is_file() { - size += fs::metadata(&p).map(|m| m.len()).unwrap_or(0); - } else if p.is_dir() { - size += calculate_directory_size(&p); + + // Never follow symlinks to avoid cycles and unbounded traversal. + let Ok(meta) = fs::symlink_metadata(&p) else { + continue; + }; + + let file_type = meta.file_type(); + if file_type.is_symlink() { + continue; + } + + if file_type.is_file() { + *total = total.saturating_add(meta.len()); + } else if file_type.is_dir() { + walk(&p, depth + 1, entries_seen, total); } } } - size + + let mut entries_seen = 0; + let mut total = 0; + walk(path, 0, &mut entries_seen, &mut total); + total } fn format_bytes(bytes: u64) -> String { diff --git a/src/mcp/sse_server.rs b/src/mcp/sse_server.rs index 5000400..f660836 100644 --- a/src/mcp/sse_server.rs +++ b/src/mcp/sse_server.rs @@ -59,7 +59,6 @@ pub async fn start_sse_server(addr: SocketAddr) -> anyhow::Result<()> { let app = Router::new() .route("/sse", get(sse_handler)) .route("/messages", post(message_handler)) - .layer(CorsLayer::permissive()) .with_state(state); tracing::info!("MCP SSE Server listening on {}", addr); @@ -156,18 +155,10 @@ async fn message_handler( if let Some(name) = name { match RepoMcpServer::execute_tool(name, args).await { Ok(result_value) => { - // Format result as MCP CallToolResult with text content - let content_text = result_value.to_string(); Some(json!({ "jsonrpc": "2.0", "id": request.get("id"), - "result": { - "content": [{ - "type": "text", - "text": content_text - }], - "isError": false - } + "result": result_value })) } Err(e) => Some(json!({ diff --git a/src/storage/mod.rs b/src/storage/mod.rs index fb44398..19ea185 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -142,6 +142,137 @@ async fn migrate_repos_table(db: &DatabaseConnection) -> Result<()> { "ALTER TABLE repos ADD COLUMN latest_commit_at INTEGER NOT NULL DEFAULT 0", ) .await?; + + if repos_table_needs_rebuild(db).await? { + rebuild_repos_table(db).await?; + } + + Ok(()) +} + +async fn repos_table_needs_rebuild(db: &DatabaseConnection) -> Result { + // Legacy schema had a `timestamp` column that can block inserts now that + // repo writes no longer set it. Rebuild to canonical schema when present. + sqlite_has_column(db, "repos", "timestamp").await +} + +async fn rebuild_repos_table(db: &DatabaseConnection) -> Result<()> { + let has_language = sqlite_has_column(db, "repos", "language").await?; + let has_size = sqlite_has_column(db, "repos", "size").await?; + let has_latest_commit_at = sqlite_has_column(db, "repos", "latest_commit_at").await?; + let has_bundle = sqlite_has_column(db, "repos", "bundle").await?; + let has_is_external = sqlite_has_column(db, "repos", "is_external").await?; + let has_created_at = sqlite_has_column(db, "repos", "created_at").await?; + let has_updated_at = sqlite_has_column(db, "repos", "updated_at").await?; + let has_timestamp = sqlite_has_column(db, "repos", "timestamp").await?; + + let now_expr = "CAST(strftime('%s','now') AS INTEGER)"; + + let language_expr = if has_language { + "COALESCE(language, '')" + } else { + "''" + }; + + let size_expr = if has_size { "COALESCE(size, 0)" } else { "0" }; + + let latest_commit_expr = if has_latest_commit_at { + "COALESCE(latest_commit_at, 0)" + } else if has_timestamp { + "COALESCE(timestamp, 0)" + } else { + "0" + }; + + let bundle_expr = if has_bundle { + "COALESCE(bundle, '')" + } else { + "''" + }; + + let is_external_expr = if has_is_external { + "COALESCE(is_external, 0)" + } else { + "0" + }; + + let created_expr = if has_created_at { + format!("COALESCE(created_at, {now_expr})") + } else if has_timestamp { + format!("COALESCE(timestamp, {now_expr})") + } else { + now_expr.to_string() + }; + + let updated_expr = if has_updated_at { + format!("COALESCE(updated_at, {now_expr})") + } else if has_created_at { + format!("COALESCE(created_at, {now_expr})") + } else if has_timestamp { + format!("COALESCE(timestamp, {now_expr})") + } else { + now_expr.to_string() + }; + + let sql = format!( + "BEGIN IMMEDIATE; + CREATE TABLE repos_new ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + creator TEXT NOT NULL, + description TEXT NOT NULL, + language TEXT NOT NULL DEFAULT '', + size INTEGER NOT NULL DEFAULT 0, + latest_commit_at INTEGER NOT NULL DEFAULT 0, + path TEXT NOT NULL, + bundle TEXT NOT NULL DEFAULT '', + is_external INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); + INSERT OR REPLACE INTO repos_new ( + id, + name, + creator, + description, + language, + size, + latest_commit_at, + path, + bundle, + is_external, + created_at, + updated_at + ) + SELECT + id, + name, + creator, + description, + {language_expr}, + {size_expr}, + {latest_commit_expr}, + path, + {bundle_expr}, + {is_external_expr}, + {created_expr}, + {updated_expr} + FROM repos + WHERE id IS NOT NULL + AND name IS NOT NULL + AND creator IS NOT NULL + AND description IS NOT NULL + AND path IS NOT NULL; + DROP TABLE repos; + ALTER TABLE repos_new RENAME TO repos; + COMMIT;" + ); + + if let Err(err) = db.execute_unprepared(&sql).await { + let _ = db.execute_unprepared("ROLLBACK;").await; + return Err(err.into()); + } + Ok(()) } diff --git a/tests/bundle_two_nodes.rs b/tests/bundle_two_nodes.rs index 6fb9d26..93c53a4 100644 --- a/tests/bundle_two_nodes.rs +++ b/tests/bundle_two_nodes.rs @@ -65,19 +65,37 @@ async fn test_bundle_transfer_between_two_nodes() { .with_test_writer() .try_init(); + let cert_dir = std::env::current_dir() + .unwrap() + .join("tmp/bundle_two_nodes_certs"); + fs::remove_dir_all(&cert_dir).ok(); + fs::create_dir_all(&cert_dir).expect("Failed to create test cert directory"); + + let sender_cert_path = cert_dir.join("cert_sender.pem").to_string_lossy().to_string(); + let sender_key_path = cert_dir.join("key_sender.pem").to_string_lossy().to_string(); + let receiver_cert_path = cert_dir + .join("cert_receiver.pem") + .to_string_lossy() + .to_string(); + let receiver_key_path = cert_dir + .join("key_receiver.pem") + .to_string_lossy() + .to_string(); + let ca_cert_path = cert_dir.join("ca-cert.pem").to_string_lossy().to_string(); + println!("📋 Step 1: Setting up certificates"); // Ensure certificates exist megaengine::transport::cert::ensure_certificates( - "cert/cert_sender.pem", - "cert/key_sender.pem", - "cert/ca-cert.pem", + &sender_cert_path, + &sender_key_path, + &ca_cert_path, ) .expect("Failed to ensure sender certificates"); megaengine::transport::cert::ensure_certificates( - "cert/cert_receiver.pem", - "cert/key_receiver.pem", - "cert/ca-cert.pem", + &receiver_cert_path, + &receiver_key_path, + &ca_cert_path, ) .expect("Failed to ensure receiver certificates"); @@ -116,15 +134,15 @@ async fn test_bundle_transfer_between_two_nodes() { // Start QUIC servers let sender_config = QuicConfig::new( sender_addr, - "cert/cert_sender.pem".to_string(), - "cert/key_sender.pem".to_string(), - "cert/ca-cert.pem".to_string(), + sender_cert_path.clone(), + sender_key_path.clone(), + ca_cert_path.clone(), ); let receiver_config = QuicConfig::new( receiver_addr, - "cert/cert_receiver.pem".to_string(), - "cert/key_receiver.pem".to_string(), - "cert/ca-cert.pem".to_string(), + receiver_cert_path.clone(), + receiver_key_path.clone(), + ca_cert_path.clone(), ); sender_node @@ -368,6 +386,7 @@ async fn test_bundle_transfer_between_two_nodes() { fs::remove_dir_all(&sender_bundle_storage).ok(); fs::remove_dir_all(&receiver_bundle_storage).ok(); fs::remove_file(&bundle_path).ok(); + fs::remove_dir_all(&cert_dir).ok(); println!("✅ Cleanup completed"); println!("\n========================================"); @@ -404,6 +423,7 @@ async fn test_bundle_transfer_between_two_nodes() { fs::remove_dir_all(&sender_bundle_storage).ok(); fs::remove_dir_all(&receiver_bundle_storage).ok(); fs::remove_file(&bundle_path).ok(); + fs::remove_dir_all(&cert_dir).ok(); panic!("Bundle reception failed"); } @@ -416,9 +436,6 @@ async fn test_bundle_transfer_between_two_nodes() { megaengine::storage::node_model::delete_node_from_db(&receiver_node.node_id().to_string()) .await; - // 清理生成的证书文件 - let _ = std::fs::remove_file("cert/cert_sender.pem"); - let _ = std::fs::remove_file("cert/key_sender.pem"); - let _ = std::fs::remove_file("cert/cert_receiver.pem"); - let _ = std::fs::remove_file("cert/key_receiver.pem"); + // 清理生成的证书目录(包含 CA 文件) + fs::remove_dir_all(&cert_dir).ok(); } diff --git a/tests/gossip_three_nodes.rs b/tests/gossip_three_nodes.rs index 3c0cc6b..e0366ae 100644 --- a/tests/gossip_three_nodes.rs +++ b/tests/gossip_three_nodes.rs @@ -163,4 +163,6 @@ async fn test_gossip_three_nodes_message_relay() { let _ = std::fs::remove_file("cert/key2.pem"); let _ = std::fs::remove_file("cert/cert3.pem"); let _ = std::fs::remove_file("cert/key3.pem"); + let _ = std::fs::remove_file("cert/ca-cert.pem"); + let _ = std::fs::remove_file("cert/ca-cert-key.pem"); } From 96e39b9afcb4dfc4cfb9c84598c3a7bfc5882c32 Mon Sep 17 00:00:00 2001 From: wujian <353981613@qq.com> Date: Fri, 13 Mar 2026 17:35:27 +0800 Subject: [PATCH 40/42] fix --- src/mcp/sse_server.rs | 12 +++++------- tests/bundle_two_nodes.rs | 10 ++++++++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/mcp/sse_server.rs b/src/mcp/sse_server.rs index f660836..92e3bb7 100644 --- a/src/mcp/sse_server.rs +++ b/src/mcp/sse_server.rs @@ -154,13 +154,11 @@ async fn message_handler( if let Some(name) = name { match RepoMcpServer::execute_tool(name, args).await { - Ok(result_value) => { - Some(json!({ - "jsonrpc": "2.0", - "id": request.get("id"), - "result": result_value - })) - } + Ok(result_value) => Some(json!({ + "jsonrpc": "2.0", + "id": request.get("id"), + "result": result_value + })), Err(e) => Some(json!({ "jsonrpc": "2.0", "id": request.get("id"), diff --git a/tests/bundle_two_nodes.rs b/tests/bundle_two_nodes.rs index 93c53a4..9329727 100644 --- a/tests/bundle_two_nodes.rs +++ b/tests/bundle_two_nodes.rs @@ -71,8 +71,14 @@ async fn test_bundle_transfer_between_two_nodes() { fs::remove_dir_all(&cert_dir).ok(); fs::create_dir_all(&cert_dir).expect("Failed to create test cert directory"); - let sender_cert_path = cert_dir.join("cert_sender.pem").to_string_lossy().to_string(); - let sender_key_path = cert_dir.join("key_sender.pem").to_string_lossy().to_string(); + let sender_cert_path = cert_dir + .join("cert_sender.pem") + .to_string_lossy() + .to_string(); + let sender_key_path = cert_dir + .join("key_sender.pem") + .to_string_lossy() + .to_string(); let receiver_cert_path = cert_dir .join("cert_receiver.pem") .to_string_lossy() From 4fa5621c343b3eb4458e1825802f52bfda334bbf Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Fri, 13 Mar 2026 17:49:27 +0800 Subject: [PATCH 41/42] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/storage/mod.rs | 237 +++++++++++++++++++++++++-------------------- 1 file changed, 134 insertions(+), 103 deletions(-) diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 19ea185..fb9f841 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -214,64 +214,70 @@ async fn rebuild_repos_table(db: &DatabaseConnection) -> Result<()> { now_expr.to_string() }; - let sql = format!( - "BEGIN IMMEDIATE; - CREATE TABLE repos_new ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - creator TEXT NOT NULL, - description TEXT NOT NULL, - language TEXT NOT NULL DEFAULT '', - size INTEGER NOT NULL DEFAULT 0, - latest_commit_at INTEGER NOT NULL DEFAULT 0, - path TEXT NOT NULL, - bundle TEXT NOT NULL DEFAULT '', - is_external INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ); - INSERT OR REPLACE INTO repos_new ( - id, - name, - creator, - description, - language, - size, - latest_commit_at, - path, - bundle, - is_external, - created_at, - updated_at - ) - SELECT - id, - name, - creator, - description, - {language_expr}, - {size_expr}, - {latest_commit_expr}, - path, - {bundle_expr}, - {is_external_expr}, - {created_expr}, - {updated_expr} - FROM repos - WHERE id IS NOT NULL - AND name IS NOT NULL - AND creator IS NOT NULL - AND description IS NOT NULL - AND path IS NOT NULL; - DROP TABLE repos; - ALTER TABLE repos_new RENAME TO repos; - COMMIT;" + let create_sql = "\ + CREATE TABLE repos_new (\ + id TEXT PRIMARY KEY,\ + name TEXT NOT NULL,\ + creator TEXT NOT NULL,\ + description TEXT NOT NULL,\ + language TEXT NOT NULL DEFAULT '',\ + size INTEGER NOT NULL DEFAULT 0,\ + latest_commit_at INTEGER NOT NULL DEFAULT 0,\ + path TEXT NOT NULL,\ + bundle TEXT NOT NULL DEFAULT '',\ + is_external INTEGER NOT NULL DEFAULT 0,\ + created_at INTEGER NOT NULL,\ + updated_at INTEGER NOT NULL\ + );"; + + let insert_sql = format!( + "\ + INSERT OR REPLACE INTO repos_new (\ + id,\ + name,\ + creator,\ + description,\ + language,\ + size,\ + latest_commit_at,\ + path,\ + bundle,\ + is_external,\ + created_at,\ + updated_at\ + )\ + SELECT\ + id,\ + name,\ + creator,\ + description,\ + {language_expr},\ + {size_expr},\ + {latest_commit_expr},\ + path,\ + {bundle_expr},\ + {is_external_expr},\ + {created_expr},\ + {updated_expr}\ + FROM repos\ + WHERE id IS NOT NULL\ + AND name IS NOT NULL\ + AND creator IS NOT NULL\ + AND description IS NOT NULL\ + AND path IS NOT NULL;" ); - if let Err(err) = db.execute_unprepared(&sql).await { - let _ = db.execute_unprepared("ROLLBACK;").await; - return Err(err.into()); - } + let drop_sql = "DROP TABLE repos;"; + let rename_sql = "ALTER TABLE repos_new RENAME TO repos;"; + + let txn = db.begin().await?; + + txn.execute_unprepared(create_sql).await?; + txn.execute_unprepared(&insert_sql).await?; + txn.execute_unprepared(drop_sql).await?; + txn.execute_unprepared(rename_sql).await?; + + txn.commit().await?; Ok(()) } @@ -308,30 +314,49 @@ async fn rebuild_refs_table(db: &DatabaseConnection) -> Result<()> { "CAST(strftime('%s','now') AS INTEGER)" }; - let sql = format!( - "BEGIN IMMEDIATE; - CREATE TABLE refs_new ( + // Execute the migration statements one-by-one within a transaction + let txn = db.begin().await?; + + txn.execute(Statement::from_string( + DbBackend::Sqlite, + "CREATE TABLE refs_new ( repo_id TEXT NOT NULL, ref_name TEXT NOT NULL, commit_hash TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, PRIMARY KEY (repo_id, ref_name) - ); - INSERT OR REPLACE INTO refs_new (repo_id, ref_name, commit_hash, created_at, updated_at) + )" + .to_owned(), + )) + .await?; + + let insert_sql = format!( + "INSERT OR REPLACE INTO refs_new (repo_id, ref_name, commit_hash, created_at, updated_at) SELECT repo_id, ref_name, commit_hash, {created_expr}, {updated_expr} FROM refs - WHERE repo_id IS NOT NULL AND ref_name IS NOT NULL; - DROP TABLE refs; - ALTER TABLE refs_new RENAME TO refs; - COMMIT;" + WHERE repo_id IS NOT NULL AND ref_name IS NOT NULL" ); - if let Err(err) = db.execute_unprepared(&sql).await { - let rollback = "ROLLBACK;"; - let _ = db.execute_unprepared(rollback).await; - return Err(err.into()); - } + txn.execute(Statement::from_string( + DbBackend::Sqlite, + insert_sql, + )) + .await?; + + txn.execute(Statement::from_string( + DbBackend::Sqlite, + "DROP TABLE refs".to_owned(), + )) + .await?; + + txn.execute(Statement::from_string( + DbBackend::Sqlite, + "ALTER TABLE refs_new RENAME TO refs".to_owned(), + )) + .await?; + + txn.commit().await?; Ok(()) } @@ -416,9 +441,11 @@ async fn ensure_schema(db: &DatabaseConnection) -> Result<()> { /// 初始化数据库连接并创建表 pub async fn get_db_conn() -> Result { use std::collections::HashMap; + use std::sync::Arc; use tokio::sync::Mutex; - static DB_POOL: OnceCell>> = OnceCell::const_new(); + static DB_POOL: OnceCell>>>> = + OnceCell::const_new(); let pool = DB_POOL .get_or_init(|| async { Mutex::new(HashMap::new()) }) @@ -426,40 +453,44 @@ pub async fn get_db_conn() -> Result { let path = db_path(); - { - let map = pool.lock().await; - if let Some(db) = map.get(&path) { - return Ok(db.clone()); - } - } + // 为每个数据库路径维护一个独立的初始化单元,确保每个路径的初始化只执行一次 + let cell = { + let mut map = pool.lock().await; + map.entry(path.clone()) + .or_insert_with(|| Arc::new(OnceCell::const_new())) + .clone() + }; // 延迟初始化并缓存全局连接(仅第一次会执行创建表操作) - let db_path = path.clone(); - - // 确保目录存在 - if let Some(parent) = db_path.parent() { - fs::create_dir_all(parent).ok(); - } - - // 使用合适的 SQLite URL 格式 - let db_url = format!("sqlite://{}?mode=rwc", db_path.display()); - - let mut opt = ConnectOptions::new(db_url); - opt.max_connections(8) - .min_connections(1) - .connect_timeout(Duration::from_secs(8)) - .idle_timeout(Duration::from_secs(8)) - .sqlx_logging(false); - - let db = Database::connect(opt).await?; - - // 运行迁移/建表,兼容已有数据库结构升级 - ensure_schema(&db).await?; - - { - let mut map = pool.lock().await; - map.insert(path, db.clone()); - } + let db = cell + .get_or_try_init(|| { + let db_path = path.clone(); + async move { + // 确保目录存在 + if let Some(parent) = db_path.parent() { + fs::create_dir_all(parent).ok(); + } + + // 使用合适的 SQLite URL 格式 + let db_url = format!("sqlite://{}?mode=rwc", db_path.display()); + + let mut opt = ConnectOptions::new(db_url); + opt.max_connections(8) + .min_connections(1) + .connect_timeout(Duration::from_secs(8)) + .idle_timeout(Duration::from_secs(8)) + .sqlx_logging(false); + + let db = Database::connect(opt).await?; + + // 运行迁移/建表,兼容已有数据库结构升级 + ensure_schema(&db).await?; + + Ok::(db) + } + }) + .await? + .clone(); Ok(db) } From 8f0122aace818fc09466afbf452622de2a99b787 Mon Sep 17 00:00:00 2001 From: Wu jian <353981613@qq.com> Date: Fri, 13 Mar 2026 17:59:50 +0800 Subject: [PATCH 42/42] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/chat/service.rs | 44 +++++++++++++++++++++++------------------ src/git/git_repo.rs | 11 ++++------- src/gossip/message.rs | 28 +++++++++++++++++++++++--- src/identity/keypair.rs | 1 - src/mcp/sse_server.rs | 11 +++++++++-- 5 files changed, 63 insertions(+), 32 deletions(-) diff --git a/src/chat/service.rs b/src/chat/service.rs index 195f4f7..50e0c7a 100644 --- a/src/chat/service.rs +++ b/src/chat/service.rs @@ -118,36 +118,48 @@ async fn try_send_pending_msg( }; let data = serde_json::to_vec(&envelope)?; - let mgr = manager.lock().await; - // Try to find if we are connected or have a known route? // Gossip usually just floods peers if not knowing better. // If we have direct connection or routing table, use it. // For now: broadcast to all connected peers. - let peers = mgr.list_peers().await; + // Obtain the current peer list while holding the mutex only briefly. + let peers = { + let mgr = manager.lock().await; + mgr.list_peers().await + }; + if peers.is_empty() { return Err(anyhow!("No peers connected to send message")); } if peers.contains(&receiver_node_id) { // Direct send to receiver; propagate any error to the caller. - mgr.send_gossip_message(receiver_node_id.clone(), data.clone()) - .await - .map_err(|e| { - anyhow!( - "Failed to send gossip message to receiver {}: {}", - receiver_node_id, - e - ) - })?; + let send_result = { + let mgr = manager.lock().await; + mgr.send_gossip_message(receiver_node_id.clone(), data.clone()) + .await + }; + + send_result.map_err(|e| { + anyhow!( + "Failed to send gossip message to receiver {}: {}", + receiver_node_id, + e + ) + })?; } else { // Broadcast to all peers; require at least one successful send. let mut at_least_one_success = false; let mut last_err: Option = None; for peer in peers { - match mgr.send_gossip_message(peer.clone(), data.clone()).await { + let send_result = { + let mgr = manager.lock().await; + mgr.send_gossip_message(peer.clone(), data.clone()).await + }; + + match send_result { Ok(()) => { at_least_one_success = true; } @@ -252,12 +264,6 @@ pub async fn process_incoming_chat( signature: "".to_string(), }; - let ack_sig = my_node.sign_message(msg.msg_id.as_bytes())?; - let ack_msg = ChatAckMessage { - signature: hex::encode(ack_sig), - ..ack_msg - }; - let gossip_msg = GossipMessage::ChatAck(ack_msg); let mut signed_ack = SignedMessage { diff --git a/src/git/git_repo.rs b/src/git/git_repo.rs index 73609d4..88cd363 100644 --- a/src/git/git_repo.rs +++ b/src/git/git_repo.rs @@ -42,20 +42,17 @@ pub fn read_repo_refs(path: &str) -> Result format!("refs/heads/{}", name), - BranchType::Remote => format!("refs/remotes/{}", name), - }; + let ref_name = format!("refs/heads/{}", name); refs.insert(ref_name, oid.to_string()); } } diff --git a/src/gossip/message.rs b/src/gossip/message.rs index 7b11c39..bf8978f 100644 --- a/src/gossip/message.rs +++ b/src/gossip/message.rs @@ -137,11 +137,33 @@ impl SignedMessage { Ok(sign_message) } + fn canonicalize_value(value: serde_json::Value) -> serde_json::Value { + match value { + serde_json::Value::Object(map) => { + // Sort object keys to obtain a deterministic representation. + let mut entries: Vec<(String, serde_json::Value)> = map.into_iter().collect(); + entries.sort_by(|a, b| a.0.cmp(&b.0)); + + let mut new_map = serde_json::Map::new(); + for (k, v) in entries { + new_map.insert(k, Self::canonicalize_value(v)); + } + serde_json::Value::Object(new_map) + } + serde_json::Value::Array(vec) => { + serde_json::Value::Array(vec.into_iter().map(Self::canonicalize_value).collect()) + } + other => other, + } + } + pub fn self_hash(&self) -> Vec { let mut hasher = Sha256::new(); - // Canonicalize JSON serialization by converting to Value first (which sorts map keys) - let message_value = serde_json::to_value(&self.message).unwrap_or(serde_json::Value::Null); - let message_bytes = serde_json::to_vec(&message_value).unwrap_or_default(); + // Canonicalize JSON by recursively sorting object keys before serialization. + let message_value = + serde_json::to_value(&self.message).unwrap_or(serde_json::Value::Null); + let canonical_value = Self::canonicalize_value(message_value); + let message_bytes = serde_json::to_vec(&canonical_value).unwrap_or_default(); hasher.update(self.node_id.0.as_bytes()); hasher.update(&message_bytes); diff --git a/src/identity/keypair.rs b/src/identity/keypair.rs index 4b5ad37..0b32e76 100644 --- a/src/identity/keypair.rs +++ b/src/identity/keypair.rs @@ -1,4 +1,3 @@ -#![allow(deprecated)] use anyhow::{anyhow, Result}; use chacha20poly1305::{ aead::{Aead, KeyInit}, diff --git a/src/mcp/sse_server.rs b/src/mcp/sse_server.rs index 92e3bb7..f080d5c 100644 --- a/src/mcp/sse_server.rs +++ b/src/mcp/sse_server.rs @@ -6,6 +6,7 @@ use axum::{ IntoResponse, }, routing::{get, post}, + http::Method, Json, Router, }; use futures::stream::Stream; @@ -13,7 +14,7 @@ use serde::Deserialize; use serde_json::{json, Value}; use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; use tokio::sync::{mpsc, RwLock}; -use tower_http::cors::CorsLayer; +use tower_http::cors::{Any, CorsLayer}; use uuid::Uuid; // App state to hold active sessions @@ -56,10 +57,16 @@ pub async fn start_sse_server(addr: SocketAddr) -> anyhow::Result<()> { sessions: RwLock::new(HashMap::new()), }); + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods([Method::GET, Method::POST]) + .allow_headers(Any); + let app = Router::new() .route("/sse", get(sse_handler)) .route("/messages", post(message_handler)) - .with_state(state); + .with_state(state) + .layer(cors); tracing::info!("MCP SSE Server listening on {}", addr); let listener = tokio::net::TcpListener::bind(addr).await?;