Merge branch 'bm-convenience'

This commit is contained in:
Matthias Beyer 2015-12-28 21:54:26 +01:00
commit 333530607c
6 changed files with 306 additions and 68 deletions

View file

@ -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 <mail@beyermatthias.de>
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 <mail@beyermatthias.de>
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 <mail@beyermatthias.de>
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

View file

@ -23,6 +23,7 @@ mod runtime;
mod module;
mod storage;
mod ui;
mod util;
use module::bm::BM;

View file

@ -34,3 +34,7 @@ pub fn get_url_from_header(header: &FHD) -> Option<String> {
headerhelpers::data::get_url_from_header(header)
}
pub fn rebuild_header_with_tags(header: &FHD, tags: Vec<String>) -> Option<FHD> {
get_url_from_header(header).map(|url| build_header(url, tags))
}

View file

@ -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<HP>(&self, url: &String, parser: &Parser<HP>) -> 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::<Vec<String>>();
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<String>) -> 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<F>(&self, matches: &ArgMatches, generate_new_tags: F) -> bool
where F: Fn(Vec<String>, &Vec<String>) -> Vec<String>
{
use self::header::rebuild_header_with_tags;
let cli_tags = matches.value_of("tags")
.map(|ts| {
ts.split(",")
.map(String::from)
.collect::<Vec<String>>()
})
.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<Rc<RefCell<File>>>)
{
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::<Vec<String>>();
(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<Rc<RefCell<File>>> {
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<Rc<RefCell<File>>> {
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<String>) -> Vec<Rc<RefCell<File>>> {
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

View file

@ -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
}

6
src/util/mod.rs Normal file
View file

@ -0,0 +1,6 @@
use url::Url;
pub fn is_url(url: &String) -> bool {
Url::parse(&url[..]).is_ok()
}