From c085d9183ed700401423145bde1b72b5f080b4e0 Mon Sep 17 00:00:00 2001 From: alemidev Date: Sat, 26 Mar 2022 21:48:18 +0100 Subject: [PATCH] modularized storage, implemented basic json backend --- Cargo.toml | 7 +- src/main.rs | 53 +++++--- src/remote.rs | 112 ++++++---------- src/storage.rs | 296 +++++++++++------------------------------- src/storage/json.rs | 180 +++++++++++++++++++++++++ src/storage/sqlite.rs | 192 +++++++++++++++++++++++++++ src/utils.rs | 3 +- 7 files changed, 532 insertions(+), 311 deletions(-) create mode 100644 src/storage/json.rs create mode 100644 src/storage/sqlite.rs diff --git a/Cargo.toml b/Cargo.toml index b3365f1..d98d6ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,13 +12,16 @@ edition = "2021" regex = "1.5.5" sha2 = "0.10.2" base64 = "0.13.0" -chrono = "0.4.19" colored = "2.0.0" rpassword = "6.0.1" +tempfile = "3.3.0" git-version = "0.3.5" # ughh just for git hash const_format = "0.2.22" # ughh just for git hash notify-rust = "4" -uuid = { version = "0.8.2", features = ["v4"] } +serde = "1.0.136" +serde_json = "1.0" +chrono = { version = "0.4.19", features = ["serde"] } +uuid = { version = "0.8.2", features = ["v4", "serde"] } clap = { version = "3.1.6", features = ["derive"] } rusqlite = { version="0.27.0", features=["bundled-sqlcipher", "chrono", "backup"] } dirs = "4.0.0" # cross-platform paths diff --git a/src/main.rs b/src/main.rs index 187eb9c..a4631d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,17 @@ +mod remote; mod storage; mod utils; -mod remote; use chrono::{DateTime, Local, Utc}; use clap::{Parser, Subcommand}; use colored::Colorize; use const_format::concatcp; use git_version::git_version; -use regex::Regex; -use storage::{open_sqlite_storage, Memo, MemoStorage, AuthStorage, StateStorage, SQLiteStorage}; -use utils::{find_by_regex, parse_human_duration, HumanDisplay}; -use remote::RemoteSync; use notify_rust::Notification; +use regex::Regex; +use remote::RemoteSync; +use storage::{AuthStorage, Memo, MemoStorage, SQLiteStorage, StateStorage, JsonStorage}; +use utils::{find_by_regex, parse_human_duration, HumanDisplay}; const GIT_VERSION: &str = git_version!(); const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -26,13 +26,15 @@ const VERSION: &str = concatcp!(PKG_VERSION, "-", GIT_VERSION); struct Cli { #[clap(subcommand)] command: Option, + //#[clap(short, long, help = "name of db file")] + //name: Option, #[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(short, long, help = "location for database file")] + #[clap(short, long, help = "specify location for database file")] path: Option, } @@ -69,24 +71,30 @@ fn main() { //let home_path = std::env!("HOME").to_string(); //this is linux-only let mut db_path: std::path::PathBuf = dirs::home_dir().unwrap(); db_path.push(".local/share/memo-cli.db"); //+ "/.local/share/memo-cli.db"; - //WARNING: this is only for testing, it has no proper error handling and will panic is HOME is invalid or None + //WARNING: this is only for testing, it has no proper error handling and will panic is HOME is invalid or None if let Some(db) = args.path { db_path = std::path::PathBuf::from(db); } - let storage = open_sqlite_storage(db_path.to_str().unwrap(), true).unwrap(); + let mut storage = SQLiteStorage::new(db_path.to_str().unwrap(), true).unwrap(); 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.fetch(storage.get_hash().unwrap().as_str(), "http://127.0.0.1:8443"); + 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()); + println!( + "[!] could not fetch db : {}", + res.err().unwrap().to_string() + ); } } @@ -159,8 +167,16 @@ fn main() { let mut builder = String::new(); let timing = if let Some(state) = storage.get_state().ok() { let now = Local::now(); - format!("last run: {}", state.last_run.with_timezone(&now.timezone()).format("%a %d/%m %H:%M")) - } else { Local::now().format("%a %d/%m, %H:%M").to_string() }; + format!( + "last run: {}", + state + .last_run + .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"); } @@ -183,19 +199,26 @@ fn main() { print!("{}", builder); } - if !args.notify { // shitty assumption: notification is run by a daemon TODO + if !args.notify { + // shitty assumption: notification is run by a daemon TODO storage.set_run_time(Utc::now()).unwrap(); } } } if args.sync { - let res = storage.store(storage.get_hash().unwrap().as_str(), "http://127.0.0.1:8443"); + 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()); + println!( + "[!] could not upload db : {}", + res.err().unwrap().to_string() + ); } } } diff --git a/src/remote.rs b/src/remote.rs index 085db59..dca451c 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -1,19 +1,42 @@ -use crate::storage::{Memo, SQLiteStorage, MemoStorage, AuthStorage, open_sqlite_storage}; -use std::fs; -use std::io::{Write, Read}; -use rusqlite::params; +use crate::storage::{MemoError, MemoStorage, AuthStorage}; use std::collections::HashSet; use uuid::Uuid; +use std::io::Read; -pub trait RemoteSync : AuthStorage+MemoStorage { - fn store(&self, hash:&str, server:&str) -> Result<(), ureq::Error>; - fn fetch(&self, hash:&str, server:&str) -> Result<(), ureq::Error>; - fn merge(&self, other:&Self) -> Result<(), rusqlite::Error>; -} +pub trait RemoteSync : AuthStorage+MemoStorage+Sized { + fn serialize(&self) -> Result, MemoError>; + fn deserialize(data: Vec) -> Result; -impl RemoteSync for SQLiteStorage { + fn upload(&self, hash: &str, server: &str) -> Result { + let dest = server.to_string() + "/put"; + // TODO encrypt + let resp = ureq::post(dest.as_str()).send_json(ureq::json!({ + "file":hash, + "payload":base64::encode(self.serialize()?.as_slice()) + }))?; + return Ok(resp.status()); + } - fn merge(&self, other: &SQLiteStorage) -> Result<(), rusqlite::Error> { + fn download(&mut self, hash: &str, server: &str) -> Result { + let dest = server.to_string() + "/get"; + let mut resp = ureq::post(dest.as_str()) + .send_json(ureq::json!({ + "file":hash, + "payload":"" + }))? + .into_reader(); + + let mut data: Vec = vec![0; 0]; + resp.read_to_end(&mut data)?; + // TODO decrypt + + let other = Self::deserialize(data).unwrap(); + + return Ok(self.merge(&other).unwrap()); + } + + fn merge(&mut self, other: &Self) -> Result { // TODO don't use error specific to rusqlite + let mut count = 0; let mut memo_ids : HashSet = HashSet::new(); for memo in self.all(false)?.iter() { memo_ids.insert(memo.id); @@ -21,67 +44,16 @@ impl RemoteSync for SQLiteStorage { for memo in self.all(true)?.iter() { memo_ids.insert(memo.id); } - let mut stmt = other.conn.prepare("SELECT * FROM memo")?; - let r = stmt.query_map([], |row| { - let id = Uuid::parse_str(row.get::(0)?.as_str()).unwrap(); - let m = Memo{ - id: id, - body: row.get(1)?, - due: row.get(2).ok(), - done: row.get(3).ok(), - }; - if memo_ids.contains(&id) { - self.set(&m)?; // TODO only replace if more recently edited!! + + for memo in other.all(true)?.iter() { + if memo_ids.contains(&memo.id) { + self.set(memo)?; // TODO only replace if more recently edited!! } else { - self.insert(&m)?; + self.insert(memo.clone())?; + count += 1; } - Ok(()) - })?.count(); - Ok(()) - } - - fn store(&self, hash:&str, server:&str) -> Result<(), ureq::Error> { - let tmpfile = "asd"; // TODO - self.conn.backup(rusqlite::DatabaseName::Main, tmpfile, None).unwrap(); - - let tmp_db = open_sqlite_storage(tmpfile, false).unwrap(); // TODO - tmp_db.conn.execute(format!("PRAGMA key = {} ;", self.get_key().unwrap()).as_str(), []).unwrap(); // For some reason it won't work with normal params! ??? - tmp_db.conn.close().unwrap(); - - let contents = fs::read(tmpfile)?; - let dest = server.to_string() + "/put"; - let _resp = ureq::post(dest.as_str()) - .send_json(ureq::json!({ - "file":hash, - "payload":base64::encode(contents.as_slice()) - }))?; - return Ok(()); - } - - fn fetch(&self, hash:&str, server:&str) -> Result<(), ureq::Error> { - let dest = server.to_string() + "/get"; - let mut resp = ureq::post(dest.as_str()) - .send_json(ureq::json!({ - "file":hash, - "payload":"" - }))?.into_reader(); - - let tmpfile = "asd"; // TODO - { - let mut f = fs::File::create(tmpfile)?; - let mut data : Vec = vec![0;0]; - resp.read_to_end(&mut data)?; - f.write(data.as_slice())?; } - let tmp_db = open_sqlite_storage(tmpfile, false).unwrap(); - tmp_db.conn.execute(format!("PRAGMA key = {} ;", self.get_key().unwrap()).as_str(), []).unwrap(); // For some reason it won't work with normal params! ??? - - self.merge(&tmp_db).unwrap(); - - tmp_db.conn.close().unwrap(); - - // TODO delete tempfile - - return Ok(()); + Ok(count) } } + diff --git a/src/storage.rs b/src/storage.rs index 67f22dc..bd87860 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1,9 +1,17 @@ +pub mod sqlite; +pub mod json; + +pub use sqlite::SQLiteStorage; +pub use json::JsonStorage; + use chrono::{DateTime, Utc}; -use rusqlite::{params, Connection, Error}; +use std::error::Error; +use std::collections::LinkedList; use std::fmt; use uuid::Uuid; -use sha2::{Sha512, Digest}; +use serde::{Serialize, Deserialize}; +#[derive(Clone, Serialize, Deserialize)] pub struct Memo { pub id: Uuid, pub body: String, @@ -22,252 +30,94 @@ impl fmt::Display for Memo { if self.due.is_some() { due_str = self.due.unwrap().to_string(); } + let mut done_str = "null".to_string(); + if self.done.is_some() { + done_str = self.done.unwrap().to_string(); + } return write!( f, - "Memo(id={id}, body={body}, due={due})", + "Memo(id={id}, body={body}, due={due}, done={done})", id = self.id, body = self.body, due = due_str, + done = done_str ); } } +#[derive(Debug)] +pub struct MemoError { + pub cause: &'static str, + pub sqlite: Option, + pub io: Option, + pub ureq: Option, +} + +impl ToString for MemoError { + fn to_string(&self) -> String { + format!( + "MemoError(cause={cause}, sqlite={sqlite}, io={io}, ureq={ureq})", + cause=self.cause, + sqlite= if self.sqlite.is_some() { self.sqlite.as_ref().unwrap().to_string() } else { "null".to_string() }, + io= if self.io.is_some() { self.io.as_ref().unwrap().to_string() } else { "null".to_string() }, + ureq= if self.ureq.is_some() { self.ureq.as_ref().unwrap().to_string() } else { "null".to_string() }, + ) + } +} + +impl From for MemoError { + fn from(e: rusqlite::Error) -> Self { Self{cause: "sqlite", sqlite:Some(e), io:None, ureq:None} } +} + +impl From for MemoError { + fn from(e: std::io::Error) -> Self { Self{cause: "io", sqlite:None, io:Some(e), ureq:None} } +} + +impl From for MemoError { + fn from(e: ureq::Error) -> Self { Self{cause: "ureq", sqlite:None, io:None, ureq:Some(e)} } +} + pub trait MemoStorage { - fn all(&self, done: bool) -> Result, Error>; - fn set(&self, memo: &Memo) -> Result; - fn del(&self, id: &Uuid) -> Result; - fn get(&self, id: &Uuid) -> Result; - fn add(&self, body: &str, due: Option>) -> Result<(), Error>; - fn insert(&self, m: &Memo) -> Result<(), Error>; + fn all(&self, done: bool) -> Result, MemoError>; + fn get(&self, id: &Uuid) -> Result; + fn set(&mut self, memo: &Memo) -> Result; + fn del(&mut self, id: &Uuid) -> Result; + fn insert(&mut self, m: Memo) -> Result<(), MemoError>; + + fn add(&mut self, body: &str, due: Option>) -> Result<(), MemoError> { + self.insert(Memo { + id: Uuid::new_v4(), + body: body.to_string(), + due: due, + done: None, + }) + } } pub trait AuthStorage { - fn get_hash(&self) -> Result; - fn get_key(&self) -> Result; - fn set_key(&self, key:&str) -> Result, Error>; + fn get_hash(&self) -> Result; + fn get_key(&self) -> Result; + fn set_key(&mut self, key: &str) -> Result, MemoError>; } pub trait StateStorage { - fn set_state(&self, state:State) -> Result, Error>; - fn get_state(&self) -> Result; + fn set_state(&mut self, state: State) -> Result, MemoError>; + fn get_state(&self) -> Result; - fn set_run_time(&self, time:DateTime) -> Result<(), Error> { - let mut state = self.get_state().unwrap_or(State{last_run:time, last_sync:None}); // TODO jank way to not fail on 1st use + fn set_run_time(&mut self, time: DateTime) -> Result<(), MemoError> { + let mut state = self.get_state().unwrap_or(State { + last_run: time, + last_sync: None, + }); // TODO jank way to not fail on 1st use state.last_run = time; self.set_state(state)?; Ok(()) } - fn set_sync_time(&self, time:DateTime) -> Result<(), Error> { + fn set_sync_time(&mut self, time: DateTime) -> Result<(), MemoError> { let mut state = self.get_state()?; state.last_sync = Some(time); self.set_state(state)?; Ok(()) } } - -// SQLiteStorage - -pub struct SQLiteStorage { - pub conn: Connection, // TODO make back private -} - -pub fn open_sqlite_storage(path: &str, init: bool) -> Result { - let connection = Connection::open(path)?; - - // TODO check if table exist and is valid - if init { - connection.execute( - "CREATE TABLE IF NOT EXISTS memo ( - id TEXT PRIMARY KEY, - body TEXT NOT NULL, - due DATETIME, - done DATETIME DEFAULT NULL - );", - [], - )?; - connection.execute( - "CREATE TABLE IF NOT EXISTS state ( - last_run DATETIME DEFAULT NULL, - last_sync DATETIME DEFAULT NULL - );", - [], - )?; - connection.execute( - "CREATE TABLE IF NOT EXISTS auth ( - key TEXT PRIMARY KEY, - hash TEXT NOT NULL - );", - [], - )?; - } - - return Ok(SQLiteStorage { conn: connection }); -} - -impl AuthStorage for SQLiteStorage { - fn get_hash(&self) -> Result { - return Ok(self.conn.query_row( - "SELECT * FROM auth", [], - |row| { - return Ok(row.get(1)?); - }, - )?); - } - - fn get_key(&self) -> Result { - return Ok(self.conn.query_row( - "SELECT * FROM auth", [], - |row| { - return Ok(row.get(0)?); - }, - )?); - } - - fn set_key(&self, key: &str) -> Result, Error> { - 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 { - return Ok(self.conn.query_row( - "SELECT * FROM state", [], - |row| { - return Ok(State{ - last_run:row.get(0)?, - last_sync:row.get(1).ok() - }); - }, - )?); - } - - fn set_state(&self, state:State) -> Result, Error> { - let old_state = self.get_state().ok(); - self.conn.execute("DELETE FROM state;", [])?; - self.conn.execute( - "INSERT INTO state (last_run, last_sync) VALUES (?, ?);", - params![state.last_run, state.last_sync] - )?; - return Ok(old_state); - } -} - -impl MemoStorage for SQLiteStorage { - fn all(&self, done: bool) -> Result, Error> { - let mut results = Vec::new(); - let not_null = if done { "NOT" } else { "" }; - - /* - * SQLite considers NULL as smallest value, so we will always get events with no due date - * first. To circumvent this, we first query all memos with a due date, and then all - * others. This is kinda jank but will do for now. - */ - - { - let mut statement = self.conn.prepare( - format!(// TODO eww but I can't find a way to insert "NOT" with rusqlite - "SELECT * FROM memo WHERE due IS NOT NULL AND done IS {} NULL ORDER BY due, id", - not_null - ) - .as_str(), - )?; - let mut rows = statement.query([])?; - - while let Some(row) = rows.next()? { - results.push(Memo { - id: Uuid::parse_str(row.get::(0)?.as_str()).unwrap(), - body: row.get(1)?, - due: row.get(2).ok(), - done: row.get(3).ok(), - }); - } - } - - { - let mut statement = self.conn.prepare( - format!(// TODO eww but I can't find a way to insert "NOT" with rusqlite - "SELECT * FROM memo WHERE due IS NULL AND done IS {} NULL ORDER BY due, id", - not_null - ) - .as_str(), - )?; - let mut rows = statement.query([])?; - - while let Some(row) = rows.next()? { - results.push(Memo { - id: Uuid::parse_str(row.get::(0)?.as_str()).unwrap(), - body: row.get(1)?, - due: row.get(2).ok(), - done: row.get(3).ok(), - }); - } - } - - return Ok(results); - } - - fn add(&self, body: &str, due: Option>) -> Result<(), Error> { - self.insert(&Memo{ - id: Uuid::new_v4(), - body: body.to_string(), - due: due, - done: None - }) - } - - fn insert(&self, m: &Memo) -> Result<(), Error> { - self.conn.execute( - "INSERT INTO memo (id, body, due, done) VALUES (?, ?, ?, ?)", - params![m.id.to_string(), m.body, m.due, m.done], - )?; - Ok(()) - } - - fn set(&self, memo: &Memo) -> Result { - let count = self.conn.execute( - "UPDATE memo SET body = ?, due = ?, done = ? WHERE id = ?", - params![memo.body, memo.due, memo.done, memo.id.to_string()], - )?; - if count > 0 { - return Ok(true); - } else { - return Ok(false); - } - } - - fn del(&self, id: &Uuid) -> Result { - let count = self - .conn - .execute("UPDATE memo SET done = ? WHERE id = ?", params![Utc::now(), id.to_string()])?; - if count > 0 { - return Ok(true); - } else { - return Ok(false); - } - } - - fn get(&self, id: &Uuid) -> Result { - return Ok(self.conn.query_row( - "SELECT * FROM memo WHERE id = ? AND done = 0", - params![id.to_string()], - |row| { - return Ok(Memo { - id: Uuid::parse_str(row.get::(0)?.as_str()).unwrap(), - body: row.get(1)?, - due: row.get(2).ok(), - done: row.get(3).ok(), - }); - }, - )?); - } -} diff --git a/src/storage/json.rs b/src/storage/json.rs new file mode 100644 index 0000000..91d17b5 --- /dev/null +++ b/src/storage/json.rs @@ -0,0 +1,180 @@ +use std::fs::File; +use std::io::BufReader; +use chrono::{DateTime, Utc}; +use serde::{Serialize, Deserialize}; +use std::io::Write; +use sha2::{Digest, Sha512}; +use std::collections::LinkedList; + +use crate::storage::{Memo, State, MemoError, AuthStorage, MemoStorage, StateStorage}; +use crate::remote::RemoteSync; + +pub struct JsonStorage { + path: String, + data: Store, +} + +#[derive(Serialize, Deserialize)] +struct Store { + key: String, + hash: String, + last_sync: Option>, + last_run: DateTime, + memo: Vec, + archive: Vec, +} + +impl Default for Store { + fn default() -> Self { + return Store{ + key: "".to_string(), + hash: "".to_string(), + last_sync: None, + last_run: Utc::now(), + memo: Vec::new(), + archive: Vec::new(), + }; + } +} + +impl JsonStorage { + pub fn new(path: &str) -> Result { + let mut data = Store::default(); + let file = File::open(path); + if let Ok(f) = file { + let reader = BufReader::new(f); + data = serde_json::from_reader(reader).unwrap(); + } + + // TODO check if table exist and is valid + + return Ok(JsonStorage{ path: path.to_string(), data: data }); + } + + pub fn save(&self) -> Result<(), std::io::Error> { + let mut f = std::fs::File::create(&self.path)?; + f.write(serde_json::to_string(&self.data)?.as_bytes())?; + Ok(()) + } +} + +impl AuthStorage for JsonStorage { + fn get_hash(&self) -> Result { + return Ok(self.data.hash.to_string()); + } + + fn get_key(&self) -> Result { + return Ok(self.data.key.to_string()); + } + + fn set_key(&mut self, key: &str) -> Result, 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 { + return Ok( + State { + last_run: self.data.last_run, + last_sync: self.data.last_sync, + } + ); + } + + fn set_state(&mut self, state: State) -> Result, MemoError> { + let old_state = Some(State{last_run: self.data.last_run, last_sync:self.data.last_sync}); + self.data.last_sync = state.last_sync; + self.data.last_run = state.last_run; + self.save()?; + return Ok(old_state); + } +} + +impl MemoStorage for JsonStorage { + fn all(&self, done: bool) -> Result, MemoError> { + let mut results_due : LinkedList = LinkedList::new(); + let mut results_not_due : LinkedList = LinkedList::new(); + + for memo in &self.data.memo { + if done ^ memo.done.is_none() { // if done is true, has to not be none, if done is false, it has to be none + if memo.due.is_some() { + results_due.push_back((*memo).clone()); + } else { + results_not_due.push_back((*memo).clone()); + } + } + } + + results_due.append(&mut results_not_due); + + return Ok(results_due); + } + + fn insert(&mut self, m: Memo) -> Result<(), MemoError> { + self.data.memo.push(m); + self.save()?; + Ok(()) + } + + fn set(&mut self, m: &Memo) -> Result { + 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 { + 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.remove(index as usize); + self.save()?; + } + return Ok(count); + } + + fn get(&self, id: &uuid::Uuid) -> Result { + for memo in &self.data.memo { + if memo.id == *id { + return Ok(memo.clone()); + } + } + return Err(MemoError{cause:"not found", sqlite:None, io:None, ureq:None}); + } +} + + +impl RemoteSync for JsonStorage { + fn serialize(&self) -> Result, MemoError> { + return Ok(std::fs::read(&self.path).unwrap()); + } + + fn deserialize(data: Vec) -> Result { + return Ok(JsonStorage{ + path: "".to_string(), + data: serde_json::from_slice(data.as_slice()).unwrap(), + }); + } + +} diff --git a/src/storage/sqlite.rs b/src/storage/sqlite.rs new file mode 100644 index 0000000..9c770eb --- /dev/null +++ b/src/storage/sqlite.rs @@ -0,0 +1,192 @@ +use crate::remote::RemoteSync; +use crate::storage::{AuthStorage, Memo, MemoError, MemoStorage, State, StateStorage}; +use chrono::{DateTime, Utc}; +use rusqlite::DatabaseName::Main; +use rusqlite::{params, Connection}; +use std::io::Error; +use sha2::{Digest, Sha512}; +use std::collections::LinkedList; +use std::fs; +use std::io::Write; +use uuid::Uuid; +use tempfile::NamedTempFile; + +pub struct SQLiteStorage { + conn: Connection, + path: String, + tmpfile: Option, +} + +impl SQLiteStorage { + pub fn new(path: &str, init: bool) -> Result { + let connection = Connection::open(path)?; + + // TODO check if table exist and is valid + if init { + connection.execute( + "CREATE TABLE IF NOT EXISTS memo ( + id TEXT PRIMARY KEY, + body TEXT NOT NULL, + due DATETIME, + done DATETIME DEFAULT NULL + );", + [], + )?; + connection.execute( + "CREATE TABLE IF NOT EXISTS state ( + last_run DATETIME DEFAULT NULL, + last_sync DATETIME DEFAULT NULL + );", + [], + )?; + connection.execute( + "CREATE TABLE IF NOT EXISTS auth ( + key TEXT PRIMARY KEY, + hash TEXT NOT NULL + );", + [], + )?; + } + + return Ok(SQLiteStorage { conn: connection , path: path.to_string(), tmpfile: None }); + } +} + +impl AuthStorage for SQLiteStorage { + fn get_hash(&self) -> Result { + return Ok(self.conn.query_row("SELECT * FROM auth", [], |row| { + return Ok(row.get(1)?); + })?); + } + + fn get_key(&self) -> Result { + return Ok(self.conn.query_row("SELECT * FROM auth", [], |row| { + return Ok(row.get(0)?); + })?); + } + + fn set_key(&mut self, key: &str) -> Result, MemoError> { + 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 { + return Ok(self.conn.query_row("SELECT * FROM state", [], |row| { + return Ok(State { + last_run: row.get(0)?, + last_sync: row.get(1).ok(), + }); + })?); + } + + fn set_state(&mut self, state: State) -> Result, MemoError> { + let old_state = self.get_state().ok(); + self.conn.execute("DELETE FROM state;", [])?; + self.conn.execute( + "INSERT INTO state (last_run, last_sync) VALUES (?, ?);", + params![state.last_run, state.last_sync], + )?; + return Ok(old_state); + } +} + +impl MemoStorage for SQLiteStorage { + fn all(&self, done: bool) -> Result, MemoError> { + let mut results_due : LinkedList = LinkedList::new(); + let mut results_not_due : LinkedList = LinkedList::new(); + let not_null = if done { "NOT" } else { "" }; + + let mut statement = self.conn.prepare( + format!( + // TODO eww but I can't find a way to insert "NOT" with rusqlite + "SELECT * FROM memo WHERE done IS {} NULL ORDER BY due, id", + not_null + ) + .as_str(), + )?; + let mut rows = statement.query([])?; + + while let Some(row) = rows.next()? { + let tgt = if row.get::>(2).is_ok() { &mut results_due } else { &mut results_not_due }; + tgt.push_back(Memo { + id: Uuid::parse_str(row.get::(0)?.as_str()).unwrap(), + body: row.get(1)?, + due: row.get(2).ok(), + done: row.get(3).ok(), + }); + } + + results_due.append(&mut results_not_due); + + return Ok(results_due); + } + + fn insert(&mut self, m: Memo) -> Result<(), MemoError> { + self.conn.execute( + "INSERT INTO memo (id, body, due, done) VALUES (?, ?, ?, ?)", + params![m.id.to_string(), m.body, m.due, m.done], + )?; + Ok(()) + } + + fn set(&mut self, m: &Memo) -> Result { + return Ok(self.conn.execute( + "UPDATE memo SET body = ?, due = ?, done = ? WHERE id = ?", + params![m.body, m.due, m.done, m.id.to_string()], + )?); + } + + fn del(&mut self, id: &Uuid) -> Result { + return Ok(self.conn.execute( + "UPDATE memo SET done = ? WHERE id = ?", + params![Utc::now(), id.to_string()], + )?); + } + + fn get(&self, id: &Uuid) -> Result { + return Ok(self.conn.query_row( + "SELECT * FROM memo WHERE id = ? AND done = 0", + params![id.to_string()], + |row| { + return Ok(Memo { + id: Uuid::parse_str(row.get::(0)?.as_str()).unwrap(), + body: row.get(1)?, + due: row.get(2).ok(), + done: row.get(3).ok(), + }); + }, + )?); + } +} + + +impl RemoteSync for SQLiteStorage { + fn serialize(&self) -> Result, 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) -> Result { + let mut tmpfile = NamedTempFile::new()?; + tmpfile.write(data.as_slice())?; + let path = tmpfile.path().to_str().unwrap(); + return Ok(SQLiteStorage{ + conn: Connection::open(path).unwrap(), + path: path.to_string(), + tmpfile: Some(tmpfile), + }); + } + +} diff --git a/src/utils.rs b/src/utils.rs index aaa1e11..17a6b9e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,6 +2,7 @@ use crate::storage::Memo; use chrono::{Duration, Utc}; use regex::{Error, Regex}; use colored::Colorize; +use std::collections::LinkedList; pub trait HumanDisplay { fn human(&self) -> String; @@ -91,7 +92,7 @@ pub fn parse_human_duration(input: &str) -> Result { return Ok(Duration::seconds(secs)); } -pub fn find_by_regex(re: Regex, memos: Vec) -> Option { +pub fn find_by_regex(re: Regex, memos: LinkedList) -> Option { let mut found = false; let mut out: Option = None; for memo in memos {