feat: refactor storage, add tag, add tree view

This commit is contained in:
əlemi 2022-12-15 02:23:43 +01:00
parent 408cb9fda1
commit cfe29f0c12
No known key found for this signature in database
GPG key ID: BBCBFE5D7244634E
8 changed files with 399 additions and 326 deletions

112
src/categorize.rs Normal file
View 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;
}
}
}

View file

@ -2,7 +2,9 @@ mod remote;
mod storage; mod storage;
mod utils; mod utils;
mod model; mod model;
mod categorize;
use categorize::{make_tree, format_tree, traverse};
use chrono::{DateTime, Local, Utc}; use chrono::{DateTime, Local, Utc};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use colored::Colorize; use colored::Colorize;
@ -10,13 +12,13 @@ use const_format::concatcp;
use git_version::git_version; use git_version::git_version;
use notify_rust::Notification; use notify_rust::Notification;
use regex::Regex; use regex::Regex;
use remote::RemoteSync; // use remote::RemoteSync;
use std::path::PathBuf; use std::path::PathBuf;
use storage::{ use storage::{
AuthStorage, JsonStorage, MemoStorage, SQLiteStorage, StateStorage, JsonStorage, SQLiteStorage,
SUPPORTED_FORMATS, 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}; use utils::{find_by_regex, find_db_file, parse_human_duration, HumanDisplay};
const GIT_VERSION: &str = git_version!(); const GIT_VERSION: &str = git_version!();
@ -32,10 +34,6 @@ const VERSION: &str = concatcp!(PKG_VERSION, "-", GIT_VERSION);
struct Cli { struct Cli {
#[clap(subcommand)] #[clap(subcommand)]
command: Option<Commands>, 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")] #[clap(short, long, help = "synchronize memo db")]
sync: bool, sync: bool,
#[clap(long, help = "name of db file, without extension")] #[clap(long, help = "name of db file, without extension")]
@ -55,6 +53,17 @@ enum Commands {
body: Vec<String>, body: Vec<String>,
#[clap(short, long, help = "due time relative to now")] #[clap(short, long, help = "due time relative to now")]
due: Option<String>, // TODO allow to pass date 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 /// mark existing memo as done
Done { Done {
@ -65,6 +74,8 @@ enum Commands {
/// change existing memo /// change existing memo
Edit { Edit {
search: String, search: String,
#[clap(short, long, help = "set memo tag")]
tag: Option<String>,
#[clap(short, long, help = "set memo message")] #[clap(short, long, help = "set memo message")]
body: Option<String>, body: Option<String>,
#[clap(short, long, help = "set due time relative to now")] #[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> fn run_commands<T>(mut storage: T, args: Cli) -> Result<(), MemoError>
where where T: StorageDriver,
T: MemoStorage + AuthStorage + StateStorage + RemoteSync,
{ {
if args.sync { // if args.sync {
if storage.get_key().is_err() { // if storage.get_key().is_err() {
let key = rpassword::prompt_password("[$] new passphrase: ").unwrap(); // let key = rpassword::prompt_password("[$] new passphrase: ").unwrap();
storage.set_key(key.as_str()).unwrap(); // storage.set_key(key.as_str()).unwrap();
} // }
let res = storage.download( // let res = storage.download(
storage.get_hash().unwrap().as_str(), // storage.get_hash().unwrap().as_str(),
"http://127.0.0.1:8443", // "http://127.0.0.1:8443",
); // );
if res.is_ok() { // if res.is_ok() {
println!("[v] downloaded remote db"); // println!("[v] downloaded remote db");
} else { // } else {
println!( // println!(
"[!] could not fetch db : {}", // "[!] could not fetch db : {}",
res.err().unwrap().to_string() // res.err().unwrap().to_string()
); // );
} // }
} // }
match &args.command { let ctx = storage.ctx().unwrap();
Some(Commands::New { body, due }) => {
match args.command {
Some(Commands::New { body, due, tag }) => {
let mut due_date: Option<DateTime<Utc>> = None; let mut due_date: Option<DateTime<Utc>> = None;
if let Some(d) = due { if let Some(d) = due {
if d.len() > 0 { if d.len() > 0 {
@ -142,8 +154,7 @@ where
} }
let txt = body.join(" "); let txt = body.join(" ");
println!("{} new memo: {}", "[+]".bold(), &txt); println!("{} new memo: {}", "[+]".bold(), &txt);
storage.add(txt, due_date).unwrap(); storage.add(tag.unwrap_or("".to_string()), txt, due_date).unwrap();
storage.set_edit_time(Utc::now()).unwrap();
} }
Some(Commands::Done { search, many }) => { Some(Commands::Done { search, many }) => {
let rex = Regex::new(search.as_str()); let rex = Regex::new(search.as_str());
@ -152,7 +163,7 @@ where
if let Some(re) = rex.ok() { if let Some(re) = rex.ok() {
for memo in storage.all(false).unwrap() { for memo in storage.all(false).unwrap() {
if re.is_match(memo.body.as_str()) { if re.is_match(memo.body.as_str()) {
if *many { if many {
storage.del(&memo.id).unwrap(); storage.del(&memo.id).unwrap();
println!("[-] done task : {}", memo.body); println!("[-] done task : {}", memo.body);
} else if found { } else if found {
@ -171,20 +182,22 @@ where
} }
if let Some(rm) = to_remove { if let Some(rm) = to_remove {
storage.del(&rm.id).unwrap(); storage.del(&rm.id).unwrap();
storage.set_edit_time(Utc::now()).unwrap();
println!("{} done memo: {}", "[-]".bold(), rm.body); println!("{} done memo: {}", "[-]".bold(), rm.body);
} }
} else { } else {
println!("{} invalid regex", format!("[{}]", "!".red()).bold()); 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( let mut m = find_by_regex(
regex::Regex::new(search.as_str()).unwrap(), regex::Regex::new(search.as_str()).unwrap(),
storage.all(false).unwrap(), storage.all(false).unwrap(),
) )
.unwrap(); .unwrap();
if let Some(t) = tag {
m.tag = t;
}
if let Some(b) = body { if let Some(b) = body {
m.body = b.to_owned(); m.body = b.to_owned();
} }
@ -195,41 +208,64 @@ where
m.due = Some(Utc::now() + parse_human_duration(d.as_str()).unwrap()); m.due = Some(Utc::now() + parse_human_duration(d.as_str()).unwrap());
} }
} }
// storage.set(&m).unwrap(); // TODO fix editing memos let m_str = m.colored();
storage.set_edit_time(Utc::now()).unwrap(); storage.put(m).unwrap();
println!( println!(
"{} updated memo\n{}", "{} updated memo\n{}",
format!("[{}]", ">".green()).bold().to_string(), 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 => { None => {
let all = storage.all(args.old).unwrap(); let all = storage.all(false).unwrap();
let mut builder = String::new(); display_memos(all, None, &ctx, false);
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");
} }
}
// 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 { if all.len() < 1 {
builder.push_str("[ ] nothing to remember\n"); builder.push_str("[ ] nothing to remember\n");
} }
for m in all {
let tmp = if args.notify { m.human() } else { m.colored() }; let tree = make_tree(all);
builder.push_str(tmp.as_str()); let mut tree_ref = &tree;
builder.push('\n'); 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() Notification::new()
.summary(format!("memo-cli | {}", timing).as_str()) .summary(format!("memo-cli | {}", timing).as_str())
.body(builder.as_str()) .body(builder.as_str())
@ -240,24 +276,5 @@ where
println!("{} | {}", "memo-cli".bold(), timing); println!("{} | {}", "memo-cli".bold(), timing);
print!("{}", builder); 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(())
} }

View file

@ -6,6 +6,7 @@ use uuid::Uuid;
#[derive(Clone, Eq, Serialize, Deserialize)] #[derive(Clone, Eq, Serialize, Deserialize)]
pub struct Memo { pub struct Memo {
pub id: Uuid, pub id: Uuid,
pub tag: String,
pub body: String, pub body: String,
pub due: Option<DateTime<Utc>>, pub due: Option<DateTime<Utc>>,
pub done: Option<DateTime<Utc>>, pub done: Option<DateTime<Utc>>,
@ -13,6 +14,7 @@ pub struct Memo {
} }
impl Memo { impl Memo {
#[allow(dead_code)] // TODO temporary
pub fn new(body: String, due: Option<DateTime<Utc>>) -> Self { pub fn new(body: String, due: Option<DateTime<Utc>>) -> Self {
Memo { body, due, ..Default::default() } Memo { body, due, ..Default::default() }
} }
@ -22,6 +24,7 @@ impl Default for Memo {
fn default() -> Self { fn default() -> Self {
Memo { Memo {
id: Uuid::new_v4(), id: Uuid::new_v4(),
tag: "".to_string(),
body: "".to_string(), body: "".to_string(),
due: None, due: None,
done: None, done: None,
@ -72,8 +75,9 @@ impl fmt::Display for Memo {
} }
return write!( return write!(
f, 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, id = self.id,
tag = self.tag,
body = self.body, body = self.body,
due = due_str, due = due_str,
done = done_str, done = done_str,

View file

@ -1,15 +1,15 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use uuid::Uuid; use uuid::Uuid;
pub struct State { pub struct Context {
pub last_edit: DateTime<Utc>, pub last_edit: DateTime<Utc>,
pub last_sync: Option<DateTime<Utc>>, pub last_sync: Option<DateTime<Utc>>,
pub last_memo: Option<Uuid>, pub last_memo: Option<Uuid>,
} }
impl Default for State { impl Default for Context {
fn default() -> Self { fn default() -> Self {
State { Context {
last_edit: Utc::now(), last_edit: Utc::now(),
last_sync: None, last_sync: None,
last_memo: None, last_memo: None,

View file

@ -1,5 +1,4 @@
use crate::storage::{MemoStorage, AuthStorage}; use crate::{model::memo::MemoError, storage::StorageDriver};
use crate::model::memo::MemoError;
use std::collections::HashSet; use std::collections::HashSet;
use uuid::Uuid; use uuid::Uuid;
use std::io::Read; use std::io::Read;
@ -9,7 +8,7 @@ use std::io::Read;
#[test] #[test]
fn always_succeeds() { } fn always_succeeds() { }
pub trait RemoteSync : AuthStorage+MemoStorage+Sized { pub trait RemoteSync : StorageDriver+Sized {
fn serialize(&self) -> Result<Vec<u8>, MemoError>; fn serialize(&self) -> Result<Vec<u8>, MemoError>;
fn deserialize(data: Vec<u8>) -> Result<Self, MemoError>; fn deserialize(data: Vec<u8>) -> Result<Self, MemoError>;
@ -63,7 +62,7 @@ pub trait RemoteSync : AuthStorage+MemoStorage+Sized {
todo!() todo!()
} }
} else { } else {
self.insert(memo.clone())?; // self.insert(memo.clone())?;
count += 1; count += 1;
} }
} }

View file

@ -3,13 +3,13 @@ use std::io::BufReader;
use std::path::PathBuf; use std::path::PathBuf;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use uuid::Uuid;
use std::io::Write; use std::io::Write;
use sha2::{Digest, Sha512}; // use sha2::{Digest, Sha512};
use std::collections::LinkedList; 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 { pub struct JsonStorage {
path: PathBuf, path: PathBuf,
@ -60,30 +60,30 @@ impl JsonStorage {
} }
} }
impl AuthStorage for JsonStorage { // impl AuthStorage for JsonStorage {
fn get_hash(&self) -> Result<String, MemoError> { // fn get_hash(&self) -> Result<String, MemoError> {
return Ok(self.data.hash.to_string()); // 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> { impl StorageDriver for JsonStorage {
return Ok(self.data.key.to_string()); fn ctx(&self) -> Result<Context, MemoError> {
}
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> {
return Ok( return Ok(
State { Context {
last_edit: self.data.last_edit, last_edit: self.data.last_edit,
last_sync: self.data.last_sync, last_sync: self.data.last_sync,
last_memo: None, // TODO 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> { fn all(&self, done: bool) -> Result<Vec<Memo>, MemoError> {
let mut results_due : LinkedList<Memo> = LinkedList::new(); let mut results_due : LinkedList<Memo> = LinkedList::new();
let mut results_not_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); 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.memo.push(m);
self.data.last_edit = Utc::now();
self.save()?; self.save()?;
Ok(()) 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> { fn get(&self, id: &uuid::Uuid) -> Result<Memo, MemoError> {
for memo in &self.data.memo { for memo in &self.data.memo {
if memo.id == *id { if memo.id == *id {
@ -173,17 +133,17 @@ impl MemoStorage for JsonStorage {
} }
impl RemoteSync for JsonStorage { // impl RemoteSync for JsonStorage {
fn serialize(&self) -> Result<Vec<u8>, MemoError> { // fn serialize(&self) -> Result<Vec<u8>, MemoError> {
self.save()?; // self.save()?;
return Ok(std::fs::read(&self.path).unwrap()); // return Ok(std::fs::read(&self.path).unwrap());
} // }
//
fn deserialize(data: Vec<u8>) -> Result<Self, MemoError> { // fn deserialize(data: Vec<u8>) -> Result<Self, MemoError> {
return Ok(JsonStorage{ // return Ok(JsonStorage{
path: PathBuf::new(), // path: PathBuf::new(),
data: serde_json::from_slice(data.as_slice()).unwrap(), // data: serde_json::from_slice(data.as_slice()).unwrap(),
}); // });
} // }
//
} // }

View file

@ -1,6 +1,6 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use uuid::Uuid; use uuid::Uuid;
use crate::model::{memo::{Memo, MemoError}, state::State}; use crate::model::{memo::{Memo, MemoError}, state::Context};
pub mod json; pub mod json;
pub mod sqlite; pub mod sqlite;
@ -10,46 +10,56 @@ pub use sqlite::SQLiteStorage;
pub const SUPPORTED_FORMATS: &'static [&'static str] = &["db", "json"]; pub const SUPPORTED_FORMATS: &'static [&'static str] = &["db", "json"];
#[allow(dead_code)] // TODO temp
pub enum MemoSetField { pub enum MemoSetField {
Body(String), Body(String),
Due(Option<DateTime<Utc>>), Due(Option<DateTime<Utc>>),
Done(Option<DateTime<Utc>>), Done(Option<DateTime<Utc>>),
} }
pub trait MemoStorage { pub trait StorageDriver {
fn all(&self, done: bool) -> Result<Vec<Memo>, MemoError>;
/// 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 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> { /// Return a vector with clones of all elements in storage (optionally include concluded ones)
self.insert(Memo::new(body, due)) fn all(&self, done:bool) -> Result<Vec<Memo>, MemoError>;
}
/// Return current storage context
fn ctx(&self) -> Result<Context, MemoError>;
fn update(&mut self, id: &Uuid, field: MemoSetField) -> Result<Memo, MemoError> {
let mut memo = self.get(id)?;
match field {
MemoSetField::Body(body) => memo.body = body,
MemoSetField::Due(due) => memo.due = due,
MemoSetField::Done(done) => memo.done = done,
} }
pub trait AuthStorage { self.put(memo.clone())?;
fn get_hash(&self) -> Result<String, MemoError>;
fn get_key(&self) -> Result<String, MemoError>; Ok(memo)
fn set_key(&mut self, key: &str) -> Result<Option<String>, MemoError>;
} }
pub trait StateStorage { fn add(&mut self, tag: String, body: String, due: Option<DateTime<Utc>>) -> Result<Memo, MemoError> {
fn set_state(&mut self, state: State) -> Result<Option<State>, MemoError>; let m = Memo {
fn get_state(&self) -> Result<State, MemoError>; id: Uuid::new_v4(),
tag, body, due,
fn set_edit_time(&mut self, time: DateTime<Utc>) -> Result<(), MemoError> { last_edit: Utc::now(),
let mut state = self.get_state() done: None,
.unwrap_or(State { last_edit: time, ..Default::default() }); };
state.last_edit = time; self.put(m.clone())?;
self.set_state(state)?; Ok(m)
Ok(())
} }
fn set_sync_time(&mut self, time: DateTime<Utc>) -> Result<(), MemoError> { fn del(&mut self, id: &Uuid) -> Result<Memo, MemoError> {
let mut state = self.get_state()?; let mut m = self.get(id)?;
state.last_sync = Some(time); m.done = Some(Utc::now());
self.set_state(state)?; self.put(m.clone())?;
Ok(()) return Ok(m);
} }
} }

View file

@ -1,21 +1,18 @@
use crate::remote::RemoteSync; // use crate::remote::RemoteSync;
use crate::storage::{AuthStorage, Memo, MemoError, MemoStorage, State, StateStorage}; use crate::model::{memo::{Memo, MemoError}, state::Context};
use crate::storage::StorageDriver;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
// use rusqlite::DatabaseName::Main; // use rusqlite::DatabaseName::Main;
use rusqlite::{params, Connection}; use rusqlite::{params, Connection};
use sha2::{Digest, Sha512}; // use sha2::{Digest, Sha512};
use std::path::PathBuf; use std::path::PathBuf;
use std::collections::LinkedList; use std::collections::LinkedList;
use std::fs;
use std::io::Write;
use uuid::Uuid; use uuid::Uuid;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use super::MemoSetField;
pub struct SQLiteStorage { pub struct SQLiteStorage {
conn: Connection, conn: Connection,
path: PathBuf, // path: PathBuf,
#[allow(dead_code)] // I just use this to bind the tempfile life with the SQLiteStorage obj #[allow(dead_code)] // I just use this to bind the tempfile life with the SQLiteStorage obj
tmpfile: Option<NamedTempFile>, tmpfile: Option<NamedTempFile>,
@ -30,6 +27,7 @@ impl SQLiteStorage {
connection.execute( connection.execute(
"CREATE TABLE IF NOT EXISTS memo ( "CREATE TABLE IF NOT EXISTS memo (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
tag TEXT NOT NULL,
body TEXT NOT NULL, body TEXT NOT NULL,
due DATETIME, due DATETIME,
done DATETIME DEFAULT NULL, done DATETIME DEFAULT NULL,
@ -41,53 +39,55 @@ impl SQLiteStorage {
"CREATE TABLE IF NOT EXISTS state ( "CREATE TABLE IF NOT EXISTS state (
last_edit DATETIME DEFAULT NULL, last_edit DATETIME DEFAULT NULL,
last_sync DATETIME DEFAULT NULL last_sync DATETIME DEFAULT NULL
last_memo TEXT DEFAULT NULL
);", );",
[], [],
)?; )?;
connection.execute(
"CREATE TABLE IF NOT EXISTS auth ( // connection.execute(
key TEXT PRIMARY KEY, // "CREATE TABLE IF NOT EXISTS auth (
hash TEXT NOT NULL // 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 { // impl AuthStorage for SQLiteStorage {
fn get_hash(&self) -> Result<String, MemoError> { // fn get_hash(&self) -> Result<String, MemoError> {
return Ok(self.conn.query_row("SELECT * FROM auth", [], |row| { // return Ok(self.conn.query_row("SELECT * FROM auth", [], |row| {
return Ok(row.get(1)?); // 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> { impl StorageDriver for SQLiteStorage {
return Ok(self.conn.query_row("SELECT * FROM auth", [], |row| { fn ctx(&self) -> Result<Context, MemoError> {
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> {
return Ok(self.conn.query_row("SELECT * FROM state", [], |row| { return Ok(self.conn.query_row("SELECT * FROM state", [], |row| {
return Ok(State { return Ok(Context {
last_edit: row.get(0)?, last_edit: row.get(0)?,
last_sync: row.get(1).ok(), last_sync: row.get(1).ok(),
..Default::default() ..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> { fn all(&self, done: bool) -> Result<Vec<Memo>, MemoError> {
let mut results_due : LinkedList<Memo> = LinkedList::new(); let mut results_due : LinkedList<Memo> = LinkedList::new();
let mut results_not_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 }; let tgt = if row.get::<usize, DateTime<Utc>>(2).is_ok() { &mut results_due } else { &mut results_not_due };
tgt.push_back(Memo { tgt.push_back(Memo {
id: Uuid::parse_str(row.get::<usize, String>(0)?.as_str()).unwrap(), id: Uuid::parse_str(row.get::<usize, String>(0)?.as_str()).unwrap(),
body: row.get(1)?, tag: row.get(1)?,
due: row.get(2).ok(), body: row.get(2)?,
done: row.get(3).ok(), due: row.get(3).ok(),
last_edit: row.get(4)?, 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()); return Ok(results_due.into_iter().collect());
} }
fn insert(&mut self, m: Memo) -> Result<(), MemoError> { fn put(&mut self, m: Memo) -> Result<(), MemoError> {
self.conn.execute( 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 (?, ?, ?, ?, ?)", "INSERT INTO memo (id, body, due, done, last_edit) VALUES (?, ?, ?, ?, ?)",
params![m.id.to_string(), m.body, m.due, m.done, m.last_edit], 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(()) Ok(())
} }
fn set(&mut self, id: &Uuid, field: MemoSetField) -> Result<usize, MemoError> { // fn del(&mut self, id: &Uuid) -> Result<usize, MemoError> {
match field { // return Ok(self.conn.execute(
MemoSetField::Body(body) => { // "UPDATE memo SET done = ? WHERE id = ?",
Ok(self.conn.execute( // params![Utc::now(), id.to_string()],
"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 get(&self, id: &Uuid) -> Result<Memo, MemoError> { fn get(&self, id: &Uuid) -> Result<Memo, MemoError> {
return Ok(self.conn.query_row( return Ok(self.conn.query_row(
"SELECT * FROM memo WHERE id = ? AND done = 0", "SELECT * FROM memo WHERE id = ?",
params![id.to_string()], params![id.to_string()],
|row| { |row| {
return Ok(Memo { return Ok(Memo {
id: Uuid::parse_str(row.get::<usize, String>(0)?.as_str()).unwrap(), id: Uuid::parse_str(row.get::<usize, String>(0)?.as_str()).unwrap(),
body: row.get(1)?, tag: row.get(1)?,
due: row.get(2).ok(), body: row.get(2)?,
done: row.get(3).ok(), due: row.get(3).ok(),
last_edit: row.get(4)?, done: row.get(4).ok(),
last_edit: row.get(5)?,
}); });
}, },
)?); )?);
@ -194,23 +165,23 @@ impl MemoStorage for SQLiteStorage {
} }
impl RemoteSync for SQLiteStorage { // impl RemoteSync for SQLiteStorage {
fn serialize(&self) -> Result<Vec<u8>, MemoError> { // 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 // return Ok(fs::read(&self.path).unwrap()); // TODO can I do this? just read the db while it's open
// let tmpfile = NamedTempFile::new()?; // // let tmpfile = NamedTempFile::new()?;
// self.conn.backup(Main, tmpfile.path(), None).unwrap(); // // self.conn.backup(Main, tmpfile.path(), None).unwrap();
// return Ok(fs::read(tmpfile).unwrap()); // // return Ok(fs::read(tmpfile).unwrap());
} // }
//
fn deserialize(data: Vec<u8>) -> Result<Self, MemoError> { // fn deserialize(data: Vec<u8>) -> Result<Self, MemoError> {
let mut tmpfile = NamedTempFile::new()?; // let mut tmpfile = NamedTempFile::new()?;
tmpfile.write(data.as_slice())?; // tmpfile.write(data.as_slice())?;
let path = tmpfile.path(); // let path = tmpfile.path();
return Ok(SQLiteStorage{ // return Ok(SQLiteStorage{
conn: Connection::open(path).unwrap(), // conn: Connection::open(path).unwrap(),
path: path.to_path_buf(), // path: path.to_path_buf(),
tmpfile: Some(tmpfile), // tmpfile: Some(tmpfile),
}); // });
} // }
//
} // }