lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

store.rs (6854B)


      1 use crate::error::RadrootsNostrAccountsError;
      2 use crate::model::RadrootsNostrAccountStoreState;
      3 use radroots_runtime::json::{JsonFile, JsonWriteOptions};
      4 use std::path::{Path, PathBuf};
      5 use std::sync::{Arc, RwLock};
      6 
      7 pub trait RadrootsNostrAccountStore: Send + Sync {
      8     fn load(&self) -> Result<RadrootsNostrAccountStoreState, RadrootsNostrAccountsError>;
      9     fn save(
     10         &self,
     11         state: &RadrootsNostrAccountStoreState,
     12     ) -> Result<(), RadrootsNostrAccountsError>;
     13 }
     14 
     15 #[derive(Debug, Clone)]
     16 pub struct RadrootsNostrFileAccountStore {
     17     path: PathBuf,
     18 }
     19 
     20 impl RadrootsNostrFileAccountStore {
     21     pub fn new(path: impl AsRef<Path>) -> Self {
     22         Self {
     23             path: path.as_ref().to_path_buf(),
     24         }
     25     }
     26 
     27     pub fn path(&self) -> &Path {
     28         self.path.as_path()
     29     }
     30 }
     31 
     32 #[derive(Debug, Clone, Default)]
     33 pub struct RadrootsNostrMemoryAccountStore {
     34     state: Arc<RwLock<RadrootsNostrAccountStoreState>>,
     35 }
     36 
     37 impl RadrootsNostrMemoryAccountStore {
     38     pub fn new() -> Self {
     39         Self::default()
     40     }
     41 }
     42 
     43 impl RadrootsNostrAccountStore for RadrootsNostrFileAccountStore {
     44     fn load(&self) -> Result<RadrootsNostrAccountStoreState, RadrootsNostrAccountsError> {
     45         if !self.path.exists() {
     46             return Ok(RadrootsNostrAccountStoreState::default());
     47         }
     48         let file = JsonFile::<RadrootsNostrAccountStoreState>::load(self.path.as_path())?;
     49         Ok(file.value)
     50     }
     51 
     52     fn save(
     53         &self,
     54         state: &RadrootsNostrAccountStoreState,
     55     ) -> Result<(), RadrootsNostrAccountsError> {
     56         let mut file = JsonFile::load_or_create_with(self.path.as_path(), || state.clone())?;
     57         file.set_options(JsonWriteOptions {
     58             pretty: true,
     59             mode_unix: Some(0o600),
     60         });
     61         file.value = state.clone();
     62         if let Err(err) = file.save() {
     63             return Err(err.into());
     64         }
     65         Ok(())
     66     }
     67 }
     68 
     69 impl RadrootsNostrAccountStore for RadrootsNostrMemoryAccountStore {
     70     fn load(&self) -> Result<RadrootsNostrAccountStoreState, RadrootsNostrAccountsError> {
     71         let guard = self
     72             .state
     73             .read()
     74             .map_err(|_| RadrootsNostrAccountsError::Store("memory store lock poisoned".into()))?;
     75         Ok(guard.clone())
     76     }
     77 
     78     fn save(
     79         &self,
     80         state: &RadrootsNostrAccountStoreState,
     81     ) -> Result<(), RadrootsNostrAccountsError> {
     82         let mut guard = self
     83             .state
     84             .write()
     85             .map_err(|_| RadrootsNostrAccountsError::Store("memory store lock poisoned".into()))?;
     86         *guard = state.clone();
     87         Ok(())
     88     }
     89 }
     90 
     91 #[cfg(test)]
     92 mod tests {
     93     use super::*;
     94     use std::thread;
     95 
     96     #[test]
     97     fn file_store_round_trip() {
     98         let temp = tempfile::tempdir().expect("tempdir");
     99         let path = temp.path().join("accounts.json");
    100         let store = RadrootsNostrFileAccountStore::new(path.as_path());
    101 
    102         let state = RadrootsNostrAccountStoreState::default();
    103         store.save(&state).expect("save");
    104         let loaded = store.load().expect("load");
    105         assert_eq!(loaded.version, state.version);
    106         assert!(loaded.accounts.is_empty());
    107     }
    108 
    109     #[test]
    110     fn file_store_load_missing_and_path_accessor() {
    111         let temp = tempfile::tempdir().expect("tempdir");
    112         let path = temp.path().join("missing.json");
    113         let store = RadrootsNostrFileAccountStore::new(path.as_path());
    114 
    115         assert_eq!(store.path(), path.as_path());
    116         let loaded = store.load().expect("load");
    117         assert_eq!(
    118             loaded.version,
    119             RadrootsNostrAccountStoreState::default().version
    120         );
    121         assert!(loaded.accounts.is_empty());
    122     }
    123 
    124     #[test]
    125     fn file_store_load_reports_parse_error() {
    126         let temp = tempfile::tempdir().expect("tempdir");
    127         let path = temp.path().join("invalid.json");
    128         std::fs::write(&path, "{").expect("write invalid json");
    129         let store = RadrootsNostrFileAccountStore::new(path.as_path());
    130 
    131         let err = store.load().expect_err("invalid json");
    132         assert!(err.to_string().starts_with("store error:"));
    133     }
    134 
    135     #[test]
    136     fn file_store_save_reports_parse_error() {
    137         let temp = tempfile::tempdir().expect("tempdir");
    138         let path = temp.path().join("invalid.json");
    139         std::fs::write(&path, "{").expect("write invalid json");
    140         let store = RadrootsNostrFileAccountStore::new(path.as_path());
    141 
    142         let err = store
    143             .save(&RadrootsNostrAccountStoreState::default())
    144             .expect_err("invalid json save");
    145         assert!(err.to_string().starts_with("store error:"));
    146     }
    147 
    148     #[cfg(unix)]
    149     #[test]
    150     fn file_store_save_reports_write_error() {
    151         use std::os::unix::fs::PermissionsExt;
    152 
    153         let temp = tempfile::tempdir().expect("tempdir");
    154         let path = temp.path().join("accounts.json");
    155         let json =
    156             serde_json::to_string(&RadrootsNostrAccountStoreState::default()).expect("serialize");
    157         std::fs::write(&path, json).expect("write json");
    158         let store = RadrootsNostrFileAccountStore::new(path.as_path());
    159 
    160         let mut perms = std::fs::metadata(temp.path())
    161             .expect("dir metadata")
    162             .permissions();
    163         perms.set_mode(0o500);
    164         std::fs::set_permissions(temp.path(), perms).expect("set perms");
    165 
    166         let err = store
    167             .save(&RadrootsNostrAccountStoreState::default())
    168             .expect_err("read-only save");
    169         assert!(err.to_string().starts_with("store error:"));
    170 
    171         let mut perms = std::fs::metadata(temp.path())
    172             .expect("dir metadata")
    173             .permissions();
    174         perms.set_mode(0o700);
    175         std::fs::set_permissions(temp.path(), perms).expect("restore perms");
    176     }
    177 
    178     #[test]
    179     fn memory_store_round_trip() {
    180         let store = RadrootsNostrMemoryAccountStore::new();
    181         let state = RadrootsNostrAccountStoreState::default();
    182         store.save(&state).expect("save");
    183 
    184         let loaded = store.load().expect("load");
    185         assert_eq!(loaded.version, state.version);
    186         assert_eq!(loaded.default_account_id, state.default_account_id);
    187     }
    188 
    189     #[test]
    190     fn memory_store_reports_poisoned_lock() {
    191         let store = RadrootsNostrMemoryAccountStore::new();
    192         let shared = store.state.clone();
    193         let _ = thread::spawn(move || {
    194             let _guard = shared.write().expect("write");
    195             panic!("poison memory store");
    196         })
    197         .join();
    198 
    199         let load = store.load().expect_err("poisoned load");
    200         assert!(load.to_string().contains("memory store lock poisoned"));
    201 
    202         let save = store
    203             .save(&RadrootsNostrAccountStoreState::default())
    204             .expect_err("poisoned save");
    205         assert!(save.to_string().contains("memory store lock poisoned"));
    206     }
    207 }