use crate::model::{
core_config::Config,
crypto::EncryptedDocument,
errors::{LbErrKind, LbResult},
file_metadata::DocumentHmac,
};
use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use uuid::Uuid;
#[cfg(not(target_family = "wasm"))]
use {
crate::model::errors::Unexpected,
std::io::ErrorKind,
tokio::{
fs::{self, File, OpenOptions},
io::{AsyncReadExt, AsyncWriteExt},
},
};
#[derive(Clone)]
pub struct AsyncDocs {
location: PathBuf,
}
#[cfg(target_family = "wasm")]
impl AsyncDocs {
pub async fn insert(
&self, _id: Uuid, _hmac: Option<DocumentHmac>, _document: &EncryptedDocument,
) -> LbResult<()> {
Ok(())
}
pub async fn get(&self, _id: Uuid, _hmac: Option<DocumentHmac>) -> LbResult<EncryptedDocument> {
Err(LbErrKind::FileNonexistent.into())
}
pub async fn maybe_get(
&self, _id: Uuid, _hmac: Option<DocumentHmac>,
) -> LbResult<Option<EncryptedDocument>> {
Ok(None)
}
pub async fn delete(&self, _id: Uuid, _hmac: Option<DocumentHmac>) -> LbResult<()> {
Ok(())
}
pub fn exists(&self, id: Uuid, hmac: Option<DocumentHmac>) -> bool {
true
}
pub(crate) async fn retain(&self, _file_hmacs: HashSet<(Uuid, [u8; 32])>) -> LbResult<()> {
Ok(())
}
}
#[cfg(not(target_family = "wasm"))]
impl AsyncDocs {
pub async fn insert(
&self, id: Uuid, hmac: Option<DocumentHmac>, document: &EncryptedDocument,
) -> LbResult<()> {
if let Some(hmac) = hmac {
let value = &bincode::serialize(document).map_unexpected()?;
let path_str = key_path(&self.location, id, hmac) + ".pending";
let path = Path::new(&path_str);
trace!("write\t{} {:?} bytes", &path_str, value.len());
fs::create_dir_all(path.parent().unwrap()).await?;
let mut f = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.await?;
f.write_all(value).await?;
Ok(fs::rename(path, key_path(&self.location, id, hmac)).await?)
} else {
Ok(())
}
}
pub fn exists(&self, id: Uuid, hmac: Option<DocumentHmac>) -> bool {
if let Some(hmac) = hmac {
let path_str = key_path(&self.location, id, hmac);
let path = Path::new(&path_str);
path.exists()
} else {
false }
}
pub async fn get(&self, id: Uuid, hmac: Option<DocumentHmac>) -> LbResult<EncryptedDocument> {
self.maybe_get(id, hmac)
.await?
.ok_or_else(|| LbErrKind::FileNonexistent.into())
}
pub async fn maybe_get(
&self, id: Uuid, hmac: Option<DocumentHmac>,
) -> LbResult<Option<EncryptedDocument>> {
if let Some(hmac) = hmac {
let path_str = key_path(&self.location, id, hmac);
let path = Path::new(&path_str);
trace!("read\t{}", &path_str);
let maybe_data: Option<Vec<u8>> = match File::open(path).await {
Ok(mut f) => {
let mut buffer: Vec<u8> = Vec::new();
f.read_to_end(&mut buffer).await?;
Some(buffer)
}
Err(err) => match err.kind() {
ErrorKind::NotFound => None,
_ => return Err(err.into()),
},
};
Ok(match maybe_data {
Some(data) => bincode::deserialize(&data).map(Some).map_unexpected()?,
None => None,
})
} else {
Ok(None)
}
}
pub async fn maybe_size(&self, id: Uuid, hmac: Option<DocumentHmac>) -> LbResult<Option<u64>> {
match hmac {
Some(hmac) => {
let path_str = key_path(&self.location, id, hmac);
let path = Path::new(&path_str);
Ok(path.metadata().ok().map(|meta| meta.len()))
}
None => Ok(None),
}
}
pub async fn delete(&self, id: Uuid, hmac: Option<DocumentHmac>) -> LbResult<()> {
if let Some(hmac) = hmac {
let path_str = key_path(&self.location, id, hmac);
let path = Path::new(&path_str);
trace!("delete\t{}", &path_str);
if path.exists() {
fs::remove_file(path).await.map_unexpected()?;
}
}
Ok(())
}
pub(crate) async fn retain(&self, file_hmacs: HashSet<(Uuid, [u8; 32])>) -> LbResult<()> {
let dir_path = namespace_path(&self.location);
fs::create_dir_all(&dir_path).await?;
let mut entries = fs::read_dir(&dir_path).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.ok_or(LbErrKind::Unexpected("could not get filename from os".to_string()))?;
if file_name.contains("pending") {
continue;
}
let (id_str, hmac_str) = file_name.split_at(36);
let id = Uuid::parse_str(id_str).map_err(|err| {
LbErrKind::Unexpected(format!("could not parse doc name as uuid {err:?}"))
})?;
let hmac_base64 = hmac_str
.strip_prefix('-')
.ok_or(LbErrKind::Unexpected("doc name missing -".to_string()))?;
let hmac_bytes =
base64::decode_config(hmac_base64, base64::URL_SAFE).map_err(|err| {
LbErrKind::Unexpected(format!("document disk file name malformed: {err:?}"))
})?;
let hmac: DocumentHmac = hmac_bytes.try_into().map_err(|err| {
LbErrKind::Unexpected(format!("document disk file name malformed {err:?}"))
})?;
if !file_hmacs.contains(&(id, hmac)) {
self.delete(id, Some(hmac)).await?;
}
}
Ok(())
}
}
pub fn namespace_path(writeable_path: &Path) -> String {
format!("{}/documents", writeable_path.to_str().unwrap())
}
pub fn key_path(writeable_path: &Path, key: Uuid, hmac: DocumentHmac) -> String {
let hmac = base64::encode_config(hmac, base64::URL_SAFE);
format!("{}/{}-{}", namespace_path(writeable_path), key, hmac)
}
impl From<&Config> for AsyncDocs {
fn from(cfg: &Config) -> Self {
Self { location: PathBuf::from(&cfg.writeable_path) }
}
}