storage.rs (10883B)
1 //! Canonical storage capability composition. 2 3 /// Backend-neutral storage status owned by `radroots_storage`. 4 pub type Status = radroots_storage::StorageStatus; 5 6 /// Backend-neutral integrity status owned by `radroots_storage`. 7 pub type IntegrityStatus = radroots_storage::status::IntegrityStatus; 8 9 /// Validated SQLite open configuration; this contains no connection or pool. 10 #[cfg(feature = "sqlite")] 11 pub type SqliteOptions = radroots_storage_sqlite::OpenOptions; 12 13 /// Explicit SQLite lifecycle mode. 14 #[cfg(feature = "sqlite")] 15 pub type SqliteOpenMode = radroots_storage_sqlite::OpenMode; 16 17 /// Validated SQLite-owned paths; this contains no backend handle. 18 #[cfg(feature = "sqlite")] 19 pub type SqlitePaths = radroots_storage_sqlite::Paths; 20 21 use radroots_storage::backup::{ 22 BackupCapabilityError, BackupId, BackupManifest, BackupOperation, BackupPlan, BackupTransition, 23 ReliabilityRevision, RestoreCapabilityError, RestoreMemberStatus, RestoreOperation, 24 RestorePlan, RestoreTransition, StorageReliability, 25 }; 26 27 /// Borrowed reliability operations over the canonical backend-neutral SPI. 28 #[derive(Clone, Copy)] 29 pub struct Operations<'a> { 30 storage: &'a dyn radroots_storage::Storage, 31 } 32 33 impl<'a> Operations<'a> { 34 pub(crate) const fn new(storage: &'a dyn radroots_storage::Storage) -> Self { 35 Self { storage } 36 } 37 38 /// Waits for earlier owner writes, including cancelled caller work. Hold 39 /// application write exclusion before this call until inventory/capture 40 /// completes; this method does not itself stop new application commands. 41 pub async fn settle_backup_writes(&self) -> Result<(), BackupCapabilityError> { 42 StorageReliability::settle_backup_writes(self.storage).await 43 } 44 45 /// Captures actual members through the canonical owner. Related application 46 /// state and media still require host coordination; no metadata transition 47 /// is accepted as evidence of a snapshot. 48 pub async fn capture_backup( 49 &self, 50 plan: BackupPlan, 51 ) -> Result<BackupManifest, BackupCapabilityError> { 52 StorageReliability::capture_backup(self.storage, plan).await 53 } 54 55 /// Verifies the owner's exact staged bundle without finalizing it. 56 pub async fn verify_backup( 57 &self, 58 plan: BackupPlan, 59 manifest: BackupManifest, 60 ) -> Result<(), BackupCapabilityError> { 61 StorageReliability::verify_backup(self.storage, plan, manifest).await 62 } 63 64 /// Verifies and finalizes the owner's bundle, retaining its opaque identity. 65 pub async fn finalize_backup( 66 &self, 67 plan: BackupPlan, 68 manifest: BackupManifest, 69 ) -> Result<(), BackupCapabilityError> { 70 StorageReliability::finalize_backup(self.storage, plan, manifest).await 71 } 72 73 /// Stages actual verified members through the canonical owner without 74 /// changing live state. The host owns identity, media and command exclusion. 75 pub async fn stage_restore( 76 &self, 77 plan: RestorePlan, 78 ) -> Result<Vec<RestoreMemberStatus>, RestoreCapabilityError> { 79 StorageReliability::stage_restore(self.storage, plan).await 80 } 81 82 /// Verifies staging, closes the canonical owner and installs its retained 83 /// snapshot. Reopen explicitly after an installation attempt, then reconcile 84 /// historical operations before allowing delivery. No path is returned. 85 pub async fn finalize_restore(&self, plan: RestorePlan) -> Result<(), RestoreCapabilityError> { 86 StorageReliability::finalize_restore(self.storage, plan).await 87 } 88 89 /// Begins or resumes one idempotent backup plan. 90 pub async fn begin_backup( 91 &self, 92 plan: BackupPlan, 93 ) -> Result<BackupOperation, radroots_storage::Error> { 94 StorageReliability::begin_backup(self.storage, plan).await 95 } 96 97 /// Applies one optimistic backup transition. 98 pub async fn transition_backup( 99 &self, 100 backup_id: BackupId, 101 expected_revision: ReliabilityRevision, 102 transition: BackupTransition, 103 at_unix_ms: u64, 104 ) -> Result<BackupOperation, radroots_storage::Error> { 105 StorageReliability::transition_backup( 106 self.storage, 107 backup_id, 108 expected_revision, 109 transition, 110 at_unix_ms, 111 ) 112 .await 113 } 114 115 /// Begins or resumes one staged restore plan. 116 pub async fn begin_restore( 117 &self, 118 plan: RestorePlan, 119 ) -> Result<RestoreOperation, radroots_storage::Error> { 120 StorageReliability::begin_restore(self.storage, plan).await 121 } 122 123 /// Applies one optimistic staged restore transition. 124 pub async fn transition_restore( 125 &self, 126 backup_id: BackupId, 127 expected_revision: ReliabilityRevision, 128 transition: RestoreTransition, 129 at_unix_ms: u64, 130 ) -> Result<RestoreOperation, radroots_storage::Error> { 131 StorageReliability::transition_restore( 132 self.storage, 133 backup_id, 134 expected_revision, 135 transition, 136 at_unix_ms, 137 ) 138 .await 139 } 140 141 /// Returns passive backend status without initiating recovery work. 142 pub async fn status(&self) -> Result<Status, radroots_storage::Error> { 143 StorageReliability::status(self.storage).await 144 } 145 146 /// Runs backend-owned integrity inspection. 147 pub async fn integrity(&self) -> Result<IntegrityStatus, radroots_storage::Error> { 148 StorageReliability::integrity(self.storage).await 149 } 150 } 151 152 impl std::fmt::Debug for Operations<'_> { 153 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 154 formatter 155 .debug_struct("Operations") 156 .field("storage", &"<borrowed canonical storage>") 157 .finish() 158 } 159 } 160 161 #[cfg(all(test, any(feature = "memory", feature = "sqlite")))] 162 mod backup_tests; 163 164 #[cfg(all(test, any(feature = "memory", feature = "sqlite")))] 165 mod restore_tests; 166 167 #[cfg(all(test, feature = "memory"))] 168 mod tests { 169 use std::sync::Arc; 170 171 use radroots_storage::{ 172 backup::{ 173 BackupFormatVersion, BackupManifest, BackupMember, BackupMemberKind, 174 BackupSecretPolicy, BackupStage, MemberDigest, MemberVerification, RestoreMemberStatus, 175 RestoreStage, 176 }, 177 event::SourceGeneration, 178 memory::MemoryStorage, 179 status::IntegrityHealth, 180 }; 181 182 use crate::ClientBuilder; 183 184 use super::*; 185 186 fn manifest(backup_id: BackupId) -> BackupManifest { 187 BackupManifest::new( 188 BackupFormatVersion::V1, 189 backup_id, 190 1_800_000_000_100, 191 BackupSecretPolicy::ExcludeProtectedStorage, 192 vec![ 193 BackupMember::new( 194 "runtime/events.bin", 195 BackupMemberKind::Runtime, 196 16, 197 MemberDigest::new([3; 32]), 198 ) 199 .expect("member"), 200 ], 201 ) 202 .expect("manifest") 203 } 204 205 #[tokio::test] 206 async fn memory_reliability_preserves_staging_interruption_integrity_and_native_states() { 207 let storage = Arc::new(MemoryStorage::new( 208 SourceGeneration::new([8; 32]).expect("generation"), 209 )); 210 let client = ClientBuilder::new() 211 .storage(storage) 212 .build() 213 .expect("client"); 214 let operations = client.storage_operations().expect("operations"); 215 let backup_id = BackupId::new([9; 16]).expect("backup id"); 216 let plan = BackupPlan::new( 217 backup_id, 218 BackupFormatVersion::V1, 219 BackupSecretPolicy::ExcludeProtectedStorage, 220 1_800_000_000_000, 221 ) 222 .expect("plan"); 223 224 drop(operations.begin_backup(plan.clone())); 225 let planned = operations.begin_backup(plan).await.expect("planned"); 226 assert_eq!(planned.stage(), BackupStage::Planned); 227 let captured = operations 228 .transition_backup( 229 backup_id, 230 planned.revision(), 231 BackupTransition::Captured(manifest(backup_id)), 232 1_800_000_000_200, 233 ) 234 .await 235 .expect("captured"); 236 let verified = operations 237 .transition_backup( 238 backup_id, 239 captured.revision(), 240 BackupTransition::Verified, 241 1_800_000_000_300, 242 ) 243 .await 244 .expect("verified"); 245 let finalized = operations 246 .transition_backup( 247 backup_id, 248 verified.revision(), 249 BackupTransition::Finalize, 250 1_800_000_000_400, 251 ) 252 .await 253 .expect("finalized"); 254 assert_eq!(finalized.stage(), BackupStage::Finalized); 255 256 let restore_plan = RestorePlan::new( 257 manifest(backup_id), 258 BackupSecretPolicy::ExcludeProtectedStorage, 259 1_800_000_001_000, 260 ) 261 .expect("restore plan"); 262 let staging = operations 263 .begin_restore(restore_plan.clone()) 264 .await 265 .expect("staging"); 266 assert_eq!(staging.stage(), RestoreStage::Staging); 267 let replayed = operations 268 .begin_restore(restore_plan) 269 .await 270 .expect("resume staging"); 271 assert_eq!(replayed, staging); 272 let verifying = operations 273 .transition_restore( 274 backup_id, 275 staging.revision(), 276 RestoreTransition::Staged, 277 1_800_000_001_100, 278 ) 279 .await 280 .expect("verifying"); 281 let finalizing = operations 282 .transition_restore( 283 backup_id, 284 verifying.revision(), 285 RestoreTransition::Verified(vec![ 286 RestoreMemberStatus::new("runtime/events.bin", MemberVerification::Verified) 287 .expect("member status"), 288 ]), 289 1_800_000_001_200, 290 ) 291 .await 292 .expect("finalizing"); 293 let restored = operations 294 .transition_restore( 295 backup_id, 296 finalizing.revision(), 297 RestoreTransition::Finalize, 298 1_800_000_001_300, 299 ) 300 .await 301 .expect("restored"); 302 assert_eq!(restored.stage(), RestoreStage::Finalized); 303 assert_eq!( 304 operations.integrity().await.expect("integrity").health(), 305 IntegrityHealth::Healthy 306 ); 307 assert_eq!( 308 operations 309 .status() 310 .await 311 .expect("status") 312 .integrity() 313 .health(), 314 IntegrityHealth::Healthy 315 ); 316 } 317 }