lb_rs/service/
documents.rs

1use std::collections::HashSet;
2use std::sync::atomic::Ordering;
3
4use crate::Lb;
5use crate::model::clock::get_time;
6use crate::model::crypto::DecryptedDocument;
7use crate::model::errors::{LbErrKind, LbResult};
8use crate::model::file_like::FileLike;
9use crate::model::file_metadata::{DocumentHmac, FileType};
10use crate::model::lazy::LazyTree;
11use crate::model::signed_meta::SignedMeta;
12use crate::model::tree_like::TreeLike;
13use crate::model::validate;
14use uuid::Uuid;
15
16use super::activity;
17use super::events::Actor;
18
19impl Lb {
20    #[instrument(level = "debug", skip(self), err(Debug))]
21    pub async fn read_document(
22        &self, id: Uuid, user_activity: bool,
23    ) -> LbResult<DecryptedDocument> {
24        let tx = self.ro_tx().await;
25        let db = tx.db();
26
27        let mut tree = (&db.base_metadata).to_staged(&db.local_metadata).to_lazy();
28
29        let doc = self.read_document_helper(id, &mut tree).await?;
30
31        drop(tx);
32
33        if user_activity {
34            self.add_doc_event(activity::DocEvent::Read(id, get_time().0))
35                .await?;
36        }
37
38        Ok(doc)
39    }
40
41    #[instrument(level = "debug", skip(self, content), err(Debug))]
42    pub async fn write_document(&self, id: Uuid, content: &[u8]) -> LbResult<()> {
43        let mut tx = self.begin_tx().await;
44        let db = tx.db();
45
46        let local_hmac = db
47            .local_metadata
48            .get()
49            .get(&id)
50            .and_then(|m| m.document_hmac())
51            .copied();
52        let base_hmac = db
53            .base_metadata
54            .get()
55            .get(&id)
56            .and_then(|m| m.document_hmac())
57            .copied();
58
59        let hmac_to_cleanup = if base_hmac != local_hmac { local_hmac } else { None };
60
61        let mut tree = (&db.base_metadata)
62            .to_staged(&mut db.local_metadata)
63            .to_lazy();
64
65        let id = match tree.find(&id)?.file_type() {
66            FileType::Document | FileType::Folder => id,
67            FileType::Link { target } => target,
68        };
69        let encrypted_document = tree.update_document(&id, content, &self.keychain)?;
70        let hmac = tree.find(&id)?.document_hmac().copied();
71        self.docs.insert(id, hmac, &encrypted_document).await?;
72        if hmac != hmac_to_cleanup {
73            self.docs.delete(id, hmac_to_cleanup).await?;
74        }
75        tx.end();
76
77        self.events.doc_written(id, None);
78        self.add_doc_event(activity::DocEvent::Write(id, get_time().0))
79            .await?;
80
81        Ok(())
82    }
83
84    #[instrument(level = "debug", skip(self), err(Debug))]
85    pub async fn read_document_with_hmac(
86        &self, id: Uuid, user_activity: bool,
87    ) -> LbResult<(Option<DocumentHmac>, DecryptedDocument)> {
88        let tx = self.ro_tx().await;
89        let db = tx.db();
90
91        let mut tree = (&db.base_metadata).to_staged(&db.local_metadata).to_lazy();
92
93        let doc = self.read_document_helper(id, &mut tree).await?;
94        let hmac = tree.find(&id)?.document_hmac().copied();
95        drop(tx);
96
97        if user_activity {
98            self.add_doc_event(activity::DocEvent::Read(id, get_time().0))
99                .await?;
100        }
101
102        Ok((hmac, doc))
103    }
104
105    #[instrument(level = "debug", skip(self, content), err(Debug))]
106    pub async fn safe_write(
107        &self, id: Uuid, old_hmac: Option<DocumentHmac>, content: Vec<u8>,
108    ) -> LbResult<DocumentHmac> {
109        let mut tx = self.begin_tx().await;
110        let db = tx.db();
111
112        let local_hmac = db
113            .local_metadata
114            .get()
115            .get(&id)
116            .and_then(|m| m.document_hmac())
117            .copied();
118        let base_hmac = db
119            .base_metadata
120            .get()
121            .get(&id)
122            .and_then(|m| m.document_hmac())
123            .copied();
124
125        let hmac_to_cleanup = if base_hmac != local_hmac { local_hmac } else { None };
126
127        let mut tree = (&db.base_metadata)
128            .to_staged(&mut db.local_metadata)
129            .to_lazy();
130
131        let file = tree.find(&id)?;
132        if file.document_hmac() != old_hmac.as_ref() {
133            return Err(LbErrKind::ReReadRequired.into());
134        }
135        let id = match file.file_type() {
136            FileType::Document | FileType::Folder => id,
137            FileType::Link { target } => target,
138        };
139        // todo can we not borrow here?
140        let encrypted_document = tree.update_document(&id, &content, &self.keychain)?;
141        let hmac = tree.find(&id)?.document_hmac();
142        let hmac = *hmac.ok_or_else(|| {
143            LbErrKind::Unexpected(format!("hmac missing for a document we just wrote {id}"))
144        })?;
145        self.docs
146            .insert(id, Some(hmac), &encrypted_document)
147            .await?;
148        if Some(hmac) != hmac_to_cleanup {
149            self.docs.delete(id, hmac_to_cleanup).await?;
150        }
151        tx.end();
152
153        // todo: when workspace isn't the only writer, this arg needs to be exposed
154        // this will happen when lb-fs is integrated into an app and shares an lb-rs with ws
155        // or it will happen when there are multiple co-operative core processes.
156        self.events.doc_written(id, Some(Actor::Workspace));
157        self.add_doc_event(activity::DocEvent::Write(id, get_time().0))
158            .await?;
159
160        Ok(hmac)
161    }
162
163    pub(crate) async fn cleanup(&self) -> LbResult<()> {
164        // there is a risk that dont_delete is set to true after we check it
165        if self.docs.dont_delete.load(Ordering::SeqCst) {
166            debug!("skipping doc cleanup due to active sync");
167            return Ok(());
168        }
169
170        let tx = self.ro_tx().await;
171        let db = tx.db();
172
173        let tree = db.base_metadata.stage(&db.local_metadata);
174
175        let base_files = tree.base.all_files()?.into_iter();
176        let local_files = tree.staged.all_files()?.into_iter();
177
178        let file_hmacs = base_files
179            .chain(local_files)
180            .filter_map(|f| f.document_hmac().map(|hmac| (*f.id(), *hmac)))
181            .collect::<HashSet<_>>();
182
183        self.docs.retain(file_hmacs).await?;
184
185        drop(tx);
186
187        Ok(())
188    }
189
190    /// This fn is what will fetch the document remotely if it's not present locally
191    pub(crate) async fn read_document_helper<T>(
192        &self, id: Uuid, tree: &mut LazyTree<T>,
193    ) -> LbResult<DecryptedDocument>
194    where
195        T: TreeLike<F = SignedMeta>,
196    {
197        let file = tree.find(&id)?;
198        validate::is_document(file)?;
199        let hmac = file.document_hmac().copied();
200
201        if tree.calculate_deleted(&id)? {
202            return Err(LbErrKind::FileNonexistent.into());
203        }
204
205        let doc = match hmac {
206            Some(hmac) => {
207                let doc = self.docs.get(id, Some(hmac)).await?;
208                tree.decrypt_document(&id, &doc, &self.keychain)?
209            }
210            None => vec![],
211        };
212
213        Ok(doc)
214    }
215}