Merge pull request #200 from matthiasbeyer/libimagstore/hooks
Libimagstore/hooks
This commit is contained in:
commit
68db3b4ce3
14 changed files with 950 additions and 10 deletions
|
@ -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
|
||||
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.
|
||||
|
||||
|
|
|
@ -156,6 +156,13 @@ impl Configuration {
|
|||
&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 {
|
||||
|
|
|
@ -68,7 +68,14 @@ impl<'a> Runtime<'a> {
|
|||
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 {
|
||||
cli_matches: matches,
|
||||
configuration: cfg,
|
||||
|
|
|
@ -12,6 +12,7 @@ regex = "0.1.54"
|
|||
semver = "0.2"
|
||||
toml = "0.1.25"
|
||||
version = "2.0.1"
|
||||
crossbeam = "0.2.8"
|
||||
|
||||
[dev-dependencies]
|
||||
tempdir = "0.3.4"
|
||||
|
|
247
libimagstore/src/configuration.rs
Normal file
247
libimagstore/src/configuration.rs
Normal 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
|
||||
}
|
||||
|
||||
|
|
@ -10,6 +10,7 @@ use std::convert::From;
|
|||
*/
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum StoreErrorKind {
|
||||
ConfigurationError,
|
||||
FileError,
|
||||
IdLocked,
|
||||
IdNotFound,
|
||||
|
@ -27,11 +28,16 @@ pub enum StoreErrorKind {
|
|||
HeaderPathTypeFailure,
|
||||
HeaderKeyNotFound,
|
||||
HeaderTypeFailure,
|
||||
HookRegisterError,
|
||||
HookExecutionError,
|
||||
PreHookExecuteError,
|
||||
PostHookExecuteError,
|
||||
// maybe more
|
||||
}
|
||||
|
||||
fn store_error_type_as_str(e: &StoreErrorKind) -> &'static str {
|
||||
match e {
|
||||
&StoreErrorKind::ConfigurationError => "Store Configuration Error",
|
||||
&StoreErrorKind::FileError => "File Error",
|
||||
&StoreErrorKind::IdLocked => "ID locked",
|
||||
&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::HeaderKeyNotFound => "Header Key not found",
|
||||
&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",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
27
libimagstore/src/hook/accessor.rs
Normal file
27
libimagstore/src/hook/accessor.rs
Normal 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;
|
||||
}
|
||||
|
||||
|
144
libimagstore/src/hook/aspect.rs
Normal file
144
libimagstore/src/hook/aspect.rs
Normal 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))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
105
libimagstore/src/hook/error.rs
Normal file
105
libimagstore/src/hook/error.rs
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
20
libimagstore/src/hook/mod.rs
Normal file
20
libimagstore/src/hook/mod.rs
Normal 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);
|
||||
}
|
||||
|
13
libimagstore/src/hook/position.rs
Normal file
13
libimagstore/src/hook/position.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
#[derive(Debug)]
|
||||
pub enum HookPosition {
|
||||
PreRead,
|
||||
PostRead,
|
||||
PreCreate,
|
||||
PostCreate,
|
||||
PreRetrieve,
|
||||
PostRetrieve,
|
||||
PreUpdate,
|
||||
PostUpdate,
|
||||
PreDelete,
|
||||
PostDelete,
|
||||
}
|
3
libimagstore/src/hook/result.rs
Normal file
3
libimagstore/src/hook/result.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
use hook::error::HookError;
|
||||
|
||||
pub type HookResult<T> = Result<T, HookError>;
|
|
@ -7,9 +7,12 @@ extern crate regex;
|
|||
extern crate toml;
|
||||
#[cfg(test)] extern crate tempdir;
|
||||
extern crate semver;
|
||||
extern crate crossbeam;
|
||||
|
||||
pub mod storeid;
|
||||
pub mod error;
|
||||
pub mod hook;
|
||||
pub mod store;
|
||||
mod configuration;
|
||||
mod lazyfile;
|
||||
|
||||
|
|
|
@ -9,15 +9,30 @@ use std::collections::BTreeMap;
|
|||
use std::io::{Seek, SeekFrom};
|
||||
use std::convert::From;
|
||||
use std::convert::Into;
|
||||
use std::sync::Mutex;
|
||||
use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use toml::{Table, Value};
|
||||
use regex::Regex;
|
||||
use crossbeam;
|
||||
use crossbeam::ScopedJoinHandle;
|
||||
|
||||
use error::{ParserErrorKind, ParserError};
|
||||
use error::{StoreError, StoreErrorKind};
|
||||
use storeid::{StoreId, StoreIdIterator};
|
||||
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
|
||||
pub type Result<T> = RResult<T, StoreError>;
|
||||
|
||||
|
@ -90,6 +105,26 @@ impl StoreEntry {
|
|||
pub struct Store {
|
||||
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
|
||||
*
|
||||
|
@ -103,8 +138,14 @@ pub struct Store {
|
|||
impl Store {
|
||||
|
||||
/// 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 configuration::*;
|
||||
|
||||
debug!("Validating Store configuration");
|
||||
if !config_is_valid(&store_config) {
|
||||
return Err(StoreError::new(StoreErrorKind::ConfigurationError, None));
|
||||
}
|
||||
|
||||
debug!("Building new Store object");
|
||||
if !location.exists() {
|
||||
|
@ -122,11 +163,84 @@ impl Store {
|
|||
}
|
||||
}
|
||||
|
||||
debug!("Store building succeeded");
|
||||
Ok(Store {
|
||||
let pre_read_aspects = get_pre_read_aspect_names(&store_config)
|
||||
.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,
|
||||
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())),
|
||||
})
|
||||
};
|
||||
|
||||
debug!("Store building succeeded");
|
||||
Ok(store)
|
||||
}
|
||||
|
||||
fn storify_id(&self, id: StoreId) -> StoreId {
|
||||
|
@ -140,6 +254,10 @@ impl Store {
|
|||
/// Creates the Entry at the given location (inside the entry)
|
||||
pub fn create<'a>(&'a self, id: StoreId) -> Result<FileLockEntry<'a>> {
|
||||
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();
|
||||
if hsmap.is_err() {
|
||||
return Err(StoreError::new(StoreErrorKind::LockPoisoned, None))
|
||||
|
@ -153,13 +271,21 @@ impl Store {
|
|||
se.status = StoreEntryStatus::Borrowed;
|
||||
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
|
||||
/// dropped, the new Entry is written to disk
|
||||
pub fn retrieve<'a>(&'a self, id: StoreId) -> Result<FileLockEntry<'a>> {
|
||||
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
|
||||
.write()
|
||||
.map_err(|_| StoreError::new(StoreErrorKind::LockPoisoned, None))
|
||||
|
@ -170,6 +296,14 @@ impl Store {
|
|||
entry
|
||||
})
|
||||
.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
|
||||
|
@ -178,8 +312,16 @@ impl Store {
|
|||
}
|
||||
|
||||
/// Return the `FileLockEntry` and write to disk
|
||||
pub fn update<'a>(&'a self, entry: FileLockEntry<'a>) -> Result<()> {
|
||||
self._update(&entry)
|
||||
pub fn update<'a>(&'a self, mut entry: FileLockEntry<'a>) -> Result<()> {
|
||||
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.
|
||||
|
@ -230,7 +372,11 @@ impl Store {
|
|||
/// Delete an entry
|
||||
pub fn delete(&self, id: StoreId) -> Result<()> {
|
||||
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() {
|
||||
return Err(StoreError::new(StoreErrorKind::LockPoisoned, None))
|
||||
}
|
||||
|
@ -244,7 +390,11 @@ impl Store {
|
|||
|
||||
// remove the entry first, then the file
|
||||
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
|
||||
|
@ -252,6 +402,113 @@ impl Store {
|
|||
&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 {
|
||||
|
|
Loading…
Reference in a new issue