use color_eyre::eyre::{self, eyre, Result};
use openssl::{base64, sha::Sha256};
use serde_jcs::to_vec as to_jcs;
use serde_json::Value;
use std::path::PathBuf;
use tokio::{fs, io::AsyncWriteExt};
pub fn b64sha256(bytes: &[u8]) -> String {
let mut hash = Sha256::new();
hash.update(&bytes);
let hash = base64::encode_block(&hash.finish());
let hash = hash.replace('/', "+");
return hash;
}
pub struct Database {
path: PathBuf,
}
pub trait PutInDB {
async fn put_in_db(
&self,
db: &mut Database,
) -> Result<String>;
}
impl PutInDB for Value {
async fn put_in_db(
&self,
db: &mut Database,
) -> Result<String> {
mk_item(&self)?.put_in_db(db).await
}
}
impl PutInDB for Item {
async fn put_in_db(
&self,
db: &mut Database,
) -> Result<String> {
match db.put_item(self).await {
Err(x) => Err(x),
Ok(_) => Ok(self.hash_b64.clone()),
}
}
}
#[derive(Clone)]
pub struct Item {
pub hash_b64: String,
pub json_utf8: String,
}
impl Database {
pub fn open(path: PathBuf) -> Result<Self> {
use std::fs;
if !path.try_exists()? {
fs::create_dir_all(&path)?;
};
if path.is_dir() {
Ok(Self { path })
} else {
Err(eyre!("File exists at DB Directory"))
}
}
pub async fn put_item(
&mut self,
item: &Item,
) -> Result<()> {
let item_path = self.path.join(&item.hash_b64);
if !item_path.try_exists()? {
fs::File::create(&item_path)
.await?
.write_all(item.json_utf8.as_bytes())
.await?;
}
Ok(())
}
pub async fn put_obj(
&mut self,
object: &Value,
) -> Result<Item> {
let item: Item = mk_item(object)?;
self.put_item(&item).await?;
Ok(item)
}
pub async fn get_item(
&self,
hash_b64: &str,
) -> Result<Item> {
let path = self.path.join(hash_b64);
let json_utf8 = fs::read(path).await?;
let json_utf8 = String::from_utf8(json_utf8)?;
let item = Item {
hash_b64: hash_b64.to_string(),
json_utf8,
};
item.check_hash()?;
return Ok(item);
}
pub async fn get_obj(
&self,
hash_b64: &str,
) -> Result<Value> {
use std::str::FromStr;
let item = self.get_item(hash_b64).await?;
Ok(Value::from_str(&item.json_utf8)?)
}
pub fn get_path(&self) -> &PathBuf {
&self.path
}
}
impl core::fmt::Debug for Item {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
writeln!(
f,
"DISPLAY: Item {{ hash_b64: {}, json_utf8: {} }}",
self.hash_b64, self.json_utf8,
)
}
}
impl Item {
fn check_hash(&self) -> Result<&Self> {
let is_valid = self.hash_b64 == b64sha256(self.json_utf8.as_bytes());
if is_valid {
Ok(self)
} else {
Err(eyre!("Invalid Hash"))
}
}
}
impl TryFrom<Value> for Item {
type Error = eyre::Error;
fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
mk_item(&value)
}
}
impl TryFrom<Item> for Value {
fn try_from(value: Item) -> std::result::Result<Self, Self::Error> {
value.check_hash()?;
Ok(serde_json::from_str(&value.json_utf8)?)
}
type Error = eyre::Error;
}
pub fn mk_item(obj: &Value) -> Result<Item> {
let json_utf8: Vec<u8> = to_jcs(&obj)?;
let hash_b64 = b64sha256(&json_utf8);
let json_utf8: String = String::from_utf8(json_utf8)?;
Ok(Item {
json_utf8,
hash_b64,
})
}