lockbook_shared/
document_repo.rs1use 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); 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}