app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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 }