lockbook_shared/
document_repo.rs

1use crate::core_config::Config;
2use crate::file_metadata::DocumentHmac;
3use crate::SharedResult;
4use crate::{crypto::*, SharedErrorKind};
5use std::collections::HashSet;
6use std::convert::TryInto;
7use std::fs::{self, File, OpenOptions};
8use std::io::{ErrorKind, Read, Write};
9use std::path::Path;
10use tracing::*;
11use uuid::Uuid;
12
13pub trait DocumentService: Clone + Send + 'static {
14    fn insert(
15        &self, id: &Uuid, hmac: Option<&DocumentHmac>, document: &EncryptedDocument,
16    ) -> SharedResult<()>;
17
18    fn get(&self, id: &Uuid, hmac: Option<&DocumentHmac>) -> SharedResult<EncryptedDocument> {
19        self.maybe_get(id, hmac)?
20            .ok_or_else(|| SharedErrorKind::FileNonexistent.into())
21    }
22
23    fn maybe_get(
24        &self, id: &Uuid, hmac: Option<&DocumentHmac>,
25    ) -> SharedResult<Option<EncryptedDocument>>;
26
27    fn delete(&self, id: &Uuid, hmac: Option<&DocumentHmac>) -> SharedResult<()>;
28
29    fn retain(&self, file_hmacs: HashSet<(&Uuid, &DocumentHmac)>) -> SharedResult<()>;
30}
31
32#[derive(Clone)]
33pub struct OnDiskDocuments {
34    config: Config,
35}
36
37impl From<&Config> for OnDiskDocuments {
38    fn from(value: &Config) -> Self {
39        Self { config: value.clone() }
40    }
41}
42
43impl DocumentService for OnDiskDocuments {
44    fn insert(
45        &self, id: &Uuid, hmac: Option<&DocumentHmac>, document: &EncryptedDocument,
46    ) -> SharedResult<()> {
47        if let Some(hmac) = hmac {
48            let value = &bincode::serialize(document)?;
49            let path_str = key_path(&self.config.writeable_path, id, hmac) + ".pending";
50            let path = Path::new(&path_str);
51            trace!("write\t{} {:?} bytes", &path_str, value.len());
52            fs::create_dir_all(path.parent().unwrap())?;
53            let mut f = OpenOptions::new()
54                .write(true)
55                .create(true)
56                .truncate(true)
57                .open(path)?;
58            f.write_all(value)?;
59            Ok(fs::rename(path, key_path(&self.config.writeable_path, id, hmac))?)
60        } else {
61            Ok(())
62        }
63    }
64
65    #[instrument(level = "debug", skip(self), err(Debug))]
66    fn get(&self, id: &Uuid, hmac: Option<&DocumentHmac>) -> SharedResult<EncryptedDocument> {
67        self.maybe_get(id, hmac)?
68            .ok_or_else(|| SharedErrorKind::FileNonexistent.into())
69    }
70
71    #[instrument(level = "debug", skip(self), err(Debug))]
72    fn maybe_get(
73        &self, id: &Uuid, hmac: Option<&DocumentHmac>,
74    ) -> SharedResult<Option<EncryptedDocument>> {
75        if let Some(hmac) = hmac {
76            let path_str = key_path(&self.config.writeable_path, id, hmac);
77            let path = Path::new(&path_str);
78            trace!("read\t{}", &path_str);
79            let maybe_data: Option<Vec<u8>> = match File::open(path) {
80                Ok(mut f) => {
81                    let mut buffer: Vec<u8> = Vec::new();
82                    f.read_to_end(&mut buffer)?;
83                    Some(buffer)
84                }
85                Err(err) => match err.kind() {
86                    ErrorKind::NotFound => None,
87                    _ => return Err(err.into()),
88                },
89            };
90
91            Ok(match maybe_data {
92                Some(data) => bincode::deserialize(&data).map(Some)?,
93                None => None,
94            })
95        } else {
96            Ok(None)
97        }
98    }
99
100    fn delete(&self, id: &Uuid, hmac: Option<&DocumentHmac>) -> SharedResult<()> {
101        if let Some(hmac) = hmac {
102            let path_str = key_path(&self.config.writeable_path, id, hmac);
103            let path = Path::new(&path_str);
104            trace!("delete\t{}", &path_str);
105            if path.exists() {
106                fs::remove_file(path)?;
107            }
108        }
109
110        Ok(())
111    }
112
113    fn retain(&self, file_hmacs: HashSet<(&Uuid, &DocumentHmac)>) -> SharedResult<()> {
114        let dir_path = namespace_path(&self.config.writeable_path);
115        fs::create_dir_all(&dir_path)?;
116        let entries = fs::read_dir(&dir_path)?;
117        for entry in entries {
118            let path = entry?.path();
119            let (id_str, hmac_str) = path
120                .file_name()
121                .and_then(|name| name.to_str())
122                .ok_or(SharedErrorKind::Unexpected("document disk file name malformed"))?
123                .split_at(36); // Uuid's are 36 characters long in string form
124            let id = Uuid::parse_str(id_str)
125                .map_err(|_| SharedErrorKind::Unexpected("document disk file name malformed"))?;
126            let hmac: DocumentHmac = base64::decode_config(
127                hmac_str
128                    .strip_prefix('-')
129                    .ok_or(SharedErrorKind::Unexpected("document disk file name malformed"))?,
130                base64::URL_SAFE,
131            )
132            .map_err(|_| SharedErrorKind::Unexpected("document disk file name malformed"))?
133            .try_into()
134            .map_err(|_| SharedErrorKind::Unexpected("document disk file name malformed"))?;
135            if !file_hmacs.contains(&(&id, &hmac)) {
136                self.delete(&id, Some(&hmac))?;
137            }
138        }
139
140        Ok(())
141    }
142}
143
144pub fn namespace_path(writeable_path: &str) -> String {
145    format!("{}/documents", writeable_path)
146}
147
148pub fn key_path(writeable_path: &str, key: &Uuid, hmac: &DocumentHmac) -> String {
149    let hmac = base64::encode_config(hmac, base64::URL_SAFE);
150    format!("{}/{}-{}", namespace_path(writeable_path), key, hmac)
151}