summaryrefslogtreecommitdiffstats
path: root/lib/core/libimagstore/src/store.rs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/core/libimagstore/src/store.rs')
-rw-r--r--lib/core/libimagstore/src/store.rs1747
1 files changed, 1747 insertions, 0 deletions
diff --git a/lib/core/libimagstore/src/store.rs b/lib/core/libimagstore/src/store.rs
new file mode 100644
index 00000000..a0ee33e5
--- /dev/null
+++ b/lib/core/libimagstore/src/store.rs
@@ -0,0 +1,1747 @@
+//
+// imag - the personal information management suite for the commandline
+// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; version
+// 2.1 of the License.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+//
+
+use std::collections::HashMap;
+use std::ops::Drop;
+use std::path::PathBuf;
+use std::result::Result as RResult;
+use std::sync::Arc;
+use std::sync::RwLock;
+use std::io::Read;
+use std::convert::Into;
+use std::ops::Deref;
+use std::ops::DerefMut;
+use std::fmt::Formatter;
+use std::fmt::Debug;
+use std::fmt::Error as FMTError;
+
+use toml::Value;
+use glob::glob;
+use walkdir::WalkDir;
+use walkdir::Iter as WalkDirIter;
+
+use error::{StoreError as SE, StoreErrorKind as SEK};
+use error::MapErrInto;
+use storeid::{IntoStoreId, StoreId, StoreIdIterator};
+use file_abstraction::FileAbstractionInstance;
+use toml_ext::*;
+// We re-export the following things so tests can use them
+pub use file_abstraction::FileAbstraction;
+pub use file_abstraction::FSFileAbstraction;
+pub use file_abstraction::InMemoryFileAbstraction;
+
+use libimagerror::into::IntoError;
+use libimagerror::trace::trace_error;
+use libimagutil::debug_result::*;
+
+use self::glob_store_iter::*;
+
+/// The Result Type returned by any interaction with the store that could fail
+pub type Result<T> = RResult<T, SE>;
+
+
+#[derive(Debug, PartialEq)]
+enum StoreEntryStatus {
+ Present,
+ Borrowed
+}
+
+/// A store entry, depending on the option type it is either borrowed currently
+/// or not.
+#[derive(Debug)]
+struct StoreEntry {
+ id: StoreId,
+ file: Box<FileAbstractionInstance>,
+ status: StoreEntryStatus,
+}
+
+pub enum StoreObject {
+ Id(StoreId),
+ Collection(PathBuf),
+}
+
+pub struct Walk {
+ store_path: PathBuf,
+ dirwalker: WalkDirIter,
+}
+
+impl Walk {
+
+ fn new(mut store_path: PathBuf, mod_name: &str) -> Walk {
+ let pb = store_path.clone();
+ store_path.push(mod_name);
+ Walk {
+ store_path: pb,
+ dirwalker: WalkDir::new(store_path).into_iter(),
+ }
+ }
+}
+
+impl ::std::ops::Deref for Walk {
+ type Target = WalkDirIter;
+
+ fn deref(&self) -> &Self::Target {
+ &self.dirwalker
+ }
+}
+
+impl Iterator for Walk {
+ type Item = StoreObject;
+
+ fn next(&mut self) -> Option<Self::Item> {
+
+ while let Some(something) = self.dirwalker.next() {
+ debug!("[Walk] Processing next item: {:?}", something);
+ match something {
+ Ok(next) => if next.file_type().is_dir() {
+ debug!("Found directory...");
+ return Some(StoreObject::Collection(next.path().to_path_buf()))
+ } else /* if next.file_type().is_file() */ {
+ debug!("Found file...");
+ let n = next.path().to_path_buf();
+ let sid = match StoreId::from_full_path(&self.store_path, n) {
+ Err(e) => {
+ debug!("Could not construct StoreId object from it");
+ trace_error(&e);
+ continue;
+ },
+ Ok(o) => o,
+ };
+ return Some(StoreObject::Id(sid))
+ },
+ Err(e) => {
+ warn!("Error in Walker");
+ debug!("{:?}", e);
+ return None;
+ }
+ }
+ }
+
+ return None;
+ }
+}
+
+impl StoreEntry {
+
+ fn new(id: StoreId, backend: &Box<FileAbstraction>) -> Result<StoreEntry> {
+ let pb = try!(id.clone().into_pathbuf());
+
+ #[cfg(feature = "fs-lock")]
+ {
+ try!(open_file(pb.clone())
+ .and_then(|f| f.lock_exclusive().map_err_into(SEK::FileError))
+ .map_err_into(SEK::IoError));
+ }
+
+ Ok(StoreEntry {
+ id: id,
+ file: backend.new_instance(pb),
+ status: StoreEntryStatus::Present,
+ })
+ }
+
+ /// The entry is currently borrowed, meaning that some thread is currently
+ /// mutating it
+ fn is_borrowed(&self) -> bool {
+ self.status == StoreEntryStatus::Borrowed
+ }
+
+ fn get_entry(&mut self) -> Result<Entry> {
+ if !self.is_borrowed() {
+ self.file
+ .get_file_content(self.id.clone())
+ .or_else(|err| if err.err_type() == SEK::FileNotFound {
+ Ok(Entry::new(self.id.clone()))
+ } else {
+ Err(err)
+ })
+ } else {
+ Err(SE::new(SEK::EntryAlreadyBorrowed, None))
+ }
+ }
+
+ fn write_entry(&mut self, entry: &Entry) -> Result<()> {
+ if self.is_borrowed() {
+ assert_eq!(self.id, entry.location);
+ self.file.write_file_content(entry)
+ .map_err_into(SEK::FileError)
+ .map(|_| ())
+ } else {
+ Ok(())
+ }
+ }
+}
+
+#[cfg(feature = "fs-lock")]
+impl Drop for StoreEntry {
+
+ fn drop(self) {
+ self.get_entry()
+ .and_then(|entry| open_file(entry.get_location().clone()).map_err_into(SEK::IoError))
+ .and_then(|f| f.unlock().map_err_into(SEK::FileError))
+ .map_err_into(SEK::IoError)
+ }
+
+}
+
+
+/// The Store itself, through this object one can interact with IMAG's entries
+pub struct Store {
+ location: PathBuf,
+
+ ///
+ /// Configuration object of the store
+ ///
+ configuration: Option<Value>,
+
+ ///
+ /// Internal Path->File cache map
+ ///
+ /// Caches the files, so they remain flock()ed
+ ///
+ /// Could be optimized for a threadsafe HashMap
+ ///
+ entries: Arc<RwLock<HashMap<StoreId, StoreEntry>>>,
+
+ /// The backend to use
+ ///
+ /// This provides the filesystem-operation functions (or pretends to)
+ backend: Box<FileAbstraction>,
+}
+
+impl Store {
+
+ /// Create a new Store object
+ ///
+ /// This opens a Store in `location` using the configuration from `store_config` (if absent, it
+ /// uses defaults).
+ ///
+ /// If the configuration is not valid, this fails.
+ ///
+ /// If the location does not exist, creating directories is by default denied and the operation
+ /// fails, if not configured otherwise.
+ /// An error is returned in this case.
+ ///
+ /// If the path exists and is a file, the operation is aborted as well, an error is returned.
+ ///
+ /// # Return values
+ ///
+ /// - On success: Store object
+ /// - On Failure:
+ /// - ConfigurationError if config is faulty
+ /// - IoError(FileError(CreateStoreDirDenied())) if store location does not exist and creating
+ /// is denied
+ /// - StorePathCreate(_) if creating the store directory failed
+ /// - StorePathExists() if location exists but is a file
+ pub fn new(location: PathBuf, store_config: Option<Value>) -> Result<Store> {
+ let backend = Box::new(FSFileAbstraction::new());
+ Store::new_with_backend(location, store_config, backend)
+ }
+
+ /// Create a Store object as descripbed in `Store::new()` documentation, but with an alternative
+ /// backend implementation.
+ ///
+ /// Do not use directly, only for testing purposes.
+ pub fn new_with_backend(location: PathBuf,
+ store_config: Option<Value>,
+ backend: Box<FileAbstraction>) -> Result<Store> {
+ use configuration::*;
+
+ debug!("Validating Store configuration");
+ let _ = try!(config_is_valid(&store_config).map_err_into(SEK::ConfigurationError));
+
+ debug!("Building new Store object");
+ if !location.exists() {
+ if !config_implicit_store_create_allowed(store_config.as_ref()) {
+ warn!("Implicitely creating store directory is denied");
+ warn!(" -> Either because configuration does not allow it");
+ warn!(" -> or because there is no configuration");
+ return Err(SEK::CreateStoreDirDenied.into_error())
+ .map_err_into(SEK::FileError)
+ .map_err_into(SEK::IoError);
+ }
+
+ try!(backend.create_dir_all(&location)
+ .map_err_into(SEK::StorePathCreate)
+ .map_dbg_err_str("Failed"));
+ } else if location.is_file() {
+ debug!("Store path exists as file");
+ return Err(SEK::StorePathExists.into_error());
+ }
+
+ let store = Store {
+ location: location.clone(),
+ configuration: store_config,
+ entries: Arc::new(RwLock::new(HashMap::new())),
+ backend: backend,
+ };
+
+ debug!("Store building succeeded");
+ debug!("------------------------");
+ debug!("{:?}", store);
+ debug!("------------------------");
+
+ Ok(store)
+ }
+
+ /// Reset the backend of the store during runtime
+ ///
+ /// # Warning
+ ///
+ /// This is dangerous!
+ /// You should not be able to do that in application code, only the libimagrt should be used to
+ /// do this via safe and careful wrapper functions!
+ ///
+ /// If you are able to do this without using `libimagrt`, please file an issue report.
+ ///
+ /// # Purpose
+ ///
+ /// With the I/O backend of the store, the store is able to pipe itself out via (for example)
+ /// JSON. But because we need a functionality where we load contents from the filesystem and
+ /// then pipe it to stdout, we need to be able to replace the backend during runtime.
+ ///
+ /// This also applies the other way round: If we get the store from stdin and have to persist it
+ /// to stdout, we need to be able to replace the in-memory backend with the real filesystem
+ /// backend.
+ ///
+ pub fn reset_backend(&mut self, mut backend: Box<FileAbstraction>) -> Result<()> {
+ self.backend
+ .drain()
+ .and_then(|drain| backend.fill(drain))
+ .map(|_| self.backend = backend)
+ }
+
+ /// Get the store configuration
+ pub fn config(&self) -> Option<&Value> {
+ self.configuration.as_ref()
+ }
+
+ /// Verify the store.
+ ///
+ /// This function is not intended to be called by normal programs but only by `imag-store`.
+ #[cfg(feature = "verify")]
+ pub fn verify(&self) -> bool {
+ use libimagerror::trace::trace_error_dbg;
+
+ info!("Header | Content length | Path");
+ info!("-------+----------------+-----");
+
+ WalkDir::new(self.location.clone())
+ .into_iter()
+ .all(|res| match res {
+ Ok(dent) => {
+ if dent.file_type().is_file() {
+ match self.get(PathBuf::from(dent.path())) {
+ Ok(Some(fle)) => {
+ let p = fle.get_location();
+ let content_len = fle.get_content().len();
+ let header = if fle.get_header().verify().is_ok() {
+ "ok"
+ } else {
+ "broken"
+ };
+
+ info!("{: >6} | {: >14} | {:?}", header, content_len, p.deref());
+ true
+ },
+
+ Ok(None) => {
+ info!("{: >6} | {: >14} | {:?}", "?", "couldn't load", dent.path());
+ true
+ },
+
+ Err(e) => {
+ trace_error_dbg(&e);
+ if_cfg_panic!("Error verifying: {:?}", e);
+ debug!("{:?}", e);
+ false
+ },
+ }
+ } else {
+ info!("{: >6} | {: >14} | {:?}", "?", "<no file>", dent.path());
+ true
+ }
+ },
+
+ Err(e) => {
+ trace_error_dbg(&e);
+ if_cfg_panic!("Error verifying: {:?}", e);
+ debug!("{:?}", e);
+ false
+ },
+ })
+ }
+
+ /// Creates the Entry at the given location (inside the entry)
+ ///
+ /// # Return value
+ ///
+ /// On success: FileLockEntry
+ ///
+ /// On error:
+ /// - Errors StoreId::into_storeid() might return
+ /// - CreateCallError(LockPoisoned()) if the internal lock is poisened.
+ /// - CreateCallError(EntryAlreadyExists()) if the entry exists already.
+ ///
+ pub fn create<'a, S: IntoStoreId>(&'a self, id: S) -> Result<FileLockEntry<'a>> {
+ let id = try!(id.into_storeid()).with_base(self.path().clone());
+
+ debug!("Creating id: '{}'", id);
+
+ {
+ let mut hsmap = match self.entries.write() {
+ Err(_) => return Err(SEK::LockPoisoned.into_error()).map_err_into(SEK::CreateCallError),
+ Ok(s) => s,
+ };
+
+ if hsmap.contains_key(&id) {
+ debug!("Cannot create, internal cache already contains: '{}'", id);
+ return Err(SEK::EntryAlreadyExists.into_error()).map_err_into(SEK::CreateCallError);
+ }
+ hsmap.insert(id.clone(), {
+ debug!("Creating: '{}'", id);
+ let mut se = try!(StoreEntry::new(id.clone(), &self.backend));
+ se.status = StoreEntryStatus::Borrowed;
+ se
+ });
+ }
+
+ debug!("Constructing FileLockEntry: '{}'", id);
+
+ Ok(FileLockEntry::new(self, Entry::new(id)))
+ }
+
+ /// Borrow a given Entry. When the `FileLockEntry` is either `update`d or
+ /// dropped, the new Entry is written to disk
+ ///
+ /// Implicitely creates a entry in the store if there is no entry with the id `id`. For a
+ /// non-implicitely-create look at `Store::get`.
+ ///
+ /// # Return value
+ ///
+ /// On success: FileLockEntry
+ ///
+ /// On error:
+ /// - Errors StoreId::into_storeid() might return
+ /// - RetrieveCallError(LockPoisoned()) if the internal lock is poisened.
+ ///
+ pub fn retrieve<'a, S: IntoStoreId>(&'a self, id: S) -> Result<FileLockEntry<'a>> {
+ let id = try!(id.into_storeid()).with_base(self.path().clone());
+ debug!("Retrieving id: '{}'", id);
+ let entry = try!({
+ self.entries
+ .write()
+ .map_err(|_| SE::new(SEK::LockPoisoned, None))
+ .and_then(|mut es| {
+ let new_se = try!(StoreEntry::new(id.clone(), &self.backend));
+ let mut se = es.entry(id.clone()).or_insert(new_se);
+ let entry = se.get_entry();
+ se.status = StoreEntryStatus::Borrowed;
+ entry
+ })
+ .map_err_into(SEK::RetrieveCallError)
+ });
+
+ debug!("Constructing FileLockEntry: '{}'", id);
+ Ok(FileLockEntry::new(self, entry))
+ }
+
+ /// Get an entry from the store if it exists.
+ ///
+ /// # Return value
+ ///
+ /// On success: Some(FileLockEntry) or None
+ ///
+ /// On error:
+ /// - Errors StoreId::into_storeid() might return
+ /// - Errors Store::retrieve() might return
+ ///
+ pub fn get<'a, S: IntoStoreId + Clone>(&'a self, id: S) -> Result<Option<FileLockEntry<'a>>> {
+ let id = try!(id.into_storeid()).with_base(self.path().clone());
+
+ debug!("Getting id: '{}'", id);
+
+ let exists = try!(id.exists()) || try!(self.entries
+ .read()
+ .map(|map| map.contains_key(&id))
+ .map_err(|_| SE::new(SEK::LockPoisoned, None))
+ .map_err_into(SEK::GetCallError)
+ );
+
+ if !exists {
+ debug!("Does not exist in internal cache or filesystem: {:?}", id);
+ return Ok(None);
+ }
+
+ self.retrieve(id).map(Some).map_err_into(SEK::GetCallError)
+ }
+
+ /// Iterate over all StoreIds for one module name
+ ///
+ /// # Returns
+ ///
+ /// On success: An iterator over all entries in the module
+ ///
+ /// On failure:
+ /// - RetrieveForModuleCallError(GlobError(EncodingError())) if the path string cannot be
+ /// encoded
+ /// - GRetrieveForModuleCallError(GlobError(lobError())) if the glob() failed.
+ ///
+ pub fn retrieve_for_module(&self, mod_name: &str) -> Result<StoreIdIterator> {
+ let mut path = self.path().clone();
+ path.push(mod_name);
+
+ debug!("Retrieving for module: '{}'", mod_name);
+
+ path.to_str()
+ .ok_or(SE::new(SEK::EncodingError, None))
+ .and_then(|path| {
+ let path = [ path, "/**/*" ].join("");
+ debug!("glob()ing with '{}'", path);
+ glob(&path[..]).map_err_into(SEK::GlobError)
+ })
+ .map(|paths| GlobStoreIdIterator::new(paths, self.path().clone()).into())
+ .map_err_into(SEK::GlobError)
+ .map_err_into(SEK::RetrieveForModuleCallError)
+ }
+
+ /// Walk the store tree for the module
+ ///
+ /// The difference between a `Walk` and a `StoreIdIterator` is that with a `Walk`, one can find
+ /// "collections" (folders).
+ pub fn walk<'a>(&'a self, mod_name: &str) -> Walk {
+ debug!("Creating Walk object for {}", mod_name);
+ Walk::new(self.path().clone(), mod_name)
+ }
+
+ /// Return the `FileLockEntry` and write to disk
+ ///
+ /// See `Store::_update()`.
+ ///
+ pub fn update<'a>(&'a self, entry: &mut FileLockEntry<'a>) -> Result<()> {
+ debug!("Updating FileLockEntry at '{}'", entry.get_location());
+ self._update(entry, false).map_err_into(SEK::UpdateCallError)
+ }
+
+ /// Internal method to write to the filesystem store.
+ ///
+ /// # Assumptions
+ ///
+ /// This method assumes that entry is dropped _right after_ the call, hence
+ /// it is not public.
+ ///
+ /// # Return value
+ ///
+ /// On success: Entry
+ ///
+ /// On error:
+ /// - UpdateCallError(LockPoisoned()) if the internal write lock cannot be aquierd.
+ /// - IdNotFound() if the entry was not found in the stor
+ /// - Errors Entry::verify() might return
+ /// - Errors StoreEntry::write_entry() might return
+ ///
+ fn _update<'a>(&'a self, entry: &mut FileLockEntry<'a>, modify_presence: bool) -> Result<()> {
+ let mut hsmap = match self.entries.write() {
+ Err(_) => return Err(SE::new(SEK::LockPoisoned, None)),
+ Ok(e) => e,
+ };
+
+ let mut se = try!(hsmap.get_mut(&entry.location).ok_or(SE::new(SEK::IdNotFound, None)));
+
+ assert!(se.is_borrowed(), "Tried to update a non borrowed entry.");
+
+ debug!("Verifying Entry");
+ try!(entry.entry.verify());
+
+ debug!("Writing Entry");
+ try!(se.write_entry(&entry.entry));
+ if modify_presence {
+ se.status = StoreEntryStatus::Present;
+ }
+
+ Ok(())
+ }
+
+ /// Retrieve a copy of a given entry, this cannot be used to mutate
+ /// the one on disk
+ ///
+ /// # Return value
+ ///
+ /// On success: Entry
+ ///
+ /// On error:
+ /// - RetrieveCopyCallError(LockPoisoned()) if the internal write lock cannot be aquierd.
+ /// - RetrieveCopyCallError(IdLocked()) if the Entry is borrowed currently
+ /// - Errors StoreEntry::new() might return
+ ///
+ pub fn retrieve_copy<S: IntoStoreId>(&self, id: S) -> Result<Entry> {
+ let id = try!(id.into_storeid()).with_base(self.path().clone());
+ debug!("Retrieving copy of '{}'", id);
+ let entries = match self.entries.write() {
+ Err(_) => {
+ return Err(SE::new(SEK::LockPoisoned, None))
+ .map_err_into(SEK::RetrieveCopyCallError);
+ },
+ Ok(e) => e,
+ };
+
+ // if the entry is currently modified by the user, we cannot drop it
+ if entries.get(&id).map(|e| e.is_borrowed()).unwrap_or(false) {
+ return Err(SE::new(SEK::IdLocked, None)).map_err_into(SEK::RetrieveCopyCallError);
+ }
+
+ try!(StoreEntry::new(id, &self.backend)).get_entry()
+ }
+
+ /// Delete an entry
+ ///
+ /// # Return value
+ ///
+ /// On success: ()
+ ///
+ /// On error:
+ /// - DeleteCallError(LockPoisoned()) if the internal write lock cannot be aquierd.
+ /// - DeleteCallError(FileNotFound()) if the StoreId refers to a non-existing entry.
+ /// - DeleteCallError(FileError()) if the internals failed to remove the file.
+ ///
+ pub fn delete<S: IntoStoreId>(&self, id: S) -> Result<()> {
+ let id = try!(id.into_storeid()).with_base(self.path().clone());
+
+ debug!("Deleting id: '{}'", id);
+
+ {
+ let mut entries = match self.entries.write() {
+ Err(_) => return Err(SE::new(SEK::LockPoisoned, None))
+ .map_err_into(SEK::DeleteCallError),
+ Ok(e) => e,
+ };
+
+ // if the entry is currently modified by the user, we cannot drop it
+ match entries.get(&id) {
+ None => {
+ return Err(SEK::FileNotFound.into_error()).map_err_into(SEK::DeleteCallError)
+ },
+ Some(e) => if e.is_borrowed() {
+ return Err(SE::new(SEK::IdLocked, None)).map_err_into(SEK::DeleteCallError)
+ }
+ }
+
+ // remove the entry first, then the file
+ entries.remove(&id);
+ let pb = try!(id.clone().with_base(self.path().clone()).into_pathbuf());
+ if let Err(e) = self.backend.remove_file(&pb) {
+ return Err(SEK::FileError.into_error_with_cause(Box::new(e)))
+ .map_err_into(SEK::DeleteCallError);
+ }
+ }
+
+ debug!("Deleted");
+ Ok(())
+ }
+
+ /// Save a copy of the Entry in another place
+ pub fn save_to(&self, entry: &FileLockEntry, new_id: StoreId) -> Result<()> {
+ debug!("Saving '{}' to '{}'", entry.get_location(), new_id);
+ self.save_to_other_location(entry, new_id, false)
+ }
+
+ /// Save an Entry in another place
+ /// Removes the original entry
+ pub fn save_as(&self, entry: FileLockEntry, new_id: StoreId) -> Result<()> {
+ debug!("Saving '{}' as '{}'", entry.get_location(), new_id);
+ self.save_to_other_location(&entry, new_id, true)
+ }
+
+ fn save_to_other_location(&self, entry: &FileLockEntry, new_id: StoreId, remove_old: bool)
+ -> Result<()>
+ {
+ let new_id = new_id.with_base(self.path().clone());
+ let hsmap = try!(
+ self.entries
+ .write()
+ .map_err(|_| SEK::LockPoisoned.into_error())
+ .map_err_into(SEK::MoveCallError)
+ );
+
+ if hsmap.contains_key(&new_id) {
+ return Err(SEK::EntryAlreadyExists.into_error()).map_err_into(SEK::MoveCallError)
+ }
+
+ let old_id = entry.get_location().clone();
+
+ let old_id_as_path = try!(old_id.clone().with_base(self.path().clone()).into_pathbuf());
+ let new_id_as_path = try!(new_id.clone().with_base(self.path().clone()).into_pathbuf());
+ self.backend.copy(&old_id_as_path, &new_id_as_path)
+ .and_then(|_| {
+ if remove_old {
+ debug!("Removing old '{:?}'", old_id_as_path);
+ self.backend.remove_file(&old_id_as_path)
+ } else {
+ Ok(())
+ }
+ })
+ .map_err_into(SEK::FileError)
+ .map_err_into(SEK::MoveCallError)
+ }
+
+ /// Move an entry without loading
+ ///
+ /// This function moves an entry from one path to another.
+ ///
+ /// Generally, this function shouldn't be used by library authors, if they "just" want to move
+ /// something around. A library for moving entries while caring about meta-data and links.
+ ///
+ /// # Errors
+ ///
+ /// This function returns an error in certain cases:
+ ///
+ /// * If the about-to-be-moved entry is borrowed
+ /// * If the lock on the internal data structure cannot be aquired
+ /// * If the new path already exists
+ /// * If the about-to-be-moved entry does not exist
+ /// * If the FS-operation failed
+ ///
+ /// # Warnings
+ ///
+ /// This should be used with _great_ care, as moving an entry from `a` to `b` might result in
+ /// dangling links (see below).
+ ///
+ /// ## Moving linked entries
+ ///
+ /// If the entry which is moved is linked to another entry, these links get invalid (but we do
+ /// not detect this here). As links are always two-way-links, so `a` is not only linked to `b`,
+ /// but also the other way round, moving `b` to `c` results in the following scenario:
+ ///
+ /// * `a` links to `b`, which does not exist anymore.
+ /// * `c` links to `a`, which does exist.
+ ///
+ /// So the link is _partly dangling_, so to say.
+ ///
+ pub fn move_by_id(&self, old_id: StoreId, new_id: StoreId) -> Result<()> {
+ let new_id = new_id.with_base(self.path().clone());
+ let old_id = old_id.with_base(self.path().clone());
+
+ debug!("Moving '{}' to '{}'", old_id, new_id);
+
+ {
+ let mut hsmap = match self.entries.write() {
+ Err(_) => return Err(SE::new(SEK::LockPoisoned, None)),
+ Ok(m) => m,
+ };
+
+ if hsmap.contains_key(&new_id) {
+ return Err(SEK::EntryAlreadyExists.into_error());
+ }
+
+ // if we do not have an entry here, we fail in `FileAbstraction::rename()` below.
+ // if we have one, but it is borrowed, we really should not rename it, as this might
+ // lead to strange errors
+ if hsmap.get(&old_id).map(|e| e.is_borrowed()).unwrap_or(false) {
+ return Err(SEK::EntryAlreadyBorrowed.into_error());
+ }
+
+ let old_id_pb = try!(old_id.clone().with_base(self.path().clone()).into_pathbuf());
+ let new_id_pb = try!(new_id.clone().with_base(self.path().clone()).into_pathbuf());
+
+ match self.backend.rename(&old_id_pb, &new_id_pb) {
+ Err(e) => return Err(SEK::EntryRenameError.into_error_with_cause(Box::new(e))),
+ Ok(_) => {
+ debug!("Rename worked on filesystem");
+
+ // assert enforced through check hsmap.contains_key(&new_id) above.
+ // Should therefor never fail
+ assert!(hsmap
+ .remove(&old_id)
+ .and_then(|mut entry| {
+ entry.id = new_id.clone();
+ hsmap.insert(new_id.clone(), entry)
+ }).is_none())
+ }
+ }
+
+ }
+
+ debug!("Moved");
+ Ok(())
+ }
+
+ /// Get _all_ entries in the store (by id as iterator)
+ pub fn entries(&self) -> Result<StoreIdIterator> {
+ let iter = Walk::new(self.path().clone(), "")
+ .filter_map(|id| match id {
+ StoreObject::Id(sid) => Some(sid),
+ _ => None
+ });
+
+ Ok(StoreIdIterator::new(Box::new(iter)))
+
+ }
+
+ /// Gets the path where this store is on the disk
+ pub fn path(&self) -> &PathBuf {
+ &self.location
+ }
+
+}
+
+impl Debug for Store {
+
+ /// TODO: Make pretty.
+ fn fmt(&self, fmt: &mut Formatter) -> RResult<(), FMTError> {
+ try!(write!(fmt, " --- Store ---\n"));
+ try!(write!(fmt, "\n"));
+ try!(write!(fmt, " - location : {:?}\n", self.location));
+ try!(write!(fmt, " - configuration : {:?}\n", self.configuration));
+ try!(write!(fmt, "\n"));
+ try!(write!(fmt, "Entries:\n"));
+ try!(write!(fmt, "{:?}", self.entries));
+ try!(write!(fmt, "\n"));
+ Ok(())
+ }
+
+}
+
+impl Drop for Store {
+
+ ///
+ /// Unlock all files on drop
+ //
+ /// TODO: Unlock them
+ ///
+ fn drop(&mut self) {
+ debug!("Dropping store");
+ }
+
+}
+
+/// A struct that allows you to borrow an Entry
+pub struct FileLockEntry<'a> {
+ store: &'a Store,
+ entry: Entry,
+}
+
+impl<'a> FileLockEntry<'a, > {
+
+ /// Create a new FileLockEntry based on a `Entry` object.
+ ///
+ /// Only for internal use.
+ fn new(store: &'a Store, entry: Entry) -> FileLockEntry<'a> {
+ FileLockEntry {
+ store: store,
+ entry: entry,
+ }
+ }
+}
+
+impl<'a> Debug for FileLockEntry<'a> {
+ fn fmt(&self, fmt: &mut Formatter) -> RResult<(), FMTError> {
+ write!(fmt, "FileLockEntry(Store = {})", self.store.location.to_str()
+ .unwrap_or("Unknown Path"))
+ }
+}
+
+impl<'a> Deref for FileLockEntry<'a> {
+ type Target = Entry;
+
+ fn deref(&self) -> &Self::Target {
+ &self.entry
+ }
+}
+
+impl<'a> DerefMut for FileLockEntry<'a> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.entry
+ }
+}
+
+#[cfg(not(test))]
+impl<'a> Drop for FileLockEntry<'a> {
+
+ /// This will silently ignore errors, use `Store::update` if you want to catch the errors
+ ///
+ /// This might panic if the store was compiled with the early-panic feature (which is not
+ /// intended for production use, though).
+ fn drop(&mut self) {
+ use libimagerror::trace::trace_error_dbg;
+ match self.store._update(self, true) {
+ Err(e) => {
+ trace_error_dbg(&e);
+ if_cfg_panic!("ERROR WHILE DROPPING: {:?}", e);
+ },
+ Ok(_) => { },
+ }
+ }
+}
+
+#[cfg(test)]
+impl<'a> Drop for FileLockEntry<'a> {
+
+ /// This will not silently ignore errors but prints the result of the _update() call for testing
+ fn drop(&mut self) {
+ let _ = self.store._update(self, true).map_err(|e| trace_error(&e));
+ }
+
+}
+
+
+/// `EntryContent` type
+pub type EntryContent = String;
+
+/// An Entry of the store
+//
+/// Contains location, header and content part.
+#[derive(Debug, Clone)]
+pub struct Entry {
+ location: StoreId,
+ header: Value,
+ content: EntryContent,
+}
+
+impl Entry {
+
+ /// Create a new store entry with its location at `loc`.
+ ///
+ /// This creates the entry with the default header from `Entry::default_header()` and an empty
+ /// content.
+ pub fn new(loc: StoreId) -> Entry {
+ Entry {
+ location: loc,
+ header: Entry::default_header(),
+ content: EntryContent::new()
+ }
+ }
+
+ /// Get the default Header for an Entry.
+ ///
+ /// This function should be used to get a new Header, as the default header may change. Via
+ /// this function, compatibility is ensured.
+ pub fn default_header() -> Value { // BTreeMap<String, Value>
+ Value::default_header()
+ }
+
+ /// See `Entry::from_str()`, as this function is used internally. This is just a wrapper for
+ /// convenience.
+ pub fn from_reader<S: IntoStoreId>(loc: S, file: &mut Read) -> Result<Entry> {
+ let text = {
+ let mut s = String::new();
+ try!(file.read_to_string(&mut s));
+ s
+ };
+ Self::from_str(loc, &text[..])
+ }
+
+ /// Create a new Entry, with contents from the string passed.
+ ///
+ /// The passed string _must_ be a complete valid store entry, including header. So this is
+ /// probably not what end-users want to call.
+ ///
+ /// # Return value
+ ///
+ /// This errors if
+ ///
+ /// - String cannot be matched on regex to find header and content
+ /// - Header cannot be parsed into a TOML object
+ ///
+ pub fn from_str<S: IntoStoreId>(loc: S, s: &str) -> Result<Entry> {
+ use util::entry_buffer_to_header_content;
+
+ let (header, content) = try!(entry_buffer_to_header_content(s));
+
+ Ok(Entry {
+ location: try!(loc.into_storeid()),
+ header: header,
+ content: content,
+ })
+ }
+
+ /// Return the string representation of this entry
+ ///
+ /// This means not only the content of the entry, but the complete entry (from memory, not from
+ /// disk).
+ pub fn to_str(&self) -> String {
+ format!("---\n{header}---\n{content}",
+ header = ::toml::ser::to_string(&self.header).unwrap(),
+ content = self.content)
+ }
+
+ /// Get the location of the Entry
+ pub fn get_location(&self) -> &StoreId {
+ &self.location
+ }
+
+ /// Get the header of the Entry
+ pub fn get_header(&self) -> &Value {
+ &self.header
+ }
+
+ /// Get the header mutably of the Entry
+ pub fn get_header_mut(&mut self) -> &mut Value {
+ &mut self.header
+ }
+
+ /// Get the content of the Entry
+ pub fn get_content(&self) -> &EntryContent {
+ &self.content
+ }
+
+ /// Get the content mutably of the Entry
+ pub fn get_content_mut(&mut self) -> &mut EntryContent {
+ &mut self.content
+ }
+
+ /// Verify the entry.
+ ///
+ /// Currently, this only verifies the header. This might change in the future.
+ pub fn verify(&self) -> Result<()> {
+ self.header.verify()
+ }
+
+}
+
+impl PartialEq for Entry {
+
+ fn eq(&self, other: &Entry) -> bool {
+ self.location == other.location && // As the location only compares from the store root
+ self.header == other.header && // and the other Entry could be from another store (not
+ self.content == other.content // implemented by now, but we think ahead here)
+ }
+
+}