notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 11edde45f434160634e54e3b195a0e2d34ae3ade
parent 329385bd900d98899d9608d5142d63e2153cf1ea
Author: kernelkind <kernelkind@gmail.com>
Date:   Sun, 29 Jun 2025 16:39:41 -0400

split `AccountStorage` into reader & writer

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/notedeck/src/account/accounts.rs | 23++++++++++++++---------
Mcrates/notedeck/src/storage/account_storage.rs | 104+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mcrates/notedeck/src/storage/file_storage.rs | 2+-
Mcrates/notedeck/src/storage/mod.rs | 2+-
4 files changed, 83 insertions(+), 48 deletions(-)

diff --git a/crates/notedeck/src/account/accounts.rs b/crates/notedeck/src/account/accounts.rs @@ -3,6 +3,7 @@ use tracing::{debug, info}; use crate::account::cache::AccountCache; use crate::account::mute::AccountMutedData; use crate::account::relay::{AccountRelayData, RelayDefaults}; +use crate::storage::AccountStorageWriter; use crate::user_account::UserAccountSerializable; use crate::{AccountStorage, MuteFun, RelaySpec, SingleUnkIdAction, UnknownIds, UserAccount}; use enostr::{ClientMessage, FilledKeypair, Keypair, Pubkey, RelayPool}; @@ -16,7 +17,7 @@ use std::sync::Arc; /// Represents all user-facing operations related to account management. pub struct Accounts { pub cache: AccountCache, - key_store: Option<AccountStorage>, + storage_writer: Option<AccountStorageWriter>, relay_defaults: RelayDefaults, needs_relay_config: bool, } @@ -40,8 +41,10 @@ impl Accounts { unknown_id.process_action(unknown_ids, ndb, txn); - if let Some(keystore) = &key_store { - match keystore.get_accounts() { + let mut storage_writer = None; + if let Some(keystore) = key_store { + let (reader, writer) = keystore.rw(); + match reader.get_accounts() { Ok(accounts) => { for account in accounts { add_account_from_storage(&mut cache, ndb, txn, account).process_action( @@ -55,16 +58,18 @@ impl Accounts { tracing::error!("could not get keys: {e}"); } } - if let Some(selected) = keystore.get_selected_key().ok().flatten() { + if let Some(selected) = reader.get_selected_key().ok().flatten() { cache.select(selected); } + + storage_writer = Some(writer); }; let relay_defaults = RelayDefaults::new(forced_relays); Accounts { cache, - key_store, + storage_writer, relay_defaults, needs_relay_config: true, } @@ -75,7 +80,7 @@ impl Accounts { return; }; - if let Some(key_store) = &self.key_store { + if let Some(key_store) = &self.storage_writer { if let Err(e) = key_store.remove_key(&removed.key) { tracing::error!("Could not remove account {pk}: {e}"); } @@ -118,7 +123,7 @@ impl Accounts { ) }; - if let Some(key_store) = &self.key_store { + if let Some(key_store) = &self.storage_writer { if let Err(e) = key_store.write_account(&acc.get_acc().into()) { tracing::error!("Could not add key for {:?}: {e}", kp.pubkey); } @@ -139,7 +144,7 @@ impl Accounts { let cur_acc = self.get_selected_account(); - let Some(key_store) = &self.key_store else { + let Some(key_store) = &self.storage_writer else { return false; }; @@ -190,7 +195,7 @@ impl Accounts { return; } - if let Some(key_store) = &self.key_store { + if let Some(key_store) = &self.storage_writer { if let Err(e) = key_store.select_key(Some(*pk)) { tracing::error!("Could not select key {:?}: {e}", pk); } diff --git a/crates/notedeck/src/storage/account_storage.rs b/crates/notedeck/src/storage/account_storage.rs @@ -7,7 +7,7 @@ use super::file_storage::{delete_file, write_file, Directory}; static SELECTED_PUBKEY_FILE_NAME: &str = "selected_pubkey"; /// An OS agnostic file key storage implementation -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct AccountStorage { accounts_directory: Directory, selected_key_directory: Directory, @@ -21,56 +21,53 @@ impl AccountStorage { } } + pub fn rw(self) -> (AccountStorageReader, AccountStorageWriter) { + ( + AccountStorageReader::new(self.clone()), + AccountStorageWriter::new(self), + ) + } +} + +pub struct AccountStorageWriter { + storage: AccountStorage, +} + +impl AccountStorageWriter { + pub fn new(storage: AccountStorage) -> Self { + Self { storage } + } + pub fn write_account(&self, account: &UserAccountSerializable) -> Result<()> { let mut writer = TokenWriter::new("\t"); account.serialize_tokens(&mut writer); write_file( - &self.accounts_directory.file_path, + &self.storage.accounts_directory.file_path, account.key.pubkey.hex(), writer.str(), ) } - pub fn get_accounts(&self) -> Result<Vec<UserAccountSerializable>> { - let keys = self - .accounts_directory - .get_files()? - .values() - .filter_map(|serialized| deserialize_storage(serialized).ok()) - .collect(); - Ok(keys) - } - pub fn remove_key(&self, key: &Keypair) -> Result<()> { - delete_file(&self.accounts_directory.file_path, key.pubkey.hex()) - } - - pub fn get_selected_key(&self) -> Result<Option<Pubkey>> { - match self - .selected_key_directory - .get_file(SELECTED_PUBKEY_FILE_NAME.to_owned()) - { - Ok(pubkey_str) => Ok(Some(serde_json::from_str(&pubkey_str)?)), - Err(crate::Error::Io(_)) => Ok(None), - Err(e) => Err(e), - } + delete_file(&self.storage.accounts_directory.file_path, key.pubkey.hex()) } pub fn select_key(&self, pubkey: Option<Pubkey>) -> Result<()> { if let Some(pubkey) = pubkey { write_file( - &self.selected_key_directory.file_path, + &self.storage.selected_key_directory.file_path, SELECTED_PUBKEY_FILE_NAME.to_owned(), &serde_json::to_string(&pubkey.hex())?, ) } else if self + .storage .selected_key_directory .get_file(SELECTED_PUBKEY_FILE_NAME.to_owned()) .is_ok() { // Case where user chose to have no selected pubkey, but one already exists Ok(delete_file( - &self.selected_key_directory.file_path, + &self.storage.selected_key_directory.file_path, SELECTED_PUBKEY_FILE_NAME.to_owned(), )?) } else { @@ -79,6 +76,39 @@ impl AccountStorage { } } +pub struct AccountStorageReader { + storage: AccountStorage, +} + +impl AccountStorageReader { + pub fn new(storage: AccountStorage) -> Self { + Self { storage } + } + + pub fn get_accounts(&self) -> Result<Vec<UserAccountSerializable>> { + let keys = self + .storage + .accounts_directory + .get_files()? + .values() + .filter_map(|serialized| deserialize_storage(serialized).ok()) + .collect(); + Ok(keys) + } + + pub fn get_selected_key(&self) -> Result<Option<Pubkey>> { + match self + .storage + .selected_key_directory + .get_file(SELECTED_PUBKEY_FILE_NAME.to_owned()) + { + Ok(pubkey_str) => Ok(Some(serde_json::from_str(&pubkey_str)?)), + Err(crate::Error::Io(_)) => Ok(None), + Err(e) => Err(e), + } + } +} + fn deserialize_storage(serialized: &str) -> Result<UserAccountSerializable> { let data = serialized.split("\t").collect::<Vec<&str>>(); let mut parser = TokenParser::new(&data); @@ -119,14 +149,14 @@ mod tests { #[test] fn test_basic() { let kp = enostr::FullKeypair::generate().to_keypair(); - let storage = AccountStorage::mock().unwrap(); - let resp = storage.write_account(&UserAccountSerializable::new(kp.clone())); + let (reader, writer) = AccountStorage::mock().unwrap().rw(); + let resp = writer.write_account(&UserAccountSerializable::new(kp.clone())); assert!(resp.is_ok()); - assert_num_storage(&storage.get_accounts(), 1); + assert_num_storage(&reader.get_accounts(), 1); - assert!(storage.remove_key(&kp).is_ok()); - assert_num_storage(&storage.get_accounts(), 0); + assert!(writer.remove_key(&kp).is_ok()); + assert_num_storage(&reader.get_accounts(), 0); } fn assert_num_storage(keys_response: &Result<Vec<UserAccountSerializable>>, n: usize) { @@ -144,21 +174,21 @@ mod tests { fn test_select_key() { let kp = enostr::FullKeypair::generate().to_keypair(); - let storage = AccountStorage::mock().unwrap(); - let _ = storage.write_account(&UserAccountSerializable::new(kp.clone())); - assert_num_storage(&storage.get_accounts(), 1); + let (reader, writer) = AccountStorage::mock().unwrap().rw(); + let _ = writer.write_account(&UserAccountSerializable::new(kp.clone())); + assert_num_storage(&reader.get_accounts(), 1); - let resp = storage.select_key(Some(kp.pubkey)); + let resp = writer.select_key(Some(kp.pubkey)); assert!(resp.is_ok()); - let resp = storage.get_selected_key(); + let resp = reader.get_selected_key(); assert!(resp.is_ok()); } #[test] fn test_get_selected_key_when_no_file() { - let storage = AccountStorage::mock().unwrap(); + let storage = AccountStorage::mock().unwrap().rw().0; // Should return Ok(None) when no key has been selected match storage.get_selected_key() { diff --git a/crates/notedeck/src/storage/file_storage.rs b/crates/notedeck/src/storage/file_storage.rs @@ -59,7 +59,7 @@ pub enum DataPathType { Cache, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct Directory { pub file_path: PathBuf, } diff --git a/crates/notedeck/src/storage/mod.rs b/crates/notedeck/src/storage/mod.rs @@ -1,5 +1,5 @@ mod account_storage; mod file_storage; -pub use account_storage::AccountStorage; +pub use account_storage::{AccountStorage, AccountStorageReader, AccountStorageWriter}; pub use file_storage::{delete_file, write_file, DataPath, DataPathType, Directory};