diff --git a/Cargo.toml b/Cargo.toml index 93d2d84..c2a588a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ all-features = true [dependencies] failure = "0.1.1" serde = "1" +tempfile = "3" [dependencies.ron] optional = true @@ -40,7 +41,6 @@ version = "0.6" [dev-dependencies] lazy_static = "1" serde_derive = "1" -tempfile = "3" [features] default = [] diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 52e16e1..122ecb9 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -30,6 +30,9 @@ mod mmap; #[cfg(feature = "mmap")] pub use self::mmap::MmapStorage; +mod path; +pub use self::path::PathBackend; + /// A backend using a file #[derive(Debug)] pub struct FileBackend(::std::fs::File); diff --git a/src/backend/path.rs b/src/backend/path.rs new file mode 100644 index 0000000..3328b87 --- /dev/null +++ b/src/backend/path.rs @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! Module which implements the [`PathBackend`], storing data in a file on the +//! file system (with a path) and featuring atomic saves. + +use super::Backend; +use ::error; +use ::error::RustbreakErrorKind as ErrorKind; +use std::fs::OpenOptions; +use std::path::{Path, PathBuf}; +use failure::ResultExt; +use tempfile::NamedTempFile; + +/// A [`Backend`] using a file given the path. +/// +/// Features atomic saves, so that the database file won't be corrupted or +/// deleted if the program panics during the save. +#[derive(Debug)] +pub struct PathBackend { + path: PathBuf, +} + +impl PathBackend { + /// Opens a new [`PathBackend`] for a given path. + pub fn open(path: PathBuf) -> error::Result { + OpenOptions::new().write(true).create(true).open(path.as_path()) + .context(ErrorKind::Backend)?; + Ok(Self {path}) + } +} + +impl Backend for PathBackend { + fn get_data(&mut self) -> error::Result> { + use ::std::io::Read; + + let mut file = OpenOptions::new().read(true) + .open(self.path.as_path()).context(ErrorKind::Backend)?; + let mut buffer = vec![]; + file.read_to_end(&mut buffer).context(ErrorKind::Backend)?; + Ok(buffer) + } + + /// Write the byte slice to the backend. This uses and atomic save. + /// + /// This won't corrupt the existing database file if the program panics + /// during the save. + fn put_data(&mut self, data: &[u8]) -> error::Result<()> { + use ::std::io::Write; + + let mut tempf = NamedTempFile::new_in(self.path.parent().unwrap_or(Path::new("."))) + .context(ErrorKind::Backend)?; + tempf.write_all(data).context(ErrorKind::Backend)?; + tempf.as_file().sync_all().context(ErrorKind::Backend)?; + tempf.persist(self.path.as_path()).context(ErrorKind::Backend)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use tempfile::NamedTempFile; + use super::{Backend, PathBackend}; + + #[test] + fn test_path_backend() { + let file = NamedTempFile::new() + .expect("could not create temporary file"); + let mut backend = PathBackend::open(file.path().to_owned()) + .expect("could not create backend"); + let data = [4, 5, 1, 6, 8, 1]; + + backend.put_data(&data).expect("could not put data"); + assert_eq!(backend.get_data().expect("could not get data"), data); + } +} diff --git a/src/lib.rs b/src/lib.rs index 7feaccf..2c95bea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -199,7 +199,6 @@ extern crate base64; #[cfg(feature = "mmap")] extern crate memmap; -#[cfg(test)] extern crate tempfile; /// The rustbreak errors that can be returned @@ -220,7 +219,7 @@ use serde::Serialize; use serde::de::DeserializeOwned; use failure::ResultExt; -use backend::{Backend, MemoryBackend, FileBackend}; +use backend::{Backend, MemoryBackend, FileBackend, PathBackend}; #[cfg(feature = "mmap")] use backend::MmapStorage; @@ -671,6 +670,29 @@ impl Database } } +/// A database backed by a file, using atomic saves. +pub type PathDatabase = Database; + +impl Database + where + Data: Serialize + DeserializeOwned + Debug + Clone + Send, + DeSer: DeSerializer + Debug + Send + Sync + Clone +{ + /// Create new [`PathDatabase`] from a [`Path`](std::path::Path). + pub fn from_path(path: S, data: Data) + -> error::Result> + where S: AsRef + { + let backend = FileBackend::open(path).context(error::RustbreakErrorKind::Backend)?; + + Ok(Database { + data: RwLock::new(data), + backend: Mutex::new(backend), + deser: DeSer::default(), + }) + } +} + /// A database backed by a `Vec` pub type MemoryDatabase = Database;