diff --git a/src/main.rs b/src/main.rs index 44a8c32f..0d4716a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,7 +43,7 @@ fn main() { debug!("Runtime : {:?}", &rt); if let Some(matches) = rt.config.cli_matches.subcommand_matches("bm") { - let res = BM::new(&rt).exec(matches.subcommand_matches("bm").unwrap()); + let res = BM::new(&rt).exec(matches); info!("BM exited with {}", res); } else { info!("No commandline call...") diff --git a/src/module/bm/header.rs b/src/module/bm/header.rs new file mode 100644 index 00000000..0f0f9076 --- /dev/null +++ b/src/module/bm/header.rs @@ -0,0 +1,36 @@ +use module::helpers::header as headerhelpers; +use storage::file::header::data::FileHeaderData as FHD; +use storage::file::header::spec::FileHeaderSpec as FHS; + +pub fn get_spec() -> FHS { + FHS::Map { + keys: vec![ + headerhelpers::tags::spec::url_key(), + headerhelpers::tags::spec::tags_key(), + ] + } +} + +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(headerhelpers::tags::data::build_tag_array(tags)) + } + ] + } +} + +pub fn get_tags_from_header(header: &FHD) -> Vec { + headerhelpers::tags::data::get_tags_from_header(header) +} + +pub fn get_url_from_header(header: &FHD) -> Option { + headerhelpers::data::get_url_from_header(header) +} + diff --git a/src/module/bm/mod.rs b/src/module/bm/mod.rs index a390020d..ff56e591 100644 --- a/src/module/bm/mod.rs +++ b/src/module/bm/mod.rs @@ -6,6 +6,14 @@ use clap::ArgMatches; use runtime::Runtime; use module::Module; +use storage::file::hash::FileHash; +use storage::file::id::FileID; +use storage::parser::FileHeaderParser; +use storage::parser::Parser; +use storage::json::parser::JsonHeaderParser; + +mod header; + pub struct BM<'a> { rt: &'a Runtime<'a>, } @@ -22,16 +30,185 @@ impl<'a> BM<'a> { &self.rt } + fn command_add(&self, matches: &ArgMatches) -> bool { + use self::header::build_header; + + let parser = Parser::new(JsonHeaderParser::new(None)); + + let url = matches.value_of("url").map(String::from).unwrap(); // clap ensures this is present + let tags = matches.value_of("tags").and_then(|s| { + Some(s.split(",").map(String::from).collect()) + }).unwrap_or(vec![]); + + debug!("Building header with"); + debug!(" url = '{:?}'", url); + debug!(" tags = '{:?}'", tags); + let header = build_header(url, tags); + + let fileid = self.rt.store().new_file_with_header(self, header); + self.rt.store().load(&fileid).and_then(|file| { + info!("Created file in memory: {}", fileid); + Some(self.rt.store().persist(&parser, file)) + }).unwrap_or(false) + } + + fn command_list(&self, matches: &ArgMatches) -> bool { + use ui::file::{FilePrinter, TablePrinter}; + use self::header::get_url_from_header; + use self::header::get_tags_from_header; + use std::ops::Deref; + + let parser = Parser::new(JsonHeaderParser::new(None)); + let files = self.rt.store().load_for_module(self, &parser); + let printer = TablePrinter::new(self.rt.is_verbose(), self.rt.is_debugging()); + + printer.print_files_custom(files.into_iter(), + &|file| { + let fl = file.deref().borrow(); + let hdr = fl.header(); + let url = get_url_from_header(hdr).unwrap_or(String::from("Parser error")); + let tags = get_tags_from_header(hdr); + + debug!("Custom printer field: url = '{:?}'", url); + debug!("Custom printer field: tags = '{:?}'", tags); + + vec![url, tags.join(", ")] + } + ); + true + } + + fn command_remove(&self, matches: &ArgMatches) -> bool { + use std::process::exit; + + let result = + if matches.is_present("id") { + debug!("Removing by ID (Hash)"); + let hash = FileHash::from(matches.value_of("id").unwrap()); + self.remove_by_hash(hash) + } else if matches.is_present("tags") { + debug!("Removing by tags"); + let tags = matches.value_of("tags") + .unwrap() + .split(",") + .map(String::from) + .collect::>(); + self.remove_by_tags(tags) + } else if matches.is_present("match") { + debug!("Removing by match"); + self.remove_by_match(String::from(matches.value_of("match").unwrap())) + } else { + error!("Unexpected error. Exiting"); + exit(1); + false + }; + + if result { + info!("Removing succeeded"); + } else { + info!("Removing failed"); + } + + return result; + } + + fn remove_by_hash(&self, hash: FileHash) -> bool { + use std::ops::Deref; + + debug!("Removing for hash = '{:?}'", hash); + let parser = Parser::new(JsonHeaderParser::new(None)); + + let file = self.rt.store().load_by_hash(self, &parser, hash); + debug!("file = {:?}", file); + file.map(|file| { + debug!("File loaded, can remove now: {:?}", file); + let f = file.deref().borrow(); + self.rt.store().remove(f.id().clone()) + }).unwrap_or(false) + } + + fn remove_by_tags(&self, tags: Vec) -> bool { + use std::fs::remove_file; + use std::ops::Deref; + use self::header::get_tags_from_header; + + let parser = Parser::new(JsonHeaderParser::new(None)); + self.rt + .store() + .load_for_module(self, &parser) + .iter() + .filter(|file| { + let f = file.deref().borrow(); + get_tags_from_header(f.header()).iter().any(|tag| { + tags.iter().any(|remtag| remtag == tag) + }) + }).map(|file| { + let f = file.deref().borrow(); + self.rt.store().remove(f.id().clone()) + }).all(|x| x) + } + + fn remove_by_match(&self, matcher: String) -> bool { + use self::header::get_url_from_header; + use std::fs::remove_file; + use std::ops::Deref; + use std::process::exit; + use regex::Regex; + + let re = Regex::new(&matcher[..]).unwrap_or_else(|e| { + error!("Cannot build regex out of '{}'", matcher); + error!("{}", e); + exit(1); + }); + + debug!("Compiled '{}' to regex: '{:?}'", matcher, re); + + let parser = Parser::new(JsonHeaderParser::new(None)); + self.rt + .store() + .load_for_module(self, &parser) + .iter() + .filter(|file| { + let f = file.deref().borrow(); + let url = get_url_from_header(f.header()); + debug!("url = {:?}", url); + url.map(|u| { + debug!("Matching '{}' ~= '{}'", re.as_str(), u); + re.is_match(&u[..]) + }).unwrap_or(false) + }).map(|file| { + let f = file.deref().borrow(); + self.rt.store().remove(f.id().clone()) + }).all(|x| x) + } + } impl<'a> Module<'a> for BM<'a> { fn exec(&self, matches: &ArgMatches) -> bool { - unimplemented!() + match matches.subcommand_name() { + Some("add") => { + self.command_add(matches.subcommand_matches("add").unwrap()) + }, + + Some("list") => { + self.command_list(matches.subcommand_matches("list").unwrap()) + }, + + Some("remove") => { + self.command_remove(matches.subcommand_matches("remove").unwrap()) + }, + + Some(_) | None => { + info!("No command given, doing nothing"); + false + }, + } } fn name(&self) -> &'static str { - "bm" + "bookmark" } } @@ -43,3 +220,4 @@ impl<'a> Debug for BM<'a> { } } + diff --git a/src/module/helpers/header/mod.rs b/src/module/helpers/header/mod.rs index 0d4ba67d..250decc2 100644 --- a/src/module/helpers/header/mod.rs +++ b/src/module/helpers/header/mod.rs @@ -19,7 +19,15 @@ pub mod data { } }).and_then(|urlkey| { match urlkey.deref().clone() { - FHD::Text(s) => Some(s), + FHD::Key{name: _, value: ref v} => { + match v.deref().clone() { + FHD::Text(s) => Some(s), + _ => { + warn!("Malformed Header Data: Expected Text, found non-Text"); + None + }, + } + } _ => { warn!("Malformed Header Data: Expected Text, found non-Text"); None diff --git a/src/module/helpers/header/tags.rs b/src/module/helpers/header/tags.rs index 85f49976..a78a2fb4 100644 --- a/src/module/helpers/header/tags.rs +++ b/src/module/helpers/header/tags.rs @@ -20,7 +20,7 @@ pub mod data { use std::ops::Deref; use storage::file::header::data::FileHeaderData as FHD; - pub fn build_tag_array(tags: &Vec) -> FHD { + pub 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) } }