lb-rs 26.4.13

The rust library for interacting with your lockbook.
Documentation
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 // is this the move?
        }
    }

    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); // UUIDs are 36 characters long in string form

            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) }
    }
}