diff --git a/etc/cli.yml b/etc/cli.yml index d86660ea..780fe377 100644 --- a/etc/cli.yml +++ b/etc/cli.yml @@ -113,6 +113,77 @@ subcommands: help: Sets the level of debugging information required: false + subcommands: + - add: + about: Add bookmark + version: 0.1 + author: Matthias Beyer + args: + - url: + short: u + long: url + help: Add a new URL as bookmark + required: true + takes_value: true + + - tags: + short: t + long: tags + help: Add these tags to the URL + required: false + takes_value: true + + - list: + about: List bookmarks + version: 0.1 + author: Matthias Beyer + args: + - match: + short: m + long: match + help: Match for regex + required: false + takes_value: true + + - tags: + short: t + long: tags + help: Filter for these tags + required: false + takes_value: true + + - remove: + about: Remove bookmark(s) + version: 0.1 + author: Matthias Beyer + args: + - id: + long: id + help: Delete Bookmark with ID + required: false + takes_value: true + + - match: + short: m + long: match + help: Match for regex + required: false + takes_value: true + + - tags: + short: t + long: tags + help: Filter for these tags + required: false + takes_value: true + + - check: + short: c + long: check + help: Ensure there are no references to this link + required: false + takes_value: false + - todo: about: Todo module version: 0.1 diff --git a/src/main.rs b/src/main.rs index f073866d..62834703 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,14 +4,19 @@ #[macro_use] extern crate serde_json; #[macro_use] extern crate glob; #[macro_use] extern crate uuid; +#[macro_use] extern crate regex; #[macro_use] extern crate prettytable; extern crate config; -extern crate regex; use cli::CliConfig; use configuration::Configuration; use runtime::{ImagLogger, Runtime}; use clap::App; +use module::Module; +use module::ModuleError; +use module::CommandEnv; +use module::bm::BMModule; +use storage::backend::StorageBackend; mod cli; mod configuration; @@ -20,6 +25,8 @@ mod module; mod storage; mod ui; +use std::process::exit; + fn main() { let yaml = load_yaml!("../etc/cli.yml"); let app = App::from_yaml(yaml); @@ -36,5 +43,39 @@ fn main() { debug!("Runtime : {:?}", &rt); + let backend = StorageBackend::new(&rt).unwrap_or_else(|e| { + error!("Error: {}", e); + exit(1); + }); + + if let Some(matches) = rt.config.cli_matches.subcommand_matches("bm") { + let module = BMModule::new(&rt); + let commands = module.get_commands(&rt); + if let Some(command) = matches.subcommand_name() { + debug!("Subcommand: {}", command); + + let cmdenv = CommandEnv { + rt: &rt, + bk: &backend, + matches: matches.subcommand_matches(command).unwrap(), + }; + + let result = match commands.get(command) { + Some(f) => f(&module, cmdenv), + None => Err(ModuleError::new("No subcommand found")), + }; + + debug!("Result of command: {:?}", result); + } else { + debug!("No subcommand"); + } + + module.shutdown(&rt); + } else { + // Err(ModuleError::mk("No commandline call")) + info!("No commandline call...") + } + + info!("Hello, world!"); } diff --git a/src/module/bm/commands.rs b/src/module/bm/commands.rs new file mode 100644 index 00000000..80f2bd42 --- /dev/null +++ b/src/module/bm/commands.rs @@ -0,0 +1,173 @@ +use runtime::Runtime; +use storage::backend::{StorageBackendError, StorageBackend}; + +use module::Module; +use module::ModuleError; +use module::CommandResult; +use module::CommandEnv; + +use module::bm::header::build_header; +use module::bm::header::get_tags_from_header; +use storage::json::parser::JsonHeaderParser; +use storage::parser::{Parser, FileHeaderParser}; +use storage::file::File; +use ui::file::{FilePrinter, TablePrinter}; +use std::vec::IntoIter; + +use clap::ArgMatches; +use regex::Regex; + +pub fn add_command(module: &Module, env: CommandEnv) -> CommandResult { + let url = env.matches.value_of("url").unwrap(); + let tags = get_tags(env.rt, env.matches); + info!("Adding url '{}' with tags '{:?}'", url, tags); + + let header = build_header(&String::from(url), &tags); + let file = File::new_with_header(module, header); + let parser = Parser::new(JsonHeaderParser::new(None)); + let putres = env.bk.put_file(file, &parser); + + putres.map_err(|sberr| { + let mut err = ModuleError::new("Storage Backend Error"); + err.caused_by = Some(Box::new(sberr)); + err + }) +} + +pub fn list_command(module: &Module, env: CommandEnv) -> CommandResult { + let printer = TablePrinter::new(env.rt.is_verbose(), env.rt.is_debugging()); + let files = get_filtered_files_from_backend(module, &env); + + debug!("Printing files now"); + printer.print_files(files); + + Ok(()) +} + +pub fn remove_command(module: &Module, env: CommandEnv) -> CommandResult { + let checked : bool = run_removal_checking(&env); + debug!("Checked mode: {}", checked); + if let Some(id) = get_id(env.rt, env.matches) { + debug!("Remove by id: {}", id); + + let parser = Parser::new(JsonHeaderParser::new(None)); + let file = env.bk.get_file_by_id(module, &id, &parser).unwrap(); + debug!("Remove file : {:?}", file); + + if let Err(e) = env.bk.remove_file(module, file, checked) { + debug!("Remove failed"); + let mut err = ModuleError::new("Removing file failed"); + err.caused_by = Some(Box::new(e)); + Err(err) + } else { + debug!("Remove worked"); + Ok(()) + } + } else { + debug!("Remove more than one file"); + + let files = get_filtered_files_from_backend(module, &env); + let nfiles = files.len(); + info!("Removing {} Files", nfiles); + + let errs = files.map(|file| { + debug!("Remove file: {:?}", file); + env.bk.remove_file(module, file, checked) + }) + .filter(|e| e.is_err()) + .map(|e| { + let err = e.err().unwrap(); + warn!("Error occured in Filesystem operation: {}", err); + err + }) + .collect::>(); + + let nerrs = errs.len(); + + if nerrs != 0 { + warn!("{} Errors occured while removing {} files", nerrs, nfiles); + let moderr = ModuleError::new("File removal failed"); + + // TODO : Collect StorageBackendErrors + + Err(moderr) + } else { + Ok(()) + } + } +} + +/* + * + * Private helpers + * + */ + +fn get_filtered_files_from_backend<'a>(module: &'a Module, + env: &CommandEnv) -> IntoIter> +{ + let parser = Parser::new(JsonHeaderParser::new(None)); + let tags = get_tags(env.rt, env.matches); + debug!("Tags: {:?}", tags); + env.bk.iter_files(module, &parser).and_then(|files| { + let f = files.filter(|file| { + if tags.len() != 0 { + debug!("Checking tags of: {:?}", file.id()); + get_tags_from_header(&file.header()).iter() + .any(|t| tags.contains(t)) + } else { + true + } + }).filter(|file| { + debug!("Checking matches of: {:?}", file.id()); + get_matcher(env.rt, env.matches) + .and_then(|r| Some(file.matches_with(&r))) + .unwrap_or(true) + }).collect::>(); + Some(f) + }).unwrap_or(Vec::::new()).into_iter() +} + +fn get_tags<'a>(rt: &Runtime, sub: &ArgMatches<'a, 'a>) -> Vec { + debug!("Fetching tags from commandline"); + sub.value_of("tags").and_then(|tags| + Some(tags.split(",") + .into_iter() + .map(|s| s.to_string()) + .filter(|e| + if e.contains(" ") { + warn!("Tag contains spaces: '{}'", e); + false + } else { + true + }).collect() + ) + ).or(Some(vec![])).unwrap() + +} + +fn get_matcher<'a>(rt: &Runtime, sub: &ArgMatches<'a, 'a>) -> Option { + debug!("Fetching matcher from commandline"); + if let Some(s) = sub.value_of("match") { + if let Ok(r) = Regex::new(s) { + return Some(r) + } else { + error!("Regex error, continuing without regex"); + } + } + None + +} + +fn get_id<'a>(rt: &Runtime, sub: &ArgMatches<'a, 'a>) -> Option { + debug!("Fetching id from commandline"); + sub.value_of("id").and_then(|s| Some(String::from(s))) +} + +/* + * Checks whether the commandline call was set to run the removal "checked", + * so if another entry from the store refers to this ID, do not remove the file. + */ +fn run_removal_checking(env: &CommandEnv) -> bool { + env.matches.is_present("check") +} diff --git a/src/module/bm/header.rs b/src/module/bm/header.rs new file mode 100644 index 00000000..10f20d80 --- /dev/null +++ b/src/module/bm/header.rs @@ -0,0 +1,79 @@ +use storage::file::FileHeaderSpec as FHS; +use storage::file::FileHeaderData as FHD; +use std::ops::Deref; + +pub fn get_spec() -> FHS { + FHS::Map { keys: vec![ url_key(), tags_key() ] } +} + +fn url_key() -> FHS { + FHS::Key { name: String::from("URL"), value_type: Box::new(FHS::Text) } +} + +fn tags_key() -> FHS { + FHS::Key { name: String::from("TAGS"), value_type: Box::new(text_array()) } +} + +fn text_array() -> FHS { + FHS::Array { allowed_types: vec![FHS::Text] } +} + + +pub fn build_header(url: &String, tags: &Vec) -> FHD { + FHD::Map { + keys: vec![ + FHD::Key { + name: String::from("URL"), + value: Box::new(FHD::Text(url.clone())) + }, + FHD::Key { + name: String::from("TAGS"), + value: Box::new(build_tag_array(tags)) + } + ] + } +} + +fn build_tag_array(tags: &Vec) -> FHD { + let texttags = tags.into_iter().map(|t| FHD::Text(t.clone())).collect(); + FHD::Array { values: Box::new(texttags) } +} + +pub fn get_tags_from_header(header: &FHD) -> Vec { + let mut tags : Vec = vec![]; + + fn match_array(a: &Box) -> Vec { + let mut tags : Vec = vec![]; + + match a.deref() { + &FHD::Array{values: ref vs} => { + let values : Vec = vs.deref().clone(); + for value in values { + match value { + FHD::Text(t) => tags.push(t), + _ => warn!("Malformed Header Data: Expected Text, found non-Text"), + } + } + } + _ => warn!("Malformed Header Data: Expected Array, found non-Array"), + } + + tags + } + + match header { + &FHD::Map{keys: ref ks} => { + let keys : Vec = ks.clone(); + for key in keys { + match key { + FHD::Key{name: _, value: ref v} => return match_array(v), + _ => warn!("Malformed Header Data: Expected Key, found non-Key"), + } + } + }, + _ => warn!("Malformed Header Data: Expected Map, found non-Map"), + } + + tags +} + diff --git a/src/module/bm/mod.rs b/src/module/bm/mod.rs new file mode 100644 index 00000000..e963564b --- /dev/null +++ b/src/module/bm/mod.rs @@ -0,0 +1,69 @@ +use runtime::Runtime; +use module::Module; +use module::CommandMap; +use module::ModuleResult; +use module::ModuleError; +use std::path::Path; +use std::result::Result; +use std::fmt::Result as FMTResult; +use std::fmt::Formatter; +use std::fmt::Debug; +use clap::ArgMatches; +use regex::Regex; + +mod header; +mod commands; + +use self::header::build_header; +use storage::json::parser::JsonHeaderParser; +use storage::parser::FileHeaderParser; + +use self::commands::*; + +pub struct BMModule { + path: Option, +} + +const CALLNAMES : &'static [&'static str] = &[ "bm", "bookmark" ]; + +impl BMModule { + + pub fn new(rt : &Runtime) -> BMModule { + BMModule { + path: None + } + } + +} + +impl Module for BMModule { + + fn callnames(&self) -> &'static [&'static str] { + CALLNAMES + } + + fn name(&self) -> &'static str{ + "bookmark" + } + + fn shutdown(&self, rt : &Runtime) -> ModuleResult { + Ok(()) + } + + fn get_commands(&self, rt: &Runtime) -> CommandMap { + let mut hm = CommandMap::new(); + hm.insert("add", add_command); + hm.insert("list", list_command); + hm.insert("remove", remove_command); + hm + } +} + +impl Debug for BMModule { + + fn fmt(&self, fmt: &mut Formatter) -> FMTResult { + write!(fmt, "[Module][BM]"); + Ok(()) + } + +} diff --git a/src/module/command.rs b/src/module/command.rs deleted file mode 100644 index e83ce232..00000000 --- a/src/module/command.rs +++ /dev/null @@ -1,11 +0,0 @@ -use std::result::Result; - -use super::ModuleError; -use storage::backend::{StorageBackend, StorageBackendError}; - -type CommandError = Result; -type CommandResult = Result<(), Result>; - -pub trait ExecutableCommand { - fn exec(StorageBackend) -> CommandResult; -} diff --git a/src/module/mod.rs b/src/module/mod.rs index ceca732f..7d0d0562 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -3,21 +3,28 @@ use std::error::Error; use std::fmt::Formatter; use std::fmt::Result as FMTResult; use std::fmt::Display; +use std::fmt::Debug; +use std::path::Path; use std::result::Result; +use std::collections::HashMap; -use storage::backend::StorageBackend; -use self::command::ExecutableCommand; -mod command; +use clap::{App, ArgMatches}; + +use storage::backend::{StorageBackend, StorageBackendError}; + +pub mod bm; #[derive(Debug)] pub struct ModuleError { desc: String, + caused_by: Option>, } impl ModuleError { - fn mk(desc: &'static str) -> ModuleError { + pub fn new(desc: &'static str) -> ModuleError { ModuleError { desc: desc.to_owned().to_string(), + caused_by: None, } } } @@ -29,7 +36,7 @@ impl Error for ModuleError { } fn cause(&self) -> Option<&Error> { - None + self.caused_by.as_ref().map(|e| &**e) } } @@ -40,20 +47,23 @@ impl Display for ModuleError { } } +pub struct CommandEnv<'a> { + pub rt: &'a Runtime<'a>, + pub bk: &'a StorageBackend, + pub matches: &'a ArgMatches<'a, 'a>, +} + pub type ModuleResult = Result<(), ModuleError>; +pub type CommandResult = ModuleResult; +pub type CommandMap<'a> = HashMap<&'a str, fn(&Module, CommandEnv) -> CommandResult>; -pub trait Module { +pub trait Module : Debug { - fn new(rt : &Runtime) -> Self; - fn callnames() -> &'static [&'static str]; + fn callnames(&self) -> &'static [&'static str]; fn name(&self) -> &'static str; - - fn execute(&self, rt : &Runtime) -> ModuleResult; fn shutdown(&self, rt : &Runtime) -> ModuleResult; - fn getCommandBuilder() -> F - where F: FnOnce(StorageBackend) -> T, - T: ExecutableCommand; + fn get_commands(&self, rt: &Runtime) -> CommandMap; } diff --git a/src/storage/backend.rs b/src/storage/backend.rs index 077cf8b4..e89dc049 100644 --- a/src/storage/backend.rs +++ b/src/storage/backend.rs @@ -52,13 +52,6 @@ impl StorageBackend { }) } - fn build(rt: &Runtime, m: &M) -> StorageBackend { - let path = rt.get_rtp() + m.name() + "/store"; - // TODO: Don't use "/store" but value from configuration - debug!("Building StorageBackend for {}", path); - StorageBackend::new(path) - } - fn get_file_ids(&self, m: &Module) -> Option> { let list = glob(&self.prefix_of_files_for_module(m)[..]); @@ -267,12 +260,12 @@ impl StorageBackendError { fn build(action: &'static str, desc: &'static str, - data : Option) -> StorageBackendError + data : Option) -> StorageBackendError { StorageBackendError { action: String::from(action), desc: String::from(desc), - dataDump: data, + data_dump: data, caused_by: None, } } @@ -291,7 +284,7 @@ impl Error for StorageBackendError { } -impl Display for StorageBackendError { +impl<'a> Display for StorageBackendError { fn fmt(&self, f: &mut Formatter) -> FMTResult { write!(f, "StorageBackendError[{}]: {}", self.action, self.desc) diff --git a/src/storage/file.rs b/src/storage/file.rs index 75bc3b8e..aca195c4 100644 --- a/src/storage/file.rs +++ b/src/storage/file.rs @@ -16,7 +16,7 @@ pub enum FileHeaderSpec { UInteger, Float, Text, - Key { name: &'static str, value_type: Box }, + Key { name: String, value_type: Box }, Map { keys: Vec }, Array { allowed_types: Vec }, } @@ -30,7 +30,7 @@ pub enum FileHeaderData { UInteger(u64), Float(f64), Text(String), - Key { name: &'static str, value: Box }, + Key { name: String, value: Box }, Map { keys: Vec }, Array { values: Box> }, }