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