SQLite-backed Key / Value Store
1
fork

Configure Feed

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

feat!: removed File store backend

Removed the file storage backend as this is going to be DB-only going
forward

+9 -237
+2 -2
Cargo.toml
··· 4 4 edition = "2021" 5 5 authors = ["Graham Keenan <graham.keenan@outlook.com>"] 6 6 license = "MIT OR Apache-2.0" 7 - description = "Key/Value store to share values between different shell sessions" 7 + description = "CLI Key/Value store backed by an SQLite3 DB" 8 8 readme = "README.md" 9 9 homepage = "https://github.com/Tyrannican/safir" 10 10 repository = "https://github.com/Tyrannican/safir" 11 - keywords = ["cli", "terminal", "utility", "key-value", "store"] 11 + keywords = ["cli", "terminal", "utility", "key-value", "store", "database"] 12 12 categories = ["command-line-utilities"] 13 13 14 14 [dependencies]
-7
src/cli.rs
··· 1 1 //! CLI for using the Safir binary 2 - use crate::store::config::SafirMode; 3 2 pub use clap::{Parser, Subcommand}; 4 3 5 4 /// CLI arguments for running the program ··· 47 46 Export { 48 47 /// Keys to export the values 49 48 keys: Vec<String>, 50 - }, 51 - 52 - /// Sets the mode for Safir (active on the next run of Safir) 53 - Mode { 54 - /// Mode to set (KV-file store or SQLite DB store) 55 - mode: SafirMode, 56 49 }, 57 50 58 51 /// List all values in the store
-9
src/main.rs
··· 32 32 } 33 33 Commands::Clear => safir.clear().await?, 34 34 Commands::Purge => safir.purge().await?, 35 - Commands::Mode { mode } => { 36 - let mut cfg = safir.get_config(); 37 - cfg.mode = mode; 38 - cfg.write().context("writing config out")?; 39 - println!( 40 - "Set store mode to: '{:?}'\nActive on the next run of Safir!", 41 - cfg.mode 42 - ); 43 - } 44 35 Commands::Use { environment } => { 45 36 let mut cfg = safir.get_config(); 46 37 cfg.environment = environment.clone();
-12
src/store/config.rs
··· 1 1 use anyhow::{Context, Result}; 2 - use clap::ValueEnum; 3 2 use serde::{Deserialize, Serialize}; 4 3 5 4 use std::path::{Path, PathBuf}; 6 5 7 - #[derive(ValueEnum, Default, Debug, Copy, PartialEq, Eq, Clone, Serialize, Deserialize)] 8 - #[serde(rename_all = "lowercase")] 9 - pub enum SafirMode { 10 - #[default] 11 - File, 12 - Database, 13 - } 14 - 15 6 #[derive(Default, Debug, Clone, Serialize, Deserialize)] 16 7 pub struct SafirConfig { 17 8 #[serde(skip)] 18 9 pub filepath: PathBuf, 19 10 20 11 pub environment: String, 21 - 22 - pub mode: SafirMode, 23 12 } 24 13 25 14 impl SafirConfig { ··· 29 18 let cfg = Self { 30 19 filepath: fp, 31 20 environment: "default".to_string(), 32 - mode: SafirMode::File, 33 21 }; 34 22 cfg.write().context("writing config out")?; 35 23
+3 -3
src/store/db_store.rs
··· 127 127 let confirm_msg = 128 128 "Are you sure you want to purge the safirstore?\nThis will delete the folder and any data inside!"; 129 129 130 - if confirm_entry(&confirm_msg) { 130 + if confirm_entry(confirm_msg) { 131 131 purge_directory(ws); 132 132 } 133 133 Ok(()) 134 134 } 135 135 136 136 async fn environments(&self) -> Result<Vec<String>> { 137 - let query = format!("select distinct environment from safir"); 138 - let result: Vec<String> = sqlx::query_scalar(&query).fetch_all(&self.pool).await?; 137 + let query = "select distinct environment from safir"; 138 + let result: Vec<String> = sqlx::query_scalar(query).fetch_all(&self.pool).await?; 139 139 Ok(result) 140 140 } 141 141
-197
src/store/file_store.rs
··· 1 - use crate::{ 2 - store::{config::SafirConfig, SafirStore}, 3 - utils::{self, KVPair}, 4 - }; 5 - 6 - use anyhow::Result; 7 - use async_trait::async_trait; 8 - use serde_json::Value; 9 - 10 - use std::{ 11 - collections::HashMap, 12 - path::{Path, PathBuf}, 13 - }; 14 - 15 - #[derive(Debug, Clone)] 16 - pub struct KVStore { 17 - loc: PathBuf, 18 - environment: String, 19 - store: HashMap<String, HashMap<String, String>>, 20 - config: SafirConfig, 21 - } 22 - 23 - impl KVStore { 24 - pub fn load(ws: PathBuf, config: SafirConfig) -> Self { 25 - let store_path = ws.join("safirstore.json"); 26 - let safir = if store_path.exists() { 27 - let store = Self::load_store(&store_path, &config); 28 - Self { 29 - loc: store_path, 30 - environment: config.environment.clone(), 31 - config, 32 - store, 33 - } 34 - } else { 35 - let mut store = HashMap::new(); 36 - store.insert(config.environment.clone(), HashMap::new()); 37 - 38 - Self { 39 - loc: store_path, 40 - store, 41 - environment: config.environment.clone(), 42 - config, 43 - } 44 - }; 45 - 46 - safir.write_store(); 47 - safir 48 - } 49 - 50 - /// Loads the store from disk 51 - /// This is stupid having to reload the map but it will allow users that had 52 - /// the old format to port over to the new format seamlessly 53 - pub fn load_store( 54 - path: impl AsRef<Path>, 55 - config: &SafirConfig, 56 - ) -> HashMap<String, HashMap<String, String>> { 57 - let contents = std::fs::read_to_string(path.as_ref()).expect("unable to store contents"); 58 - 59 - let store: HashMap<String, Value> = 60 - serde_json::from_str(&contents).expect("unable to deserialize contents"); 61 - 62 - // If they're all objects then this is the new format, load appropriately 63 - if store.values().all(|v| v.is_object()) { 64 - return serde_json::from_str::<HashMap<String, HashMap<String, String>>>(&contents) 65 - .unwrap(); 66 - } 67 - 68 - // Old map (no environment) - Load and create new environment 69 - let old_map = serde_json::from_str::<HashMap<String, String>>(&contents).unwrap(); 70 - let mut new_map = HashMap::new(); 71 - new_map.insert(config.environment.clone(), HashMap::new()); 72 - 73 - let env = new_map.get_mut(config.environment.as_str()).unwrap(); 74 - for (key, value) in old_map.into_iter() { 75 - env.insert(key, value); 76 - } 77 - 78 - new_map 79 - } 80 - 81 - /// Writes the store to disk 82 - pub fn write_store(&self) { 83 - use std::io::Write; 84 - let str_store = 85 - serde_json::to_string_pretty(&self.store).expect("unable to serialize store contents"); 86 - 87 - let mut file = std::fs::File::create(&self.loc).expect("unable to get file handle"); 88 - 89 - file.write_all(str_store.as_bytes()) 90 - .expect("unable to write store out to disk"); 91 - } 92 - 93 - pub fn get_environment(&mut self) -> &mut HashMap<String, String> { 94 - self.store 95 - .entry(self.environment.clone()) 96 - .or_insert(HashMap::new()); 97 - 98 - self.store.get_mut(&self.environment).unwrap() 99 - } 100 - } 101 - 102 - #[async_trait] 103 - impl SafirStore for KVStore { 104 - async fn add(&mut self, key: String, value: String) -> Result<()> { 105 - let env = self.get_environment(); 106 - if let Some(v) = env.get(&key) { 107 - let confirm_msg = format!("Key {key} already exists ({v}), Replace?"); 108 - if utils::confirm_entry(&confirm_msg) { 109 - env.insert(key, value); 110 - } 111 - } else { 112 - env.insert(key, value); 113 - } 114 - 115 - self.write_store(); 116 - 117 - Ok(()) 118 - } 119 - 120 - async fn get(&self, keys: Vec<String>) -> Result<Vec<KVPair>> { 121 - let inner = match self.store.get(&self.environment) { 122 - Some(inner) => inner, 123 - None => return Ok(vec![]), 124 - }; 125 - 126 - let kvs: Vec<KVPair> = keys 127 - .into_iter() 128 - .filter_map(|key| match inner.get(&key) { 129 - Some(value) => Some((key, value.clone())), 130 - None => None, 131 - }) 132 - .collect(); 133 - 134 - Ok(kvs) 135 - } 136 - 137 - async fn list(&self) -> Result<Vec<KVPair>> { 138 - let inner = match self.store.get(&self.environment) { 139 - Some(inner) => inner, 140 - None => return Ok(vec![]), 141 - }; 142 - 143 - let kvs: Vec<KVPair> = inner 144 - .iter() 145 - .map(|(key, value)| (key.clone(), value.clone())) 146 - .collect(); 147 - 148 - Ok(kvs) 149 - } 150 - 151 - async fn remove(&mut self, keys: Vec<String>) -> Result<()> { 152 - let inner = self.get_environment(); 153 - for key in keys.iter() { 154 - if let Some(v) = inner.get(key) { 155 - let confirm_msg = format!("Remove {key} ({v}) from the store?"); 156 - if utils::confirm_entry(&confirm_msg) { 157 - inner.remove(key); 158 - } 159 - } 160 - } 161 - 162 - self.write_store(); 163 - 164 - Ok(()) 165 - } 166 - async fn clear(&mut self) -> Result<()> { 167 - let inner = self.get_environment(); 168 - let confirm_msg = "Are you sure you want to clear the cache of all contents?"; 169 - if utils::confirm_entry(&confirm_msg) { 170 - inner.clear(); 171 - } 172 - 173 - self.write_store(); 174 - 175 - Ok(()) 176 - } 177 - 178 - async fn purge(&mut self) -> Result<()> { 179 - let confirm_msg = 180 - "Are you sure you want to remove the .safirstore directory and ALL contents?"; 181 - let ws = utils::load_safir_workspace(); 182 - if utils::confirm_entry(&confirm_msg) { 183 - utils::purge_directory(ws); 184 - std::process::exit(0); 185 - } 186 - 187 - Ok(()) 188 - } 189 - 190 - async fn environments(&self) -> Result<Vec<String>> { 191 - Ok(self.store.keys().map(|e| e.to_string()).collect()) 192 - } 193 - 194 - fn get_config(&self) -> SafirConfig { 195 - self.config.clone() 196 - } 197 - }
+4 -7
src/store/mod.rs
··· 1 1 pub mod config; 2 2 pub mod db_store; 3 - pub mod file_store; 4 3 5 4 use crate::utils::{load_safir_workspace, KVPair}; 6 - use config::{SafirConfig, SafirMode}; 5 + use config::SafirConfig; 7 6 8 7 use anyhow::Result; 9 8 use async_trait::async_trait; 10 9 use db_store::SqliteStore; 11 - use file_store::KVStore; 12 10 11 + // Trait to be used by any storage backend 12 + // SQLite3 for now 13 13 #[async_trait] 14 14 pub trait SafirStore { 15 15 async fn add(&mut self, key: String, value: String) -> Result<()>; ··· 26 26 let ws = load_safir_workspace(); 27 27 let cfg = SafirConfig::load(&ws).expect("can't load safir config"); 28 28 29 - match cfg.mode { 30 - SafirMode::File => Ok(Box::new(KVStore::load(ws, cfg))), 31 - SafirMode::Database => Ok(Box::new(SqliteStore::load(ws, cfg).await?)), 32 - } 29 + Ok(Box::new(SqliteStore::load(ws, cfg).await?)) 33 30 }