From c3ca6297d49ca4bd5c2b86b33a31815d3032f1b4 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Sun, 31 Mar 2024 01:10:51 +0100 Subject: [PATCH 1/2] feat(iroh): implement basic author api - `author.export` - `author.import` - `author.delete` --- iroh-sync/src/actor.rs | 30 ++++++++++++++ iroh-sync/src/store/fs.rs | 11 +++++ iroh/src/client/authors.rs | 82 ++++++++++++++++++++++++++++++++++++- iroh/src/node/rpc.rs | 19 ++++++++- iroh/src/rpc_protocol.rs | 43 +++++++++++++++++-- iroh/src/sync_engine/rpc.rs | 21 +++++++++- 6 files changed, 198 insertions(+), 8 deletions(-) diff --git a/iroh-sync/src/actor.rs b/iroh-sync/src/actor.rs index bc03ee56ea..ea9ee0fc5f 100644 --- a/iroh-sync/src/actor.rs +++ b/iroh-sync/src/actor.rs @@ -30,6 +30,18 @@ enum Action { #[debug("reply")] reply: oneshot::Sender>, }, + #[display("ExportAuthor")] + ExportAuthor { + author: AuthorId, + #[debug("reply")] + reply: oneshot::Sender>>, + }, + #[display("DeleteAuthor")] + DeleteAuthor { + author: AuthorId, + #[debug("reply")] + reply: oneshot::Sender>, + }, #[display("NewReplica")] ImportNamespace { capability: Capability, @@ -473,6 +485,18 @@ impl SyncHandle { rx.await? } + pub async fn export_author(&self, author: AuthorId) -> Result> { + let (reply, rx) = oneshot::channel(); + self.send(Action::ExportAuthor { author, reply }).await?; + rx.await? + } + + pub async fn delete_author(&self, author: AuthorId) -> Result<()> { + let (reply, rx) = oneshot::channel(); + self.send(Action::DeleteAuthor { author, reply }).await?; + rx.await? + } + pub async fn import_namespace(&self, capability: Capability) -> Result { let (reply, rx) = oneshot::channel(); self.send(Action::ImportNamespace { capability, reply }) @@ -561,6 +585,12 @@ impl Actor { let id = author.id(); send_reply(reply, self.store.import_author(author).map(|_| id)) } + Action::ExportAuthor { author, reply } => { + send_reply(reply, self.store.get_author(&author)) + } + Action::DeleteAuthor { author, reply } => { + send_reply(reply, self.store.delete_author(author)) + } Action::ImportNamespace { capability, reply } => send_reply_with(reply, self, |this| { let id = capability.id(); let outcome = this.store.import_namespace(capability.clone())?; diff --git a/iroh-sync/src/store/fs.rs b/iroh-sync/src/store/fs.rs index ab88861d01..52f356abcd 100644 --- a/iroh-sync/src/store/fs.rs +++ b/iroh-sync/src/store/fs.rs @@ -276,6 +276,17 @@ impl Store { Ok(()) } + /// Delte an author. + pub fn delete_author(&self, author: AuthorId) -> Result<()> { + let write_tx = self.db.begin_write()?; + { + let mut author_table = write_tx.open_table(AUTHORS_TABLE)?; + author_table.remove(author.as_bytes())?; + } + write_tx.commit()?; + Ok(()) + } + /// List all author keys in this store. pub fn list_authors(&self) -> Result> { // TODO: avoid collect diff --git a/iroh/src/client/authors.rs b/iroh/src/client/authors.rs index 19a3ba75b6..367d4b7e53 100644 --- a/iroh/src/client/authors.rs +++ b/iroh/src/client/authors.rs @@ -1,9 +1,12 @@ use anyhow::Result; use futures::{Stream, TryStreamExt}; -use iroh_sync::AuthorId; +use iroh_sync::{Author, AuthorId}; use quic_rpc::{RpcClient, ServiceConnection}; -use crate::rpc_protocol::{AuthorCreateRequest, AuthorListRequest, ProviderService}; +use crate::rpc_protocol::{ + AuthorCreateRequest, AuthorDeleteRequest, AuthorExportRequest, AuthorImportRequest, + AuthorListRequest, ProviderService, +}; use super::flatten; @@ -28,4 +31,79 @@ where let stream = self.rpc.server_streaming(AuthorListRequest {}).await?; Ok(flatten(stream).map_ok(|res| res.author_id)) } + + /// Export the given author. + /// + /// Warning: This contains sensitive data. + pub async fn export(&self, author: AuthorId) -> Result> { + let res = self.rpc.rpc(AuthorExportRequest { author }).await??; + Ok(res.author) + } + + /// Import the given author. + /// + /// Warning: This contains sensitive data. + pub async fn import(&self, author: Author) -> Result<()> { + self.rpc.rpc(AuthorImportRequest { author }).await??; + Ok(()) + } + + /// Deletes the given author by id. + /// + /// Warning: This permanently removes this author. + pub async fn delete(&self, author: AuthorId) -> Result<()> { + self.rpc.rpc(AuthorDeleteRequest { author }).await??; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::node::Node; + + use super::*; + + #[tokio::test] + async fn test_authors() -> Result<()> { + let node = Node::memory().spawn().await?; + + let author_id = node.authors.create().await?; + + assert_eq!( + node.authors + .list() + .await? + .try_collect::>() + .await? + .len(), + 1 + ); + + let author = node + .authors + .export(author_id) + .await? + .expect("should have author"); + node.authors.delete(author_id).await?; + assert!(node + .authors + .list() + .await? + .try_collect::>() + .await? + .is_empty()); + + node.authors.import(author).await?; + assert_eq!( + node.authors + .list() + .await? + .try_collect::>() + .await? + .len(), + 1 + ); + + Ok(()) + } } diff --git a/iroh/src/node/rpc.rs b/iroh/src/node/rpc.rs index d20fd12ddd..a7d4c7606f 100644 --- a/iroh/src/node/rpc.rs +++ b/iroh/src/node/rpc.rs @@ -131,8 +131,23 @@ impl Handler { }) .await } - AuthorImport(_msg) => { - todo!() + AuthorImport(msg) => { + chan.rpc(msg, handler, |handler, req| async move { + handler.inner.sync.author_import(req).await + }) + .await + } + AuthorExport(msg) => { + chan.rpc(msg, handler, |handler, req| async move { + handler.inner.sync.author_export(req).await + }) + .await + } + AuthorDelete(msg) => { + chan.rpc(msg, handler, |handler, req| async move { + handler.inner.sync.author_delete(req).await + }) + .await } DocOpen(msg) => { chan.rpc(msg, handler, |handler, req| async move { diff --git a/iroh/src/rpc_protocol.rs b/iroh/src/rpc_protocol.rs index 024941a322..053150f51e 100644 --- a/iroh/src/rpc_protocol.rs +++ b/iroh/src/rpc_protocol.rs @@ -25,7 +25,7 @@ use iroh_net::{ use iroh_sync::{ actor::OpenState, store::{DownloadPolicy, Query}, - PeerIdBytes, {AuthorId, CapabilityKind, Entry, NamespaceId, SignedEntry}, + Author, PeerIdBytes, {AuthorId, CapabilityKind, Entry, NamespaceId, SignedEntry}, }; use quic_rpc::{ message::{BidiStreaming, BidiStreamingMsg, Msg, RpcMsg, ServerStreaming, ServerStreamingMsg}, @@ -484,11 +484,44 @@ pub struct AuthorCreateResponse { pub author_id: AuthorId, } +/// Delete an author +#[derive(Serialize, Deserialize, Debug)] +pub struct AuthorDeleteRequest { + /// The id of the author to delete + pub author: AuthorId, +} + +impl RpcMsg for AuthorDeleteRequest { + type Response = RpcResult; +} + +/// Response for [`AuthorDeleteRequest`] +#[derive(Serialize, Deserialize, Debug)] +pub struct AuthorDeleteResponse; + +/// Exports an author +#[derive(Serialize, Deserialize, Debug)] +pub struct AuthorExportRequest { + /// The id of the author to delete + pub author: AuthorId, +} + +impl RpcMsg for AuthorExportRequest { + type Response = RpcResult; +} + +/// Response for [`AuthorExportRequest`] +#[derive(Serialize, Deserialize, Debug)] +pub struct AuthorExportResponse { + /// The author + pub author: Option, +} + /// Import author from secret key #[derive(Serialize, Deserialize, Debug)] pub struct AuthorImportRequest { - /// The secret key for the author - pub key: KeyBytes, + /// The author to import + pub author: Author, } impl RpcMsg for AuthorImportRequest { @@ -1123,6 +1156,8 @@ pub enum ProviderRequest { AuthorList(AuthorListRequest), AuthorCreate(AuthorCreateRequest), AuthorImport(AuthorImportRequest), + AuthorExport(AuthorExportRequest), + AuthorDelete(AuthorDeleteRequest), } /// The response enum, listing all possible responses. @@ -1177,6 +1212,8 @@ pub enum ProviderResponse { AuthorList(RpcResult), AuthorCreate(RpcResult), AuthorImport(RpcResult), + AuthorExport(RpcResult), + AuthorDelete(RpcResult), } impl Service for ProviderService { diff --git a/iroh/src/sync_engine/rpc.rs b/iroh/src/sync_engine/rpc.rs index 4b3afffa3c..ed586b5356 100644 --- a/iroh/src/sync_engine/rpc.rs +++ b/iroh/src/sync_engine/rpc.rs @@ -6,7 +6,10 @@ use iroh_bytes::{store::Store as BaoStore, BlobFormat}; use iroh_sync::{Author, NamespaceSecret}; use tokio_stream::StreamExt; -use crate::rpc_protocol::{DocGetSyncPeersRequest, DocGetSyncPeersResponse}; +use crate::rpc_protocol::{ + AuthorDeleteRequest, AuthorDeleteResponse, AuthorExportRequest, AuthorExportResponse, + AuthorImportRequest, AuthorImportResponse, DocGetSyncPeersRequest, DocGetSyncPeersResponse, +}; use crate::{ rpc_protocol::{ AuthorCreateRequest, AuthorCreateResponse, AuthorListRequest, AuthorListResponse, @@ -60,6 +63,22 @@ impl SyncEngine { }) } + pub async fn author_import(&self, req: AuthorImportRequest) -> RpcResult { + let author_id = self.sync.import_author(req.author).await?; + Ok(AuthorImportResponse { author_id }) + } + + pub async fn author_export(&self, req: AuthorExportRequest) -> RpcResult { + let author = self.sync.export_author(req.author).await?; + + Ok(AuthorExportResponse { author }) + } + + pub async fn author_delete(&self, req: AuthorDeleteRequest) -> RpcResult { + self.sync.delete_author(req.author).await?; + Ok(AuthorDeleteResponse) + } + pub async fn doc_create(&self, _req: DocCreateRequest) -> RpcResult { let namespace = NamespaceSecret::new(&mut rand::rngs::OsRng {}); let id = namespace.id(); From 026948693c5c422e7e284ad274d6b259b158efc5 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Sun, 31 Mar 2024 01:18:23 +0100 Subject: [PATCH 2/2] feat(iroh-cli): import new author commands --- iroh-cli/src/commands/author.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/iroh-cli/src/commands/author.rs b/iroh-cli/src/commands/author.rs index c584d9d028..09880369bd 100644 --- a/iroh-cli/src/commands/author.rs +++ b/iroh-cli/src/commands/author.rs @@ -1,9 +1,10 @@ use anyhow::{bail, Result}; use clap::Parser; +use derive_more::FromStr; use futures::TryStreamExt; use iroh::base::base32::fmt_short; -use iroh::sync::AuthorId; +use iroh::sync::{Author, AuthorId}; use iroh::{client::Iroh, rpc_protocol::ProviderService}; use quic_rpc::ServiceConnection; @@ -19,6 +20,12 @@ pub enum AuthorCommands { #[clap(long)] switch: bool, }, + /// Delete an author. + Delete { author: AuthorId }, + /// Export an author + Export { author: AuthorId }, + /// Import an author + Import { author: String }, /// List authors. #[clap(alias = "ls")] List, @@ -53,6 +60,28 @@ impl AuthorCommands { println!("Active author is now {}", fmt_short(author_id.as_bytes())); } } + Self::Delete { author } => { + iroh.authors.delete(author).await?; + println!("Deleted author {}", fmt_short(author.as_bytes())); + } + Self::Export { author } => match iroh.authors.export(author).await? { + Some(author) => { + println!("{}", author); + } + None => { + println!("No author found {}", fmt_short(author)); + } + }, + Self::Import { author } => match Author::from_str(&author) { + Ok(author) => { + let id = author.id(); + iroh.authors.import(author).await?; + println!("Imported {}", fmt_short(id)); + } + Err(err) => { + eprintln!("Invalid author key: {}", err); + } + }, } Ok(()) }