SQLite-backed Key / Value Store
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: got the db client working as a store

+122 -51
+14 -11
src/main.rs
··· 11 11 let cli = Cli::parse(); 12 12 let mut safir = store::init_safir().await.context("loading safir store")?; 13 13 14 - // This is stupid 15 14 match cli.command { 16 15 Commands::Add { key, value } => safir.add(key.to_owned(), value.to_owned()).await?, 17 - Commands::Get { keys } => safir.get(keys.to_owned()).await?, 16 + Commands::Get { keys } => { 17 + let kvs = safir.get(keys.to_owned()).await?; 18 + utils::display_multiple_kv(kvs); 19 + } 18 20 Commands::Rm { keys } => safir.remove(keys.to_owned()).await?, 19 - Commands::Alias { keys: _ } => { 20 - // TODO: Fix custom displays 21 - unimplemented!("alias needs work"); 22 - // safir.custom_display("alias", keys.to_owned()).await?; 21 + Commands::Alias { keys } => { 22 + let kvs = safir.get(keys.to_owned()).await?; 23 + utils::custom_display("alias", kvs); 24 + } 25 + Commands::Export { keys } => { 26 + let kvs = safir.get(keys.to_owned()).await?; 27 + utils::custom_display("export", kvs); 23 28 } 24 - Commands::Export { keys: _ } => { 25 - unimplemented!("export needs work"); 26 - // safir.custom_display("export", keys.to_owned()).await?; 29 + Commands::List => { 30 + let kvs = safir.list().await?; 31 + utils::display_multiple_kv(kvs); 27 32 } 28 - Commands::List => safir.list().await?, 29 33 Commands::Clear => safir.clear().await?, 30 34 Commands::Purge => safir.purge().await?, 31 35 Commands::Mode { mode } => { ··· 35 39 } 36 40 } 37 41 38 - // utils::write_store(&safir.store, &safir.file); 39 42 Ok(()) 40 43 }
+57 -5
src/store/db_store.rs
··· 5 5 use std::path::PathBuf; 6 6 use std::str::FromStr; 7 7 8 - use crate::store::{config::SafirConfig, SafirStore}; 8 + use crate::{ 9 + store::{config::SafirConfig, SafirStore}, 10 + utils::{confirm_entry, load_safir_workspace, purge_directory, KVPair}, 11 + }; 9 12 10 13 #[derive(Debug, Clone)] 11 14 pub struct SqliteStore { ··· 46 49 #[async_trait] 47 50 impl SafirStore for SqliteStore { 48 51 async fn add(&mut self, key: String, value: String) -> Result<()> { 52 + sqlx::query("insert into safir(key, value) values(?1, ?2)") 53 + .bind(&key) 54 + .bind(&value) 55 + .execute(&self.pool) 56 + .await 57 + .with_context(|| format!("insert {key} - {value} into database"))?; 58 + 49 59 Ok(()) 50 60 } 51 - async fn get(&self, keys: Vec<String>) -> Result<()> { 52 - Ok(()) 61 + 62 + async fn get(&self, keys: Vec<String>) -> Result<Vec<KVPair>> { 63 + let keys = keys 64 + .into_iter() 65 + .map(|k| format!("'{k}'")) 66 + .collect::<Vec<String>>(); 67 + 68 + let query = format!("select * from safir where key in ({})", keys.join(", ")); 69 + let results: Vec<KVPair> = sqlx::query_as::<_, KVPair>(&query) 70 + .fetch_all(&self.pool) 71 + .await?; 72 + 73 + Ok(results) 53 74 } 54 - async fn list(&self) -> Result<()> { 55 - Ok(()) 75 + 76 + async fn list(&self) -> Result<Vec<KVPair>> { 77 + let results: Vec<KVPair> = sqlx::query_as::<_, KVPair>("select * from safir") 78 + .fetch_all(&self.pool) 79 + .await?; 80 + 81 + Ok(results) 56 82 } 83 + 57 84 async fn remove(&mut self, keys: Vec<String>) -> Result<()> { 85 + let keys = keys 86 + .into_iter() 87 + .map(|k| format!("'{k}'")) 88 + .collect::<Vec<String>>(); 89 + 90 + let query = format!("delete from safir where key in ({})", keys.join(", ")); 91 + let _ = sqlx::query_as::<_, KVPair>(&query) 92 + .fetch_all(&self.pool) 93 + .await?; 94 + 58 95 Ok(()) 59 96 } 97 + 60 98 async fn clear(&mut self) -> Result<()> { 99 + if confirm_entry("Are you sure you want to clear the safirstore?") { 100 + let _ = sqlx::query_as::<_, KVPair>("delete from safir") 101 + .fetch_all(&self.pool) 102 + .await?; 103 + } 104 + 61 105 Ok(()) 62 106 } 107 + 63 108 async fn purge(&mut self) -> Result<()> { 109 + let ws = load_safir_workspace(); 110 + let confirm_msg = 111 + "Are you sure you want to purge the safirstore?\nThis will delete the folder and any data inside!"; 112 + 113 + if confirm_entry(&confirm_msg) { 114 + purge_directory(ws); 115 + } 64 116 Ok(()) 65 117 } 66 118
+27 -16
src/store/file_store.rs
··· 1 1 use crate::{ 2 2 store::{config::SafirConfig, SafirStore}, 3 - utils, 3 + utils::{self, KVPair}, 4 4 }; 5 5 6 6 use anyhow::Result; ··· 10 10 11 11 #[derive(Debug, Clone)] 12 12 pub struct KVStore { 13 - path: PathBuf, 13 + loc: PathBuf, 14 14 store: HashMap<String, String>, 15 15 config: SafirConfig, 16 16 } ··· 27 27 }; 28 28 29 29 Self { 30 - path: store_path, 30 + loc: store_path, 31 31 config, 32 32 store, 33 33 } ··· 46 46 self.store.insert(key, value); 47 47 } 48 48 49 + utils::write_store(&self.store, &self.loc); 50 + 49 51 Ok(()) 50 52 } 51 53 52 - async fn get(&self, keys: Vec<String>) -> Result<()> { 53 - for key in keys.iter() { 54 - if let Some(value) = self.store.get(key) { 55 - utils::display_kv(key, value); 56 - } 57 - } 54 + async fn get(&self, keys: Vec<String>) -> Result<Vec<KVPair>> { 55 + let kvs: Vec<KVPair> = keys 56 + .into_iter() 57 + .filter_map(|key| match self.store.get(&key) { 58 + Some(value) => Some((key, value.clone())), 59 + None => None, 60 + }) 61 + .collect(); 58 62 59 - Ok(()) 63 + Ok(kvs) 60 64 } 61 65 62 - async fn list(&self) -> Result<()> { 63 - for (key, value) in self.store.iter() { 64 - utils::display_kv(key, value); 65 - } 66 + async fn list(&self) -> Result<Vec<KVPair>> { 67 + let kvs: Vec<KVPair> = self 68 + .store 69 + .iter() 70 + .map(|(key, value)| (key.clone(), value.clone())) 71 + .collect(); 66 72 67 - Ok(()) 73 + Ok(kvs) 68 74 } 69 75 70 76 async fn remove(&mut self, keys: Vec<String>) -> Result<()> { ··· 76 82 } 77 83 } 78 84 } 85 + 86 + utils::write_store(&self.store, &self.loc); 79 87 80 88 Ok(()) 81 89 } ··· 85 93 self.store.clear(); 86 94 } 87 95 96 + utils::write_store(&self.store, &self.loc); 97 + 88 98 Ok(()) 89 99 } 90 100 91 101 async fn purge(&mut self) -> Result<()> { 92 102 let confirm_msg = 93 103 "Are you sure you want to remove the .safirstore directory and ALL contents?"; 104 + let ws = utils::load_safir_workspace(); 94 105 if utils::confirm_entry(&confirm_msg) { 95 - utils::purge_directory(self.path.clone()); 106 + utils::purge_directory(ws); 96 107 std::process::exit(0); 97 108 } 98 109
+4 -4
src/store/mod.rs
··· 2 2 pub mod db_store; 3 3 pub mod file_store; 4 4 5 - use crate::utils; 5 + use crate::utils::{load_safir_workspace, KVPair}; 6 6 use config::{SafirConfig, SafirMode}; 7 7 8 8 use anyhow::Result; ··· 13 13 #[async_trait] 14 14 pub trait SafirStore { 15 15 async fn add(&mut self, key: String, value: String) -> Result<()>; 16 - async fn get(&self, keys: Vec<String>) -> Result<()>; 17 - async fn list(&self) -> Result<()>; 16 + async fn get(&self, keys: Vec<String>) -> Result<Vec<KVPair>>; 17 + async fn list(&self) -> Result<Vec<KVPair>>; 18 18 async fn remove(&mut self, keys: Vec<String>) -> Result<()>; 19 19 async fn clear(&mut self) -> Result<()>; 20 20 async fn purge(&mut self) -> Result<()>; ··· 22 22 } 23 23 24 24 pub async fn init_safir() -> Result<Box<dyn SafirStore>> { 25 - let ws = utils::create_safir_workspace(); 25 + let ws = load_safir_workspace(); 26 26 let cfg = SafirConfig::load(&ws).expect("can't load safir config"); 27 27 28 28 match cfg.mode {
+20 -15
src/utils.rs
··· 8 8 /// Debug flag for testing without affecting my existing store 9 9 const DEBUG: bool = true; 10 10 11 + /// Type to represent a KVPair 12 + pub type KVPair = (String, String); 13 + 11 14 /// Confirmation dialog for important calls 12 15 pub fn confirm_entry(msg: &str) -> bool { 13 16 let mut answer = String::new(); ··· 19 22 .expect("unable to get input from user"); 20 23 21 24 let answer = answer.trim().to_lowercase(); 22 - if answer == "y" || answer == "yes" { 23 - return true; 24 - } 25 25 26 - false 26 + answer == "y" || answer == "yes" 27 27 } 28 28 29 - /// Outputs the Key-Value pair 30 - pub fn display_kv(key: &str, value: &str) { 31 - println!("{key}=\"{value}\"") 29 + /// Outputs multiple KV pairs 30 + pub fn display_multiple_kv(kvs: Vec<KVPair>) { 31 + for kv in kvs.into_iter() { 32 + let (key, value) = kv; 33 + println!("{key}=\"{value}\""); 34 + } 32 35 } 33 36 34 37 /// Output key-value pairs with a leading string (e.g. alias or export) 35 - pub fn custom_display(display_cmd: &str, keys: Vec<String>, values: Vec<String>) { 36 - for (key, value) in keys.iter().zip(values.iter()) { 38 + pub fn custom_display(display_cmd: &str, kvs: Vec<KVPair>) { 39 + for kv in kvs.into_iter() { 40 + let (key, value) = kv; 37 41 println!("{display_cmd} {key}=\"{value}\""); 38 42 } 39 43 } ··· 59 63 60 64 /// Remove the .safirstore directory 61 65 pub fn purge_directory(path: impl AsRef<Path>) { 62 - std::fs::remove_dir_all(path).expect("unable to remove safirstore directory"); 66 + if path.as_ref().exists() { 67 + std::fs::remove_dir_all(path).expect("unable to remove safirstore directory"); 68 + } 63 69 } 64 70 65 71 /// Create the .safirstore directory in the user HOME 66 - pub fn create_safir_workspace() -> PathBuf { 72 + pub fn load_safir_workspace() -> PathBuf { 67 73 let store_dir = if DEBUG { 68 74 ".debug_safirstore" 69 75 } else { 70 76 ".safirstore" 71 77 }; 72 78 73 - if DEBUG { 74 - println!("DEBUG: Creating safir store at debug location"); 75 - } 76 - 77 79 match dirs::home_dir() { 78 80 Some(home) => { 79 81 let working_dir = home.join(store_dir); 82 + if DEBUG && !working_dir.exists() { 83 + println!("DEBUG: Creating safir store at debug location"); 84 + } 80 85 fs::create_dir_all(&working_dir).expect("unable to create main directory"); 81 86 82 87 working_dir