···2233Documenting changes between versions beginning from v0.3.0
4455+## v0.5.0
66+77+Added the ability to operate Safir as a Memcache server (storing contents in-memory instead of on disk).
88+Requires the [Rubin CLI](https://crates.io/crates/rubin-cli) to be installed.
99+1010+The Memcache service operates using TCP sockets to communicate with the Rubin server to access and update storage.
1111+It operates on `localhost` or `127.0.0.1` and on port `9876`.
1212+1313+It works in the same fashion as Safir and does not update the store on disk when activated.
1414+1515+Initialisation and cleanup is handles with the `start` and `stop` commands.
1616+1717+* New memcache option available for using Safir as an in-memory store
1818+* Minor edits and bugfixes
1919+520## v0.4.0
621722**BREAKING CHANGE**: A newer version of `Rubin` is now used which is incompatible with older versions of Safir.
···3535 export Output the export command for a key / value pair to be entered into a shell session
3636 clear Clear all keys/values from the store
3737 purge Purges the .safirstore directory, removing it and its contents
3838+ mem Start or stop the Memcache (in-memory store) service
3839 help Print this message or the help of the given subcommand(s)
39404041Options:
4142 -h, --help Print help
4243 -V, --version Print version
4344```
4545+4646+## Memcache service
4747+4848+Safir offers the ability to run the store as a dedicate memcache service using in-memory storage.
4949+5050+The service runs on `localhost` or `127.0.0.1` on dedicated port `9876`.
5151+Once activated, Safir will continue to act as before expect that all new values added are given to the memcache instead of being saved on disk.
5252+5353+This can be enabled / diabled with the `start` and `stop` commands respectively.
5454+Note that when the memcache service is disabled, ALL data contained within it is lost so use wisely.
5555+5656+In cases where you want to save the contents of the memcache, the `dump` command will allow for the contents to be saved out to disk in JSON format.
5757+This behaves as a snapshot as the contents of the cache persist after usage.
5858+5959+### Requirements
6060+6161+Using this requires that the [Rubin CLI](https://crates.io/crates/rubin-cli) be installed.
6262+6363+```bash
6464+cargo install rubin-cli
6565+```
6666+6767+### Usage
6868+6969+Start or stop the Memcache (in-memory store) service
7070+7171+Usage: safir mem <COMMAND>
7272+7373+Commands:
7474+ start Start the Safir Memcache server
7575+ stop Stop the Safir Memcache server
7676+ dump Dump the Safir Memcache server to disk
7777+ help Print this message or the help of the given subcommand(s)
7878+7979+Options:
8080+ -h, --help Print help
8181+ -V, --version Print version
44824583## v0.3.0 -> v0.4.0
4684
···35353636 /// Purges the .safirstore directory, removing it and its contents
3737 Purge,
3838+3939+ /// Start or stop the Memcache (in-memory store) service
4040+ #[clap(subcommand)]
4141+ Mem(MemArgs),
3842}
39434044/// Arguments for adding a value to the store with a given key
···7175 /// Name of the keys to display (e.g. alias / export)
7276 pub keys: Vec<String>,
7377}
7878+7979+/// Arguments for the Mem sub command
8080+#[derive(Subcommand, Debug)]
8181+pub enum MemArgs {
8282+ /// Start the Safir Memcache server
8383+ Start,
8484+8585+ /// Stop the Safir Memcache server
8686+ Stop,
8787+8888+ /// Dump the Safir Memcache server to disk
8989+ Dump(DumpArgs),
9090+}
9191+9292+#[derive(Args, Debug)]
9393+pub struct DumpArgs {
9494+ /// Path to save the store to
9595+ pub path: String,
9696+}
+124-10
src/main.rs
···11+mod cfg;
12mod cli;
23mod safir;
44+mod utils;
3546use cli::*;
5788+use std::process::{Command, Stdio};
99+610#[tokio::main]
711async fn main() -> std::io::Result<()> {
812 let cli = Cli::parse();
99- let mut safir = safir::Safir::init().await?;
1313+ let store_dir = utils::create_safir_directory().await?;
1414+ let safir_cfg = &store_dir.join("safir.cfg");
1515+1616+ let mut cfg = utils::load_safir_config(&safir_cfg).await?;
1717+1818+ // Should probably only initialise when not using memcache but meh...
1919+ // Easier this way
2020+ let mut safir = safir::Safir::init(&store_dir).await?;
2121+ let safir_mem = safir::SafirMemcache::new();
10221123 match &cli.command {
1224 Commands::Add(args) => {
1313- safir
1414- .add_entry(args.key.clone(), args.value.clone())
1515- .await?;
2525+ if cfg.memcache_flag {
2626+ safir_mem.add_entry(&args.key, &args.value).await?
2727+ } else {
2828+ safir
2929+ .add_entry(args.key.clone(), args.value.clone())
3030+ .await?;
3131+ }
1632 }
1733 Commands::Get(args) => {
1818- if let Some(key) = &args.key {
3434+ if cfg.memcache_flag {
3535+ if let Some(key) = &args.key {
3636+ safir_mem.get_string(key).await?;
3737+ } else {
3838+ utils::print_header();
3939+ utils::print_output("A key is required for memcache GET command!");
4040+ }
4141+ } else if let Some(key) = &args.key {
1942 safir.get_entry(key.clone())?;
2043 } else {
2144 safir.display_all();
2245 }
2346 }
2447 Commands::Rm(args) => {
2525- safir.remove_entry(args.key.clone()).await?;
4848+ if cfg.memcache_flag {
4949+ safir_mem.remove_entry(args.key.clone()).await?;
5050+ } else {
5151+ safir.remove_entry(args.key.clone()).await?;
5252+ }
2653 }
2754 Commands::Alias(args) => {
2828- safir.set_commands("alias", &args.keys);
5555+ if cfg.memcache_flag {
5656+ safir_mem.set_commands("alias", &args.keys).await;
5757+ } else {
5858+ safir.set_commands("alias", &args.keys);
5959+ }
2960 }
3061 Commands::Export(args) => {
3131- safir.set_commands("export", &args.keys);
6262+ if cfg.memcache_flag {
6363+ safir_mem.set_commands("export", &args.keys).await;
6464+ } else {
6565+ safir.set_commands("export", &args.keys);
6666+ }
3267 }
3368 Commands::Clear => {
3434- safir.clear_entries().await?;
6969+ if cfg.memcache_flag {
7070+ safir_mem.clear_entries().await?;
7171+ } else {
7272+ safir.clear_entries().await?;
7373+ }
3574 }
3636- Commands::Purge => safir.purge(),
7575+ Commands::Purge => {
7676+ if !cfg.memcache_flag {
7777+ safir.purge();
7878+ }
7979+ }
8080+ Commands::Mem(args) => match args {
8181+ MemArgs::Start => {
8282+ if !utils::check_rubin_installed() {
8383+ eprintln!(
8484+ "The Rubin binary must be installed to use this feature, please install it via cargo using `cargo install rubin-cli`"
8585+ );
8686+ return Ok(());
8787+ }
8888+8989+ if let Some(pid) = cfg.memcache_pid {
9090+ println!(
9191+ "Safir memcache service is already running on 127.0.0.1:9876 - PID {}",
9292+ pid
9393+ );
9494+9595+ return Ok(());
9696+ }
9797+9898+ let child = Command::new("rubin")
9999+ .args(["server"])
100100+ .stdout(Stdio::null())
101101+ .stderr(Stdio::null())
102102+ .stdin(Stdio::null())
103103+ .spawn()
104104+ .expect("unable to spawn child process");
105105+106106+ let pid = child.id();
107107+ cfg = cfg.pid(Some(pid)).set_memcache(true);
108108+ cfg.write(&safir_cfg).await?;
109109+ println!(
110110+ "Safir memcache service started at 127.0.0.1:9876 - PID {}",
111111+ pid
112112+ );
113113+ }
114114+ MemArgs::Stop => {
115115+ if !utils::check_rubin_installed() {
116116+ eprintln!("The Rubin binary must be installed to use this feature, please install it via cargo using `cargo install rubin-cli`");
117117+ return Ok(());
118118+ }
119119+120120+ let pid = match cfg.memcache_pid {
121121+ Some(pid) => pid,
122122+ None => {
123123+ println!("Safir memcache service does not seem to be running.");
124124+ return Ok(());
125125+ }
126126+ };
127127+128128+ if let Err(err) = utils::kill_process(pid).await {
129129+ eprintln!(
130130+ "Safir memcache service failed to stop, manual removal may be necessary - {}",
131131+ err
132132+ );
133133+ } else {
134134+ cfg = cfg.pid(None).set_memcache(false);
135135+ cfg.write(&safir_cfg).await?;
136136+ println!("Stopping Safir memcache service!");
137137+ }
138138+ }
139139+ MemArgs::Dump(args) => {
140140+ if cfg.memcache_pid.is_none() {
141141+ println!("Safir memcache service does not seem to be running");
142142+ return Ok(());
143143+ }
144144+145145+ if let Err(e) = safir_mem.dump_store(&args.path).await {
146146+ eprintln!("unable to dump Safir memcache service: {}", e);
147147+ }
148148+ }
149149+ },
37150 }
151151+38152 Ok(())
39153}
+69-29
src/safir.rs
···88//!
99//! Safir gives you the option to add / get / remove items from the store
1010//! and to clear / purge when you're finished with them.
1111-use std::io::{Result, Write};
1111+use std::{io::Result, path::Path};
12121313use colored::*;
14141515-use rubin::store::persistence::PersistentStore;
1616-1717-/// Formats and prints the message to stdout
1818-fn print_output(msg: &str) {
1919- println!("{}", format!("{}\n", msg));
2020-}
2121-2222-/// Prints the Safirstore header
2323-fn print_header() {
2424- println!("{}", "--=Safirstore=--\n".bold());
2525-}
1515+use crate::utils::{confirm_entry, print_header, print_output};
1616+use rubin::{net::client::RubinClient, store::persistence::PersistentStore};
26172718/// Safir Store (fancy wrapper around reading and writing to a JSON file)
2819pub struct Safir {
···31223223impl Safir {
3324 /// Initialises the Safirstore if not already initialised
3434- pub async fn init() -> Result<Self> {
3535- let home_dir = dirs::home_dir().unwrap();
3636- let store_path = home_dir.join(".safirstore/safirstore.json");
2525+ pub async fn init(store_loc: &Path) -> Result<Self> {
2626+ let store_path = store_loc.join("safirstore.json");
3727 let mut ps = if store_path.exists() {
3828 PersistentStore::from_existing(store_path).await?
3929 } else {
···1049410595 /// Clear the the contents of the store and update onto disk
10696 pub async fn clear_entries(&mut self) -> Result<()> {
107107- if self.confirm_entry("Are you sure you want to clear the store?") {
9797+ if confirm_entry("Are you sure you want to clear the store?") {
10898 self.store.clear_strings().await?;
10999 }
110100···113103114104 /// Remove the store directory and all contents
115105 pub fn purge(&mut self) {
116116- if self.confirm_entry("Are you sure you want to purge Safirstore?") {
106106+ if confirm_entry("Are you sure you want to purge Safirstore?") {
117107 std::fs::remove_dir_all(&self.store.path)
118108 .expect("unable to remove safirstore directory");
119109 }
120110 }
111111+}
121112122122- /// Confirmation dialog for important calls
123123- fn confirm_entry(&self, msg: &str) -> bool {
124124- let mut answer = String::new();
125125- print!("{} (y/n) ", msg);
126126- std::io::stdout().flush().expect("failed to flush buffer");
113113+pub struct SafirMemcache {
114114+ client: RubinClient,
115115+}
127116128128- let _ = std::io::stdin()
129129- .read_line(&mut answer)
130130- .expect("unable to get input from user");
117117+impl SafirMemcache {
118118+ pub fn new() -> Self {
119119+ Self {
120120+ client: RubinClient::new("127.0.0.1", 9876),
121121+ }
122122+ }
131123132132- let answer = answer.trim();
133133- if answer == "y" || answer == "Y" {
134134- return true;
124124+ pub async fn add_entry(&self, key: &str, value: &str) -> Result<()> {
125125+ self.client.insert_string(key, value).await?;
126126+ Ok(())
127127+ }
128128+129129+ pub async fn get_string(&self, key: &str) -> Result<()> {
130130+ print_header();
131131+ let output = if let Ok(val) = self.client.get_string(key).await {
132132+ format!("{}: \"{}\"", key.bold().yellow(), val)
133133+ } else {
134134+ format!("{}: ", key.bold().yellow())
135135+ };
136136+137137+ print_output(&output);
138138+139139+ Ok(())
140140+ }
141141+142142+ pub async fn remove_entry(&self, keys: Vec<String>) -> Result<()> {
143143+ for key in &keys {
144144+ self.client.remove_string(key).await?;
135145 }
136146137137- false
147147+ Ok(())
148148+ }
149149+150150+ pub async fn set_commands(&self, prefix: &str, keys: &Vec<String>) {
151151+ print_header();
152152+ let prefix = match prefix {
153153+ "alias" => "alias".bold().green(),
154154+ "export" => "export".bold().magenta(),
155155+ _ => prefix.bold(),
156156+ };
157157+158158+ for key in keys {
159159+ if let Ok(value) = self.client.get_string(key).await {
160160+ println!("{} {}=\"{}\"\n", prefix, key.bold().yellow(), value);
161161+ }
162162+ }
163163+ }
164164+165165+ pub async fn clear_entries(&self) -> Result<()> {
166166+ if confirm_entry("Are you sure you want to clear the store?") {
167167+ self.client.clear_strings().await?;
168168+ }
169169+170170+ Ok(())
171171+ }
172172+173173+ pub async fn dump_store(&self, path: &str) -> Result<()> {
174174+ self.client.dump_store(path).await?;
175175+ println!("Safir memcache dumped to {}", path);
176176+177177+ Ok(())
138178 }
139179}
+112
src/utils.rs
···11+use std::io::{self, Write};
22+use std::path::{Path, PathBuf};
33+44+use crate::cfg::SafirConfig;
55+66+use colored::*;
77+use sysinfo::{Pid, System, SystemExt};
88+use tokio::fs;
99+1010+pub fn check_rubin_installed() -> bool {
1111+ if which::which("rubin").is_ok() {
1212+ return true;
1313+ }
1414+1515+ false
1616+}
1717+1818+pub fn check_process_running(pid: u32) -> bool {
1919+ let mut system = System::new_all();
2020+ system.refresh_all();
2121+ if system.process(Pid::from(pid as usize)).is_some() {
2222+ return true;
2323+ }
2424+2525+ false
2626+}
2727+2828+pub async fn path_exists(path: impl AsRef<Path>) -> bool {
2929+ path.as_ref().exists()
3030+}
3131+3232+pub async fn create_safir_directory() -> io::Result<PathBuf> {
3333+ let home_dir = dirs::home_dir().unwrap();
3434+ let store_path = home_dir.join(".safirstore");
3535+ fs::create_dir_all(&store_path).await?;
3636+3737+ Ok(store_path)
3838+}
3939+4040+#[cfg(target_family = "unix")]
4141+pub async fn kill_process(pid: u32) -> io::Result<()> {
4242+ if let Ok(process) = psutil::process::Process::new(pid) {
4343+ if let Err(err) = process.kill() {
4444+ eprintln!("failed to kill process: {}", err);
4545+ }
4646+ } else {
4747+ eprintln!("failed to get process information");
4848+ }
4949+5050+ Ok(())
5151+}
5252+5353+#[cfg(target_os = "windows")]
5454+pub async fn kill_process(pid: u32) {
5555+ println!("*** Windows -- This is experimental and may not work as intended! ***");
5656+ let output = Command::new("taskkill")
5757+ .arg("/F")
5858+ .arg("/PID")
5959+ .arg(pid.to_string())
6060+ .output()
6161+ .expect("failed to call taskkill");
6262+6363+ if !output.status.success() {
6464+ eprintln!("failed to terminate process");
6565+ }
6666+}
6767+6868+/// Formats and prints the message to stdout
6969+pub fn print_output(msg: &str) {
7070+ println!("{}\n", msg);
7171+}
7272+7373+/// Prints the Safirstore header
7474+pub fn print_header() {
7575+ println!("{}", "--=Safirstore=--\n".bold());
7676+}
7777+7878+/// Confirmation dialog for important calls
7979+pub fn confirm_entry(msg: &str) -> bool {
8080+ let mut answer = String::new();
8181+ print!("{} (y/n) ", msg);
8282+ std::io::stdout().flush().expect("failed to flush buffer");
8383+8484+ let _ = std::io::stdin()
8585+ .read_line(&mut answer)
8686+ .expect("unable to get input from user");
8787+8888+ let answer = answer.trim().to_lowercase();
8989+ if answer == "y" {
9090+ return true;
9191+ }
9292+9393+ false
9494+}
9595+9696+pub async fn load_safir_config(safir_cfg: impl AsRef<Path>) -> io::Result<SafirConfig> {
9797+ let mut cfg = if path_exists(&safir_cfg).await {
9898+ SafirConfig::load(&safir_cfg).await?
9999+ } else {
100100+ SafirConfig::new()
101101+ };
102102+103103+ // Used in cases where the process has ended ungracefully and the config hasnt been updated
104104+ if let Some(pid) = cfg.memcache_pid {
105105+ if !check_process_running(pid) {
106106+ cfg = SafirConfig::new();
107107+ cfg.write(&safir_cfg).await?;
108108+ }
109109+ }
110110+111111+ Ok(cfg)
112112+}