#![deny(
warnings,
missing_copy_implementations,
missing_debug_implementations,
missing_docs,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces,
unused_qualifications,
unused_extern_crates,
unused_must_use,
unused_results,
variant_size_differences
)]
#[macro_use]
extern crate anyhow;
pub mod errors;
pub mod res;
use std::collections;
use std::fs;
use std::io;
use std::io::Write;
use std::path;
use std::sync;
use atomicwrites;
use crate::errors::Error;
use crate::res::{empty_ok, Res};
pub trait Cave: Send + Sync {
fn get(&self, name: &str) -> Res;
fn set(&self, name: &str, data: &[u8]) -> Res;
fn delete(&self, name: &str) -> Res;
fn not_found(&self, name: &str) -> Res {
Err(Error::NotFound(name.into()))
}
}
#[derive(Debug)]
pub struct MemoryCave {
hash_map: sync::RwLock<collections::HashMap<String, Vec<u8>>>,
}
impl MemoryCave {
pub fn new() -> Self {
Self {
hash_map: sync::RwLock::new(collections::HashMap::new()),
}
}
}
impl Cave for MemoryCave {
fn get(&self, name: &str) -> Res {
match self.hash_map.read().unwrap().get(name) {
Some(data) => Ok(data.to_vec()),
None => self.not_found(name),
}
}
fn set(&self, name: &str, data: &[u8]) -> Res {
let _ = self
.hash_map
.write()
.unwrap()
.insert(name.to_string(), data.to_vec());
empty_ok()
}
fn delete(&self, name: &str) -> Res {
match self.hash_map.write().unwrap().remove(name) {
Some(_) => empty_ok(),
None => self.not_found(name),
}
}
}
#[derive(Debug)]
pub struct FileCave {
dir: path::PathBuf,
}
impl FileCave {
pub fn new(dir: &path::Path) -> Result<Self, Error> {
let md = match fs::metadata(dir) {
Err(e) => return Err(Error::Internal(e.into())),
Ok(md) => md,
};
if !md.is_dir() {
return Err(Error::internal_from_msg(format!(
"Provided path is not a directory: {:?}",
dir
)));
}
Ok(Self {
dir: dir.to_owned(),
})
}
fn create_path(&self, name: &str) -> path::PathBuf {
self.dir.join(name)
}
fn convert_io_error(e: io::Error, name: &str) -> Error {
match e.kind() {
io::ErrorKind::NotFound => Error::NotFound(name.into()),
_ => Error::Internal(e.into()),
}
}
}
impl Cave for FileCave {
fn get(&self, name: &str) -> Res {
let path = self.create_path(name);
match fs::read(path) {
Ok(buf) => Ok(buf),
Err(e) => Err(Self::convert_io_error(e, name)),
}
}
fn set(&self, name: &str, data: &[u8]) -> Res {
let path = self.create_path(name);
let af = atomicwrites::AtomicFile::new(path, atomicwrites::AllowOverwrite);
match af.write(|f| f.write_all(data)) {
Ok(_) => empty_ok(),
Err(e) => Err(Error::Internal(e.into())),
}
}
fn delete(&self, name: &str) -> Res {
let path = self.create_path(name);
match fs::remove_file(path) {
Ok(_) => empty_ok(),
Err(e) => Err(Self::convert_io_error(e, name)),
}
}
}
#[cfg(feature = "with-rocksdb")]
#[derive(Debug)]
pub struct RocksDBCave {
db: rocksdb::DB,
}
#[cfg(feature = "with-rocksdb")]
impl RocksDBCave {
pub fn new(dir: &path::Path) -> Result<Self, Error> {
match rocksdb::DB::open_default(dir) {
Ok(db) => Ok(Self { db }),
Err(e) => Err(Error::Internal(e.into())),
}
}
}
#[cfg(feature = "with-rocksdb")]
impl Cave for RocksDBCave {
fn get(&self, name: &str) -> Res {
match self.db.get(name.as_bytes()) {
Ok(o) => match o {
Some(buf) => Ok(buf),
None => self.not_found(name),
},
Err(e) => Err(Error::Internal(e.into())),
}
}
fn set(&self, name: &str, data: &[u8]) -> Res {
match self.db.put(name.as_bytes(), data) {
Ok(_) => empty_ok(),
Err(e) => Err(Error::Internal(e.into())),
}
}
fn delete(&self, name: &str) -> Res {
match self.get(name) {
Ok(_) => (),
e => return e,
}
match self.db.delete(name.as_bytes()) {
Ok(_) => empty_ok(),
Err(e) => Err(Error::Internal(e.into())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_fs;
fn _test_simple(b: Box<dyn Cave>) {
let not_found_err = Err(Error::NotFound("test".to_string()));
let value1 = Ok("value".as_bytes().to_vec());
let value2 = Ok("value2".as_bytes().to_vec());
let value3 = Ok("value3".as_bytes().to_vec());
let res = b.get("test");
assert_eq!(res, not_found_err);
let res = b.delete("test");
assert_eq!(res, not_found_err);
let res = b.set("test", "value".as_bytes());
assert_eq!(res, empty_ok());
let res = b.get("test");
assert_eq!(res, value1);
let res = b.set("test", "value2".as_bytes());
assert_eq!(res, empty_ok());
let res = b.get("test");
assert_eq!(res, value2);
let res = b.delete("test");
assert_eq!(res, empty_ok());
let res = b.get("test");
assert_eq!(res, not_found_err);
let res = b.delete("test");
assert_eq!(res, not_found_err);
let res = b.set("test", "value3".as_bytes());
assert_eq!(res, empty_ok());
let res = b.get("test");
assert_eq!(res, value3);
}
#[test]
fn test_memory_backend_simple() {
let mb = MemoryCave::new();
_test_simple(Box::new(mb))
}
#[test]
fn test_file_backend_simple() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let fb = FileCave::new(temp_dir.path()).unwrap();
_test_simple(Box::new(fb))
}
#[test]
fn test_file_backend_errors() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let internal_err = Error::Internal(anyhow!(""));
let no_path = temp_dir.path().join("nonexistent");
let res = FileCave::new(&no_path);
assert_eq!(res.is_err(), true);
let err = res.unwrap_err();
assert_eq!(err, internal_err);
let empty_file = temp_dir.path().join("empty_file");
let res = fs::File::create(&empty_file);
assert_eq!(res.is_ok(), true);
let res = FileCave::new(&empty_file);
assert_eq!(res.is_err(), true);
let err = res.unwrap_err();
assert_eq!(err, internal_err);
let internal_err = Err(internal_err);
let not_found_err: Res = Err(Error::NotFound("test".to_string()));
let dir = temp_dir.path().join("dir");
let res = fs::create_dir(&dir);
assert_eq!(res.is_ok(), true);
let fb = FileCave::new(&dir).unwrap();
fs::remove_dir(&dir).unwrap();
let res = fb.set("test", &[]);
assert_eq!(res, internal_err);
let res = fb.get("test");
assert_eq!(res, not_found_err);
let res = fb.delete("test");
assert_eq!(res, not_found_err);
}
#[cfg(feature = "with-rocksdb")]
#[test]
fn test_rocksdb_backend_simple() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let rb = RocksDBCave::new(temp_dir.path()).unwrap();
_test_simple(Box::new(rb));
}
#[cfg(feature = "with-rocksdb")]
#[test]
fn test_rocksdb_backend_errors() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let internal_err = Error::Internal(anyhow!(""));
let empty_file = temp_dir.path().join("empty_file");
let _ = fs::File::create(&empty_file).unwrap();
let res = RocksDBCave::new(&empty_file);
assert_eq!(res.is_err(), true);
let err = res.unwrap_err();
assert_eq!(err, internal_err);
let msg = format!("{:?}", err);
assert_eq!(msg.contains("Failed to create RocksDB directory"), true);
let temp_dir = assert_fs::TempDir::new().unwrap();
let corrupted_file = temp_dir.path().join("CURRENT");
let mut file = fs::File::create(&corrupted_file).unwrap();
file.write_all(b"corrupted").unwrap();
let res = RocksDBCave::new(&corrupted_file);
assert_eq!(res.is_err(), true);
let err = res.unwrap_err();
assert_eq!(err, internal_err);
let msg = format!("{:?}", err);
assert_eq!(msg.contains("Failed to create RocksDB directory"), true);
}
}