···44edition = "2021"
55authors = ["Graham Keenan <graham.keenan@outlook.com>"]
66license = "MIT OR Apache-2.0"
77-description = "Key/Value store to share values between different shell sessions"
77+description = "CLI Key/Value store backed by an SQLite3 DB"
88readme = "README.md"
99homepage = "https://github.com/Tyrannican/safir"
1010repository = "https://github.com/Tyrannican/safir"
1111-keywords = ["cli", "terminal", "utility", "key-value", "store"]
1111+keywords = ["cli", "terminal", "utility", "key-value", "store", "database"]
1212categories = ["command-line-utilities"]
13131414[dependencies]
-7
src/cli.rs
···11//! CLI for using the Safir binary
22-use crate::store::config::SafirMode;
32pub use clap::{Parser, Subcommand};
4354/// CLI arguments for running the program
···4746 Export {
4847 /// Keys to export the values
4948 keys: Vec<String>,
5050- },
5151-5252- /// Sets the mode for Safir (active on the next run of Safir)
5353- Mode {
5454- /// Mode to set (KV-file store or SQLite DB store)
5555- mode: SafirMode,
5649 },
57505851 /// List all values in the store
-9
src/main.rs
···3232 }
3333 Commands::Clear => safir.clear().await?,
3434 Commands::Purge => safir.purge().await?,
3535- Commands::Mode { mode } => {
3636- let mut cfg = safir.get_config();
3737- cfg.mode = mode;
3838- cfg.write().context("writing config out")?;
3939- println!(
4040- "Set store mode to: '{:?}'\nActive on the next run of Safir!",
4141- cfg.mode
4242- );
4343- }
4435 Commands::Use { environment } => {
4536 let mut cfg = safir.get_config();
4637 cfg.environment = environment.clone();
···127127 let confirm_msg =
128128 "Are you sure you want to purge the safirstore?\nThis will delete the folder and any data inside!";
129129130130- if confirm_entry(&confirm_msg) {
130130+ if confirm_entry(confirm_msg) {
131131 purge_directory(ws);
132132 }
133133 Ok(())
134134 }
135135136136 async fn environments(&self) -> Result<Vec<String>> {
137137- let query = format!("select distinct environment from safir");
138138- let result: Vec<String> = sqlx::query_scalar(&query).fetch_all(&self.pool).await?;
137137+ let query = "select distinct environment from safir";
138138+ let result: Vec<String> = sqlx::query_scalar(query).fetch_all(&self.pool).await?;
139139 Ok(result)
140140 }
141141
-197
src/store/file_store.rs
···11-use crate::{
22- store::{config::SafirConfig, SafirStore},
33- utils::{self, KVPair},
44-};
55-66-use anyhow::Result;
77-use async_trait::async_trait;
88-use serde_json::Value;
99-1010-use std::{
1111- collections::HashMap,
1212- path::{Path, PathBuf},
1313-};
1414-1515-#[derive(Debug, Clone)]
1616-pub struct KVStore {
1717- loc: PathBuf,
1818- environment: String,
1919- store: HashMap<String, HashMap<String, String>>,
2020- config: SafirConfig,
2121-}
2222-2323-impl KVStore {
2424- pub fn load(ws: PathBuf, config: SafirConfig) -> Self {
2525- let store_path = ws.join("safirstore.json");
2626- let safir = if store_path.exists() {
2727- let store = Self::load_store(&store_path, &config);
2828- Self {
2929- loc: store_path,
3030- environment: config.environment.clone(),
3131- config,
3232- store,
3333- }
3434- } else {
3535- let mut store = HashMap::new();
3636- store.insert(config.environment.clone(), HashMap::new());
3737-3838- Self {
3939- loc: store_path,
4040- store,
4141- environment: config.environment.clone(),
4242- config,
4343- }
4444- };
4545-4646- safir.write_store();
4747- safir
4848- }
4949-5050- /// Loads the store from disk
5151- /// This is stupid having to reload the map but it will allow users that had
5252- /// the old format to port over to the new format seamlessly
5353- pub fn load_store(
5454- path: impl AsRef<Path>,
5555- config: &SafirConfig,
5656- ) -> HashMap<String, HashMap<String, String>> {
5757- let contents = std::fs::read_to_string(path.as_ref()).expect("unable to store contents");
5858-5959- let store: HashMap<String, Value> =
6060- serde_json::from_str(&contents).expect("unable to deserialize contents");
6161-6262- // If they're all objects then this is the new format, load appropriately
6363- if store.values().all(|v| v.is_object()) {
6464- return serde_json::from_str::<HashMap<String, HashMap<String, String>>>(&contents)
6565- .unwrap();
6666- }
6767-6868- // Old map (no environment) - Load and create new environment
6969- let old_map = serde_json::from_str::<HashMap<String, String>>(&contents).unwrap();
7070- let mut new_map = HashMap::new();
7171- new_map.insert(config.environment.clone(), HashMap::new());
7272-7373- let env = new_map.get_mut(config.environment.as_str()).unwrap();
7474- for (key, value) in old_map.into_iter() {
7575- env.insert(key, value);
7676- }
7777-7878- new_map
7979- }
8080-8181- /// Writes the store to disk
8282- pub fn write_store(&self) {
8383- use std::io::Write;
8484- let str_store =
8585- serde_json::to_string_pretty(&self.store).expect("unable to serialize store contents");
8686-8787- let mut file = std::fs::File::create(&self.loc).expect("unable to get file handle");
8888-8989- file.write_all(str_store.as_bytes())
9090- .expect("unable to write store out to disk");
9191- }
9292-9393- pub fn get_environment(&mut self) -> &mut HashMap<String, String> {
9494- self.store
9595- .entry(self.environment.clone())
9696- .or_insert(HashMap::new());
9797-9898- self.store.get_mut(&self.environment).unwrap()
9999- }
100100-}
101101-102102-#[async_trait]
103103-impl SafirStore for KVStore {
104104- async fn add(&mut self, key: String, value: String) -> Result<()> {
105105- let env = self.get_environment();
106106- if let Some(v) = env.get(&key) {
107107- let confirm_msg = format!("Key {key} already exists ({v}), Replace?");
108108- if utils::confirm_entry(&confirm_msg) {
109109- env.insert(key, value);
110110- }
111111- } else {
112112- env.insert(key, value);
113113- }
114114-115115- self.write_store();
116116-117117- Ok(())
118118- }
119119-120120- async fn get(&self, keys: Vec<String>) -> Result<Vec<KVPair>> {
121121- let inner = match self.store.get(&self.environment) {
122122- Some(inner) => inner,
123123- None => return Ok(vec![]),
124124- };
125125-126126- let kvs: Vec<KVPair> = keys
127127- .into_iter()
128128- .filter_map(|key| match inner.get(&key) {
129129- Some(value) => Some((key, value.clone())),
130130- None => None,
131131- })
132132- .collect();
133133-134134- Ok(kvs)
135135- }
136136-137137- async fn list(&self) -> Result<Vec<KVPair>> {
138138- let inner = match self.store.get(&self.environment) {
139139- Some(inner) => inner,
140140- None => return Ok(vec![]),
141141- };
142142-143143- let kvs: Vec<KVPair> = inner
144144- .iter()
145145- .map(|(key, value)| (key.clone(), value.clone()))
146146- .collect();
147147-148148- Ok(kvs)
149149- }
150150-151151- async fn remove(&mut self, keys: Vec<String>) -> Result<()> {
152152- let inner = self.get_environment();
153153- for key in keys.iter() {
154154- if let Some(v) = inner.get(key) {
155155- let confirm_msg = format!("Remove {key} ({v}) from the store?");
156156- if utils::confirm_entry(&confirm_msg) {
157157- inner.remove(key);
158158- }
159159- }
160160- }
161161-162162- self.write_store();
163163-164164- Ok(())
165165- }
166166- async fn clear(&mut self) -> Result<()> {
167167- let inner = self.get_environment();
168168- let confirm_msg = "Are you sure you want to clear the cache of all contents?";
169169- if utils::confirm_entry(&confirm_msg) {
170170- inner.clear();
171171- }
172172-173173- self.write_store();
174174-175175- Ok(())
176176- }
177177-178178- async fn purge(&mut self) -> Result<()> {
179179- let confirm_msg =
180180- "Are you sure you want to remove the .safirstore directory and ALL contents?";
181181- let ws = utils::load_safir_workspace();
182182- if utils::confirm_entry(&confirm_msg) {
183183- utils::purge_directory(ws);
184184- std::process::exit(0);
185185- }
186186-187187- Ok(())
188188- }
189189-190190- async fn environments(&self) -> Result<Vec<String>> {
191191- Ok(self.store.keys().map(|e| e.to_string()).collect())
192192- }
193193-194194- fn get_config(&self) -> SafirConfig {
195195- self.config.clone()
196196- }
197197-}
+4-7
src/store/mod.rs
···11pub mod config;
22pub mod db_store;
33-pub mod file_store;
4354use crate::utils::{load_safir_workspace, KVPair};
66-use config::{SafirConfig, SafirMode};
55+use config::SafirConfig;
7687use anyhow::Result;
98use async_trait::async_trait;
109use db_store::SqliteStore;
1111-use file_store::KVStore;
12101111+// Trait to be used by any storage backend
1212+// SQLite3 for now
1313#[async_trait]
1414pub trait SafirStore {
1515 async fn add(&mut self, key: String, value: String) -> Result<()>;
···2626 let ws = load_safir_workspace();
2727 let cfg = SafirConfig::load(&ws).expect("can't load safir config");
28282929- match cfg.mode {
3030- SafirMode::File => Ok(Box::new(KVStore::load(ws, cfg))),
3131- SafirMode::Database => Ok(Box::new(SqliteStore::load(ws, cfg).await?)),
3232- }
2929+ Ok(Box::new(SqliteStore::load(ws, cfg).await?))
3330}