daybreak/backend/
path.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5//! Module which implements the [`PathBackend`], storing data in a file on the
6//! file system (with a path) and featuring atomic saves.
7
8use super::Backend;
9use crate::error;
10use std::fs::OpenOptions;
11use std::path::{Path, PathBuf};
12use tempfile::NamedTempFile;
13
14/// A [`Backend`] using a file given the path.
15///
16/// Features atomic saves, so that the database file won't be corrupted or
17/// deleted if the program panics during the save.
18#[derive(Debug)]
19pub struct PathBackend {
20    path: PathBuf,
21}
22
23impl PathBackend {
24    /// Opens a new [`PathBackend`] for a given path.
25    /// Errors when the file doesn't yet exist.
26    pub fn from_path_or_fail(path: PathBuf) -> error::BackendResult<Self> {
27        OpenOptions::new().read(true).open(path.as_path())?;
28        Ok(Self { path })
29    }
30
31    /// Opens a new [`PathBackend`] for a given path.
32    /// Creates a file if it doesn't yet exist.
33    ///
34    /// Returns the [`PathBackend`] and whether the file already existed.
35    pub fn from_path_or_create(path: PathBuf) -> error::BackendResult<(Self, bool)> {
36        let exists = path.as_path().is_file();
37        OpenOptions::new()
38            .write(true)
39            .create(true)
40            .open(path.as_path())?;
41        Ok((Self { path }, exists))
42    }
43
44    /// Opens a new [`PathBackend`] for a given path.
45    /// Creates a file if it doesn't yet exist, and calls `closure` with it.
46    pub fn from_path_or_create_and<C>(path: PathBuf, closure: C) -> error::BackendResult<Self>
47    where
48        C: FnOnce(&mut std::fs::File),
49    {
50        let exists = path.as_path().is_file();
51        let mut file = OpenOptions::new()
52            .read(true)
53            .write(true)
54            .create(true)
55            .open(path.as_path())?;
56        if !exists {
57            closure(&mut file)
58        }
59        Ok(Self { path })
60    }
61}
62
63impl Backend for PathBackend {
64    fn get_data(&mut self) -> error::BackendResult<Vec<u8>> {
65        use std::io::Read;
66
67        let mut file = OpenOptions::new().read(true).open(self.path.as_path())?;
68        let mut buffer = vec![];
69        file.read_to_end(&mut buffer)?;
70        Ok(buffer)
71    }
72
73    /// Write the byte slice to the backend. This uses and atomic save.
74    ///
75    /// This won't corrupt the existing database file if the program panics
76    /// during the save.
77    fn put_data(&mut self, data: &[u8]) -> error::BackendResult<()> {
78        use std::io::Write;
79
80        #[allow(clippy::or_fun_call)] // `Path::new` is a zero cost conversion
81        let mut tempf = NamedTempFile::new_in(self.path.parent().unwrap_or(Path::new(".")))?;
82        tempf.write_all(data)?;
83        tempf.as_file().sync_all()?;
84        tempf.persist(self.path.as_path())?;
85        Ok(())
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::{Backend, PathBackend};
92    use std::io::Write;
93    use tempfile::NamedTempFile;
94
95    #[test]
96    #[cfg_attr(miri, ignore)]
97    fn test_path_backend_existing() {
98        let file = NamedTempFile::new().expect("could not create temporary file");
99        let (mut backend, existed) = PathBackend::from_path_or_create(file.path().to_owned())
100            .expect("could not create backend");
101        assert!(existed);
102        let data = [4, 5, 1, 6, 8, 1];
103
104        backend.put_data(&data).expect("could not put data");
105        assert_eq!(backend.get_data().expect("could not get data"), data);
106    }
107
108    #[test]
109    #[cfg_attr(miri, ignore)]
110    fn test_path_backend_new() {
111        let dir = tempfile::tempdir().expect("could not create temporary directory");
112        let mut file_path = dir.path().to_owned();
113        file_path.push("rustbreak_path_db.db");
114        let (mut backend, existed) =
115            PathBackend::from_path_or_create(file_path).expect("could not create backend");
116        assert!(!existed);
117        let data = [4, 5, 1, 6, 8, 1];
118
119        backend.put_data(&data).expect("could not put data");
120        assert_eq!(backend.get_data().expect("could not get data"), data);
121        dir.close().expect("Error while deleting temp directory!");
122    }
123
124    #[test]
125    #[cfg_attr(miri, ignore)]
126    fn test_path_backend_nofail() {
127        let file = NamedTempFile::new().expect("could not create temporary file");
128        let file_path = file.path().to_owned();
129        let mut backend = PathBackend::from_path_or_fail(file_path).expect("should not fail");
130        let data = [4, 5, 1, 6, 8, 1];
131
132        backend.put_data(&data).expect("could not put data");
133        assert_eq!(backend.get_data().expect("could not get data"), data);
134    }
135
136    #[test]
137    #[cfg_attr(miri, ignore)]
138    fn test_path_backend_fail_notfound() {
139        let dir = tempfile::tempdir().expect("could not create temporary directory");
140        let mut file_path = dir.path().to_owned();
141        file_path.push("rustbreak_path_db.db");
142        let err =
143            PathBackend::from_path_or_fail(file_path).expect_err("should fail with file not found");
144        if let crate::error::BackendError::Io(io_err) = &err {
145            assert_eq!(std::io::ErrorKind::NotFound, io_err.kind());
146        } else {
147            panic!("Wrong kind of error returned: {}", err);
148        };
149        dir.close().expect("Error while deleting temp directory!");
150    }
151
152    // If the file already exists, the closure shouldn't be called.
153    #[test]
154    #[cfg_attr(miri, ignore)]
155    fn test_path_backend_create_and_existing_nocall() {
156        let file = NamedTempFile::new().expect("could not create temporary file");
157        let mut backend = PathBackend::from_path_or_create_and(file.path().to_owned(), |_| {
158            panic!("Closure called but file already existed");
159        })
160        .expect("could not create backend");
161        let data = [4, 5, 1, 6, 8, 1];
162
163        backend.put_data(&data).expect("could not put data");
164        assert_eq!(backend.get_data().expect("could not get data"), data);
165    }
166
167    // If the file does not yet exist, the closure should be called.
168    #[test]
169    #[cfg_attr(miri, ignore)]
170    fn test_path_backend_create_and_new() {
171        let dir = tempfile::tempdir().expect("could not create temporary directory");
172        let mut file_path = dir.path().to_owned();
173        file_path.push("rustbreak_path_db.db");
174        let mut backend = PathBackend::from_path_or_create_and(file_path, |f| {
175            f.write_all(b"this is a new file")
176                .expect("could not write to file")
177        })
178        .expect("could not create backend");
179        assert_eq!(
180            backend.get_data().expect("could not get data"),
181            b"this is a new file"
182        );
183        let data = [4, 5, 1, 6, 8, 1];
184
185        backend.put_data(&data).expect("could not put data");
186        assert_eq!(backend.get_data().expect("could not get data"), data);
187        dir.close().expect("Error while deleting temp directory!");
188    }
189}