High-performance implementation of plcbundle written in Rust
at main 460 lines 17 kB view raw
1use anyhow::Result; 2use clap::{Args, Subcommand}; 3use plcbundle::{LoadOptions, constants}; 4use sonic_rs::{JsonContainerTrait, JsonValueTrait}; 5use std::path::PathBuf; 6use std::time::Instant; 7 8#[derive(Args)] 9#[command( 10 about = "Operation queries and inspection", 11 long_about = "Access individual operations within bundles using flexible addressing schemes. 12You can reference operations by bundle number and position (e.g., '42 1337' for 13bundle 42, position 1337), or by global position (e.g., '410000' which represents 14bundle 42, position 0). 15 16Global positions are 0-indexed and calculated as ((bundleNumber - 1) × 10,000) + position, 17making it easy to reference operations across the entire repository with a single number. 18For example, 0 = bundle 1 position 0, 1 = bundle 1 position 1, 10000 = bundle 2 position 0. 19 20The 'get' subcommand retrieves operations as JSON, with automatic pretty-printing 21and syntax highlighting when outputting to a terminal. The 'show' subcommand 22displays operations in a human-readable format with detailed metadata. The 'find' 23subcommand searches across all bundles for an operation with a specific CID. 24 25All subcommands support JMESPath queries to extract specific fields from operations, 26making it easy to script and process operation data.", 27 alias = "operation", 28 alias = "record", 29 help_template = " # Get operation as JSON\n \ 30 {bin} op get 42 1337\n \ 31 {bin} op get 420000\n\n \ 32 # Show operation (formatted)\n \ 33 {bin} op show 42 1337\n \ 34 {bin} op show 88410345\n\n \ 35 # Find by CID\n \ 36 {bin} op find bafyreig3..." 37)] 38pub struct OpCommand { 39 #[command(subcommand)] 40 pub command: OpCommands, 41} 42 43#[derive(Subcommand)] 44pub enum OpCommands { 45 /// Get operation as JSON (machine-readable) 46 /// 47 /// Supports two input formats: 48 /// 1. Bundle number + position: get 42 1337 49 /// 2. Global position (0-indexed): get 0 (first operation), get 410000 (bundle 42 position 0) 50 /// 51 /// Global position (0-indexed) = ((bundleNumber - 1) × 10,000) + position 52 /// 53 /// By default, pretty-prints with colors when outputting to a terminal. 54 /// Use --raw to force raw JSON output (useful for piping). 55 /// Use -q/--query to extract a value using JMESPath. 56 #[command(help_template = crate::clap_help!( 57 examples: " # By bundle + position (auto pretty-print in terminal)\n \ 58 {bin} op get 42 1337\n\n \ 59 # By global position (0-indexed)\n \ 60 {bin} op get 0\n \ 61 {bin} op get 410000\n\n \ 62 {bin} op get 88410345\n\n \ 63 # Query with JMESPath\n \ 64 {bin} op get 42 1337 -q 'operation.type'\n \ 65 {bin} op get 42 1337 -q 'did'\n \ 66 {bin} op get 88410345 -q 'did'\n\n \ 67 # Force raw JSON output\n \ 68 {bin} op get 42 1337 --raw\n\n \ 69 # Pipe to jq (auto-detects non-TTY, uses raw)\n \ 70 {bin} op get 42 1337 | jq .did" 71 ))] 72 Get { 73 /// Bundle number (or global position if only one arg provided) 74 bundle: u32, 75 76 /// Operation position within bundle (optional if using global position) 77 #[arg(value_name = "POSITION")] 78 position: Option<usize>, 79 80 /// Query operation JSON using JMESPath expression 81 /// 82 /// Extracts a value from the operation using JMESPath. 83 /// Examples: 'did', 'operation.type', 'operation.handle', 'cid' 84 #[arg(short = 'q', long = "query")] 85 query: Option<String>, 86 87 /// Force raw JSON output (no pretty printing, no colors) 88 /// 89 /// By default, output is pretty-printed with colors when writing to a terminal. 90 /// Use this flag to force raw JSON output, useful for piping to other tools. 91 #[arg(long = "raw")] 92 raw: bool, 93 }, 94 95 /// Show operation with formatted output 96 /// 97 /// Displays operation in human-readable format with: 98 /// • Bundle location and global position 99 /// • DID and CID 100 /// • Timestamp 101 /// • Nullification status 102 /// • Parsed operation details 103 /// • Performance metrics (when not quiet) 104 #[command(help_template = crate::clap_help!( 105 examples: " # By bundle + position\n \ 106 {bin} op show 42 1337\n\n \ 107 # By global position (0-indexed)\n \ 108 {bin} op show 0\n \ 109 {bin} op show 88410345\n\n \ 110 # Quiet mode (minimal output)\n \ 111 {bin} op show 42 1337 -q" 112 ))] 113 Show { 114 /// Bundle number (or global position if only one arg) 115 bundle: u32, 116 117 /// Operation position within bundle (optional if using global position) 118 position: Option<usize>, 119 }, 120 121 /// Find operation by CID across all bundles 122 /// 123 /// Searches the entire repository for an operation with the given CID 124 /// and returns its location (bundle + position). 125 /// 126 /// Note: This performs a full scan and can be slow on large repositories. 127 #[command(help_template = crate::clap_help!( 128 examples: " # Find by CID\n \ 129 {bin} op find bafyreig3tg4k...\n\n \ 130 # Use with op get\n \ 131 {bin} op find bafyreig3... | awk '{print $3, $5}' | xargs {bin} op get" 132 ))] 133 Find { 134 /// CID to search for 135 cid: String, 136 }, 137} 138 139pub fn run(cmd: OpCommand, dir: PathBuf, quiet: bool) -> Result<()> { 140 match cmd.command { 141 OpCommands::Get { 142 bundle, 143 position, 144 query, 145 raw, 146 } => { 147 cmd_op_get(dir, bundle, position, query, raw, quiet)?; 148 } 149 OpCommands::Show { bundle, position } => { 150 cmd_op_show(dir, bundle, position, quiet)?; 151 } 152 OpCommands::Find { cid } => { 153 cmd_op_find(dir, cid, quiet)?; 154 } 155 } 156 Ok(()) 157} 158 159/// Parse operation position - supports both global position and bundle + position 160/// Small numbers (< 10000) are treated as bundle 1, position N (0-indexed) 161/// Global positions are 0-indexed: 0 = bundle 1 position 0, 10000 = bundle 2 position 0 162pub fn parse_op_position(bundle: u32, position: Option<usize>) -> (u32, usize) { 163 match position { 164 Some(pos) => (bundle, pos), 165 None => { 166 // Single argument: interpret as global or shorthand 167 if bundle < constants::BUNDLE_SIZE as u32 { 168 // Small numbers: shorthand for "bundle 1, position N" (0-indexed) 169 // op get 0 → bundle 1, position 0 170 // op get 1 → bundle 1, position 1 171 // op get 9999 → bundle 1, position 9999 172 (1, bundle as usize) 173 } else { 174 // Large numbers: global position (0-indexed) 175 // op get 10000 → bundle 2, position 0 176 // op get 88410345 → bundle 8842, position 345 177 let global_pos = bundle as u64; 178 let (bundle_num, op_pos) = 179 plcbundle::constants::global_to_bundle_position(global_pos); 180 (bundle_num, op_pos) 181 } 182 } 183 } 184} 185 186pub fn cmd_op_get( 187 dir: PathBuf, 188 bundle: u32, 189 position: Option<usize>, 190 query: Option<String>, 191 raw: bool, 192 quiet: bool, 193) -> Result<()> { 194 let (bundle_num, op_index) = parse_op_position(bundle, position); 195 196 let manager = super::utils::create_manager(dir, false, quiet, false)?; 197 198 // Get the operation JSON 199 let json = if quiet { 200 manager.get_operation_raw(bundle_num, op_index)? 201 } else { 202 let result = manager.get_operation_with_stats(bundle_num, op_index)?; 203 let global_pos = plcbundle::constants::bundle_position_to_global(bundle_num, op_index); 204 205 log::info!( 206 "[Load] Bundle {}:{:04} (pos={}) in {:?} | {} bytes", 207 bundle_num, 208 op_index, 209 global_pos, 210 result.load_duration, 211 result.size_bytes 212 ); 213 214 result.raw_json 215 }; 216 217 // If query is provided, apply JMESPath query 218 let output_json = if let Some(query_expr) = query { 219 // Compile JMESPath expression 220 let expr = jmespath::compile(&query_expr).map_err(|e| { 221 anyhow::anyhow!("Failed to compile JMESPath query '{}': {}", query_expr, e) 222 })?; 223 224 // Execute query 225 let data = jmespath::Variable::from_json(&json) 226 .map_err(|e| anyhow::anyhow!("Failed to parse operation JSON: {}", e))?; 227 228 let result = expr 229 .search(&data) 230 .map_err(|e| anyhow::anyhow!("JMESPath query failed: {}", e))?; 231 232 if result.is_null() { 233 anyhow::bail!("Query '{}' returned null (field not found)", query_expr); 234 } 235 236 // Convert result to JSON string 237 // Note: jmespath uses serde_json internally, so we use serde_json here (not bundle/operation data) 238 if result.is_string() { 239 result.as_string().unwrap().to_string() 240 } else { 241 serde_json::to_string(&*result) 242 .map_err(|e| anyhow::anyhow!("Failed to serialize query result: {}", e))? 243 } 244 } else { 245 json 246 }; 247 248 // Determine if we should pretty print: 249 // - Pretty print if stdout is a TTY (interactive terminal) and --raw is not set 250 // - Use raw output if --raw is set or if output is piped (not a TTY) 251 #[cfg(feature = "cli")] 252 let should_pretty = !raw && super::utils::is_stdout_tty(); 253 #[cfg(not(feature = "cli"))] 254 let should_pretty = false; // No TTY detection without CLI feature 255 256 if should_pretty { 257 // Try to parse and pretty print the result 258 match sonic_rs::from_str::<sonic_rs::Value>(&output_json) { 259 Ok(parsed) => { 260 let pretty_json = sonic_rs::to_string_pretty(&parsed)?; 261 #[cfg(feature = "cli")] 262 { 263 println!("{}", super::utils::colorize_json(&pretty_json)); 264 } 265 #[cfg(not(feature = "cli"))] 266 { 267 println!("{}", pretty_json); 268 } 269 } 270 Err(_) => { 271 // If it's not valid JSON (e.g., a string result), just output as-is 272 println!("{}", output_json); 273 } 274 } 275 } else { 276 println!("{}", output_json); 277 } 278 279 Ok(()) 280} 281 282pub fn cmd_op_show(dir: PathBuf, bundle: u32, position: Option<usize>, quiet: bool) -> Result<()> { 283 let (bundle_num, op_index) = parse_op_position(bundle, position); 284 285 let manager = super::utils::create_manager(dir, false, quiet, false)?; 286 287 // Use the new get_operation API instead of loading entire bundle 288 let load_start = Instant::now(); 289 let op = manager.get_operation(bundle_num, op_index)?; 290 let load_duration = load_start.elapsed(); 291 292 let parse_start = Instant::now(); 293 // Operation is already parsed by get_operation 294 let parse_duration = parse_start.elapsed(); 295 296 let global_pos = plcbundle::constants::bundle_position_to_global(bundle_num, op_index); 297 298 // Extract operation details 299 let op_type = op 300 .operation 301 .get("type") 302 .and_then(|v| v.as_str()) 303 .unwrap_or("unknown"); 304 305 let handle = op 306 .operation 307 .get("handle") 308 .and_then(|v| v.as_str()) 309 .or_else(|| { 310 op.operation 311 .get("alsoKnownAs") 312 .and_then(|v| v.as_array()) 313 .and_then(|arr| arr.first()) 314 .and_then(|v| v.as_str()) 315 .map(|s| s.trim_start_matches("at://")) 316 }); 317 318 let pds = op 319 .operation 320 .get("services") 321 .and_then(|services_val| services_val.get("atproto_pds")) 322 .and_then(|pds_val| pds_val.get("endpoint")) 323 .and_then(|v| v.as_str()); 324 325 // Display formatted output 326 println!("═══════════════════════════════════════════════════════════════"); 327 println!(" Operation {}", global_pos); 328 println!("═══════════════════════════════════════════════════════════════\n"); 329 330 println!("Location"); 331 println!("────────"); 332 println!(" Bundle: {}", bundle_num); 333 println!(" Position: {}", op_index); 334 println!(" Global position: {}\n", global_pos); 335 336 println!("Identity"); 337 println!("────────"); 338 println!(" DID: {}", op.did); 339 if let Some(cid) = &op.cid { 340 println!(" CID: {}\n", cid); 341 } else { 342 println!(); 343 } 344 345 println!("Timestamp"); 346 println!("─────────"); 347 println!(" Created: {}", op.created_at); 348 println!(); 349 350 println!("Status"); 351 println!("──────"); 352 use super::utils::colors; 353 let status = if op.nullified { 354 format!("{}✗ Nullified{}", colors::RED, colors::RESET) 355 } else { 356 format!("{}✓ Active{}", colors::GREEN, colors::RESET) 357 }; 358 println!(" {}\n", status); 359 360 // Performance metrics (when not quiet) 361 if !quiet { 362 let total_time = load_duration + parse_duration; 363 let json_size = sonic_rs::to_string(&op).map(|s| s.len()).unwrap_or(0); 364 365 println!("Performance"); 366 println!("───────────"); 367 println!(" Load time: {:?}", load_duration); 368 println!(" Parse time: {:?}", parse_duration); 369 println!(" Total time: {:?}", total_time); 370 371 if json_size > 0 { 372 println!(" Data size: {} bytes", json_size); 373 let mb_per_sec = (json_size as f64) / load_duration.as_secs_f64() / (1024.0 * 1024.0); 374 println!(" Load speed: {:.2} MB/s", mb_per_sec); 375 } 376 377 println!(); 378 } 379 380 // Operation details 381 if !op.nullified { 382 println!("Details"); 383 println!("───────"); 384 println!(" Type: {}", op_type); 385 386 if let Some(h) = handle { 387 println!(" Handle: {}", h); 388 } 389 390 if let Some(p) = pds { 391 println!(" PDS: {}", p); 392 } 393 394 println!(); 395 } 396 397 // Show full JSON when not quiet 398 if !quiet { 399 println!("Raw JSON"); 400 println!("────────"); 401 let json = sonic_rs::to_string_pretty(&op)?; 402 println!("{}\n", json); 403 } 404 405 Ok(()) 406} 407 408pub fn cmd_op_find(dir: PathBuf, cid: String, quiet: bool) -> Result<()> { 409 let manager = super::utils::create_manager(dir, false, quiet, false)?; 410 let last_bundle = manager.get_last_bundle(); 411 412 if !quiet { 413 log::info!("Searching {} bundles for CID: {}\n", last_bundle, cid); 414 } 415 416 for bundle_num in 1..=last_bundle { 417 let result = match manager.load_bundle(bundle_num, LoadOptions::default()) { 418 Ok(r) => r, 419 Err(_) => continue, 420 }; 421 422 for (i, op) in result.operations.iter().enumerate() { 423 let cid_matches = op.cid.as_ref() == Some(&cid); 424 425 if cid_matches { 426 let global_pos = plcbundle::constants::bundle_position_to_global(bundle_num, i); 427 428 println!("Found: bundle {}, position {}", bundle_num, i); 429 println!("Global position: {}\n", global_pos); 430 431 println!(" DID: {}", op.did); 432 println!(" Created: {}", op.created_at); 433 434 use super::utils::colors; 435 let status = if op.nullified { 436 format!("{}✗ Nullified{}", colors::RED, colors::RESET) 437 } else { 438 format!("{}✓ Active{}", colors::GREEN, colors::RESET) 439 }; 440 println!(" Status: {}", status); 441 442 return Ok(()); 443 } 444 } 445 446 // Progress indicator every 100 bundles 447 if !quiet && bundle_num % 100 == 0 { 448 eprint!("Searched through bundle {}...\r", bundle_num); 449 use std::io::Write; 450 std::io::stderr().flush()?; 451 } 452 } 453 454 if !quiet { 455 log::error!("\nCID not found: {}", cid); 456 log::info!("(Searched {} bundles)", last_bundle); 457 } 458 459 anyhow::bail!("CID not found"); 460}