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