Merge pull request #200 from matthiasbeyer/libimagstore/hooks

Libimagstore/hooks
This commit is contained in:
Matthias Beyer 2016-03-19 14:41:53 +01:00
commit 68db3b4ce3
14 changed files with 950 additions and 10 deletions

View file

@ -68,3 +68,99 @@ It also MUST contain a getter for this variable.
It MUST NOT contain a setter for this variable, as changing the store while the It MUST NOT contain a setter for this variable, as changing the store while the
programm is running is not allowed. programm is running is not allowed.
## Hook system {#sec:libstore:hooks}
The store library includes a hook system, which can be used to execute arbitrary
code before or after the store was accessed. The following hooks are available:
* `PreReadHook`
* `PostReadHook`
* `PreCreateHook`
* `PostCreateHook`
* `PreUpdateHook`
* `PostUpdateHook`
* `PreDeleteHook`
* `PostDeleteHook`
These are called "Hook positions" in the following.
Which are executed before or after the store action is executed. The `Pre`-Hooks
can deny the execution by returning an error. The `Post`-Hooks can (for the
appropriate store actions) alter the hook result.
Registering hooks with the store is implemented via functions on the `Store`
type itself. Hooks MUST NEVER be removed from the `Store` object during runtime,
only adding hooks to the store is allowed.
As the hooks are simply trait objects, one is able to implement arbitrary hooks,
for example
* Simple consistency-checks for the store
* Version control system adaption for the store (via git for example)
* Encryption of store entries (via gnupg for example)
* Automatic backup on every change to the store (via rsnapshot for example)
Some hooks MAY be shipped with the imag source distribution and be enabled by
default.
Execution order of the hooks is a not-yet-solved problem.
### Hook-Aspects {#sec:libstore:hooks:aspects}
Each hook can be assigned to an "Aspect". There MAY BE zero or more aspects for
each Hook position. Aspects can be sorted and configured via the configuration
file, whereas each aspect has its own configuration section:
```{#lst:hooks:aspects:cfg .toml .numberLines caption="Hook config section"}
[store]
// Defines order of aspects for the pre-read hook position
pre-read-aspects = [ "misc" ]
// Defines order of aspects for the post-read hook position
post-read-aspects = [ "decryption" ]
// ...
// configuration for the "misc" hook aspect
[[aspects.misc]]
parallel-execution = true
// configuration for the "decryption" hook aspect
[[aspects.decryption]]
parallel-execution = false
```
Aspects are executed in the same order they appear in the configuration (in the
`pre-read-aspects = []` array, for example).
Aspects _could_ be sorted in different order for each hook position.
Aspect names are unique, so one aspect "misc" in "pre-read-aspects" is the
same as in "post-read-aspects" and both be configured via `aspects.misc`, though
they do not share hooks.
Aspects where parallel execution is enabled MAY BE executed in sequence if one
of the hooks wants mutable access to the data they hook into.
Hooks can then be assigned to one hook aspect. Hooks MUST never be assigned to
more than one hook aspect. Hooks which are not assigned to any aspect MUST never
be executed.
```{#lst:hooks:cfg .toml .numberLines caption="Hook configuration"}
[hooks]
// decrypt hook with gnupg. An appropriate "gnupg-encrypt" hook must be defined
// to be fully operational, of course
[[gnupg-decrypt]]
aspect = "decryption"
key = "0x123456789"
// version control hook. Sorted into aspect "misc" here.
[[git]]
aspect = "misc"
// ...
```
Hooks MAY HAVE arbitrary configuration keys.

View file

