diff --git a/Cargo.toml b/Cargo.toml index 1e2c254e..c9e7d3b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ members = [ "lib/entry/libimagentrydatetime", "lib/entry/libimagentryedit", "lib/entry/libimagentryfilter", + "lib/entry/libimagentrygps", "lib/entry/libimagentrylink", "lib/entry/libimagentrylist", "lib/entry/libimagentrymarkdown", diff --git a/bin/core/imag-link/Cargo.toml b/bin/core/imag-link/Cargo.toml index ce87acaf..15b7cc0e 100644 --- a/bin/core/imag-link/Cargo.toml +++ b/bin/core/imag-link/Cargo.toml @@ -33,3 +33,9 @@ path = "../../../lib/etc/libimagutil" default-features = false features = ["testing"] +[dev-dependencies.libimagrt] +version = "0.4.0" +path = "../../../lib/core/libimagrt" +default-features = false +features = ["testing"] + diff --git a/bin/core/imag-link/src/main.rs b/bin/core/imag-link/src/main.rs index 0acbf710..72b9498c 100644 --- a/bin/core/imag-link/src/main.rs +++ b/bin/core/imag-link/src/main.rs @@ -108,40 +108,39 @@ fn handle_internal_linking(rt: &Runtime) { } } - match cmd.value_of("list") { - Some(list) => handle_internal_linking_list_call(rt, cmd, list), - None => { - match cmd.subcommand_name() { - Some("add") => { - let (mut from, to) = get_from_to_entry(&rt, "add"); - for mut to_entry in to { - if let Err(e) = to_entry.add_internal_link(&mut from) { - trace_error_exit(&e, 1); - } - } - }, - - Some("remove") => { - let (mut from, to) = get_from_to_entry(&rt, "remove"); - for mut to_entry in to { - if let Err(e) = to_entry.remove_internal_link(&mut from) { - trace_error_exit(&e, 1); - } - } - }, - - _ => unreachable!(), + match cmd.subcommand_name() { + Some("list") => { + cmd.subcommand_matches("list") + .map(|matches| handle_internal_linking_list_call(rt, cmd, matches)); + }, + Some("add") => { + let (mut from, to) = get_from_to_entry(&rt, "add"); + for mut to_entry in to { + if let Err(e) = to_entry.add_internal_link(&mut from) { + trace_error_exit(&e, 1); + } }; - } + }, + + Some("remove") => { + let (mut from, to) = get_from_to_entry(&rt, "remove"); + for mut to_entry in to { + if let Err(e) = to_entry.remove_internal_link(&mut from) { + trace_error_exit(&e, 1); + } + }; + }, + + _ => unreachable!(), } } #[inline] -fn handle_internal_linking_list_call(rt: &Runtime, cmd: &ArgMatches, list: &str) { +fn handle_internal_linking_list_call(rt: &Runtime, cmd: &ArgMatches, list: &ArgMatches) { use libimagentrylink::external::is_external_link_storeid; debug!("List..."); - for entry in list.split(',') { + for entry in list.values_of("entries").unwrap() { // clap has our back debug!("Listing for '{}'", entry); match get_entry_by_name(rt, entry) { Ok(Some(e)) => { @@ -362,6 +361,7 @@ mod tests { with help "imag-link mocking app"; } use self::mock::generate_test_runtime; + use self::mock::reset_test_runtime; use libimagutil::testing::DEFAULT_ENTRY; fn create_test_default_entry<'a, S: AsRef>(rt: &'a Runtime, name: S) -> StoreResult { @@ -392,7 +392,7 @@ mod tests { #[test] fn test_link_modificates() { - let rt = generate_test_runtime(vec!["internal", "add", "--from", "test1", "--to", "test2"]) + let rt = generate_test_runtime(vec!["internal", "add", "test1", "test2"]) .unwrap(); let test_id1 = create_test_default_entry(&rt, "test1").unwrap(); @@ -412,7 +412,7 @@ mod tests { #[test] fn test_linking_links() { - let rt = generate_test_runtime(vec!["internal", "add", "--from", "test1", "--to", "test2"]) + let rt = generate_test_runtime(vec!["internal", "add", "test1", "test2"]) .unwrap(); let test_id1 = create_test_default_entry(&rt, "test1").unwrap(); @@ -432,7 +432,7 @@ mod tests { #[test] fn test_multilinking() { - let rt = generate_test_runtime(vec!["internal", "add", "--from", "test1", "--to", "test2"]) + let rt = generate_test_runtime(vec!["internal", "add", "test1", "test2"]) .unwrap(); let test_id1 = create_test_default_entry(&rt, "test1").unwrap(); @@ -450,4 +450,87 @@ mod tests { assert_eq!(*test_links1, links_toml_value(vec!["test2"])); assert_eq!(*test_links2, links_toml_value(vec!["test1"])); } + + #[test] + fn test_linking_more_than_two() { + let rt = generate_test_runtime(vec!["internal", "add", "test1", "test2", "test3"]) + .unwrap(); + + let test_id1 = create_test_default_entry(&rt, "test1").unwrap(); + let test_id2 = create_test_default_entry(&rt, "test2").unwrap(); + let test_id3 = create_test_default_entry(&rt, "test3").unwrap(); + + handle_internal_linking(&rt); + handle_internal_linking(&rt); + + let test_entry1 = rt.store().get(test_id1).unwrap().unwrap(); + let test_links1 = get_entry_links(&test_entry1).unwrap(); + + let test_entry2 = rt.store().get(test_id2).unwrap().unwrap(); + let test_links2 = get_entry_links(&test_entry2).unwrap(); + + let test_entry3 = rt.store().get(test_id3).unwrap().unwrap(); + let test_links3 = get_entry_links(&test_entry3).unwrap(); + + assert_eq!(*test_links1, links_toml_value(vec!["test2", "test3"])); + assert_eq!(*test_links2, links_toml_value(vec!["test1"])); + assert_eq!(*test_links3, links_toml_value(vec!["test1"])); + } + + // Remove tests + + #[test] + fn test_linking_links_unlinking_removes_links() { + let rt = generate_test_runtime(vec!["internal", "add", "test1", "test2"]) + .unwrap(); + + let test_id1 = create_test_default_entry(&rt, "test1").unwrap(); + let test_id2 = create_test_default_entry(&rt, "test2").unwrap(); + + handle_internal_linking(&rt); + + let rt = reset_test_runtime(vec!["internal", "remove", "test1", "test2"], rt) + .unwrap(); + + handle_internal_linking(&rt); + + let test_entry1 = rt.store().get(test_id1).unwrap().unwrap(); + let test_links1 = get_entry_links(&test_entry1).unwrap(); + + let test_entry2 = rt.store().get(test_id2).unwrap().unwrap(); + let test_links2 = get_entry_links(&test_entry2).unwrap(); + + assert_eq!(*test_links1, links_toml_value(vec![])); + assert_eq!(*test_links2, links_toml_value(vec![])); + } + + #[test] + fn test_linking_and_unlinking_more_than_two() { + let rt = generate_test_runtime(vec!["internal", "add", "test1", "test2", "test3"]) + .unwrap(); + + let test_id1 = create_test_default_entry(&rt, "test1").unwrap(); + let test_id2 = create_test_default_entry(&rt, "test2").unwrap(); + let test_id3 = create_test_default_entry(&rt, "test3").unwrap(); + + handle_internal_linking(&rt); + + let rt = reset_test_runtime(vec!["internal", "remove", "test1", "test2", "test3"], rt) + .unwrap(); + + handle_internal_linking(&rt); + + let test_entry1 = rt.store().get(test_id1).unwrap().unwrap(); + let test_links1 = get_entry_links(&test_entry1).unwrap(); + + let test_entry2 = rt.store().get(test_id2).unwrap().unwrap(); + let test_links2 = get_entry_links(&test_entry2).unwrap(); + + let test_entry3 = rt.store().get(test_id3).unwrap().unwrap(); + let test_links3 = get_entry_links(&test_entry3).unwrap(); + + assert_eq!(*test_links1, links_toml_value(vec![])); + assert_eq!(*test_links2, links_toml_value(vec![])); + assert_eq!(*test_links3, links_toml_value(vec![])); + } } diff --git a/bin/core/imag-link/src/ui.rs b/bin/core/imag-link/src/ui.rs index 2b83fb1e..7459a25d 100644 --- a/bin/core/imag-link/src/ui.rs +++ b/bin/core/imag-link/src/ui.rs @@ -28,15 +28,14 @@ pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { .about("Add link from one entry to another (and vice-versa)") .version("0.1") .arg(Arg::with_name("from") - .long("from") - .short("f") + .index(1) .takes_value(true) .required(true) + .multiple(false) .help("Link from this entry") .value_name("ENTRY")) .arg(Arg::with_name("to") - .long("to") - .short("t") + .index(2) .takes_value(true) .required(true) .multiple(true) @@ -48,15 +47,14 @@ pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { .about("Remove a link between two or more entries") .version("0.1") .arg(Arg::with_name("from") - .long("from") - .short("f") + .index(1) .takes_value(true) .required(true) + .multiple(false) .help("Remove Link from this entry") .value_name("ENTRY")) .arg(Arg::with_name("to") - .long("to") - .short("t") + .index(2) .takes_value(true) .required(true) .multiple(true) @@ -64,20 +62,23 @@ pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { .value_name("ENTRIES")) ) - .arg(Arg::with_name("list") - .long("list") - .short("l") - .takes_value(true) - .required(false) - .help("List links to this entry") - .value_name("ENTRY")) + .subcommand(SubCommand::with_name("list") + .about("List links to this entry") + .version("0.1") + .arg(Arg::with_name("entries") + .index(1) + .takes_value(true) + .multiple(true) + .required(true) + .help("List these entries, seperate by comma") + .value_name("ENTRIES")) - - .arg(Arg::with_name("list-externals-too") - .long("list-external") - .takes_value(false) - .required(false) - .help("If --list is provided, also list external links (debugging helper that might be removed at some point")) + .arg(Arg::with_name("list-externals-too") + .long("list-external") + .takes_value(false) + .required(false) + .help("If --list is provided, also list external links (debugging helper that might be removed at some point")) + ) .arg(Arg::with_name("check-consistency") .long("check-consistency") diff --git a/bin/core/imag-ref/src/main.rs b/bin/core/imag-ref/src/main.rs index 092ca762..1420f5f8 100644 --- a/bin/core/imag-ref/src/main.rs +++ b/bin/core/imag-ref/src/main.rs @@ -48,7 +48,7 @@ use ui::build_ui; use std::path::PathBuf; -use libimagentryref::reference::Ref; +use libimagentryref::refstore::RefStore; use libimagentryref::flags::RefFlags; use libimagerror::trace::trace_error; use libimagrt::setup::generate_runtime_setup; @@ -82,7 +82,7 @@ fn add(rt: &Runtime) { .with_content_hashing(cmd.is_present("track-content")) .with_permission_tracking(cmd.is_present("track-permissions")); - match Ref::create(rt.store(), path, flags) { + match RefStore::create(rt.store(), path, flags) { Ok(r) => { debug!("Reference created: {:?}", r); info!("Ok"); @@ -102,7 +102,7 @@ fn remove(rt: &Runtime) { let yes = cmd.is_present("yes"); if yes || ask_bool(&format!("Delete Ref with hash '{}'", hash)[..], None) { - match Ref::delete_by_hash(rt.store(), hash) { + match rt.store().delete_by_hash(hash) { Err(e) => trace_error(&e), Ok(_) => info!("Ok"), } @@ -126,7 +126,7 @@ fn list(rt: &Runtime) { let iter = match rt.store().retrieve_for_module("ref") { Ok(iter) => iter.filter_map(|id| { - match Ref::get(rt.store(), id) { + match rt.store().get(id) { Ok(r) => Some(r), Err(e) => { trace_error(&e); @@ -145,7 +145,7 @@ fn list(rt: &Runtime) { .check_changed(do_check_changed) .check_changed_content(do_check_changed_content) .check_changed_permiss(do_check_changed_permiss) - .list(iter.map(|e| e.into())) + .list(iter.filter_map(Into::into)) .ok(); } diff --git a/bin/core/imag-store/Cargo.toml b/bin/core/imag-store/Cargo.toml index be5b1521..55aac2ed 100644 --- a/bin/core/imag-store/Cargo.toml +++ b/bin/core/imag-store/Cargo.toml @@ -28,3 +28,18 @@ libimagutil = { version = "0.4.0", path = "../../../lib/etc/libimagutil" } [features] early-panic = [ "libimagstore/early-panic" ] +[dev-dependencies.libimagutil] +version = "0.4.0" +path = "../../../lib/etc/libimagutil" +default-features = false +features = ["testing"] + +[dev-dependencies.libimagrt] +version = "0.4.0" +path = "../../../lib/core/libimagrt" +default-features = false +features = ["testing"] + +[dev-dependencies.toml-query] +version = "0.3" + diff --git a/bin/core/imag-store/src/create.rs b/bin/core/imag-store/src/create.rs index d4fb9421..74ffbec9 100644 --- a/bin/core/imag-store/src/create.rs +++ b/bin/core/imag-store/src/create.rs @@ -178,3 +178,39 @@ fn string_from_raw_src(raw_src: &str) -> String { } content } + +#[cfg(test)] +mod tests { + use super::create; + + use std::path::PathBuf; + use toml_query::read::TomlValueReadExt; + use toml::Value; + + make_mock_app! { + app "imag-link"; + modulename mock; + version "0.4.0"; + with help "imag-link mocking app"; + } + use self::mock::generate_test_runtime; + + #[test] + fn test_create_simple() { + let test_name = "test_create_simple"; + let rt = generate_test_runtime(vec!["create", "-p", "test_create_simple"]).unwrap(); + + create(&rt); + + let e = rt.store().get(PathBuf::from(test_name)); + assert!(e.is_ok()); + let e = e.unwrap(); + assert!(e.is_some()); + let e = e.unwrap(); + + let version = e.get_header().read("imag.version").map(Option::unwrap).unwrap(); + assert_eq!(Value::String(String::from("0.4.0")), *version); + } + +} + diff --git a/bin/core/imag-store/src/delete.rs b/bin/core/imag-store/src/delete.rs index 1413de77..7b9e56ed 100644 --- a/bin/core/imag-store/src/delete.rs +++ b/bin/core/imag-store/src/delete.rs @@ -46,3 +46,38 @@ pub fn delete(rt: &Runtime) { .or_else(|| warn_exit("No subcommand 'delete'. Will exit now", 1)); } +#[cfg(test)] +mod tests { + use create::create; + use super::delete; + + use std::path::PathBuf; + + make_mock_app! { + app "imag-link"; + modulename mock; + version "0.4.0"; + with help "imag-link mocking app"; + } + use self::mock::generate_test_runtime; + use self::mock::reset_test_runtime; + + #[test] + fn test_create_simple() { + let test_name = "test_create_simple"; + let rt = generate_test_runtime(vec!["create", "-p", "test_create_simple"]).unwrap(); + + create(&rt); + + let rt = reset_test_runtime(vec!["delete", "--id", "test_create_simple"], rt).unwrap(); + + delete(&rt); + + let e = rt.store().get(PathBuf::from(test_name)); + assert!(e.is_ok()); + let e = e.unwrap(); + assert!(e.is_none()); + } + +} + diff --git a/bin/core/imag-store/src/main.rs b/bin/core/imag-store/src/main.rs index 434c84a8..572f9e0d 100644 --- a/bin/core/imag-store/src/main.rs +++ b/bin/core/imag-store/src/main.rs @@ -35,14 +35,21 @@ extern crate clap; #[macro_use] extern crate log; extern crate toml; +#[cfg(test)] extern crate toml_query; #[macro_use] extern crate version; #[macro_use] extern crate error_chain; extern crate libimagrt; extern crate libimagstore; -extern crate libimagutil; extern crate libimagerror; +#[cfg(test)] +#[macro_use] +extern crate libimagutil; + +#[cfg(not(test))] +extern crate libimagutil; + use libimagrt::setup::generate_runtime_setup; mod create; diff --git a/bin/core/imag-tag/Cargo.toml b/bin/core/imag-tag/Cargo.toml index bb00f889..e1bf382a 100644 --- a/bin/core/imag-tag/Cargo.toml +++ b/bin/core/imag-tag/Cargo.toml @@ -35,3 +35,9 @@ path = "../../../lib/etc/libimagutil" default-features = false features = ["testing"] +[dev-dependencies.libimagrt] +version = "0.4.0" +path = "../../../lib/core/libimagrt" +default-features = false +features = ["testing"] + diff --git a/bin/core/imag-tag/src/main.rs b/bin/core/imag-tag/src/main.rs index 1829b3fd..3e03dfd8 100644 --- a/bin/core/imag-tag/src/main.rs +++ b/bin/core/imag-tag/src/main.rs @@ -48,10 +48,11 @@ use libimagrt::setup::generate_runtime_setup; use libimagentrytag::tagable::Tagable; use libimagentrytag::tag::Tag; use libimagerror::trace::{trace_error, trace_error_exit}; -use libimagentrytag::ui::{get_add_tags, get_remove_tags}; use libimagstore::storeid::StoreId; use libimagutil::warn_exit::warn_exit; +use clap::ArgMatches; + mod ui; use ui::build_ui; @@ -181,6 +182,42 @@ fn list(id: PathBuf, rt: &Runtime) { } } +/// Get the tags which should be added from the commandline +/// +/// Returns none if the argument was not specified +fn get_add_tags(matches: &ArgMatches) -> Option> { + let a = "add-tags"; + extract_tags(matches, a, '+') + .or_else(|| matches.values_of(a).map(|values| values.map(String::from).collect())) +} + +/// Get the tags which should be removed from the commandline +/// +/// Returns none if the argument was not specified +fn get_remove_tags(matches: &ArgMatches) -> Option> { + let r = "remove-tags"; + extract_tags(matches, r, '+') + .or_else(|| matches.values_of(r).map(|values| values.map(String::from).collect())) +} + +fn extract_tags(matches: &ArgMatches, specifier: &str, specchar: char) -> Option> { + if let Some(submatch) = matches.subcommand_matches("tags") { + submatch.values_of(specifier) + .map(|values| values.map(String::from).collect()) + } else { + matches.values_of("specify-tags") + .map(|argmatches| { + argmatches + .map(String::from) + .filter(|s| s.starts_with(specchar)) + .map(|s| { + String::from(s.split_at(1).1) + }) + .collect() + }) + } +} + #[cfg(test)] mod tests { use std::path::PathBuf; @@ -190,12 +227,11 @@ mod tests { use toml_query::read::TomlValueReadExt; use toml_query::error::Result as TomlQueryResult; - use libimagentrytag::ui::{get_add_tags, get_remove_tags}; use libimagrt::runtime::Runtime; use libimagstore::storeid::StoreId; use libimagstore::store::{Result as StoreResult, FileLockEntry}; - use super::alter; + use super::*; make_mock_app! { app "imag-tag"; diff --git a/bin/core/imag-tag/src/ui.rs b/bin/core/imag-tag/src/ui.rs index ee9b6feb..e8caa492 100644 --- a/bin/core/imag-tag/src/ui.rs +++ b/bin/core/imag-tag/src/ui.rs @@ -19,7 +19,7 @@ use clap::{Arg, App, ArgGroup, SubCommand}; -use libimagentrytag::ui::{tag_add_arg, tag_remove_arg}; +use libimagentrytag::tag::is_tag; pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { app.arg(Arg::with_name("id") @@ -30,8 +30,22 @@ pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { .help("Use this entry") .value_name("ID")) - .arg(tag_add_arg()) - .arg(tag_remove_arg()) + .arg(Arg::with_name("add-tags") + .short("a") + .long("add") + .takes_value(true) + .value_name("tags") + .multiple(true) + .validator(is_tag) + .help("Add tags, seperated by comma or by specifying multiple times")) + .arg(Arg::with_name("remove-tags") + .short("r") + .long("remove") + .takes_value(true) + .value_name("tags") + .multiple(true) + .validator(is_tag) + .help("Remove tags, seperated by comma or by specifying multiple times")) .subcommand(SubCommand::with_name("list") .about("List tags (default)") diff --git a/bin/domain/imag-bookmark/Cargo.toml b/bin/domain/imag-bookmark/Cargo.toml index ba7769bf..1590c234 100644 --- a/bin/domain/imag-bookmark/Cargo.toml +++ b/bin/domain/imag-bookmark/Cargo.toml @@ -21,5 +21,4 @@ version = "2.0.1" libimagrt = { version = "0.4.0", path = "../../../lib/core/libimagrt" } libimagerror = { version = "0.4.0", path = "../../../lib/core/libimagerror" } libimagbookmark = { version = "0.4.0", path = "../../../lib/domain/libimagbookmark" } -libimagentrytag = { version = "0.4.0", path = "../../../lib/entry/libimagentrytag" } libimagutil = { version = "0.4.0", path = "../../../lib/etc/libimagutil" } diff --git a/bin/domain/imag-bookmark/src/main.rs b/bin/domain/imag-bookmark/src/main.rs index 43924e73..f8b50a2b 100644 --- a/bin/domain/imag-bookmark/src/main.rs +++ b/bin/domain/imag-bookmark/src/main.rs @@ -37,7 +37,6 @@ extern crate clap; #[macro_use] extern crate version; extern crate libimagbookmark; -extern crate libimagentrytag; extern crate libimagrt; extern crate libimagerror; extern crate libimagutil; diff --git a/bin/domain/imag-bookmark/src/ui.rs b/bin/domain/imag-bookmark/src/ui.rs index 0b142574..d2943f81 100644 --- a/bin/domain/imag-bookmark/src/ui.rs +++ b/bin/domain/imag-bookmark/src/ui.rs @@ -19,7 +19,6 @@ use clap::{Arg, App, SubCommand}; -use libimagentrytag::ui::tag_add_arg; use libimagutil::cli_validators::*; pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { @@ -44,7 +43,6 @@ pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { .value_name("URL") .validator(is_url) .help("Add this URL, multiple possible")) - .arg(tag_add_arg()) ) .subcommand(SubCommand::with_name("remove") diff --git a/bin/domain/imag-diary/src/create.rs b/bin/domain/imag-diary/src/create.rs index 3d66599b..69760399 100644 --- a/bin/domain/imag-diary/src/create.rs +++ b/bin/domain/imag-diary/src/create.rs @@ -19,16 +19,18 @@ use std::process::exit; +use clap::ArgMatches; + use libimagdiary::diary::Diary; use libimagdiary::diaryid::DiaryId; use libimagdiary::error::DiaryErrorKind as DEK; use libimagdiary::error::ResultExt; use libimagentryedit::edit::Edit; use libimagrt::runtime::Runtime; -use libimagerror::trace::trace_error; -use libimagdiary::entry::Entry; -use libimagdiary::error::Result; +use libimagerror::trace::trace_error_exit; use libimagutil::warn_exit::warn_exit; +use libimagstore::store::FileLockEntry; +use libimagstore::store::Store; use util::get_diary_name; @@ -36,88 +38,90 @@ pub fn create(rt: &Runtime) { let diaryname = get_diary_name(rt) .unwrap_or_else( || warn_exit("No diary selected. Use either the configuration file or the commandline option", 1)); - let prevent_edit = rt.cli().subcommand_matches("create").unwrap().is_present("no-edit"); + let mut entry = create_entry(rt.store(), &diaryname, rt); - fn create_entry<'a>(diary: &'a Diary, rt: &Runtime) -> Result> { - use std::str::FromStr; - - let create = rt.cli().subcommand_matches("create").unwrap(); - if !create.is_present("timed") { - debug!("Creating non-timed entry"); - diary.new_entry_today() - } else { - let id = match create.value_of("timed") { - Some("h") | Some("hourly") => { - debug!("Creating hourly-timed entry"); - let time = DiaryId::now(String::from(diary.name())); - let hr = create - .value_of("hour") - .map(|v| { debug!("Creating hourly entry with hour = {:?}", v); v }) - .and_then(|s| { - FromStr::from_str(s) - .map_err(|_| warn!("Could not parse hour: '{}'", s)) - .ok() - }) - .unwrap_or(time.hour()); - - time.with_hour(hr).with_minute(0) - }, - - Some("m") | Some("minutely") => { - debug!("Creating minutely-timed entry"); - let time = DiaryId::now(String::from(diary.name())); - let hr = create - .value_of("hour") - .map(|h| { debug!("hour = {:?}", h); h }) - .and_then(|s| { - FromStr::from_str(s) - .map_err(|_| warn!("Could not parse hour: '{}'", s)) - .ok() - }) - .unwrap_or(time.hour()); - - let min = create - .value_of("minute") - .map(|m| { debug!("minute = {:?}", m); m }) - .and_then(|s| { - FromStr::from_str(s) - .map_err(|_| warn!("Could not parse minute: '{}'", s)) - .ok() - }) - .unwrap_or(time.minute()); - - time.with_hour(hr).with_minute(min) - }, - - Some(_) => { - warn!("Timed creation failed: Unknown spec '{}'", - create.value_of("timed").unwrap()); - exit(1); - }, - - None => warn_exit("Unexpected error, cannot continue", 1) - }; - - diary.new_entry_by_id(id) - } - } - - let diary = Diary::open(rt.store(), &diaryname[..]); - let res = create_entry(&diary, rt) - .and_then(|mut entry| { - if prevent_edit { - debug!("Not editing new diary entry"); - Ok(()) - } else { - debug!("Editing new diary entry"); - entry.edit_content(rt).chain_err(|| DEK::DiaryEditError) - } - }); + let res = if rt.cli().subcommand_matches("create").unwrap().is_present("no-edit") { + debug!("Not editing new diary entry"); + Ok(()) + } else { + debug!("Editing new diary entry"); + entry.edit_content(rt) + .chain_err(|| DEK::DiaryEditError) + }; if let Err(e) = res { - trace_error(&e); + trace_error_exit(&e, 1); } else { info!("Ok!"); } } +fn create_entry<'a>(diary: &'a Store, diaryname: &str, rt: &Runtime) -> FileLockEntry<'a> { + let create = rt.cli().subcommand_matches("create").unwrap(); + let entry = if !create.is_present("timed") { + debug!("Creating non-timed entry"); + diary.new_entry_today(diaryname) + } else { + let id = create_id_from_clispec(&create, &diaryname); + diary.retrieve(id).chain_err(|| DEK::StoreReadError) + }; + + match entry { + Err(e) => trace_error_exit(&e, 1), + Ok(e) => { + debug!("Created: {}", e.get_location()); + e + } + } +} + + +fn create_id_from_clispec(create: &ArgMatches, diaryname: &str) -> DiaryId { + use std::str::FromStr; + + let get_hourly_id = |create: &ArgMatches| -> DiaryId { + let time = DiaryId::now(String::from(diaryname)); + let hr = create + .value_of("hour") + .map(|v| { debug!("Creating hourly entry with hour = {:?}", v); v }) + .and_then(|s| { + FromStr::from_str(s) + .map_err(|_| warn!("Could not parse hour: '{}'", s)) + .ok() + }) + .unwrap_or(time.hour()); + + time.with_hour(hr) + }; + + match create.value_of("timed") { + Some("h") | Some("hourly") => { + debug!("Creating hourly-timed entry"); + get_hourly_id(create) + }, + + Some("m") | Some("minutely") => { + let time = get_hourly_id(create); + let min = create + .value_of("minute") + .map(|m| { debug!("minute = {:?}", m); m }) + .and_then(|s| { + FromStr::from_str(s) + .map_err(|_| warn!("Could not parse minute: '{}'", s)) + .ok() + }) + .unwrap_or(time.minute()); + + time.with_minute(min) + }, + + Some(_) => { + warn!("Timed creation failed: Unknown spec '{}'", + create.value_of("timed").unwrap()); + exit(1); + }, + + None => warn_exit("Unexpected error, cannot continue", 1) + } +} + diff --git a/bin/domain/imag-diary/src/delete.rs b/bin/domain/imag-diary/src/delete.rs index da44bc92..17f24a91 100644 --- a/bin/domain/imag-diary/src/delete.rs +++ b/bin/domain/imag-diary/src/delete.rs @@ -17,15 +17,17 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // +use std::process::exit; + use chrono::naive::NaiveDateTime; -use libimagdiary::diary::Diary; use libimagdiary::diaryid::DiaryId; use libimagrt::runtime::Runtime; use libimagerror::trace::trace_error_exit; use libimagtimeui::datetime::DateTime; use libimagtimeui::parse::Parse; use libimagutil::warn_exit::warn_exit; +use libimagstore::storeid::IntoStoreId; use util::get_diary_name; @@ -35,36 +37,34 @@ pub fn delete(rt: &Runtime) { let diaryname = get_diary_name(rt) .unwrap_or_else(|| warn_exit("No diary selected. Use either the configuration file or the commandline option", 1)); - let diary = Diary::open(rt.store(), &diaryname[..]); - debug!("Diary opened: {:?}", diary); - - let datetime : Option = rt + let to_del_location = rt .cli() .subcommand_matches("delete") .unwrap() .value_of("datetime") .map(|dt| { debug!("DateTime = {:?}", dt); dt }) .and_then(DateTime::parse) - .map(|dt| dt.into()); + .map(|dt| dt.into()) + .ok_or_else(|| { + warn!("Not deleting entries, because missing date/time specification"); + exit(1); + }) + .and_then(|dt: NaiveDateTime| { + DiaryId::from_datetime(diaryname.clone(), dt) + .into_storeid() + .map(|id| rt.store().retrieve(id)) + .unwrap_or_else(|e| trace_error_exit(&e, 1)) + }) + .unwrap_or_else(|e| trace_error_exit(&e, 1)) + .get_location() + .clone(); - let to_del = match datetime { - Some(dt) => Some(diary.retrieve(DiaryId::from_datetime(diaryname.clone(), dt))), - None => diary.get_youngest_entry(), - }; - - let to_del = match to_del { - Some(Ok(e)) => e, - - Some(Err(e)) => trace_error_exit(&e, 1), - None => warn_exit("No entry", 1) - }; - - if !ask_bool(&format!("Deleting {:?}", to_del.get_location())[..], Some(true)) { + if !ask_bool(&format!("Deleting {:?}", to_del_location), Some(true)) { info!("Aborting delete action"); return; } - if let Err(e) = diary.delete_entry(to_del) { + if let Err(e) = rt.store().delete(to_del_location) { trace_error_exit(&e, 1) } diff --git a/bin/domain/imag-diary/src/edit.rs b/bin/domain/imag-diary/src/edit.rs index b83e916b..c3c99813 100644 --- a/bin/domain/imag-diary/src/edit.rs +++ b/bin/domain/imag-diary/src/edit.rs @@ -17,6 +17,8 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // +use std::process::exit; + use chrono::naive::NaiveDateTime; use libimagdiary::diary::Diary; @@ -30,33 +32,39 @@ use libimagerror::trace::MapErrTrace; use libimagtimeui::datetime::DateTime; use libimagtimeui::parse::Parse; use libimagutil::warn_exit::warn_exit; +use libimagerror::trace::trace_error_exit; use util::get_diary_name; pub fn edit(rt: &Runtime) { let diaryname = get_diary_name(rt).unwrap_or_else(|| warn_exit("No diary name", 1)); - let diary = Diary::open(rt.store(), &diaryname[..]); - let datetime : Option = rt - .cli() + rt.cli() .subcommand_matches("edit") .unwrap() .value_of("datetime") .and_then(DateTime::parse) - .map(|dt| dt.into()); - - let to_edit = match datetime { - Some(dt) => Some(diary.retrieve(DiaryId::from_datetime(diaryname.clone(), dt))), - None => diary.get_youngest_entry(), - }; - - match to_edit { - Some(Ok(mut e)) => e.edit_content(rt).chain_err(|| DEK::IOError), - - Some(Err(e)) => Err(e), - None => Err(DE::from_kind(DEK::EntryNotInDiary)), - } - .map_err_trace().ok(); + .map(|dt| dt.into()) + .map(|dt: NaiveDateTime| DiaryId::from_datetime(diaryname.clone(), dt)) + .or_else(|| { + rt.store() + .get_youngest_entry_id(&diaryname) + .map(|optid| match optid { + Ok(id) => id, + Err(e) => trace_error_exit(&e, 1), + }) + }) + .ok_or_else(|| { + error!("No entries in diary. Aborting"); + exit(1) + }) + .and_then(|id| rt.store().get(id)) + .map(|opte| match opte { + Some(mut e) => e.edit_content(rt).chain_err(|| DEK::IOError), + None => Err(DE::from_kind(DEK::EntryNotInDiary)), + }) + .map_err_trace() + .ok(); } diff --git a/bin/domain/imag-diary/src/list.rs b/bin/domain/imag-diary/src/list.rs index f915712f..768ebc45 100644 --- a/bin/domain/imag-diary/src/list.rs +++ b/bin/domain/imag-diary/src/list.rs @@ -42,9 +42,7 @@ pub fn list(rt: &Runtime) { .unwrap_or(String::from("<>")) } - let diary = Diary::open(rt.store(), &diaryname[..]); - debug!("Diary opened: {:?}", diary); - diary.entries() + Diary::entries(rt.store(), &diaryname) .and_then(|es| { debug!("Iterator for listing: {:?}", es); diff --git a/bin/domain/imag-diary/src/view.rs b/bin/domain/imag-diary/src/view.rs index 041a1fe0..1e60383b 100644 --- a/bin/domain/imag-diary/src/view.rs +++ b/bin/domain/imag-diary/src/view.rs @@ -27,10 +27,9 @@ use util::get_diary_name; pub fn view(rt: &Runtime) { let diaryname = get_diary_name(rt).unwrap_or_else(|| warn_exit("No diary name", 1)); - let diary = Diary::open(rt.store(), &diaryname[..]); let hdr = rt.cli().subcommand_matches("view").unwrap().is_present("show-header"); - diary.entries() + Diary::entries(rt.store(), &diaryname) .and_then(|entries| DV::new(hdr).view_entries(entries.into_iter().filter_map(Result::ok))) .map_err_trace() .ok(); diff --git a/bin/domain/imag-mail/Cargo.toml b/bin/domain/imag-mail/Cargo.toml index 606fdfed..b9b31787 100644 --- a/bin/domain/imag-mail/Cargo.toml +++ b/bin/domain/imag-mail/Cargo.toml @@ -17,5 +17,4 @@ version = "2.0.1" libimagrt = { version = "0.4.0", path = "../../../lib/core/libimagrt" } libimagerror = { version = "0.4.0", path = "../../../lib/core/libimagerror" } libimagmail = { version = "0.4.0", path = "../../../lib/domain/libimagmail" } -libimagentryref = { version = "0.4.0", path = "../../../lib/entry/libimagentryref" } libimagutil = { version = "0.4.0", path = "../../../lib/etc/libimagutil" } diff --git a/bin/domain/imag-mail/src/main.rs b/bin/domain/imag-mail/src/main.rs index 02c107c5..7987e281 100644 --- a/bin/domain/imag-mail/src/main.rs +++ b/bin/domain/imag-mail/src/main.rs @@ -25,11 +25,9 @@ extern crate libimagrt; extern crate libimagmail; extern crate libimagerror; extern crate libimagutil; -extern crate libimagentryref; use libimagerror::trace::{MapErrTrace, trace_error, trace_error_exit}; use libimagmail::mail::Mail; -use libimagentryref::reference::Ref; use libimagrt::runtime::Runtime; use libimagrt::setup::generate_runtime_setup; use libimagutil::info_result::*; @@ -74,11 +72,11 @@ fn list(rt: &Runtime) { let iter = match store.retrieve_for_module("ref") { Ok(iter) => iter.filter_map(|id| { - Ref::get(store, id) - .chain_err(|| MEK::RefHandlingError) - .and_then(|rf| Mail::from_ref(rf)) - .map_err_trace() - .ok() + match store.get(id).chain_err(|| MEK::RefHandlingError).map_err_trace() { + Ok(Some(fle)) => Mail::from_fle(fle).map_err_trace().ok(), + Ok(None) => None, + Err(e) => trace_error_exit(&e, 1), + } }), Err(e) => trace_error_exit(&e, 1), }; diff --git a/bin/domain/imag-notes/Cargo.toml b/bin/domain/imag-notes/Cargo.toml index 475f36f5..180fe61a 100644 --- a/bin/domain/imag-notes/Cargo.toml +++ b/bin/domain/imag-notes/Cargo.toml @@ -23,5 +23,4 @@ libimagrt = { version = "0.4.0", path = "../../../lib/core/libimagrt" } libimagerror = { version = "0.4.0", path = "../../../lib/core/libimagerror" } libimagnotes = { version = "0.4.0", path = "../../../lib/domain/libimagnotes" } libimagentryedit = { version = "0.4.0", path = "../../../lib/entry/libimagentryedit" } -libimagentrytag = { version = "0.4.0", path = "../../../lib/entry/libimagentrytag" } libimagutil = { version = "0.4.0", path = "../../../lib/etc/libimagutil" } diff --git a/bin/domain/imag-notes/src/main.rs b/bin/domain/imag-notes/src/main.rs index 5d07f5b7..83c54fab 100644 --- a/bin/domain/imag-notes/src/main.rs +++ b/bin/domain/imag-notes/src/main.rs @@ -25,7 +25,6 @@ extern crate itertools; extern crate libimagnotes; extern crate libimagrt; extern crate libimagentryedit; -extern crate libimagentrytag; extern crate libimagerror; extern crate libimagutil; diff --git a/bin/domain/imag-notes/src/ui.rs b/bin/domain/imag-notes/src/ui.rs index 906c5524..f9ae7390 100644 --- a/bin/domain/imag-notes/src/ui.rs +++ b/bin/domain/imag-notes/src/ui.rs @@ -19,8 +19,6 @@ use clap::{Arg, App, SubCommand}; -use libimagentrytag::ui::tag_argument; - pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { app .subcommand(SubCommand::with_name("create") @@ -62,8 +60,6 @@ pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { .required(true) .help("Edit Note with this name") .value_name("NAME")) - - .arg(tag_argument()) ) .subcommand(SubCommand::with_name("list") diff --git a/bin/domain/imag-todo/src/main.rs b/bin/domain/imag-todo/src/main.rs index 43545d2a..20ad60e4 100644 --- a/bin/domain/imag-todo/src/main.rs +++ b/bin/domain/imag-todo/src/main.rs @@ -35,7 +35,7 @@ use toml::Value; use libimagrt::runtime::Runtime; use libimagrt::setup::generate_runtime_setup; -use libimagtodo::task::Task; +use libimagtodo::taskstore::TaskStore; use libimagerror::trace::{MapErrTrace, trace_error, trace_error_exit}; mod ui; @@ -61,9 +61,11 @@ fn tw_hook(rt: &Runtime) { let subcmd = rt.cli().subcommand_matches("tw-hook").unwrap(); if subcmd.is_present("add") { let stdin = stdin(); - let stdin = stdin.lock(); // implements BufRead which is required for `Task::import()` - match Task::import(rt.store(), stdin) { + // implements BufRead which is required for `Store::import_task_from_reader()` + let stdin = stdin.lock(); + + match rt.store().import_task_from_reader(stdin) { Ok((_, line, uuid)) => println!("{}\nTask {} stored in imag", line, uuid), Err(e) => trace_error_exit(&e, 1), } @@ -71,7 +73,7 @@ fn tw_hook(rt: &Runtime) { // The used hook is "on-modify". This hook gives two json-objects // per usage und wants one (the second one) back. let stdin = stdin(); - Task::delete_by_imports(rt.store(), stdin.lock()).map_err_trace().ok(); + rt.store().delete_tasks_by_imports(stdin.lock()).map_err_trace().ok(); } else { // Should not be possible, as one argument is required via // ArgGroup @@ -92,30 +94,35 @@ fn list(rt: &Runtime) { is_match!(e.kind(), &::toml_query::error::ErrorKind::IdentifierNotFoundInDocument(_)) }; - let res = Task::all(rt.store()) // get all tasks + let res = rt.store().all_tasks() // get all tasks .map(|iter| { // and if this succeeded // filter out the ones were we can read the uuid - let uuids : Vec<_> = iter.filter_map(|t| match t { - Ok(v) => match v.get_header().read(&String::from("todo.uuid")) { - Ok(Some(&Value::String(ref u))) => Some(u.clone()), - Ok(Some(_)) => { - warn!("Header type error"); - None - }, - Ok(None) => { - warn!("Header missing field"); - None + let uuids : Vec<_> = iter.filter_map(|storeid| { + match rt.store().retrieve(storeid) { + Ok(fle) => { + match fle.get_header().read(&String::from("todo.uuid")) { + Ok(Some(&Value::String(ref u))) => Some(u.clone()), + Ok(Some(_)) => { + error!("Header type error, expected String at 'todo.uuid' in {}", + fle.get_location()); + None + }, + Ok(None) => { + error!("Header missing field in {}", fle.get_location()); + None + }, + Err(e) => { + if !no_identifier(&e) { + trace_error(&e); + } + None + } + } }, Err(e) => { - if !no_identifier(&e) { - trace_error(&e); - } + trace_error(&e); None - } - }, - Err(e) => { - trace_error(&e); - None + }, } }) .collect(); diff --git a/doc/src/09020-changelog.md b/doc/src/09020-changelog.md index 738ec9a1..f0105f4b 100644 --- a/doc/src/09020-changelog.md +++ b/doc/src/09020-changelog.md @@ -25,6 +25,9 @@ This section contains the changelog from the last release to the next release. * The codebase was moved to a more tree-ish approach, where several subdirectories were introduced for different types of crates * The documentation got a major overhaul and was partly rewritten + * The logger is now configurable via the config file. +* New + * `libimagentrygps` was introduced * Fixed bugs * The config loading in `libimagrt` [was fixed](http://git.imag-pim.org/imag/commit/?id=9193d50f96bce099665d2eb716bcaa29a8d9b8ff). diff --git a/imagrc.toml b/imagrc.toml index 5c5598b5..955f5238 100644 --- a/imagrc.toml +++ b/imagrc.toml @@ -1,6 +1,35 @@ # This is a example configuration file for the imag suite. # It is written in TOML +[imag.logging] +level = "debug" +destinations = [ "-" ] + +# Valid variables for logging: +# * "level" +# * "module_path" +# * "file" +# * "line" +# * "target" +# * "message" +# +# Valid functions to be applied: +# * "black" +# * "blue" +# * "cyan" +# * "green" +# * "purple" +# * "red" +# * "white" +# * "yellow" + +[imag.logging.format] +trace = "[imag][{{red level}}][{{module_path}}]: {{message}}" +debug = "[imag][{{cyan level}}]: {{message}}" +info = "[imag]: {{message}}" +warn = "[imag][{{bold level}}]: {{yellow message}}" +error = "[imag][{{red level}}]: {{red message}}" + # # Configuration options for the user interface # diff --git a/lib/core/libimagrt/Cargo.toml b/lib/core/libimagrt/Cargo.toml index 90b2dcbd..5f8fe240 100644 --- a/lib/core/libimagrt/Cargo.toml +++ b/lib/core/libimagrt/Cargo.toml @@ -24,7 +24,16 @@ ansi_term = "0.9" is-match = "0.1" toml-query = "0.3.0" error-chain = "0.10" +handlebars = "0.29.0" libimagstore = { version = "0.4.0", path = "../../../lib/core/libimagstore" } libimagerror = { version = "0.4.0", path = "../../../lib/core/libimagerror" } libimagutil = { version = "0.4.0", path = "../../../lib/etc/libimagutil" } + +[features] +default = [] + +# Enable testing functionality. Used for building the libimagrt for testing CLI +# apps. Do not use in production! +testing = [] + diff --git a/lib/core/libimagrt/src/error.rs b/lib/core/libimagrt/src/error.rs index 69055197..3ecca79e 100644 --- a/lib/core/libimagrt/src/error.rs +++ b/lib/core/libimagrt/src/error.rs @@ -38,6 +38,71 @@ error_chain! { display("Process exited with failure") } + IOLogFileOpenError { + description("IO Error: Could not open logfile") + display("IO Error: Could not open logfile") + } + + ConfigReadError { + description("Error while reading the configuration") + display("Error while reading the configuration") + } + + ConfigTypeError { + description("Error while reading the configuration: Type Error") + display("Error while reading the configuration: Type Error") + } + + GlobalLogLevelConfigMissing { + description("Global config 'imag.logging.level' missing") + display("Global config 'imag.logging.level' missing") + } + + GlobalDestinationConfigMissing { + description("Global config 'imag.logging.destinations' missing") + display("Global config 'imag.logging.destinations' missing") + } + + InvalidLogLevelSpec { + description("Invalid log level specification: Only 'trace', 'debug', 'info', 'warn', 'error' are allowed") + display("Invalid log level specification: Only 'trace', 'debug', 'info', 'warn', 'error' are allowed") + } + + TomlReadError { + description("Error while reading in TOML document") + display("Error while reading in TOML document") + } + + TemplateStringRegistrationError { + description("Error while registering logging template string") + display("Error while registering logging template string") + } + + ConfigMissingLoggingFormatTrace { + description("Missing config for logging format for trace logging") + display("Missing config for logging format for trace logging") + } + + ConfigMissingLoggingFormatDebug { + description("Missing config for logging format for debug logging") + display("Missing config for logging format for debug logging") + } + + ConfigMissingLoggingFormatInfo { + description("Missing config for logging format for info logging") + display("Missing config for logging format for info logging") + } + + ConfigMissingLoggingFormatWarn { + description("Missing config for logging format for warn logging") + display("Missing config for logging format for warn logging") + } + + ConfigMissingLoggingFormatError { + description("Missing config for logging format for error logging") + display("Missing config for logging format for error logging") + } + } } diff --git a/lib/core/libimagrt/src/lib.rs b/lib/core/libimagrt/src/lib.rs index ed69f58d..486435ed 100644 --- a/lib/core/libimagrt/src/lib.rs +++ b/lib/core/libimagrt/src/lib.rs @@ -41,6 +41,7 @@ extern crate itertools; #[cfg(unix)] extern crate xdg_basedir; extern crate env_logger; extern crate ansi_term; +extern crate handlebars; extern crate clap; extern crate toml; diff --git a/lib/core/libimagrt/src/logger.rs b/lib/core/libimagrt/src/logger.rs index 992a9d94..8785e27b 100644 --- a/lib/core/libimagrt/src/logger.rs +++ b/lib/core/libimagrt/src/logger.rs @@ -19,67 +19,114 @@ use std::io::Write; use std::io::stderr; +use std::collections::BTreeMap; +use configuration::Configuration; +use error::RuntimeErrorKind as EK; +use error::RuntimeError as RE; +use error::ResultExt; +use runtime::Runtime; + +use clap::ArgMatches; use log::{Log, LogLevel, LogRecord, LogMetadata}; +use toml::Value; +use toml_query::read::TomlValueReadExt; +use handlebars::Handlebars; -use ansi_term::Style; -use ansi_term::Colour; -use ansi_term::ANSIString; +type ModuleName = String; +type Result = ::std::result::Result; + +enum LogDestination { + Stderr, + File(::std::fs::File), +} + +impl Default for LogDestination { + fn default() -> LogDestination { + LogDestination::Stderr + } +} + +struct ModuleSettings { + enabled: bool, + level: Option, + + #[allow(unused)] + destinations: Option>, +} /// Logger implementation for `log` crate. pub struct ImagLogger { - prefix: String, - dbg_fileline: bool, - lvl: LogLevel, - color_enabled: bool, + global_loglevel : LogLevel, + + #[allow(unused)] + global_destinations : Vec, + // global_format_trace : , + // global_format_debug : , + // global_format_info : , + // global_format_warn : , + // global_format_error : , + module_settings : BTreeMap, + + handlebars: Handlebars, } impl ImagLogger { /// Create a new ImagLogger object with a certain level - pub fn new(lvl: LogLevel) -> ImagLogger { - ImagLogger { - prefix: "[imag]".to_owned(), - dbg_fileline: true, - lvl: lvl, - color_enabled: true + pub fn new(matches: &ArgMatches, config: Option<&Configuration>) -> Result { + let mut handlebars = Handlebars::new(); + + handlebars.register_helper("black" , Box::new(self::template_helpers::ColorizeBlackHelper)); + handlebars.register_helper("blue" , Box::new(self::template_helpers::ColorizeBlueHelper)); + handlebars.register_helper("cyan" , Box::new(self::template_helpers::ColorizeCyanHelper)); + handlebars.register_helper("green" , Box::new(self::template_helpers::ColorizeGreenHelper)); + handlebars.register_helper("purple" , Box::new(self::template_helpers::ColorizePurpleHelper)); + handlebars.register_helper("red" , Box::new(self::template_helpers::ColorizeRedHelper)); + handlebars.register_helper("white" , Box::new(self::template_helpers::ColorizeWhiteHelper)); + handlebars.register_helper("yellow" , Box::new(self::template_helpers::ColorizeYellowHelper)); + + handlebars.register_helper("underline" , Box::new(self::template_helpers::UnderlineHelper)); + handlebars.register_helper("bold" , Box::new(self::template_helpers::BoldHelper)); + handlebars.register_helper("blink" , Box::new(self::template_helpers::BlinkHelper)); + handlebars.register_helper("strikethrough" , Box::new(self::template_helpers::StrikethroughHelper)); + + { + let fmt = try!(aggregate_global_format_trace(matches, config)); + try!(handlebars.register_template_string("TRACE", fmt) // name must be uppercase + .chain_err(|| EK::TemplateStringRegistrationError)); } - } - - /// Set debugging to include file and line - pub fn with_dbg_file_and_line(mut self, b: bool) -> ImagLogger { - self.dbg_fileline = b; - self - } - - /// Set debugging to include prefix - pub fn with_prefix(mut self, pref: String) -> ImagLogger { - self.prefix = pref; - self - } - - /// Set debugging to have color - pub fn with_color(mut self, b: bool) -> ImagLogger { - self.color_enabled = b; - self - } - - /// Helper function to colorize a string with a certain Style - fn style_or_not(&self, c: Style, s: String) -> ANSIString { - if self.color_enabled { - c.paint(s) - } else { - ANSIString::from(s) + { + let fmt = try!(aggregate_global_format_debug(matches, config)); + try!(handlebars.register_template_string("DEBUG", fmt) // name must be uppercase + .chain_err(|| EK::TemplateStringRegistrationError)); } + { + let fmt = try!(aggregate_global_format_info(matches, config)); + try!(handlebars.register_template_string("INFO", fmt) // name must be uppercase + .chain_err(|| EK::TemplateStringRegistrationError)); + } + { + let fmt = try!(aggregate_global_format_warn(matches, config)); + try!(handlebars.register_template_string("WARN", fmt) // name must be uppercase + .chain_err(|| EK::TemplateStringRegistrationError)); + } + { + let fmt = try!(aggregate_global_format_error(matches, config)); + try!(handlebars.register_template_string("ERROR", fmt) // name must be uppercase + .chain_err(|| EK::TemplateStringRegistrationError)); + } + + Ok(ImagLogger { + global_loglevel : try!(aggregate_global_loglevel(matches, config)), + global_destinations : try!(aggregate_global_destinations(matches, config)), + module_settings : try!(aggregate_module_settings(matches, config)), + handlebars : handlebars, + }) } - /// Helper function to colorize a string with a certain Color - fn color_or_not(&self, c: Colour, s: String) -> ANSIString { - if self.color_enabled { - c.paint(s) - } else { - ANSIString::from(s) - } + pub fn global_loglevel(&self) -> LogLevel { + self.global_loglevel } } @@ -87,47 +134,432 @@ impl ImagLogger { impl Log for ImagLogger { fn enabled(&self, metadata: &LogMetadata) -> bool { - metadata.level() <= self.lvl + metadata.level() <= self.global_loglevel } fn log(&self, record: &LogRecord) { - use ansi_term::Colour::Red; - use ansi_term::Colour::Yellow; - use ansi_term::Colour::Cyan; + if record.location().module_path().starts_with("handlebars") { + // This is a ugly, yet necessary hack. When logging, we use handlebars for templating. + // But as the handlebars library itselfs logs via a normal logging macro ("debug!()"), + // we have a recursion in our chain. + // + // To prevent this recursion, we return here. + // + // (As of handlebars 0.29.0 - please check whether you can update handlebars if you see + // this. Hopefully the next version has a compiletime flag to disable logging) + return; + } - if self.enabled(record.metadata()) { - // TODO: This is just simple logging. Maybe we can enhance this lateron - let loc = record.location(); - match record.metadata().level() { - LogLevel::Debug => { - let lvl = self.color_or_not(Cyan, format!("{}", record.level())); - let args = self.color_or_not(Cyan, format!("{}", record.args())); - if self.dbg_fileline { - let file = self.color_or_not(Cyan, format!("{}", loc.file())); - let ln = self.color_or_not(Cyan, format!("{}", loc.line())); + let mut data = BTreeMap::new(); - writeln!(stderr(), "{}[{: <5}][{}][{: >5}]: {}", self.prefix, lvl, file, ln, args).ok(); - } else { - writeln!(stderr(), "{}[{: <5}]: {}", self.prefix, lvl, args).ok(); - } - }, - LogLevel::Warn | LogLevel::Error => { - let lvl = self.style_or_not(Red.blink(), format!("{}", record.level())); - let args = self.color_or_not(Red, format!("{}", record.args())); + { + data.insert("level", format!("{}", record.level())); + data.insert("module_path", String::from(record.location().module_path())); + data.insert("file", String::from(record.location().file())); + data.insert("line", format!("{}", record.location().line())); + data.insert("target", String::from(record.target())); + data.insert("message", format!("{}", record.args())); + } - writeln!(stderr(), "{}[{: <5}]: {}", self.prefix, lvl, args).ok(); - }, - LogLevel::Info => { - let lvl = self.color_or_not(Yellow, format!("{}", record.level())); - let args = self.color_or_not(Yellow, format!("{}", record.args())); + let logtext = self + .handlebars + .render(&format!("{}", record.level()), &data) + .unwrap_or_else(|e| format!("Failed rendering logging data: {:?}\n", e)); - writeln!(stderr(), "{}[{: <5}]: {}", self.prefix, lvl, args).ok(); - }, - _ => { - writeln!(stderr(), "{}[{: <5}]: {}", self.prefix, record.level(), record.args()).ok(); - }, + self.module_settings + .get(record.target()) + .map(|module_setting| { + let set = module_setting.enabled && + module_setting.level.unwrap_or(self.global_loglevel) >= record.level(); + + if set { + let _ = write!(stderr(), "{}\n", logtext); + } + }) + .unwrap_or_else(|| { + if self.global_loglevel >= record.level() { + // Yes, we log + let _ = write!(stderr(), "{}\n", logtext); + } + }); + } +} + +fn match_log_level_str(s: &str) -> Result { + match s { + "trace" => Ok(LogLevel::Trace), + "debug" => Ok(LogLevel::Debug), + "info" => Ok(LogLevel::Info), + "warn" => Ok(LogLevel::Warn), + "error" => Ok(LogLevel::Error), + _ => return Err(RE::from_kind(EK::InvalidLogLevelSpec)), + } +} + +fn aggregate_global_loglevel(matches: &ArgMatches, config: Option<&Configuration>) + -> Result +{ + match config { + Some(cfg) => match cfg + .read("imag.logging.level") + .chain_err(|| EK::ConfigReadError) + { + Ok(Some(&Value::String(ref s))) => match_log_level_str(s), + Ok(Some(_)) => Err(RE::from_kind(EK::ConfigTypeError)), + Ok(None) => Err(RE::from_kind(EK::GlobalLogLevelConfigMissing)), + Err(e) => Err(e) + }, + None => { + if matches.is_present(Runtime::arg_debugging_name()) { + return Ok(LogLevel::Debug) + } + + matches + .value_of(Runtime::arg_verbosity_name()) + .map(match_log_level_str) + .unwrap_or(Ok(LogLevel::Info)) + } + } +} + +fn translate_destination(raw: &str) -> Result { + use std::fs::OpenOptions; + + match raw { + "-" => Ok(LogDestination::Stderr), + other => { + OpenOptions::new() + .append(true) + .create(true) + .open(other) + .map(LogDestination::File) + .chain_err(|| EK::IOLogFileOpenError) + } + } +} + + +fn translate_destinations(raw: &Vec) -> Result> { + raw.iter() + .fold(Ok(vec![]), |acc, val| { + acc.and_then(|mut v| { + let dest = match *val { + Value::String(ref s) => try!(translate_destination(s)), + _ => return Err(RE::from_kind(EK::ConfigTypeError)), + }; + v.push(dest); + Ok(v) + }) + }) +} + +fn aggregate_global_destinations(matches: &ArgMatches, config: Option<&Configuration>) + -> Result> +{ + + match config { + Some(cfg) => match cfg + .read("imag.logging.destinations") + .chain_err(|| EK::ConfigReadError) + { + Ok(Some(&Value::Array(ref a))) => translate_destinations(a), + Ok(Some(_)) => Err(RE::from_kind(EK::ConfigTypeError)), + Ok(None) => Err(RE::from_kind(EK::GlobalDestinationConfigMissing)), + Err(e) => Err(e) + }, + None => { + if let Some(values) = matches.value_of(Runtime::arg_logdest_name()) { + // parse logdest specification from commandline + + values.split(",") + .fold(Ok(vec![]), move |acc, dest| { + acc.and_then(|mut v| { + v.push(try!(translate_destination(dest))); + Ok(v) + }) + }) + } else { + Ok(vec![ LogDestination::default() ]) } } } } +#[inline] +fn aggregate_global_format( + read_str: &str, + cli_match_name: &str, + error_kind_if_missing: EK, + matches: &ArgMatches, + config: Option<&Configuration> + ) +-> Result +{ + match config { + Some(cfg) => match cfg + .read(read_str) + .chain_err(|| EK::ConfigReadError) + { + Ok(Some(&Value::String(ref s))) => Ok(s.clone()), + Ok(Some(_)) => Err(RE::from_kind(EK::ConfigTypeError)), + Ok(None) => Err(RE::from_kind(error_kind_if_missing)), + Err(e) => Err(e) + }, + None => match matches.value_of(cli_match_name).map(String::from) { + Some(s) => Ok(s), + None => Err(RE::from_kind(error_kind_if_missing)) + } + } +} + +fn aggregate_global_format_trace(matches: &ArgMatches, config: Option<&Configuration>) + -> Result +{ + aggregate_global_format("imag.logging.format.trace", + Runtime::arg_override_trace_logging_format(), + EK::ConfigMissingLoggingFormatTrace, + matches, + config) +} + +fn aggregate_global_format_debug(matches: &ArgMatches, config: Option<&Configuration>) + -> Result +{ + aggregate_global_format("imag.logging.format.debug", + Runtime::arg_override_debug_logging_format(), + EK::ConfigMissingLoggingFormatDebug, + matches, + config) +} + +fn aggregate_global_format_info(matches: &ArgMatches, config: Option<&Configuration>) + -> Result +{ + aggregate_global_format("imag.logging.format.info", + Runtime::arg_override_info_logging_format(), + EK::ConfigMissingLoggingFormatInfo, + matches, + config) +} + +fn aggregate_global_format_warn(matches: &ArgMatches, config: Option<&Configuration>) + -> Result +{ + aggregate_global_format("imag.logging.format.warn", + Runtime::arg_override_warn_logging_format(), + EK::ConfigMissingLoggingFormatWarn, + matches, + config) +} + +fn aggregate_global_format_error(matches: &ArgMatches, config: Option<&Configuration>) + -> Result +{ + aggregate_global_format("imag.logging.format.error", + Runtime::arg_override_error_logging_format(), + EK::ConfigMissingLoggingFormatError, + matches, + config) +} + +fn aggregate_module_settings(_matches: &ArgMatches, config: Option<&Configuration>) + -> Result> +{ + match config { + Some(cfg) => match cfg + .read("imag.logging.modules") + .chain_err(|| EK::ConfigReadError) + { + Ok(Some(&Value::Table(ref t))) => { + // translate the module settings from the table `t` + let mut settings = BTreeMap::new(); + + for (module_name, v) in t { + let destinations = try!(match v.read("destinations") { + Ok(Some(&Value::Array(ref a))) => translate_destinations(a).map(Some), + Ok(None) => Ok(None), + Ok(Some(_)) => Err(RE::from_kind(EK::ConfigTypeError)), + Err(e) => Err(e).chain_err(|| EK::TomlReadError), + }); + + let level = try!(match v.read("level") { + Ok(Some(&Value::String(ref s))) => match_log_level_str(s).map(Some), + Ok(None) => Ok(None), + Ok(Some(_)) => Err(RE::from_kind(EK::ConfigTypeError)), + Err(e) => Err(e).chain_err(|| EK::TomlReadError), + }); + + let enabled = try!(match v.read("enabled") { + Ok(Some(&Value::Boolean(b))) => Ok(b), + Ok(None) => Ok(false), + Ok(Some(_)) => Err(RE::from_kind(EK::ConfigTypeError)), + Err(e) => Err(e).chain_err(|| EK::TomlReadError), + }); + + let module_settings = ModuleSettings { + enabled: enabled, + level: level, + destinations: destinations, + }; + + // We don't care whether there was a value, we override it. + let _ = settings.insert(module_name.to_owned(), module_settings); + } + + Ok(settings) + }, + Ok(Some(_)) => Err(RE::from_kind(EK::ConfigTypeError)), + Ok(None) => { + // No modules configured. This is okay! + Ok(BTreeMap::new()) + }, + Err(e) => Err(e), + }, + None => { + write!(stderr(), "No Configuration.").ok(); + write!(stderr(), "cannot find module-settings for logging.").ok(); + write!(stderr(), "Will use global defaults").ok(); + + Ok(BTreeMap::new()) + } + } +} + +mod template_helpers { + use handlebars::{Handlebars, HelperDef, JsonRender, RenderError, RenderContext, Helper}; + use ansi_term::Colour; + use ansi_term::Style; + + #[derive(Clone, Copy)] + pub struct ColorizeBlackHelper; + + impl HelperDef for ColorizeBlackHelper { + fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> { + colorize(Colour::Black, h, hb, rc) + } + } + + #[derive(Clone, Copy)] + pub struct ColorizeBlueHelper; + + impl HelperDef for ColorizeBlueHelper { + fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> { + colorize(Colour::Blue, h, hb, rc) + } + } + + #[derive(Clone, Copy)] + pub struct ColorizeCyanHelper; + + impl HelperDef for ColorizeCyanHelper { + fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> { + colorize(Colour::Cyan, h, hb, rc) + } + } + + #[derive(Clone, Copy)] + pub struct ColorizeGreenHelper; + + impl HelperDef for ColorizeGreenHelper { + fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> { + colorize(Colour::Green, h, hb, rc) + } + } + + #[derive(Clone, Copy)] + pub struct ColorizePurpleHelper; + + impl HelperDef for ColorizePurpleHelper { + fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> { + colorize(Colour::Purple, h, hb, rc) + } + } + + #[derive(Clone, Copy)] + pub struct ColorizeRedHelper; + + impl HelperDef for ColorizeRedHelper { + fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> { + colorize(Colour::Red, h, hb, rc) + } + } + + #[derive(Clone, Copy)] + pub struct ColorizeWhiteHelper; + + impl HelperDef for ColorizeWhiteHelper { + fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> { + colorize(Colour::White, h, hb, rc) + } + } + + #[derive(Clone, Copy)] + pub struct ColorizeYellowHelper; + + impl HelperDef for ColorizeYellowHelper { + fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> { + colorize(Colour::Yellow, h, hb, rc) + } + } + + fn colorize(color: Colour, h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> { + let p = try!(h.param(0).ok_or(RenderError::new("Too few arguments"))); + + try!(write!(rc.writer(), "{}", color.paint(p.value().render()))); + Ok(()) + } + + #[derive(Clone, Copy)] + pub struct UnderlineHelper; + + impl HelperDef for UnderlineHelper { + fn call(&self, h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(), + RenderError> { + let p = try!(h.param(0).ok_or(RenderError::new("Too few arguments"))); + let s = Style::new().underline(); + try!(write!(rc.writer(), "{}", s.paint(p.value().render()))); + Ok(()) + } + } + + #[derive(Clone, Copy)] + pub struct BoldHelper; + + impl HelperDef for BoldHelper { + fn call(&self, h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(), + RenderError> { + let p = try!(h.param(0).ok_or(RenderError::new("Too few arguments"))); + let s = Style::new().bold(); + try!(write!(rc.writer(), "{}", s.paint(p.value().render()))); + Ok(()) + } + } + + #[derive(Clone, Copy)] + pub struct BlinkHelper; + + impl HelperDef for BlinkHelper { + fn call(&self, h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(), + RenderError> { + let p = try!(h.param(0).ok_or(RenderError::new("Too few arguments"))); + let s = Style::new().blink(); + try!(write!(rc.writer(), "{}", s.paint(p.value().render()))); + Ok(()) + } + } + + #[derive(Clone, Copy)] + pub struct StrikethroughHelper; + + impl HelperDef for StrikethroughHelper { + fn call(&self, h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(), + RenderError> { + let p = try!(h.param(0).ok_or(RenderError::new("Too few arguments"))); + let s = Style::new().strikethrough(); + try!(write!(rc.writer(), "{}", s.paint(p.value().render()))); + Ok(()) + } + } + +} + diff --git a/lib/core/libimagrt/src/runtime.rs b/lib/core/libimagrt/src/runtime.rs index d0bbdf94..ffb27b51 100644 --- a/lib/core/libimagrt/src/runtime.rs +++ b/lib/core/libimagrt/src/runtime.rs @@ -20,14 +20,12 @@ use std::path::PathBuf; use std::process::Command; use std::env; -use std::io::stderr; -use std::io::Write; +use std::process::exit; pub use clap::App; use clap::{Arg, ArgMatches}; use log; -use log::LogLevelFilter; use configuration::{Configuration, InternalConfiguration}; use error::RuntimeError; @@ -35,6 +33,7 @@ use error::RuntimeErrorKind; use error::ResultExt; use logger::ImagLogger; +use libimagerror::trace::*; use libimagstore::store::Store; use libimagstore::file_abstraction::InMemoryFileAbstraction; use spec::CliSpec; @@ -77,8 +76,8 @@ impl<'a> Runtime<'a> { Err(e) => if !is_match!(e.kind(), &ConfigErrorKind::NoConfigFileFound) { return Err(e).chain_err(|| RuntimeErrorKind::Instantiate); } else { - warn!("No config file found."); - warn!("Continuing without configuration file"); + println!("No config file found."); + println!("Continuing without configuration file"); None }, @@ -111,16 +110,10 @@ impl<'a> Runtime<'a> { where C: Clone + CliSpec<'a> + InternalConfiguration { use std::io::stdout; - use clap::Shell; - let is_debugging = matches.is_present(Runtime::arg_debugging_name()); - if cli_app.enable_logging() { - let is_verbose = matches.is_present(Runtime::arg_verbosity_name()); - let colored = !matches.is_present(Runtime::arg_no_color_output_name()); - - Runtime::init_logger(is_debugging, is_verbose, colored); + Runtime::init_logger(&matches, config.as_ref()) } match matches.value_of(Runtime::arg_generate_compl()) { @@ -150,9 +143,9 @@ impl<'a> Runtime<'a> { None => None, }; - if is_debugging { - write!(stderr(), "Config: {:?}\n", config).ok(); - write!(stderr(), "Store-config: {:?}\n", store_config).ok(); + if matches.is_present(Runtime::arg_debugging_name()) { + debug!("Config: {:?}\n", config); + debug!("Store-config: {:?}\n", store_config); } let store_result = if cli_app.use_inmemory_fs() { @@ -199,9 +192,11 @@ impl<'a> Runtime<'a> { .arg(Arg::with_name(Runtime::arg_verbosity_name()) .short("v") .long("verbose") - .help("Enables verbosity") + .help("Enables verbosity, can be used to set log level to one of 'trace', 'debug', 'info', 'warn' or 'error'") .required(false) - .takes_value(false)) + .takes_value(true) + .possible_values(&["trace", "debug", "info", "warn", "error"]) + .value_name("LOGLEVEL")) .arg(Arg::with_name(Runtime::arg_debugging_name()) .long("debug") @@ -253,6 +248,61 @@ impl<'a> Runtime<'a> { .value_name("SHELL") .possible_values(&["bash", "fish", "zsh"])) + .arg(Arg::with_name(Runtime::arg_logdest_name()) + .long(Runtime::arg_logdest_name()) + .help("Override the logging destinations from the configuration: values can be seperated by ',', a value of '-' marks the stderr output, everything else is expected to be a path") + .required(false) + .takes_value(true) + .value_name("LOGDESTS")) + + .arg(Arg::with_name(Runtime::arg_override_module_logging_setting_name()) + .long(Runtime::arg_override_module_logging_setting_name()) + .help("Override a module logging setting for one module. Format: ==, whereas is either 'enabled', 'level' or 'destinations' - This commandline argument is CURRENTLY NOT IMPLEMENTED") + .multiple(true) + .required(false) + .takes_value(true) + .value_name("SPEC")) + + .arg(Arg::with_name(Runtime::arg_override_trace_logging_format()) + .long(Runtime::arg_override_trace_logging_format()) + .help("Override the logging format for the trace logging") + .multiple(false) + .required(false) + .takes_value(true) + .value_name("FMT")) + + .arg(Arg::with_name(Runtime::arg_override_debug_logging_format()) + .long(Runtime::arg_override_debug_logging_format()) + .help("Override the logging format for the debug logging") + .multiple(false) + .required(false) + .takes_value(true) + .value_name("FMT")) + + .arg(Arg::with_name(Runtime::arg_override_info_logging_format()) + .long(Runtime::arg_override_info_logging_format()) + .help("Override the logging format for the info logging") + .multiple(false) + .required(false) + .takes_value(true) + .value_name("FMT")) + + .arg(Arg::with_name(Runtime::arg_override_warn_logging_format()) + .long(Runtime::arg_override_warn_logging_format()) + .help("Override the logging format for the warn logging") + .multiple(false) + .required(false) + .takes_value(true) + .value_name("FMT")) + + .arg(Arg::with_name(Runtime::arg_override_error_logging_format()) + .long(Runtime::arg_override_error_logging_format()) + .help("Override the logging format for the error logging") + .multiple(false) + .required(false) + .takes_value(true) + .value_name("FMT")) + } /// Get the argument names of the Runtime which are available @@ -314,26 +364,73 @@ impl<'a> Runtime<'a> { "generate-completion" } + /// Extract the Store object from the Runtime object, destroying the Runtime object + /// + /// # Warning + /// + /// This function is for testing _only_! It can be used to re-build a Runtime object with an + /// alternative Store. + #[cfg(feature = "testing")] + pub fn extract_store(self) -> Store { + self.store + } + + /// Re-set the Store object within + /// + /// # Warning + /// + /// This function is for testing _only_! It can be used to re-build a Runtime object with an + /// alternative Store. + #[cfg(feature = "testing")] + pub fn with_store(mut self, s: Store) -> Self { + self.store = s; + self + } + + /// Get the argument name for the logging destination + pub fn arg_logdest_name() -> &'static str { + "logging-destinations" + } + + pub fn arg_override_module_logging_setting_name() -> &'static str { + "override-module-log-setting" + } + + pub fn arg_override_trace_logging_format() -> &'static str { + "override-logging-format-trace" + } + + pub fn arg_override_debug_logging_format() -> &'static str { + "override-logging-format-debug" + } + + pub fn arg_override_info_logging_format() -> &'static str { + "override-logging-format-info" + } + + pub fn arg_override_warn_logging_format() -> &'static str { + "override-logging-format-warn" + } + + pub fn arg_override_error_logging_format() -> &'static str { + "override-logging-format-error" + } + /// Initialize the internal logger - fn init_logger(is_debugging: bool, is_verbose: bool, colored: bool) { + fn init_logger(matches: &ArgMatches, config: Option<&Configuration>) { use std::env::var as env_var; use env_logger; if env_var("IMAG_LOG_ENV").is_ok() { env_logger::init().unwrap(); } else { - let lvl = if is_debugging { - LogLevelFilter::Debug - } else if is_verbose { - LogLevelFilter::Info - } else { - LogLevelFilter::Warn - }; - log::set_logger(|max_log_lvl| { - max_log_lvl.set(lvl); - debug!("Init logger with {}", lvl); - Box::new(ImagLogger::new(lvl.to_log_level().unwrap()).with_color(colored)) + let logger = ImagLogger::new(matches, config) + .map_err_trace() + .unwrap_or_else(|_| exit(1)); + max_log_lvl.set(logger.global_loglevel().to_log_level_filter()); + debug!("Init logger with {}", logger.global_loglevel()); + Box::new(logger) }) .map_err(|e| panic!("Could not setup logger: {:?}", e)) .ok(); diff --git a/lib/domain/libimagdiary/src/diary.rs b/lib/domain/libimagdiary/src/diary.rs index 8bc94865..f8eecded 100644 --- a/lib/domain/libimagdiary/src/diary.rs +++ b/lib/domain/libimagdiary/src/diary.rs @@ -19,110 +19,116 @@ use std::cmp::Ordering; +use libimagstore::store::FileLockEntry; use libimagstore::store::Store; -use libimagstore::storeid::IntoStoreId; use libimagerror::trace::trace_error; use chrono::offset::Local; use chrono::Datelike; use itertools::Itertools; use chrono::naive::NaiveDateTime; +use chrono::Timelike; -use entry::Entry; +use entry::DiaryEntry; use diaryid::DiaryId; -use error::DiaryError as DE; use error::DiaryErrorKind as DEK; use error::ResultExt; use error::Result; use iter::DiaryEntryIterator; -use is_in_diary::IsInDiary; +use iter::DiaryNameIterator; -#[derive(Debug)] -pub struct Diary<'a> { - store: &'a Store, - name: &'a str, -} - -impl<'a> Diary<'a> { - - pub fn open(store: &'a Store, name: &'a str) -> Diary<'a> { - Diary { - store: store, - name: name, - } - } +pub trait Diary { // create or get a new entry for today - pub fn new_entry_today(&self) -> Result { + fn new_entry_today(&self, diary_name: &str) -> Result; + + // create or get a new entry for now + fn new_entry_now(&self, diary_name: &str) -> Result; + + // Get an iterator for iterating over all entries of a Diary + fn entries(&self, diary_name: &str) -> Result; + + fn get_youngest_entry_id(&self, diary_name: &str) -> Option>; + + /// Get all diary names + fn diary_names(&self) -> Result; + +} + +impl Diary for Store { + + // create or get a new entry for today + fn new_entry_today(&self, diary_name: &str) -> Result { let dt = Local::now(); let ndt = dt.naive_local(); - let id = DiaryId::new(String::from(self.name), ndt.year(), ndt.month(), ndt.day(), 0, 0); - self.new_entry_by_id(id) + let id = DiaryId::new(String::from(diary_name), ndt.year(), ndt.month(), ndt.day(), 0, 0); + + self.retrieve(id).chain_err(|| DEK::StoreReadError) } - pub fn new_entry_by_id(&self, id: DiaryId) -> Result { - self.retrieve(id.with_diary_name(String::from(self.name))) - } + fn new_entry_now(&self, diary_name: &str) -> Result { + let dt = Local::now(); + let ndt = dt.naive_local(); + let id = DiaryId::new(String::from(diary_name), + ndt.year(), + ndt.month(), + ndt.day(), + ndt.hour(), + ndt.minute()); - pub fn retrieve(&self, id: DiaryId) -> Result { - id.into_storeid() - .and_then(|id| self.store.retrieve(id)) - .map(|fle| Entry::new(fle)) - .chain_err(|| DEK::StoreWriteError) + self.retrieve(id).chain_err(|| DEK::StoreReadError) } // Get an iterator for iterating over all entries - pub fn entries(&self) -> Result> { - self.store - .retrieve_for_module("diary") - .map(|iter| DiaryEntryIterator::new(self.name, self.store, iter)) + fn entries(&self, diary_name: &str) -> Result { + self.retrieve_for_module("diary") + .map(|iter| DiaryEntryIterator::new(self, String::from(diary_name), iter)) .chain_err(|| DEK::StoreReadError) } - pub fn delete_entry(&self, entry: Entry) -> Result<()> { - if !entry.is_in_diary(self.name) { - return Err(DE::from_kind(DEK::EntryNotInDiary)); - } - let id = entry.get_location().clone(); - drop(entry); - - self.store.delete(id).chain_err(|| DEK::StoreWriteError) - } - - pub fn get_youngest_entry(&self) -> Option> { - match self.entries() { + fn get_youngest_entry_id(&self, diary_name: &str) -> Option> { + match Diary::entries(self, diary_name) { Err(e) => Some(Err(e)), Ok(entries) => { - entries.sorted_by(|a, b| { - match (a, b) { - (&Ok(ref a), &Ok(ref b)) => { - let a : NaiveDateTime = a.diary_id().into(); - let b : NaiveDateTime = b.diary_id().into(); + entries + .map(|e| e.and_then(|e| e.diary_id())) + .sorted_by(|a, b| { + match (a, b) { + (&Ok(ref a), &Ok(ref b)) => { + let a : NaiveDateTime = a.clone().into(); + let b : NaiveDateTime = b.clone().into(); - a.cmp(&b) - }, + a.cmp(&b) + }, - (&Ok(_), &Err(ref e)) => { - trace_error(e); - Ordering::Less - }, - (&Err(ref e), &Ok(_)) => { - trace_error(e); - Ordering::Greater - }, - (&Err(ref e1), &Err(ref e2)) => { - trace_error(e1); - trace_error(e2); - Ordering::Equal - }, - } - }).into_iter().next() + (&Ok(_), &Err(ref e)) => { + trace_error(e); + Ordering::Less + }, + (&Err(ref e), &Ok(_)) => { + trace_error(e); + Ordering::Greater + }, + (&Err(ref e1), &Err(ref e2)) => { + trace_error(e1); + trace_error(e2); + Ordering::Equal + }, + } + }) + .into_iter() + //.map(|sidres| sidres.map(|sid| DiaryId::from_storeid(&sid))) + .next() } } } - pub fn name(&self) -> &'a str { - &self.name + /// Get all diary names + fn diary_names(&self) -> Result { + self.retrieve_for_module("diary") + .chain_err(|| DEK::StoreReadError) + .map(DiaryNameIterator::new) } + } diff --git a/lib/domain/libimagdiary/src/entry.rs b/lib/domain/libimagdiary/src/entry.rs index 4e654f5e..40206404 100644 --- a/lib/domain/libimagdiary/src/entry.rs +++ b/lib/domain/libimagdiary/src/entry.rs @@ -17,74 +17,24 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // -use std::ops::Deref; -use std::ops::DerefMut; - -use libimagstore::store::FileLockEntry; -use libimagentryedit::edit::Edit; -use libimagentryedit::error::Result as EditResult; -use libimagrt::runtime::Runtime; +use libimagstore::store::Entry; use diaryid::DiaryId; use diaryid::FromStoreId; +use error::Result; -#[derive(Debug)] -pub struct Entry<'a>(FileLockEntry<'a>); - -impl<'a> Deref for Entry<'a> { - type Target = FileLockEntry<'a>; - - fn deref(&self) -> &FileLockEntry<'a> { - &self.0 - } - +pub trait DiaryEntry { + fn diary_id(&self) -> Result; } -impl<'a> DerefMut for Entry<'a> { - - fn deref_mut(&mut self) -> &mut FileLockEntry<'a> { - &mut self.0 - } - -} - -impl<'a> Entry<'a> { - - pub fn new(fle: FileLockEntry<'a>) -> Entry<'a> { - Entry(fle) - } +impl DiaryEntry for Entry { /// Get the diary id for this entry. /// /// TODO: calls Option::unwrap() as it assumes that an existing Entry has an ID that is parsable - pub fn diary_id(&self) -> DiaryId { - DiaryId::from_storeid(&self.0.get_location().clone()).unwrap() + fn diary_id(&self) -> Result { + DiaryId::from_storeid(&self.get_location().clone()) } } -impl<'a> Into> for Entry<'a> { - - fn into(self) -> FileLockEntry<'a> { - self.0 - } - -} - -impl<'a> From> for Entry<'a> { - - fn from(fle: FileLockEntry<'a>) -> Entry<'a> { - Entry::new(fle) - } - -} - -impl<'a> Edit for Entry<'a> { - - fn edit_content(&mut self, rt: &Runtime) -> EditResult<()> { - self.0.edit_content(rt) - } - -} - - diff --git a/lib/domain/libimagdiary/src/error.rs b/lib/domain/libimagdiary/src/error.rs index 5418d05c..07975b9e 100644 --- a/lib/domain/libimagdiary/src/error.rs +++ b/lib/domain/libimagdiary/src/error.rs @@ -73,6 +73,11 @@ error_chain! { display("Error while parsing ID") } + DiaryNameFindingError { + description("Error while finding a diary name") + display("Error while finding a diary name") + } + } } diff --git a/lib/domain/libimagdiary/src/is_in_diary.rs b/lib/domain/libimagdiary/src/is_in_diary.rs index 09228072..8b48e1cc 100644 --- a/lib/domain/libimagdiary/src/is_in_diary.rs +++ b/lib/domain/libimagdiary/src/is_in_diary.rs @@ -24,6 +24,8 @@ pub trait IsInDiary { fn is_in_diary(&self, name: &str) -> bool; + fn is_a_diary_entry(&self) -> bool; + } impl IsInDiary for Entry { @@ -32,6 +34,10 @@ impl IsInDiary for Entry { self.get_location().clone().is_in_diary(name) } + fn is_a_diary_entry(&self) -> bool { + self.get_location().clone().is_a_diary_entry() + } + } impl IsInDiary for StoreId { @@ -40,5 +46,9 @@ impl IsInDiary for StoreId { self.local().starts_with(format!("diary/{}", name)) } + fn is_a_diary_entry(&self) -> bool { + self.local().starts_with("diary") + } + } diff --git a/lib/domain/libimagdiary/src/iter.rs b/lib/domain/libimagdiary/src/iter.rs index 63bd0779..8d22aff8 100644 --- a/lib/domain/libimagdiary/src/iter.rs +++ b/lib/domain/libimagdiary/src/iter.rs @@ -21,21 +21,22 @@ use std::fmt::{Debug, Formatter, Error as FmtError}; use std::result::Result as RResult; use libimagstore::store::Store; +use libimagstore::store::FileLockEntry; use libimagstore::storeid::StoreIdIterator; +use libimagerror::trace::trace_error; use diaryid::DiaryId; use diaryid::FromStoreId; use is_in_diary::IsInDiary; -use entry::Entry as DiaryEntry; use error::DiaryErrorKind as DEK; +use error::DiaryError as DE; use error::ResultExt; use error::Result; -use libimagerror::trace::trace_error; /// A iterator for iterating over diary entries pub struct DiaryEntryIterator<'a> { store: &'a Store, - name: &'a str, + name: String, iter: StoreIdIterator, year: Option, @@ -54,7 +55,7 @@ impl<'a> Debug for DiaryEntryIterator<'a> { impl<'a> DiaryEntryIterator<'a> { - pub fn new(diaryname: &'a str, store: &'a Store, iter: StoreIdIterator) -> DiaryEntryIterator<'a> { + pub fn new(store: &'a Store, diaryname: String, iter: StoreIdIterator) -> DiaryEntryIterator<'a> { DiaryEntryIterator { store: store, name: diaryname, @@ -87,9 +88,9 @@ impl<'a> DiaryEntryIterator<'a> { } impl<'a> Iterator for DiaryEntryIterator<'a> { - type Item = Result>; + type Item = Result>; - fn next(&mut self) -> Option>> { + fn next(&mut self) -> Option { loop { let next = match self.iter.next() { Some(s) => s, @@ -97,7 +98,7 @@ impl<'a> Iterator for DiaryEntryIterator<'a> { }; debug!("Next element: {:?}", next); - if next.is_in_diary(self.name) { + if next.is_in_diary(&self.name) { debug!("Seems to be in diary: {:?}", next); let id = match DiaryId::from_storeid(&next) { Ok(i) => i, @@ -118,9 +119,7 @@ impl<'a> Iterator for DiaryEntryIterator<'a> { return Some(self .store .retrieve(next) - .map(|fle| DiaryEntry::new(fle)) - .chain_err(|| DEK::StoreReadError) - ); + .chain_err(|| DEK::StoreReadError)); } } else { debug!("Not in the requested diary ({}): {:?}", self.name, next); @@ -130,3 +129,37 @@ impl<'a> Iterator for DiaryEntryIterator<'a> { } + +/// Get diary names. +/// +/// # Warning +/// +/// Does _not_ run a `unique` on the iterator! +pub struct DiaryNameIterator(StoreIdIterator); + +impl DiaryNameIterator { + pub fn new(s: StoreIdIterator) -> DiaryNameIterator { + DiaryNameIterator(s) + } +} + +impl Iterator for DiaryNameIterator { + type Item = Result; + + fn next(&mut self) -> Option { + self.0 + .next() + .map(|s| { + s.to_str() + .chain_err(|| DEK::DiaryNameFindingError) + .and_then(|s| { + s.split("diary/") + .nth(1) + .and_then(|n| n.split("/").nth(0).map(String::from)) + .ok_or(DE::from_kind(DEK::DiaryNameFindingError)) + }) + }) + } + +} + diff --git a/lib/domain/libimagdiary/src/viewer.rs b/lib/domain/libimagdiary/src/viewer.rs index e87bc202..b37061b1 100644 --- a/lib/domain/libimagdiary/src/viewer.rs +++ b/lib/domain/libimagdiary/src/viewer.rs @@ -19,13 +19,15 @@ //! A diary viewer built on libimagentryview. -use entry::Entry; +use entry::DiaryEntry; use error::DiaryErrorKind as DEK; use error::ResultExt; use error::Result; +use libimagstore::store::FileLockEntry; use libimagentryview::viewer::Viewer; use libimagentryview::builtin::plain::PlainViewer; +use libimagerror::trace::trace_error; /// This viewer does _not_ implement libimagentryview::viewer::Viewer because we need to be able to /// call some diary-type specific functions on the entries passed to this. @@ -46,10 +48,12 @@ impl DiaryViewer { /// View all entries from the iterator, or stop immediately if an error occurs, returning that /// error. - pub fn view_entries<'a, I: Iterator>>(&self, entries: I) -> Result<()> { + pub fn view_entries<'a, I: Iterator>>(&self, entries: I) -> Result<()> { for entry in entries { - let id = entry.diary_id(); - println!("{} :\n", id); + match entry.diary_id() { + Ok(id) => println!("{} :\n", id), + Err(e) => trace_error(&e), + } let _ = try!(self.0 .view_entry(&entry) .chain_err(|| DEK::ViewError) diff --git a/lib/domain/libimagmail/src/iter.rs b/lib/domain/libimagmail/src/iter.rs index 92cf2981..89a84539 100644 --- a/lib/domain/libimagmail/src/iter.rs +++ b/lib/domain/libimagmail/src/iter.rs @@ -27,16 +27,16 @@ use mail::Mail; use error::Result; -use libimagentryref::reference::Ref; +use libimagstore::store::FileLockEntry; use std::marker::PhantomData; -pub struct MailIter<'a, I: 'a + Iterator>> { - _marker: PhantomData<&'a I>, +pub struct MailIter<'a, I: Iterator>> { + _marker: PhantomData, i: I, } -impl<'a, I: Iterator>> MailIter<'a, I> { +impl<'a, I: Iterator>> MailIter<'a, I> { pub fn new(i: I) -> MailIter<'a, I> { MailIter { _marker: PhantomData, i: i } @@ -44,12 +44,11 @@ impl<'a, I: Iterator>> MailIter<'a, I> { } -impl<'a, I: Iterator>> Iterator for MailIter<'a, I> { - +impl<'a, I: Iterator>> Iterator for MailIter<'a, I> { type Item = Result>; - fn next(&mut self) -> Option>> { - self.i.next().map(Mail::from_ref) + fn next(&mut self) -> Option { + self.i.next().map(Mail::from_fle) } } diff --git a/lib/domain/libimagmail/src/mail.rs b/lib/domain/libimagmail/src/mail.rs index 426409cc..201bceee 100644 --- a/lib/domain/libimagmail/src/mail.rs +++ b/lib/domain/libimagmail/src/mail.rs @@ -23,8 +23,10 @@ use std::fs::File; use std::io::Read; use libimagstore::store::Store; +use libimagstore::store::FileLockEntry; use libimagentryref::reference::Ref; use libimagentryref::flags::RefFlags; +use libimagentryref::refstore::RefStore; use email::MimeMessage; use email::results::ParsingResult as EmailParsingResult; @@ -47,7 +49,7 @@ impl From for Buffer { } } -pub struct Mail<'a>(Ref<'a>, Buffer); +pub struct Mail<'a>(FileLockEntry<'a>, Buffer); impl<'a> Mail<'a> { @@ -58,7 +60,7 @@ impl<'a> Mail<'a> { let f = RefFlags::default().with_content_hashing(true).with_permission_tracking(false); let p = PathBuf::from(p.as_ref()); - Ref::create_with_hasher(store, p, f, h) + store.create_with_hasher(p, f, h) .chain_err(|| MEK::RefCreationError) .and_then(|reference| { debug!("Build reference file: {:?}", reference); @@ -79,20 +81,19 @@ impl<'a> Mail<'a> { /// Opens a mail by the passed hash pub fn open>(store: &Store, hash: S) -> Result> { debug!("Opening Mail by Hash"); - Ref::get_by_hash(store, String::from(hash.as_ref())) + store.get_by_hash(String::from(hash.as_ref())) .chain_err(|| MEK::FetchByHashError) .chain_err(|| MEK::FetchError) .and_then(|o| match o { - Some(r) => Mail::from_ref(r).map(Some), + Some(r) => Mail::from_fle(r).map(Some), None => Ok(None), }) } /// Implement me as TryFrom as soon as it is stable - pub fn from_ref(r: Ref<'a>) -> Result { - debug!("Building Mail object from Ref: {:?}", r); - r.fs_file() + pub fn from_fle(fle: FileLockEntry<'a>) -> Result> { + fle.fs_file() .chain_err(|| MEK::RefHandlingError) .and_then(|path| File::open(path).chain_err(|| MEK::IOError)) .and_then(|mut file| { @@ -102,7 +103,7 @@ impl<'a> Mail<'a> { .chain_err(|| MEK::IOError) }) .map(Buffer::from) - .map(|buffer| Mail(r, buffer)) + .map(|buffer| Mail(fle, buffer)) } pub fn get_field(&self, field: &str) -> Result> { diff --git a/lib/domain/libimagtodo/src/error.rs b/lib/domain/libimagtodo/src/error.rs index 90b33310..a65bba8a 100644 --- a/lib/domain/libimagtodo/src/error.rs +++ b/lib/domain/libimagtodo/src/error.rs @@ -48,6 +48,20 @@ error_chain! { display("Encountered non-UTF8 characters while reading input") } + HeaderFieldMissing { + description("Header field missing") + display("Header field missing") + } + + HeaderTypeError { + description("Header field type error") + display("Header field type error") + } + + UuidParserError { + description("Uuid parser error") + display("Uuid parser error") + } } } diff --git a/lib/domain/libimagtodo/src/lib.rs b/lib/domain/libimagtodo/src/lib.rs index b4313c27..2da11610 100644 --- a/lib/domain/libimagtodo/src/lib.rs +++ b/lib/domain/libimagtodo/src/lib.rs @@ -50,4 +50,5 @@ module_entry_path_mod!("todo"); pub mod error; pub mod task; +pub mod taskstore; diff --git a/lib/domain/libimagtodo/src/task.rs b/lib/domain/libimagtodo/src/task.rs index ec7b79e5..039bdbfb 100644 --- a/lib/domain/libimagtodo/src/task.rs +++ b/lib/domain/libimagtodo/src/task.rs @@ -17,272 +17,31 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // -use std::collections::BTreeMap; -use std::ops::{Deref, DerefMut}; -use std::io::BufRead; -use std::result::Result as RResult; - -use toml::Value; -use uuid::Uuid; - -use task_hookrs::task::Task as TTask; -use task_hookrs::import::{import_task, import_tasks}; - -use libimagstore::store::{FileLockEntry, Store}; -use libimagstore::storeid::{IntoStoreId, StoreIdIterator, StoreId}; -use module_path::ModuleEntryPath; - -use error::{TodoErrorKind as TEK, ResultExt}; use error::TodoError as TE; +use error::TodoErrorKind as TEK; +use error::ResultExt; use error::Result; -/// Task struct containing a `FileLockEntry` -#[derive(Debug)] -pub struct Task<'a>(FileLockEntry<'a>); +use libimagstore::store::Entry; -impl<'a> Task<'a> { - - /// Concstructs a new `Task` with a `FileLockEntry` - pub fn new(fle: FileLockEntry<'a>) -> Task<'a> { - Task(fle) - } - - pub fn import(store: &'a Store, mut r: R) -> Result<(Task<'a>, String, Uuid)> { - let mut line = String::new(); - try!(r.read_line(&mut line).chain_err(|| TEK::UTF8Error)); - import_task(&line.as_str()) - .map_err(|_| TE::from_kind(TEK::ImportError)) - .and_then(|t| { - let uuid = t.uuid().clone(); - t.into_task(store).map(|t| (t, line, uuid)) - }) - } - - /// Get a task from an import string. That is: read the imported string, get the UUID from it - /// and try to load this UUID from store. - /// - /// Possible return values are: - /// - /// * Ok(Ok(Task)) - /// * Ok(Err(String)) - where the String is the String read from the `r` parameter - /// * Err(_) - where the error is an error that happened during evaluation - /// - pub fn get_from_import(store: &'a Store, mut r: R) -> Result, String>> - where R: BufRead - { - let mut line = String::new(); - try!(r.read_line(&mut line).chain_err(|| TEK::UTF8Error)); - Task::get_from_string(store, line) - } - - /// Get a task from a String. The String is expected to contain the JSON-representation of the - /// Task to get from the store (only the UUID really matters in this case) - /// - /// For an explanation on the return values see `Task::get_from_import()`. - pub fn get_from_string(store: &'a Store, s: String) -> Result, String>> { - import_task(s.as_str()) - .map_err(|_| TE::from_kind(TEK::ImportError)) - .map(|t| t.uuid().clone()) - .and_then(|uuid| Task::get_from_uuid(store, uuid)) - .and_then(|o| match o { - None => Ok(Err(s)), - Some(t) => Ok(Ok(t)), - }) - } - - /// Get a task from an UUID. - /// - /// If there is no task with this UUID, this returns `Ok(None)`. - pub fn get_from_uuid(store: &'a Store, uuid: Uuid) -> Result>> { - ModuleEntryPath::new(format!("taskwarrior/{}", uuid)) - .into_storeid() - .and_then(|store_id| store.get(store_id)) - .map(|o| o.map(Task::new)) - .chain_err(|| TEK::StoreError) - } - - /// Same as Task::get_from_import() but uses Store::retrieve() rather than Store::get(), to - /// implicitely create the task if it does not exist. - pub fn retrieve_from_import(store: &'a Store, mut r: R) -> Result> { - let mut line = String::new(); - try!(r.read_line(&mut line).chain_err(|| TEK::UTF8Error)); - Task::retrieve_from_string(store, line) - } - - /// Retrieve a task from a String. The String is expected to contain the JSON-representation of - /// the Task to retrieve from the store (only the UUID really matters in this case) - pub fn retrieve_from_string(store: &'a Store, s: String) -> Result> { - Task::get_from_string(store, s) - .and_then(|opt| match opt { - Ok(task) => Ok(task), - Err(string) => import_task(string.as_str()) - .map_err(|_| TE::from_kind(TEK::ImportError)) - .and_then(|t| t.into_task(store)), - }) - } - - pub fn delete_by_imports(store: &Store, r: R) -> Result<()> { - use serde_json::ser::to_string as serde_to_string; - use task_hookrs::status::TaskStatus; - - for (counter, res_ttask) in import_tasks(r).into_iter().enumerate() { - match res_ttask { - Ok(ttask) => { - if counter % 2 == 1 { - // Only every second task is needed, the first one is the - // task before the change, and the second one after - // the change. The (maybe modified) second one is - // expected by taskwarrior. - match serde_to_string(&ttask).chain_err(|| TEK::ImportError) { - // use println!() here, as we talk with TW - Ok(val) => println!("{}", val), - Err(e) => return Err(e), - } - - // Taskwarrior does not have the concept of deleted tasks, but only modified - // ones. - // - // Here we check if the status of a task is deleted and if yes, we delete it - // from the store. - if *ttask.status() == TaskStatus::Deleted { - match Task::delete_by_uuid(store, *ttask.uuid()) { - Ok(_) => info!("Deleted task {}", *ttask.uuid()), - Err(e) => return Err(e), - } - } - } // end if c % 2 - }, - Err(e) => return Err(e).map_err(|_| TE::from_kind(TEK::ImportError)), - } - } - Ok(()) - } - - pub fn delete_by_uuid(store: &Store, uuid: Uuid) -> Result<()> { - ModuleEntryPath::new(format!("taskwarrior/{}", uuid)) - .into_storeid() - .and_then(|id| store.delete(id)) - .chain_err(|| TEK::StoreError) - } - - pub fn all_as_ids(store: &Store) -> Result { - store.retrieve_for_module("todo/taskwarrior") - .chain_err(|| TEK::StoreError) - } - - pub fn all(store: &Store) -> Result { - Task::all_as_ids(store) - .map(|iter| TaskIterator::new(store, iter)) - } +use uuid::Uuid; +use toml::Value; +use toml_query::read::TomlValueReadExt; +pub trait Task { + fn get_uuid(&self) -> Result; } -impl<'a> Deref for Task<'a> { - type Target = FileLockEntry<'a>; - - fn deref(&self) -> &FileLockEntry<'a> { - &self.0 - } - -} - -impl<'a> DerefMut for Task<'a> { - - fn deref_mut(&mut self) -> &mut FileLockEntry<'a> { - &mut self.0 - } - -} - -/// A trait to get a `libimagtodo::task::Task` out of the implementing object. -pub trait IntoTask<'a> { - - /// # Usage - /// ```ignore - /// use std::io::stdin; - /// - /// use task_hookrs::task::Task; - /// use task_hookrs::import::import; - /// use libimagstore::store::{Store, FileLockEntry}; - /// - /// if let Ok(task_hookrs_task) = import(stdin()) { - /// // Store is given at runtime - /// let task = task_hookrs_task.into_filelockentry(store); - /// println!("Task with uuid: {}", task.flentry.get_header().get("todo.uuid")); - /// } - /// ``` - fn into_task(self, store : &'a Store) -> Result>; - -} - -impl<'a> IntoTask<'a> for TTask { - - fn into_task(self, store : &'a Store) -> Result> { - use toml_query::read::TomlValueReadExt; - use toml_query::set::TomlValueSetExt; - - let uuid = self.uuid(); - ModuleEntryPath::new(format!("taskwarrior/{}", uuid)) - .into_storeid() - .chain_err(|| TEK::StoreIdError) - .and_then(|id| { - store.retrieve(id) - .chain_err(|| TEK::StoreError) - .and_then(|mut fle| { - { - let hdr = fle.get_header_mut(); - if try!(hdr.read("todo").chain_err(|| TEK::StoreError)).is_none() { - try!(hdr - .set("todo", Value::Table(BTreeMap::new())) - .chain_err(|| TEK::StoreError)); - } - - try!(hdr.set("todo.uuid", Value::String(format!("{}",uuid))) - .chain_err(|| TEK::StoreError)); - } - - // If none of the errors above have returned the function, everything is fine - Ok(Task::new(fle)) - }) - }) - } - -} - -trait FromStoreId { - fn from_storeid<'a>(&'a Store, StoreId) -> Result>; -} - -impl<'a> FromStoreId for Task<'a> { - - fn from_storeid<'b>(store: &'b Store, id: StoreId) -> Result> { - store.retrieve(id) - .chain_err(|| TEK::StoreError) - .map(Task::new) - } -} - -pub struct TaskIterator<'a> { - store: &'a Store, - iditer: StoreIdIterator, -} - -impl<'a> TaskIterator<'a> { - - pub fn new(store: &'a Store, iditer: StoreIdIterator) -> TaskIterator<'a> { - TaskIterator { - store: store, - iditer: iditer, +impl Task for Entry { + fn get_uuid(&self) -> Result { + match self.get_header().read("todo.uuid") { + Ok(Some(&Value::String(ref uuid))) => { + Uuid::parse_str(uuid).chain_err(|| TEK::UuidParserError) + }, + Ok(Some(_)) => Err(TE::from_kind(TEK::HeaderTypeError)), + Ok(None) => Err(TE::from_kind(TEK::HeaderFieldMissing)), + Err(e) => Err(e).chain_err(|| TEK::StoreError), } } - -} - -impl<'a> Iterator for TaskIterator<'a> { - type Item = Result>; - - fn next(&mut self) -> Option>> { - self.iditer.next().map(|id| Task::from_storeid(self.store, id)) - } } diff --git a/lib/domain/libimagtodo/src/taskstore.rs b/lib/domain/libimagtodo/src/taskstore.rs new file mode 100644 index 00000000..4a1dc2fe --- /dev/null +++ b/lib/domain/libimagtodo/src/taskstore.rs @@ -0,0 +1,207 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015, 2016 Matthias Beyer 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::BTreeMap; +use std::io::BufRead; +use std::result::Result as RResult; + +use toml::Value; +use uuid::Uuid; + +use task_hookrs::task::Task as TTask; +use task_hookrs::import::{import_task, import_tasks}; + +use libimagstore::store::{FileLockEntry, Store}; +use libimagstore::storeid::{IntoStoreId, StoreIdIterator}; +use module_path::ModuleEntryPath; + +use error::TodoErrorKind as TEK; +use error::TodoError as TE; +use error::Result; +use error::ResultExt; + +/// Task struct containing a `FileLockEntry` +pub trait TaskStore<'a> { + fn import_task_from_reader(&'a self, r: R) -> Result<(FileLockEntry<'a>, String, Uuid)>; + fn get_task_from_import(&'a self, r: R) -> Result, String>>; + fn get_task_from_string(&'a self, s: String) -> Result, String>>; + fn get_task_from_uuid(&'a self, uuid: Uuid) -> Result>>; + fn retrieve_task_from_import(&'a self, r: R) -> Result>; + fn retrieve_task_from_string(&'a self, s: String) -> Result>; + fn delete_tasks_by_imports(&self, r: R) -> Result<()>; + fn delete_task_by_uuid(&self, uuid: Uuid) -> Result<()>; + fn all_tasks(&self) -> Result; + fn new_from_twtask(&'a self, task: TTask) -> Result>; +} + +impl<'a> TaskStore<'a> for Store { + + fn import_task_from_reader(&'a self, mut r: R) -> Result<(FileLockEntry<'a>, String, Uuid)> { + let mut line = String::new(); + try!(r.read_line(&mut line).map_err(|_| TE::from_kind(TEK::UTF8Error))); + import_task(&line.as_str()) + .map_err(|_| TE::from_kind(TEK::ImportError)) + .and_then(|t| { + let uuid = t.uuid().clone(); + self.new_from_twtask(t).map(|t| (t, line, uuid)) + }) + } + + /// Get a task from an import string. That is: read the imported string, get the UUID from it + /// and try to load this UUID from store. + /// + /// Possible return values are: + /// + /// * Ok(Ok(Task)) + /// * Ok(Err(String)) - where the String is the String read from the `r` parameter + /// * Err(_) - where the error is an error that happened during evaluation + /// + fn get_task_from_import(&'a self, mut r: R) -> Result, String>> { + let mut line = String::new(); + try!(r.read_line(&mut line).chain_err(|| TEK::UTF8Error)); + self.get_task_from_string(line) + } + + /// Get a task from a String. The String is expected to contain the JSON-representation of the + /// Task to get from the store (only the UUID really matters in this case) + /// + /// For an explanation on the return values see `Task::get_from_import()`. + fn get_task_from_string(&'a self, s: String) -> Result, String>> { + import_task(s.as_str()) + .map_err(|_| TE::from_kind(TEK::ImportError)) + .map(|t| t.uuid().clone()) + .and_then(|uuid| self.get_task_from_uuid(uuid)) + .and_then(|o| match o { + None => Ok(Err(s)), + Some(t) => Ok(Ok(t)), + }) + } + + /// Get a task from an UUID. + /// + /// If there is no task with this UUID, this returns `Ok(None)`. + fn get_task_from_uuid(&'a self, uuid: Uuid) -> Result>> { + ModuleEntryPath::new(format!("taskwarrior/{}", uuid)) + .into_storeid() + .and_then(|store_id| self.get(store_id)) + .chain_err(|| TEK::StoreError) + } + + /// Same as Task::get_from_import() but uses Store::retrieve() rather than Store::get(), to + /// implicitely create the task if it does not exist. + fn retrieve_task_from_import(&'a self, mut r: R) -> Result> { + let mut line = String::new(); + try!(r.read_line(&mut line).chain_err(|| TEK::UTF8Error)); + self.retrieve_task_from_string(line) + } + + /// Retrieve a task from a String. The String is expected to contain the JSON-representation of + /// the Task to retrieve from the store (only the UUID really matters in this case) + fn retrieve_task_from_string(&'a self, s: String) -> Result> { + self.get_task_from_string(s) + .and_then(|opt| match opt { + Ok(task) => Ok(task), + Err(string) => import_task(string.as_str()) + .map_err(|_| TE::from_kind(TEK::ImportError)) + .and_then(|t| self.new_from_twtask(t)), + }) + } + + fn delete_tasks_by_imports(&self, r: R) -> Result<()> { + use serde_json::ser::to_string as serde_to_string; + use task_hookrs::status::TaskStatus; + + for (counter, res_ttask) in import_tasks(r).into_iter().enumerate() { + match res_ttask { + Ok(ttask) => { + if counter % 2 == 1 { + // Only every second task is needed, the first one is the + // task before the change, and the second one after + // the change. The (maybe modified) second one is + // expected by taskwarrior. + match serde_to_string(&ttask).chain_err(|| TEK::ImportError) { + // use println!() here, as we talk with TW + Ok(val) => println!("{}", val), + Err(e) => return Err(e), + } + + // Taskwarrior does not have the concept of deleted tasks, but only modified + // ones. + // + // Here we check if the status of a task is deleted and if yes, we delete it + // from the store. + if *ttask.status() == TaskStatus::Deleted { + match self.delete_task_by_uuid(*ttask.uuid()) { + Ok(_) => info!("Deleted task {}", *ttask.uuid()), + Err(e) => return Err(e), + } + } + } // end if c % 2 + }, + Err(e) => return Err(TE::from_kind(TEK::ImportError)), + } + } + Ok(()) + } + + fn delete_task_by_uuid(&self, uuid: Uuid) -> Result<()> { + ModuleEntryPath::new(format!("taskwarrior/{}", uuid)) + .into_storeid() + .and_then(|id| self.delete(id)) + .chain_err(|| TEK::StoreError) + } + + fn all_tasks(&self) -> Result { + self.retrieve_for_module("todo/taskwarrior") + .chain_err(|| TEK::StoreError) + } + + fn new_from_twtask(&'a self, task: TTask) -> Result> { + use toml_query::read::TomlValueReadExt; + use toml_query::set::TomlValueSetExt; + + let uuid = task.uuid(); + ModuleEntryPath::new(format!("taskwarrior/{}", uuid)) + .into_storeid() + .chain_err(|| TEK::StoreIdError) + .and_then(|id| { + self.retrieve(id) + .chain_err(|| TEK::StoreError) + .and_then(|mut fle| { + { + let hdr = fle.get_header_mut(); + if try!(hdr.read("todo").chain_err(|| TEK::StoreError)).is_none() { + try!(hdr + .set("todo", Value::Table(BTreeMap::new())) + .chain_err(|| TEK::StoreError)); + } + + try!(hdr.set("todo.uuid", Value::String(format!("{}",uuid))) + .chain_err(|| TEK::StoreError)); + } + + // If none of the errors above have returned the function, everything is fine + Ok(fle) + }) + }) + + } + +} + diff --git a/lib/entry/libimagentrygps/Cargo.toml b/lib/entry/libimagentrygps/Cargo.toml new file mode 100644 index 00000000..a8d8c771 --- /dev/null +++ b/lib/entry/libimagentrygps/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "libimagentrygps" +version = "0.4.0" +authors = ["Matthias Beyer "] + +description = "Library for the imag core distribution" + +keywords = ["imag", "PIM", "personal", "information", "management"] +readme = "../README.md" +license = "LGPL-2.1" + +[dependencies] +toml = "^0.4" +toml-query = "0.3.0" +serde_derive = "1" +serde = "1" +error-chain = "0.10" + +libimagstore = { version = "0.4.0", path = "../../../lib/core/libimagstore" } +libimagerror = { version = "0.4.0", path = "../../../lib/core/libimagerror" } + +[dev-dependencies] +env_logger = "0.3" + diff --git a/lib/entry/libimagentrygps/src/entry.rs b/lib/entry/libimagentrygps/src/entry.rs new file mode 100644 index 00000000..0a4bcf7d --- /dev/null +++ b/lib/entry/libimagentrygps/src/entry.rs @@ -0,0 +1,123 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015, 2016 Matthias Beyer 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 error::Result; +use error::GPSErrorKind as GPSEK; +use error::ResultExt; +use types::*; + +use libimagstore::store::Entry; + +use toml_query::read::TomlValueReadExt; +use toml_query::insert::TomlValueInsertExt; + +pub trait GPSEntry { + + fn set_coordinates(&mut self, c: Coordinates) -> Result<()>; + fn get_coordinates(&self) -> Result>; + +} + +impl GPSEntry for Entry { + + fn set_coordinates(&mut self, c: Coordinates) -> Result<()> { + self.get_header_mut() + .insert("gps.coordinates", c.into()) + .map(|_| ()) + .chain_err(|| GPSEK::HeaderWriteError) + } + + fn get_coordinates(&self) -> Result> { + match self.get_header().read("gps.coordinates").chain_err(|| GPSEK::HeaderWriteError) { + Ok(Some(hdr)) => Coordinates::from_value(hdr).map(Some), + Ok(None) => Ok(None), + Err(e) => Err(e), + } + } + +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use libimagstore::store::Store; + + use entry::*; + + fn setup_logging() { + use env_logger; + let _ = env_logger::init().unwrap_or(()); + } + + fn get_store() -> Store { + Store::new(PathBuf::from("/"), None).unwrap() + } + + #[test] + fn test_set_gps() { + setup_logging(); + + let store = get_store(); + + let mut entry = store.create(PathBuf::from("test_set_gps")).unwrap(); + + let coordinates = Coordinates { + latitude: GPSValue::new(0, 0, 0), + longitude: GPSValue::new(0, 0, 0), + }; + + let res = entry.set_coordinates(coordinates); + + assert!(res.is_ok()); + } + + #[test] + fn test_setget_gps() { + setup_logging(); + + let store = get_store(); + + let mut entry = store.create(PathBuf::from("test_setget_gps")).unwrap(); + + let coordinates = Coordinates { + latitude: GPSValue::new(0, 0, 0), + longitude: GPSValue::new(0, 0, 0), + }; + + let res = entry.set_coordinates(coordinates); + assert!(res.is_ok()); + + let coordinates = entry.get_coordinates(); + + assert!(coordinates.is_ok()); + let coordinates = coordinates.unwrap(); + + assert!(coordinates.is_some()); + let coordinates = coordinates.unwrap(); + + assert_eq!(0, coordinates.longitude.degree); + assert_eq!(0, coordinates.longitude.minutes); + assert_eq!(0, coordinates.longitude.seconds); + assert_eq!(0, coordinates.latitude.degree); + assert_eq!(0, coordinates.latitude.minutes); + assert_eq!(0, coordinates.latitude.seconds); + } +} + diff --git a/lib/entry/libimagentrygps/src/error.rs b/lib/entry/libimagentrygps/src/error.rs new file mode 100644 index 00000000..b40f9536 --- /dev/null +++ b/lib/entry/libimagentrygps/src/error.rs @@ -0,0 +1,87 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015, 2016 Matthias Beyer 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 +// + +error_chain! { + types { + GPSError, GPSErrorKind, ResultExt, Result; + } + + errors { + StoreReadError { + description("Store read error") + display("Store read error") + } + + StoreWriteError { + description("Store write error") + display("Store write error") + } + + HeaderWriteError { + description("Couldn't write Header for annotation") + display("Couldn't write Header for annotation") + } + + HeaderReadError { + description("Couldn't read Header of Entry") + display("Couldn't read Header of Entry") + } + + HeaderTypeError { + description("Header field has unexpected type") + display("Header field has unexpected type") + } + + TypeError { + description("Type Error") + display("Type Error") + } + + DegreeMissing { + description("'degree' value missing") + display("'degree' value missing") + } + + MinutesMissing { + description("'minutes' value missing") + display("'minutes' value missing") + } + + SecondsMissing { + description("'seconds' value missing") + display("'seconds' value missing") + } + + LongitudeMissing { + description("'longitude' value missing") + display("'longitude' value missing") + } + + LatitudeMissing { + description("'latitude' value missing") + display("'latitude' value missing") + } + + NumberConversionError { + description("Cannot convert number to fit into variable") + display("Cannot convert number to fit into variable") + } + } +} + diff --git a/lib/entry/libimagentrytag/src/exec.rs b/lib/entry/libimagentrygps/src/lib.rs similarity index 59% rename from lib/entry/libimagentrytag/src/exec.rs rename to lib/entry/libimagentrygps/src/lib.rs index 57ad3512..7df3a23a 100644 --- a/lib/entry/libimagentrytag/src/exec.rs +++ b/lib/entry/libimagentrygps/src/lib.rs @@ -17,30 +17,18 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // -use clap::ArgMatches; +extern crate toml; +extern crate toml_query; +#[macro_use] extern crate serde_derive; +#[macro_use] extern crate error_chain; -use libimagstore::store::FileLockEntry; +extern crate libimagstore; +#[macro_use] extern crate libimagerror; -use error::Result; -use tagable::*; -use ui::{get_add_tags, get_remove_tags}; +#[cfg(test)] +extern crate env_logger; -pub fn exec_cli_for_entry(matches: &ArgMatches, entry: &mut FileLockEntry) -> Result<()> { - if let Some(ts) = get_add_tags(matches) { - for t in ts { - if let Err(e) = entry.add_tag(t) { - return Err(e); - } - } - } +pub mod entry; +pub mod error; +pub mod types; - if let Some(ts) = get_remove_tags(matches) { - for t in ts { - if let Err(e) = entry.remove_tag(t) { - return Err(e); - } - } - } - - Ok(()) -} diff --git a/lib/entry/libimagentrygps/src/types.rs b/lib/entry/libimagentrygps/src/types.rs new file mode 100644 index 00000000..587b17b4 --- /dev/null +++ b/lib/entry/libimagentrygps/src/types.rs @@ -0,0 +1,156 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015, 2016 Matthias Beyer 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::BTreeMap; + +use toml::Value; + +use error::GPSErrorKind as GPSEK; +use error::GPSError as GPSE; +use error::Result; +use error::ResultExt; + +pub trait FromValue : Sized { + fn from_value(v: &Value) -> Result; +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct GPSValue { + pub degree: i8, + pub minutes: i8, + pub seconds: i8 +} + +impl GPSValue { + + pub fn new(d: i8, m: i8, s: i8) -> GPSValue { + GPSValue { + degree: d, + minutes: m, + seconds: s + } + } +} + + +impl Into for GPSValue { + + fn into(self) -> Value { + let mut map = BTreeMap::new(); + let _ = map.insert("degree".to_owned(), Value::Integer(self.degree as i64)); + let _ = map.insert("minutes".to_owned(), Value::Integer(self.minutes as i64)); + let _ = map.insert("seconds".to_owned(), Value::Integer(self.seconds as i64)); + Value::Table(map) + } + +} + +impl FromValue for GPSValue { + fn from_value(v: &Value) -> Result { + match *v { + Value::Table(ref map) => { + Ok(GPSValue::new( + map.get("degree") + .ok_or_else(|| GPSE::from_kind(GPSEK::DegreeMissing)) + .and_then(|v| match *v { + Value::Integer(i) => i64_to_i8(i), + _ => Err(GPSE::from_kind(GPSEK::HeaderTypeError)), + })?, + + map + .get("minutes") + .ok_or_else(|| GPSE::from_kind(GPSEK::MinutesMissing)) + .and_then(|v| match *v { + Value::Integer(i) => i64_to_i8(i), + _ => Err(GPSE::from_kind(GPSEK::HeaderTypeError)), + })?, + + map + .get("seconds") + .ok_or_else(|| GPSE::from_kind(GPSEK::SecondsMissing)) + .and_then(|v| match *v { + Value::Integer(i) => i64_to_i8(i), + _ => Err(GPSE::from_kind(GPSEK::HeaderTypeError)), + })? + )) + } + _ => Err(GPSE::from_kind(GPSEK::TypeError)) + } + } + +} + +/// Data-transfer type for transfering longitude-latitude-pairs +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct Coordinates { + pub longitude: GPSValue, + pub latitude: GPSValue, +} + +impl Coordinates { + pub fn new(long: GPSValue, lat: GPSValue) -> Coordinates { + Coordinates { + longitude: long, + latitude: lat, + } + } +} + +impl Into for Coordinates { + + fn into(self) -> Value { + let mut map = BTreeMap::new(); + let _ = map.insert("longitude".to_owned(), self.longitude.into()); + let _ = map.insert("latitude".to_owned(), self.latitude.into()); + Value::Table(map) + } + +} + +impl FromValue for Coordinates { + fn from_value(v: &Value) -> Result { + match *v { + Value::Table(ref map) => { + Ok(Coordinates::new( + match map.get("longitude") { + Some(v) => GPSValue::from_value(v), + None => Err(GPSE::from_kind(GPSEK::LongitudeMissing)), + }?, + + match map.get("latitude") { + Some(v) => GPSValue::from_value(v), + None => Err(GPSE::from_kind(GPSEK::LongitudeMissing)), + }? + )) + } + _ => Err(GPSE::from_kind(GPSEK::TypeError)) + } + } + +} + +/// Helper to convert a i64 to i8 or return an error if this doesn't work. +fn i64_to_i8(i: i64) -> Result { + if i > (::max_value() as i64) { + Err(GPSE::from_kind(GPSEK::NumberConversionError)) + } else { + Ok(i as i8) + } +} + diff --git a/lib/entry/libimagentrylink/src/external.rs b/lib/entry/libimagentrylink/src/external.rs index b13ecbc5..7da4f345 100644 --- a/lib/entry/libimagentrylink/src/external.rs +++ b/lib/entry/libimagentrylink/src/external.rs @@ -35,7 +35,6 @@ use std::collections::BTreeMap; use std::fmt::Debug; use libimagstore::store::Entry; -use libimagstore::store::FileLockEntry; use libimagstore::store::Store; use libimagstore::storeid::StoreId; use libimagstore::storeid::IntoStoreId; @@ -58,37 +57,32 @@ use url::Url; use crypto::sha1::Sha1; use crypto::digest::Digest; -/// "Link" Type, just an abstraction over `FileLockEntry` to have some convenience internally. -pub struct Link<'a> { - link: FileLockEntry<'a> +pub trait Link { + + fn get_link_uri_from_filelockentry(&self) -> Result>; + + fn get_url(&self) -> Result>; + } -impl<'a> Link<'a> { +impl Link for Entry { - pub fn new(fle: FileLockEntry<'a>) -> Link<'a> { - Link { link: fle } - } - - /// Get a link Url object from a `FileLockEntry`, ignore errors. - fn get_link_uri_from_filelockentry(file: &FileLockEntry<'a>) -> Option { - file.get_header() + fn get_link_uri_from_filelockentry(&self) -> Result> { + self.get_header() .read("imag.content.url") - .ok() + .chain_err(|| LEK::EntryHeaderReadError) .and_then(|opt| match opt { Some(&Value::String(ref s)) => { debug!("Found url, parsing: {:?}", s); - Url::parse(&s[..]).ok() + Url::parse(&s[..]).chain_err(|| LEK::InvalidUri).map(Some) }, - _ => None + Some(_) => Err(LE::from_kind(LEK::LinkParserFieldTypeError)), + None => Ok(None), }) } - pub fn get_url(&self) -> Result> { - let opt = self.link - .get_header() - .read("imag.content.url"); - - match opt { + fn get_url(&self) -> Result> { + match self.get_header().read("imag.content.url") { Ok(Some(&Value::String(ref s))) => { Url::parse(&s[..]) .map(Some) @@ -261,23 +255,32 @@ pub mod iter { type Item = Result; fn next(&mut self) -> Option { - use super::get_external_link_from_file; + use external::Link; - self.0 - .next() - .map(|id| { - debug!("Retrieving entry for id: '{:?}'", id); - self.1 - .retrieve(id.clone()) - .chain_err(|| LEK::StoreReadError) - .map_dbg_err(|_| format!("Retrieving entry for id: '{:?}' failed", id)) - .and_then(|f| { - debug!("Store::retrieve({:?}) succeeded", id); - debug!("getting external link from file now"); - get_external_link_from_file(&f) - .map_dbg_err(|e| format!("URL -> Err = {:?}", e)) - }) - }) + loop { + let next = self.0 + .next() + .map(|id| { + debug!("Retrieving entry for id: '{:?}'", id); + self.1 + .retrieve(id.clone()) + .chain_err(|| LEK::StoreReadError) + .map_dbg_err(|_| format!("Retrieving entry for id: '{:?}' failed", id)) + .and_then(|f| { + debug!("Store::retrieve({:?}) succeeded", id); + debug!("getting external link from file now"); + f.get_link_uri_from_filelockentry() + .map_dbg_err(|e| format!("URL -> Err = {:?}", e)) + }) + }); + + match next { + Some(Ok(Some(link))) => return Some(Ok(link)), + Some(Ok(None)) => continue, + Some(Err(e)) => return Some(Err(e)), + None => return None + } + } } } @@ -291,11 +294,6 @@ pub fn is_external_link_storeid + Debug>(id: A) -> bool { id.as_ref().local().starts_with("links/external") } -fn get_external_link_from_file(entry: &FileLockEntry) -> Result { - Link::get_link_uri_from_filelockentry(entry) // TODO: Do not hide error by using this function - .ok_or(LE::from_kind(LEK::StoreReadError)) -} - /// 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. diff --git a/lib/entry/libimagentryref/Cargo.toml b/lib/entry/libimagentryref/Cargo.toml index 983de732..c6a89b7c 100644 --- a/lib/entry/libimagentryref/Cargo.toml +++ b/lib/entry/libimagentryref/Cargo.toml @@ -18,9 +18,9 @@ itertools = "0.5" log = "0.3" rust-crypto = "0.2" toml = "^0.4" -walkdir = "1.0.*" toml-query = "0.3.0" error-chain = "0.10" +walkdir = "1.0.*" libimagstore = { version = "0.4.0", path = "../../../lib/core/libimagstore" } libimagerror = { version = "0.4.0", path = "../../../lib/core/libimagerror" } diff --git a/lib/entry/libimagentryref/src/hashers/nbytes.rs b/lib/entry/libimagentryref/src/hashers/nbytes.rs index be7b444b..ec232fd8 100644 --- a/lib/entry/libimagentryref/src/hashers/nbytes.rs +++ b/lib/entry/libimagentryref/src/hashers/nbytes.rs @@ -52,15 +52,13 @@ impl Hasher for NBytesHasher { } fn create_hash(&mut self, _: &PathBuf, contents: &mut R) -> Result { - let s = contents + let s = try!(contents .bytes() .take(self.n) .collect::, _>>() .chain_err(|| REK::IOError) - .and_then(|v| String::from_utf8(v).chain_err(|| REK::IOError)) - .chain_err(|| REK::UTF8Error) - .chain_err(|| REK::IOError); - self.hasher.input_str(&try!(s)[..]); + .and_then(|v| String::from_utf8(v).chain_err(|| REK::UTF8Error))); + self.hasher.input_str(&s[..]); Ok(self.hasher.result_str()) } diff --git a/lib/entry/libimagentryref/src/lib.rs b/lib/entry/libimagentryref/src/lib.rs index c72ee298..2ef25567 100644 --- a/lib/entry/libimagentryref/src/lib.rs +++ b/lib/entry/libimagentryref/src/lib.rs @@ -55,3 +55,5 @@ pub mod hasher; pub mod hashers; pub mod lister; pub mod reference; +pub mod refstore; +mod util; diff --git a/lib/entry/libimagentryref/src/lister.rs b/lib/entry/libimagentryref/src/lister.rs index e560ac34..0cf4d5bc 100644 --- a/lib/entry/libimagentryref/src/lister.rs +++ b/lib/entry/libimagentryref/src/lister.rs @@ -20,13 +20,14 @@ use std::default::Default; use std::io::stdout; use std::io::Write; +use std::ops::Deref; use libimagentrylist::lister::Lister; use libimagentrylist::error::Result; -use libimagentrylist::error::ResultExt; use libimagerror::trace::trace_error; use libimagstore::store::FileLockEntry; use libimagentrylist::error::ListErrorKind as LEK; +use libimagentrylist::error as lerror; use reference::Ref; @@ -86,13 +87,45 @@ impl Lister for RefLister { debug!("fold({:?}, {:?})", accu, entry); let r = accu.and_then(|_| { debug!("Listing Entry: {:?}", entry); - lister_fn(entry, - self.check_dead, - self.check_changed, - self.check_changed_content, - self.check_changed_permiss) + { + let is_dead = if self.check_dead { + if try!(lerror::ResultExt::chain_err(entry.fs_link_exists(), || LEK::FormatError)) { + "dead" + } else { + "alive" + } + } else { + "not checked" + }; + + let is_changed = if self.check_changed { + if check_changed(entry.deref()) { "changed" } else { "unchanged" } + } else { + "not checked" + }; + + let is_changed_content = if self.check_changed_content { + if check_changed_content(entry.deref()) { "changed" } else { "unchanged" } + } else { + "not checked" + }; + + let is_changed_permiss = if self.check_changed_permiss { + if check_changed_permiss(entry.deref()) { "changed" } else { "unchanged" } + } else { + "not checked" + }; + + Ok(format!("{} | {} | {} | {} | {} | {}", + is_dead, + is_changed, + is_changed_content, + is_changed_permiss, + entry.get_path_hash().unwrap_or_else(|_| String::from("Cannot get hash")), + entry.get_location())) + } .and_then(|s| { - write!(stdout(), "{}\n", s).chain_err(|| LEK::IOError) + lerror::ResultExt::chain_err(write!(stdout(), "{}\n", s), || LEK::FormatError) }) }) .map(|_| ()); @@ -104,68 +137,11 @@ impl Lister for RefLister { } -fn lister_fn(fle: FileLockEntry, - do_check_dead: bool, - do_check_changed: bool, - do_check_changed_content: bool, - do_check_changed_permiss: bool) -> Result -{ - Ref::from_filelockentry(fle) - .map(|r| { - let is_dead = if do_check_dead { - if check_dead(&r) { "dead" } else { "alive" } - } else { - "not checked" - }; - - let is_changed = if do_check_changed { - if check_changed(&r) { "changed" } else { "unchanged" } - } else { - "not checked" - }; - - let is_changed_content = if do_check_changed_content { - if check_changed_content(&r) { "changed" } else { "unchanged" } - } else { - "not checked" - }; - - let is_changed_permiss = if do_check_changed_permiss { - if check_changed_permiss(&r) { "changed" } else { "unchanged" } - } else { - "not checked" - }; - - format!("{} | {} | {} | {} | {} | {}", - is_dead, - is_changed, - is_changed_content, - is_changed_permiss, - r.get_path_hash().unwrap_or_else(|_| String::from("Cannot get hash")), - r.get_location()) - }) - .chain_err(|| LEK::FormatError) -} - -fn check_dead(r: &Ref) -> bool { - match r.fs_link_exists() { - Ok(b) => b, - Err(e) => { - warn!("Could not check whether the ref {} exists on the FS:", r); - trace_error(&e); - - // We continue here and tell the callee that this reference is dead, what is kind of - // true actually, as we might not have access to it right now - true - }, - } -} - -fn check_changed(r: &Ref) -> bool { +fn check_changed(r: &R) -> bool { check_changed_content(r) && check_changed_permiss(r) } -fn check_changed_content(r: &Ref) -> bool { +fn check_changed_content(r: &R) -> bool { let eq = r.get_current_hash() .and_then(|hash| r.get_stored_hash().map(|stored| (hash, stored))) .map(|(hash, stored)| hash == stored); @@ -173,7 +149,7 @@ fn check_changed_content(r: &Ref) -> bool { match eq { Ok(eq) => eq, Err(e) => { - warn!("Could not check whether the ref {} changed on the FS:", r); + warn!("Could not check whether the ref changed on the FS"); trace_error(&e); // We continue here and tell the callee that this reference is unchanged @@ -182,7 +158,7 @@ fn check_changed_content(r: &Ref) -> bool { } } -fn check_changed_permiss(_: &Ref) -> bool { +fn check_changed_permiss(_: &R) -> bool { warn!("Permission changes tracking not supported yet."); false } diff --git a/lib/entry/libimagentryref/src/reference.rs b/lib/entry/libimagentryref/src/reference.rs index c9a7479a..049b83d5 100644 --- a/lib/entry/libimagentryref/src/reference.rs +++ b/lib/entry/libimagentryref/src/reference.rs @@ -21,230 +21,104 @@ //! files outside of the imag store. use std::path::PathBuf; -use std::ops::Deref; -use std::ops::DerefMut; -use std::collections::BTreeMap; use std::fs::File; -use std::fmt::{Display, Error as FmtError, Formatter}; use std::fs::Permissions; -use std::result::Result as RResult; -use libimagstore::store::FileLockEntry; -use libimagstore::storeid::StoreId; -use libimagstore::storeid::IntoStoreId; -use libimagstore::store::Store; +use libimagstore::store::Entry; use toml::Value; use toml_query::read::TomlValueReadExt; use toml_query::set::TomlValueSetExt; -use toml_query::insert::TomlValueInsertExt; use error::RefErrorKind as REK; use error::RefError as RE; use error::ResultExt; -use flags::RefFlags; use error::Result; use hasher::*; -use module_path::ModuleEntryPath; -#[derive(Debug)] -pub struct Ref<'a>(FileLockEntry<'a>); - -impl<'a> Ref<'a> { - - /// Try to build a Ref object based on an existing FileLockEntry object - pub fn from_filelockentry(fle: FileLockEntry<'a>) -> Result> { - Ref::read_reference(&fle).map(|_| Ref(fle)) - } - - /// Try to get `si` as Ref object from the store - pub fn get(store: &'a Store, si: StoreId) -> Result> { - match store.get(si) { - Err(e) => return Err(e).chain_err(|| REK::StoreReadError), - Ok(None) => return Err(RE::from_kind(REK::RefNotInStore)), - Ok(Some(fle)) => Ref::from_filelockentry(fle), - } - } - - /// Get a Ref object from the store by hash. - /// - /// Returns None if the hash cannot be found. - pub fn get_by_hash(store: &'a Store, hash: String) -> Result>> { - ModuleEntryPath::new(hash) - .into_storeid() - .and_then(|id| store.get(id)) - .map(|opt_fle| opt_fle.map(|fle| Ref(fle))) - .chain_err(|| REK::StoreReadError) - } - - /// Delete a ref by hash - /// - /// If the returned Result contains an error, the ref might not be deleted. - pub fn delete_by_hash(store: &'a Store, hash: String) -> Result<()> { - ModuleEntryPath::new(hash) - .into_storeid() - .and_then(|id| store.delete(id)) - .chain_err(|| REK::StoreWriteError) - } - - fn read_reference(fle: &FileLockEntry<'a>) -> Result { - match fle.get_header().read("ref.path") { - Ok(Some(&Value::String(ref s))) => Ok(PathBuf::from(s)), - Ok(Some(_)) => Err(RE::from_kind(REK::HeaderTypeError)), - Ok(None) => Err(RE::from_kind(REK::HeaderFieldMissingError)), - Err(e) => Err(e).chain_err(|| REK::StoreReadError), - } - } - - pub fn create_with_hasher(store: &'a Store, pb: PathBuf, flags: RefFlags, mut h: H) - -> Result> - { - if !pb.exists() { - return Err(RE::from_kind(REK::RefTargetDoesNotExist)); - } - if flags.get_content_hashing() && pb.is_dir() { - return Err(RE::from_kind(REK::RefTargetCannotBeHashed)); - } - - let (mut fle, content_hash, permissions, canonical_path) = { // scope to be able to fold - try!(File::open(pb.clone()) - .chain_err(|| REK::RefTargetFileCannotBeOpened) - - // If we were able to open this file, - // we hash the contents of the file and return (file, hash) - .and_then(|mut file| { - let opt_contenthash = if flags.get_content_hashing() { - Some(try!(h.create_hash(&pb, &mut file))) - } else { - None - }; - - Ok((file, opt_contenthash)) - }) - - // and then we get the permissions if we have to - // and return (file, content hash, permissions) - .and_then(|(file, opt_contenthash)| { - let opt_permissions = if flags.get_permission_tracking() { - Some(try!(file - .metadata() - .map(|md| md.permissions()) - .chain_err(|| REK::RefTargetCannotReadPermissions) - )) - } else { - None - }; - - Ok((opt_contenthash, opt_permissions)) - }) - - // and then we try to canonicalize the PathBuf, because we want to store a - // canonicalized path - // and return (file, content hash, permissions, canonicalized path) - .and_then(|(opt_contenthash, opt_permissions)| { - pb.canonicalize() - .map(|can| (opt_contenthash, opt_permissions, can)) - // if PathBuf::canonicalize() failed, build an error from the return value - .chain_err(|| REK::PathCanonicalizationError) - }) - - // and then we hash the canonicalized path - // and return (file, content hash, permissions, canonicalized path, path hash) - .and_then(|(opt_contenthash, opt_permissions, can)| { - let path_hash = try!(Ref::hash_path(&can) - .chain_err(|| REK::PathHashingError) - ); - - Ok((opt_contenthash, opt_permissions, can, path_hash)) - }) - - // and then we convert the PathBuf of the canonicalized path to a String to be able - // to save it in the Ref FileLockEntry obj - // and return - // (file, content hash, permissions, canonicalized path as String, path hash) - .and_then(|(opt_conhash, opt_perm, can, path_hash)| { - match can.to_str().map(String::from) { - // UTF convert error in PathBuf::to_str(), - None => Err(RE::from_kind(REK::PathUTF8Error)), - Some(can) => Ok((opt_conhash, opt_perm, can, path_hash)) - } - }) - - // and then we create the FileLockEntry in the Store - // and return (filelockentry, content hash, permissions, canonicalized path) - .and_then(|(opt_conhash, opt_perm, can, path_hash)| { - let fle = try!(store - .create(ModuleEntryPath::new(path_hash)) - .chain_err(|| REK::StoreWriteError) - ); - - Ok((fle, opt_conhash, opt_perm, can)) - }) - ) - }; - - for tpl in [ - Some((String::from("ref"), Value::Table(BTreeMap::new()))), - Some((String::from("ref.permissions"), Value::Table(BTreeMap::new()))), - Some((String::from("ref.path"), Value::String(canonical_path))), - Some((String::from("ref.content_hash"), Value::Table(BTreeMap::new()))), - - content_hash.map(|hash| { - (format!("ref.content_hash.{}", h.hash_name()), Value::String(hash)) - }), - permissions.map(|p| { - (String::from("ref.permissions.ro"), Value::Boolean(p.readonly())) - }), - ].into_iter() - { - match tpl { - &Some((ref s, ref v)) => { - match fle.get_header_mut().insert(s, v.clone()) { - Ok(None) => { - debug!("Header insert worked"); - } - Ok(Some(val)) => { - debug!("Overwrote: {}, which was: {:?}", s, val); - }, - Err(e) => { - return Err(e).chain_err(|| REK::HeaderFieldWriteError); - }, - } - } - &None => { - debug!("Not going to insert."); - } - } - } - - Ok(Ref(fle)) - } - - /// Create a Ref object which refers to `pb` - pub fn create(store: &'a Store, pb: PathBuf, flags: RefFlags) -> Result> { - Ref::create_with_hasher(store, pb, flags, DefaultHasher::new()) - } - - /// Creates a Hash from a PathBuf by making the PathBuf absolute and then running a hash - /// algorithm on it - fn hash_path(pb: &PathBuf) -> Result { - use crypto::sha1::Sha1; - use crypto::digest::Digest; - - match pb.to_str() { - Some(s) => { - let mut hasher = Sha1::new(); - hasher.input_str(s); - Ok(hasher.result_str()) - }, - None => return Err(RE::from_kind(REK::PathUTF8Error)), - } - } +pub trait Ref { /// Get the hash from the path of the ref - pub fn get_path_hash(&self) -> Result { - self.0 - .get_location() + fn get_path_hash(&self) -> Result; + + /// Get the hash of the link target which is stored in the ref object + fn get_stored_hash(&self) -> Result; + + /// Get the hahs of the link target which is stored in the ref object, which is hashed with a + /// custom Hasher instance. + fn get_stored_hash_with_hasher(&self, h: &H) -> Result; + + /// Get the hash of the link target by reading the link target and hashing the contents + fn get_current_hash(&self) -> Result; + + /// Get the hash of the link target by reading the link target and hashing the contents with the + /// custom hasher + fn get_current_hash_with_hasher(&self, h: H) -> Result; + + /// check whether the pointer the Ref represents still points to a file which exists + fn fs_link_exists(&self) -> Result; + + /// Alias for `r.fs_link_exists() && r.deref().is_file()` + fn is_ref_to_file(&self) -> Result; + + /// Alias for `r.fs_link_exists() && r.deref().is_dir()` + fn is_ref_to_dir(&self) -> Result; + + /// Alias for `!Ref::fs_link_exists()` + fn is_dangling(&self) -> Result; + + /// check whether the pointer the Ref represents is valid + /// This includes: + /// - Hashsum of the file is still the same as stored in the Ref + /// - file permissions are still valid + fn fs_link_valid(&self) -> Result; + + /// Check whether the file permissions of the referenced file are equal to the stored + /// permissions + fn fs_link_valid_permissions(&self) -> Result; + + /// Check whether the Hashsum of the referenced file is equal to the stored hashsum + fn fs_link_valid_hash(&self) -> Result; + + /// Update the Ref by re-checking the file from FS + /// This errors if the file is not present or cannot be read() + fn update_ref(&mut self) -> Result<()>; + + /// Update the Ref by re-checking the file from FS using the passed Hasher instance + /// This errors if the file is not present or cannot be read() + fn update_ref_with_hasher(&mut self, h: &H) -> Result<()>; + + /// Get the path of the file which is reffered to by this Ref + fn fs_file(&self) -> Result; + + /// Re-find a referenced file + /// + /// This function tries to re-find a ref by searching all directories in `search_roots` recursively + /// for a file which matches the hash of the Ref. + /// + /// If `search_roots` is `None`, it starts at the filesystem root `/`. + /// + /// If the target cannot be found, this yields a RefTargetDoesNotExist error kind. + /// + /// # Warning + /// + /// This option causes heavy I/O as it recursively searches the Filesystem. + fn refind(&self, search_roots: Option>) -> Result; + + /// See documentation of `Ref::refind()` + fn refind_with_hasher(&self, search_roots: Option>, h: H) + -> Result; + + /// Get the permissions of the file which are present + fn get_current_permissions(&self) -> Result; +} + + +impl Ref for Entry { + + /// Get the hash from the path of the ref + fn get_path_hash(&self) -> Result { + self.get_location() .clone() .into_pathbuf() .chain_err(|| REK::StoreIdError) @@ -258,14 +132,14 @@ impl<'a> Ref<'a> { } /// Get the hash of the link target which is stored in the ref object - pub fn get_stored_hash(&self) -> Result { + fn get_stored_hash(&self) -> Result { self.get_stored_hash_with_hasher(&DefaultHasher::new()) } /// Get the hahs of the link target which is stored in the ref object, which is hashed with a /// custom Hasher instance. - pub fn get_stored_hash_with_hasher(&self, h: &H) -> Result { - match self.0.get_header().read(&format!("ref.content_hash.{}", h.hash_name())[..]) { + fn get_stored_hash_with_hasher(&self, h: &H) -> Result { + match self.get_header().read(&format!("ref.content_hash.{}", h.hash_name())[..]) { // content hash stored... Ok(Some(&Value::String(ref s))) => Ok(s.clone()), @@ -281,13 +155,13 @@ impl<'a> Ref<'a> { } /// Get the hash of the link target by reading the link target and hashing the contents - pub fn get_current_hash(&self) -> Result { + fn get_current_hash(&self) -> Result { self.get_current_hash_with_hasher(DefaultHasher::new()) } /// Get the hash of the link target by reading the link target and hashing the contents with the /// custom hasher - pub fn get_current_hash_with_hasher(&self, mut h: H) -> Result { + fn get_current_hash_with_hasher(&self, mut h: H) -> Result { self.fs_file() .and_then(|pb| { File::open(pb.clone()) @@ -297,38 +171,23 @@ impl<'a> Ref<'a> { .and_then(|(path, mut file)| h.create_hash(&path, &mut file)) } - /// Get the permissions of the file which are present - fn get_current_permissions(&self) -> Result { - self.fs_file() - .and_then(|pb| { - File::open(pb) - .chain_err(|| REK::HeaderFieldReadError) - }) - .and_then(|file| { - file - .metadata() - .map(|md| md.permissions()) - .chain_err(|| REK::RefTargetCannotReadPermissions) - }) - } - /// check whether the pointer the Ref represents still points to a file which exists - pub fn fs_link_exists(&self) -> Result { + fn fs_link_exists(&self) -> Result { self.fs_file().map(|pathbuf| pathbuf.exists()) } /// Alias for `r.fs_link_exists() && r.deref().is_file()` - pub fn is_ref_to_file(&self) -> Result { + fn is_ref_to_file(&self) -> Result { self.fs_file().map(|pathbuf| pathbuf.is_file()) } /// Alias for `r.fs_link_exists() && r.deref().is_dir()` - pub fn is_ref_to_dir(&self) -> Result { + fn is_ref_to_dir(&self) -> Result { self.fs_file().map(|pathbuf| pathbuf.is_dir()) } /// Alias for `!Ref::fs_link_exists()` - pub fn is_dangling(&self) -> Result { + fn is_dangling(&self) -> Result { self.fs_link_exists().map(|b| !b) } @@ -336,7 +195,7 @@ impl<'a> Ref<'a> { /// This includes: /// - Hashsum of the file is still the same as stored in the Ref /// - file permissions are still valid - pub fn fs_link_valid(&self) -> Result { + fn fs_link_valid(&self) -> Result { match (self.fs_link_valid_permissions(), self.fs_link_valid_hash()) { (Ok(true) , Ok(true)) => Ok(true), (Ok(_) , Ok(_)) => Ok(false), @@ -347,8 +206,8 @@ impl<'a> Ref<'a> { /// Check whether the file permissions of the referenced file are equal to the stored /// permissions - pub fn fs_link_valid_permissions(&self) -> Result { - self.0 + fn fs_link_valid_permissions(&self) -> Result { + self .get_header() .read("ref.permissions.ro") .chain_err(|| REK::HeaderFieldReadError) @@ -364,7 +223,7 @@ impl<'a> Ref<'a> { } /// Check whether the Hashsum of the referenced file is equal to the stored hashsum - pub fn fs_link_valid_hash(&self) -> Result { + fn fs_link_valid_hash(&self) -> Result { let stored_hash = try!(self.get_stored_hash()); let current_hash = try!(self.get_current_hash()); Ok(stored_hash == current_hash) @@ -372,23 +231,23 @@ impl<'a> Ref<'a> { /// Update the Ref by re-checking the file from FS /// This errors if the file is not present or cannot be read() - pub fn update_ref(&mut self) -> Result<()> { + fn update_ref(&mut self) -> Result<()> { self.update_ref_with_hasher(&DefaultHasher::new()) } /// Update the Ref by re-checking the file from FS using the passed Hasher instance /// This errors if the file is not present or cannot be read() - pub fn update_ref_with_hasher(&mut self, h: &H) -> Result<()> { + fn update_ref_with_hasher(&mut self, h: &H) -> Result<()> { let current_hash = try!(self.get_current_hash()); // uses the default hasher let current_perm = try!(self.get_current_permissions()); - try!(self.0 + try!(self .get_header_mut() .set("ref.permissions.ro", Value::Boolean(current_perm.readonly())) .chain_err(|| REK::StoreWriteError) ); - try!(self.0 + try!(self .get_header_mut() .set(&format!("ref.content_hash.{}", h.hash_name())[..], Value::String(current_hash)) .chain_err(|| REK::StoreWriteError) @@ -398,8 +257,8 @@ impl<'a> Ref<'a> { } /// Get the path of the file which is reffered to by this Ref - pub fn fs_file(&self) -> Result { - match self.0.get_header().read("ref.path") { + fn fs_file(&self) -> Result { + match self.get_header().read("ref.path") { Ok(Some(&Value::String(ref s))) => Ok(PathBuf::from(s)), Ok(Some(_)) => Err(RE::from_kind(REK::HeaderTypeError)), Ok(None) => Err(RE::from_kind(REK::HeaderFieldMissingError)), @@ -407,53 +266,6 @@ impl<'a> Ref<'a> { } } - /// Check whether there is a reference to the file at `pb` - pub fn exists(store: &Store, pb: PathBuf) -> Result { - pb.canonicalize() - .chain_err(|| REK::PathCanonicalizationError) - .and_then(|can| { - Ref::hash_path(&can) - .chain_err(|| REK::PathHashingError) - }) - .and_then(|hash| { - store.retrieve_for_module("ref").map(|iter| (hash, iter)) - .chain_err(|| REK::StoreReadError) - }) - .and_then(|(hash, possible_refs)| { - // This is kind of a manual Iterator::filter() call what we do here, but with the - // actual ::filter method we cannot return the error in a nice way, so we do it - // manually here. If you can come up with a better version of this, feel free to - // take this note as a todo. - for r in possible_refs { - let contains_hash = try!(r.to_str() - .chain_err(|| REK::TypeConversionError) - .map(|s| s.contains(&hash[..]))); - - if !contains_hash { - continue; - } - - match store.get(r) { - Ok(Some(fle)) => { - if Ref::read_reference(&fle).map(|path| path == pb).unwrap_or(false) { - return Ok(true) - } - }, - - Ok(None) => { // Something weird just happened - return Err(RE::from_kind(REK::StoreReadError)); - }, - - Err(e) => { - return Err(e).chain_err(|| REK::StoreReadError); - }, - } - } - - Ok(false) - }) - } - /// Re-find a referenced file /// /// This function tries to re-find a ref by searching all directories in `search_roots` recursively @@ -466,11 +278,12 @@ impl<'a> Ref<'a> { /// # Warning /// /// This option causes heavy I/O as it recursively searches the Filesystem. - pub fn refind(&self, search_roots: Option>) -> Result { + fn refind(&self, search_roots: Option>) -> Result { self.refind_with_hasher(search_roots, DefaultHasher::new()) } - pub fn refind_with_hasher(&self, search_roots: Option>, mut h: H) + /// See documentation of `Ref::refind()` + fn refind_with_hasher(&self, search_roots: Option>, mut h: H) -> Result { use itertools::Itertools; @@ -514,43 +327,19 @@ impl<'a> Ref<'a> { }) } -} - -impl<'a> Deref for Ref<'a> { - type Target = FileLockEntry<'a>; - - fn deref(&self) -> &FileLockEntry<'a> { - &self.0 - } - -} - -impl<'a> DerefMut for Ref<'a> { - - fn deref_mut(&mut self) -> &mut FileLockEntry<'a> { - &mut self.0 - } - -} - -impl<'a> Display for Ref<'a> { - - fn fmt(&self, fmt: &mut Formatter) -> RResult<(), FmtError> { - let path = self.fs_file() - .map(|pb| String::from(pb.to_str().unwrap_or(""))) - .unwrap_or(String::from("Could not read Path from reference object")); - - let hash = self.get_stored_hash().unwrap_or(String::from("")); - - write!(fmt, "Ref({} -> {})", hash, path) - } - -} - -impl<'a> Into> for Ref<'a> { - - fn into(self) -> FileLockEntry<'a> { - self.0 + /// Get the permissions of the file which are present + fn get_current_permissions(&self) -> Result { + self.fs_file() + .and_then(|pb| { + File::open(pb) + .chain_err(|| REK::HeaderFieldReadError) + }) + .and_then(|file| { + file + .metadata() + .map(|md| md.permissions()) + .chain_err(|| REK::RefTargetCannotReadPermissions) + }) } } diff --git a/lib/entry/libimagentryref/src/refstore.rs b/lib/entry/libimagentryref/src/refstore.rs new file mode 100644 index 00000000..b005a665 --- /dev/null +++ b/lib/entry/libimagentryref/src/refstore.rs @@ -0,0 +1,268 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015, 2016 Matthias Beyer 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::path::PathBuf; +use std::collections::BTreeMap; +use std::fs::File; + +use libimagstore::store::FileLockEntry; +use libimagstore::storeid::StoreId; +use libimagstore::storeid::IntoStoreId; +use libimagstore::store::Store; + +use toml::Value; + +use error::RefErrorKind as REK; +use error::RefError as RE; +use error::ResultExt; +use error::Result; +use flags::RefFlags; +use hasher::*; +use module_path::ModuleEntryPath; +use util::*; + +pub trait RefStore { + + /// Check whether there is a reference to the file at `pb` + fn exists(&self, pb: PathBuf) -> Result; + + /// Try to get `si` as Ref object from the store + fn get<'a>(&'a self, si: StoreId) -> Result>; + + /// Get a Ref object from the store by hash. + /// + /// Returns None if the hash cannot be found. + fn get_by_hash<'a>(&'a self, hash: String) -> Result>>; + + /// Delete a ref by hash + /// + /// If the returned Result contains an error, the ref might not be deleted. + fn delete_by_hash(&self, hash: String) -> Result<()>; + + /// Create a Ref object which refers to `pb` + fn create<'a>(&'a self, pb: PathBuf, flags: RefFlags) -> Result>; + + fn create_with_hasher<'a, H: Hasher>(&'a self, pb: PathBuf, flags: RefFlags, h: H) + -> Result>; + +} + +impl RefStore for Store { + + /// Check whether there is a reference to the file at `pb` + fn exists(&self, pb: PathBuf) -> Result { + pb.canonicalize() + .chain_err(|| REK::PathCanonicalizationError) + .and_then(|c| hash_path(&c)) + .chain_err(|| REK::PathHashingError) + .and_then(|hash| { + self.retrieve_for_module("ref") + .map(|iter| (hash, iter)) + .chain_err(|| REK::StoreReadError) + }) + .and_then(|(hash, possible_refs)| { + // This is kind of a manual Iterator::filter() call what we do here, but with the + // actual ::filter method we cannot return the error in a nice way, so we do it + // manually here. If you can come up with a better version of this, feel free to + // take this note as a todo. + for r in possible_refs { + let contains_hash = try!(r.to_str() + .chain_err(|| REK::TypeConversionError) + .map(|s| s.contains(&hash[..])) + ); + + if !contains_hash { + continue; + } + + match self.get(r) { + Ok(Some(fle)) => { + if read_reference(&fle).map(|path| path == pb).unwrap_or(false) { + return Ok(true) + } + }, + + Ok(None) => return Err(RE::from_kind(REK::StoreReadError)), + Err(e) => return Err(e).chain_err(|| REK::StoreReadError) + } + } + + Ok(false) + }) + } + + /// Try to get `si` as Ref object from the store + fn get<'a>(&'a self, si: StoreId) -> Result> { + match self.get(si) { + Err(e) => return Err(e).chain_err(|| REK::StoreReadError), + Ok(None) => return Err(RE::from_kind(REK::RefNotInStore)), + Ok(Some(fle)) => Ok(fle), + } + } + + /// Get a Ref object from the store by hash. + /// + /// Returns None if the hash cannot be found. + fn get_by_hash<'a>(&'a self, hash: String) -> Result>> { + ModuleEntryPath::new(hash) + .into_storeid() + .and_then(|id| self.get(id)) + .chain_err(|| REK::StoreReadError) + } + + /// Delete a ref by hash + /// + /// If the returned Result contains an error, the ref might not be deleted. + fn delete_by_hash(&self, hash: String) -> Result<()> { + ModuleEntryPath::new(hash) + .into_storeid() + .and_then(|id| self.delete(id)) + .chain_err(|| REK::StoreWriteError) + } + + /// Create a Ref object which refers to `pb` + fn create<'a>(&'a self, pb: PathBuf, flags: RefFlags) -> Result> { + self.create_with_hasher(pb, flags, DefaultHasher::new()) + } + + fn create_with_hasher<'a, H: Hasher>(&'a self, pb: PathBuf, flags: RefFlags, mut h: H) + -> Result> + { + use toml_query::insert::TomlValueInsertExt; + + if !pb.exists() { + return Err(RE::from_kind(REK::RefTargetDoesNotExist)); + } + if flags.get_content_hashing() && pb.is_dir() { + return Err(RE::from_kind(REK::RefTargetCannotBeHashed)); + } + + let (mut fle, content_hash, permissions, canonical_path) = { // scope to be able to fold + try!(File::open(pb.clone()) + .chain_err(|| REK::RefTargetFileCannotBeOpened) + + // If we were able to open this file, + // we hash the contents of the file and return (file, hash) + .and_then(|mut file| { + let opt_contenthash = if flags.get_content_hashing() { + Some(try!(h.create_hash(&pb, &mut file))) + } else { + None + }; + + Ok((file, opt_contenthash)) + }) + + // and then we get the permissions if we have to + // and return (file, content hash, permissions) + .and_then(|(file, opt_contenthash)| { + let opt_permissions = if flags.get_permission_tracking() { + Some(try!(file + .metadata() + .map(|md| md.permissions()) + .chain_err(|| REK::RefTargetCannotReadPermissions) + )) + } else { + None + }; + + Ok((opt_contenthash, opt_permissions)) + }) + + // and then we try to canonicalize the PathBuf, because we want to store a + // canonicalized path + // and return (file, content hash, permissions, canonicalized path) + .and_then(|(opt_contenthash, opt_permissions)| { + pb.canonicalize() + .map(|can| (opt_contenthash, opt_permissions, can)) + // if PathBuf::canonicalize() failed, build an error from the return value + .chain_err(|| REK::PathCanonicalizationError) + }) + + // and then we hash the canonicalized path + // and return (file, content hash, permissions, canonicalized path, path hash) + .and_then(|(opt_contenthash, opt_permissions, can)| { + let path_hash = try!(hash_path(&can).chain_err(|| REK::PathHashingError)); + + Ok((opt_contenthash, opt_permissions, can, path_hash)) + }) + + // and then we convert the PathBuf of the canonicalized path to a String to be able + // to save it in the Ref FileLockEntry obj + // and return + // (file, content hash, permissions, canonicalized path as String, path hash) + .and_then(|(opt_conhash, opt_perm, can, path_hash)| { + match can.to_str().map(String::from) { + // UTF convert error in PathBuf::to_str(), + None => Err(RE::from_kind(REK::PathUTF8Error)), + Some(can) => Ok((opt_conhash, opt_perm, can, path_hash)) + } + }) + + // and then we create the FileLockEntry in the Store + // and return (filelockentry, content hash, permissions, canonicalized path) + .and_then(|(opt_conhash, opt_perm, can, path_hash)| { + let fle = try!(self + .create(ModuleEntryPath::new(path_hash)) + .chain_err(|| REK::StoreWriteError) + ); + + Ok((fle, opt_conhash, opt_perm, can)) + }) + ) + }; + + for tpl in [ + Some((String::from("ref"), Value::Table(BTreeMap::new()))), + Some((String::from("ref.permissions"), Value::Table(BTreeMap::new()))), + Some((String::from("ref.path"), Value::String(canonical_path))), + Some((String::from("ref.content_hash"), Value::Table(BTreeMap::new()))), + + content_hash.map(|hash| { + (format!("ref.content_hash.{}", h.hash_name()), Value::String(hash)) + }), + permissions.map(|p| { + (String::from("ref.permissions.ro"), Value::Boolean(p.readonly())) + }), + ].into_iter() + { + match tpl { + &Some((ref s, ref v)) => { + match fle.get_header_mut().insert(s, v.clone()) { + Ok(Some(_)) => { + let e = RE::from_kind(REK::HeaderFieldAlreadyExistsError); + return Err(e).chain_err(|| REK::HeaderFieldWriteError); + }, + Ok(None) => { + // Okay, we just inserted a new header value... + }, + Err(e) => return Err(e).chain_err(|| REK::HeaderFieldWriteError), + } + } + &None => { + debug!("Not going to insert."); + } + } + } + + Ok(fle) + } + +} + diff --git a/lib/entry/libimagentryref/src/util.rs b/lib/entry/libimagentryref/src/util.rs new file mode 100644 index 00000000..c69e4d48 --- /dev/null +++ b/lib/entry/libimagentryref/src/util.rs @@ -0,0 +1,57 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015, 2016 Matthias Beyer 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::path::PathBuf; + +use error::RefErrorKind as REK; +use error::RefError as RE; +use error::Result; +use error::ResultExt; + +use libimagstore::store::Entry; + +use toml::Value; +use toml_query::read::TomlValueReadExt; + +/// Creates a Hash from a PathBuf by making the PathBuf absolute and then running a hash +/// algorithm on it +pub fn hash_path(pb: &PathBuf) -> Result { + use crypto::sha1::Sha1; + use crypto::digest::Digest; + + match pb.to_str() { + Some(s) => { + let mut hasher = Sha1::new(); + hasher.input_str(s); + Ok(hasher.result_str()) + }, + None => return Err(RE::from_kind(REK::PathUTF8Error)), + } +} + +/// Read the reference from a file +pub fn read_reference(refentry: &Entry) -> Result { + match refentry.get_header().read("ref.path") { + Ok(Some(&Value::String(ref s))) => Ok(PathBuf::from(s)), + Ok(Some(_)) => Err(RE::from_kind(REK::HeaderTypeError)), + Ok(None) => Err(RE::from_kind(REK::HeaderFieldMissingError)), + Err(e) => Err(e).chain_err(|| REK::StoreReadError), + } +} + diff --git a/lib/entry/libimagentrytag/src/lib.rs b/lib/entry/libimagentrytag/src/lib.rs index cf2be7d9..2a191486 100644 --- a/lib/entry/libimagentrytag/src/lib.rs +++ b/lib/entry/libimagentrytag/src/lib.rs @@ -49,8 +49,6 @@ extern crate libimagstore; extern crate libimagerror; pub mod error; -pub mod exec; pub mod tag; pub mod tagable; -pub mod ui; diff --git a/lib/entry/libimagentrytag/src/ui.rs b/lib/entry/libimagentrytag/src/ui.rs deleted file mode 100644 index 734708b3..00000000 --- a/lib/entry/libimagentrytag/src/ui.rs +++ /dev/null @@ -1,125 +0,0 @@ -// -// imag - the personal information management suite for the commandline -// Copyright (C) 2015, 2016 Matthias Beyer 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 clap::{Arg, ArgMatches, App, SubCommand}; - -use tag::Tag; -use tag::is_tag; - -/// Generates a `clap::SubCommand` to be integrated in the commandline-ui builder for building a -/// "tags --add foo --remove bar" subcommand to do tagging action. -pub fn tag_subcommand<'a, 'b>() -> App<'a, 'b> { - SubCommand::with_name(tag_subcommand_name()) - .author("Matthias Beyer ") - .version("0.1") - .about("Add or remove tags") - .arg(tag_add_arg()) - .arg(tag_remove_arg()) -} - -pub fn tag_add_arg<'a, 'b>() -> Arg<'a, 'b> { - Arg::with_name(tag_subcommand_add_arg_name()) - .short("a") - .long("add") - .takes_value(true) - .value_name("tags") - .multiple(true) - .validator(is_tag) - .help("Add tags, seperated by comma or by specifying multiple times") -} - -pub fn tag_remove_arg<'a, 'b>() -> Arg<'a, 'b> { - Arg::with_name(tag_subcommand_remove_arg_name()) - .short("r") - .long("remove") - .takes_value(true) - .value_name("tags") - .multiple(true) - .validator(is_tag) - .help("Remove tags, seperated by comma or by specifying multiple times") -} - -pub fn tag_subcommand_name() -> &'static str { - "tags" -} - -pub fn tag_subcommand_add_arg_name() -> &'static str { - "add-tags" -} - -pub fn tag_subcommand_remove_arg_name() -> &'static str { - "remove-tags" -} - -pub fn tag_subcommand_names() -> Vec<&'static str> { - vec![tag_subcommand_add_arg_name(), tag_subcommand_remove_arg_name()] -} - -/// Generates a `clap::Arg` which can be integrated into the commandline-ui builder for building a -/// "-t" or "--tags" argument which takes values for tagging actions (add, remove) -pub fn tag_argument<'a, 'b>() -> Arg<'a, 'b> { - Arg::with_name(tag_argument_name()) - .short("t") - .long("tags") - .takes_value(true) - .multiple(true) - .validator(is_tag) - .help("Add or remove tags, prefixed by '+' (for adding) or '-' (for removing)") -} - -pub fn tag_argument_name() -> &'static str { - "specify-tags" -} - -/// Get the tags which should be added from the commandline -/// -/// Returns none if the argument was not specified -pub fn get_add_tags(matches: &ArgMatches) -> Option> { - let add = tag_subcommand_add_arg_name(); - extract_tags(matches, add, '+') - .or_else(|| matches.values_of(add).map(|values| values.map(String::from).collect())) -} - -/// Get the tags which should be removed from the commandline -/// -/// Returns none if the argument was not specified -pub fn get_remove_tags(matches: &ArgMatches) -> Option> { - let rem = tag_subcommand_remove_arg_name(); - extract_tags(matches, rem, '+') - .or_else(|| matches.values_of(rem).map(|values| values.map(String::from).collect())) -} - -fn extract_tags(matches: &ArgMatches, specifier: &str, specchar: char) -> Option> { - if let Some(submatch) = matches.subcommand_matches("tags") { - submatch.values_of(specifier) - .map(|values| values.map(String::from).collect()) - } else { - matches.values_of("specify-tags") - .map(|argmatches| { - argmatches - .map(String::from) - .filter(|s| s.starts_with(specchar)) - .map(|s| { - String::from(s.split_at(1).1) - }) - .collect() - }) - } -} - diff --git a/lib/etc/libimaginteraction/src/readline.rs b/lib/etc/libimaginteraction/src/readline.rs index 58275444..7d498ce5 100644 --- a/lib/etc/libimaginteraction/src/readline.rs +++ b/lib/etc/libimaginteraction/src/readline.rs @@ -19,7 +19,7 @@ use error::InteractionError as IE; use error::InteractionErrorKind as IEK; -use error::MapErrInto; +use error::ResultExt; use toml::Value; @@ -46,36 +46,36 @@ impl Readline { let histfile = try!(match histfile { Value::String(s) => PathBuf::from(s), _ => Err(IE::from_kind(IEK::ConfigTypeError)) - .map_err_into(IEK::ConfigError) - .map_err_into(IEK::ReadlineError) + .chain_err(|| IEK::ConfigError) + .chain_err(|| IEK::ReadlineError) }); let histsize = try!(match histsize { Value::Integer(i) => i, _ => Err(IE::from_kind(IEK::ConfigTypeError)) - .map_err_into(IEK::ConfigError) - .map_err_into(IEK::ReadlineError) + .chain_err(|| IEK::ConfigError) + .chain_err(|| IEK::ReadlineError) }); let histigndups = try!(match histigndups { Value::Boolean(b) => b, _ => Err(IE::from_kind(IEK::ConfigTypeError)) - .map_err_into(IEK::ConfigError) - .map_err_into(IEK::ReadlineError) + .chain_err(|| IEK::ConfigError) + .chain_err(|| IEK::ReadlineError) }); let histignspace = try!(match histignspace { Value::Boolean(b) => b, _ => Err(IE::from_kind(IEK::ConfigTypeError)) - .map_err_into(IEK::ConfigError) - .map_err_into(IEK::ReadlineError) + .chain_err(|| IEK::ConfigError) + .chain_err(|| IEK::ReadlineError) }); let prompt = try!(match prompt { Value::String(s) => s, _ => Err(IE::from_kind(IEK::ConfigTypeError)) - .map_err_into(IEK::ConfigError) - .map_err_into(IEK::ReadlineError) + .chain_err(|| IEK::ConfigError) + .chain_err(|| IEK::ReadlineError) }); let config = Config::builder(). @@ -88,10 +88,10 @@ impl Readline { if !histfile.exists() { let _ = try!(File::create(histfile.clone()) - .map_err_into(IEK::ReadlineHistoryFileCreationError)); + .chain_err(|| IEK::ReadlineHistoryFileCreationError)); } - let _ = try!(editor.load_history(&histfile).map_err_into(ReadlineError)); + let _ = try!(editor.load_history(&histfile).chain_err(|| ReadlineError)); Ok(Readline { editor: editor, diff --git a/lib/etc/libimagutil/src/testing.rs b/lib/etc/libimagutil/src/testing.rs index 13a70d99..823e3f60 100644 --- a/lib/etc/libimagutil/src/testing.rs +++ b/lib/etc/libimagutil/src/testing.rs @@ -95,19 +95,34 @@ macro_rules! make_mock_app { } } + #[allow(unused)] pub fn generate_minimal_test_config() -> Option { ::toml::de::from_str("[store]\nimplicit-create=true") .map(Configuration::with_value) .ok() } + #[allow(unused)] pub fn generate_test_runtime<'a>(mut args: Vec<&'static str>) -> Result, RuntimeError> { - let mut cli_args = vec!["imag-link", "--rtp", "/tmp"]; + let mut cli_args = vec![$appname, "--rtp", "/tmp"]; cli_args.append(&mut args); let cli_app = MockLinkApp::new(cli_args); Runtime::with_configuration(cli_app, generate_minimal_test_config()) } + + #[allow(unused)] + pub fn reset_test_runtime<'a>(mut args: Vec<&'static str>, old_runtime: Runtime) + -> Result, RuntimeError> + { + let mut cli_args = vec![$appname, "--rtp", "/tmp"]; + + cli_args.append(&mut args); + + let cli_app = MockLinkApp::new(cli_args); + Runtime::with_configuration(cli_app, generate_minimal_test_config()) + .map(|rt| rt.with_store(old_runtime.extract_store())) + } } }; diff --git a/scripts/hooks/applypatch-msg.check-signed-off.sh b/scripts/hooks/applypatch-msg.check-signed-off.sh new file mode 100644 index 00000000..3cf81abb --- /dev/null +++ b/scripts/hooks/applypatch-msg.check-signed-off.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# +# An hook script to check the commit log message taken by +# applypatch from an e-mail message for proper "Signed-off-by" line(s). +# +# To enable this hook, copy this file to ".git/hooks/applypatch-msg" and make it +# executable. + +# +# This hook is used when applying patches which are send via mail, to verify the +# Signed-off-by line is in the commit message. +# + +. git-sh-setup + +RED='\e[0;31m' # Red +YELLOW='\e[0;33m' # Yellow +NORMAL='\e[0m' # Text Reset + +warn() { + echo -e >&2 "${YELLOW}$*${DEFAULT}" +} + +abort() { + echo -e >&2 "${RED}$*${DEFAULT}" + exit 1 +} + +headline=$(head -n 1 $1 | wc -c) +[[ $headline -gt 50 ]] && warn "Headline of patch longer than 50 chars" + +grep "^Signed-off-by" $1 >/dev/null 2>/dev/null && abort "No Signed-off-by line" + diff --git a/scripts/hooks/pre-commit.signoffby-missing-warn.sh b/scripts/hooks/pre-commit.signoffby-missing-warn.sh new file mode 100644 index 00000000..470e6575 --- /dev/null +++ b/scripts/hooks/pre-commit.signoffby-missing-warn.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# +# The following snippet can be used to _WARN_ if a Signed-off-by line is missing +# in the commit message +# + +RED='\e[0;31m' # Red +NORMAL='\e[0m' # Text Reset + +if [ "1" != "$(grep -c '^Signed-off-by: ' "$1")" ]; then + printf >&2 "%sMissing Signed-off-by line.%s\n" "$RED" "$NORMAL" + + # To not only warn, but abort the commit, uncomment the next line + # exit 1 +fi + diff --git a/scripts/hooks/pre-push.fixup-warn.sh b/scripts/hooks/pre-push.fixup-warn.sh new file mode 100644 index 00000000..3ad7233e --- /dev/null +++ b/scripts/hooks/pre-push.fixup-warn.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +# +# The following snippet can be used to WARN about "!fixup" / "WIP" / "TMP" +# commits when pushing +# +# Aborting the push is possible +# + +remote="$1" +url="$2" + +z40=0000000000000000000000000000000000000000 + +while read local_ref local_sha remote_ref remote_sha +do + if [ "$local_sha" = $z40 ] + then + # Branch is deleted, nothing to check here, move along. + else + if [ "$remote_sha" = $z40 ] + then + # New branch, examine all commits + range="$local_sha" + else + # Update to existing branch, examine new commits + range="$remote_sha..$local_sha" + fi + + # Check for WIP commit + commit=$(git rev-list -n 1 --grep '^WIP|^TMP|!fixup' "$range") + if [ -n "$commit" ] + then + echo >&2 "Found WIP commit in $local_ref, not pushing" + + # TO NOT ONLY WARN BUT ABORT UNCOMMENT THE NEXT LINE + # exit 1 + fi + + # Check for commits without sign-off + if [ "$remote_sha" = $z40 ]; then + # New branch is pushed, we only want to check commits that are not + # on master. + range="$(git merge-base master "$local_sha")..$local_sha" + fi + while read ref; do + msg=$(git log -n 1 --format=%B "$ref") + if ! grep -q '^Signed-off-by: ' <<<"$msg"; then + echo >&2 "Unsigned commit $ref" + exit 1 + fi + done < <(git rev-list "$range") + # The process substitution above is a hack to make sure loop runs in + # the same shell and can actually exit the whole script. + fi +done + +exit 0 diff --git a/scripts/hooks/pre-push.signoffby-missing-warn.sh b/scripts/hooks/pre-push.signoffby-missing-warn.sh new file mode 100644 index 00000000..c5618bdc --- /dev/null +++ b/scripts/hooks/pre-push.signoffby-missing-warn.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# +# The following snippet can be used to WARN about a missing signed-off-by line +# in commits when pushing +# +# Aborting the push is possible +# + +remote="$1" +url="$2" + +z40=0000000000000000000000000000000000000000 + +while read local_ref local_sha remote_ref remote_sha +do + if [ "$local_sha" = $z40 ] + then + # Branch is deleted, nothing to check here, move along. + else + if [ "$remote_sha" = $z40 ] + then + # New branch, examine all commits + range="$local_sha" + else + # Update to existing branch, examine new commits + range="$remote_sha..$local_sha" + fi + + if [ "$remote_sha" = $z40 ]; then + # New branch is pushed, we only want to check commits that are not + # on master. + range="$(git merge-base master "$local_sha")..$local_sha" + fi + while read ref; do + msg=$(git log -n 1 --format=%B "$ref") + if ! grep -q '^Signed-off-by: ' <<<"$msg"; then + echo >&2 "Unsigned commit $ref" + + # TO NOT ONLY WARN BUT ABORT UNCOMMENT THE NEXT LINE + # exit 1 + fi + done < <(git rev-list "$range") + # The process substitution above is a hack to make sure loop runs in + # the same shell and can actually exit the whole script. + fi +done + +exit 0 + diff --git a/scripts/signed-off-by-in-branch.sh b/scripts/signed-off-by-in-branch.sh new file mode 100644 index 00000000..480f5a0a --- /dev/null +++ b/scripts/signed-off-by-in-branch.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# Checks whether all commit between $1 and $2 have a signed-off-by line + +RED='\e[0;31m' # Red +NORMAL='\e[0m' # Text Reset + +faulty=$(git rev-list --grep "Signed-off-by" --invert-grep $1..$2 | wc -l) + +if [[ $faulty -eq 0 ]] +then + echo >&2 "All good" +else + echo -en >&2 "${RED}Got $faulty non Signed-off-by commits${NORMAL}" + echo -e >&2 "${RED}between $1 and $2${NORMAL}" +fi +