Skip to main content

blockworx_store/storage/
native.rs

1//! A container that is a directory of real files.
2//!
3//! Every future here is ready the moment it is made, so the store resolves
4//! them with one poll ([`super::ready_now`]) and the write ordering the
5//! container asks for — the payload before the rev, the rev before the row
6//! that names it — is the ordering the disk sees.
7
8use std::io::Write as _;
9use std::path::{Path, PathBuf};
10
11use super::{Entry, Name, Residency, Storage};
12
13/// A container's directory.
14#[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    /// The directory an entry is written into, made if it is not there.
34    /// A container whose `revs/` somebody removed is repaired by the write
35    /// that needs it.
36    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
45/// Where the platform puts a user's documents, falling back to the home
46/// directory and then to the working directory — a machine with no
47/// `Documents` folder still gets a document somewhere it can find it.
48/// `None` where the platform names none of the three, the browser
49/// included.
50pub 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    /// The line and an fsync before returning, so a crash anywhere in here
93    /// loses at most this write — and loses it visibly, since the manifest
94    /// then ends without a newline.
95    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    /// The directory moves; the session carries on through it, because
133    /// every entry is named relative to a root this now points somewhere
134    /// else.
135    ///
136    /// No directory fsync, unlike a fresh container's layout: a rename a
137    /// power cut takes back leaves a whole container under its old name,
138    /// where an interrupted layout would leave half of one.
139    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}