SQLite-backed Key / Value Store
1
fork

Configure Feed

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

refactor: moved some content around

config now has it's own dedicated file and fixed up the migration
script

+102 -52
+3 -2
migrations/20240620173629_create_db.sql
··· 1 1 -- Add migration script here 2 2 create table if not exists safir ( 3 - key text not null primary key 3 + key text not null primary key, 4 4 value text not null 5 - ) 5 + ); 6 + 6 7 create index if not exists idx_key on safir(key);
+1 -1
src/cli.rs
··· 1 1 //! CLI for using the Safir binary 2 - use crate::store::SafirMode; 2 + use crate::store::config::SafirMode; 3 3 pub use clap::{Parser, Subcommand}; 4 4 5 5 /// CLI arguments for running the program
+3 -1
src/main.rs
··· 29 29 Commands::Clear => safir.clear().await?, 30 30 Commands::Purge => safir.purge().await?, 31 31 Commands::Mode { mode } => { 32 - println!("Mode: {mode:?}"); 32 + let mut cfg = safir.get_config(); 33 + cfg.mode = mode; 34 + cfg.write().context("writing config out")?; 33 35 } 34 36 } 35 37
+56
src/store/config.rs
··· 1 + use anyhow::{Context, Result}; 2 + use clap::ValueEnum; 3 + use serde::{Deserialize, Serialize}; 4 + 5 + use std::path::{Path, PathBuf}; 6 + 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 + #[derive(Default, Debug, Clone, Serialize, Deserialize)] 16 + pub struct SafirConfig { 17 + #[serde(skip)] 18 + pub filepath: PathBuf, 19 + 20 + pub mode: SafirMode, 21 + } 22 + 23 + impl SafirConfig { 24 + pub fn load(workdir: impl AsRef<Path>) -> Result<Self> { 25 + let fp = workdir.as_ref().join("safirstore.cfg"); 26 + if !fp.exists() { 27 + let cfg = Self { 28 + filepath: fp, 29 + mode: SafirMode::File, 30 + }; 31 + cfg.write().context("writing config out")?; 32 + 33 + return Ok(cfg); 34 + } 35 + let contents = std::fs::read_to_string(&fp).context("loading safir config")?; 36 + let mut cfg: SafirConfig = 37 + serde_json::from_str(&contents).context("deserializing safir config")?; 38 + 39 + cfg.filepath = fp; 40 + 41 + Ok(cfg) 42 + } 43 + 44 + pub fn write(&self) -> Result<()> { 45 + use std::io::Write; 46 + 47 + let contents = serde_json::to_string_pretty(&self).context("serializing config")?; 48 + let mut fd = 49 + std::fs::File::create(&self.filepath).context("creating config file descriptor")?; 50 + 51 + fd.write_all(contents.as_bytes()) 52 + .context("writing config contents")?; 53 + 54 + Ok(()) 55 + } 56 + }
+9 -4
src/store/db_store.rs
··· 5 5 use std::path::PathBuf; 6 6 use std::str::FromStr; 7 7 8 - use crate::store::SafirStore; 8 + use crate::store::{config::SafirConfig, SafirStore}; 9 9 10 10 #[derive(Debug, Clone)] 11 11 pub struct SqliteStore { 12 12 pool: SqlitePool, 13 + config: SafirConfig, 13 14 } 14 15 15 16 impl SqliteStore { 16 - pub(crate) async fn load(wd: PathBuf) -> Result<Self> { 17 + pub(crate) async fn load(ws: PathBuf, config: SafirConfig) -> Result<Self> { 17 18 let lead = PathBuf::from("sqlite:/"); 18 - let db_name = lead.join(wd).join("safirstore.db"); 19 + let db_name = lead.join(ws).join("safirstore.db"); 19 20 20 21 let connect_opts = SqliteConnectOptions::from_str(db_name.to_str().unwrap())? 21 22 .optimize_on_close(true, None) ··· 29 30 .context("creating database")?; 30 31 31 32 Self::setup_db(&pool).await?; 32 - Ok(Self { pool }) 33 + Ok(Self { pool, config }) 33 34 } 34 35 35 36 async fn setup_db(pool: &SqlitePool) -> Result<()> { ··· 61 62 } 62 63 async fn purge(&mut self) -> Result<()> { 63 64 Ok(()) 65 + } 66 + 67 + fn get_config(&self) -> SafirConfig { 68 + self.config.clone() 64 69 } 65 70 }
+15 -12
src/store/file_store.rs
··· 1 - use crate::{store::SafirStore, utils}; 1 + use crate::{ 2 + store::{config::SafirConfig, SafirStore}, 3 + utils, 4 + }; 5 + 2 6 use anyhow::Result; 3 7 use async_trait::async_trait; 8 + 4 9 use std::{collections::HashMap, path::PathBuf}; 5 10 6 11 #[derive(Debug, Clone)] 7 12 pub struct KVStore { 8 - pub path: PathBuf, 9 - pub store: HashMap<String, String>, 13 + path: PathBuf, 14 + store: HashMap<String, String>, 15 + config: SafirConfig, 10 16 } 11 17 12 18 impl KVStore { 13 - pub fn load(ws: PathBuf) -> Self { 19 + pub fn load(ws: PathBuf, config: SafirConfig) -> Self { 14 20 let store_path = ws.join("safirstore.json"); 15 21 let store = if store_path.exists() { 16 22 utils::load_store(&store_path) ··· 22 28 23 29 Self { 24 30 path: store_path, 31 + config, 25 32 store, 26 - } 27 - } 28 - 29 - pub fn custom_display(&self, display_cmd: &str, keys: Vec<String>) { 30 - for key in keys.iter() { 31 - if let Some(value) = self.store.get(key) { 32 - println!("{display_cmd} {key}=\"{value}\""); 33 - } 34 33 } 35 34 } 36 35 } ··· 98 97 } 99 98 100 99 Ok(()) 100 + } 101 + 102 + fn get_config(&self) -> SafirConfig { 103 + self.config.clone() 101 104 } 102 105 }
+7 -31
src/store/mod.rs
··· 1 + pub mod config; 1 2 pub mod db_store; 2 3 pub mod file_store; 3 4 4 5 use crate::utils; 5 - 6 - use std::path::Path; 6 + use config::{SafirConfig, SafirMode}; 7 7 8 - use anyhow::{Context, Result}; 8 + use anyhow::Result; 9 9 use async_trait::async_trait; 10 - use clap::ValueEnum; 11 10 use db_store::SqliteStore; 12 11 use file_store::KVStore; 13 - use serde::{Deserialize, Serialize}; 14 12 15 13 #[async_trait] 16 14 pub trait SafirStore { ··· 20 18 async fn remove(&mut self, keys: Vec<String>) -> Result<()>; 21 19 async fn clear(&mut self) -> Result<()>; 22 20 async fn purge(&mut self) -> Result<()>; 23 - } 24 - 25 - #[derive(ValueEnum, Default, Debug, Copy, PartialEq, Eq, Clone, Serialize, Deserialize)] 26 - #[serde(rename_all = "lowercase")] 27 - pub enum SafirMode { 28 - #[default] 29 - File, 30 - Database, 31 - } 32 - 33 - #[derive(Default, Debug, Clone, Serialize, Deserialize)] 34 - pub struct SafirConfig { 35 - mode: SafirMode, 36 - } 37 - 38 - impl SafirConfig { 39 - pub fn load(workdir: impl AsRef<Path>) -> Result<Self> { 40 - let fp = workdir.as_ref().join("safirstore.cfg"); 41 - if !fp.exists() { 42 - return Ok(Self::default()); 43 - } 44 - let contents = std::fs::read_to_string(&fp).context("loading safir config")?; 45 - serde_json::from_str(&contents).context("deserializing safir config") 46 - } 21 + fn get_config(&self) -> SafirConfig; 47 22 } 48 23 49 24 pub async fn init_safir() -> Result<Box<dyn SafirStore>> { 50 25 let ws = utils::create_safir_workspace(); 51 26 let cfg = SafirConfig::load(&ws).expect("can't load safir config"); 27 + 52 28 match cfg.mode { 53 - SafirMode::File => Ok(Box::new(KVStore::load(ws))), 54 - SafirMode::Database => Ok(Box::new(SqliteStore::load(ws).await?)), 29 + SafirMode::File => Ok(Box::new(KVStore::load(ws, cfg))), 30 + SafirMode::Database => Ok(Box::new(SqliteStore::load(ws, cfg).await?)), 55 31 } 56 32 }
+8 -1
src/utils.rs
··· 31 31 println!("{key}=\"{value}\"") 32 32 } 33 33 34 + /// 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()) { 37 + println!("{display_cmd} {key}=\"{value}\""); 38 + } 39 + } 40 + 34 41 /// Loads the store from disk 35 42 pub fn load_store(path: impl AsRef<Path>) -> HashMap<String, String> { 36 43 let contents = std::fs::read_to_string(path.as_ref()).expect("unable to store contents"); ··· 58 65 /// Create the .safirstore directory in the user HOME 59 66 pub fn create_safir_workspace() -> PathBuf { 60 67 let store_dir = if DEBUG { 61 - ".safirstore_debug" 68 + ".debug_safirstore" 62 69 } else { 63 70 ".safirstore" 64 71 };