backup.rs (5802B)
1 use core::{fmt, num::NonZeroU64}; 2 use std::path::Path; 3 4 use radroots_runtime_paths::RuntimeContext; 5 use radroots_service_sqlite::{ 6 BackupCreatedAtUnixMs, BackupManifestSha256, MigrationAppliedAtUnixSeconds, 7 MigrationBuildIdentity, ServiceBackupManifest, ServiceDatabaseIdentity, VerifiedServiceBackup, 8 finalize_staged_restore, stage_verified_restore, verify_backup_bundle, 9 }; 10 11 use crate::db::{invalid_storage_contract, map_service_error}; 12 use crate::{Database, HarvestCircleStorageContract}; 13 use harvestcircle_domain::{SafeError, SafeErrorCode, SafeMessage}; 14 15 /// Retained, non-forgeable proof of one identity-bound HarvestCircle backup. 16 /// 17 /// This type intentionally exposes no path, descriptor, or raw database handle. 18 /// It is single-use restore authority and cannot be cloned: 19 /// 20 /// ```compile_fail 21 /// use harvestcircle_storage::VerifiedHarvestCircleBackup; 22 /// 23 /// fn require_clone<T: Clone>() {} 24 /// require_clone::<VerifiedHarvestCircleBackup>(); 25 /// ``` 26 pub struct VerifiedHarvestCircleBackup { 27 inner: VerifiedServiceBackup, 28 } 29 30 impl fmt::Debug for VerifiedHarvestCircleBackup { 31 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 32 formatter.write_str("VerifiedHarvestCircleBackup([redacted])") 33 } 34 } 35 36 /// Verifies one untrusted canonical backup bundle without mutating it. 37 /// 38 /// The expected digest, database identity, and positive member limit are 39 /// trusted inputs. This operation is synchronous and performs bounded file and 40 /// SQLite reads; callers own any supervised worker and deadline. 41 pub fn verify_harvestcircle_backup( 42 manifest_bytes: &[u8], 43 expected_manifest_digest: BackupManifestSha256, 44 bundle_directory: &Path, 45 expected_identity: &ServiceDatabaseIdentity, 46 maximum_state_bytes: NonZeroU64, 47 ) -> Result<VerifiedHarvestCircleBackup, SafeError> { 48 verify_backup_bundle( 49 manifest_bytes, 50 expected_manifest_digest, 51 bundle_directory, 52 expected_identity, 53 maximum_state_bytes, 54 ) 55 .map(|inner| VerifiedHarvestCircleBackup { inner }) 56 .map_err(|_| invalid_backup()) 57 } 58 59 impl Database { 60 /// Captures one point-in-time backup into a caller-selected new directory. 61 pub async fn capture_online_backup( 62 &self, 63 staging_directory: &Path, 64 created_at_unix_ms: BackupCreatedAtUnixMs, 65 ) -> Result<ServiceBackupManifest, SafeError> { 66 self.host() 67 .capture_online_backup(staging_directory, created_at_unix_ms) 68 .await 69 .map_err(map_service_error) 70 } 71 72 /// Closes live state, installs one verified backup, and reopens recovered state. 73 /// 74 /// Exclusive mutable access prevents concurrent use of the pre-restore 75 /// host. Preflight failure leaves that host open and usable. Failure after 76 /// close may leave exact recovery evidence; a later ordinary 77 /// `Database::open` reconciles that evidence. 78 pub async fn restore_verified_backup( 79 &mut self, 80 context: &RuntimeContext, 81 verified: VerifiedHarvestCircleBackup, 82 applied_at_unix_s: u64, 83 build: &MigrationBuildIdentity, 84 ) -> Result<(), SafeError> { 85 let contract = HarvestCircleStorageContract::from_runtime_context(context) 86 .map_err(|_| invalid_storage_contract())?; 87 let expected = self.metadata().identity(); 88 let backup_metadata = verified.inner.database_metadata(); 89 if expected.service() != contract.paths().service() 90 || expected.instance() != contract.paths().instance() 91 || expected.supported_state_schema_version() != contract.state_schema_version() 92 || expected.application_id() != contract.application_id() 93 || backup_metadata.service() != expected.service() 94 || backup_metadata.instance() != expected.instance() 95 || backup_metadata.source_generation() != expected.source_generation() 96 || backup_metadata.application_id() != expected.application_id() 97 || backup_metadata.state_schema_version() > expected.supported_state_schema_version() 98 { 99 return Err(invalid_storage_contract()); 100 } 101 let applied_at = MigrationAppliedAtUnixSeconds::new(applied_at_unix_s) 102 .map_err(|_| invalid_storage_contract())?; 103 104 self.close().await?; 105 let staged = stage_verified_restore( 106 contract.paths(), 107 &expected, 108 contract.migrations(), 109 contract.schema(), 110 verified.inner, 111 ) 112 .await 113 .map_err(map_restore_error)?; 114 finalize_staged_restore(staged) 115 .await 116 .map_err(map_restore_error)?; 117 let reopened = Self::open_existing(&contract, applied_at, build).await?; 118 *self = reopened; 119 Ok(()) 120 } 121 } 122 123 const fn invalid_backup() -> SafeError { 124 SafeError::new( 125 SafeErrorCode::StorageBackupInvalid, 126 SafeMessage::new("The selected backup could not be verified."), 127 ) 128 } 129 130 fn map_restore_error(error: radroots_service_sqlite::ServiceSqliteError) -> SafeError { 131 use radroots_service_sqlite::ServiceSqliteErrorKind; 132 133 match error.kind() { 134 ServiceSqliteErrorKind::Metadata 135 | ServiceSqliteErrorKind::Migration 136 | ServiceSqliteErrorKind::Integrity 137 | ServiceSqliteErrorKind::Backup => invalid_backup(), 138 ServiceSqliteErrorKind::Recovery => SafeError::new( 139 SafeErrorCode::StorageQuarantined, 140 SafeMessage::new("The application state requires recovery."), 141 ), 142 ServiceSqliteErrorKind::Authority 143 | ServiceSqliteErrorKind::Open 144 | ServiceSqliteErrorKind::Create 145 | ServiceSqliteErrorKind::Pragma 146 | ServiceSqliteErrorKind::Restore => map_service_error(error), 147 } 148 }