diff --git a/Cargo.toml b/Cargo.toml index d56c862..f9b20af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,14 +10,17 @@ edition = "2021" [dependencies] regex = "1.5.5" +sha2 = "0.10.2" base64 = "0.13.0" chrono = "0.4.19" colored = "2.0.0" +rpassword = "6.0.1" git-version = "0.3.5" # ughh just for git hash const_format = "0.2.22" # ughh just for git hash libnotify = "1.0.3" +uuid = { version = "0.8.2", features = ["v4"] } clap = { version = "3.1.6", features = ["derive"] } -rusqlite = { version="0.27.0", features=["chrono"] } +rusqlite = { version="0.27.0", features=["chrono", "backup"] } ureq = { version="2.4.0", features=["json"] } [profile.release] diff --git a/src/main.rs b/src/main.rs index f490bb3..ad18438 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,8 +72,14 @@ fn main() { db_path = db; } + let storage = open_sqlite_storage(&db_path, true).unwrap(); + if args.sync { - let res = SQLiteStorage::fetch("asdasd", "http://127.0.0.1:8443"); + 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"); if res.is_ok() { println!("[v] downloaded remote db"); } else { @@ -81,8 +87,6 @@ fn main() { } } - let storage = open_sqlite_storage(&db_path).unwrap(); - match args.command { Some(Commands::New { body, due }) => { let mut due_date: Option> = None; @@ -103,7 +107,7 @@ fn main() { for memo in storage.all(false).unwrap() { if re.is_match(memo.body.as_str()) { if many { - storage.del(memo.id).unwrap(); + storage.del(&memo.id).unwrap(); println!("[-] done task : {}", memo.body); } else if found { println!("[!] would remove multiple tasks"); @@ -116,7 +120,7 @@ fn main() { } } if let Some(rm) = to_remove { - storage.del(rm.id).unwrap(); + storage.del(&rm.id).unwrap(); println!("{} done memo: {}", "[-]".bold(), rm.body); } } else { @@ -178,11 +182,15 @@ fn main() { println!("{} | {}", "memo-cli".bold(), timing); print!("{}", builder); } + + if !args.notify { // shitty assumption: notification is run by a daemon TODO + storage.set_run_time(Utc::now()).unwrap(); + } } } if args.sync { - let res = SQLiteStorage::store("asdasd", "http://127.0.0.1:8443"); + let res = storage.store(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(); @@ -190,6 +198,4 @@ fn main() { println!("[!] could not upload db : {}", res.err().unwrap().to_string()); } } - - storage.set_run_time(Utc::now()).unwrap(); } diff --git a/src/remote.rs b/src/remote.rs index 5b919f6..085db59 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -1,18 +1,54 @@ -use crate::storage::SQLiteStorage; +use crate::storage::{Memo, SQLiteStorage, MemoStorage, AuthStorage, open_sqlite_storage}; use std::fs; use std::io::{Write, Read}; +use rusqlite::params; +use std::collections::HashSet; +use uuid::Uuid; -pub trait RemoteSync { - fn store(hash:&str, server:&str) -> Result<(), ureq::Error>; - fn fetch(hash:&str, server:&str) -> Result<(), ureq::Error>; +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>; } impl RemoteSync for SQLiteStorage { - fn store(hash:&str, server:&str) -> Result<(), ureq::Error> { - let home_dir = env!("HOME").to_string(); - let contents = fs::read(home_dir + "/.local/share/memo-cli.db")?; + fn merge(&self, other: &SQLiteStorage) -> Result<(), rusqlite::Error> { + let mut memo_ids : HashSet = HashSet::new(); + for memo in self.all(false)?.iter() { + memo_ids.insert(memo.id); + } + 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!! + } else { + self.insert(&m)?; + } + 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!({ @@ -22,7 +58,7 @@ impl RemoteSync for SQLiteStorage { return Ok(()); } - fn fetch(hash:&str, server:&str) -> Result<(), ureq::Error> { + 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!({ @@ -30,11 +66,22 @@ impl RemoteSync for SQLiteStorage { "payload":"" }))?.into_reader(); - let home_dir = env!("HOME").to_string(); - let mut f = fs::File::create(home_dir + "/.local/share/memo-cli.db")?; - let mut data : Vec = vec![0;0]; - resp.read_to_end(&mut data)?; - f.write(data.as_slice())?; + 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(()); } } diff --git a/src/storage.rs b/src/storage.rs index 7c81567..67f22dc 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1,11 +1,14 @@ use chrono::{DateTime, Utc}; use rusqlite::{params, Connection, Error}; use std::fmt; +use uuid::Uuid; +use sha2::{Sha512, Digest}; pub struct Memo { - pub id: u32, + pub id: Uuid, pub body: String, pub due: Option>, + pub done: Option>, } pub struct State { @@ -31,13 +34,15 @@ impl fmt::Display for Memo { pub trait MemoStorage { fn all(&self, done: bool) -> Result, Error>; - fn add(&self, body: &str, due: Option>) -> Result<(), Error>; fn set(&self, memo: &Memo) -> Result; - fn del(&self, id: u32) -> Result; - fn get(&self, id: u32) -> 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>; } pub trait AuthStorage { + fn get_hash(&self) -> Result; fn get_key(&self) -> Result; fn set_key(&self, key:&str) -> Result, Error>; } @@ -64,38 +69,52 @@ pub trait StateStorage { // SQLiteStorage pub struct SQLiteStorage { - conn: Connection, + pub conn: Connection, // TODO make back private } -pub fn open_sqlite_storage(path: &str) -> Result { +pub fn open_sqlite_storage(path: &str, init: bool) -> Result { let connection = Connection::open(path)?; + // TODO check if table exist and is valid - connection.execute( - "CREATE TABLE IF NOT EXISTS memo ( - id INTEGER 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 - );", - [], - )?; + 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", [], @@ -107,10 +126,13 @@ impl AuthStorage for SQLiteStorage { 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 VALUES (?);", - params![key] + "INSERT INTO auth (key, hash) VALUES (?, ?);", + params![key, hash] )?; return Ok(old_key); } @@ -163,9 +185,10 @@ impl MemoStorage for SQLiteStorage { while let Some(row) = rows.next()? { results.push(Memo { - id: row.get(0)?, + id: Uuid::parse_str(row.get::(0)?.as_str()).unwrap(), body: row.get(1)?, - due: row.get(2)?, + due: row.get(2).ok(), + done: row.get(3).ok(), }); } } @@ -182,9 +205,10 @@ impl MemoStorage for SQLiteStorage { while let Some(row) = rows.next()? { results.push(Memo { - id: row.get(0)?, + id: Uuid::parse_str(row.get::(0)?.as_str()).unwrap(), body: row.get(1)?, - due: row.get(2)?, + due: row.get(2).ok(), + done: row.get(3).ok(), }); } } @@ -193,23 +217,26 @@ impl MemoStorage for SQLiteStorage { } fn add(&self, body: &str, due: Option>) -> Result<(), Error> { - // TODO join these 2 ifs? - if due.is_some() { - self.conn.execute( - "INSERT INTO memo (body, due) VALUES (?, ?)", - params![body, due], - )?; - } else { - self.conn - .execute("INSERT INTO memo (body) VALUES (?)", params![body])?; - } - return Ok(()); + 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 = ? WHERE id = ?", - params![memo.body, memo.due, memo.id], + "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); @@ -218,10 +245,10 @@ impl MemoStorage for SQLiteStorage { } } - fn del(&self, id: u32) -> Result { + fn del(&self, id: &Uuid) -> Result { let count = self .conn - .execute("UPDATE memo SET done = ? WHERE id = ?", params![Utc::now(), id])?; + .execute("UPDATE memo SET done = ? WHERE id = ?", params![Utc::now(), id.to_string()])?; if count > 0 { return Ok(true); } else { @@ -229,15 +256,16 @@ impl MemoStorage for SQLiteStorage { } } - fn get(&self, id: u32) -> Result { + fn get(&self, id: &Uuid) -> Result { return Ok(self.conn.query_row( "SELECT * FROM memo WHERE id = ? AND done = 0", - params![id], + params![id.to_string()], |row| { return Ok(Memo { - id: row.get(0)?, + id: Uuid::parse_str(row.get::(0)?.as_str()).unwrap(), body: row.get(1)?, - due: row.get(2).unwrap_or(None), + due: row.get(2).ok(), + done: row.get(3).ok(), }); }, )?);