upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-26 16:11:05 +0530
committerYour Name <you@example.com>2026-05-26 16:11:05 +0530
commit8816a192c95cf539b65975469a2d61aed46f0414 (patch)
tree7590318244a56fabbfa6919ef6d0fab5be529134 /src/main.rs
feat: initial implementation of grasp-mirror daemon
GRASP mirror daemon that discovers repos from watched npubs and mirrors git data + Nostr events across all known GRASP servers for redundancy. Features: - Configurable npub watch list via .env (MIRROR_NPUBS) - TOML config for GRASP server list, index relays, storage paths - NIP-11 verification of GRASP servers on startup - Discovery of repos via kind:30617 announcements on index relays - Git mirroring (bare clone + push --mirror) to missing GRASP servers - Nostr event forwarding to all GRASP server embedded relays - SQLite state tracking for sync status and event dedup - Optional signing key for updating announcements with new clone URLs - CLI subcommands: daemon, status, verify, mirror-once Architecture: config.rs - TOML + .env config loading db.rs - SQLite state tracking health.rs - NIP-11 GRASP server verification discovery.rs - Relay subscription, kind:30617 parsing git_mirror.rs - Bare clone + push to GRASP servers nostr_mirror.rs - Event forwarding to all GRASP relays signing.rs - Optional announcement updates main.rs - CLI entry point, daemon loop
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs279
1 files changed, 279 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..b709d44
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,279 @@
1mod config;
2mod db;
3mod discovery;
4mod git_mirror;
5mod health;
6mod nostr_mirror;
7mod signing;
8
9use anyhow::Result;
10use clap::Parser;
11use std::path::PathBuf;
12use std::sync::Arc;
13
14#[derive(Parser, Debug)]
15#[command(name = "grasp-mirror", about = "GRASP mirror daemon")]
16struct Cli {
17 #[arg(short, long, default_value = "config.toml")]
18 config: PathBuf,
19
20 #[command(subcommand)]
21 command: Option<Command>,
22}
23
24#[derive(clap::Subcommand, Debug)]
25enum Command {
26 Daemon,
27 Status,
28 Verify,
29 MirrorOnce,
30}
31
32#[tokio::main]
33async fn main() -> Result<()> {
34 tracing_subscriber::fmt()
35 .with_env_filter(
36 tracing_subscriber::EnvFilter::try_from_default_env()
37 .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
38 )
39 .init();
40
41 let cli = Cli::parse();
42 let config = config::ResolvedConfig::load(&cli.config)?;
43
44 tracing::info!(
45 npubs = config.npubs.len(),
46 servers = config.servers.known.len(),
47 index_relays = config.discovery.index_relays.len(),
48 "starting grasp-mirror"
49 );
50
51 let db = db::MirrorDb::open(&config.storage.database).await?;
52
53 match cli.command.unwrap_or(Command::Daemon) {
54 Command::Daemon => run_daemon(config, db).await,
55 Command::Status => run_status(db).await,
56 Command::Verify => run_verify(config).await,
57 Command::MirrorOnce => run_mirror_once(config, db).await,
58 }
59}
60
61fn build_nostr_client(relay_urls: &[String]) -> nostr_sdk::Client {
62 let client = nostr_sdk::Client::default();
63 for url in relay_urls {
64 let _ = client.add_relay(url);
65 }
66 client
67}
68
69async fn run_daemon(config: config::ResolvedConfig, db: db::MirrorDb) -> Result<()> {
70 let db = Arc::new(db);
71 let config = Arc::new(config);
72
73 let servers = health::verify_all_servers(&config.servers.known).await;
74 let healthy: Vec<_> = servers
75 .values()
76 .filter(|s| s.is_grasp_server())
77 .cloned()
78 .collect();
79
80 tracing::info!(
81 total = servers.len(),
82 healthy = healthy.len(),
83 "server verification complete"
84 );
85
86 let healthy = Arc::new(healthy);
87
88 let relay_urls = config.relay_urls();
89 let nostr_client = build_nostr_client(&relay_urls);
90 nostr_client.connect().await;
91
92 let mirror = git_mirror::GitMirror::new(&config.storage.mirror_dir);
93 let nostr_mirror = nostr_mirror::NostrMirror::new(nostr_client.clone());
94
95 let mut interval = tokio::time::interval(std::time::Duration::from_secs(
96 config.discovery.poll_interval_secs,
97 ));
98
99 tracing::info!(
100 "daemon started, polling every {}s",
101 config.discovery.poll_interval_secs
102 );
103
104 loop {
105 tokio::select! {
106 _ = interval.tick() => {
107 if let Err(e) = mirror_cycle(
108 &config,
109 &db,
110 &nostr_client,
111 &mirror,
112 &nostr_mirror,
113 &healthy,
114 ).await {
115 tracing::error!(error = %e, "mirror cycle failed");
116 }
117 }
118 _ = tokio::signal::ctrl_c() => {
119 tracing::info!("shutting down");
120 break;
121 }
122 }
123 }
124
125 Ok(())
126}
127
128async fn mirror_cycle(
129 config: &Arc<config::ResolvedConfig>,
130 db: &Arc<db::MirrorDb>,
131 nostr_client: &nostr_sdk::Client,
132 mirror: &git_mirror::GitMirror,
133 nostr_mirror: &nostr_mirror::NostrMirror,
134 servers: &Arc<Vec<health::GraspServer>>,
135) -> Result<()> {
136 tracing::info!("starting mirror cycle");
137
138 let relay_urls = config.relay_urls();
139 let repos = discovery::discover_repos_from_relays(nostr_client, &config.npubs, &relay_urls)
140 .await?;
141
142 tracing::info!(count = repos.len(), "discovered repos");
143
144 discovery::persist_discovered_repos(db, &repos).await?;
145
146 let server_map: std::collections::HashMap<String, health::GraspServer> = servers
147 .iter()
148 .map(|s| (s.domain.clone(), s.clone()))
149 .collect();
150
151 for repo in &repos {
152 let missing = discovery::identify_missing_servers(repo, &server_map);
153
154 if missing.is_empty() {
155 tracing::debug!(identifier = %repo.identifier, "repo already on all servers");
156 continue;
157 }
158
159 tracing::info!(
160 identifier = %repo.identifier,
161 missing = missing.iter().map(|s| s.domain.as_str()).collect::<Vec<_>>().join(", "),
162 "mirroring to missing servers"
163 );
164
165 mirror.mirror_repo_to_servers(db, repo, &missing).await?;
166 nostr_mirror.forward_repo_events(db, repo, servers).await?;
167 }
168
169 nostr_mirror
170 .sync_all_events(db, &config.npubs, servers)
171 .await?;
172
173 tracing::info!("mirror cycle complete");
174 Ok(())
175}
176
177async fn run_status(db: db::MirrorDb) -> Result<()> {
178 let repos = db.get_all_repos().await?;
179 let syncs = db.get_sync_summary().await?;
180
181 println!("=== GRASP Mirror Status ===\n");
182 println!("Repos tracked: {}\n", repos.len());
183
184 for repo in &repos {
185 println!(" {} ({})", repo.identifier, &repo.pubkey[..12]);
186 }
187
188 println!("\nSync records: {}\n", syncs.len());
189 for sync in &syncs {
190 let status = if sync.git_synced && sync.nostr_synced {
191 "OK"
192 } else if sync.error.is_some() {
193 "ERR"
194 } else {
195 "PENDING"
196 };
197 println!(
198 " repo:{} server:{} git:{} nostr:{} [{}]",
199 sync.repo_id,
200 sync.server_domain,
201 if sync.git_synced { "Y" } else { "N" },
202 if sync.nostr_synced { "Y" } else { "N" },
203 status,
204 );
205 if let Some(err) = &sync.error {
206 println!(" error: {}", err);
207 }
208 }
209
210 Ok(())
211}
212
213async fn run_verify(config: config::ResolvedConfig) -> Result<()> {
214 let servers = health::verify_all_servers(&config.servers.known).await;
215
216 println!("=== GRASP Server Verification ===\n");
217
218 for (domain, server) in &servers {
219 let grasp = server.is_grasp_server();
220 let version = server
221 .nip11
222 .as_ref()
223 .and_then(|i| i.version.clone())
224 .unwrap_or_else(|| "unknown".to_string());
225 let grasps = server
226 .nip11
227 .as_ref()
228 .and_then(|i| i.supported_grasps.clone())
229 .map(|g| g.join(", "))
230 .unwrap_or_else(|| "NONE".to_string());
231
232 println!(
233 " {} [{}] v={} GRASP={}",
234 domain,
235 if grasp { "OK" } else { "SKIP" },
236 version,
237 grasps,
238 );
239 }
240
241 let healthy = servers.values().filter(|s| s.is_grasp_server()).count();
242 println!(
243 "\n{}/{} verified as GRASP servers",
244 healthy,
245 servers.len()
246 );
247
248 Ok(())
249}
250
251async fn run_mirror_once(config: config::ResolvedConfig, db: db::MirrorDb) -> Result<()> {
252 let db = Arc::new(db);
253 let config = Arc::new(config);
254
255 let servers = health::verify_all_servers(&config.servers.known).await;
256 let healthy: Vec<_> = servers
257 .values()
258 .filter(|s| s.is_grasp_server())
259 .cloned()
260 .collect();
261
262 let relay_urls = config.relay_urls();
263 let nostr_client = build_nostr_client(&relay_urls);
264 nostr_client.connect().await;
265
266 let mirror = git_mirror::GitMirror::new(&config.storage.mirror_dir);
267 let nostr_mirror = nostr_mirror::NostrMirror::new(nostr_client.clone());
268 let healthy = Arc::new(healthy);
269
270 mirror_cycle(
271 &config,
272 &db,
273 &nostr_client,
274 &mirror,
275 &nostr_mirror,
276 &healthy,
277 )
278 .await
279}