Skip to main content

lockbook_server_lib/
document_service.rs

1use crate::ServerError;
2use crate::config::Config;
3use async_trait::async_trait;
4use lb_rs::model::crypto::EncryptedDocument;
5use lb_rs::model::file_metadata::DocumentHmac;
6use std::collections::HashMap;
7use std::fmt::Debug;
8use std::path::PathBuf;
9use std::sync::{Arc, Mutex};
10use tokio::fs::{File, remove_file};
11use tokio::io::{AsyncReadExt, AsyncWriteExt};
12use uuid::Uuid;
13
14#[async_trait]
15pub trait DocumentService: Send + Sync + Clone + 'static {
16    async fn insert<T: Debug>(
17        &self, id: &Uuid, hmac: &DocumentHmac, content: &EncryptedDocument,
18    ) -> Result<(), ServerError<T>>;
19    async fn get<T: Debug>(
20        &self, id: &Uuid, hmac: &DocumentHmac,
21    ) -> Result<EncryptedDocument, ServerError<T>>;
22    async fn maybe_get<T: Debug>(
23        &self, id: &Uuid, hmac: &DocumentHmac,
24    ) -> Result<Option<EncryptedDocument>, ServerError<T>>;
25    async fn delete<T: Debug>(&self, id: &Uuid, hmac: &DocumentHmac) -> Result<(), ServerError<T>>;
26
27    fn exists(&self, id: &Uuid, hmac: &DocumentHmac) -> bool;
28    fn get_path(&self, id: &Uuid, hmac: &DocumentHmac) -> PathBuf;
29}
30
31#[derive(Clone)]
32pub struct OnDiskDocuments {
33    config: Config,
34}
35
36impl From<&Config> for OnDiskDocuments {
37    fn from(value: &Config) -> Self {
38        Self { config: value.clone() }
39    }
40}
41
42#[async_trait]
43impl DocumentService for OnDiskDocuments {
44    async fn insert<T: Debug>(
45        &self, id: &Uuid, hmac: &DocumentHmac, content: &EncryptedDocument,
46    ) -> Result<(), ServerError<T>> {
47        let content = bincode::serialize(content)?;
48        let path = self.get_path(id, hmac);
49        let mut file = File::create(path.clone()).await?;
50        file.write_all(&content)
51            .await
52            .map_err(|err| internal!("{:?}", err))?;
53        file.flush().await.map_err(|err| internal!("{:?}", err))?;
54        Ok(())
55    }
56
57    async fn get<T: Debug>(
58        &self, id: &Uuid, hmac: &DocumentHmac,
59    ) -> Result<EncryptedDocument, ServerError<T>> {
60        let path = self.get_path(id, hmac);
61        let mut file = File::open(path.clone()).await?;
62        let mut content = vec![];
63        file.read_to_end(&mut content).await?;
64        let content = bincode::deserialize(&content)?;
65        Ok(content)
66    }
67
68    async fn maybe_get<T: Debug>(
69        &self, id: &Uuid, hmac: &DocumentHmac,
70    ) -> Result<Option<EncryptedDocument>, ServerError<T>> {
71        let path = self.get_path(id, hmac);
72        if !path.exists() {
73            return Ok(None);
74        }
75
76        Ok(Some(self.get(id, hmac).await?))
77    }
78
79    async fn delete<T: Debug>(&self, id: &Uuid, hmac: &DocumentHmac) -> Result<(), ServerError<T>> {
80        let path = self.get_path(id, hmac);
81        // I'm not sure this check should exist, the two situations it gets utilized is when we re-delete
82        // an already deleted file and when we move a file from version 0 -> N. Maybe it would be more
83        // efficient for the caller to look at the metadata and make a more informed decision about
84        // whether this needs to be called or not. Perhaps an async version should be used if we do keep
85        // the check.
86        if path.exists() {
87            remove_file(path).await?;
88        }
89        Ok(())
90    }
91
92    fn exists(&self, id: &Uuid, hmac: &DocumentHmac) -> bool {
93        self.get_path(id, hmac).exists()
94    }
95
96    fn get_path(&self, id: &Uuid, hmac: &DocumentHmac) -> PathBuf {
97        let mut path = self.config.files.path.clone();
98        // we may need to truncate this
99        let hmac = base64::encode_config(hmac, base64::URL_SAFE);
100        path.push(format!("{id}-{hmac}"));
101        path
102    }
103}
104
105/// For use with fuzzer, not to be hooked up in prod
106#[derive(Clone, Default)]
107pub struct InMemDocuments {
108    pub docs: Arc<Mutex<HashMap<String, EncryptedDocument>>>,
109}
110
111#[async_trait]
112impl DocumentService for InMemDocuments {
113    async fn insert<T: Debug>(
114        &self, id: &Uuid, hmac: &DocumentHmac, content: &EncryptedDocument,
115    ) -> Result<(), ServerError<T>> {
116        let hmac = base64::encode_config(hmac, base64::URL_SAFE);
117        let key = format!("{id}-{hmac}");
118        self.docs.lock().unwrap().insert(key, content.clone());
119        Ok(())
120    }
121
122    async fn get<T: Debug>(
123        &self, id: &Uuid, hmac: &DocumentHmac,
124    ) -> Result<EncryptedDocument, ServerError<T>> {
125        let hmac = base64::encode_config(hmac, base64::URL_SAFE);
126        let key = format!("{id}-{hmac}");
127        Ok(self.docs.lock().unwrap().get(&key).unwrap().clone())
128    }
129
130    async fn maybe_get<T: Debug>(
131        &self, id: &Uuid, hmac: &DocumentHmac,
132    ) -> Result<Option<EncryptedDocument>, ServerError<T>> {
133        let hmac = base64::encode_config(hmac, base64::URL_SAFE);
134        let key = format!("{id}-{hmac}");
135        Ok(self.docs.lock().unwrap().get(&key).cloned())
136    }
137
138    fn exists(&self, id: &Uuid, hmac: &DocumentHmac) -> bool {
139        let hmac = base64::encode_config(hmac, base64::URL_SAFE);
140        let key = format!("{id}-{hmac}");
141        self.docs.lock().unwrap().contains_key(&key)
142    }
143
144    fn get_path(&self, _id: &Uuid, _hmac: &DocumentHmac) -> PathBuf {
145        unimplemented!()
146    }
147
148    async fn delete<T: Debug>(&self, id: &Uuid, hmac: &DocumentHmac) -> Result<(), ServerError<T>> {
149        let hmac = base64::encode_config(hmac, base64::URL_SAFE);
150        let key = format!("{id}-{hmac}");
151        self.docs.lock().unwrap().remove(&key);
152
153        Ok(())
154    }
155}