mirror of
https://git.alemi.dev/memo-cli.git
synced 2024-11-14 05:39:19 +01:00
feat: refactor storage, add tag, add tree view
This commit is contained in:
parent
408cb9fda1
commit
cfe29f0c12
8 changed files with 399 additions and 326 deletions
112
src/categorize.rs
Normal file
112
src/categorize.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
use colored::Colorize;
|
||||
|
||||
use crate::{model::memo::Memo, utils::HumanDisplay};
|
||||
|
||||
pub struct MemoNode {
|
||||
tag: String,
|
||||
memo: Vec<Memo>,
|
||||
sub: Vec<MemoNode>,
|
||||
}
|
||||
|
||||
pub type Tree = MemoNode;
|
||||
|
||||
impl Tree {
|
||||
pub fn new() -> Tree {
|
||||
Tree {
|
||||
tag: "".into(),
|
||||
memo: vec![],
|
||||
sub: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_tree(node: &MemoNode, depth: i32, colored: bool) -> String {
|
||||
let mut out = "".to_string();
|
||||
let mut prefix = "".to_string(); // TODO string builder?
|
||||
for i in 0..depth {
|
||||
if i == depth - 1 {
|
||||
prefix.push_str("├");
|
||||
} else {
|
||||
prefix.push_str("│");
|
||||
}
|
||||
}
|
||||
|
||||
if colored {
|
||||
prefix = prefix.bright_black().to_string();
|
||||
}
|
||||
|
||||
|
||||
for memo in &node.memo {
|
||||
let memo_str = if colored { memo.colored() } else { memo.human() };
|
||||
let sep = if colored { "╴".bright_black().to_string() } else { "╴".to_string() };
|
||||
out.push_str(format!("{}{}{}\n", prefix, sep, memo_str).as_str());
|
||||
}
|
||||
|
||||
for sub in &node.sub {
|
||||
let sep = if colored { "┐".bright_black().to_string() } else { "┐".to_string() };
|
||||
out.push_str(format!("{}{} #{}\n", prefix, sep, sub.tag).as_str());
|
||||
out.push_str(format_tree(sub, depth + 1, colored).as_str());
|
||||
}
|
||||
|
||||
// if prefix.len() > 0 {
|
||||
// out.push_str(&prefix.replace("├", "╵\n"));
|
||||
// }
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn insert(memo: Memo, node:&mut MemoNode, frag: Vec<String>) {
|
||||
match frag.first() {
|
||||
None => node.memo.push(memo),
|
||||
Some(x) => {
|
||||
if !node.sub.iter().any(|sub| &sub.tag == x) { // TODO have to iter twice coz borrows
|
||||
node.sub.push(MemoNode {
|
||||
tag: x.clone(),
|
||||
memo: vec![],
|
||||
sub: vec![],
|
||||
});
|
||||
}
|
||||
let sub = node.sub.iter_mut().find(|sub| &sub.tag == x).expect("Could not find matching node");
|
||||
let sub_frag = frag.iter().enumerate().filter(|(i,_x)| i > &0).map(|(_i,x)| x.clone()).collect();
|
||||
insert(memo, sub, sub_frag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn make_tree(memos: Vec<Memo>) -> Tree {
|
||||
let mut tree = Tree::new();
|
||||
|
||||
for memo in memos {
|
||||
let fragments : Vec<String> = memo.tag.split(".")
|
||||
.map(|x| x.to_string())
|
||||
.filter(|x| x.len() > 0)
|
||||
.collect();
|
||||
|
||||
insert(memo, &mut tree, fragments);
|
||||
}
|
||||
|
||||
tree
|
||||
}
|
||||
|
||||
pub fn traverse(tree: &Tree, query: String) -> &Tree {
|
||||
return recursive_traverse(
|
||||
tree,
|
||||
query.split(".").filter(|x| x.len() > 0).map(|x| x.to_string()).collect()
|
||||
);
|
||||
}
|
||||
|
||||
fn recursive_traverse(tree: &Tree, query: Vec<String>) -> &Tree {
|
||||
match query.first() {
|
||||
None => { return tree; },
|
||||
Some(frag) => {
|
||||
for sub in &tree.sub {
|
||||
if &sub.tag == frag {
|
||||
let sub_frag = query.iter().enumerate().filter(|(i, _x)| i > &0).map(|(_i,x)| x.clone()).collect();
|
||||
return recursive_traverse(sub, sub_frag);
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
}
|
173
src/main.rs
173
src/main.rs
|
@ -2,7 +2,9 @@ mod remote;
|
|||
mod storage;
|
||||
mod utils;
|
||||
mod model;
|
||||
mod categorize;
|
||||
|
||||
use categorize::{make_tree, format_tree, traverse};
|
||||
use chrono::{DateTime, Local, Utc};
|
||||
use clap::{Parser, Subcommand};
|
||||
use colored::Colorize;
|
||||
|
@ -10,13 +12,13 @@ use const_format::concatcp;
|
|||
use git_version::git_version;
|
||||
use notify_rust::Notification;
|
||||
use regex::Regex;
|
||||
use remote::RemoteSync;
|
||||
// use remote::RemoteSync;
|
||||
use std::path::PathBuf;
|
||||
use storage::{
|
||||
AuthStorage, JsonStorage, MemoStorage, SQLiteStorage, StateStorage,
|
||||
SUPPORTED_FORMATS,
|
||||
JsonStorage, SQLiteStorage,
|
||||
SUPPORTED_FORMATS, StorageDriver,
|
||||
};
|
||||
use model::memo::{Memo, MemoError};
|
||||
use model::{memo::{Memo, MemoError}, state::Context};
|
||||
use utils::{find_by_regex, find_db_file, parse_human_duration, HumanDisplay};
|
||||
|
||||
const GIT_VERSION: &str = git_version!();
|
||||
|
@ -32,10 +34,6 @@ const VERSION: &str = concatcp!(PKG_VERSION, "-", GIT_VERSION);
|
|||
struct Cli {
|
||||
#[clap(subcommand)]
|
||||
command: Option<Commands>,
|
||||
#[clap(short, long, help = "show memos in a notification")]
|
||||
notify: bool,
|
||||
#[clap(long, help = "show completed tasks")]
|
||||
old: bool,
|
||||
#[clap(short, long, help = "synchronize memo db")]
|
||||
sync: bool,
|
||||
#[clap(long, help = "name of db file, without extension")]
|
||||
|
@ -55,6 +53,17 @@ enum Commands {
|
|||
body: Vec<String>,
|
||||
#[clap(short, long, help = "due time relative to now")]
|
||||
due: Option<String>, // TODO allow to pass date
|
||||
#[clap(short, long, help = "memo tag, used for categorizing")]
|
||||
tag: Option<String>,
|
||||
},
|
||||
/// list existing memos
|
||||
List {
|
||||
#[clap(short, long, help = "list with given tag as root")]
|
||||
tag: Option<String>,
|
||||
#[clap(short, long, help = "show memos in a notification")]
|
||||
notify: bool,
|
||||
#[clap(long, help = "show completed tasks")]
|
||||
old: bool,
|
||||
},
|
||||
/// mark existing memo as done
|
||||
Done {
|
||||
|
@ -65,6 +74,8 @@ enum Commands {
|
|||
/// change existing memo
|
||||
Edit {
|
||||
search: String,
|
||||
#[clap(short, long, help = "set memo tag")]
|
||||
tag: Option<String>,
|
||||
#[clap(short, long, help = "set memo message")]
|
||||
body: Option<String>,
|
||||
#[clap(short, long, help = "set due time relative to now")]
|
||||
|
@ -110,30 +121,31 @@ fn main() {
|
|||
}
|
||||
|
||||
fn run_commands<T>(mut storage: T, args: Cli) -> Result<(), MemoError>
|
||||
where
|
||||
T: MemoStorage + AuthStorage + StateStorage + RemoteSync,
|
||||
where T: StorageDriver,
|
||||
{
|
||||
if args.sync {
|
||||
if storage.get_key().is_err() {
|
||||
let key = rpassword::prompt_password("[$] new passphrase: ").unwrap();
|
||||
storage.set_key(key.as_str()).unwrap();
|
||||
}
|
||||
let res = storage.download(
|
||||
storage.get_hash().unwrap().as_str(),
|
||||
"http://127.0.0.1:8443",
|
||||
);
|
||||
if res.is_ok() {
|
||||
println!("[v] downloaded remote db");
|
||||
} else {
|
||||
println!(
|
||||
"[!] could not fetch db : {}",
|
||||
res.err().unwrap().to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
// if args.sync {
|
||||
// if storage.get_key().is_err() {
|
||||
// let key = rpassword::prompt_password("[$] new passphrase: ").unwrap();
|
||||
// storage.set_key(key.as_str()).unwrap();
|
||||
// }
|
||||
// let res = storage.download(
|
||||
// storage.get_hash().unwrap().as_str(),
|
||||
// "http://127.0.0.1:8443",
|
||||
// );
|
||||
// if res.is_ok() {
|
||||
// println!("[v] downloaded remote db");
|
||||
// } else {
|
||||
// println!(
|
||||
// "[!] could not fetch db : {}",
|
||||
// res.err().unwrap().to_string()
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
match &args.command {
|
||||
Some(Commands::New { body, due }) => {
|
||||
let ctx = storage.ctx().unwrap();
|
||||
|
||||
match args.command {
|
||||
Some(Commands::New { body, due, tag }) => {
|
||||
let mut due_date: Option<DateTime<Utc>> = None;
|
||||
if let Some(d) = due {
|
||||
if d.len() > 0 {
|
||||
|
@ -142,8 +154,7 @@ where
|
|||
}
|
||||
let txt = body.join(" ");
|
||||
println!("{} new memo: {}", "[+]".bold(), &txt);
|
||||
storage.add(txt, due_date).unwrap();
|
||||
storage.set_edit_time(Utc::now()).unwrap();
|
||||
storage.add(tag.unwrap_or("".to_string()), txt, due_date).unwrap();
|
||||
}
|
||||
Some(Commands::Done { search, many }) => {
|
||||
let rex = Regex::new(search.as_str());
|
||||
|
@ -152,7 +163,7 @@ where
|
|||
if let Some(re) = rex.ok() {
|
||||
for memo in storage.all(false).unwrap() {
|
||||
if re.is_match(memo.body.as_str()) {
|
||||
if *many {
|
||||
if many {
|
||||
storage.del(&memo.id).unwrap();
|
||||
println!("[-] done task : {}", memo.body);
|
||||
} else if found {
|
||||
|
@ -171,20 +182,22 @@ where
|
|||
}
|
||||
if let Some(rm) = to_remove {
|
||||
storage.del(&rm.id).unwrap();
|
||||
storage.set_edit_time(Utc::now()).unwrap();
|
||||
println!("{} done memo: {}", "[-]".bold(), rm.body);
|
||||
}
|
||||
} else {
|
||||
println!("{} invalid regex", format!("[{}]", "!".red()).bold());
|
||||
}
|
||||
}
|
||||
Some(Commands::Edit { search, body, due }) => {
|
||||
Some(Commands::Edit { search, tag, body, due }) => {
|
||||
let mut m = find_by_regex(
|
||||
regex::Regex::new(search.as_str()).unwrap(),
|
||||
storage.all(false).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if let Some(t) = tag {
|
||||
m.tag = t;
|
||||
}
|
||||
if let Some(b) = body {
|
||||
m.body = b.to_owned();
|
||||
}
|
||||
|
@ -195,41 +208,64 @@ where
|
|||
m.due = Some(Utc::now() + parse_human_duration(d.as_str()).unwrap());
|
||||
}
|
||||
}
|
||||
// storage.set(&m).unwrap(); // TODO fix editing memos
|
||||
storage.set_edit_time(Utc::now()).unwrap();
|
||||
let m_str = m.colored();
|
||||
storage.put(m).unwrap();
|
||||
println!(
|
||||
"{} updated memo\n{}",
|
||||
format!("[{}]", ">".green()).bold().to_string(),
|
||||
m.colored(),
|
||||
m_str,
|
||||
);
|
||||
}
|
||||
Some(Commands::List { tag, notify, old }) => {
|
||||
let all = storage.all(old).unwrap();
|
||||
display_memos(all, tag, &ctx, notify);
|
||||
},
|
||||
None => {
|
||||
let all = storage.all(args.old).unwrap();
|
||||
let mut builder = String::new();
|
||||
let timing = if let Some(state) = storage.get_state().ok() {
|
||||
let now = Local::now();
|
||||
format!(
|
||||
"last edit: {}",
|
||||
state
|
||||
.last_edit
|
||||
.with_timezone(&now.timezone())
|
||||
.format("%a %d/%m %H:%M")
|
||||
)
|
||||
} else {
|
||||
Local::now().format("%a %d/%m, %H:%M").to_string()
|
||||
};
|
||||
if args.old {
|
||||
builder.push_str("Archived memos:\n");
|
||||
let all = storage.all(false).unwrap();
|
||||
display_memos(all, None, &ctx, false);
|
||||
}
|
||||
}
|
||||
|
||||
// if args.sync {
|
||||
// let res = storage.upload(
|
||||
// storage.get_hash().unwrap().as_str(),
|
||||
// "http://127.0.0.1:8443",
|
||||
// );
|
||||
// if res.is_ok() {
|
||||
// println!("[^] uploaded local db");
|
||||
// storage.set_sync_time(Utc::now()).unwrap();
|
||||
// } else {
|
||||
// println!(
|
||||
// "[!] could not upload db : {}",
|
||||
// res.err().unwrap().to_string()
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_memos(all: Vec<Memo>, query: Option<String>, ctx: &Context, notify: bool) {
|
||||
// let all = storage.all(args.old).unwrap();
|
||||
let mut builder = String::new();
|
||||
let timing = format!(
|
||||
"last edit: {}",
|
||||
ctx
|
||||
.last_edit
|
||||
.with_timezone(&Local::now().timezone())
|
||||
.format("%a %d/%m %H:%M")
|
||||
);
|
||||
if all.len() < 1 {
|
||||
builder.push_str("[ ] nothing to remember\n");
|
||||
}
|
||||
for m in all {
|
||||
let tmp = if args.notify { m.human() } else { m.colored() };
|
||||
builder.push_str(tmp.as_str());
|
||||
builder.push('\n');
|
||||
|
||||
let tree = make_tree(all);
|
||||
let mut tree_ref = &tree;
|
||||
if let Some(q) = query {
|
||||
tree_ref = traverse(tree_ref, q);
|
||||
}
|
||||
if args.notify {
|
||||
builder.push_str(format_tree(tree_ref, 0, !notify).as_str());
|
||||
if notify {
|
||||
Notification::new()
|
||||
.summary(format!("memo-cli | {}", timing).as_str())
|
||||
.body(builder.as_str())
|
||||
|
@ -240,24 +276,5 @@ where
|
|||
println!("{} | {}", "memo-cli".bold(), timing);
|
||||
print!("{}", builder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if args.sync {
|
||||
let res = storage.upload(
|
||||
storage.get_hash().unwrap().as_str(),
|
||||
"http://127.0.0.1:8443",
|
||||
);
|
||||
if res.is_ok() {
|
||||
println!("[^] uploaded local db");
|
||||
storage.set_sync_time(Utc::now()).unwrap();
|
||||
} else {
|
||||
println!(
|
||||
"[!] could not upload db : {}",
|
||||
res.err().unwrap().to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use uuid::Uuid;
|
|||
#[derive(Clone, Eq, Serialize, Deserialize)]
|
||||
pub struct Memo {
|
||||
pub id: Uuid,
|
||||
pub tag: String,
|
||||
pub body: String,
|
||||
pub due: Option<DateTime<Utc>>,
|
||||
pub done: Option<DateTime<Utc>>,
|
||||
|
@ -13,6 +14,7 @@ pub struct Memo {
|
|||
}
|
||||
|
||||
impl Memo {
|
||||
#[allow(dead_code)] // TODO temporary
|
||||
pub fn new(body: String, due: Option<DateTime<Utc>>) -> Self {
|
||||
Memo { body, due, ..Default::default() }
|
||||
}
|
||||
|
@ -22,6 +24,7 @@ impl Default for Memo {
|
|||
fn default() -> Self {
|
||||
Memo {
|
||||
id: Uuid::new_v4(),
|
||||
tag: "".to_string(),
|
||||
body: "".to_string(),
|
||||
due: None,
|
||||
done: None,
|
||||
|
@ -72,8 +75,9 @@ impl fmt::Display for Memo {
|
|||
}
|
||||
return write!(
|
||||
f,
|
||||
"Memo(id={id}, body={body}, due={due}, done={done}, last_edit={last_edit})",
|
||||
"Memo(id={id}, tag={tag}, body={body}, due={due}, done={done}, last_edit={last_edit})",
|
||||
id = self.id,
|
||||
tag = self.tag,
|
||||
body = self.body,
|
||||
due = due_str,
|
||||
done = done_str,
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct State {
|
||||
pub struct Context {
|
||||
pub last_edit: DateTime<Utc>,
|
||||
pub last_sync: Option<DateTime<Utc>>,
|
||||
pub last_memo: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
impl Default for Context {
|
||||
fn default() -> Self {
|
||||
State {
|
||||
Context {
|
||||
last_edit: Utc::now(),
|
||||
last_sync: None,
|
||||
last_memo: None,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use crate::storage::{MemoStorage, AuthStorage};
|
||||
use crate::model::memo::MemoError;
|
||||
use crate::{model::memo::MemoError, storage::StorageDriver};
|
||||
use std::collections::HashSet;
|
||||
use uuid::Uuid;
|
||||
use std::io::Read;
|
||||
|
@ -9,7 +8,7 @@ use std::io::Read;
|
|||
#[test]
|
||||
fn always_succeeds() { }
|
||||
|
||||
pub trait RemoteSync : AuthStorage+MemoStorage+Sized {
|
||||
pub trait RemoteSync : StorageDriver+Sized {
|
||||
fn serialize(&self) -> Result<Vec<u8>, MemoError>;
|
||||
fn deserialize(data: Vec<u8>) -> Result<Self, MemoError>;
|
||||
|
||||
|
@ -63,7 +62,7 @@ pub trait RemoteSync : AuthStorage+MemoStorage+Sized {
|
|||
todo!()
|
||||
}
|
||||
} else {
|
||||
self.insert(memo.clone())?;
|
||||
// self.insert(memo.clone())?;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@ use std::io::BufReader;
|
|||
use std::path::PathBuf;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use uuid::Uuid;
|
||||
use std::io::Write;
|
||||
use sha2::{Digest, Sha512};
|
||||
// use sha2::{Digest, Sha512};
|
||||
use std::collections::LinkedList;
|
||||
|
||||
use crate::storage::{Memo, State, MemoError, AuthStorage, MemoStorage, StateStorage, MemoSetField};
|
||||
use crate::remote::RemoteSync;
|
||||
// use crate::remote::RemoteSync;
|
||||
use crate::storage::StorageDriver;
|
||||
use crate::model::{memo::{Memo, MemoError}, state::Context};
|
||||
|
||||
pub struct JsonStorage {
|
||||
path: PathBuf,
|
||||
|
@ -60,30 +60,30 @@ impl JsonStorage {
|
|||
}
|
||||
}
|
||||
|
||||
impl AuthStorage for JsonStorage {
|
||||
fn get_hash(&self) -> Result<String, MemoError> {
|
||||
return Ok(self.data.hash.to_string());
|
||||
}
|
||||
// impl AuthStorage for JsonStorage {
|
||||
// fn get_hash(&self) -> Result<String, MemoError> {
|
||||
// return Ok(self.data.hash.to_string());
|
||||
// }
|
||||
//
|
||||
// fn get_key(&self) -> Result<String, MemoError> {
|
||||
// return Ok(self.data.key.to_string());
|
||||
// }
|
||||
//
|
||||
// fn set_key(&mut self, key: &str) -> Result<Option<String>, MemoError> {
|
||||
// let old_key = Some(self.data.key.to_string());
|
||||
// let mut hasher = Sha512::new();
|
||||
// hasher.update(key.as_bytes());
|
||||
// self.data.hash = base64::encode(hasher.finalize());
|
||||
// self.data.key = key.to_string();
|
||||
// self.save()?;
|
||||
// return Ok(old_key);
|
||||
// }
|
||||
// }
|
||||
|
||||
fn get_key(&self) -> Result<String, MemoError> {
|
||||
return Ok(self.data.key.to_string());
|
||||
}
|
||||
|
||||
fn set_key(&mut self, key: &str) -> Result<Option<String>, MemoError> {
|
||||
let old_key = Some(self.data.key.to_string());
|
||||
let mut hasher = Sha512::new();
|
||||
hasher.update(key.as_bytes());
|
||||
self.data.hash = base64::encode(hasher.finalize());
|
||||
self.data.key = key.to_string();
|
||||
self.save()?;
|
||||
return Ok(old_key);
|
||||
}
|
||||
}
|
||||
|
||||
impl StateStorage for JsonStorage {
|
||||
fn get_state(&self) -> Result<State, MemoError> {
|
||||
impl StorageDriver for JsonStorage {
|
||||
fn ctx(&self) -> Result<Context, MemoError> {
|
||||
return Ok(
|
||||
State {
|
||||
Context {
|
||||
last_edit: self.data.last_edit,
|
||||
last_sync: self.data.last_sync,
|
||||
last_memo: None, // TODO
|
||||
|
@ -91,16 +91,6 @@ impl StateStorage for JsonStorage {
|
|||
);
|
||||
}
|
||||
|
||||
fn set_state(&mut self, state: State) -> Result<Option<State>, MemoError> {
|
||||
let old_state = Some(State{last_edit: self.data.last_edit, last_sync:self.data.last_sync, last_memo:None}); // TODO
|
||||
self.data.last_sync = state.last_sync;
|
||||
self.data.last_edit = state.last_edit;
|
||||
self.save()?;
|
||||
return Ok(old_state);
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoStorage for JsonStorage {
|
||||
fn all(&self, done: bool) -> Result<Vec<Memo>, MemoError> {
|
||||
let mut results_due : LinkedList<Memo> = LinkedList::new();
|
||||
let mut results_not_due : LinkedList<Memo> = LinkedList::new();
|
||||
|
@ -124,44 +114,14 @@ impl MemoStorage for JsonStorage {
|
|||
return Ok(out);
|
||||
}
|
||||
|
||||
fn insert(&mut self, m: Memo) -> Result<(), MemoError> {
|
||||
fn put(&mut self, m: Memo) -> Result<(), MemoError> {
|
||||
self.data.memo.retain(|x| x.id != m.id);
|
||||
self.data.memo.push(m);
|
||||
self.data.last_edit = Utc::now();
|
||||
self.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set(&mut self, id: &Uuid, field: MemoSetField) -> Result<usize, MemoError> {
|
||||
todo!()
|
||||
// let mut count = 0;
|
||||
// for (_i, memo) in self.data.memo.iter().enumerate() {
|
||||
// if memo.id == m.id {
|
||||
// // TODO improve
|
||||
// // self.data.memo[i].body = m.body;
|
||||
// // self.data.memo[i].due = m.due;
|
||||
// // self.data.memo[i].done = m.done;
|
||||
// count += 1;
|
||||
// }
|
||||
// }
|
||||
// self.save()?;
|
||||
// Ok(count)
|
||||
}
|
||||
|
||||
fn del(&mut self, id: &uuid::Uuid) -> Result<usize, MemoError> {
|
||||
let mut count = 0;
|
||||
let mut index : i32 = -1;
|
||||
for (i, memo) in self.data.memo.iter().enumerate() {
|
||||
if memo.id == *id {
|
||||
index = i as i32;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
if index >= 0 {
|
||||
self.data.memo[index as usize].done = Some(Utc::now());
|
||||
self.save()?;
|
||||
}
|
||||
return Ok(count);
|
||||
}
|
||||
|
||||
fn get(&self, id: &uuid::Uuid) -> Result<Memo, MemoError> {
|
||||
for memo in &self.data.memo {
|
||||
if memo.id == *id {
|
||||
|
@ -173,17 +133,17 @@ impl MemoStorage for JsonStorage {
|
|||
}
|
||||
|
||||
|
||||
impl RemoteSync for JsonStorage {
|
||||
fn serialize(&self) -> Result<Vec<u8>, MemoError> {
|
||||
self.save()?;
|
||||
return Ok(std::fs::read(&self.path).unwrap());
|
||||
}
|
||||
|
||||
fn deserialize(data: Vec<u8>) -> Result<Self, MemoError> {
|
||||
return Ok(JsonStorage{
|
||||
path: PathBuf::new(),
|
||||
data: serde_json::from_slice(data.as_slice()).unwrap(),
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
// impl RemoteSync for JsonStorage {
|
||||
// fn serialize(&self) -> Result<Vec<u8>, MemoError> {
|
||||
// self.save()?;
|
||||
// return Ok(std::fs::read(&self.path).unwrap());
|
||||
// }
|
||||
//
|
||||
// fn deserialize(data: Vec<u8>) -> Result<Self, MemoError> {
|
||||
// return Ok(JsonStorage{
|
||||
// path: PathBuf::new(),
|
||||
// data: serde_json::from_slice(data.as_slice()).unwrap(),
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
use crate::model::{memo::{Memo, MemoError}, state::State};
|
||||
use crate::model::{memo::{Memo, MemoError}, state::Context};
|
||||
|
||||
pub mod json;
|
||||
pub mod sqlite;
|
||||
|
@ -10,46 +10,56 @@ pub use sqlite::SQLiteStorage;
|
|||
|
||||
pub const SUPPORTED_FORMATS: &'static [&'static str] = &["db", "json"];
|
||||
|
||||
#[allow(dead_code)] // TODO temp
|
||||
pub enum MemoSetField {
|
||||
Body(String),
|
||||
Due(Option<DateTime<Utc>>),
|
||||
Done(Option<DateTime<Utc>>),
|
||||
}
|
||||
|
||||
pub trait MemoStorage {
|
||||
fn all(&self, done: bool) -> Result<Vec<Memo>, MemoError>;
|
||||
pub trait StorageDriver {
|
||||
|
||||
/// Insert a new element into storage, replacing any previous one. updates last_edit in ctx
|
||||
fn put(&mut self, memo: Memo) -> Result<(), MemoError>;
|
||||
|
||||
/// Get memo from storage with matching id
|
||||
fn get(&self, id: &Uuid) -> Result<Memo, MemoError>;
|
||||
fn del(&mut self, id: &Uuid) -> Result<usize, MemoError>;
|
||||
fn set(&mut self, id: &Uuid, field: MemoSetField) -> Result<usize, MemoError>;
|
||||
fn insert(&mut self, m: Memo) -> Result<(), MemoError>;
|
||||
|
||||
fn add(&mut self, body: String, due: Option<DateTime<Utc>>) -> Result<(), MemoError> {
|
||||
self.insert(Memo::new(body, due))
|
||||
}
|
||||
}
|
||||
/// Return a vector with clones of all elements in storage (optionally include concluded ones)
|
||||
fn all(&self, done:bool) -> Result<Vec<Memo>, MemoError>;
|
||||
|
||||
pub trait AuthStorage {
|
||||
fn get_hash(&self) -> Result<String, MemoError>;
|
||||
fn get_key(&self) -> Result<String, MemoError>;
|
||||
fn set_key(&mut self, key: &str) -> Result<Option<String>, MemoError>;
|
||||
}
|
||||
/// Return current storage context
|
||||
fn ctx(&self) -> Result<Context, MemoError>;
|
||||
|
||||
pub trait StateStorage {
|
||||
fn set_state(&mut self, state: State) -> Result<Option<State>, MemoError>;
|
||||
fn get_state(&self) -> Result<State, MemoError>;
|
||||
fn update(&mut self, id: &Uuid, field: MemoSetField) -> Result<Memo, MemoError> {
|
||||
let mut memo = self.get(id)?;
|
||||
|
||||
fn set_edit_time(&mut self, time: DateTime<Utc>) -> Result<(), MemoError> {
|
||||
let mut state = self.get_state()
|
||||
.unwrap_or(State { last_edit: time, ..Default::default() });
|
||||
state.last_edit = time;
|
||||
self.set_state(state)?;
|
||||
Ok(())
|
||||
match field {
|
||||
MemoSetField::Body(body) => memo.body = body,
|
||||
MemoSetField::Due(due) => memo.due = due,
|
||||
MemoSetField::Done(done) => memo.done = done,
|
||||
}
|
||||
|
||||
fn set_sync_time(&mut self, time: DateTime<Utc>) -> Result<(), MemoError> {
|
||||
let mut state = self.get_state()?;
|
||||
state.last_sync = Some(time);
|
||||
self.set_state(state)?;
|
||||
Ok(())
|
||||
self.put(memo.clone())?;
|
||||
|
||||
Ok(memo)
|
||||
}
|
||||
|
||||
fn add(&mut self, tag: String, body: String, due: Option<DateTime<Utc>>) -> Result<Memo, MemoError> {
|
||||
let m = Memo {
|
||||
id: Uuid::new_v4(),
|
||||
tag, body, due,
|
||||
last_edit: Utc::now(),
|
||||
done: None,
|
||||
};
|
||||
self.put(m.clone())?;
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
fn del(&mut self, id: &Uuid) -> Result<Memo, MemoError> {
|
||||
let mut m = self.get(id)?;
|
||||
m.done = Some(Utc::now());
|
||||
self.put(m.clone())?;
|
||||
return Ok(m);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
use crate::remote::RemoteSync;
|
||||
use crate::storage::{AuthStorage, Memo, MemoError, MemoStorage, State, StateStorage};
|
||||
// use crate::remote::RemoteSync;
|
||||
use crate::model::{memo::{Memo, MemoError}, state::Context};
|
||||
use crate::storage::StorageDriver;
|
||||
use chrono::{DateTime, Utc};
|
||||
// use rusqlite::DatabaseName::Main;
|
||||
use rusqlite::{params, Connection};
|
||||
use sha2::{Digest, Sha512};
|
||||
// use sha2::{Digest, Sha512};
|
||||
use std::path::PathBuf;
|
||||
use std::collections::LinkedList;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use uuid::Uuid;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use super::MemoSetField;
|
||||
|
||||
pub struct SQLiteStorage {
|
||||
conn: Connection,
|
||||
path: PathBuf,
|
||||
// path: PathBuf,
|
||||
|
||||
#[allow(dead_code)] // I just use this to bind the tempfile life with the SQLiteStorage obj
|
||||
tmpfile: Option<NamedTempFile>,
|
||||
|
@ -30,6 +27,7 @@ impl SQLiteStorage {
|
|||
connection.execute(
|
||||
"CREATE TABLE IF NOT EXISTS memo (
|
||||
id TEXT PRIMARY KEY,
|
||||
tag TEXT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
due DATETIME,
|
||||
done DATETIME DEFAULT NULL,
|
||||
|
@ -41,53 +39,55 @@ impl SQLiteStorage {
|
|||
"CREATE TABLE IF NOT EXISTS state (
|
||||
last_edit DATETIME DEFAULT NULL,
|
||||
last_sync DATETIME DEFAULT NULL
|
||||
last_memo TEXT DEFAULT NULL
|
||||
);",
|
||||
[],
|
||||
)?;
|
||||
connection.execute(
|
||||
"CREATE TABLE IF NOT EXISTS auth (
|
||||
key TEXT PRIMARY KEY,
|
||||
hash TEXT NOT NULL
|
||||
);",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// connection.execute(
|
||||
// "CREATE TABLE IF NOT EXISTS auth (
|
||||
// key TEXT PRIMARY KEY,
|
||||
// hash TEXT NOT NULL
|
||||
// );",
|
||||
// [],
|
||||
// )?;
|
||||
}
|
||||
|
||||
return Ok(SQLiteStorage { conn: connection , path, tmpfile: None });
|
||||
return Ok(SQLiteStorage { conn: connection , tmpfile: None });
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthStorage for SQLiteStorage {
|
||||
fn get_hash(&self) -> Result<String, MemoError> {
|
||||
return Ok(self.conn.query_row("SELECT * FROM auth", [], |row| {
|
||||
return Ok(row.get(1)?);
|
||||
})?);
|
||||
}
|
||||
// impl AuthStorage for SQLiteStorage {
|
||||
// fn get_hash(&self) -> Result<String, MemoError> {
|
||||
// return Ok(self.conn.query_row("SELECT * FROM auth", [], |row| {
|
||||
// return Ok(row.get(1)?);
|
||||
// })?);
|
||||
// }
|
||||
//
|
||||
// fn get_key(&self) -> Result<String, MemoError> {
|
||||
// return Ok(self.conn.query_row("SELECT * FROM auth", [], |row| {
|
||||
// return Ok(row.get(0)?);
|
||||
// })?);
|
||||
// }
|
||||
//
|
||||
// fn set_key(&mut self, key: &str) -> Result<Option<String>, MemoError> { // TODO move the hashing itself to a default method
|
||||
// let old_key = self.get_key().ok();
|
||||
// let mut hasher = Sha512::new();
|
||||
// hasher.update(key.as_bytes());
|
||||
// let hash = base64::encode(hasher.finalize());
|
||||
// self.conn.execute("DELETE FROM auth;", [])?;
|
||||
// self.conn.execute(
|
||||
// "INSERT INTO auth (key, hash) VALUES (?, ?);",
|
||||
// params![key, hash],
|
||||
// )?;
|
||||
// return Ok(old_key);
|
||||
// }
|
||||
// }
|
||||
|
||||
fn get_key(&self) -> Result<String, MemoError> {
|
||||
return Ok(self.conn.query_row("SELECT * FROM auth", [], |row| {
|
||||
return Ok(row.get(0)?);
|
||||
})?);
|
||||
}
|
||||
|
||||
fn set_key(&mut self, key: &str) -> Result<Option<String>, MemoError> { // TODO move the hashing itself to a default method
|
||||
let old_key = self.get_key().ok();
|
||||
let mut hasher = Sha512::new();
|
||||
hasher.update(key.as_bytes());
|
||||
let hash = base64::encode(hasher.finalize());
|
||||
self.conn.execute("DELETE FROM auth;", [])?;
|
||||
self.conn.execute(
|
||||
"INSERT INTO auth (key, hash) VALUES (?, ?);",
|
||||
params![key, hash],
|
||||
)?;
|
||||
return Ok(old_key);
|
||||
}
|
||||
}
|
||||
|
||||
impl StateStorage for SQLiteStorage {
|
||||
fn get_state(&self) -> Result<State, MemoError> {
|
||||
impl StorageDriver for SQLiteStorage {
|
||||
fn ctx(&self) -> Result<Context, MemoError> {
|
||||
return Ok(self.conn.query_row("SELECT * FROM state", [], |row| {
|
||||
return Ok(State {
|
||||
return Ok(Context {
|
||||
last_edit: row.get(0)?,
|
||||
last_sync: row.get(1).ok(),
|
||||
..Default::default()
|
||||
|
@ -95,18 +95,6 @@ impl StateStorage for SQLiteStorage {
|
|||
})?);
|
||||
}
|
||||
|
||||
fn set_state(&mut self, state: State) -> Result<Option<State>, MemoError> {
|
||||
let old_state = self.get_state().ok();
|
||||
self.conn.execute("DELETE FROM state;", [])?;
|
||||
self.conn.execute(
|
||||
"INSERT INTO state (last_edit, last_sync) VALUES (?, ?);",
|
||||
params![state.last_edit, state.last_sync],
|
||||
)?;
|
||||
return Ok(old_state);
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoStorage for SQLiteStorage {
|
||||
fn all(&self, done: bool) -> Result<Vec<Memo>, MemoError> {
|
||||
let mut results_due : LinkedList<Memo> = LinkedList::new();
|
||||
let mut results_not_due : LinkedList<Memo> = LinkedList::new();
|
||||
|
@ -126,10 +114,11 @@ impl MemoStorage for SQLiteStorage {
|
|||
let tgt = if row.get::<usize, DateTime<Utc>>(2).is_ok() { &mut results_due } else { &mut results_not_due };
|
||||
tgt.push_back(Memo {
|
||||
id: Uuid::parse_str(row.get::<usize, String>(0)?.as_str()).unwrap(),
|
||||
body: row.get(1)?,
|
||||
due: row.get(2).ok(),
|
||||
done: row.get(3).ok(),
|
||||
last_edit: row.get(4)?,
|
||||
tag: row.get(1)?,
|
||||
body: row.get(2)?,
|
||||
due: row.get(3).ok(),
|
||||
done: row.get(4).ok(),
|
||||
last_edit: row.get(5)?,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -138,55 +127,37 @@ impl MemoStorage for SQLiteStorage {
|
|||
return Ok(results_due.into_iter().collect());
|
||||
}
|
||||
|
||||
fn insert(&mut self, m: Memo) -> Result<(), MemoError> {
|
||||
self.conn.execute(
|
||||
fn put(&mut self, m: Memo) -> Result<(), MemoError> {
|
||||
let tx = self.conn.transaction()?;
|
||||
tx.execute("DELETE FROM memo WHERE id = ?", params![m.id.to_string()])?;
|
||||
tx.execute(
|
||||
"INSERT INTO memo (id, body, due, done, last_edit) VALUES (?, ?, ?, ?, ?)",
|
||||
params![m.id.to_string(), m.body, m.due, m.done, m.last_edit],
|
||||
)?;
|
||||
tx.execute("UPDATE state SET last_edit = ?", params![Utc::now()])?;
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set(&mut self, id: &Uuid, field: MemoSetField) -> Result<usize, MemoError> {
|
||||
match field {
|
||||
MemoSetField::Body(body) => {
|
||||
Ok(self.conn.execute(
|
||||
"UPDATE memo SET body = ?, last_edit = ? WHERE id = ?",
|
||||
params![body, Utc::now(), id.to_string()],
|
||||
)?)
|
||||
},
|
||||
MemoSetField::Due(due) => {
|
||||
Ok(self.conn.execute(
|
||||
"UPDATE memo SET due = ?, last_edit = ? WHERE id = ?",
|
||||
params![due, Utc::now(), id.to_string()]
|
||||
)?)
|
||||
},
|
||||
MemoSetField::Done(done) => {
|
||||
Ok(self.conn.execute(
|
||||
"UPDATE memo SET done = ?, last_edit = ? WHERE id = ?",
|
||||
params![done, Utc::now(), id.to_string()]
|
||||
)?)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn del(&mut self, id: &Uuid) -> Result<usize, MemoError> {
|
||||
return Ok(self.conn.execute(
|
||||
"UPDATE memo SET done = ? WHERE id = ?",
|
||||
params![Utc::now(), id.to_string()],
|
||||
)?);
|
||||
}
|
||||
// fn del(&mut self, id: &Uuid) -> Result<usize, MemoError> {
|
||||
// return Ok(self.conn.execute(
|
||||
// "UPDATE memo SET done = ? WHERE id = ?",
|
||||
// params![Utc::now(), id.to_string()],
|
||||
// )?);
|
||||
// }
|
||||
|
||||
fn get(&self, id: &Uuid) -> Result<Memo, MemoError> {
|
||||
return Ok(self.conn.query_row(
|
||||
"SELECT * FROM memo WHERE id = ? AND done = 0",
|
||||
"SELECT * FROM memo WHERE id = ?",
|
||||
params![id.to_string()],
|
||||
|row| {
|
||||
return Ok(Memo {
|
||||
id: Uuid::parse_str(row.get::<usize, String>(0)?.as_str()).unwrap(),
|
||||
body: row.get(1)?,
|
||||
due: row.get(2).ok(),
|
||||
done: row.get(3).ok(),
|
||||
last_edit: row.get(4)?,
|
||||
tag: row.get(1)?,
|
||||
body: row.get(2)?,
|
||||
due: row.get(3).ok(),
|
||||
done: row.get(4).ok(),
|
||||
last_edit: row.get(5)?,
|
||||
});
|
||||
},
|
||||
)?);
|
||||
|
@ -194,23 +165,23 @@ impl MemoStorage for SQLiteStorage {
|
|||
}
|
||||
|
||||
|
||||
impl RemoteSync for SQLiteStorage {
|
||||
fn serialize(&self) -> Result<Vec<u8>, MemoError> {
|
||||
return Ok(fs::read(&self.path).unwrap()); // TODO can I do this? just read the db while it's open
|
||||
// let tmpfile = NamedTempFile::new()?;
|
||||
// self.conn.backup(Main, tmpfile.path(), None).unwrap();
|
||||
// return Ok(fs::read(tmpfile).unwrap());
|
||||
}
|
||||
|
||||
fn deserialize(data: Vec<u8>) -> Result<Self, MemoError> {
|
||||
let mut tmpfile = NamedTempFile::new()?;
|
||||
tmpfile.write(data.as_slice())?;
|
||||
let path = tmpfile.path();
|
||||
return Ok(SQLiteStorage{
|
||||
conn: Connection::open(path).unwrap(),
|
||||
path: path.to_path_buf(),
|
||||
tmpfile: Some(tmpfile),
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
// impl RemoteSync for SQLiteStorage {
|
||||
// fn serialize(&self) -> Result<Vec<u8>, MemoError> {
|
||||
// return Ok(fs::read(&self.path).unwrap()); // TODO can I do this? just read the db while it's open
|
||||
// // let tmpfile = NamedTempFile::new()?;
|
||||
// // self.conn.backup(Main, tmpfile.path(), None).unwrap();
|
||||
// // return Ok(fs::read(tmpfile).unwrap());
|
||||
// }
|
||||
//
|
||||
// fn deserialize(data: Vec<u8>) -> Result<Self, MemoError> {
|
||||
// let mut tmpfile = NamedTempFile::new()?;
|
||||
// tmpfile.write(data.as_slice())?;
|
||||
// let path = tmpfile.path();
|
||||
// return Ok(SQLiteStorage{
|
||||
// conn: Connection::open(path).unwrap(),
|
||||
// path: path.to_path_buf(),
|
||||
// tmpfile: Some(tmpfile),
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
|
Loading…
Reference in a new issue