1use crate::model::{
2 core_config::Config,
3 crypto::EncryptedDocument,
4 errors::{LbErrKind, LbResult},
5 file_metadata::DocumentHmac,
6};
7use std::{
8 collections::HashSet,
9 path::{Path, PathBuf},
10};
11use uuid::Uuid;
12
13#[cfg(not(target_family = "wasm"))]
14use {
15 crate::model::errors::Unexpected,
16 std::io::ErrorKind,
17 tokio::{
18 fs::{self, File, OpenOptions},
19 io::{AsyncReadExt, AsyncWriteExt},
20 },
21};
22
23#[derive(Clone)]
24pub struct AsyncDocs {
25 location: PathBuf,
26}
27
28#[cfg(target_family = "wasm")]
29impl AsyncDocs {
30 pub async fn insert(
31 &self, _id: Uuid, _hmac: Option<DocumentHmac>, _document: &EncryptedDocument,
32 ) -> LbResult<()> {
33 Ok(())
34 }
35
36 pub async fn insert_pending(
37 &self, _id: Uuid, _hmac: DocumentHmac, _document: &EncryptedDocument,
38 ) -> LbResult<()> {
39 Ok(())
40 }
41
42 pub async fn promote_pending(&self, _id: Uuid, _hmac: DocumentHmac) -> LbResult<()> {
43 Ok(())
44 }
45
46 pub async fn get(&self, _id: Uuid, _hmac: Option<DocumentHmac>) -> LbResult<EncryptedDocument> {
47 Err(LbErrKind::FileNonexistent.into())
48 }
49
50 pub async fn maybe_get(
51 &self, _id: Uuid, _hmac: Option<DocumentHmac>,
52 ) -> LbResult<Option<EncryptedDocument>> {
53 Ok(None)
54 }
55
56 pub async fn delete(&self, _id: Uuid, _hmac: Option<DocumentHmac>) -> LbResult<()> {
57 Ok(())
58 }
59
60 pub fn exists(&self, id: Uuid, hmac: Option<DocumentHmac>) -> bool {
61 true
62 }
63
64 pub(crate) async fn retain(&self, _file_hmacs: HashSet<(Uuid, [u8; 32])>) -> LbResult<()> {
65 Ok(())
66 }
67}
68
69#[cfg(not(target_family = "wasm"))]
70impl AsyncDocs {
71 pub async fn insert(
72 &self, id: Uuid, hmac: Option<DocumentHmac>, document: &EncryptedDocument,
73 ) -> LbResult<()> {
74 if let Some(hmac) = hmac {
75 self.insert_pending(id, hmac, document).await?;
76 self.promote_pending(id, hmac).await?;
77 }
78 Ok(())
79 }
80
81 pub async fn insert_pending(
82 &self, id: Uuid, hmac: DocumentHmac, document: &EncryptedDocument,
83 ) -> LbResult<()> {
84 let value = &bincode::serialize(document).map_unexpected()?;
85 let path_str = pending_path(&self.location, id, hmac);
86 let path = Path::new(&path_str);
87 trace!("write pending\t{} {:?} bytes", &path_str, value.len());
88 fs::create_dir_all(path.parent().unwrap()).await?;
89 let mut f = OpenOptions::new()
90 .write(true)
91 .create(true)
92 .truncate(true)
93 .open(path)
94 .await?;
95 f.write_all(value).await?;
96 Ok(())
97 }
98
99 pub async fn promote_pending(&self, id: Uuid, hmac: DocumentHmac) -> LbResult<()> {
100 let pending = pending_path(&self.location, id, hmac);
101 let final_path = key_path(&self.location, id, hmac);
102 Ok(fs::rename(&pending, &final_path).await?)
103 }
104
105 pub fn exists(&self, id: Uuid, hmac: Option<DocumentHmac>) -> bool {
106 if let Some(hmac) = hmac {
107 let path_str = key_path(&self.location, id, hmac);
108 let path = Path::new(&path_str);
109 path.exists()
110 } else {
111 false }
113 }
114
115 pub async fn get(&self, id: Uuid, hmac: Option<DocumentHmac>) -> LbResult<EncryptedDocument> {
116 self.maybe_get(id, hmac)
117 .await?
118 .ok_or_else(|| LbErrKind::FileNonexistent.into())
119 }
120
121 pub async fn maybe_get(
122 &self, id: Uuid, hmac: Option<DocumentHmac>,
123 ) -> LbResult<Option<EncryptedDocument>> {
124 if let Some(hmac) = hmac {
125 let path_str = key_path(&self.location, id, hmac);
126 let path = Path::new(&path_str);
127 trace!("read\t{}", &path_str);
128 let maybe_data: Option<Vec<u8>> = match File::open(path).await {
129 Ok(mut f) => {
130 let mut buffer: Vec<u8> = Vec::new();
131 f.read_to_end(&mut buffer).await?;
132 Some(buffer)
133 }
134 Err(err) => match err.kind() {
135 ErrorKind::NotFound => None,
136 _ => return Err(err.into()),
137 },
138 };
139
140 Ok(match maybe_data {
141 Some(data) => bincode::deserialize(&data).map(Some).map_unexpected()?,
142 None => None,
143 })
144 } else {
145 Ok(None)
146 }
147 }
148
149 pub async fn maybe_size(&self, id: Uuid, hmac: Option<DocumentHmac>) -> LbResult<Option<u64>> {
150 match hmac {
151 Some(hmac) => {
152 let path_str = key_path(&self.location, id, hmac);
153 let path = Path::new(&path_str);
154 Ok(path.metadata().ok().map(|meta| meta.len()))
155 }
156 None => Ok(None),
157 }
158 }
159
160 pub async fn delete(&self, id: Uuid, hmac: Option<DocumentHmac>) -> LbResult<()> {
161 if let Some(hmac) = hmac {
162 let path_str = key_path(&self.location, id, hmac);
163 let path = Path::new(&path_str);
164 trace!("delete\t{}", &path_str);
165 if path.exists() {
166 fs::remove_file(path).await.map_unexpected()?;
167 }
168 }
169
170 Ok(())
171 }
172
173 pub(crate) async fn retain(&self, file_hmacs: HashSet<(Uuid, [u8; 32])>) -> LbResult<()> {
174 let dir_path = namespace_path(&self.location);
175 fs::create_dir_all(&dir_path).await?;
176 let mut entries = fs::read_dir(&dir_path).await?;
177
178 while let Some(entry) = entries.next_entry().await? {
179 let path = entry.path();
180 let file_name = path
181 .file_name()
182 .and_then(|name| name.to_str())
183 .ok_or(LbErrKind::Unexpected("could not get filename from os".to_string()))?;
184
185 if file_name.contains("pending") {
186 continue;
187 }
188
189 let (id_str, hmac_str) = file_name.split_at(36); let id = Uuid::parse_str(id_str).map_err(|err| {
192 LbErrKind::Unexpected(format!("could not parse doc name as uuid {err:?}"))
193 })?;
194
195 let hmac_base64 = hmac_str
196 .strip_prefix('-')
197 .ok_or(LbErrKind::Unexpected("doc name missing -".to_string()))?;
198
199 let hmac_bytes =
200 base64::decode_config(hmac_base64, base64::URL_SAFE).map_err(|err| {
201 LbErrKind::Unexpected(format!("document disk file name malformed: {err:?}"))
202 })?;
203
204 let hmac: DocumentHmac = hmac_bytes.try_into().map_err(|err| {
205 LbErrKind::Unexpected(format!("document disk file name malformed {err:?}"))
206 })?;
207
208 if !file_hmacs.contains(&(id, hmac)) {
209 self.delete(id, Some(hmac)).await?;
210 }
211 }
212 Ok(())
213 }
214}
215
216pub fn namespace_path(writeable_path: &Path) -> String {
217 format!("{}/documents", writeable_path.to_str().unwrap())
218}
219
220pub fn key_path(writeable_path: &Path, key: Uuid, hmac: DocumentHmac) -> String {
221 let hmac = base64::encode_config(hmac, base64::URL_SAFE);
222 format!("{}/{}-{}", namespace_path(writeable_path), key, hmac)
223}
224
225fn pending_path(writeable_path: &Path, key: Uuid, hmac: DocumentHmac) -> String {
226 key_path(writeable_path, key, hmac) + ".pending"
227}
228
229impl From<&Config> for AsyncDocs {
230 fn from(cfg: &Config) -> Self {
231 Self { location: PathBuf::from(&cfg.writeable_path) }
232 }
233}