Merge branch 'bm-convenience'
This commit is contained in:
commit
333530607c
6 changed files with 306 additions and 68 deletions
96
etc/cli.yml
96
etc/cli.yml
|
@ -118,12 +118,98 @@ subcommands:
|
||||||
required: false
|
required: false
|
||||||
takes_value: true
|
takes_value: true
|
||||||
|
|
||||||
- check:
|
- add_tags:
|
||||||
short: c
|
about: Add tags to bookmark(s)
|
||||||
long: check
|
version: 0.1
|
||||||
help: Ensure there are no references to this link
|
author: Matthias Beyer <mail@beyermatthias.de>
|
||||||
|
args:
|
||||||
|
- with_id:
|
||||||
|
long: with-id
|
||||||
|
help: Add tags to bookmark with ID
|
||||||
required: false
|
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:
|
- todo:
|
||||||
about: Todo module
|
about: Todo module
|
||||||
|
|
|
@ -23,6 +23,7 @@ mod runtime;
|
||||||
mod module;
|
mod module;
|
||||||
mod storage;
|
mod storage;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
mod util;
|
||||||
|
|
||||||
use module::bm::BM;
|
use module::bm::BM;
|
||||||
|
|
||||||
|
|
|
@ -34,3 +34,7 @@ pub fn get_url_from_header(header: &FHD) -> Option<String> {
|
||||||
headerhelpers::data::get_url_from_header(header)
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,29 @@
|
||||||
use std::fmt::{Debug, Display, Formatter};
|
use std::fmt::{Debug, Display, Formatter};
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::cell::RefCell;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::process::exit;
|
||||||
|
|
||||||
use clap::ArgMatches;
|
use clap::ArgMatches;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
use runtime::Runtime;
|
use runtime::Runtime;
|
||||||
use module::Module;
|
use module::Module;
|
||||||
|
|
||||||
|
use storage::Store;
|
||||||
use storage::file::hash::FileHash;
|
use storage::file::hash::FileHash;
|
||||||
use storage::file::id::FileID;
|
use storage::file::id::FileID;
|
||||||
|
use storage::file::File;
|
||||||
use storage::parser::FileHeaderParser;
|
use storage::parser::FileHeaderParser;
|
||||||
use storage::parser::Parser;
|
use storage::parser::Parser;
|
||||||
use storage::json::parser::JsonHeaderParser;
|
use storage::json::parser::JsonHeaderParser;
|
||||||
|
|
||||||
mod header;
|
mod header;
|
||||||
|
|
||||||
|
use self::header::get_url_from_header;
|
||||||
|
use self::header::get_tags_from_header;
|
||||||
|
|
||||||
pub struct BM<'a> {
|
pub struct BM<'a> {
|
||||||
rt: &'a Runtime<'a>,
|
rt: &'a Runtime<'a>,
|
||||||
}
|
}
|
||||||
|
@ -31,11 +41,20 @@ impl<'a> BM<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn command_add(&self, matches: &ArgMatches) -> bool {
|
fn command_add(&self, matches: &ArgMatches) -> bool {
|
||||||
|
use std::process::exit;
|
||||||
use self::header::build_header;
|
use self::header::build_header;
|
||||||
|
|
||||||
let parser = Parser::new(JsonHeaderParser::new(None));
|
let parser = Parser::new(JsonHeaderParser::new(None));
|
||||||
|
|
||||||
let url = matches.value_of("url").map(String::from).unwrap(); // clap ensures this is present
|
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| {
|
let tags = matches.value_of("tags").and_then(|s| {
|
||||||
Some(s.split(",").map(String::from).collect())
|
Some(s.split(",").map(String::from).collect())
|
||||||
}).unwrap_or(vec![]);
|
}).unwrap_or(vec![]);
|
||||||
|
@ -52,10 +71,37 @@ impl<'a> BM<'a> {
|
||||||
}).unwrap_or(false)
|
}).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 {
|
fn command_list(&self, matches: &ArgMatches) -> bool {
|
||||||
use ui::file::{FilePrinter, TablePrinter};
|
use ui::file::{FilePrinter, TablePrinter};
|
||||||
use self::header::get_url_from_header;
|
|
||||||
use self::header::get_tags_from_header;
|
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
let parser = Parser::new(JsonHeaderParser::new(None));
|
let parser = Parser::new(JsonHeaderParser::new(None));
|
||||||
|
@ -81,27 +127,21 @@ impl<'a> BM<'a> {
|
||||||
fn command_remove(&self, matches: &ArgMatches) -> bool {
|
fn command_remove(&self, matches: &ArgMatches) -> bool {
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
|
||||||
let result =
|
let (filtered, files) = self.get_files(matches, "id", "match", "tags");
|
||||||
if matches.is_present("id") {
|
|
||||||
debug!("Removing by ID (Hash)");
|
if !filtered {
|
||||||
let hash = FileHash::from(matches.value_of("id").unwrap());
|
error!("Unexpected error. Exiting");
|
||||||
self.remove_by_hash(hash)
|
exit(1);
|
||||||
} else if matches.is_present("tags") {
|
}
|
||||||
debug!("Removing by tags");
|
|
||||||
let tags = matches.value_of("tags")
|
let result = files
|
||||||
.unwrap()
|
.iter()
|
||||||
.split(",")
|
.map(|file| {
|
||||||
.map(String::from)
|
debug!("File loaded, can remove now: {:?}", file);
|
||||||
.collect::<Vec<String>>();
|
let f = file.deref().borrow();
|
||||||
self.remove_by_tags(tags)
|
self.rt.store().remove(f.id().clone())
|
||||||
} else if matches.is_present("match") {
|
})
|
||||||
debug!("Removing by match");
|
.all(|x| x);
|
||||||
self.remove_by_match(String::from(matches.value_of("match").unwrap()))
|
|
||||||
} else {
|
|
||||||
error!("Unexpected error. Exiting");
|
|
||||||
exit(1);
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
if result {
|
if result {
|
||||||
info!("Removing succeeded");
|
info!("Removing succeeded");
|
||||||
|
@ -112,49 +152,122 @@ impl<'a> BM<'a> {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_by_hash(&self, hash: FileHash) -> bool {
|
fn command_add_tags(&self, matches: &ArgMatches) -> bool {
|
||||||
use std::ops::Deref;
|
self.alter_tags_in_files(matches, |old_tags, cli_tags| {
|
||||||
|
let mut new_tags = old_tags.clone();
|
||||||
debug!("Removing for hash = '{:?}'", hash);
|
new_tags.append(&mut cli_tags.clone());
|
||||||
let parser = Parser::new(JsonHeaderParser::new(None));
|
new_tags
|
||||||
|
})
|
||||||
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<String>) -> bool {
|
fn command_rm_tags(&self, matches: &ArgMatches) -> bool {
|
||||||
use std::fs::remove_file;
|
self.alter_tags_in_files(matches, |old_tags, cli_tags| {
|
||||||
use std::ops::Deref;
|
old_tags.clone()
|
||||||
use self::header::get_tags_from_header;
|
.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));
|
let parser = Parser::new(JsonHeaderParser::new(None));
|
||||||
self.rt
|
self.rt
|
||||||
.store()
|
.store()
|
||||||
.load_for_module(self, &parser)
|
.load_by_hash(self, &parser, hash)
|
||||||
.iter()
|
.map(|f| vec![f])
|
||||||
.filter(|file| {
|
.unwrap_or(vec![])
|
||||||
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 {
|
fn get_files_by_match(&self, matcher: String) -> Vec<Rc<RefCell<File>>> {
|
||||||
use self::header::get_url_from_header;
|
let parser = Parser::new(JsonHeaderParser::new(None));
|
||||||
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| {
|
let re = Regex::new(&matcher[..]).unwrap_or_else(|e| {
|
||||||
error!("Cannot build regex out of '{}'", matcher);
|
error!("Cannot build regex out of '{}'", matcher);
|
||||||
error!("{}", e);
|
error!("{}", e);
|
||||||
|
@ -163,11 +276,10 @@ impl<'a> BM<'a> {
|
||||||
|
|
||||||
debug!("Compiled '{}' to regex: '{:?}'", matcher, re);
|
debug!("Compiled '{}' to regex: '{:?}'", matcher, re);
|
||||||
|
|
||||||
let parser = Parser::new(JsonHeaderParser::new(None));
|
|
||||||
self.rt
|
self.rt
|
||||||
.store()
|
.store()
|
||||||
.load_for_module(self, &parser)
|
.load_for_module(self, &parser)
|
||||||
.iter()
|
.into_iter()
|
||||||
.filter(|file| {
|
.filter(|file| {
|
||||||
let f = file.deref().borrow();
|
let f = file.deref().borrow();
|
||||||
let url = get_url_from_header(f.header());
|
let url = get_url_from_header(f.header());
|
||||||
|
@ -176,10 +288,23 @@ impl<'a> BM<'a> {
|
||||||
debug!("Matching '{}' ~= '{}'", re.as_str(), u);
|
debug!("Matching '{}' ~= '{}'", re.as_str(), u);
|
||||||
re.is_match(&u[..])
|
re.is_match(&u[..])
|
||||||
}).unwrap_or(false)
|
}).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();
|
let f = file.deref().borrow();
|
||||||
self.rt.store().remove(f.id().clone())
|
get_tags_from_header(f.header()).iter().any(|tag| {
|
||||||
}).all(|x| x)
|
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())
|
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 => {
|
Some(_) | None => {
|
||||||
info!("No command given, doing nothing");
|
info!("No command given, doing nothing");
|
||||||
false
|
false
|
||||||
|
|
|
@ -40,6 +40,10 @@ impl File {
|
||||||
&self.header
|
&self.header
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_header(&mut self, new_header: FileHeaderData) {
|
||||||
|
self.header = new_header;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn data(&self) -> &String {
|
pub fn data(&self) -> &String {
|
||||||
&self.data
|
&self.data
|
||||||
}
|
}
|
||||||
|
|
6
src/util/mod.rs
Normal file
6
src/util/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub fn is_url(url: &String) -> bool {
|
||||||
|
Url::parse(&url[..]).is_ok()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue