1use std::path::{Path, PathBuf};
26
27use kernel::{B3Hash, Payload};
28use serde_json::{Map, Value};
29
30use crate::error::{StorageError, io_err};
31use crate::real_fs::RealFs;
32use crate::vfs::Vfs;
33
34pub struct DigestCache {
35 dir: PathBuf,
36 fs: RealFs,
37}
38
39impl DigestCache {
40 pub fn open(dir: &Path) -> Result<DigestCache, StorageError> {
41 let mut fs = RealFs::new();
42 fs.create_dir_all(dir)
43 .map_err(io_err("create digest cache dir", dir))?;
44 let tmp = dir.join("tmp");
45 fs.create_dir_all(&tmp)
46 .map_err(io_err("create digest cache tmp", &tmp))?;
47 let leftovers = fs.list(&tmp).map_err(io_err("list digest tmp", &tmp))?;
51 for leftover in leftovers {
52 fs.remove_file(&leftover)
53 .map_err(io_err("sweep digest tmp", &leftover))?;
54 }
55 Ok(DigestCache {
56 dir: dir.to_path_buf(),
57 fs,
58 })
59 }
60
61 fn entry_path(&self, content: &B3Hash) -> PathBuf {
62 self.dir.join(format!("{content}.json"))
63 }
64
65 pub fn put(&mut self, content: &B3Hash, tree_json: &[u8]) -> Result<(), StorageError> {
69 let final_path = self.entry_path(content);
70 if self.fs.exists(&final_path) {
71 return Ok(());
72 }
73 let tmp_path = self.dir.join("tmp").join(format!("{content}.part"));
74 if self.fs.exists(&tmp_path) {
75 self.fs
76 .truncate(&tmp_path, 0)
77 .map_err(io_err("reset digest tmp", &tmp_path))?;
78 }
79 self.fs
80 .append(&tmp_path, tree_json)
81 .map_err(io_err("write digest entry", &tmp_path))?;
82 self.fs
83 .sync_data(&tmp_path)
84 .map_err(io_err("sync digest entry", &tmp_path))?;
85 self.fs
86 .rename(&tmp_path, &final_path)
87 .map_err(io_err("publish digest entry", &final_path))?;
88 self.fs
89 .sync_dir(&self.dir)
90 .map_err(io_err("sync digest cache dir", &self.dir))?;
91 Ok(())
92 }
93
94 pub fn get(&self, content: &B3Hash) -> Result<Option<Vec<u8>>, StorageError> {
95 match self.fs.read(&self.entry_path(content)) {
96 Ok(bytes) => Ok(Some(bytes)),
97 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
98 Err(err) => Err(io_err("read digest entry", &self.entry_path(content))(err)),
99 }
100 }
101
102 pub fn invalidate(&mut self, content: &B3Hash, reason: &str) -> Result<Payload, StorageError> {
107 let path = self.entry_path(content);
108 let existed = self.fs.exists(&path);
109 if existed {
110 self.fs
111 .remove_file(&path)
112 .map_err(io_err("remove digest entry", &path))?;
113 }
114 let mut map = Map::new();
115 map.insert("content".to_owned(), Value::String(content.to_string()));
116 map.insert("reason".to_owned(), Value::String(reason.to_owned()));
117 map.insert("existed".to_owned(), Value::Bool(existed));
118 Payload::new(map).map_err(|source| StorageError::Draft { source })
119 }
120}
121
122#[cfg(test)]
123#[allow(
124 clippy::unwrap_used,
125 clippy::expect_used,
126 clippy::panic,
127 clippy::indexing_slicing,
128 reason = "test code"
129)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn the_same_content_digests_once_for_life() {
135 let tmp = tempfile::tempdir().unwrap();
136 let mut cache = DigestCache::open(tmp.path()).unwrap();
137 let content = B3Hash::digest(b"a long document");
138 cache.put(&content, br#"{"tree":"first"}"#).unwrap();
139 cache.put(&content, br#"{"tree":"second"}"#).unwrap();
142 assert_eq!(
143 cache.get(&content).unwrap().as_deref(),
144 Some(&br#"{"tree":"first"}"#[..])
145 );
146 }
147
148 #[test]
149 fn a_miss_is_absence_not_failure() {
150 let tmp = tempfile::tempdir().unwrap();
151 let cache = DigestCache::open(tmp.path()).unwrap();
152 let never = B3Hash::digest(b"never stored");
153 assert_eq!(cache.get(&never).unwrap(), None);
154 }
155
156 #[test]
157 fn invalidation_removes_the_entry_and_says_whether_it_was_there() {
158 let tmp = tempfile::tempdir().unwrap();
159 let mut cache = DigestCache::open(tmp.path()).unwrap();
160 let content = B3Hash::digest(b"doc");
161 cache.put(&content, b"{}").unwrap();
162 let payload = cache.invalidate(&content, "source edited").unwrap();
163 let value = serde_json::to_value(&payload).unwrap();
164 assert_eq!(value["existed"], true);
165 assert_eq!(value["reason"], "source edited");
166 assert_eq!(cache.get(&content).unwrap(), None);
167 let payload = cache.invalidate(&content, "again").unwrap();
169 assert_eq!(serde_json::to_value(&payload).unwrap()["existed"], false);
170 }
171
172 #[test]
173 fn a_crash_between_write_and_publish_leaves_nothing_at_the_final_name() {
174 let tmp = tempfile::tempdir().unwrap();
175 let mut cache = DigestCache::open(tmp.path()).unwrap();
176 let content = B3Hash::digest(b"doc");
177 std::fs::write(
179 tmp.path().join("tmp").join(format!("{content}.part")),
180 b"half",
181 )
182 .unwrap();
183 assert_eq!(
184 cache.get(&content).unwrap(),
185 None,
186 "no half entry is visible"
187 );
188 cache.put(&content, b"{\"tree\":\"ok\"}").unwrap();
191 assert_eq!(
192 cache.get(&content).unwrap().as_deref(),
193 Some(&b"{\"tree\":\"ok\"}"[..])
194 );
195 }
196}