// // imag - the personal information management suite for the commandline // Copyright (C) 2015-2020 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 // #![forbid(unsafe_code)] #![deny( non_camel_case_types, non_snake_case, path_statements, trivial_numeric_casts, unstable_features, unused_allocation, unused_import_braces, unused_imports, unused_must_use, unused_mut, unused_qualifications, while_true, )] extern crate clap; #[macro_use] extern crate log; #[macro_use] extern crate vobject; extern crate toml; extern crate toml_query; extern crate handlebars; extern crate walkdir; extern crate uuid; extern crate serde_json; #[macro_use] extern crate failure; extern crate resiter; extern crate libimagcontact; extern crate libimagstore; extern crate libimagrt; extern crate libimagerror; extern crate libimagutil; extern crate libimaginteraction; extern crate libimagentryedit; extern crate libimagentryref; use std::path::PathBuf; use std::io::Write; use handlebars::Handlebars; use clap::{App, ArgMatches}; use toml_query::read::TomlValueReadExt; use toml_query::read::TomlValueReadTypeExt; use toml_query::read::Partial; use walkdir::WalkDir; use failure::Error; use failure::err_msg; use failure::Fallible as Result; use resiter::AndThen; use resiter::IterInnerOkOrElse; use resiter::Map; use resiter::Filter; use libimagrt::runtime::Runtime; use libimagrt::application::ImagApplication; use libimagstore::store::FileLockEntry; use libimagstore::storeid::StoreId; use libimagstore::iter::get::StoreIdGetIteratorExtension; use libimagcontact::store::ContactStore; use libimagcontact::contact::Contact; use libimagcontact::deser::DeserVcard; mod ui; mod util; mod create; mod edit; use crate::util::build_data_object_for_handlebars; use crate::create::create; use crate::edit::edit; /// Marker enum for implementing ImagApplication on /// /// This is used by binaries crates to execute business logic /// or to build a CLI completion. pub enum ImagContact {} impl ImagApplication for ImagContact { fn run(rt: Runtime) -> Result<()> { match rt.cli().subcommand_name().ok_or_else(|| err_msg("No subcommand called"))? { "list" => list(&rt), "import" => import(&rt), "show" => show(&rt), "edit" => edit(&rt), "find" => find(&rt), "create" => create(&rt), other => { debug!("Unknown command"); if rt.handle_unknown_subcommand("imag-contact", other, rt.cli())?.success() { Ok(()) } else { Err(err_msg("Failed to handle unknown subcommand")) } }, } } fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> { ui::build_ui(app) } fn name() -> &'static str { env!("CARGO_PKG_NAME") } fn description() -> &'static str { "Contact management tool" } fn version() -> &'static str { env!("CARGO_PKG_VERSION") } } fn list(rt: &Runtime) -> Result<()> { let scmd = rt.cli().subcommand_matches("list").unwrap(); let list_format = get_contact_print_format("contact.list_format", rt, &scmd)?; debug!("List format: {:?}", list_format); let iterator = rt .store() .all_contacts()? .into_get_iter() .map_inner_ok_or_else(|| err_msg("Did not find one entry")) .and_then_ok(|fle| { rt.report_touched(fle.get_location())?; Ok(fle) }) .and_then_ok(|e| e.deser()); if scmd.is_present("json") { debug!("Listing as JSON"); let v = iterator.collect::>>()?; let s = ::serde_json::to_string(&v)?; writeln!(rt.stdout(), "{}", s).map_err(Error::from) } else { debug!("Not listing as JSON"); let output = rt.stdout(); let mut output = output.lock(); let mut i = 0; iterator .map_ok(|dvcard| { i += 1; build_data_object_for_handlebars(i, &dvcard) }) .and_then_ok(|data| list_format.render("format", &data).map_err(Error::from)) .and_then_ok(|s| writeln!(output, "{}", s).map_err(Error::from)) .collect::>>() .map(|_| ()) } } fn import(rt: &Runtime) -> Result<()> { let scmd = rt.cli().subcommand_matches("import").unwrap(); // secured by main let force_override = scmd.is_present("force-override"); let path = scmd.value_of("path").map(PathBuf::from).unwrap(); // secured by clap let collection_name = rt.cli().value_of("contact-ref-collection-name").unwrap(); // default by clap let ref_config = rt.config() .ok_or_else(|| format_err!("No configuration, cannot continue!"))? .read_partial::()? .ok_or_else(|| format_err!("Configuration missing: {}", libimagentryref::reference::Config::LOCATION))?; // TODO: Refactor the above to libimagutil or libimagrt? if !path.exists() { return Err(format_err!("Path does not exist: {}", path.display())) } if path.is_file() { let entry = rt .store() .retrieve_from_path(&path, &ref_config, &collection_name, force_override)?; rt.report_touched(entry.get_location()).map_err(Error::from) } else if path.is_dir() { WalkDir::new(path) .min_depth(1) .into_iter() .map(|r| r.map_err(Error::from)) .and_then_ok(|entry| { if entry.file_type().is_file() { let pb = PathBuf::from(entry.path()); let fle = rt .store() .retrieve_from_path(&pb, &ref_config, &collection_name, force_override)?; rt.report_touched(fle.get_location())?; info!("Imported: {}", entry.path().to_str().unwrap_or("")); Ok(()) } else { warn!("Ignoring non-file: {}", entry.path().to_str().unwrap_or("")); Ok(()) } }) .collect::>>() .map(|_| ()) } else { Err(err_msg("Path is neither directory nor file")) } } fn show_contacts<'a, I>(rt: &Runtime, show_format: &Handlebars, iter: I) -> Result<()> where I: Iterator>> { let out = rt.stdout(); let mut outlock = out.lock(); iter.enumerate() .map(|(i, elem)| { elem.and_then(|e| { let elem = e.deser()?; let data = build_data_object_for_handlebars(i, &elem); let s = show_format.render("format", &data)?; writeln!(outlock, "{}", s).map_err(Error::from) }) }) .collect::>>() .map(|_| ()) } fn show(rt: &Runtime) -> Result<()> { let scmd = rt.cli().subcommand_matches("show").unwrap(); let show_format = get_contact_print_format("contact.show_format", rt, &scmd)?; if let Some(ids) = scmd.values_of("ids") { let ids = ids.collect::>(); if ids.iter().all(|id| id.starts_with("contact")) { let iter = ids .into_iter() .map(String::from) .map(PathBuf::from) .map(StoreId::new) .into_get_iter(rt.store()) .map_inner_ok_or_else(|| err_msg("Did not find one entry")); show_contacts(rt, &show_format, iter) } else { ids .into_iter() .map(|hash| util::find_contact_by_hash(rt, hash)) .collect::>>()? .into_iter() .map(|iter| show_contacts(rt, &show_format, iter.filter_ok(|tpl| tpl.0).map_ok(|tpl| tpl.1))) .collect::>>() .map(|_| ()) } } else { let iter = rt.ids::()? .ok_or_else(|| err_msg("No ids supplied"))? .into_iter() .map(Ok) .into_get_iter(rt.store()) .map_inner_ok_or_else(|| err_msg("Did not find one entry")); show_contacts(rt, &show_format, iter) } } fn find(rt: &Runtime) -> Result<()> { let scmd = rt.cli().subcommand_matches("find").unwrap(); let grepstring = scmd .values_of("string") .unwrap() // safed by clap .map(String::from) .collect::>(); // We don't know yet which we need, but we pay that price for simplicity of the codebase let show_format = get_contact_print_format("contact.show_format", rt, &scmd)?; let list_format = get_contact_print_format("contact.list_format", rt, &scmd)?; let iterator = rt .store() .all_contacts()? .into_get_iter() .map_inner_ok_or_else(|| err_msg("Did not find one entry")) .and_then_ok(|entry| { let card = entry.deser()?; let str_contains_any = |s: &String, v: &Vec| { v.iter().any(|i| s.contains(i)) }; let take = card.adr().iter().any(|a| str_contains_any(a, &grepstring)) || card.email().iter().any(|a| str_contains_any(&a.address, &grepstring)) || card.fullname().iter().any(|a| str_contains_any(a, &grepstring)); if take { rt.report_touched(entry.get_location())?; // optimization so we don't have to parse again in the next step Ok((true, entry, card)) } else { Ok((false, entry, card)) } }); let mut i = 0; if !rt.output_is_pipe() || rt.ignore_ids() { if scmd.is_present("json") { iterator .filter_ok(|tpl| tpl.0) .map_ok(|tpl| tpl.2) .and_then_ok(|v| { let s = ::serde_json::to_string(&v)?; writeln!(rt.stdout(), "{}", s).map_err(Error::from) }) .collect::>>() .map(|_| ()) } else if scmd.is_present("find-id") { iterator .and_then_ok(|(take, entry, _)| { if take { writeln!(rt.stdout(), "{}", entry.get_location()).map_err(Error::from) } else { Ok(()) } }) .collect::>>() .map(|_| ()) } else if scmd.is_present("find-full-id") { let storepath = rt.store().path().display(); iterator .and_then_ok(|(take, entry, _)| { if take { writeln!(rt.stdout(), "{}/{}", storepath, entry.get_location()).map_err(Error::from) } else { Ok(()) } }) .collect::>>() .map(|_| ()) } else { iterator .and_then_ok(|(take, _, card)| { if take { i += 1; let fmt = if scmd.is_present("find-show") { &show_format } else { // default: find-list &list_format }; let data = build_data_object_for_handlebars(i, &card); let s = fmt.render("format", &data)?; writeln!(rt.stdout(), "{}", s).map_err(Error::from) } else { Ok(()) } }) .collect::>>() .map(|_| ()) } } else { // if not printing, we still have to consume the iterator to report the touched IDs let _ = iterator.collect::>(); Ok(()) } } fn get_contact_print_format(config_value_path: &'static str, rt: &Runtime, scmd: &ArgMatches) -> Result { let fmt = match scmd.value_of("format").map(String::from) { Some(s) => Ok(s), None => rt.config() .ok_or_else(|| err_msg("No configuration file"))? .read_string(config_value_path)? .ok_or_else(|| err_msg("Configuration 'contact.list_format' does not exist")), }?; let mut hb = Handlebars::new(); hb.register_template_string("format", fmt)?; hb.register_escape_fn(::handlebars::no_escape); ::libimaginteraction::format::register_all_color_helpers(&mut hb); ::libimaginteraction::format::register_all_format_helpers(&mut hb); Ok(hb) }