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