diff --git a/etc/cli.yml b/etc/cli.yml index 4a6590d2..50ca507c 100644 --- a/etc/cli.yml +++ b/etc/cli.yml @@ -118,12 +118,98 @@ subcommands: required: false takes_value: true - - check: - short: c - long: check - help: Ensure there are no references to this link + - add_tags: + about: Add tags to bookmark(s) + version: 0.1 + author: Matthias Beyer + args: + - with_id: + long: with-id + help: Add tags to bookmark with ID required: false - takes_value: false + takes_value: true + + - with_match: + short: m + long: with-match + help: Add tags to bookmark(s) which match this regex + required: false + takes_value: true + + - with_tags: + long: with-tags + help: Add tags to bookmark(s) which have these tag(s) + required: false + takes_value: true + + - tags: + short: t + long: tags + help: Add these tags + required: true + takes_value: true + + - rm_tags: + about: Remove tags from bookmark(s) + version: 0.1 + author: Matthias Beyer + args: + - with_id: + long: with-id + help: Remove tags from bookmark with ID + required: false + takes_value: true + + - with_match: + short: m + long: with-match + help: Remove tags from bookmark(s) which match this regex + required: false + takes_value: true + + - with_tags: + long: with-tags + help: Remove tags from bookmark(s) which have these tag(s) + required: false + takes_value: true + + - tags: + short: t + long: tags + help: Remove these tags + required: true + takes_value: true + + - set_tags: + about: Set tags in bookmark(s) + version: 0.1 + author: Matthias Beyer + args: + - to_id: + long: to-id + help: Set tags in bookmark with this id + required: false + takes_value: true + + - to_match: + short: m + long: to-match + help: Set tags in bookmark(s) which match this regex + required: false + takes_value: true + + - to_tags: + long: to-tags + help: Set tags in bookmark(s) which have these tag(s) + required: false + takes_value: true + + - tags: + short: t + long: tags + help: Set these tags + required: true + takes_value: true - todo: about: Todo module diff --git a/src/main.rs b/src/main.rs index 0d4716a4..19ffa8c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ mod runtime; mod module; mod storage; mod ui; +mod util; use module::bm::BM; diff --git a/src/module/bm/header.rs b/src/module/bm/header.rs index 0f0f9076..3bbe2861 100644 --- a/src/module/bm/header.rs +++ b/src/module/bm/header.rs @@ -34,3 +34,7 @@ pub fn get_url_from_header(header: &FHD) -> Option { headerhelpers::data::get_url_from_header(header) } +pub fn rebuild_header_with_tags(header: &FHD, tags: Vec) -> Option { + get_url_from_header(header).map(|url| build_header(url, tags)) +} + diff --git a/src/module/bm/mod.rs b/src/module/bm/mod.rs index ff56e591..6644c59e 100644 --- a/src/module/bm/mod.rs +++ b/src/module/bm/mod.rs @@ -1,19 +1,29 @@ use std::fmt::{Debug, Display, Formatter}; +use std::rc::Rc; +use std::cell::RefCell; use std::fmt; +use std::ops::Deref; +use std::process::exit; use clap::ArgMatches; +use regex::Regex; use runtime::Runtime; use module::Module; +use storage::Store; use storage::file::hash::FileHash; use storage::file::id::FileID; +use storage::file::File; use storage::parser::FileHeaderParser; use storage::parser::Parser; use storage::json::parser::JsonHeaderParser; mod header; +use self::header::get_url_from_header; +use self::header::get_tags_from_header; + pub struct BM<'a> { rt: &'a Runtime<'a>, } @@ -31,11 +41,20 @@ impl<'a> BM<'a> { } fn command_add(&self, matches: &ArgMatches) -> bool { + use std::process::exit; 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 + + if !self.validate_url(&url, &parser) { + error!("URL validation failed, exiting."); + exit(1); + } else { + debug!("Verification succeeded"); + } + let tags = matches.value_of("tags").and_then(|s| { Some(s.split(",").map(String::from).collect()) }).unwrap_or(vec![]); @@ -52,10 +71,37 @@ impl<'a> BM<'a> { }).unwrap_or(false) } + fn validate_url(&self, url: &String, parser: &Parser) -> bool + where HP: FileHeaderParser + { + use util::is_url; + + if !is_url(url) { + error!("Url '{}' is not a valid URL. Will not store.", url); + return false; + } + + let is_in_store = self.rt + .store() + .load_for_module(self, parser) + .iter() + .any(|file| { + let f = file.deref().borrow(); + get_url_from_header(f.header()).map(|url_in_store| { + &url_in_store == url + }).unwrap_or(false) + }); + + if is_in_store { + error!("URL '{}' seems to be in the store already", url); + return false; + } + + return true; + } + 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)); @@ -81,27 +127,21 @@ impl<'a> BM<'a> { 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 - }; + let (filtered, files) = self.get_files(matches, "id", "match", "tags"); + + if !filtered { + error!("Unexpected error. Exiting"); + exit(1); + } + + let result = files + .iter() + .map(|file| { + debug!("File loaded, can remove now: {:?}", file); + let f = file.deref().borrow(); + self.rt.store().remove(f.id().clone()) + }) + .all(|x| x); if result { info!("Removing succeeded"); @@ -112,49 +152,122 @@ impl<'a> BM<'a> { 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 command_add_tags(&self, matches: &ArgMatches) -> bool { + self.alter_tags_in_files(matches, |old_tags, cli_tags| { + let mut new_tags = old_tags.clone(); + new_tags.append(&mut cli_tags.clone()); + new_tags + }) } - fn remove_by_tags(&self, tags: Vec) -> bool { - use std::fs::remove_file; - use std::ops::Deref; - use self::header::get_tags_from_header; + fn command_rm_tags(&self, matches: &ArgMatches) -> bool { + self.alter_tags_in_files(matches, |old_tags, cli_tags| { + old_tags.clone() + .into_iter() + .filter(|tag| !cli_tags.contains(tag)) + .collect() + }) + } + fn command_set_tags(&self, matches: &ArgMatches) -> bool { + self.alter_tags_in_files(matches, |old_tags, cli_tags| { + cli_tags.clone() + }) + } + + fn alter_tags_in_files(&self, matches: &ArgMatches, generate_new_tags: F) -> bool + where F: Fn(Vec, &Vec) -> Vec + { + use self::header::rebuild_header_with_tags; + + let cli_tags = matches.value_of("tags") + .map(|ts| { + ts.split(",") + .map(String::from) + .collect::>() + }) + .unwrap_or(vec![]); + + let (filter, files) = self.get_files(matches, "with_id", "with_match", "with_tags"); + + if !filter { + warn!("There were no filter applied when loading the files"); + } + + let parser = Parser::new(JsonHeaderParser::new(None)); + files + .into_iter() + .map(|file| { + debug!("Remove tags from file: {:?}", file); + + let hdr = { + let f = file.deref().borrow(); + f.header().clone() + }; + + debug!("Tags:..."); + let old_tags = get_tags_from_header(&hdr); + debug!(" old_tags = {:?}", &old_tags); + debug!(" cli_tags = {:?}", &cli_tags); + + let new_tags = generate_new_tags(old_tags, &cli_tags); + debug!(" new_tags = {:?}", &new_tags); + + let new_header = rebuild_header_with_tags(&hdr, new_tags) + .unwrap_or_else(|| { + error!("Could not rebuild header for file"); + exit(1); + }); + { + let mut f_mut = file.deref().borrow_mut(); + f_mut.set_header(new_header); + } + + self.rt.store().persist(&parser, file); + true + }) + .all(|x| x) + } + + + fn get_files(&self, + matches: &ArgMatches, + id_key: &'static str, + match_key: &'static str, + tag_key: &'static str) + -> (bool, Vec>>) + { + if matches.is_present(id_key) { + let hash = FileHash::from(matches.value_of(id_key).unwrap()); + (true, self.get_files_by_id(hash)) + } else if matches.is_present(match_key) { + let matcher = String::from(matches.value_of(match_key).unwrap()); + (true, self.get_files_by_match(matcher)) + } else if matches.is_present(tag_key) { + let tags = matches.value_of(tag_key) + .unwrap() + .split(",") + .map(String::from) + .collect::>(); + (true, self.get_files_by_tags(tags)) + } else { + // get all files + let parser = Parser::new(JsonHeaderParser::new(None)); + (false, self.rt.store().load_for_module(self, &parser)) + } + } + + fn get_files_by_id(&self, hash: FileHash) -> Vec>> { 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) + .load_by_hash(self, &parser, hash) + .map(|f| vec![f]) + .unwrap_or(vec![]) } - 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; - + fn get_files_by_match(&self, matcher: String) -> Vec>> { + let parser = Parser::new(JsonHeaderParser::new(None)); let re = Regex::new(&matcher[..]).unwrap_or_else(|e| { error!("Cannot build regex out of '{}'", matcher); error!("{}", e); @@ -163,11 +276,10 @@ impl<'a> BM<'a> { debug!("Compiled '{}' to regex: '{:?}'", matcher, re); - let parser = Parser::new(JsonHeaderParser::new(None)); self.rt .store() .load_for_module(self, &parser) - .iter() + .into_iter() .filter(|file| { let f = file.deref().borrow(); let url = get_url_from_header(f.header()); @@ -176,10 +288,23 @@ impl<'a> BM<'a> { debug!("Matching '{}' ~= '{}'", re.as_str(), u); re.is_match(&u[..]) }).unwrap_or(false) - }).map(|file| { + }) + .collect() + } + + fn get_files_by_tags(&self, tags: Vec) -> Vec>> { + let parser = Parser::new(JsonHeaderParser::new(None)); + self.rt + .store() + .load_for_module(self, &parser) + .into_iter() + .filter(|file| { let f = file.deref().borrow(); - self.rt.store().remove(f.id().clone()) - }).all(|x| x) + get_tags_from_header(f.header()).iter().any(|tag| { + tags.iter().any(|remtag| remtag == tag) + }) + }) + .collect() } } @@ -200,6 +325,18 @@ impl<'a> Module<'a> for BM<'a> { self.command_remove(matches.subcommand_matches("remove").unwrap()) }, + Some("add_tags") => { + self.command_add_tags(matches.subcommand_matches("add_tags").unwrap()) + }, + + Some("rm_tags") => { + self.command_rm_tags(matches.subcommand_matches("rm_tags").unwrap()) + }, + + Some("set_tags") => { + self.command_set_tags(matches.subcommand_matches("set_tags").unwrap()) + }, + Some(_) | None => { info!("No command given, doing nothing"); false diff --git a/src/storage/file/mod.rs b/src/storage/file/mod.rs index 14b4e1e3..16a072ef 100644 --- a/src/storage/file/mod.rs +++ b/src/storage/file/mod.rs @@ -40,6 +40,10 @@ impl File { &self.header } + pub fn set_header(&mut self, new_header: FileHeaderData) { + self.header = new_header; + } + pub fn data(&self) -> &String { &self.data } diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 00000000..6bef3251 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,6 @@ +use url::Url; + +pub fn is_url(url: &String) -> bool { + Url::parse(&url[..]).is_ok() +} +