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<()>;
}
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()
}
}
#[async_trait]
impl Storage for MemoryStorage {
async fn get(&self, key: &str) -> Result<Option<String>> {
Ok(self.inner.lock().unwrap().get(key).cloned())
}
async fn set(&self, key: &str, value: &str) -> Result<()> {
self.inner
.lock()
.unwrap()
.insert(key.to_owned(), value.to_owned());
Ok(())
}
async fn del(&self, key: &str) -> Result<()> {
self.inner.lock().unwrap().remove(key);
Ok(())
}
}
#[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(())
}
}
}
#[cfg(feature = "sqlite")]
pub use sqlite_impl::SqliteStorage;