@@ -3,17 +3,27 @@ use std::str::FromStr;
3
3
use anyhow:: Context ;
4
4
use automerge:: { Automerge , ReadDoc , transaction:: Transactable } ;
5
5
use clap:: Parser ;
6
+ use hex:: encode;
6
7
use iroh:: NodeId ;
7
8
use iroh_automerge_repo:: IrohRepo ;
8
- use samod:: { DocumentId , PeerId , Samod } ;
9
+
10
+ use samod:: { DocumentId , PeerId , Samod , storage:: TokioFilesystemStorage } ;
9
11
10
12
#[ derive( Parser , Debug ) ]
11
13
#[ command( author, version, about, long_about = None ) ]
12
14
struct Args {
13
- /// A NodeId to connect to to keep syncing with
15
+ /// A NodeId to connect to and keep syncing with
14
16
#[ clap( long) ]
15
17
sync_with : Option < NodeId > ,
16
18
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
+
17
27
#[ clap( subcommand) ]
18
28
command : Commands ,
19
29
}
@@ -45,6 +55,21 @@ pub enum Commands {
45
55
/// The document's ID
46
56
doc : String ,
47
57
} ,
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 ,
48
73
}
49
74
50
75
#[ tokio:: main]
@@ -55,10 +80,56 @@ async fn main() -> anyhow::Result<()> {
55
80
56
81
let args = Args :: parse ( ) ;
57
82
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( ) ) ;
59
125
60
126
let samod = Samod :: build_tokio ( )
61
127
. 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
+ ) ) )
62
133
. load ( )
63
134
. await ;
64
135
let proto = IrohRepo :: new ( endpoint. clone ( ) , samod) ;
@@ -100,6 +171,17 @@ async fn main() -> anyhow::Result<()> {
100
171
. map_err ( debug_err) ?;
101
172
println ! ( "Updated document" ) ;
102
173
}
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
+ }
103
185
Commands :: Print { doc } => {
104
186
let doc_id = DocumentId :: from_str ( & doc) . context ( "Couldn't parse document ID" ) ?;
105
187
let doc = proto
@@ -115,6 +197,62 @@ async fn main() -> anyhow::Result<()> {
115
197
anyhow:: Ok ( ( ) )
116
198
} ) ?;
117
199
}
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
+ }
118
256
}
119
257
120
258
println ! ( "Waiting for Ctrl+C" ) ;
0 commit comments