diff options
author | Matthias Beyer <mail@beyermatthias.de> | 2016-04-16 21:54:38 +0200 |
---|---|---|
committer | Matthias Beyer <mail@beyermatthias.de> | 2016-04-17 12:22:13 +0200 |
commit | 33aa56019ff27d70681c28b9cf28948cb072ba27 (patch) | |
tree | df7648a73767d9b974ddf4b67ea405ea2d961ca9 /libimagentrylink | |
parent | a5e9bf747ce4eba620a6ed73928267cd6ffeaa38 (diff) |
libimaglink -> libimagentrylink
Diffstat (limited to 'libimagentrylink')
-rw-r--r-- | libimagentrylink/Cargo.toml | 16 | ||||
-rw-r--r-- | libimagentrylink/README.md | 5 | ||||
-rw-r--r-- | libimagentrylink/src/error.rs | 90 | ||||
-rw-r--r-- | libimagentrylink/src/external.rs | 244 | ||||
-rw-r--r-- | libimagentrylink/src/internal.rs | 181 | ||||
-rw-r--r-- | libimagentrylink/src/lib.rs | 30 | ||||
-rw-r--r-- | libimagentrylink/src/result.rs | 6 |
7 files changed, 572 insertions, 0 deletions
diff --git a/libimagentrylink/Cargo.toml b/libimagentrylink/Cargo.toml new file mode 100644 index 00000000..cf6418ec --- /dev/null +++ b/libimagentrylink/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "libimaglink" +version = "0.1.0" +authors = ["Matthias Beyer <mail@beyermatthias.de>"] + +[dependencies] +itertools = "0.4.7" +log = "0.3.4" +toml = "0.1.27" +semver = "0.2" +url = "0.5.5" +rust-crypto = "0.2.35" + +[dependencies.libimagstore] +path = "../libimagstore" + diff --git a/libimagentrylink/README.md b/libimagentrylink/README.md new file mode 100644 index 00000000..4dc841d9 --- /dev/null +++ b/libimagentrylink/README.md @@ -0,0 +1,5 @@ +# libimaglink + +Linking library for linking entries with other entries. Used for "imag-link", +the commandline utility, but intended for use in other binaries and libraries as +well. diff --git a/libimagentrylink/src/error.rs b/libimagentrylink/src/error.rs new file mode 100644 index 00000000..bdf7ccb1 --- /dev/null +++ b/libimagentrylink/src/error.rs @@ -0,0 +1,90 @@ +use std::error::Error; +use std::fmt::Error as FmtError; +use std::fmt::{Display, Formatter}; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LinkErrorKind { + EntryHeaderReadError, + EntryHeaderWriteError, + ExistingLinkTypeWrong, + LinkTargetDoesNotExist, + InternalConversionError, + InvalidUri, + StoreReadError, + StoreWriteError, +} + +fn link_error_type_as_str(e: &LinkErrorKind) -> &'static str { + match e { + &LinkErrorKind::EntryHeaderReadError + => "Error while reading an entry header", + + &LinkErrorKind::EntryHeaderWriteError + => "Error while writing an entry header", + + &LinkErrorKind::ExistingLinkTypeWrong + => "Existing link entry has wrong type", + + &LinkErrorKind::LinkTargetDoesNotExist + => "Link target does not exist in the store", + + &LinkErrorKind::InternalConversionError + => "Error while converting values internally", + + &LinkErrorKind::InvalidUri + => "URI is not valid", + + &LinkErrorKind::StoreReadError + => "Store read error", + + &LinkErrorKind::StoreWriteError + => "Store write error", + } +} + +impl Display for LinkErrorKind { + + fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> { + try!(write!(fmt, "{}", link_error_type_as_str(self))); + Ok(()) + } + +} + +#[derive(Debug)] +pub struct LinkError { + kind: LinkErrorKind, + cause: Option<Box<Error>>, +} + +impl LinkError { + + pub fn new(errtype: LinkErrorKind, cause: Option<Box<Error>>) -> LinkError { + LinkError { + kind: errtype, + cause: cause, + } + } + +} + +impl Display for LinkError { + + fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> { + try!(write!(fmt, "[{}]", link_error_type_as_str(&self.kind))); + Ok(()) + } + +} + +impl Error for LinkError { + + fn description(&self) -> &str { + link_error_type_as_str(&self.kind) + } + + fn cause(&self) -> Option<&Error> { + self.cause.as_ref().map(|e| &**e) + } + +} diff --git a/libimagentrylink/src/external.rs b/libimagentrylink/src/external.rs new file mode 100644 index 00000000..9df53cca --- /dev/null +++ b/libimagentrylink/src/external.rs @@ -0,0 +1,244 @@ +/// External linking is a complex implementation to be able to serve a clean and easy-to-use +/// interface. +/// +/// Internally, there are no such things as "external links" (plural). Each Entry in the store can +/// only have _one_ external link. +/// +/// This library does the following therefor: It allows you to have several external links with one +/// entry, which are internally one file in the store for each link, linked with "internal +/// linking". +/// +/// This helps us greatly with deduplication of URLs. +/// + +use std::ops::Deref; +use std::ops::DerefMut; +use std::collections::BTreeMap; + +use libimagstore::store::Entry; +use libimagstore::store::FileLockEntry; +use libimagstore::store::Store; +use libimagstore::storeid::StoreId; +use libimagstore::storeid::IntoStoreId; + +use error::LinkError as LE; +use error::LinkErrorKind as LEK; +use result::Result; +use internal::InternalLinker; +use module_path::ModuleEntryPath; + +use toml::Value; +use url::Url; +use crypto::sha1::Sha1; +use crypto::digest::Digest; + +/// "Link" Type, just an abstraction over FileLockEntry to have some convenience internally. +struct Link<'a> { + link: FileLockEntry<'a> +} + +impl<'a> Link<'a> { + + pub fn new(fle: FileLockEntry<'a>) -> Link<'a> { + Link { link: fle } + } + + /// For interal use only. Load an Link from a store id, if this is actually a Link + fn retrieve(store: &'a Store, id: StoreId) -> Result<Option<Link<'a>>> { + store.retrieve(id) + .map(|fle| { + if let Some(_) = Link::get_link_uri_from_filelockentry(&fle) { + Some(Link { + link: fle + }) + } else { + None + } + }) + .map_err(|e| LE::new(LEK::StoreReadError, Some(Box::new(e)))) + } + + /// Get a link Url object from a FileLockEntry, ignore errors. + fn get_link_uri_from_filelockentry(file: &FileLockEntry<'a>) -> Option<Url> { + file.deref() + .get_header() + .read("imag.content.uri") + .ok() + .and_then(|opt| { + match opt { + Some(Value::String(s)) => Url::parse(&s[..]).ok(), + _ => None + } + }) + } + + pub fn get_url(&self) -> Result<Option<Url>> { + let opt = self.link + .deref() + .get_header() + .read("imag.content.uri"); + + match opt { + Ok(Some(Value::String(s))) => { + Url::parse(&s[..]) + .map(|s| Some(s)) + .map_err(|e| LE::new(LEK::EntryHeaderReadError, Some(Box::new(e)))) + }, + Ok(None) => Ok(None), + _ => Err(LE::new(LEK::EntryHeaderReadError, None)) + } + } + +} + +pub trait ExternalLinker : InternalLinker { + + /// Get the external links from the implementor object + fn get_external_links(&self, store: &Store) -> Result<Vec<Url>>; + + /// Set the external links for the implementor object + fn set_external_links(&mut self, store: &Store, links: Vec<Url>) -> Result<()>; + + /// Add an external link to the implementor object + fn add_external_link(&mut self, store: &Store, link: Url) -> Result<()>; + + /// Remove an external link from the implementor object + fn remove_external_link(&mut self, store: &Store, link: Url) -> Result<()>; + +} + +/// Check whether the StoreId starts with `/link/external/` +fn is_link_store_id(id: &StoreId) -> bool { + debug!("Checking whether this is a /link/external/*: '{:?}'", id); + id.starts_with("/link/external/") +} + +fn get_external_link_from_file(entry: &FileLockEntry) -> Result<Url> { + Link::get_link_uri_from_filelockentry(entry) // TODO: Do not hide error by using this function + .ok_or(LE::new(LEK::StoreReadError, None)) +} + +/// Implement ExternalLinker for Entry, hiding the fact that there is no such thing as an external +/// link in an entry, but internal links to other entries which serve as external links, as one +/// entry in the store can only have one external link. +impl ExternalLinker for Entry { + + /// Get the external links from the implementor object + fn get_external_links(&self, store: &Store) -> Result<Vec<Url>> { + // Iterate through all internal links and filter for FileLockEntries which live in + // /link/external/<SHA> -> load these files and get the external link from their headers, + // put them into the return vector. + self.get_internal_links() + .map(|vect| { + debug!("Getting external links"); + vect.into_iter() + .filter(is_link_store_id) + .map(|id| { + debug!("Retrieving entry for id: '{:?}'", id); + match store.retrieve(id.clone()) { + Ok(f) => get_external_link_from_file(&f), + Err(e) => { + debug!("Retrieving entry for id: '{:?}' failed", id); + Err(LE::new(LEK::StoreReadError, Some(Box::new(e)))) + } + } + }) + .filter_map(|x| x.ok()) // TODO: Do not ignore error here + .collect() + }) + .map_err(|e| LE::new(LEK::StoreReadError, Some(Box::new(e)))) + } + + /// Set the external links for the implementor object + fn set_external_links(&mut self, store: &Store, links: Vec<Url>) -> Result<()> { + // Take all the links, generate a SHA sum out of each one, filter out the already existing + // store entries and store the other URIs in the header of one FileLockEntry each, in + // the path /link/external/<SHA of the URL> + + debug!("Iterating {} links = {:?}", links.len(), links); + for link in links { // for all links + let hash = { + let mut s = Sha1::new(); + s.input_str(&link.serialize()[..]); + s.result_str() + }; + let file_id = ModuleEntryPath::new(format!("external/{}", hash)).into_storeid(); + + debug!("Link = '{:?}'", link); + debug!("Hash = '{:?}'", hash); + debug!("StoreId = '{:?}'", file_id); + + // retrieve the file from the store, which implicitely creates the entry if it does not + // exist + let file = store.retrieve(file_id.clone()); + if file.is_err() { + debug!("Failed to create or retrieve an file for this link '{:?}'", link); + return Err(LE::new(LEK::StoreWriteError, Some(Box::new(file.err().unwrap())))); + } + let mut file = file.unwrap(); + + debug!("Generating header content!"); + { + let mut hdr = file.deref_mut().get_header_mut(); + + let mut table = match hdr.read("imag.content") { + Ok(Some(Value::Table(table))) => table, + Ok(Some(_)) => { + warn!("There is a value at 'imag.content' which is not a table."); + warn!("Going to override this value"); + BTreeMap::new() + }, + Ok(None) => BTreeMap::new(), + Err(e) => return Err(LE::new(LEK::StoreWriteError, Some(Box::new(e)))), + }; + + let v = Value::String(link.serialize()); + + debug!("setting URL = '{:?}", v); + table.insert(String::from("url"), v); + + if let Err(e) = hdr.set("imag.content", Value::Table(table)) { + return Err(LE::new(LEK::StoreWriteError, Some(Box::new(e)))); + } else { + debug!("Setting URL worked"); + } + } + + // then add an internal link to the new file or return an error if this fails + if let Err(e) = self.add_internal_link(file.deref_mut()) { + debug!("Error adding internal link"); + return Err(LE::new(LEK::StoreWriteError, Some(Box::new(e)))); + } + } + debug!("Ready iterating"); + Ok(()) + } + + /// Add an external link to the implementor object + fn add_external_link(&mut self, store: &Store, link: Url) -> Result<()> { + // get external links, add this one, save them + debug!("Getting links"); + self.get_external_links(store) + .and_then(|mut links| { + debug!("Adding link = '{:?}' to links = {:?}", link, links); + links.push(link); + debug!("Setting {} links = {:?}", links.len(), links); + self.set_external_links(store, links) + }) + } + + /// Remove an external link from the implementor object + fn remove_external_link(&mut self, store: &Store, link: Url) -> Result<()> { + // get external links, remove this one, save them + self.get_external_links(store) + .and_then(|links| { + debug!("Removing link = '{:?}' from links = {:?}", link, links); + let links = links.into_iter() + .filter(|l| l.serialize() != link.serialize()) + .collect(); + self.set_external_links(store, links) + }) + } + +} + diff --git a/libimagentrylink/src/internal.rs b/libimagentrylink/src/internal.rs new file mode 100644 index 00000000..e22214d1 --- /dev/null +++ b/libimagentrylink/src/internal.rs @@ -0,0 +1,181 @@ +use std::cmp::Ordering; + +use libimagstore::storeid::StoreId; +use libimagstore::store::Entry; +use libimagstore::store::EntryHeader; +use libimagstore::store::Result as StoreResult; + +use error::{LinkError, LinkErrorKind}; +use result::Result; + +use toml::Value; +use itertools::Itertools; + +pub type Link = StoreId; + +pub trait InternalLinker { + + /// Get the internal links from the implementor object + fn get_internal_links(&self) -> Result<Vec<Link>>; + + /// Set the internal links for the implementor object + fn set_internal_links(&mut self, links: Vec<&mut Entry>) -> Result<Vec<Link>>; + + /// Add an internal link to the implementor object + fn add_internal_link(&mut self, link: &mut Entry) -> Result<()>; + + /// Remove an internal link from the implementor object + fn remove_internal_link(&mut self, link: &mut Entry) -> Result<()>; + +} + +impl InternalLinker for Entry { + + fn get_internal_links(&self) -> Result<Vec<Link>> { + process_rw_result(self.get_header().read("imag.links")) + } + + /// Set the links in a header and return the old links, if any. + fn set_internal_links(&mut self, links: Vec<&mut Entry>) -> Result<Vec<Link>> { + let self_location = self.get_location().clone(); + let mut new_links = vec![]; + + for link in links { + if let Err(e) = add_foreign_link(link, self_location.clone()) { + return Err(e); + } + let link = link.get_location().clone(); + new_links.push(link); + } + + let new_links = links_into_values(new_links); + if new_links.iter().any(|o| o.is_none()) { + return Err(LinkError::new(LinkErrorKind::InternalConversionError, None)); + } + let new_links = new_links.into_iter().map(|o| o.unwrap()).collect(); + process_rw_result(self.get_header_mut().set("imag.links", Value::Array(new_links))) + } + + fn add_internal_link(&mut self, link: &mut Entry) -> Result<()> { + let new_link = link.get_location().clone(); + + add_foreign_link(link, self.get_location().clone()) + .and_then(|_| { + self.get_internal_links() + .and_then(|mut links| { + links.push(new_link); + rewrite_links(self.get_header_mut(), links) + }) + }) + } + + fn remove_internal_link(&mut self, link: &mut Entry) -> Result<()> { + let own_loc = link.get_location().clone(); + let other_loc = link.get_location().clone(); + + link.get_internal_links() + .and_then(|links| { + let links = links.into_iter().filter(|l| l.clone() != own_loc).collect(); + rewrite_links(self.get_header_mut(), links) + }) + .and_then(|_| { + self.get_internal_links() + .and_then(|links| { + let links = links.into_iter().filter(|l| l.clone() != other_loc).collect(); + rewrite_links(link.get_header_mut(), links) + }) + }) + } + +} + +fn links_into_values(links: Vec<StoreId>) -> Vec<Option<Value>> { + links + .into_iter() + .map(|s| s.to_str().map(|s| String::from(s))) + .unique() + .map(|elem| elem.map(|s| Value::String(s))) + .sorted_by(|a, b| { + match (a, b) { + (&Some(Value::String(ref a)), &Some(Value::String(ref b))) => Ord::cmp(a, b), + (&None, _) | (_, &None) => Ordering::Equal, + _ => unreachable!() + } + }) +} + +fn rewrite_links(header: &mut EntryHeader, links: Vec<StoreId>) -> Result<()> { + let links = links_into_values(links); + + if links.iter().any(|o| o.is_none()) { + // if any type convert failed we fail as well + Err(LinkError::new(LinkErrorKind::InternalConversionError, None)) + } else { + // I know it is ugly + let links = links.into_iter().map(|opt| opt.unwrap()).collect(); + let process = header.set("imag.links", Value::Array(links)); + process_rw_result(process).map(|_| ()) + } +} + +/// When Linking A -> B, the specification wants us to link back B -> A. +/// This is a helper function which does this. +fn add_foreign_link(target: &mut Entry, from: StoreId) -> Result<()> { + target.get_internal_links() + .and_then(|mut links| { + links.push(from); + let links = links_into_values(links); + if links.iter().any(|o| o.is_none()) { + Err(LinkError::new(LinkErrorKind::InternalConversionError, None)) + } else { + let links = links.into_iter().map(|opt| opt.unwrap()).collect(); + process_rw_result(target.get_header_mut().set("imag.links", Value::Array(links))) + .map(|_| ()) + } + }) +} + +fn process_rw_result(links: StoreResult<Option<Value>>) -> Result<Vec<Link>> { + if links.is_err() { + debug!("RW action on store failed. Generating LinkError"); + let lerr = LinkError::new(LinkErrorKind::EntryHeaderReadError, + Some(Box::new(links.err().unwrap()))); + return Err(lerr); + } + let links = links.unwrap(); + + if links.is_none() { + debug!("We got no value from the header!"); + return Ok(vec![]) + } + let links = links.unwrap(); + + let links = { + match links { + Value::Array(a) => a, + _ => { + debug!("We expected an Array for the links, but there was a non-Array!"); + return Err(LinkError::new(LinkErrorKind::ExistingLinkTypeWrong, None)); + }, + } + }; + + if !links.iter().all(|l| match l { &Value::String(_) => true, _ => false }) { + debug!("At least one of the Values which were expected in the Array of links is a non-String!"); + debug!("Generating LinkError"); + return Err(LinkError::new(LinkErrorKind::ExistingLinkTypeWrong, None)); + } + + let links : Vec<Link> = links.into_iter() + .map(|link| { + match link { + Value::String(s) => StoreId::from(s), + _ => unreachable!(), + } + }) + .collect(); + + debug!("Ok, the RW action was successful, returning link vector now!"); + Ok(links) +} + diff --git a/libimagentrylink/src/lib.rs b/libimagentrylink/src/lib.rs new file mode 100644 index 00000000..448bb853 --- /dev/null +++ b/libimagentrylink/src/lib.rs @@ -0,0 +1,30 @@ +#![deny( + non_camel_case_types, + non_snake_case, + path_statements, + trivial_numeric_casts, + unstable_features, + unused_allocation, + unused_import_braces, + unused_imports, + unused_mut, + unused_qualifications, + while_true, +)] + +extern crate itertools; +#[macro_use] extern crate log; +extern crate toml; +extern crate semver; +extern crate url; +extern crate crypto; + +#[macro_use] extern crate libimagstore; + +module_entry_path_mod!("links", "0.1.0"); + +pub mod error; +pub mod external; +pub mod internal; +pub mod result; + diff --git a/libimagentrylink/src/result.rs b/libimagentrylink/src/result.rs new file mode 100644 index 00000000..587c0cac --- /dev/null +++ b/libimagentrylink/src/result.rs @@ -0,0 +1,6 @@ +use std::result::Result as RResult; + +use error::LinkError; + +pub type Result<T> = RResult<T, LinkError>; + |