Skip to content

Commit 5eb25a8

Browse files
relvesmatheus23
andauthored
feat: (iroh-automerge-repo) Delete, Subscribe, and persistent NodeID (#127)
* feat: (iroh-automerge-repo) Delete, Subscribe, and persistent NodeID * Fix: address review feedback * Apply clippy changes to frosty * chore: Update samod version * Small style fixes --------- Co-authored-by: Philipp Krüger <[email protected]>
1 parent 1d67667 commit 5eb25a8

File tree

4 files changed

+155
-15
lines changed

4 files changed

+155
-15
lines changed

frosty/src/main.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -392,16 +392,14 @@ async fn cosign_daemon(args: CosignArgs) -> anyhow::Result<()> {
392392
.extension()
393393
.map(|ext| ext == "secret")
394394
.unwrap_or_default()
395+
&& let Some(stem) = path.file_stem()
396+
&& let Some(text) = stem.to_str()
395397
{
396-
if let Some(stem) = path.file_stem() {
397-
if let Some(text) = stem.to_str() {
398-
let key = iroh::PublicKey::from_str(text)?;
399-
let secret_share_bytes = fs::read(&path)?;
400-
let secret_share = SecretShare::deserialize(&secret_share_bytes)?;
401-
let key_package = frost::keys::KeyPackage::try_from(secret_share)?;
402-
keys.push((key, key_package));
403-
}
404-
}
398+
let key = iroh::PublicKey::from_str(text)?;
399+
let secret_share_bytes = fs::read(&path)?;
400+
let secret_share = SecretShare::deserialize(&secret_share_bytes)?;
401+
let key_package = frost::keys::KeyPackage::try_from(secret_share)?;
402+
keys.push((key, key_package));
405403
}
406404
}
407405
if !keys.is_empty() {

iroh-automerge-repo/Cargo.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

iroh-automerge-repo/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ bytes = "1.10.1"
99
derive_more = { version = "2.0.1", features = ["debug"] }
1010
iroh = "0.91.1"
1111
n0-future = "0.2"
12-
samod = { version = "0.2.0", features = ["tokio"] }
12+
samod = { version = "0.2.1", features = ["tokio"] }
1313
tokio = { version = "1.47.1", features = ["sync", "net"] }
1414
tokio-util = { version = "0.7.15", features = ["codec"] }
1515
tracing = "0.1.41"
@@ -18,3 +18,5 @@ tracing = "0.1.41"
1818
automerge = "0.6.1"
1919
clap = { version = "4.5.41", features = ["derive"] }
2020
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
21+
rand = "0.8.5"
22+
hex = "0.4.3"

iroh-automerge-repo/src/main.rs

Lines changed: 141 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,27 @@ use std::str::FromStr;
33
use anyhow::Context;
44
use automerge::{Automerge, ReadDoc, transaction::Transactable};
55
use clap::Parser;
6+
use hex::encode;
67
use iroh::NodeId;
78
use iroh_automerge_repo::IrohRepo;
8-
use samod::{DocumentId, PeerId, Samod};
9+
10+
use samod::{DocumentId, PeerId, Samod, storage::TokioFilesystemStorage};
911

1012
#[derive(Parser, Debug)]
1113
#[command(author, version, about, long_about = None)]
1214
struct Args {
13-
/// A NodeId to connect to to keep syncing with
15+
/// A NodeId to connect to and keep syncing with
1416
#[clap(long)]
1517
sync_with: Option<NodeId>,
1618

19+
/// Path where storage files will be created
20+
#[clap(long, default_value = ".")]
21+
storage_path: String,
22+
23+
/// Print the secret key
24+
#[clap(long)]
25+
print_secret_key: bool,
26+
1727
#[clap(subcommand)]
1828
command: Commands,
1929
}
@@ -45,6 +55,21 @@ pub enum Commands {
4555
/// The document's ID
4656
doc: String,
4757
},
58+
/// Subscribe to a document and print changes as they occur
59+
Subscribe {
60+
/// The document's ID to subscribe to
61+
doc: String,
62+
},
63+
/// Delete a key from the document
64+
Delete {
65+
/// The document's ID of the document to modify
66+
doc: String,
67+
68+
/// The key you want to delete
69+
key: String,
70+
},
71+
/// Host the app and serve existing documents from the automerge-repo stored in at config-path
72+
Host,
4873
}
4974

5075
#[tokio::main]
@@ -55,10 +80,56 @@ async fn main() -> anyhow::Result<()> {
5580

5681
let args = Args::parse();
5782

58-
let endpoint = iroh::Endpoint::builder().discovery_n0().bind().await?;
83+
// Pull in the secret key from the environment variable if it exists
84+
// This key is the public Node ID used to identify the node in the network
85+
// If not provided, a random key will be generated, and a new Node ID will
86+
// be assigned each time the app is started
87+
let secret_key = match std::env::var("IROH_SECRET") {
88+
Ok(key_hex) => match iroh::SecretKey::from_str(&key_hex) {
89+
Ok(key) => Some(key),
90+
Err(_) => {
91+
println!("invalid IROH_SECRET provided: not valid hex");
92+
None
93+
}
94+
},
95+
Err(_) => None,
96+
};
97+
98+
let secret_key = match secret_key {
99+
Some(key) => {
100+
println!("Using existing key: {}", key.public());
101+
key
102+
}
103+
None => {
104+
println!("Generating new key");
105+
let mut rng = rand::rngs::OsRng;
106+
iroh::SecretKey::generate(&mut rng)
107+
}
108+
};
109+
110+
if args.print_secret_key {
111+
println!("Secret Key: {}", encode(secret_key.to_bytes()));
112+
println!(
113+
"Set env var for persistent Node ID: export IROH_SECRET={}",
114+
encode(secret_key.to_bytes())
115+
);
116+
}
117+
118+
let endpoint = iroh::Endpoint::builder()
119+
.discovery_n0()
120+
.secret_key(secret_key)
121+
.bind()
122+
.await?;
123+
124+
println!("Node ID: {}", endpoint.node_id());
59125

60126
let samod = Samod::build_tokio()
61127
.with_peer_id(PeerId::from_string(endpoint.node_id().to_string()))
128+
.with_storage(TokioFilesystemStorage::new(format!(
129+
"{}/{}",
130+
args.storage_path,
131+
endpoint.node_id()
132+
)))
62133
.load()
63134
.await;
64135
let proto = IrohRepo::new(endpoint.clone(), samod);
@@ -100,6 +171,17 @@ async fn main() -> anyhow::Result<()> {
100171
.map_err(debug_err)?;
101172
println!("Updated document");
102173
}
174+
Commands::Delete { doc, key } => {
175+
let doc_id = DocumentId::from_str(&doc).context("Couldn't parse document ID")?;
176+
let doc = proto
177+
.repo()
178+
.find(doc_id)
179+
.await?
180+
.context("Couldn't find document with this ID")?;
181+
doc.with_document(|doc| doc.transact(|tx| tx.delete(automerge::ROOT, key)))
182+
.map_err(debug_err)?;
183+
println!("Key deleted!");
184+
}
103185
Commands::Print { doc } => {
104186
let doc_id = DocumentId::from_str(&doc).context("Couldn't parse document ID")?;
105187
let doc = proto
@@ -115,6 +197,62 @@ async fn main() -> anyhow::Result<()> {
115197
anyhow::Ok(())
116198
})?;
117199
}
200+
Commands::Subscribe { doc } => {
201+
let doc_id = DocumentId::from_str(&doc).context("Couldn't parse document ID")?;
202+
let doc = proto
203+
.repo()
204+
.find(doc_id.clone())
205+
.await?
206+
.context("Couldn't find document with this ID")?;
207+
208+
println!("Subscribing to document {} for changes...", doc_id);
209+
210+
// Print initial state
211+
println!("Initial document state:");
212+
doc.with_document(|doc| {
213+
for key in doc.keys(automerge::ROOT) {
214+
let (value, _) = doc.get(automerge::ROOT, &key)?.expect("missing value");
215+
println!(" {key}={value}");
216+
}
217+
anyhow::Ok(())
218+
})?;
219+
220+
// Set up polling for changes (no push available yet)
221+
tokio::spawn(async move {
222+
// Track the last known heads to detect changes
223+
let mut last_heads = doc.with_document(|doc| doc.get_heads());
224+
loop {
225+
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
226+
227+
let current_heads = doc.with_document(|doc| doc.get_heads());
228+
if current_heads == last_heads {
229+
continue;
230+
}
231+
232+
last_heads = current_heads;
233+
234+
println!("Document changed! New state:");
235+
236+
// When changes are detected, print the updated document contents...
237+
if let Err(e) = doc.with_document(|current_doc| {
238+
for key in current_doc.keys(automerge::ROOT) {
239+
let (value, _) = current_doc
240+
.get(automerge::ROOT, &key)?
241+
.expect("missing value");
242+
println!(" {key}={value}");
243+
}
244+
anyhow::Ok(())
245+
}) {
246+
eprintln!("Error reading document content: {e}");
247+
}
248+
}
249+
});
250+
}
251+
Commands::Host => {
252+
println!("Hosting existing documents...");
253+
println!("Repository is now hosted and ready for sync operations.");
254+
println!("Other nodes can connect to sync with this repository.");
255+
}
118256
}
119257

120258
println!("Waiting for Ctrl+C");

0 commit comments

Comments
 (0)