app

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

commit 32ac332abe410ce7bc2ef524ae4610bde066127f
parent bc6f91db370f9facf64edff67ff0a5f893fe96ee
Author: triesap <triesap@radroots.dev>
Date:   Mon, 19 Jan 2026 00:41:24 +0000

app-core: add backup bundle types

- define backup bundle, manifest, and payload structures
- add store type helpers and payload accessors
- introduce backup store traits for sql, keystore, datastore
- add unit tests for store type parsing and payload helpers

Diffstat:
Mcrates/core/src/backup/mod.rs | 19+++++++++++++++++++
Acrates/core/src/backup/types.rs | 209+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 228 insertions(+), 0 deletions(-)

diff --git a/crates/core/src/backup/mod.rs b/crates/core/src/backup/mod.rs @@ -1,3 +1,22 @@ pub mod error; +pub mod types; pub use error::{RadrootsClientBackupError, RadrootsClientBackupErrorMessage}; +pub use types::{ + RadrootsClientBackupBundle, + RadrootsClientBackupBundleEnvelope, + RadrootsClientBackupBundleManifest, + RadrootsClientBackupBundlePayload, + RadrootsClientBackupBundleStoreType, + RadrootsClientBackupBundleVersion, + RadrootsClientBackupDatastoreEntry, + RadrootsClientBackupDatastorePayload, + RadrootsClientBackupDatastoreStore, + RadrootsClientBackupKeystoreEntry, + RadrootsClientBackupKeystorePayload, + RadrootsClientBackupKeystoreStore, + RadrootsClientBackupSqlPayload, + RadrootsClientBackupSqlStore, + RadrootsClientBackupStoreRef, + RADROOTS_CLIENT_BACKUP_BUNDLE_VERSION, +}; diff --git a/crates/core/src/backup/types.rs b/crates/core/src/backup/types.rs @@ -0,0 +1,209 @@ +use async_trait::async_trait; + +use crate::crypto::RadrootsClientCryptoRegistryExport; + +pub type RadrootsClientBackupBundleVersion = u8; + +pub const RADROOTS_CLIENT_BACKUP_BUNDLE_VERSION: RadrootsClientBackupBundleVersion = 1; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsClientBackupBundleStoreType { + Sql, + Keystore, + Datastore, +} + +impl RadrootsClientBackupBundleStoreType { + pub const fn as_str(self) -> &'static str { + match self { + RadrootsClientBackupBundleStoreType::Sql => "sql", + RadrootsClientBackupBundleStoreType::Keystore => "keystore", + RadrootsClientBackupBundleStoreType::Datastore => "datastore", + } + } + + pub fn parse(value: &str) -> Option<Self> { + match value { + "sql" => Some(RadrootsClientBackupBundleStoreType::Sql), + "keystore" => Some(RadrootsClientBackupBundleStoreType::Keystore), + "datastore" => Some(RadrootsClientBackupBundleStoreType::Datastore), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsClientBackupSqlPayload { + pub bytes_b64: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsClientBackupKeystoreEntry { + pub key: String, + pub value: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsClientBackupKeystorePayload { + pub entries: Vec<RadrootsClientBackupKeystoreEntry>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsClientBackupDatastoreEntry { + pub key: String, + pub value: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsClientBackupDatastorePayload { + pub entries: Vec<RadrootsClientBackupDatastoreEntry>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RadrootsClientBackupBundlePayload { + Sql { + store_id: String, + data: RadrootsClientBackupSqlPayload, + }, + Keystore { + store_id: String, + data: RadrootsClientBackupKeystorePayload, + }, + Datastore { + store_id: String, + data: RadrootsClientBackupDatastorePayload, + }, +} + +impl RadrootsClientBackupBundlePayload { + pub fn store_type(&self) -> RadrootsClientBackupBundleStoreType { + match self { + RadrootsClientBackupBundlePayload::Sql { .. } => { + RadrootsClientBackupBundleStoreType::Sql + } + RadrootsClientBackupBundlePayload::Keystore { .. } => { + RadrootsClientBackupBundleStoreType::Keystore + } + RadrootsClientBackupBundlePayload::Datastore { .. } => { + RadrootsClientBackupBundleStoreType::Datastore + } + } + } + + pub fn store_id(&self) -> &str { + match self { + RadrootsClientBackupBundlePayload::Sql { store_id, .. } => store_id, + RadrootsClientBackupBundlePayload::Keystore { store_id, .. } => store_id, + RadrootsClientBackupBundlePayload::Datastore { store_id, .. } => store_id, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsClientBackupStoreRef { + pub store_id: String, + pub store_type: RadrootsClientBackupBundleStoreType, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsClientBackupBundleManifest { + pub version: RadrootsClientBackupBundleVersion, + pub created_at: u64, + pub app_version: Option<String>, + pub stores: Vec<RadrootsClientBackupStoreRef>, + pub crypto_registry: RadrootsClientCryptoRegistryExport, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsClientBackupBundle { + pub manifest: RadrootsClientBackupBundleManifest, + pub payloads: Vec<RadrootsClientBackupBundlePayload>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsClientBackupBundleEnvelope { + pub version: u8, + pub created_at: u64, + pub kdf_salt_b64: String, + pub kdf_iterations: u32, + pub iv_b64: String, + pub ciphertext_b64: String, +} + +#[async_trait(?Send)] +pub trait RadrootsClientBackupSqlStore { + type Error; + + async fn export_backup(&self) -> Result<RadrootsClientBackupSqlPayload, Self::Error>; + async fn import_backup(&self, payload: RadrootsClientBackupSqlPayload) + -> Result<(), Self::Error>; + fn store_id(&self) -> &str; +} + +#[async_trait(?Send)] +pub trait RadrootsClientBackupKeystoreStore { + type Error; + + async fn export_backup(&self) -> Result<RadrootsClientBackupKeystorePayload, Self::Error>; + async fn import_backup( + &self, + payload: RadrootsClientBackupKeystorePayload, + ) -> Result<(), Self::Error>; + fn store_id(&self) -> &str; +} + +#[async_trait(?Send)] +pub trait RadrootsClientBackupDatastoreStore { + type Error; + + async fn export_backup(&self) -> Result<RadrootsClientBackupDatastorePayload, Self::Error>; + async fn import_backup( + &self, + payload: RadrootsClientBackupDatastorePayload, + ) -> Result<(), Self::Error>; + fn store_id(&self) -> &str; +} + +#[cfg(test)] +mod tests { + use super::{ + RadrootsClientBackupBundlePayload, + RadrootsClientBackupBundleStoreType, + }; + + #[test] + fn store_type_roundtrip() { + let sql = RadrootsClientBackupBundleStoreType::Sql; + let keystore = RadrootsClientBackupBundleStoreType::Keystore; + let datastore = RadrootsClientBackupBundleStoreType::Datastore; + + assert_eq!(sql.as_str(), "sql"); + assert_eq!(keystore.as_str(), "keystore"); + assert_eq!(datastore.as_str(), "datastore"); + assert_eq!(RadrootsClientBackupBundleStoreType::parse("sql"), Some(sql)); + assert_eq!( + RadrootsClientBackupBundleStoreType::parse("keystore"), + Some(keystore) + ); + assert_eq!( + RadrootsClientBackupBundleStoreType::parse("datastore"), + Some(datastore) + ); + assert_eq!(RadrootsClientBackupBundleStoreType::parse("other"), None); + } + + #[test] + fn payload_store_helpers() { + let payload = RadrootsClientBackupBundlePayload::Sql { + store_id: "store".to_string(), + data: super::RadrootsClientBackupSqlPayload { + bytes_b64: "bytes".to_string(), + }, + }; + assert_eq!(payload.store_id(), "store"); + assert_eq!( + payload.store_type(), + RadrootsClientBackupBundleStoreType::Sql + ); + } +}