@ -156,6 +156,13 @@ impl Configuration {
&self.config &self.config
} }
pub fn store_config(&self) -> Option<&Value> {
match &self.config {
&Value::Table(ref tabl) => tabl.get("store"),
_ => None,
}
}
} }
fn get_verbosity(v: &Value) -> bool { fn get_verbosity(v: &Value) -> bool {

View file

@ -68,7 +68,14 @@ impl<'a> Runtime<'a> {
Some(cfg.unwrap()) Some(cfg.unwrap())
}; };
Store::new(storepath).map(|store| { let store_config = {
match &cfg {
&Some(ref c) => c.store_config().map(|c| c.clone()),
_ => None
}
};
Store::new(storepath, store_config).map(|store| {
Runtime { Runtime {
cli_matches: matches, cli_matches: matches,
configuration: cfg, configuration: cfg,

View file

@ -12,6 +12,7 @@ regex = "0.1.54"
semver = "0.2" semver = "0.2"
toml = "0.1.25" toml = "0.1.25"
version = "2.0.1" version = "2.0.1"
crossbeam = "0.2.8"
[dev-dependencies] [dev-dependencies]
tempdir = "0.3.4" tempdir = "0.3.4"

View file

@ -0,0 +1,247 @@
use toml::Value;
use hook::position::HookPosition;
/// Check whether the configuration is valid for the store
///
/// The passed `Value` _must be_ the `[store]` sub-tree of the configuration. Otherwise this will
/// fail.
///
/// It checks whether the configuration looks like the store wants it to be:
///
/// ```toml
/// [store]
/// pre-create-hook-aspects = [ "misc", "encryption", "version-control"]
///
/// [[aspects.misc]]
/// parallel = true
/// [[aspects.encryption]]
/// parallel = false
/// [[aspects.version-control]]
/// parallel = false
///
/// [[hooks.gnupg]]
/// aspect = "encryption"
/// key = "0x123456789"
///
/// [[hooks.git]]
/// aspect = "version-control"
/// ```
///
/// It checks:
/// * Whether all the maps are there (whether store, store.aspects, store.aspects.example are all
/// maps)
/// * Whether each aspect configuration has a "parallel = <Boolean>" setting
/// * Whether each hook congfiguration has a "aspect = <String>" setting
///
/// It does NOT check:
/// * Whether all aspects which are used in the hook configuration are also configured
///
/// No configuration is a valid configuration, as the store will use the most conservative settings
/// automatically. This has also performance impact, as all hooks run in no-parallel mode then.
/// You have been warned!
///
///
pub fn config_is_valid(config: &Option<Value>) -> bool {
use std::collections::BTreeMap;
if config.is_none() {
return true;
}
fn has_key_with_map(v: &BTreeMap<String, Value>, key: &str) -> bool {
v.get(key).map(|t| match t { &Value::Table(_) => true, _ => false }).unwrap_or(false)
}
fn has_key_with_string_ary(v: &BTreeMap<String, Value>, key: &str) -> bool {
v.get(key)
.map(|t| match t {
&Value::Array(ref a) => a.iter().all(|elem| {
match elem {
&Value::String(_) => true,
_ => false,
}
}),
_ => false
}).unwrap_or(false)
}
/// Check that
/// * the top-level configuration
/// * is a table
/// * where all entries of a key `section` (eg. "hooks" or "aspects")
/// * Are maps
/// * where each has a key `key` (eg. "aspect" or "parallel")
/// * which fullfills constraint `f` (typecheck)
fn check_all_inner_maps_have_key_with<F>(store_config: &BTreeMap<String, Value>,
section: &str,
key: &str,
f: F)
-> bool
where F: Fn(&Value) -> bool
{
store_config.get(section) // The store config has the section `section`
.map(|section_table| {
match section_table { // which is
&Value::Table(ref section_table) => // a table
section_table
.values() // which has values,
.all(|cfg| { // and all of these values
match cfg {
&Value::Table(ref hook_config) => { // are tables
hook_config.get(key) // with a key
// fullfilling this constraint
.map(|hook_aspect| f(&hook_aspect))
.unwrap_or(false)
},
_ => false,
}
}),
_ => false,
}
})
.unwrap_or(false)
}
match config {
&Some(Value::Table(ref t)) => {
has_key_with_string_ary(t, "pre-read-hook-aspects") &&
has_key_with_string_ary(t, "post-read-hook-aspects") &&
has_key_with_string_ary(t, "pre-create-hook-aspects") &&
has_key_with_string_ary(t, "post-create-hook-aspects") &&
has_key_with_string_ary(t, "pre-retrieve-hook-aspects") &&
has_key_with_string_ary(t, "post-retrieve-hook-aspects") &&
has_key_with_string_ary(t, "pre-update-hook-aspects") &&
has_key_with_string_ary(t, "post-update-hook-aspects") &&
has_key_with_string_ary(t, "pre-delete-hook-aspects") &&
has_key_with_string_ary(t, "post-delete-hook-aspects") &&
// The section "hooks" has maps which have a key "aspect" which has a value of type
// String
check_all_inner_maps_have_key_with(t, "hooks", "aspect", |asp| {
match asp { &Value::String(_) => true, _ => false }
}) &&
// The section "aspects" has maps which have a key "parllel" which has a value of type
// Boolean
check_all_inner_maps_have_key_with(t, "aspects", "parallel", |asp| {
match asp { &Value::Boolean(_) => true, _ => false, }
})
}
_ => false,
}
}
pub fn get_pre_read_aspect_names(value: &Option<Value>) -> Vec<String> {
get_aspect_names_for_aspect_position("pre-read-hook-aspects", value)
}
pub fn get_post_read_aspect_names(value: &Option<Value>) -> Vec<String> {
get_aspect_names_for_aspect_position("post-read-hook-aspects", value)
}
pub fn get_pre_create_aspect_names(value: &Option<Value>) -> Vec<String> {
get_aspect_names_for_aspect_position("pre-create-hook-aspects", value)
}
pub fn get_post_create_aspect_names(value: &Option<Value>) -> Vec<String> {
get_aspect_names_for_aspect_position("post-create-hook-aspects", value)
}
pub fn get_pre_retrieve_aspect_names(value: &Option<Value>) -> Vec<String> {
get_aspect_names_for_aspect_position("pre-retrieve-hook-aspects", value)
}
pub fn get_post_retrieve_aspect_names(value: &Option<Value>) -> Vec<String> {
get_aspect_names_for_aspect_position("post-retrieve-hook-aspects", value)
}
pub fn get_pre_update_aspect_names(value: &Option<Value>) -> Vec<String> {
get_aspect_names_for_aspect_position("pre-update-hook-aspects", value)
}
pub fn get_post_update_aspect_names(value: &Option<Value>) -> Vec<String> {
get_aspect_names_for_aspect_position("post-update-hook-aspects", value)
}
pub fn get_pre_delete_aspect_names(value: &Option<Value>) -> Vec<String> {
get_aspect_names_for_aspect_position("pre-delete-hook-aspects", value)
}
pub fn get_post_delete_aspect_names(value: &Option<Value>) -> Vec<String> {
get_aspect_names_for_aspect_position("post-delete-hook-aspects", value)
}
#[derive(Debug)]
pub struct AspectConfig {
parallel: bool,
config: Value,
}
impl AspectConfig {
pub fn new(init: Value) -> AspectConfig {
let parallel = AspectConfig::is_parallel(&init);
AspectConfig {
config: init,
parallel: parallel,
}
}
pub fn config(&self) -> &Value {
&self.config
}
fn is_parallel(init: &Value) -> bool {
match init {
&Value::Table(ref t) =>
t.get("parallel")
.map(|value| {
match value {
&Value::Boolean(b) => b,
_ => false,
}
})
.unwrap_or(false),
_ => false,
}
}
/// Get the aspect configuration for an aspect.
///
/// Pass the store configuration object, this searches in `[aspects][<aspect_name>]`.
///
/// Returns `None` if one of the keys in the chain is not available
pub fn get_for(v: &Option<Value>, a_name: String) -> Option<AspectConfig> {
match v {
&Some(Value::Table(ref tabl)) => tabl.get(&a_name[..])
.map(|asp| AspectConfig::new(asp.clone())),
_ => None,
}
}
}
fn get_aspect_names_for_aspect_position(config_name: &'static str, value: &Option<Value>) -> Vec<String> {
let mut v = vec![];
match value {
&Some(Value::Table(ref t)) => {
match t.get(config_name) {
Some(&Value::Array(ref a)) => {
for elem in a {
match elem {
&Value::String(ref s) => v.push(s.clone()),
_ => warn!("Non-String in configuration, inside '{}'", config_name),
}
}
},
_ => warn!("'{}' configuration key should contain Array, does not", config_name),
};
},
&None => warn!("No store configuration"),
_ => warn!("Configuration is not a table"),
}
v
}

View file

@ -10,6 +10,7 @@ use std::convert::From;
*/ */
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum StoreErrorKind { pub enum StoreErrorKind {
ConfigurationError,
FileError, FileError,
IdLocked, IdLocked,
IdNotFound, IdNotFound,
@ -27,11 +28,16 @@ pub enum StoreErrorKind {
HeaderPathTypeFailure, HeaderPathTypeFailure,
HeaderKeyNotFound, HeaderKeyNotFound,
HeaderTypeFailure, HeaderTypeFailure,
HookRegisterError,
HookExecutionError,
PreHookExecuteError,
PostHookExecuteError,
// maybe more // maybe more
} }
fn store_error_type_as_str(e: &StoreErrorKind) -> &'static str { fn store_error_type_as_str(e: &StoreErrorKind) -> &'static str {
match e { match e {
&StoreErrorKind::ConfigurationError => "Store Configuration Error",
&StoreErrorKind::FileError => "File Error", &StoreErrorKind::FileError => "File Error",
&StoreErrorKind::IdLocked => "ID locked", &StoreErrorKind::IdLocked => "ID locked",
&StoreErrorKind::IdNotFound => "ID not found", &StoreErrorKind::IdNotFound => "ID not found",
@ -50,6 +56,10 @@ fn store_error_type_as_str(e: &StoreErrorKind) -> &'static str {
&StoreErrorKind::HeaderPathTypeFailure => "Header has wrong type for path", &StoreErrorKind::HeaderPathTypeFailure => "Header has wrong type for path",
&StoreErrorKind::HeaderKeyNotFound => "Header Key not found", &StoreErrorKind::HeaderKeyNotFound => "Header Key not found",
&StoreErrorKind::HeaderTypeFailure => "Header type is wrong", &StoreErrorKind::HeaderTypeFailure => "Header type is wrong",
&StoreErrorKind::HookRegisterError => "Hook register error",
&StoreErrorKind::HookExecutionError => "Hook execution error",
&StoreErrorKind::PreHookExecuteError => "Pre-Hook execution error",
&StoreErrorKind::PostHookExecuteError => "Post-Hook execution error",
} }
} }

View file

@ -0,0 +1,27 @@
use hook::result::HookResult;
use store::FileLockEntry;
use storeid::StoreId;
pub trait StoreIdAccessor : Send + Sync {
fn access(&self, &StoreId) -> HookResult<()>;
}
pub trait MutableHookDataAccessor : Send + Sync {
fn access_mut(&self, &mut FileLockEntry) -> HookResult<()>;
}
pub trait NonMutableHookDataAccessor : Send + Sync {
fn access(&self, &FileLockEntry) -> HookResult<()>;
}
pub enum HookDataAccessor<'a> {
StoreIdAccess(&'a StoreIdAccessor),
MutableAccess(&'a MutableHookDataAccessor),
NonMutableAccess(&'a NonMutableHookDataAccessor),
}
pub trait HookDataAccessorProvider {
fn accessor(&self) -> HookDataAccessor;
}

View file

@ -0,0 +1,144 @@
use store::FileLockEntry;
use storeid::StoreId;
use hook::Hook;
use hook::result::HookResult;
use hook::accessor::{StoreIdAccessor, MutableHookDataAccessor, NonMutableHookDataAccessor};
use hook::accessor::HookDataAccessor as HDA;
use hook::error::HookError as HE;
use hook::error::HookErrorKind as HEK;
use configuration::AspectConfig;
#[derive(Debug)]
pub struct Aspect {
cfg: Option<AspectConfig>,
name: String,
hooks: Vec<Box<Hook>>,
}
impl Aspect {
pub fn new(name: String, cfg: Option<AspectConfig>) -> Aspect {
Aspect {
cfg: cfg,
name: name,
hooks: vec![],
}
}
pub fn name(&self) -> &String {
&self.name
}
pub fn register_hook(&mut self, h: Box<Hook>) {
self.hooks.push(h);
}
}
impl StoreIdAccessor for Aspect {
fn access(&self, id: &StoreId) -> HookResult<()> {
use crossbeam;
use std::thread;
use std::thread::JoinHandle;
let accessors : Vec<HDA> = self.hooks.iter().map(|h| h.accessor()).collect();
if !accessors.iter().all(|a| match a { &HDA::StoreIdAccess(_) => true, _ => false }) {
return Err(HE::new(HEK::AccessTypeViolation, None));
}
let threads : Vec<HookResult<()>> = accessors
.iter()
.map(|accessor| {
crossbeam::scope(|scope| {
scope.spawn(|| {
match accessor {
&HDA::StoreIdAccess(accessor) => accessor.access(id),
_ => unreachable!(),
}
.map_err(|e| ()) // TODO: We're losing the error cause here
})
})
})
.map(|i| i.join().map_err(|_| HE::new(HEK::HookExecutionError, None)))
.collect();
threads
.into_iter()
.fold(Ok(()), |acc, elem| {
acc.and_then(|a| {
elem.map(|_| a).map_err(|_| HE::new(HEK::HookExecutionError, None))
})
})
}
}
impl MutableHookDataAccessor for Aspect {
fn access_mut(&self, fle: &mut FileLockEntry) -> HookResult<()> {
let accessors : Vec<HDA> = self.hooks.iter().map(|h| h.accessor()).collect();
fn is_file_accessor(a: &HDA) -> bool {
match a {
&HDA::MutableAccess(_) => true,
&HDA::NonMutableAccess(_) => true,
_ => false,
}
}
if !accessors.iter().all(|a| is_file_accessor(a)) {
return Err(HE::new(HEK::AccessTypeViolation, None));
}
for accessor in accessors {
match accessor {
HDA::MutableAccess(accessor) => try!(accessor.access_mut(fle)),
// TODO: Naiive implementation.
// More sophisticated version would check whether there are _chunks_ of
// NonMutableAccess accessors and execute these chunks in parallel. We do not have
// performance concerns yet, so this is okay.
HDA::NonMutableAccess(accessor) => try!(accessor.access(fle)),
_ => unreachable!(),
}
}
Ok(())
}
}
impl NonMutableHookDataAccessor for Aspect {
fn access(&self, fle: &FileLockEntry) -> HookResult<()> {
use crossbeam;
use std::thread;
use std::thread::JoinHandle;
let accessors : Vec<HDA> = self.hooks.iter().map(|h| h.accessor()).collect();
if !accessors.iter().all(|a| match a { &HDA::NonMutableAccess(_) => true, _ => false }) {
return Err(HE::new(HEK::AccessTypeViolation, None));
}
let threads : Vec<HookResult<()>> = accessors
.iter()
.map(|accessor| {
crossbeam::scope(|scope| {
scope.spawn(|| {
match accessor {
&HDA::NonMutableAccess(accessor) => accessor.access(fle),
_ => unreachable!(),
}
.map_err(|e| ()) // TODO: We're losing the error cause here
})
})
})
.map(|i| i.join().map_err(|_| HE::new(HEK::HookExecutionError, None)))
.collect();
threads
.into_iter()
.fold(Ok(()), |acc, elem| {
acc.and_then(|a| {
elem.map(|_| a).map_err(|_| HE::new(HEK::HookExecutionError, None))
})
})
}
}

View file

@ -0,0 +1,105 @@
use std::error::Error;
use std::fmt::Error as FmtError;
use std::clone::Clone;
use std::fmt::{Display, Formatter};
use std::convert::Into;
/**
* Kind of error
*/
#[derive(Clone, Copy, Debug)]
pub enum HookErrorKind {
HookExecutionError,
AccessTypeViolation,
}
pub trait IntoHookError {
fn into_hookerror(self) -> HookError;
fn into_hookerror_with_cause(self, cause: Box<Error>) -> HookError;
}
impl Into<HookError> for HookErrorKind {
fn into(self) -> HookError {
HookError::new(self, None)
}
}
impl Into<HookError> for (HookErrorKind, Box<Error>) {
fn into(self) -> HookError {
HookError::new(self.0, Some(self.1))
}
}
fn hook_error_type_as_str(e: &HookErrorKind) -> &'static str {
match e {
_ => "",
}
}
impl Display for HookErrorKind {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> {
try!(write!(fmt, "{}", hook_error_type_as_str(self)));
Ok(())
}
}
/**
* Error type
*/
#[derive(Debug)]
pub struct HookError {
err_type: HookErrorKind,
cause: Option<Box<Error>>,
}
impl HookError {
/**
* Build a new HookError from an HookErrorKind, optionally with cause
*/
pub fn new(errtype: HookErrorKind, cause: Option<Box<Error>>)
-> HookError
{
HookError {
err_type: errtype,
cause: cause,
}
}
/**
* Get the error type of this HookError
*/
pub fn err_type(&self) -> HookErrorKind {
self.err_type.clone()
}
}
impl Display for HookError {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> {
try!(write!(fmt, "[{}]", hook_error_type_as_str(&self.err_type.clone())));
Ok(())
}
}
impl Error for HookError {
fn description(&self) -> &str {
hook_error_type_as_str(&self.err_type.clone())
}
fn cause(&self) -> Option<&Error> {
self.cause.as_ref().map(|e| &**e)
}
}

View file

@ -0,0 +1,20 @@
use std::fmt::Debug;
use toml::Value;
use self::error::HookError;
use store::FileLockEntry;
pub mod accessor;
pub mod aspect;
pub mod error;
pub mod position;
pub mod result;
use hook::accessor::HookDataAccessorProvider;
pub trait Hook : HookDataAccessorProvider + Debug + Send + Sync {
fn name(&self) -> &'static str;
fn set_config(&mut self, cfg: &Value);
}

View file

@ -0,0 +1,13 @@
#[derive(Debug)]
pub enum HookPosition {
PreRead,
PostRead,
PreCreate,
PostCreate,
PreRetrieve,
PostRetrieve,
PreUpdate,
PostUpdate,
PreDelete,
PostDelete,
}

View file

@ -0,0 +1,3 @@
use hook::error::HookError;
pub type HookResult<T> = Result<T, HookError>;

View file

@ -7,9 +7,12 @@ extern crate regex;
extern crate toml; extern crate toml;
#[cfg(test)] extern crate tempdir; #[cfg(test)] extern crate tempdir;
extern crate semver; extern crate semver;
extern crate crossbeam;
pub mod storeid; pub mod storeid;
pub mod error; pub mod error;
pub mod hook;
pub mod store; pub mod store;
mod configuration;
mod lazyfile; mod lazyfile;

View file

@ -9,15 +9,30 @@ use std::collections::BTreeMap;
use std::io::{Seek, SeekFrom}; use std::io::{Seek, SeekFrom};
use std::convert::From; use std::convert::From;
use std::convert::Into; use std::convert::Into;
use std::sync::Mutex;
use std::ops::Deref;
use std::ops::DerefMut;
use toml::{Table, Value}; use toml::{Table, Value};
use regex::Regex; use regex::Regex;
use crossbeam;
use crossbeam::ScopedJoinHandle;
use error::{ParserErrorKind, ParserError}; use error::{ParserErrorKind, ParserError};
use error::{StoreError, StoreErrorKind}; use error::{StoreError, StoreErrorKind};
use storeid::{StoreId, StoreIdIterator}; use storeid::{StoreId, StoreIdIterator};
use lazyfile::LazyFile; use lazyfile::LazyFile;
use hook::aspect::Aspect;
use hook::result::HookResult;
use hook::accessor::{ MutableHookDataAccessor,
NonMutableHookDataAccessor,
StoreIdAccessor,
HookDataAccessor,
HookDataAccessorProvider};
use hook::position::HookPosition;
use hook::Hook;
/// The Result Type returned by any interaction with the store that could fail /// The Result Type returned by any interaction with the store that could fail
pub type Result<T> = RResult<T, StoreError>; pub type Result<T> = RResult<T, StoreError>;
@ -90,6 +105,26 @@ impl StoreEntry {
pub struct Store { pub struct Store {
location: PathBuf, location: PathBuf,
/**
* Configuration object of the store
*/
configuration: Option<Value>,
/*
* Registered hooks
*/
pre_read_aspects : Arc<Mutex<Vec<Aspect>>>,
post_read_aspects : Arc<Mutex<Vec<Aspect>>>,
pre_create_aspects : Arc<Mutex<Vec<Aspect>>>,
post_create_aspects : Arc<Mutex<Vec<Aspect>>>,
pre_retrieve_aspects : Arc<Mutex<Vec<Aspect>>>,
post_retrieve_aspects : Arc<Mutex<Vec<Aspect>>>,
pre_update_aspects : Arc<Mutex<Vec<Aspect>>>,
post_update_aspects : Arc<Mutex<Vec<Aspect>>>,
pre_delete_aspects : Arc<Mutex<Vec<Aspect>>>,
post_delete_aspects : Arc<Mutex<Vec<Aspect>>>,
/** /**
* Internal Path->File cache map * Internal Path->File cache map
* *
@ -103,8 +138,14 @@ pub struct Store {
impl Store { impl Store {
/// Create a new Store object /// Create a new Store object
pub fn new(location: PathBuf) -> Result<Store> { pub fn new(location: PathBuf, store_config: Option<Value>) -> Result<Store> {
use std::fs::create_dir_all; use std::fs::create_dir_all;
use configuration::*;
debug!("Validating Store configuration");
if !config_is_valid(&store_config) {
return Err(StoreError::new(StoreErrorKind::ConfigurationError, None));
}
debug!("Building new Store object"); debug!("Building new Store object");
if !location.exists() { if !location.exists() {
@ -122,11 +163,84 @@ impl Store {
} }
} }
debug!("Store building succeeded"); let pre_read_aspects = get_pre_read_aspect_names(&store_config)
Ok(Store { .into_iter().map(|n| {
let cfg = AspectConfig::get_for(&store_config, n.clone());
Aspect::new(n, cfg)
}).collect();
let post_read_aspects = get_post_read_aspect_names(&store_config)
.into_iter().map(|n| {
let cfg = AspectConfig::get_for(&store_config, n.clone());
Aspect::new(n, cfg)
}).collect();
let pre_create_aspects = get_pre_create_aspect_names(&store_config)
.into_iter().map(|n| {
let cfg = AspectConfig::get_for(&store_config, n.clone());
Aspect::new(n, cfg)
}).collect();
let post_create_aspects = get_post_create_aspect_names(&store_config)
.into_iter().map(|n| {
let cfg = AspectConfig::get_for(&store_config, n.clone());
Aspect::new(n, cfg)
}).collect();
let pre_retrieve_aspects = get_pre_retrieve_aspect_names(&store_config)
.into_iter().map(|n| {
let cfg = AspectConfig::get_for(&store_config, n.clone());
Aspect::new(n, cfg)
}).collect();
let post_retrieve_aspects = get_post_retrieve_aspect_names(&store_config)
.into_iter().map(|n| {
let cfg = AspectConfig::get_for(&store_config, n.clone());
Aspect::new(n, cfg)
}).collect();
let pre_update_aspects = get_pre_update_aspect_names(&store_config)
.into_iter().map(|n| {
let cfg = AspectConfig::get_for(&store_config, n.clone());
Aspect::new(n, cfg)
}).collect();
let post_update_aspects = get_post_update_aspect_names(&store_config)
.into_iter().map(|n| {
let cfg = AspectConfig::get_for(&store_config, n.clone());
Aspect::new(n, cfg)
}).collect();
let pre_delete_aspects = get_pre_delete_aspect_names(&store_config)
.into_iter().map(|n| {
let cfg = AspectConfig::get_for(&store_config, n.clone());
Aspect::new(n, cfg)
}).collect();
let post_delete_aspects = get_post_delete_aspect_names(&store_config)
.into_iter().map(|n| {
let cfg = AspectConfig::get_for(&store_config, n.clone());
Aspect::new(n, cfg)
}).collect();
let store = Store {
location: location, location: location,
configuration: store_config,
pre_read_aspects : Arc::new(Mutex::new(pre_read_aspects)),
post_read_aspects : Arc::new(Mutex::new(post_read_aspects)),
pre_create_aspects : Arc::new(Mutex::new(pre_create_aspects)),
post_create_aspects : Arc::new(Mutex::new(post_create_aspects)),
pre_retrieve_aspects : Arc::new(Mutex::new(pre_retrieve_aspects)),
post_retrieve_aspects : Arc::new(Mutex::new(post_retrieve_aspects)),
pre_update_aspects : Arc::new(Mutex::new(pre_update_aspects)),
post_update_aspects : Arc::new(Mutex::new(post_update_aspects)),
pre_delete_aspects : Arc::new(Mutex::new(pre_delete_aspects)),
post_delete_aspects : Arc::new(Mutex::new(post_delete_aspects)),
entries: Arc::new(RwLock::new(HashMap::new())), entries: Arc::new(RwLock::new(HashMap::new())),
}) };
debug!("Store building succeeded");
Ok(store)
} }
fn storify_id(&self, id: StoreId) -> StoreId { fn storify_id(&self, id: StoreId) -> StoreId {
@ -140,6 +254,10 @@ impl Store {
/// Creates the Entry at the given location (inside the entry) /// Creates the Entry at the given location (inside the entry)
pub fn create<'a>(&'a self, id: StoreId) -> Result<FileLockEntry<'a>> { pub fn create<'a>(&'a self, id: StoreId) -> Result<FileLockEntry<'a>> {
let id = self.storify_id(id); let id = self.storify_id(id);
if let Err(e) = self.execute_hooks_for_id(self.pre_create_aspects.clone(), &id) {
return Err(e);
}
let hsmap = self.entries.write(); let hsmap = self.entries.write();
if hsmap.is_err() { if hsmap.is_err() {
return Err(StoreError::new(StoreErrorKind::LockPoisoned, None)) return Err(StoreError::new(StoreErrorKind::LockPoisoned, None))
@ -153,13 +271,21 @@ impl Store {
se.status = StoreEntryStatus::Borrowed; se.status = StoreEntryStatus::Borrowed;
se se
}); });
Ok(FileLockEntry::new(self, Entry::new(id.clone()), id))
let mut fle = FileLockEntry::new(self, Entry::new(id.clone()), id);
self.execute_hooks_for_mut_file(self.post_create_aspects.clone(), &mut fle)
.map_err(|e| StoreError::new(StoreErrorKind::PostHookExecuteError, Some(Box::new(e))))
.map(|_| fle)
} }
/// Borrow a given Entry. When the `FileLockEntry` is either `update`d or /// Borrow a given Entry. When the `FileLockEntry` is either `update`d or
/// dropped, the new Entry is written to disk /// dropped, the new Entry is written to disk
pub fn retrieve<'a>(&'a self, id: StoreId) -> Result<FileLockEntry<'a>> { pub fn retrieve<'a>(&'a self, id: StoreId) -> Result<FileLockEntry<'a>> {
let id = self.storify_id(id); let id = self.storify_id(id);
if let Err(e) = self.execute_hooks_for_id(self.pre_retrieve_aspects.clone(), &id) {
return Err(e);
}
self.entries self.entries
.write() .write()
.map_err(|_| StoreError::new(StoreErrorKind::LockPoisoned, None)) .map_err(|_| StoreError::new(StoreErrorKind::LockPoisoned, None))
@ -170,6 +296,14 @@ impl Store {
entry entry
}) })
.map(|e| FileLockEntry::new(self, e, id)) .map(|e| FileLockEntry::new(self, e, id))
.and_then(|mut fle| {
if let Err(e) = self.execute_hooks_for_mut_file(self.pre_retrieve_aspects.clone(), &mut fle) {
Err(StoreError::new(StoreErrorKind::HookExecutionError, Some(Box::new(e))))
} else {
Ok(fle)
}
})
} }
/// Iterate over all StoreIds for one module name /// Iterate over all StoreIds for one module name
@ -178,8 +312,16 @@ impl Store {
} }
/// Return the `FileLockEntry` and write to disk /// Return the `FileLockEntry` and write to disk
pub fn update<'a>(&'a self, entry: FileLockEntry<'a>) -> Result<()> { pub fn update<'a>(&'a self, mut entry: FileLockEntry<'a>) -> Result<()> {
self._update(&entry) if let Err(e) = self.execute_hooks_for_mut_file(self.pre_update_aspects.clone(), &mut entry) {
return Err(e);
}
if let Err(e) = self._update(&entry) {
return Err(e);
}
self.execute_hooks_for_mut_file(self.post_update_aspects.clone(), &mut entry)
} }
/// Internal method to write to the filesystem store. /// Internal method to write to the filesystem store.
@ -230,7 +372,11 @@ impl Store {
/// Delete an entry /// Delete an entry
pub fn delete(&self, id: StoreId) -> Result<()> { pub fn delete(&self, id: StoreId) -> Result<()> {
let id = self.storify_id(id); let id = self.storify_id(id);
let mut entries_lock = self.entries.write(); if let Err(e) = self.execute_hooks_for_id(self.pre_delete_aspects.clone(), &id) {
return Err(e);
}
let entries_lock = self.entries.write();
if entries_lock.is_err() { if entries_lock.is_err() {
return Err(StoreError::new(StoreErrorKind::LockPoisoned, None)) return Err(StoreError::new(StoreErrorKind::LockPoisoned, None))
} }
@ -244,7 +390,11 @@ impl Store {
// remove the entry first, then the file // remove the entry first, then the file
entries.remove(&id); entries.remove(&id);
remove_file(&id).map_err(|e| StoreError::new(StoreErrorKind::FileError, Some(Box::new(e)))) if let Err(e) = remove_file(&id) {
return Err(StoreError::new(StoreErrorKind::FileError, Some(Box::new(e))));
}
self.execute_hooks_for_id(self.post_delete_aspects.clone(), &id)
} }
/// Gets the path where this store is on the disk /// Gets the path where this store is on the disk
@ -252,6 +402,113 @@ impl Store {
&self.location &self.location
} }
pub fn register_hook(&mut self,
position: HookPosition,
aspect_name: &String,
mut h: Box<Hook>)
-> Result<()>
{
debug!("Registering hook: {:?}", h);
debug!(" in position: {:?}", position);
debug!(" with aspect: {:?}", aspect_name);
let mut guard = match position {
HookPosition::PreRead => self.pre_read_aspects.clone(),
HookPosition::PostRead => self.post_read_aspects.clone(),
HookPosition::PreCreate => self.pre_create_aspects.clone(),
HookPosition::PostCreate => self.post_create_aspects.clone(),
HookPosition::PreRetrieve => self.pre_retrieve_aspects.clone(),
HookPosition::PostRetrieve => self.post_retrieve_aspects.clone(),
HookPosition::PreUpdate => self.pre_update_aspects.clone(),
HookPosition::PostUpdate => self.post_update_aspects.clone(),
HookPosition::PreDelete => self.pre_delete_aspects.clone(),
HookPosition::PostDelete => self.post_delete_aspects.clone(),
};
let mut guard = guard
.deref()
.lock()
.map_err(|_| StoreError::new(StoreErrorKind::HookRegisterError, None));
if guard.is_err() {
return Err(StoreError::new(StoreErrorKind::HookRegisterError,
Some(Box::new(guard.err().unwrap()))));
}
let mut guard = guard.unwrap();
for mut aspect in guard.deref_mut() {
if aspect.name().clone() == aspect_name.clone() {
self.get_config_for_hook(h.name()).map(|config| h.set_config(config));
aspect.register_hook(h);
return Ok(());
}
}
return Err(StoreError::new(StoreErrorKind::HookRegisterError, None));
}
fn get_config_for_hook(&self, name: &str) -> Option<&Value> {
match &self.configuration {
&Some(Value::Table(ref tabl)) => {
tabl.get("hooks")
.map(|hook_section| {
match hook_section {
&Value::Table(ref tabl) => tabl.get(name),
_ => None
}
})
.unwrap_or(None)
},
_ => None,
}
}
fn execute_hooks_for_id(&self,
aspects: Arc<Mutex<Vec<Aspect>>>,
id: &StoreId)
-> Result<()>
{
let guard = aspects.deref().lock();
if guard.is_err() { return Err(StoreError::new(StoreErrorKind::PreHookExecuteError, None)) }
guard.unwrap().deref().iter()
.fold(Ok(()), |acc, aspect| {
debug!("[Aspect][exec]: {:?}", aspect);
acc.and_then(|_| (aspect as &StoreIdAccessor).access(id))
})
.map_err(|e| StoreError::new(StoreErrorKind::PreHookExecuteError, Some(Box::new(e))))
}
fn execute_hooks_for_mut_file(&self,
aspects: Arc<Mutex<Vec<Aspect>>>,
fle: &mut FileLockEntry)
-> Result<()>
{
let guard = aspects.deref().lock();
if guard.is_err() { return Err(StoreError::new(StoreErrorKind::PreHookExecuteError, None)) }
guard.unwrap().deref().iter()
.fold(Ok(()), |acc, aspect| {
debug!("[Aspect][exec]: {:?}", aspect);
acc.and_then(|_| aspect.access_mut(fle))
})
.map_err(|e| StoreError::new(StoreErrorKind::PreHookExecuteError, Some(Box::new(e))))
}
fn execute_hooks_for_file(&self,
aspects: Arc<Mutex<Vec<Aspect>>>,
fle: &FileLockEntry)
-> Result<()>
{
let guard = aspects.deref().lock();
if guard.is_err() { return Err(StoreError::new(StoreErrorKind::PreHookExecuteError, None)) }
guard.unwrap().deref().iter()
.fold(Ok(()), |acc, aspect| {
debug!("[Aspect][exec]: {:?}", aspect);
acc.and_then(|_| (aspect as &NonMutableHookDataAccessor).access(fle))
})
.map_err(|e| StoreError::new(StoreErrorKind::PreHookExecuteError, Some(Box::new(e))))
}
} }
impl Drop for Store { impl Drop for Store {