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