blockworx_store/storage/
native.rs1use std::io::Write as _;
9use std::path::{Path, PathBuf};
10
11use super::{Entry, Name, Residency, Storage};
12
13#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct Native(PathBuf);
16
17impl Native {
18 pub fn at(root: impl Into<PathBuf>) -> Self {
19 Self(root.into())
20 }
21
22 pub fn root(&self) -> &Path {
23 &self.0
24 }
25
26 fn path(&self, at: &Entry) -> PathBuf {
27 if at.as_str().is_empty() {
28 return self.0.clone();
29 }
30 self.0.join(at.as_str())
31 }
32
33 fn holding(&self, at: &Entry) -> std::io::Result<PathBuf> {
37 let path = self.path(at);
38 if let Some(dir) = path.parent() {
39 std::fs::create_dir_all(dir)?;
40 }
41 Ok(path)
42 }
43}
44
45pub fn documents_dir() -> Option<PathBuf> {
51 dirs::document_dir()
52 .or_else(dirs::home_dir)
53 .or_else(|| std::env::current_dir().ok())
54}
55
56impl Storage for Native {
57 fn name(&self) -> Name {
58 self.0
59 .file_name()
60 .and_then(|name| Name::new(&name.to_string_lossy()))
61 .unwrap_or_else(|| Name(self.0.display().to_string()))
62 }
63
64 fn names(&self, at: &Entry) -> String {
65 self.path(at).display().to_string()
66 }
67
68 fn residency(&self) -> Residency {
69 Residency::Lazy
70 }
71
72 fn disk_path(&self) -> Option<&Path> {
73 Some(&self.0)
74 }
75
76 async fn read<'a>(&'a self, at: &'a Entry) -> std::io::Result<Option<Vec<u8>>> {
77 match std::fs::read(self.path(at)) {
78 Ok(bytes) => Ok(Some(bytes)),
79 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
80 Err(error) => Err(error),
81 }
82 }
83
84 async fn exists<'a>(&'a self, at: &'a Entry) -> std::io::Result<bool> {
85 Ok(self.path(at).exists())
86 }
87
88 async fn write<'a>(&'a self, at: &'a Entry, bytes: &'a [u8]) -> std::io::Result<()> {
89 crate::atomic::write_atomically(&self.holding(at)?, bytes)
90 }
91
92 async fn append<'a>(&'a self, at: &'a Entry, bytes: &'a [u8]) -> std::io::Result<()> {
96 let mut file = std::fs::OpenOptions::new()
97 .append(true)
98 .create(true)
99 .open(self.holding(at)?)?;
100 file.write_all(bytes)?;
101 file.sync_data()
102 }
103
104 async fn truncate<'a>(&'a self, at: &'a Entry, len: u64) -> std::io::Result<()> {
105 let file = std::fs::OpenOptions::new()
106 .append(true)
107 .open(self.path(at))?;
108 file.set_len(len)?;
109 file.sync_all()
110 }
111
112 async fn list<'a>(&'a self, dir: &'a Entry) -> std::io::Result<Vec<String>> {
113 let mut names = Vec::new();
114 for entry in std::fs::read_dir(self.path(dir))? {
115 names.push(entry?.file_name().to_string_lossy().into_owned());
116 }
117 Ok(names)
118 }
119
120 async fn remove<'a>(&'a self, at: &'a Entry) -> std::io::Result<()> {
121 let path = self.path(at);
122 if path.is_dir() {
123 return std::fs::remove_dir(path);
124 }
125 std::fs::remove_file(path)
126 }
127
128 async fn create_dir<'a>(&'a self, dir: &'a Entry) -> std::io::Result<()> {
129 std::fs::create_dir_all(self.path(dir))
130 }
131
132 async fn rename<'a>(&'a mut self, to: &'a Name) -> std::io::Result<()> {
140 let Some(parent) = self.0.parent() else {
141 return Err(std::io::Error::new(
142 std::io::ErrorKind::InvalidInput,
143 format!("{} has nowhere to be renamed in", self.0.display()),
144 ));
145 };
146 let moved = parent.join(to.as_str());
147 if moved.exists() {
148 return Err(std::io::Error::new(
149 std::io::ErrorKind::AlreadyExists,
150 format!("{} already exists", moved.display()),
151 ));
152 }
153 std::fs::rename(&self.0, &moved)?;
154 self.0 = moved;
155 Ok(())
156 }
157
158 async fn discard(&self) -> std::io::Result<()> {
159 std::fs::remove_dir(&self.0)
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::storage::ready_now;
167 use crate::temp::TempDir;
168
169 const NOTES: Entry = Entry::fixed("notes.txt");
170
171 fn storage(dir: &TempDir) -> Native {
172 Native::at(dir.join("doc.bwx"))
173 }
174
175 #[test]
176 fn what_is_written_reads_back_and_what_was_never_written_is_nothing() {
177 let dir = TempDir::new("native-round-trip");
178 let native = storage(&dir);
179 assert_eq!(
180 ready_now(native.read(&NOTES)).expect("the read answers"),
181 None
182 );
183
184 ready_now(native.write(&NOTES, b"first")).expect("the write lands");
185 assert_eq!(
186 ready_now(native.read(&NOTES)).expect("it reads back"),
187 Some(b"first".to_vec()),
188 "the directory was not made by the write that needed it",
189 );
190 ready_now(native.write(&NOTES, b"second")).expect("the rewrite lands");
191 assert_eq!(
192 ready_now(native.read(&NOTES)).expect("it reads back"),
193 Some(b"second".to_vec()),
194 );
195 assert!(ready_now(native.exists(&NOTES)).expect("the look answers"));
196 }
197
198 #[test]
199 fn an_append_adds_to_the_end_and_a_truncate_cuts_it_back() {
200 let dir = TempDir::new("native-append");
201 let native = storage(&dir);
202 ready_now(native.append(&NOTES, b"one\n")).expect("the first append");
203 ready_now(native.append(&NOTES, b"two\n")).expect("the second append");
204 assert_eq!(
205 ready_now(native.read(&NOTES)).expect("it reads back"),
206 Some(b"one\ntwo\n".to_vec()),
207 );
208
209 ready_now(native.truncate(&NOTES, 4)).expect("the truncate");
210 assert_eq!(
211 ready_now(native.read(&NOTES)).expect("it reads back"),
212 Some(b"one\n".to_vec()),
213 );
214 }
215
216 #[test]
217 fn a_container_is_named_by_its_directory_and_renamed_beside_it() {
218 let dir = TempDir::new("native-rename");
219 let mut native = storage(&dir);
220 ready_now(native.create_dir(&Entry::ROOT)).expect("the container directory");
221 ready_now(native.write(&NOTES, b"kept")).expect("something in it");
222 assert_eq!(native.name().to_string(), "doc.bwx");
223
224 let to = Name::of_document("engine").expect("a name");
225 ready_now(native.rename(&to)).expect("the container moves");
226
227 assert_eq!(native.root(), dir.join("engine.bwx"));
228 assert!(!dir.join("doc.bwx").exists(), "the old name still stands");
229 assert_eq!(
230 ready_now(native.read(&NOTES)).expect("it reads back"),
231 Some(b"kept".to_vec()),
232 "the entries did not travel with the container",
233 );
234 assert_eq!(
235 ready_now(native.rename(&Name::of_document("engine").expect("a name")))
236 .expect_err("renaming onto itself is refused")
237 .kind(),
238 std::io::ErrorKind::AlreadyExists,
239 );
240 }
241
242 #[test]
243 fn a_container_lists_what_is_in_it_and_is_discarded_once_it_is_empty() {
244 let dir = TempDir::new("native-list");
245 let native = storage(&dir);
246 ready_now(native.create_dir(&Entry::ROOT)).expect("the container directory");
247 ready_now(native.write(&NOTES, b"kept")).expect("something in it");
248 assert_eq!(
249 ready_now(native.list(&Entry::ROOT)).expect("it lists"),
250 ["notes.txt"],
251 );
252
253 assert!(
254 ready_now(native.discard()).is_err(),
255 "a container with something in it was removed",
256 );
257 ready_now(native.remove(&NOTES)).expect("the entry goes");
258 ready_now(native.discard()).expect("the empty container goes");
259 assert!(!native.root().exists());
260 }
261}