use std::collections::HashMap;
use std::path::PathBuf;
use std::str;
use anyhow::{Context, Result};
use parking_lot::Mutex;
use serde_json::Value;
use tokio::fs;
use tracing::info;
#[derive(Debug)]
pub enum Storage {
S3(aws_sdk_s3::Client, String),
LocalFS(PathBuf),
Memory(Mutex<HashMap<String, Value>>),
}
impl Storage {
#[tracing::instrument(skip(self))]
pub(crate) async fn get(&self, env: &str) -> Result<Value> {
info!("reading template");
match self {
Storage::S3(s3, bucket) => {
let key = format!("{env}.json");
let resp = s3.get_object().bucket(bucket).key(key).send().await?;
let data = resp.body.collect().await?;
let bytes = data.into_bytes();
Ok(serde_json::from_str(str::from_utf8(&bytes)?)?)
}
Storage::LocalFS(path) => {
let path = path.join(format!("{env}.json"));
let data = fs::read(&path).await?;
Ok(serde_json::from_slice(&data)?)
}
Storage::Memory(map) => Ok(map.lock().get(env).context("missing template")?.clone()),
}
}
#[tracing::instrument(skip(self))]
pub(crate) async fn set(&self, env: &str, value: &Value) -> Result<()> {
info!("writing template");
match self {
Storage::S3(s3, bucket) => {
let key = format!("{env}.json");
let content = serde_json::to_vec_pretty(value)?.into();
s3.put_object()
.bucket(bucket)
.key(key)
.body(content)
.send()
.await?;
}
Storage::LocalFS(path) => {
let path = path.join(format!("{env}.json"));
let content = serde_json::to_vec_pretty(value)?;
fs::write(&path, &content).await?;
}
Storage::Memory(map) => {
map.lock().insert(env.into(), value.clone());
}
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub(crate) async fn list(&self) -> Result<Vec<String>> {
info!("listing templates");
match self {
Storage::S3(s3, bucket) => {
let objects = s3.list_objects_v2().bucket(bucket).send().await?;
Ok(objects
.contents()
.unwrap_or_default()
.iter()
.filter_map(|object| Some(object.key()?.strip_suffix(".json")?.to_owned()))
.collect())
}
Storage::LocalFS(path) => {
let mut dir = fs::read_dir(&path).await?;
let mut results = Vec::new();
while let Some(entry) = dir.next_entry().await? {
if let Some(name) = entry.file_name().to_str() {
if let Some(env) = name.strip_suffix(".json") {
results.push(env.to_owned());
}
}
}
Ok(results)
}
Storage::Memory(map) => Ok(map.lock().keys().cloned().collect()),
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use serde_json::json;
use super::Storage;
#[tokio::test]
async fn memory_operations() -> Result<()> {
let storage = Storage::Memory(Default::default());
assert!(storage.get("hello").await.is_err());
assert!(storage.list().await?.is_empty());
storage.set("hello", &json!("world")).await?;
assert_eq!(storage.get("hello").await?, json!("world"));
assert_eq!(storage.list().await?, vec![String::from("hello")]);
Ok(())
}
}