High-performance implementation of plcbundle written in Rust
at main 285 lines 12 kB view raw
1// Root page handler 2 3use crate::constants; 4use crate::format::{format_number, format_std_duration_verbose}; 5use crate::server::ServerState; 6use crate::server::utils::extract_base_url; 7use axum::{ 8 extract::State, 9 http::{HeaderMap, HeaderValue, StatusCode, Uri}, 10 response::IntoResponse, 11}; 12 13pub async fn handle_root( 14 State(state): State<ServerState>, 15 uri: Uri, 16 headers: HeaderMap, 17) -> impl IntoResponse { 18 let index = state.manager.get_index(); 19 let bundle_count = index.bundles.len(); 20 let origin = state.manager.get_plc_origin(); 21 let uptime = state.start_time.elapsed(); 22 let mempool_stats_opt = if state.config.sync_mode { 23 state.manager.get_mempool_stats().ok() 24 } else { 25 None 26 }; 27 28 let mut response = String::new(); 29 30 // ASCII art banner 31 response.push('\n'); 32 response.push_str(&crate::server::get_ascii_art_banner(&state.config.version)); 33 response.push('\n'); 34 response.push_str(&format!(" {} server\n\n", constants::BINARY_NAME)); 35 response.push_str("*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*\n"); 36 response.push_str("| ⚠️ Preview Version – Do Not Use In Production! |\n"); 37 response.push_str("*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*\n"); 38 response.push_str("| This project and plcbundle specification is currently |\n"); 39 response.push_str("| unstable and under heavy development. Things can break at |\n"); 40 response.push_str("| any time. Do not use this for production systems. |\n"); 41 response.push_str("| Please wait for the 1.0 release. |\n"); 42 response.push_str("|________________________________________________________________|\n"); 43 response.push('\n'); 44 response.push_str("What is PLC Bundle?\n"); 45 response.push_str("━━━━━━━━━━━━━━━━━━━━\n"); 46 response.push_str("plcbundle archives AT Protocol's DID PLC Directory operations into\n"); 47 response.push_str("immutable, cryptographically-chained bundles of 10,000 operations.\n\n"); 48 response.push_str("More info: https://tangled.org/@atscan.net/plcbundle\n\n"); 49 50 if bundle_count > 0 { 51 let first_bundle = index.bundles.first().map(|b| b.bundle_number).unwrap_or(0); 52 let last_bundle = index.last_bundle; 53 let total_size: u64 = index.bundles.iter().map(|b| b.compressed_size).sum(); 54 let total_uncompressed: u64 = index.bundles.iter().map(|b| b.uncompressed_size).sum(); 55 56 response.push_str("Bundles\n"); 57 response.push_str("━━━━━━━\n"); 58 response.push_str(&format!(" Origin: {}\n", origin)); 59 response.push_str(&format!(" Bundle count: {}\n", bundle_count)); 60 61 if let Some(last_meta) = index.get_bundle(last_bundle) { 62 response.push_str(&format!( 63 " Last bundle: {} ({})\n", 64 last_bundle, 65 last_meta.end_time.split('T').next().unwrap_or("") 66 )); 67 } 68 69 response.push_str(&format!( 70 " Range: {} - {}\n", 71 first_bundle, last_bundle 72 )); 73 response.push_str(&format!( 74 " Total size: {:.2} MB\n", 75 total_size as f64 / (1000.0 * 1000.0) 76 )); 77 response.push_str(&format!( 78 " Uncompressed: {:.2} MB ({:.2}x)\n", 79 total_uncompressed as f64 / (1000.0 * 1000.0), 80 total_uncompressed as f64 / total_size as f64 81 )); 82 83 if let Some(first_meta) = index.get_bundle(first_bundle) { 84 response.push_str(&format!("\n Root: {}\n", first_meta.hash)); 85 } 86 if let Some(last_meta) = index.get_bundle(last_bundle) { 87 response.push_str(&format!(" Head: {}\n", last_meta.hash)); 88 } 89 } 90 91 if let Some(mempool_stats) = mempool_stats_opt.as_ref() { 92 response.push_str("\nMempool\n"); 93 response.push_str("━━━━━━━\n"); 94 response.push_str(&format!( 95 " Target bundle: {}\n", 96 mempool_stats.target_bundle 97 )); 98 response.push_str(&format!( 99 " Operations: {} / {}\n", 100 mempool_stats.count, 101 constants::BUNDLE_SIZE 102 )); 103 104 if mempool_stats.count > 0 { 105 let progress = (mempool_stats.count as f64 / constants::BUNDLE_SIZE as f64) * 100.0; 106 response.push_str(&format!(" Progress: {:.1}%\n", progress)); 107 108 let bar_width = 50; 109 let filled = ((bar_width as f64) 110 * (mempool_stats.count as f64 / constants::BUNDLE_SIZE as f64)) 111 as usize; 112 let bar = 113 "".repeat(filled.min(bar_width)) + &"".repeat(bar_width.saturating_sub(filled)); 114 response.push_str(&format!(" [{}]\n", bar)); 115 116 if let Some(first_time) = mempool_stats.first_time { 117 response.push_str(&format!( 118 " First op: {}\n", 119 first_time.format("%Y-%m-%d %H:%M:%S") 120 )); 121 } 122 if let Some(last_time) = mempool_stats.last_time { 123 response.push_str(&format!( 124 " Last op: {}\n", 125 last_time.format("%Y-%m-%d %H:%M:%S") 126 )); 127 } 128 } else { 129 response.push_str(" (empty)\n"); 130 } 131 } 132 133 if state.config.enable_resolver { 134 response.push_str("\nDID Resolver\n"); 135 response.push_str("━━━━━━━━━━━━\n"); 136 response.push_str(" Status: enabled\n"); 137 138 let did_stats = state.manager.get_did_index_stats(); 139 if did_stats 140 .get("exists") 141 .and_then(|v| v.as_bool()) 142 .unwrap_or(false) 143 { 144 let indexed_dids = did_stats 145 .get("total_dids") 146 .and_then(|v| v.as_i64()) 147 .unwrap_or(0) as u64; 148 let mempool_dids = mempool_stats_opt 149 .as_ref() 150 .and_then(|s| s.did_count) 151 .unwrap_or(0) as u64; 152 153 let total_dids = indexed_dids + mempool_dids; 154 response.push_str(&format!( 155 " DIDs: {} (Bundles {} + Mempool {})\n", 156 format_number(total_dids), 157 format_number(indexed_dids), 158 format_number(mempool_dids) 159 )); 160 } 161 response.push('\n'); 162 } 163 164 response.push_str("Server Stats\n"); 165 response.push_str("━━━━━━━━━━━━\n"); 166 response.push_str(&format!( 167 " Version: v{} (rust)\n", 168 state.config.version 169 )); 170 response.push_str(&format!( 171 " Sync mode: {}\n", 172 state.config.sync_mode 173 )); 174 response.push_str(&format!( 175 " WebSocket: {}\n", 176 state.config.enable_websocket 177 )); 178 if let Some(handle_resolver) = state.manager.get_handle_resolver_base_url() { 179 response.push_str(&format!(" Handle Resolver: {}\n", handle_resolver)); 180 } else { 181 response.push_str(" Handle Resolver: (not configured)\n"); 182 } 183 response.push_str(&format!( 184 " Uptime: {}\n", 185 format_std_duration_verbose(uptime) 186 )); 187 188 // Get base URL from request 189 let base_url = extract_base_url(&headers, &uri); 190 response.push_str("\n\nAPI Endpoints\n"); 191 response.push_str("━━━━━━━━━━━━━\n"); 192 response.push_str(" GET / This info page\n"); 193 response.push_str(" GET /index.json Full bundle index\n"); 194 response.push_str(" GET /bundle/:number Bundle metadata (JSON)\n"); 195 response.push_str(" GET /data/:number Raw bundle (zstd compressed)\n"); 196 response.push_str(" GET /jsonl/:number Decompressed JSONL stream\n"); 197 response.push_str(" GET /op/:cursor Get single operation\n"); 198 response.push_str(" GET /status Server status\n"); 199 response.push_str(" GET /mempool Mempool operations (JSONL)\n"); 200 201 if state.config.enable_websocket { 202 response.push_str("\nWebSocket Endpoints\n"); 203 response.push_str("━━━━━━━━━━━━━━━━━━━━━━━━\n"); 204 response.push_str(" WS /ws Live stream (new operations only)\n"); 205 response.push_str(" WS /ws?cursor=0 Stream all from beginning\n"); 206 response.push_str(" WS /ws?cursor=N Stream from cursor N\n\n"); 207 } 208 209 if state.config.enable_resolver { 210 response.push_str("\nDID Resolution\n"); 211 response.push_str("━━━━━━━━━━━━━━\n"); 212 response.push_str(" GET /:did DID Document (W3C format)\n"); 213 response.push_str(" GET /:did/data PLC State (raw format)\n"); 214 response.push_str(" GET /:did/log/audit Operation history\n"); 215 response.push_str(" GET /random Random DID sample (JSON)\n"); 216 } 217 218 response.push_str("\nCursor Format\n"); 219 response.push_str("━━━━━━━━━━━━━\n"); 220 response.push_str(" Global record number: ((bundle - 1) × 10,000) + position\n"); 221 response.push_str(" Example: global 0 = bundle 1, position 0\n"); 222 response.push_str(" Default: starts from latest (skips all historical data)\n"); 223 response.push_str(" Positions are 0-indexed (per bundle: 0..9,999)\n"); 224 response.push_str(" Example: global 10000 = bundle 2, position 0\n"); 225 226 let bundled_ops = crate::constants::total_operations_from_bundles(index.last_bundle); 227 let mempool_ops = mempool_stats_opt 228 .as_ref() 229 .map(|s| s.count as u64) 230 .unwrap_or(0); 231 let current_latest = bundled_ops + mempool_ops; 232 233 if mempool_ops > 0 { 234 response.push_str(&format!( 235 " Current latest: {} ({} bundled + {} mempool)\n\n", 236 format_number(current_latest), 237 format_number(bundled_ops), 238 format_number(mempool_ops) 239 )); 240 } else { 241 response.push_str(&format!( 242 " Current latest: {} ({} bundled)\n\n", 243 format_number(current_latest), 244 format_number(bundled_ops) 245 )); 246 } 247 248 response.push_str("\nExamples\n"); 249 response.push_str("━━━━━━━━\n"); 250 response.push_str(&format!(" curl {}/bundle/1\n", base_url)); 251 response.push_str(&format!( 252 " curl {}/data/42 -o 000042.jsonl.zst\n", 253 base_url 254 )); 255 response.push_str(&format!(" curl {}/jsonl/1\n", base_url)); 256 response.push_str(&format!(" curl {}/op/0\n", base_url)); 257 response.push_str(&format!(" curl {}/random?count=10&seed=12345\n", base_url)); 258 259 if state.config.sync_mode { 260 response.push_str(&format!(" curl {}/status\n", base_url)); 261 response.push_str(&format!(" curl {}/mempool\n", base_url)); 262 } 263 264 if state.config.enable_websocket { 265 let ws_url = if base_url.starts_with("http://") { 266 base_url.replace("http://", "ws://") 267 } else if base_url.starts_with("https://") { 268 base_url.replace("https://", "wss://") 269 } else { 270 format!("ws://{}", base_url) 271 }; 272 response.push_str(&format!(" websocat {}/ws\n", ws_url)); 273 response.push_str(&format!(" websocat '{}/ws?cursor=0'\n", ws_url)); 274 } 275 276 response.push_str("\n────────────────────────────────────────────────────────────────\n"); 277 response.push_str("https://tangled.org/@atscan.net/plcbundle\n"); 278 279 let mut headers = HeaderMap::new(); 280 headers.insert( 281 "Content-Type", 282 HeaderValue::from_static("text/plain; charset=utf-8"), 283 ); 284 (StatusCode::OK, headers, response).into_response() 285}