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