SQLite-backed Key / Value Store
1
fork

Configure Feed

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

Merge pull request #14 from Tyrannican/rewrite

Rewrite

authored by

Graham Keenan and committed by
GitHub
6b7029dc b154f8cc

+298 -71
+5 -5
Cargo.lock
··· 693 693 694 694 [[package]] 695 695 name = "safir" 696 - version = "0.8.1" 696 + version = "0.9.0" 697 697 dependencies = [ 698 698 "anyhow", 699 699 "clap", 700 - "safir-core", 701 - "tokio", 700 + "dirs", 701 + "serde_json", 702 702 ] 703 703 704 704 [[package]] ··· 756 756 757 757 [[package]] 758 758 name = "serde_json" 759 - version = "1.0.105" 759 + version = "1.0.108" 760 760 source = "registry+https://github.com/rust-lang/crates.io-index" 761 - checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" 761 + checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 762 762 dependencies = [ 763 763 "itoa", 764 764 "ryu",
+5
README.md
··· 1 1 # Safir 2 2 3 3 Repo containing the source for `safir` and `safir-mem`. 4 + 5 + :warning: `safir-mem` and `safir-core` are NO LONGER MAINTAINED :warning: 6 + 7 + They will stay part of the repo for backward usage but the `safir` project has been reworked and brought back to its roots. 8 + Everything now lives in the `safir` project.
+7
safir-core/README.md
··· 1 1 # Safir Core 2 2 3 + ## Notice 4 + 5 + :warning: This is now archived and no longer updated :warning: 6 + 7 + It was ambitious (and worked kinda) but became far too unweildy and complicated when it didn't have to. 8 + Maybe a rewrite in the future but for now, it's now archived. 9 + 3 10 Internal library used by `safir` and `safir-mem` found [here](https://github.com/Tyrannican/safir) 4 11 5 12 Not intended for public use unless you want to contribute!
+7
safir-mem/README.md
··· 1 1 # Safir-mem 2 2 3 + ## Notice 4 + 5 + :warning: This is now archived and no longer updated :warning: 6 + 7 + It was ambitious (and worked kinda) but became far too unweildy and complicated when it didn't have to. 8 + Maybe a rewrite in the future but for now, it's now archived. 9 + 3 10 Simple in-memory CLI key/value store. 4 11 5 12 The in-memory version of [Safir](https://crates.io/crates/safir)!
+25
safir/CHANGELOG.md
··· 2 2 3 3 Documenting changes between versions beginning from v0.3.0 4 4 5 + ## v0.9.0 6 + 7 + **THIS IS A BREAKING CHANGE** 8 + 9 + The entire `safir` project has been overhauled and brought back to it's original (and simple) purpose: display Key-value pairs. 10 + 11 + When working on the whole project, it became clear to me that it was growing _way_ out of control. 12 + Initially it was to be an in-memory version (similar to Redis) but an on-disk storage solution was developed first. 13 + The in-memory version spawned a whole load of side-projects (`rubin` for one) but in doing so, it became an over-engineered beast. 14 + 15 + At it's heart, `safir` was simply meant to be a small program to store key-value pairs and retrieve them later - that's it! 16 + So when I rewrote it in Go for fun, I realised that it was far too complex and could just be a simple command-line tool with simple commands. 17 + 18 + So here we are, it's back to it's original "mindset" and is now _far_ simpler, just storing KV pairs on disk. 19 + No additional libraries, no in-memory versions; just a simple KV store. 20 + 21 + The other versions are still available but are no longer maintained and this will be the "final" form going forward. 22 + 23 + Sorry for any inconvenience and hope you enjoy the more simpler version! 24 + 25 + * Overhauled project to be simpler with no crazy, custom-built backend solutions 26 + * No pretty output, just displays the KV in the format `[key]="[value]"` 27 + * No configs, just a JSON file stored at `$HOME/.safirstore/safirstore.json` 28 + * All previous commands still work as they did before (minus the additional ones for in-memory storage) 29 + 5 30 ## v0.8.0 6 31 7 32 Headless mode!
+3 -5
safir/Cargo.toml
··· 1 1 [package] 2 2 name = "safir" 3 - version = "0.8.1" 3 + version = "0.9.0" 4 4 edition = "2021" 5 5 authors = ["Graham Keenan graham.keenan@outlook.com"] 6 6 license = "MIT OR Apache-2.0" ··· 11 11 keywords = ["cli", "terminal", "utility", "key-value", "store"] 12 12 categories = ["command-line-utilities"] 13 13 14 - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 - 16 14 [dependencies] 17 15 clap = { version = "4.2.5" , features = ["derive"] } 18 - tokio = { version = "1.28.2", features = ["full"] } 19 - safir-core = { version = "0.2.2", path = "../safir-core" } 20 16 anyhow = "1.0.75" 17 + dirs = "5.0.1" 18 + serde_json = "1.0.108"
+69 -9
safir/README.md
··· 28 28 Usage: safir <COMMAND> 29 29 30 30 Commands: 31 - add Add a value to the store with the given key 32 - get Get a value from the store 33 - rm Remove values from the store 34 - alias Output the alias command for a key / value pair to be entered into a shell session 35 - export Output the export command for a key / value pair to be entered into a shell session 36 - clear Clear all keys/values from the store 37 - purge Purges the .safirstore directory, removing it and its contents 38 - headless Set the headless mode 39 - help Print this message or the help of the given subcommand(s) 31 + add Add a value to the store with the given key 32 + get Get values from the store 33 + rm Remove values from the store 34 + alias Output the alias command for key / value pairs 35 + export Output the export command for a key / value pairs 36 + list List all values in the store 37 + clear Clear all keys/values from the store 38 + purge Purges the .safirstore directory, removing it and its contents 39 + help Print this message or the help of the given subcommand(s) 40 40 41 41 Options: 42 42 -h, --help Print help 43 43 -V, --version Print version 44 44 ``` 45 45 46 + ## Examples 47 + 48 + Adding a key and value to the store: 49 + 50 + ```bash 51 + safir add api_key "api_key_value" 52 + ``` 53 + 54 + Retrieving a value from the store: 55 + 56 + ```bash 57 + safir get api_key 58 + # api_key="api_key_value" 59 + ``` 60 + 61 + Removing a value from the store: 62 + 63 + ```bash 64 + safir rm api_key 65 + ``` 66 + 67 + List all values in the store: 68 + 69 + ```bash 70 + safir list 71 + 72 + # api_key="api_key_value" 73 + # another_api_key="another_value" 74 + ``` 75 + 76 + Exporting a value: 77 + 78 + ```bash 79 + safir export api_key 80 + # export api_key="api_key_value" 81 + 82 + $(safir export api_key) # <-- Will export the value to the current shell 83 + ``` 84 + 85 + Aliasing a value: 86 + 87 + ```bash 88 + safir alias long_command 89 + # alias long_command="cd build/ && make && sudo make install" 90 + 91 + $(safir alias long_command) # <-- Will alias the command in the current shell 92 + ``` 93 + 94 + Clear the store: 95 + 96 + ```bash 97 + safir clear 98 + # Will remove all contents in the store 99 + ``` 100 + 101 + Purge the store (remove EVERYTHING `safir` related) 102 + 103 + ```bash 104 + safir purge # Will remove the .safirstore directory 105 + ```
+10 -20
safir/src/cli.rs
··· 18 18 /// Add a value to the store with the given key 19 19 Add(AddArgs), 20 20 21 - /// Get a value from the store 21 + /// Get values from the store 22 22 Get(GetArgs), 23 23 24 24 /// Remove values from the store 25 25 Rm(RemoveArgs), 26 26 27 - /// Output the alias command for a key / value pair to be entered into a shell session 27 + /// Output the alias command for key / value pairs 28 28 Alias(SetArgs), 29 29 30 - /// Output the export command for a key / value pair to be entered into a shell session 30 + /// Output the export command for a key / value pairs 31 31 Export(SetArgs), 32 32 33 + /// List all values in the store 34 + List, 35 + 33 36 /// Clear all keys/values from the store 34 37 Clear, 35 38 36 39 /// Purges the .safirstore directory, removing it and its contents 37 40 Purge, 38 - 39 - /// Set the headless mode 40 - #[clap(subcommand)] 41 - Headless(HeadlessFlags), 42 41 } 43 42 44 43 /// Arguments for adding a value to the store with a given key ··· 51 50 pub value: String, 52 51 } 53 52 54 - /// Arguments for retrieving a value from the store with a given key 53 + /// Arguments for retrieving values from the store with the given keys 55 54 #[derive(Args, Debug)] 56 55 pub struct GetArgs { 57 - /// Name of the value to retrieve from the store 56 + /// Keys to retrieve the values for 58 57 /// 59 58 /// Returns nothing if the key does not exist 60 - pub key: Option<String>, 59 + pub keys: Vec<String>, 61 60 } 62 61 63 62 /// Arguments for removing values from the store with given keys ··· 66 65 /// Name of the keys to remove from the store 67 66 /// 68 67 /// Does nothing if the keys do not exist 69 - pub key: Vec<String>, 68 + pub keys: Vec<String>, 70 69 } 71 70 72 71 /// Arguments for outputting commands with a given prefix ··· 75 74 /// Name of the keys to display (e.g. alias / export) 76 75 pub keys: Vec<String>, 77 76 } 78 - 79 - #[derive(Subcommand, Debug)] 80 - pub enum HeadlessFlags { 81 - /// Set headless mode ON 82 - On, 83 - 84 - /// Set headless mode OFF 85 - Off, 86 - }
+18 -32
safir/src/main.rs
··· 1 1 mod cli; 2 + mod utils; 3 + mod store; 4 + 5 + use cli::*; 6 + use store::Store; 2 7 3 8 use anyhow::Result; 4 - use cli::*; 5 9 6 - use safir_core::{Safir, SafirEngineType}; 7 10 8 - #[tokio::main] 9 - async fn main() -> Result<()> { 11 + fn main() -> Result<()> { 10 12 let cli = Cli::parse(); 11 - let mut safir = Safir::new(SafirEngineType::Store).await?; 13 + let mut safir = Store::init_safir(); 12 14 13 15 match &cli.command { 14 16 Commands::Add(args) => { 15 - safir 16 - .add_entry(args.key.clone(), args.value.clone()) 17 - .await?; 17 + safir.add(args.key.to_owned(), args.value.to_owned()); 18 18 } 19 19 Commands::Get(args) => { 20 - if let Some(key) = &args.key { 21 - safir.get_entry(key.clone()).await?; 22 - } else { 23 - let inner = safir.as_safir_store(); 24 - inner.display_all(); 25 - } 20 + safir.get(args.keys.to_owned()); 26 21 } 27 22 Commands::Rm(args) => { 28 - safir.remove_entry(args.key.clone()).await?; 23 + safir.remove(args.keys.to_owned()); 29 24 } 30 25 Commands::Alias(args) => { 31 - safir.set_commands("alias", &args.keys).await; 26 + safir.custom_display("alias", args.keys.to_owned()); 32 27 } 33 28 Commands::Export(args) => { 34 - safir.set_commands("export", &args.keys).await; 29 + safir.custom_display("export", args.keys.to_owned()); 30 + } 31 + Commands::List => { 32 + safir.list(); 35 33 } 36 34 Commands::Clear => { 37 - safir.clear_entries().await?; 35 + safir.clear(); 38 36 } 39 37 Commands::Purge => { 40 - let inner = safir.as_safir_store(); 41 - inner.purge(); 38 + safir.purge(); 42 39 } 43 - Commands::Headless(mode) => match mode { 44 - HeadlessFlags::On => { 45 - safir.config.headless_mode = Some(true); 46 - safir.config.write().await?; 47 - println!("Headless mode is ON"); 48 - } 49 - HeadlessFlags::Off => { 50 - safir.config.headless_mode = Some(false); 51 - safir.config.write().await?; 52 - println!("Headless mode is OFF"); 53 - } 54 - }, 55 40 } 56 41 42 + utils::write_store(&safir.store, &safir.file); 57 43 Ok(()) 58 44 }
+98
safir/src/store.rs
··· 1 + use crate::utils; 2 + use std::{fs, path::PathBuf, collections::HashMap}; 3 + 4 + pub struct Store { 5 + pub path: PathBuf, 6 + pub file: PathBuf, 7 + pub store: HashMap<String, String> 8 + } 9 + 10 + impl Store { 11 + pub fn init_safir() -> Self { 12 + match dirs::home_dir() { 13 + Some(home) => { 14 + let working_dir = home.join(".safirstore"); 15 + fs::create_dir_all(&working_dir) 16 + .expect("unable to create main directory"); 17 + 18 + let store_path = working_dir.join("safirstore.json"); 19 + let store = if store_path.exists() { 20 + utils::load_store(&store_path) 21 + } else { 22 + let store = HashMap::new(); 23 + utils::write_store(&store, &store_path); 24 + store 25 + }; 26 + 27 + return Self { 28 + path: working_dir, 29 + file: store_path, 30 + store, 31 + }; 32 + } 33 + None => { 34 + eprintln!("unable to obtain home directory path!"); 35 + std::process::exit(-1); 36 + } 37 + } 38 + } 39 + 40 + pub fn add(&mut self, key: String, value: String) { 41 + if let Some(v) = self.store.get(&key) { 42 + let confirm_msg = format!("Key {key} already exists ({v}), Replace?"); 43 + if utils::confirm_entry(&confirm_msg) { 44 + self.store.insert(key, value); 45 + } 46 + } else { 47 + self.store.insert(key, value); 48 + } 49 + } 50 + 51 + pub fn get(&self, keys: Vec<String>) { 52 + for key in keys.iter() { 53 + if let Some(value) = self.store.get(key) { 54 + utils::display_kv(key, value); 55 + } 56 + } 57 + } 58 + 59 + pub fn list(&self) { 60 + for (key, value) in self.store.iter() { 61 + utils::display_kv(key, value); 62 + } 63 + } 64 + 65 + pub fn remove(&mut self, keys: Vec<String>) { 66 + for key in keys.iter() { 67 + if let Some(v) = self.store.get(key) { 68 + let confirm_msg = format!("Remove {key} ({v}) from the store?"); 69 + if utils::confirm_entry(&confirm_msg) { 70 + self.store.remove(key); 71 + } 72 + } 73 + } 74 + } 75 + 76 + pub fn custom_display(&self, display_cmd: &str, keys: Vec<String>) { 77 + for key in keys.iter() { 78 + if let Some(value) = self.store.get(key) { 79 + println!("{display_cmd} {key}=\"{value}\""); 80 + } 81 + } 82 + } 83 + 84 + pub fn clear(&mut self) { 85 + let confirm_msg = "Are you sure you want to clear the cache of all contents?"; 86 + if utils::confirm_entry(&confirm_msg) { 87 + self.store.clear(); 88 + } 89 + } 90 + 91 + pub fn purge(&mut self) { 92 + let confirm_msg = "Are you sure you want to remove the .safirstore directory and ALL contents?"; 93 + if utils::confirm_entry(&confirm_msg) { 94 + utils::purge_directory(self.path.clone()); 95 + std::process::exit(0); 96 + } 97 + } 98 + }
+51
safir/src/utils.rs
··· 1 + use std::{io::Write, collections::HashMap, path::Path}; 2 + 3 + /// Confirmation dialog for important calls 4 + pub fn confirm_entry(msg: &str) -> bool { 5 + let mut answer = String::new(); 6 + print!("{} (y/n) ", msg); 7 + std::io::stdout().flush().expect("failed to flush buffer"); 8 + 9 + let _ = std::io::stdin() 10 + .read_line(&mut answer) 11 + .expect("unable to get input from user"); 12 + 13 + let answer = answer.trim().to_lowercase(); 14 + if answer == "y" || answer == "yes" { 15 + return true; 16 + } 17 + 18 + false 19 + } 20 + 21 + /// Outputs the Key-Value pair 22 + pub fn display_kv(key: &str, value: &str) { 23 + println!("{key}=\"{value}\"") 24 + } 25 + 26 + /// Loads the store from disk 27 + pub fn load_store(path: impl AsRef<Path>) -> HashMap<String, String> { 28 + let contents = std::fs::read_to_string(path.as_ref()) 29 + .expect("unable to store contents"); 30 + 31 + return serde_json::from_str::<HashMap<String, String>>(&contents) 32 + .expect("unable to deserialize store contents"); 33 + } 34 + 35 + /// Writes the store to disk 36 + pub fn write_store(store: &HashMap<String, String>, path: impl AsRef<Path>) { 37 + let str_store = serde_json::to_string_pretty(store) 38 + .expect("unable to serialize store contents"); 39 + 40 + let mut file = std::fs::File::create(&path) 41 + .expect("unable to get file handle"); 42 + 43 + file.write_all(str_store.as_bytes()) 44 + .expect("unable to write store out to disk"); 45 + } 46 + 47 + /// Remove the .safirstore directory 48 + pub fn purge_directory(path: impl AsRef<Path>) { 49 + std::fs::remove_dir_all(path) 50 + .expect("unable to remove safirstore directory"); 51 + }