Skip to main content

lb_rs/io/
docs.rs

1use crate::model::{
2    core_config::Config,
3    crypto::EncryptedDocument,
4    errors::{LbErrKind, LbResult},
5    file_metadata::DocumentHmac,
6};
7use std::{
8    collections::HashSet,
9    path::{Path, PathBuf},
10};
11use uuid::Uuid;
12
13#[cfg(not(target_family = "wasm"))]
14use {
15    crate::model::errors::Unexpected,
16    std::io::ErrorKind,
17    tokio::{
18        fs::{self, File, OpenOptions},
19        io::{AsyncReadExt, AsyncWriteExt},
20    },
21};
22
23#[derive(Clone)]
24pub struct AsyncDocs {
25    location: PathBuf,
26}
27
28#[cfg(target_family = "wasm")]
29impl AsyncDocs {
30    pub async fn insert(
31        &self, _id: Uuid, _hmac: Option<DocumentHmac>, _document: &EncryptedDocument,
32    ) -> LbResult<()> {
33        Ok(())
34    }
35
36    pub async fn insert_pending(
37        &self, _id: Uuid, _hmac: DocumentHmac, _document: &EncryptedDocument,
38    ) -> LbResult<()> {
39        Ok(())
40    }
41
42    pub async fn promote_pending(&self, _id: Uuid, _hmac: DocumentHmac) -> LbResult<()> {
43        Ok(())
44    }
45
46    pub async fn get(&self, _id: Uuid, _hmac: Option<DocumentHmac>) -> LbResult<EncryptedDocument> {
47        Err(LbErrKind::FileNonexistent.into())
48    }
49
50    pub async fn maybe_get(
51        &self, _id: Uuid, _hmac: Option<DocumentHmac>,
52    ) -> LbResult<Option<EncryptedDocument>> {
53        Ok(None)
54    }
55
56    pub async fn delete(&self, _id: Uuid, _hmac: Option<DocumentHmac>) -> LbResult<()> {
57        Ok(())
58    }
59
60    pub fn exists(&self, id: Uuid, hmac: Option<DocumentHmac>) -> bool {
61        true
62    }
63
64    pub(crate) async fn retain(&self, _file_hmacs: HashSet<(Uuid, [u8; 32])>) -> LbResult<()> {
65        Ok(())
66    }
67}
68
69#[cfg(not(target_family = "wasm"))]
70impl AsyncDocs {
71    pub async fn insert(
72        &self, id: Uuid, hmac: Option<DocumentHmac>, document: &EncryptedDocument,
73    ) -> LbResult<()> {
74        if let Some(hmac) = hmac {
75            self.insert_pending(id, hmac, document).await?;
76            self.promote_pending(id, hmac).await?;
77        }
78        Ok(())
79    }
80
81    pub async fn insert_pending(
82        &self, id: Uuid, hmac: DocumentHmac, document: &EncryptedDocument,
83    ) -> LbResult<()> {
84        let value = &bincode::serialize(document).map_unexpected()?;
85        let path_str = pending_path(&self.location, id, hmac);
86        let path = Path::new(&path_str);
87        trace!("write pending\t{} {:?} bytes", &path_str, value.len());
88        fs::create_dir_all(path.parent().unwrap()).await?;
89        let mut f = OpenOptions::new()
90            .write(true)
91            .create(true)
92            .truncate(true)
93            .open(path)
94            .await?;
95        f.write_all(value).await?;
96        Ok(())
97    }
98
99    pub async fn promote_pending(&self, id: Uuid, hmac: DocumentHmac) -> LbResult<()> {
100        let pending = pending_path(&self.location, id, hmac);
101        let final_path = key_path(&self.location, id, hmac);
102        Ok(fs::rename(&pending, &final_path).await?)
103    }
104
105    pub fn exists(&self, id: Uuid, hmac: Option<DocumentHmac>) -> bool {
106        if let Some(hmac) = hmac {
107            let path_str = key_path(&self.location, id, hmac);
108            let path = Path::new(&path_str);
109            path.exists()
110        } else {
111            false // is this the move?
112        }
113    }
114
115    pub async fn get(&self, id: Uuid, hmac: Option<DocumentHmac>) -> LbResult<EncryptedDocument> {
116        self.maybe_get(id, hmac)
117            .await?
118            .ok_or_else(|| LbErrKind::FileNonexistent.into())
119    }
120
121    pub async fn maybe_get(
122        &self, id: Uuid, hmac: Option<DocumentHmac>,
123    ) -> LbResult<Option<EncryptedDocument>> {
124        if let Some(hmac) = hmac {
125            let path_str = key_path(&self.location, id, hmac);
126            let path = Path::new(&path_str);
127            trace!("read\t{}", &path_str);
128            let maybe_data: Option<Vec<u8>> = match File::open(path).await {
129                Ok(mut f) => {
130                    let mut buffer: Vec<u8> = Vec::new();
131                    f.read_to_end(&mut buffer).await?;
132                    Some(buffer)
133                }
134                Err(err) => match err.kind() {
135                    ErrorKind::NotFound => None,
136                    _ => return Err(err.into()),
137                },
138            };
139
140            Ok(match maybe_data {
141                Some(data) => bincode::deserialize(&data).map(Some).map_unexpected()?,
142                None => None,
143            })
144        } else {
145            Ok(None)
146        }
147    }
148
149    pub async fn maybe_size(&self, id: Uuid, hmac: Option<DocumentHmac>) -> LbResult<Option<u64>> {
150        match hmac {
151            Some(hmac) => {
152                let path_str = key_path(&self.location, id, hmac);
153                let path = Path::new(&path_str);
154                Ok(path.metadata().ok().map(|meta| meta.len()))
155            }
156            None => Ok(None),
157        }
158    }
159
160    pub async fn delete(&self, id: Uuid, hmac: Option<DocumentHmac>) -> LbResult<()> {
161        if let Some(hmac) = hmac {
162            let path_str = key_path(&self.location, id, hmac);
163            let path = Path::new(&path_str);
164            trace!("delete\t{}", &path_str);
165            if path.exists() {
166                fs::remove_file(path).await.map_unexpected()?;
167            }
168        }
169
170        Ok(())
171    }
172
173    pub(crate) async fn retain(&self, file_hmacs: HashSet<(Uuid, [u8; 32])>) -> LbResult<()> {
174        let dir_path = namespace_path(&self.location);
175        fs::create_dir_all(&dir_path).await?;
176        let mut entries = fs::read_dir(&dir_path).await?;
177
178        while let Some(entry) = entries.next_entry().await? {
179            let path = entry.path();
180            let file_name = path
181                .file_name()
182                .and_then(|name| name.to_str())
183                .ok_or(LbErrKind::Unexpected("could not get filename from os".to_string()))?;
184
185            if file_name.contains("pending") {
186                continue;
187            }
188
189            let (id_str, hmac_str) = file_name.split_at(36); // UUIDs are 36 characters long in string form
190
191            let id = Uuid::parse_str(id_str).map_err(|err| {
192                LbErrKind::Unexpected(format!("could not parse doc name as uuid {err:?}"))
193            })?;
194
195            let hmac_base64 = hmac_str
196                .strip_prefix('-')
197                .ok_or(LbErrKind::Unexpected("doc name missing -".to_string()))?;
198
199            let hmac_bytes =
200                base64::decode_config(hmac_base64, base64::URL_SAFE).map_err(|err| {
201                    LbErrKind::Unexpected(format!("document disk file name malformed: {err:?}"))
202                })?;
203
204            let hmac: DocumentHmac = hmac_bytes.try_into().map_err(|err| {
205                LbErrKind::Unexpected(format!("document disk file name malformed {err:?}"))
206            })?;
207
208            if !file_hmacs.contains(&(id, hmac)) {
209                self.delete(id, Some(hmac)).await?;
210            }
211        }
212        Ok(())
213    }
214}
215
216pub fn namespace_path(writeable_path: &Path) -> String {
217    format!("{}/documents", writeable_path.to_str().unwrap())
218}
219
220pub fn key_path(writeable_path: &Path, key: Uuid, hmac: DocumentHmac) -> String {
221    let hmac = base64::encode_config(hmac, base64::URL_SAFE);
222    format!("{}/{}-{}", namespace_path(writeable_path), key, hmac)
223}
224
225fn pending_path(writeable_path: &Path, key: Uuid, hmac: DocumentHmac) -> String {
226    key_path(writeable_path, key, hmac) + ".pending"
227}
228
229impl From<&Config> for AsyncDocs {
230    fn from(cfg: &Config) -> Self {
231        Self { location: PathBuf::from(&cfg.writeable_path) }
232    }
233}