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
|
||||
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
|
||||
|
|
|
@ -23,6 +23,7 @@ mod runtime;
|
|||
mod module;
|
||||
mod storage;
|
||||
mod ui;
|
||||
mod util;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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::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 {
|
||||
let (filtered, files) = self.get_files(matches, "id", "match", "tags");
|
||||
|
||||
if !filtered {
|
||||
error!("Unexpected error. Exiting");
|
||||
exit(1);
|
||||
false
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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
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