omgbase_store/
doc_store.rs1use std::collections::BTreeMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11use crate::error::{Error, Result};
12
13pub trait DocStore {
16 fn exists(&self, path: &str) -> bool;
18 fn read(&self, path: &str) -> Result<Option<String>>;
20 fn write(&mut self, path: &str, bytes: &str) -> Result<()>;
22 fn rename(&mut self, from: &str, to: &str) -> Result<()>;
24 fn remove(&mut self, path: &str) -> Result<()>;
26}
27
28#[derive(Clone, Debug, Default, PartialEq, Eq)]
30pub struct MemDocStore {
31 files: BTreeMap<String, String>,
32}
33
34impl MemDocStore {
35 #[must_use]
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 #[must_use]
42 pub fn files(&self) -> &BTreeMap<String, String> {
43 &self.files
44 }
45}
46
47impl DocStore for MemDocStore {
48 fn exists(&self, path: &str) -> bool {
49 self.files.contains_key(path)
50 }
51
52 fn read(&self, path: &str) -> Result<Option<String>> {
53 Ok(self.files.get(path).cloned())
54 }
55
56 fn write(&mut self, path: &str, bytes: &str) -> Result<()> {
57 self.files.insert(path.to_owned(), bytes.to_owned());
58 Ok(())
59 }
60
61 fn rename(&mut self, from: &str, to: &str) -> Result<()> {
62 let bytes = self
63 .files
64 .remove(from)
65 .ok_or_else(|| Error::Other(format!("rename: no file at {from}")))?;
66 self.files.insert(to.to_owned(), bytes);
67 Ok(())
68 }
69
70 fn remove(&mut self, path: &str) -> Result<()> {
71 self.files.remove(path);
72 Ok(())
73 }
74}
75
76#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct FsDocStore {
80 root: PathBuf,
81}
82
83impl FsDocStore {
84 #[must_use]
85 pub fn new(root: impl Into<PathBuf>) -> Self {
86 Self { root: root.into() }
87 }
88
89 #[must_use]
91 pub fn root(&self) -> &Path {
92 &self.root
93 }
94
95 fn abs(&self, path: &str) -> PathBuf {
96 self.root.join(path)
97 }
98}
99
100fn io_err(what: &str, path: &Path, e: std::io::Error) -> Error {
101 Error::Other(format!("{what} {}: {e}", path.display()))
102}
103
104fn ensure_parent(abs: &Path) -> Result<()> {
105 if let Some(dir) = abs.parent().filter(|d| !d.as_os_str().is_empty()) {
106 fs::create_dir_all(dir).map_err(|e| io_err("cannot create", dir, e))?;
107 }
108 Ok(())
109}
110
111impl DocStore for FsDocStore {
112 fn exists(&self, path: &str) -> bool {
113 self.abs(path).exists()
114 }
115
116 fn read(&self, path: &str) -> Result<Option<String>> {
117 let abs = self.abs(path);
118 if !abs.exists() {
119 return Ok(None);
120 }
121 let bytes = fs::read(&abs).map_err(|e| io_err("cannot read", &abs, e))?;
122 Ok(Some(String::from_utf8_lossy(&bytes).into_owned()))
123 }
124
125 fn write(&mut self, path: &str, bytes: &str) -> Result<()> {
126 let abs = self.abs(path);
127 ensure_parent(&abs)?;
128 let mut tmp = abs.clone().into_os_string();
129 tmp.push(".omgtmp");
130 let tmp = PathBuf::from(tmp);
131 fs::write(&tmp, bytes).map_err(|e| io_err("cannot write", &tmp, e))?;
132 fs::rename(&tmp, &abs).map_err(|e| io_err("cannot rename into", &abs, e))
133 }
134
135 fn rename(&mut self, from: &str, to: &str) -> Result<()> {
136 let to_abs = self.abs(to);
137 ensure_parent(&to_abs)?;
138 fs::rename(self.abs(from), &to_abs).map_err(|e| io_err("cannot rename to", &to_abs, e))
139 }
140
141 fn remove(&mut self, path: &str) -> Result<()> {
142 let abs = self.abs(path);
143 if abs.exists() {
144 fs::remove_file(&abs).map_err(|e| io_err("cannot remove", &abs, e))?;
145 }
146 Ok(())
147 }
148}
149
150#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
153pub struct NullDocStore;
154
155impl DocStore for NullDocStore {
156 fn exists(&self, _path: &str) -> bool {
157 false
158 }
159
160 fn read(&self, _path: &str) -> Result<Option<String>> {
161 Ok(None)
162 }
163
164 fn write(&mut self, _path: &str, _bytes: &str) -> Result<()> {
165 Ok(())
166 }
167
168 fn rename(&mut self, _from: &str, _to: &str) -> Result<()> {
169 Ok(())
170 }
171
172 fn remove(&mut self, _path: &str) -> Result<()> {
173 Ok(())
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn mem_store_round_trips_and_renames() {
183 let mut m = MemDocStore::new();
184 assert!(!m.exists("a.md"));
185 m.write("a.md", "x\n").unwrap();
186 assert_eq!(m.read("a.md").unwrap().as_deref(), Some("x\n"));
187 m.rename("a.md", "dir/b.md").unwrap();
188 assert!(!m.exists("a.md") && m.exists("dir/b.md"));
189 assert!(m.rename("nope", "x").is_err());
190 m.remove("dir/b.md").unwrap();
191 m.remove("dir/b.md").unwrap();
192 assert!(m.files().is_empty());
193 }
194
195 #[test]
196 fn fs_store_writes_atomically_into_nested_dirs() {
197 let dir = std::env::temp_dir().join(format!(
198 "omgbase-fs-store-{}-{}",
199 std::process::id(),
200 std::time::SystemTime::now()
201 .duration_since(std::time::UNIX_EPOCH)
202 .map(|d| d.as_nanos())
203 .unwrap_or(0)
204 ));
205 let mut s = FsDocStore::new(&dir);
206 assert_eq!(s.root(), dir.as_path());
207 s.write("a/b/c.md", "hello\n").unwrap();
208 assert!(s.exists("a/b/c.md"));
209 assert!(!dir.join("a/b/c.md.omgtmp").exists());
210 assert_eq!(s.read("a/b/c.md").unwrap().as_deref(), Some("hello\n"));
211 s.rename("a/b/c.md", "d/e.md").unwrap();
212 assert!(!s.exists("a/b/c.md"));
213 assert_eq!(s.read("d/e.md").unwrap().as_deref(), Some("hello\n"));
214 s.remove("d/e.md").unwrap();
215 assert_eq!(s.read("d/e.md").unwrap(), None);
216 let _ = fs::remove_dir_all(&dir);
217 }
218
219 #[test]
220 fn null_store_does_nothing() {
221 let mut n = NullDocStore;
222 n.write("a.md", "x").unwrap();
223 assert!(!n.exists("a.md"));
224 assert_eq!(n.read("a.md").unwrap(), None);
225 n.rename("a.md", "b.md").unwrap();
226 n.remove("b.md").unwrap();
227 }
228}