lb_rs/io/
docs.rs

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