summaryrefslogtreecommitdiffstats
path: root/libimagentrylink
diff options
context:
space:
mode:
authorMatthias Beyer <mail@beyermatthias.de>2016-04-16 21:54:38 +0200
committerMatthias Beyer <mail@beyermatthias.de>2016-04-17 12:22:13 +0200
commit33aa56019ff27d70681c28b9cf28948cb072ba27 (patch)
treedf7648a73767d9b974ddf4b67ea405ea2d961ca9 /libimagentrylink
parenta5e9bf747ce4eba620a6ed73928267cd6ffeaa38 (diff)
libimaglink -> libimagentrylink
Diffstat (limited to 'libimagentrylink')
-rw-r--r--libimagentrylink/Cargo.toml16
-rw-r--r--libimagentrylink/README.md5
-rw-r--r--libimagentrylink/src/error.rs90
-rw-r--r--libimagentrylink/src/external.rs244
-rw-r--r--libimagentrylink/src/internal.rs181
-rw-r--r--libimagentrylink/src/lib.rs30
-rw-r--r--libimagentrylink/src/result.rs6
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>;
+