store.rs (25535B)
1 use std::fs; 2 use std::path::{Path, PathBuf}; 3 4 use radroots_replica_db::export::{ReplicaDbExportManifestRs, export_manifest}; 5 use radroots_replica_db::migrations; 6 use radroots_replica_sync::radroots_replica_sync_status; 7 use radroots_sdk::{ 8 BackupReceipt, BackupRequest, IntegrityReceipt, IntegrityRequest, RadrootsSdk, RestoreReceipt, 9 RestoreRequest, SdkBackupState, SdkEventStoreStorageStatus, SdkOutboxStorageStatus, 10 SdkRestoreState, SdkSqliteStoreStatus, SdkStorageKind, StorageStatusReceipt, 11 StorageStatusRequest, 12 }; 13 use radroots_sql_core::SqliteExecutor; 14 use serde::Serialize; 15 use serde_json::{Value, json}; 16 17 use crate::cli::global::LocalExportFormatArg; 18 use crate::runtime::RuntimeError; 19 use crate::runtime::config::RuntimeConfig; 20 use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession, sdk_runtime, sdk_storage_root}; 21 use crate::runtime::sync::ensure_sync_run_table; 22 use crate::view::runtime::{ 23 LocalBackupView, LocalExportView, LocalInitView, LocalLegacyReplicaStatusView, 24 LocalReplicaCountsView, LocalReplicaSyncView, LocalRestoreView, LocalStatusView, 25 SdkEventStoreStatusView, SdkIntegrityView, SdkOutboxStatusView, SdkSqliteStatusView, 26 }; 27 28 const LEGACY_REPLICA_SOURCE: &str = "legacy local replica ยท derived/migration source"; 29 const SDK_CANONICAL_SOURCE: &str = "SDK canonical event store and outbox"; 30 const SDK_CANONICAL_STORE: &str = "sdk"; 31 const SDK_BACKUP_KIND: &str = "sdk_canonical"; 32 const SDK_BACKUP_MANIFEST_FILE: &str = "manifest.json"; 33 const SDK_EVENT_STORE_FILE: &str = "event_store.sqlite"; 34 const SDK_OUTBOX_FILE: &str = "outbox.sqlite"; 35 36 pub fn init(config: &RuntimeConfig) -> Result<LocalInitView, RuntimeError> { 37 let existed = config.local.replica_db_path.exists(); 38 ensure_local_roots(config)?; 39 let executor = SqliteExecutor::open(&config.local.replica_db_path)?; 40 migrations::run_all_up(&executor)?; 41 ensure_sync_run_table(&executor)?; 42 let manifest = export_manifest(&executor)?; 43 44 Ok(LocalInitView { 45 state: if existed { 46 "ready".to_owned() 47 } else { 48 "initialized".to_owned() 49 }, 50 source: LEGACY_REPLICA_SOURCE.to_owned(), 51 local_root: config.local.root.display().to_string(), 52 replica_db: "ready".to_owned(), 53 path: config.local.replica_db_path.display().to_string(), 54 replica_db_version: manifest.replica_db_version, 55 backup_format_version: manifest.backup_format_version, 56 }) 57 } 58 59 pub fn init_preflight(config: &RuntimeConfig) -> Result<LocalInitView, RuntimeError> { 60 validate_local_roots(config)?; 61 if config.local.replica_db_path.exists() { 62 let executor = SqliteExecutor::open(&config.local.replica_db_path)?; 63 ensure_sync_run_table(&executor)?; 64 let manifest = export_manifest(&executor)?; 65 return Ok(LocalInitView { 66 state: "ready".to_owned(), 67 source: LEGACY_REPLICA_SOURCE.to_owned(), 68 local_root: config.local.root.display().to_string(), 69 replica_db: "ready".to_owned(), 70 path: config.local.replica_db_path.display().to_string(), 71 replica_db_version: manifest.replica_db_version, 72 backup_format_version: manifest.backup_format_version, 73 }); 74 } 75 76 Ok(LocalInitView { 77 state: "dry_run".to_owned(), 78 source: LEGACY_REPLICA_SOURCE.to_owned(), 79 local_root: config.local.root.display().to_string(), 80 replica_db: "missing".to_owned(), 81 path: config.local.replica_db_path.display().to_string(), 82 replica_db_version: String::new(), 83 backup_format_version: String::new(), 84 }) 85 } 86 87 pub fn status(config: &RuntimeConfig) -> Result<LocalStatusView, CliSdkAdapterError> { 88 let sdk_root = sdk_storage_root(config); 89 let sdk_existed_before_open = sdk_storage_files_exist(sdk_root.as_path()); 90 let legacy_replica = legacy_replica_status(config)?; 91 let session = CliSdkSession::connect(config)?; 92 let receipt = session.block_on(session.sdk().storage_status(StorageStatusRequest::new()))?; 93 let integrity = session.block_on(session.sdk().integrity(IntegrityRequest::new()))?; 94 Ok(sdk_status_view( 95 config, 96 sdk_root, 97 sdk_existed_before_open, 98 receipt, 99 integrity, 100 legacy_replica, 101 )) 102 } 103 104 fn legacy_replica_status( 105 config: &RuntimeConfig, 106 ) -> Result<LocalLegacyReplicaStatusView, RuntimeError> { 107 if !config.local.replica_db_path.exists() { 108 return Ok(LocalLegacyReplicaStatusView { 109 state: "unconfigured".to_owned(), 110 source: LEGACY_REPLICA_SOURCE.to_owned(), 111 replica_db: "missing".to_owned(), 112 path: config.local.replica_db_path.display().to_string(), 113 replica_db_version: String::new(), 114 backup_format_version: String::new(), 115 schema_hash: String::new(), 116 counts: LocalReplicaCountsView { 117 farms: 0, 118 listings: 0, 119 profiles: 0, 120 relays: 0, 121 event_states: 0, 122 }, 123 sync: LocalReplicaSyncView { 124 expected_count: 0, 125 pending_count: 0, 126 }, 127 reason: Some("local replica database is not initialized".to_owned()), 128 actions: vec!["radroots store init".to_owned()], 129 }); 130 } 131 132 let executor = SqliteExecutor::open(&config.local.replica_db_path)?; 133 ensure_sync_run_table(&executor)?; 134 let manifest = export_manifest(&executor)?; 135 let sync = radroots_replica_sync_status(&executor)?; 136 137 Ok(LocalLegacyReplicaStatusView { 138 state: "ready".to_owned(), 139 source: LEGACY_REPLICA_SOURCE.to_owned(), 140 replica_db: "ready".to_owned(), 141 path: config.local.replica_db_path.display().to_string(), 142 replica_db_version: manifest.replica_db_version.clone(), 143 backup_format_version: manifest.backup_format_version.clone(), 144 schema_hash: manifest.schema_hash.clone(), 145 counts: manifest_counts(&manifest), 146 sync: LocalReplicaSyncView { 147 expected_count: sync.expected_count, 148 pending_count: sync.pending_count, 149 }, 150 reason: None, 151 actions: Vec::new(), 152 }) 153 } 154 155 pub fn backup( 156 config: &RuntimeConfig, 157 output: &Path, 158 ) -> Result<LocalBackupView, CliSdkAdapterError> { 159 ensure_safe_sdk_backup_destination(config, output)?; 160 let session = CliSdkSession::connect(config)?; 161 let receipt = session.block_on(session.sdk().backup(BackupRequest::new(output)))?; 162 sdk_backup_view(receipt) 163 } 164 165 pub fn backup_preflight( 166 config: &RuntimeConfig, 167 output: &Path, 168 ) -> Result<LocalBackupView, CliSdkAdapterError> { 169 ensure_safe_sdk_backup_destination(config, output)?; 170 let session = CliSdkSession::connect(config)?; 171 let status = session.block_on(session.sdk().storage_status(StorageStatusRequest::new()))?; 172 let integrity = session.block_on(session.sdk().integrity(IntegrityRequest::new()))?; 173 let manifest = sdk_backup_manifest_preview(output, &status, &integrity); 174 Ok(LocalBackupView { 175 state: "dry_run".to_owned(), 176 source: SDK_CANONICAL_SOURCE.to_owned(), 177 backup_kind: SDK_BACKUP_KIND.to_owned(), 178 canonical_store: SDK_CANONICAL_STORE.to_owned(), 179 destination: output.display().to_string(), 180 file: output.join(SDK_BACKUP_MANIFEST_FILE).display().to_string(), 181 event_store_file: Some(output.join(SDK_EVENT_STORE_FILE).display().to_string()), 182 outbox_file: Some(output.join(SDK_OUTBOX_FILE).display().to_string()), 183 manifest_file: Some(output.join(SDK_BACKUP_MANIFEST_FILE).display().to_string()), 184 size_bytes: 0, 185 manifest, 186 reason: Some( 187 "dry run requested; SDK canonical backup directory was not written".to_owned(), 188 ), 189 actions: vec!["radroots store backup create".to_owned()], 190 }) 191 } 192 193 pub fn restore( 194 config: &RuntimeConfig, 195 source: &Path, 196 destination: Option<&Path>, 197 overwrite: bool, 198 dry_run: bool, 199 ) -> Result<LocalRestoreView, CliSdkAdapterError> { 200 let destination = destination 201 .map(Path::to_path_buf) 202 .unwrap_or_else(|| sdk_storage_root(config)); 203 ensure_safe_sdk_restore_destination(config, &destination)?; 204 let request = RestoreRequest::new(source) 205 .with_destination(destination) 206 .with_overwrite(overwrite) 207 .with_dry_run(dry_run); 208 let runtime = sdk_runtime()?; 209 let receipt = runtime.block_on(RadrootsSdk::restore(request))?; 210 sdk_restore_view(receipt, overwrite, dry_run) 211 } 212 213 pub fn export( 214 config: &RuntimeConfig, 215 format: LocalExportFormatArg, 216 output: &Path, 217 ) -> Result<LocalExportView, RuntimeError> { 218 if !config.local.replica_db_path.exists() { 219 return Ok(LocalExportView { 220 state: "unconfigured".to_owned(), 221 source: LEGACY_REPLICA_SOURCE.to_owned(), 222 format: format.as_str().to_owned(), 223 file: output.display().to_string(), 224 records: 0, 225 export_version: String::new(), 226 schema_hash: String::new(), 227 reason: Some("local replica database is not initialized".to_owned()), 228 actions: vec!["radroots store init".to_owned()], 229 }); 230 } 231 232 ensure_safe_output_path(config, output)?; 233 create_parent_dir(output)?; 234 235 let executor = SqliteExecutor::open(&config.local.replica_db_path)?; 236 let manifest = export_manifest(&executor)?; 237 let sync = radroots_replica_sync_status(&executor)?; 238 let records = match format { 239 LocalExportFormatArg::Json => { 240 let export = json!({ 241 "kind": "local_export_manifest_v1", 242 "source": LEGACY_REPLICA_SOURCE, 243 "replica_db_version": manifest.replica_db_version, 244 "backup_format_version": manifest.backup_format_version, 245 "export_version": manifest.export_version, 246 "schema_hash": manifest.schema_hash, 247 "sync": { 248 "expected_count": sync.expected_count, 249 "pending_count": sync.pending_count, 250 }, 251 "table_counts": manifest.table_counts, 252 }); 253 fs::write(output, serde_json::to_string_pretty(&export)?)?; 254 1 255 } 256 LocalExportFormatArg::Ndjson => { 257 let mut lines = Vec::new(); 258 lines.push( 259 json!({ 260 "kind": "local_export_manifest", 261 "source": LEGACY_REPLICA_SOURCE, 262 "replica_db_version": manifest.replica_db_version, 263 "backup_format_version": manifest.backup_format_version, 264 "export_version": manifest.export_version, 265 "schema_hash": manifest.schema_hash, 266 }) 267 .to_string(), 268 ); 269 lines.push( 270 json!({ 271 "kind": "local_sync_status", 272 "expected_count": sync.expected_count, 273 "pending_count": sync.pending_count, 274 }) 275 .to_string(), 276 ); 277 for table in &manifest.table_counts { 278 lines.push( 279 json!({ 280 "kind": "local_table_count", 281 "table": table.name, 282 "row_count": table.row_count, 283 }) 284 .to_string(), 285 ); 286 } 287 fs::write(output, format!("{}\n", lines.join("\n")))?; 288 lines.len() 289 } 290 }; 291 292 Ok(LocalExportView { 293 state: "exported".to_owned(), 294 source: LEGACY_REPLICA_SOURCE.to_owned(), 295 format: format.as_str().to_owned(), 296 file: output.display().to_string(), 297 records, 298 export_version: manifest.export_version, 299 schema_hash: manifest.schema_hash, 300 reason: None, 301 actions: Vec::new(), 302 }) 303 } 304 305 fn ensure_local_roots(config: &RuntimeConfig) -> Result<(), RuntimeError> { 306 fs::create_dir_all(&config.local.root)?; 307 fs::create_dir_all(&config.local.backups_dir)?; 308 fs::create_dir_all(&config.local.exports_dir)?; 309 Ok(()) 310 } 311 312 fn validate_local_roots(config: &RuntimeConfig) -> Result<(), RuntimeError> { 313 validate_directory_target(&config.local.root)?; 314 validate_directory_target(&config.local.backups_dir)?; 315 validate_directory_target(&config.local.exports_dir)?; 316 Ok(()) 317 } 318 319 fn validate_directory_target(path: &Path) -> Result<(), RuntimeError> { 320 let mut candidate = path.to_path_buf(); 321 loop { 322 if candidate.exists() { 323 if candidate.is_dir() { 324 return Ok(()); 325 } 326 return Err(RuntimeError::Config(format!( 327 "path {} is not a directory", 328 candidate.display() 329 ))); 330 } 331 if !candidate.pop() { 332 return Err(RuntimeError::Config(format!( 333 "path {} has no existing parent directory", 334 path.display() 335 ))); 336 } 337 } 338 } 339 340 fn sdk_storage_files_exist(sdk_root: &Path) -> bool { 341 sdk_root.join(SDK_EVENT_STORE_FILE).exists() && sdk_root.join(SDK_OUTBOX_FILE).exists() 342 } 343 344 fn sdk_status_view( 345 config: &RuntimeConfig, 346 sdk_root: PathBuf, 347 sdk_existed_before_open: bool, 348 receipt: StorageStatusReceipt, 349 integrity: IntegrityReceipt, 350 legacy_replica: LocalLegacyReplicaStatusView, 351 ) -> LocalStatusView { 352 let event_store_path = receipt 353 .paths 354 .as_ref() 355 .map(|paths| paths.event_store_path.display().to_string()); 356 let outbox_path = receipt 357 .paths 358 .as_ref() 359 .map(|paths| paths.outbox_path.display().to_string()); 360 let state = sdk_status_state(&receipt, &integrity).to_owned(); 361 let reason = sdk_status_reason(&state); 362 let actions = sdk_status_actions(&state); 363 LocalStatusView { 364 state, 365 source: SDK_CANONICAL_SOURCE.to_owned(), 366 local_root: config.local.root.display().to_string(), 367 canonical_store: SDK_CANONICAL_STORE.to_owned(), 368 sdk_storage: sdk_storage_kind_label(receipt.storage).to_owned(), 369 sdk_root: sdk_root.display().to_string(), 370 sdk_existed_before_open, 371 event_store: sdk_event_store_status_view(receipt.event_store, event_store_path), 372 outbox: sdk_outbox_status_view(receipt.outbox, outbox_path), 373 integrity: sdk_integrity_view(integrity), 374 legacy_replica, 375 reason, 376 actions, 377 } 378 } 379 380 fn sdk_status_state(receipt: &StorageStatusReceipt, integrity: &IntegrityReceipt) -> &'static str { 381 if receipt.event_store.store.integrity_ok 382 && receipt.outbox.store.integrity_ok 383 && integrity.event_store_ok 384 && integrity.outbox_ok 385 { 386 "ready" 387 } else { 388 "needs_attention" 389 } 390 } 391 392 fn sdk_status_reason(state: &str) -> Option<String> { 393 match state { 394 "ready" => None, 395 _ => Some("SDK canonical store integrity check failed".to_owned()), 396 } 397 } 398 399 fn sdk_status_actions(state: &str) -> Vec<String> { 400 match state { 401 "ready" => Vec::new(), 402 _ => vec!["radroots store status get".to_owned()], 403 } 404 } 405 406 fn sdk_event_store_status_view( 407 status: SdkEventStoreStorageStatus, 408 path: Option<String>, 409 ) -> SdkEventStoreStatusView { 410 SdkEventStoreStatusView { 411 path, 412 store: sdk_sqlite_status_view(status.store), 413 total_events: status.total_events, 414 projection_eligible_events: status.projection_eligible_events, 415 relay_observations: status.relay_observations, 416 last_event_seq: status.last_event_seq, 417 last_event_updated_at_ms: status.last_event_updated_at_ms, 418 } 419 } 420 421 fn sdk_outbox_status_view( 422 status: SdkOutboxStorageStatus, 423 path: Option<String>, 424 ) -> SdkOutboxStatusView { 425 SdkOutboxStatusView { 426 path, 427 store: sdk_sqlite_status_view(status.store), 428 total_events: status.total_events, 429 pending_events: status.pending_events, 430 retryable_events: status.retryable_events, 431 terminal_events: status.terminal_events, 432 failed_terminal_events: status.failed_terminal_events, 433 ready_signed_events: status.ready_signed_events, 434 publishing_events: status.publishing_events, 435 last_attempt_at_ms: status.last_attempt_at_ms, 436 last_error: status.last_error, 437 } 438 } 439 440 fn sdk_sqlite_status_view(status: SdkSqliteStoreStatus) -> SdkSqliteStatusView { 441 SdkSqliteStatusView { 442 schema_version: status.schema_version, 443 journal_mode: status.journal_mode, 444 foreign_keys_enabled: status.foreign_keys_enabled, 445 busy_timeout_ms: status.busy_timeout_ms, 446 integrity_ok: status.integrity_ok, 447 integrity_result: status.integrity_result, 448 } 449 } 450 451 fn sdk_integrity_view(receipt: IntegrityReceipt) -> SdkIntegrityView { 452 SdkIntegrityView { 453 checked_paths: receipt 454 .checked_paths 455 .into_iter() 456 .map(|path| path.display().to_string()) 457 .collect(), 458 event_store_ok: receipt.event_store_ok, 459 outbox_ok: receipt.outbox_ok, 460 event_store_result: receipt.event_store_result, 461 outbox_result: receipt.outbox_result, 462 } 463 } 464 465 fn ensure_safe_sdk_backup_destination( 466 config: &RuntimeConfig, 467 output: &Path, 468 ) -> Result<(), RuntimeError> { 469 let sdk_root = sdk_storage_root(config); 470 let sdk_event_store_path = sdk_root.join(SDK_EVENT_STORE_FILE); 471 let sdk_outbox_path = sdk_root.join(SDK_OUTBOX_FILE); 472 let forbidden_paths = [ 473 sdk_root.as_path(), 474 config.local.replica_db_path.as_path(), 475 sdk_event_store_path.as_path(), 476 sdk_outbox_path.as_path(), 477 ]; 478 if forbidden_paths.iter().any(|forbidden| output == *forbidden) { 479 return Err(RuntimeError::Config(format!( 480 "backup destination {} would overwrite canonical or legacy store data", 481 output.display() 482 ))); 483 } 484 if output.starts_with(sdk_root.as_path()) { 485 return Err(RuntimeError::Config(format!( 486 "backup destination {} must not be inside the SDK canonical store directory", 487 output.display() 488 ))); 489 } 490 Ok(()) 491 } 492 493 fn ensure_safe_sdk_restore_destination( 494 config: &RuntimeConfig, 495 destination: &Path, 496 ) -> Result<(), RuntimeError> { 497 let sdk_root = sdk_storage_root(config); 498 let sdk_event_store_path = sdk_root.join(SDK_EVENT_STORE_FILE); 499 let sdk_outbox_path = sdk_root.join(SDK_OUTBOX_FILE); 500 let forbidden_paths = [ 501 config.local.root.as_path(), 502 config.local.replica_db_path.as_path(), 503 sdk_event_store_path.as_path(), 504 sdk_outbox_path.as_path(), 505 ]; 506 if forbidden_paths 507 .iter() 508 .any(|forbidden| destination == *forbidden) 509 { 510 return Err(RuntimeError::Config(format!( 511 "restore destination {} would overwrite canonical runtime roots or store files", 512 destination.display() 513 ))); 514 } 515 if config.local.replica_db_path.starts_with(destination) 516 || config.local.backups_dir.starts_with(destination) 517 || config.local.exports_dir.starts_with(destination) 518 { 519 return Err(RuntimeError::Config(format!( 520 "restore destination {} must not contain CLI runtime state directories", 521 destination.display() 522 ))); 523 } 524 Ok(()) 525 } 526 527 fn sdk_backup_view(receipt: BackupReceipt) -> Result<LocalBackupView, CliSdkAdapterError> { 528 let event_store_file = receipt.event_store_path.as_ref().map(display_path); 529 let outbox_file = receipt.outbox_path.as_ref().map(display_path); 530 let manifest_file = receipt.manifest_path.as_ref().map(display_path); 531 let size_bytes = path_size(receipt.event_store_path.as_ref())? 532 + path_size(receipt.outbox_path.as_ref())? 533 + path_size(receipt.manifest_path.as_ref())?; 534 Ok(LocalBackupView { 535 state: sdk_backup_state_label(receipt.state).to_owned(), 536 source: SDK_CANONICAL_SOURCE.to_owned(), 537 backup_kind: SDK_BACKUP_KIND.to_owned(), 538 canonical_store: SDK_CANONICAL_STORE.to_owned(), 539 destination: display_path(&receipt.destination), 540 file: manifest_file 541 .clone() 542 .unwrap_or_else(|| receipt.destination.display().to_string()), 543 event_store_file, 544 outbox_file, 545 manifest_file, 546 size_bytes, 547 manifest: json_value(&receipt.manifest)?, 548 reason: None, 549 actions: Vec::new(), 550 }) 551 } 552 553 fn sdk_restore_view( 554 receipt: RestoreReceipt, 555 overwrite: bool, 556 dry_run: bool, 557 ) -> Result<LocalRestoreView, CliSdkAdapterError> { 558 let destination_paths = receipt.destination_paths.as_ref(); 559 let restored_paths = receipt.restored_paths.as_ref(); 560 Ok(LocalRestoreView { 561 state: sdk_restore_state_label(receipt.state).to_owned(), 562 source: SDK_CANONICAL_SOURCE.to_owned(), 563 restore_kind: SDK_BACKUP_KIND.to_owned(), 564 canonical_store: SDK_CANONICAL_STORE.to_owned(), 565 backup_source: display_path(&receipt.source), 566 destination: receipt 567 .destination 568 .as_ref() 569 .map(display_path) 570 .unwrap_or_default(), 571 event_store_file: display_path(&receipt.event_store_path), 572 outbox_file: display_path(&receipt.outbox_path), 573 manifest_file: display_path(&receipt.manifest_path), 574 destination_event_store_file: destination_paths 575 .map(|paths| display_path(&paths.event_store_path)), 576 destination_outbox_file: destination_paths.map(|paths| display_path(&paths.outbox_path)), 577 restored_event_store_file: restored_paths 578 .map(|paths| display_path(&paths.event_store_path)), 579 restored_outbox_file: restored_paths.map(|paths| display_path(&paths.outbox_path)), 580 manifest: json_value(&receipt.manifest)?, 581 verification: json_value(&receipt.verification)?, 582 overwrite, 583 dry_run, 584 reason: if dry_run { 585 Some("dry run requested; SDK canonical store was not restored".to_owned()) 586 } else { 587 None 588 }, 589 actions: if dry_run { 590 vec!["radroots store backup restore <backup-dir>".to_owned()] 591 } else { 592 Vec::new() 593 }, 594 }) 595 } 596 597 fn sdk_restore_state_label(state: SdkRestoreState) -> &'static str { 598 match state { 599 SdkRestoreState::Validated => "validated", 600 SdkRestoreState::DryRun => "dry_run", 601 SdkRestoreState::Completed => "completed", 602 _ => "unknown", 603 } 604 } 605 606 fn sdk_backup_manifest_preview( 607 output: &Path, 608 status: &StorageStatusReceipt, 609 integrity: &IntegrityReceipt, 610 ) -> Value { 611 json!({ 612 "manifest_kind": "sdk_canonical_backup_preview", 613 "destination": output.display().to_string(), 614 "source_storage": sdk_storage_kind_label(status.storage), 615 "source_paths": &status.paths, 616 "backup_paths": { 617 "event_store_path": output.join(SDK_EVENT_STORE_FILE).display().to_string(), 618 "outbox_path": output.join(SDK_OUTBOX_FILE).display().to_string(), 619 }, 620 "source_status": status, 621 "backup_verification": { 622 "event_store_ok": integrity.event_store_ok, 623 "outbox_ok": integrity.outbox_ok, 624 "event_store_result": &integrity.event_store_result, 625 "outbox_result": &integrity.outbox_result, 626 }, 627 }) 628 } 629 630 fn sdk_storage_kind_label(kind: SdkStorageKind) -> &'static str { 631 match kind { 632 SdkStorageKind::Memory => "memory", 633 SdkStorageKind::Directory => "directory", 634 _ => "unknown", 635 } 636 } 637 638 fn sdk_backup_state_label(state: SdkBackupState) -> &'static str { 639 match state { 640 SdkBackupState::Planned => "planned", 641 SdkBackupState::Completed => "completed", 642 _ => "unknown", 643 } 644 } 645 646 fn json_value(value: impl Serialize) -> Result<Value, RuntimeError> { 647 serde_json::to_value(value).map_err(RuntimeError::from) 648 } 649 650 fn path_size(path: Option<&PathBuf>) -> Result<u64, RuntimeError> { 651 path.map(fs::metadata) 652 .transpose()? 653 .map(|metadata| metadata.len()) 654 .ok_or_else(|| RuntimeError::Config("SDK backup did not report all file paths".to_owned())) 655 } 656 657 fn display_path(path: &PathBuf) -> String { 658 path.display().to_string() 659 } 660 661 fn create_parent_dir(path: &Path) -> Result<(), RuntimeError> { 662 if let Some(parent) = path.parent() { 663 fs::create_dir_all(parent)?; 664 } 665 Ok(()) 666 } 667 668 fn ensure_safe_output_path(config: &RuntimeConfig, output: &Path) -> Result<(), RuntimeError> { 669 if output == config.local.replica_db_path.as_path() { 670 return Err(RuntimeError::Config(format!( 671 "output path {} would overwrite the local replica database", 672 output.display() 673 ))); 674 } 675 Ok(()) 676 } 677 678 fn manifest_counts(manifest: &ReplicaDbExportManifestRs) -> LocalReplicaCountsView { 679 LocalReplicaCountsView { 680 farms: table_row_count(manifest, "farm"), 681 listings: table_row_count(manifest, "trade_product"), 682 profiles: table_row_count(manifest, "nostr_profile"), 683 relays: table_row_count(manifest, "direct_nostr_relay"), 684 event_states: table_row_count(manifest, "nostr_event_state"), 685 } 686 } 687 688 fn table_row_count(manifest: &ReplicaDbExportManifestRs, name: &str) -> u64 { 689 manifest 690 .table_counts 691 .iter() 692 .find(|table| table.name == name) 693 .map(|table| table.row_count) 694 .unwrap_or(0) 695 }