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 get(&self, _id: Uuid, _hmac: Option<DocumentHmac>) -> LbResult<EncryptedDocument> {
37 Err(LbErrKind::FileNonexistent.into())
38 }
39
40 pub async fn maybe_get(
41 &self, _id: Uuid, _hmac: Option<DocumentHmac>,
42 ) -> LbResult<Option<EncryptedDocument>> {
43 Ok(None)
44 }
45
46 pub async fn delete(&self, _id: Uuid, _hmac: Option<DocumentHmac>) -> LbResult<()> {
47 Ok(())
48 }
49
50 pub fn exists(&self, id: Uuid, hmac: Option<DocumentHmac>) -> bool {
51 true
52 }
53
54 pub(crate) async fn retain(&self, _file_hmacs: HashSet<(Uuid, [u8; 32])>) -> LbResult<()> {
55 Ok(())
56 }
57}
58
59#[cfg(not(target_family = "wasm"))]
60impl AsyncDocs {
61 pub async fn insert(
62 &self, id: Uuid, hmac: Option<DocumentHmac>, document: &EncryptedDocument,
63 ) -> LbResult<()> {
64 if let Some(hmac) = hmac {
65 let value = &bincode::serialize(document).map_unexpected()?;
66 let path_str = key_path(&self.location, id, hmac) + ".pending";
67 let path = Path::new(&path_str);
68 trace!("write\t{} {:?} bytes", &path_str, value.len());
69 fs::create_dir_all(path.parent().unwrap()).await?;
70 let mut f = OpenOptions::new()
71 .write(true)
72 .create(true)
73 .truncate(true)
74 .open(path)
75 .await?;
76 f.write_all(value).await?;
77 Ok(fs::rename(path, key_path(&self.location, id, hmac)).await?)
78 } else {
79 Ok(())
80 }
81 }
82
83 pub fn exists(&self, id: Uuid, hmac: Option<DocumentHmac>) -> bool {
84 if let Some(hmac) = hmac {
85 let path_str = key_path(&self.location, id, hmac);
86 let path = Path::new(&path_str);
87 path.exists()
88 } else {
89 false }
91 }
92
93 pub async fn get(&self, id: Uuid, hmac: Option<DocumentHmac>) -> LbResult<EncryptedDocument> {
94 self.maybe_get(id, hmac)
95 .await?
96 .ok_or_else(|| LbErrKind::FileNonexistent.into())
97 }
98
99 pub async fn maybe_get(
100 &self, id: Uuid, hmac: Option<DocumentHmac>,
101 ) -> LbResult<Option<EncryptedDocument>> {
102 if let Some(hmac) = hmac {
103 let path_str = key_path(&self.location, id, hmac);
104 let path = Path::new(&path_str);
105 trace!("read\t{}", &path_str);
106 let maybe_data: Option<Vec<u8>> = match File::open(path).await {
107 Ok(mut f) => {
108 let mut buffer: Vec<u8> = Vec::new();
109 f.read_to_end(&mut buffer).await?;
110 Some(buffer)
111 }
112 Err(err) => match err.kind() {
113 ErrorKind::NotFound => None,
114 _ => return Err(err.into()),
115 },
116 };
117
118 Ok(match maybe_data {
119 Some(data) => bincode::deserialize(&data).map(Some).map_unexpected()?,
120 None => None,
121 })
122 } else {
123 Ok(None)
124 }
125 }
126
127 pub async fn maybe_size(&self, id: Uuid, hmac: Option<DocumentHmac>) -> LbResult<Option<u64>> {
128 match hmac {
129 Some(hmac) => {
130 let path_str = key_path(&self.location, id, hmac);
131 let path = Path::new(&path_str);
132 Ok(path.metadata().ok().map(|meta| meta.len()))
133 }
134 None => Ok(None),
135 }
136 }
137
138 pub async fn delete(&self, id: Uuid, hmac: Option<DocumentHmac>) -> LbResult<()> {
139 if let Some(hmac) = hmac {
140 let path_str = key_path(&self.location, id, hmac);
141 let path = Path::new(&path_str);
142 trace!("delete\t{}", &path_str);
143 if path.exists() {
144 fs::remove_file(path).await.map_unexpected()?;
145 }
146 }
147
148 Ok(())
149 }
150
151 pub(crate) async fn retain(&self, file_hmacs: HashSet<(Uuid, [u8; 32])>) -> LbResult<()> {
152 let dir_path = namespace_path(&self.location);
153 fs::create_dir_all(&dir_path).await?;
154 let mut entries = fs::read_dir(&dir_path).await?;
155
156 while let Some(entry) = entries.next_entry().await? {
157 let path = entry.path();
158 let file_name = path
159 .file_name()
160 .and_then(|name| name.to_str())
161 .ok_or(LbErrKind::Unexpected("could not get filename from os".to_string()))?;
162
163 if file_name.contains("pending") {
164 continue;
165 }
166
167 let (id_str, hmac_str) = file_name.split_at(36); let id = Uuid::parse_str(id_str).map_err(|err| {
170 LbErrKind::Unexpected(format!("could not parse doc name as uuid {err:?}"))
171 })?;
172
173 let hmac_base64 = hmac_str
174 .strip_prefix('-')
175 .ok_or(LbErrKind::Unexpected("doc name missing -".to_string()))?;
176
177 let hmac_bytes =
178 base64::decode_config(hmac_base64, base64::URL_SAFE).map_err(|err| {
179 LbErrKind::Unexpected(format!("document disk file name malformed: {err:?}"))
180 })?;
181
182 let hmac: DocumentHmac = hmac_bytes.try_into().map_err(|err| {
183 LbErrKind::Unexpected(format!("document disk file name malformed {err:?}"))
184 })?;
185
186 if !file_hmacs.contains(&(id, hmac)) {
187 self.delete(id, Some(hmac)).await?;
188 }
189 }
190 Ok(())
191 }
192}
193
194pub fn namespace_path(writeable_path: &Path) -> String {
195 format!("{}/documents", writeable_path.to_str().unwrap())
196}
197
198pub fn key_path(writeable_path: &Path, key: Uuid, hmac: DocumentHmac) -> String {
199 let hmac = base64::encode_config(hmac, base64::URL_SAFE);
200 format!("{}/{}-{}", namespace_path(writeable_path), key, hmac)
201}
202
203impl From<&Config> for AsyncDocs {
204 fn from(cfg: &Config) -> Self {
205 Self { location: PathBuf::from(&cfg.writeable_path) }
206 }
207}