use crate::{config::DbUrl, Error, Result};
use async_trait::async_trait;
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
#[async_trait]
pub trait Storage: Send + Sync + 'static {
async fn get(&self, key: &str) -> Result<Option<String>>;
async fn set(&self, key: &str, value: &str) -> Result<()>;
async fn del(&self, key: &str) -> Result<()>;
async fn set_nx(&self, key: &str, value: &str) -> Result<bool> {
if self.get(key).await?.is_some() {
return Ok(false);
}
self.set(key, value).await?;
Ok(true)
}
async fn list_prefix(&self, _prefix: &str) -> Result<Vec<(String, String)>> {
Ok(Vec::new())
}
}
pub type AnyStorage = Arc<dyn Storage>;
pub fn open_storage() -> Result<AnyStorage> {
open_storage_from(DbUrl::from_env()?)
}
pub fn open_storage_from(url: DbUrl) -> Result<AnyStorage> {
match url {
DbUrl::Memory => Ok(Arc::new(MemoryStorage::new())),
#[cfg(feature = "sqlite")]
DbUrl::Sqlite(path) => Ok(Arc::new(SqliteStorage::open(&path)?)),
#[cfg(not(feature = "sqlite"))]
DbUrl::Sqlite(_) => Err(Error::Other(
"sqlite backend is not enabled - build with the `sqlite` feature".into(),
)),
DbUrl::External(url) => Err(Error::Other(format!(
"FoukoApi does not bundle a driver for {url}. Implement Storage yourself and plug it in."
))),
}
}
#[derive(Debug, Clone, Default)]
pub struct MemoryStorage {
inner: Arc<Mutex<HashMap<String, String>>>,
}
impl MemoryStorage {
pub fn new() -> Self {
Self::default()
}
fn map(&self) -> std::sync::MutexGuard<'_, HashMap<String, String>> {
self.inner.lock().unwrap_or_else(|p| p.into_inner())
}
}
#[async_trait]
impl Storage for MemoryStorage {
async fn get(&self, key: &str) -> Result<Option<String>> {
Ok(self.map().get(key).cloned())
}
async fn set(&self, key: &str, value: &str) -> Result<()> {
self.map().insert(key.to_owned(), value.to_owned());
Ok(())
}
async fn del(&self, key: &str) -> Result<()> {
self.map().remove(key);
Ok(())
}
async fn set_nx(&self, key: &str, value: &str) -> Result<bool> {
let mut map = self.map();
match map.entry(key.to_owned()) {
std::collections::hash_map::Entry::Occupied(_) => Ok(false),
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(value.to_owned());
Ok(true)
}
}
}
async fn list_prefix(&self, prefix: &str) -> Result<Vec<(String, String)>> {
Ok(self
.map()
.iter()
.filter(|(k, _)| k.starts_with(prefix))
.map(|(k, v)| (k.clone(), v.clone()))
.collect())
}
}
#[cfg(feature = "sqlite")]
#[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))]
mod sqlite_impl {
use super::*;
use rusqlite::{params, Connection};
use std::path::Path;
use std::sync::Mutex as StdMutex;
pub struct SqliteStorage {
conn: StdMutex<Connection>,
}
impl SqliteStorage {
pub fn open(path: &Path) -> Result<Self> {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent).map_err(|e| {
Error::Other(format!("creating {} dir: {e}", parent.display()))
})?;
}
}
let conn = Connection::open(path)
.map_err(|e| Error::Other(format!("opening sqlite {}: {e}", path.display())))?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS foukoapi_kv (
k TEXT PRIMARY KEY,
v TEXT NOT NULL
);",
)
.map_err(|e| Error::Other(format!("creating kv table: {e}")))?;
Ok(Self {
conn: StdMutex::new(conn),
})
}
}
#[async_trait]
impl Storage for SqliteStorage {
async fn get(&self, key: &str) -> Result<Option<String>> {
let conn = self
.conn
.lock()
.map_err(|_| Error::Other("sqlite mutex poisoned".into()))?;
Ok(conn
.query_row(
"SELECT v FROM foukoapi_kv WHERE k = ?1",
params![key],
|row| row.get::<_, String>(0),
)
.ok())
}
async fn set(&self, key: &str, value: &str) -> Result<()> {
let conn = self
.conn
.lock()
.map_err(|_| Error::Other("sqlite mutex poisoned".into()))?;
conn.execute(
"INSERT INTO foukoapi_kv (k, v) VALUES (?1, ?2)
ON CONFLICT(k) DO UPDATE SET v = excluded.v",
params![key, value],
)
.map_err(|e| Error::Other(format!("sqlite set: {e}")))?;
Ok(())
}
async fn del(&self, key: &str) -> Result<()> {
let conn = self
.conn
.lock()
.map_err(|_| Error::Other("sqlite mutex poisoned".into()))?;
conn.execute("DELETE FROM foukoapi_kv WHERE k = ?1", params![key])
.map_err(|e| Error::Other(format!("sqlite del: {e}")))?;
Ok(())
}
async fn set_nx(&self, key: &str, value: &str) -> Result<bool> {
let conn = self
.conn
.lock()
.map_err(|_| Error::Other("sqlite mutex poisoned".into()))?;
let changed = conn
.execute(
"INSERT INTO foukoapi_kv (k, v) VALUES (?1, ?2)
ON CONFLICT(k) DO NOTHING",
params![key, value],
)
.map_err(|e| Error::Other(format!("sqlite set_nx: {e}")))?;
Ok(changed > 0)
}
async fn list_prefix(&self, prefix: &str) -> Result<Vec<(String, String)>> {
let conn = self
.conn
.lock()
.map_err(|_| Error::Other("sqlite mutex poisoned".into()))?;
let pattern = format!(
"{}%",
prefix
.replace('\\', "\\\\")
.replace('%', "\\%")
.replace('_', "\\_")
);
let mut stmt = conn
.prepare("SELECT k, v FROM foukoapi_kv WHERE k LIKE ?1 ESCAPE '\\'")
.map_err(|e| Error::Other(format!("sqlite list_prefix prepare: {e}")))?;
let rows = stmt
.query_map(params![pattern], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})
.map_err(|e| Error::Other(format!("sqlite list_prefix query: {e}")))?;
let mut out = Vec::new();
for row in rows {
out.push(row.map_err(|e| Error::Other(format!("sqlite list_prefix row: {e}")))?);
}
Ok(out)
}
}
}
#[cfg(feature = "sqlite")]
pub use sqlite_impl::SqliteStorage;
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn memory_list_prefix() {
let s = MemoryStorage::new();
s.set("a:1", "x").await.unwrap();
s.set("a:2", "y").await.unwrap();
s.set("b:1", "z").await.unwrap();
let mut found = s.list_prefix("a:").await.unwrap();
found.sort();
assert_eq!(
found,
vec![
("a:1".to_owned(), "x".to_owned()),
("a:2".to_owned(), "y".to_owned())
]
);
assert!(s.list_prefix("nope:").await.unwrap().is_empty());
}
#[tokio::test]
async fn memory_set_nx() {
let s = MemoryStorage::new();
assert!(s.set_nx("k", "first").await.unwrap());
assert!(!s.set_nx("k", "second").await.unwrap());
assert_eq!(s.get("k").await.unwrap().as_deref(), Some("first"));
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn sqlite_set_nx() {
let dir = std::env::temp_dir().join(format!("foukoapi-setnx-{}", std::process::id()));
let path = dir.join("kv.db");
let s = SqliteStorage::open(&path).unwrap();
assert!(s.set_nx("k", "first").await.unwrap());
assert!(!s.set_nx("k", "second").await.unwrap());
assert_eq!(s.get("k").await.unwrap().as_deref(), Some("first"));
drop(s);
let _ = std::fs::remove_dir_all(&dir);
}
}