lb_rs/io/
docs.rs

1use crate::model::core_config::Config;
2use crate::model::crypto::EncryptedDocument;
3use crate::model::errors::{LbErrKind, LbResult, Unexpected};
4use crate::model::file_metadata::DocumentHmac;
5use std::collections::HashSet;
6use std::io::ErrorKind;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use std::sync::atomic::AtomicBool;
10use tokio::fs::{self, File, OpenOptions};
11use tokio::io::{AsyncReadExt, AsyncWriteExt};
12use uuid::Uuid;
13
14#[derive(Clone)]
15pub struct AsyncDocs {
16    pub(crate) dont_delete: Arc<AtomicBool>,
17    location: PathBuf,
18}
19
20impl AsyncDocs {
21    pub async fn insert(
22        &self, id: Uuid, hmac: Option<DocumentHmac>, document: &EncryptedDocument,
23    ) -> LbResult<()> {
24        if let Some(hmac) = hmac {
25            let value = &bincode::serialize(document).map_unexpected()?;
26            let path_str = key_path(&self.location, id, hmac) + ".pending";
27            let path = Path::new(&path_str);
28            trace!("write\t{} {:?} bytes", &path_str, value.len());
29            fs::create_dir_all(path.parent().unwrap()).await?;
30            let mut f = OpenOptions::new()
31                .write(true)
32                .create(true)
33                .truncate(true)
34                .open(path)
35                .await?;
36            f.write_all(value).await?;
37            Ok(fs::rename(path, key_path(&self.location, id, hmac)).await?)
38        } else {
39            Ok(())
40        }
41    }
42
43    pub async fn get(&self, id: Uuid, hmac: Option<DocumentHmac>) -> LbResult<EncryptedDocument> {
44        self.maybe_get(id, hmac)
45            .await?
46            .ok_or_else(|| LbErrKind::FileNonexistent.into())
47    }
48
49    pub async fn maybe_get(
50        &self, id: Uuid, hmac: Option<DocumentHmac>,
51    ) -> LbResult<Option<EncryptedDocument>> {
52        if let Some(hmac) = hmac {
53            let path_str = key_path(&self.location, id, hmac);
54            let path = Path::new(&path_str);
55            trace!("read\t{}", &path_str);
56            let maybe_data: Option<Vec<u8>> = match File::open(path).await {
57                Ok(mut f) => {
58                    let mut buffer: Vec<u8> = Vec::new();
59                    f.read_to_end(&mut buffer).await?;
60                    Some(buffer)
61                }
62                Err(err) => match err.kind() {
63                    ErrorKind::NotFound => None,
64                    _ => return Err(err.into()),
65                },
66            };
67
68            Ok(match maybe_data {
69                Some(data) => bincode::deserialize(&data).map(Some).map_unexpected()?,
70                None => None,
71            })
72        } else {
73            Ok(None)
74        }
75    }
76
77    pub async fn maybe_size(&self, id: Uuid, hmac: Option<DocumentHmac>) -> LbResult<Option<u64>> {
78        match hmac {
79            Some(hmac) => {
80                let path_str = key_path(&self.location, id, hmac);
81                let path = Path::new(&path_str);
82                Ok(path.metadata().ok().map(|meta| meta.len()))
83            }
84            None => Ok(None),
85        }
86    }
87
88    pub async fn delete(&self, id: Uuid, hmac: Option<DocumentHmac>) -> LbResult<()> {
89        if let Some(hmac) = hmac {
90            let path_str = key_path(&self.location, id, hmac);
91            let path = Path::new(&path_str);
92            trace!("delete\t{}", &path_str);
93            if path.exists() {
94                fs::remove_file(path).await.map_unexpected()?;
95            }
96        }
97
98        Ok(())
99    }
100
101    pub(crate) async fn retain(&self, file_hmacs: HashSet<(Uuid, [u8; 32])>) -> LbResult<()> {
102        let dir_path = namespace_path(&self.location);
103        fs::create_dir_all(&dir_path).await?;
104        let mut entries = fs::read_dir(&dir_path).await?;
105
106        while let Some(entry) = entries.next_entry().await? {
107            let path = entry.path();
108            let file_name = path
109                .file_name()
110                .and_then(|name| name.to_str())
111                .ok_or(LbErrKind::Unexpected("could not get filename from os".to_string()))?;
112
113            if file_name.contains("pending") {
114                continue;
115            }
116
117            let (id_str, hmac_str) = file_name.split_at(36); // UUIDs are 36 characters long in string form
118
119            let id = Uuid::parse_str(id_str).map_err(|err| {
120                LbErrKind::Unexpected(format!("could not parse doc name as uuid {err:?}"))
121            })?;
122
123            let hmac_base64 = hmac_str
124                .strip_prefix('-')
125                .ok_or(LbErrKind::Unexpected("doc name missing -".to_string()))?;
126
127            let hmac_bytes =
128                base64::decode_config(hmac_base64, base64::URL_SAFE).map_err(|err| {
129                    LbErrKind::Unexpected(format!("document disk file name malformed: {err:?}"))
130                })?;
131
132            let hmac: DocumentHmac = hmac_bytes.try_into().map_err(|err| {
133                LbErrKind::Unexpected(format!("document disk file name malformed {err:?}"))
134            })?;
135
136            if !file_hmacs.contains(&(id, hmac)) {
137                self.delete(id, Some(hmac)).await?;
138            }
139        }
140        Ok(())
141    }
142}
143
144pub fn namespace_path(writeable_path: &Path) -> String {
145    format!("{}/documents", writeable_path.to_str().unwrap())
146}
147
148pub fn key_path(writeable_path: &Path, key: Uuid, hmac: DocumentHmac) -> String {
149    let hmac = base64::encode_config(hmac, base64::URL_SAFE);
150    format!("{}/{}-{}", namespace_path(writeable_path), key, hmac)
151}
152
153impl From<&Config> for AsyncDocs {
154    fn from(cfg: &Config) -> Self {
155        Self { location: PathBuf::from(&cfg.writeable_path), dont_delete: Default::default() }
156    }
157}