mirror of
https://git.alemi.dev/memo-cli.git
synced 2024-11-14 17:49:18 +01:00
added merge, use keys to sync, encrypt sent db
there's still much to do: server discards symbols in filename but base64 might produce symbols. The edit sync is broken: new memos will be synched but not edited memos. Also it's pretty janky but I have to start somewhere.
This commit is contained in:
parent
62ee3370be
commit
497879b839
4 changed files with 159 additions and 75 deletions
|
@ -10,14 +10,17 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
regex = "1.5.5"
|
regex = "1.5.5"
|
||||||
|
sha2 = "0.10.2"
|
||||||
base64 = "0.13.0"
|
base64 = "0.13.0"
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
colored = "2.0.0"
|
colored = "2.0.0"
|
||||||
|
rpassword = "6.0.1"
|
||||||
git-version = "0.3.5" # ughh just for git hash
|
git-version = "0.3.5" # ughh just for git hash
|
||||||
const_format = "0.2.22" # ughh just for git hash
|
const_format = "0.2.22" # ughh just for git hash
|
||||||
libnotify = "1.0.3"
|
libnotify = "1.0.3"
|
||||||
|
uuid = { version = "0.8.2", features = ["v4"] }
|
||||||
clap = { version = "3.1.6", features = ["derive"] }
|
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"] }
|
ureq = { version="2.4.0", features=["json"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|
22
src/main.rs
22
src/main.rs
|
@ -72,8 +72,14 @@ fn main() {
|
||||||
db_path = db;
|
db_path = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let storage = open_sqlite_storage(&db_path, true).unwrap();
|
||||||
|
|
||||||
if args.sync {
|
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() {
|
if res.is_ok() {
|
||||||
println!("[v] downloaded remote db");
|
println!("[v] downloaded remote db");
|
||||||
} else {
|
} else {
|
||||||
|
@ -81,8 +87,6 @@ fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let storage = open_sqlite_storage(&db_path).unwrap();
|
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
Some(Commands::New { body, due }) => {
|
Some(Commands::New { body, due }) => {
|
||||||
let mut due_date: Option<DateTime<Utc>> = None;
|
let mut due_date: Option<DateTime<Utc>> = None;
|
||||||
|
@ -103,7 +107,7 @@ fn main() {
|
||||||
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 {
|
||||||
println!("[!] would remove multiple tasks");
|
println!("[!] would remove multiple tasks");
|
||||||
|
@ -116,7 +120,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(rm) = to_remove {
|
if let Some(rm) = to_remove {
|
||||||
storage.del(rm.id).unwrap();
|
storage.del(&rm.id).unwrap();
|
||||||
println!("{} done memo: {}", "[-]".bold(), rm.body);
|
println!("{} done memo: {}", "[-]".bold(), rm.body);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -178,11 +182,15 @@ fn main() {
|
||||||
println!("{} | {}", "memo-cli".bold(), timing);
|
println!("{} | {}", "memo-cli".bold(), timing);
|
||||||
print!("{}", builder);
|
print!("{}", builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !args.notify { // shitty assumption: notification is run by a daemon TODO
|
||||||
|
storage.set_run_time(Utc::now()).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.sync {
|
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() {
|
if res.is_ok() {
|
||||||
println!("[^] uploaded local db");
|
println!("[^] uploaded local db");
|
||||||
storage.set_sync_time(Utc::now()).unwrap();
|
storage.set_sync_time(Utc::now()).unwrap();
|
||||||
|
@ -190,6 +198,4 @@ fn main() {
|
||||||
println!("[!] could not upload db : {}", res.err().unwrap().to_string());
|
println!("[!] could not upload db : {}", res.err().unwrap().to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
storage.set_run_time(Utc::now()).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,54 @@
|
||||||
use crate::storage::SQLiteStorage;
|
use crate::storage::{Memo, SQLiteStorage, MemoStorage, AuthStorage, open_sqlite_storage};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{Write, Read};
|
use std::io::{Write, Read};
|
||||||
|
use rusqlite::params;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub trait RemoteSync {
|
pub trait RemoteSync : AuthStorage+MemoStorage {
|
||||||
fn store(hash:&str, server:&str) -> Result<(), ureq::Error>;
|
fn store(&self, hash:&str, server:&str) -> Result<(), ureq::Error>;
|
||||||
fn fetch(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 {
|
impl RemoteSync for SQLiteStorage {
|
||||||
|
|
||||||
fn store(hash:&str, server:&str) -> Result<(), ureq::Error> {
|
fn merge(&self, other: &SQLiteStorage) -> Result<(), rusqlite::Error> {
|
||||||
let home_dir = env!("HOME").to_string();
|
let mut memo_ids : HashSet<Uuid> = HashSet::new();
|
||||||
let contents = fs::read(home_dir + "/.local/share/memo-cli.db")?;
|
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::<usize,String>(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 dest = server.to_string() + "/put";
|
||||||
let _resp = ureq::post(dest.as_str())
|
let _resp = ureq::post(dest.as_str())
|
||||||
.send_json(ureq::json!({
|
.send_json(ureq::json!({
|
||||||
|
@ -22,7 +58,7 @@ impl RemoteSync for SQLiteStorage {
|
||||||
return Ok(());
|
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 dest = server.to_string() + "/get";
|
||||||
let mut resp = ureq::post(dest.as_str())
|
let mut resp = ureq::post(dest.as_str())
|
||||||
.send_json(ureq::json!({
|
.send_json(ureq::json!({
|
||||||
|
@ -30,11 +66,22 @@ impl RemoteSync for SQLiteStorage {
|
||||||
"payload":""
|
"payload":""
|
||||||
}))?.into_reader();
|
}))?.into_reader();
|
||||||
|
|
||||||
let home_dir = env!("HOME").to_string();
|
let tmpfile = "asd"; // TODO
|
||||||
let mut f = fs::File::create(home_dir + "/.local/share/memo-cli.db")?;
|
{
|
||||||
let mut data : Vec<u8> = vec![0;0];
|
let mut f = fs::File::create(tmpfile)?;
|
||||||
resp.read_to_end(&mut data)?;
|
let mut data : Vec<u8> = vec![0;0];
|
||||||
f.write(data.as_slice())?;
|
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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
134
src/storage.rs
134
src/storage.rs
|
@ -1,11 +1,14 @@
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use rusqlite::{params, Connection, Error};
|
use rusqlite::{params, Connection, Error};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use sha2::{Sha512, Digest};
|
||||||
|
|
||||||
pub struct Memo {
|
pub struct Memo {
|
||||||
pub id: u32,
|
pub id: Uuid,
|
||||||
pub body: String,
|
pub body: String,
|
||||||
pub due: Option<DateTime<Utc>>,
|
pub due: Option<DateTime<Utc>>,
|
||||||
|
pub done: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
|
@ -31,13 +34,15 @@ impl fmt::Display for Memo {
|
||||||
|
|
||||||
pub trait MemoStorage {
|
pub trait MemoStorage {
|
||||||
fn all(&self, done: bool) -> Result<Vec<Memo>, Error>;
|
fn all(&self, done: bool) -> Result<Vec<Memo>, Error>;
|
||||||
fn add(&self, body: &str, due: Option<DateTime<Utc>>) -> Result<(), Error>;
|
|
||||||
fn set(&self, memo: &Memo) -> Result<bool, Error>;
|
fn set(&self, memo: &Memo) -> Result<bool, Error>;
|
||||||
fn del(&self, id: u32) -> Result<bool, Error>;
|
fn del(&self, id: &Uuid) -> Result<bool, Error>;
|
||||||
fn get(&self, id: u32) -> Result<Memo, Error>;
|
fn get(&self, id: &Uuid) -> Result<Memo, Error>;
|
||||||
|
fn add(&self, body: &str, due: Option<DateTime<Utc>>) -> Result<(), Error>;
|
||||||
|
fn insert(&self, m: &Memo) -> Result<(), Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait AuthStorage {
|
pub trait AuthStorage {
|
||||||
|
fn get_hash(&self) -> Result<String, Error>;
|
||||||
fn get_key(&self) -> Result<String, Error>;
|
fn get_key(&self) -> Result<String, Error>;
|
||||||
fn set_key(&self, key:&str) -> Result<Option<String>, Error>;
|
fn set_key(&self, key:&str) -> Result<Option<String>, Error>;
|
||||||
}
|
}
|
||||||
|
@ -64,38 +69,52 @@ pub trait StateStorage {
|
||||||
// SQLiteStorage
|
// SQLiteStorage
|
||||||
|
|
||||||
pub struct SQLiteStorage {
|
pub struct SQLiteStorage {
|
||||||
conn: Connection,
|
pub conn: Connection, // TODO make back private
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_sqlite_storage(path: &str) -> Result<SQLiteStorage, Error> {
|
pub fn open_sqlite_storage(path: &str, init: bool) -> Result<SQLiteStorage, Error> {
|
||||||
let connection = Connection::open(path)?;
|
let connection = Connection::open(path)?;
|
||||||
|
|
||||||
// TODO check if table exist and is valid
|
// TODO check if table exist and is valid
|
||||||
connection.execute(
|
if init {
|
||||||
"CREATE TABLE IF NOT EXISTS memo (
|
connection.execute(
|
||||||
id INTEGER PRIMARY KEY,
|
"CREATE TABLE IF NOT EXISTS memo (
|
||||||
body TEXT NOT NULL,
|
id TEXT PRIMARY KEY,
|
||||||
due DATETIME,
|
body TEXT NOT NULL,
|
||||||
done DATETIME DEFAULT NULL
|
due DATETIME,
|
||||||
);",
|
done DATETIME DEFAULT NULL
|
||||||
[],
|
);",
|
||||||
)?;
|
[],
|
||||||
connection.execute(
|
)?;
|
||||||
"CREATE TABLE IF NOT EXISTS state (
|
connection.execute(
|
||||||
last_run DATETIME DEFAULT NULL,
|
"CREATE TABLE IF NOT EXISTS state (
|
||||||
last_sync DATETIME DEFAULT NULL
|
last_run DATETIME DEFAULT NULL,
|
||||||
);",
|
last_sync DATETIME DEFAULT NULL
|
||||||
[],
|
);",
|
||||||
)?;
|
[],
|
||||||
connection.execute(
|
)?;
|
||||||
"CREATE TABLE IF NOT EXISTS auth (
|
connection.execute(
|
||||||
key TEXT PRIMARY KEY
|
"CREATE TABLE IF NOT EXISTS auth (
|
||||||
);",
|
key TEXT PRIMARY KEY,
|
||||||
[],
|
hash TEXT NOT NULL
|
||||||
)?;
|
);",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(SQLiteStorage { conn: connection });
|
return Ok(SQLiteStorage { conn: connection });
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthStorage for SQLiteStorage {
|
impl AuthStorage for SQLiteStorage {
|
||||||
|
fn get_hash(&self) -> Result<String, Error> {
|
||||||
|
return Ok(self.conn.query_row(
|
||||||
|
"SELECT * FROM auth", [],
|
||||||
|
|row| {
|
||||||
|
return Ok(row.get(1)?);
|
||||||
|
},
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
|
||||||
fn get_key(&self) -> Result<String, Error> {
|
fn get_key(&self) -> Result<String, Error> {
|
||||||
return Ok(self.conn.query_row(
|
return Ok(self.conn.query_row(
|
||||||
"SELECT * FROM auth", [],
|
"SELECT * FROM auth", [],
|
||||||
|
@ -107,10 +126,13 @@ impl AuthStorage for SQLiteStorage {
|
||||||
|
|
||||||
fn set_key(&self, key: &str) -> Result<Option<String>, Error> {
|
fn set_key(&self, key: &str) -> Result<Option<String>, Error> {
|
||||||
let old_key = self.get_key().ok();
|
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("DELETE FROM auth;", [])?;
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
"INSERT INTO auth VALUES (?);",
|
"INSERT INTO auth (key, hash) VALUES (?, ?);",
|
||||||
params![key]
|
params![key, hash]
|
||||||
)?;
|
)?;
|
||||||
return Ok(old_key);
|
return Ok(old_key);
|
||||||
}
|
}
|
||||||
|
@ -163,9 +185,10 @@ impl MemoStorage for SQLiteStorage {
|
||||||
|
|
||||||
while let Some(row) = rows.next()? {
|
while let Some(row) = rows.next()? {
|
||||||
results.push(Memo {
|
results.push(Memo {
|
||||||
id: row.get(0)?,
|
id: Uuid::parse_str(row.get::<usize, String>(0)?.as_str()).unwrap(),
|
||||||
body: row.get(1)?,
|
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()? {
|
while let Some(row) = rows.next()? {
|
||||||
results.push(Memo {
|
results.push(Memo {
|
||||||
id: row.get(0)?,
|
id: Uuid::parse_str(row.get::<usize, String>(0)?.as_str()).unwrap(),
|
||||||
body: row.get(1)?,
|
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<DateTime<Utc>>) -> Result<(), Error> {
|
fn add(&self, body: &str, due: Option<DateTime<Utc>>) -> Result<(), Error> {
|
||||||
// TODO join these 2 ifs?
|
self.insert(&Memo{
|
||||||
if due.is_some() {
|
id: Uuid::new_v4(),
|
||||||
self.conn.execute(
|
body: body.to_string(),
|
||||||
"INSERT INTO memo (body, due) VALUES (?, ?)",
|
due: due,
|
||||||
params![body, due],
|
done: None
|
||||||
)?;
|
})
|
||||||
} else {
|
}
|
||||||
self.conn
|
|
||||||
.execute("INSERT INTO memo (body) VALUES (?)", params![body])?;
|
fn insert(&self, m: &Memo) -> Result<(), Error> {
|
||||||
}
|
self.conn.execute(
|
||||||
return Ok(());
|
"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<bool, Error> {
|
fn set(&self, memo: &Memo) -> Result<bool, Error> {
|
||||||
let count = self.conn.execute(
|
let count = self.conn.execute(
|
||||||
"UPDATE memo SET body = ?, due = ? WHERE id = ?",
|
"UPDATE memo SET body = ?, due = ?, done = ? WHERE id = ?",
|
||||||
params![memo.body, memo.due, memo.id],
|
params![memo.body, memo.due, memo.done, memo.id.to_string()],
|
||||||
)?;
|
)?;
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
|
@ -218,10 +245,10 @@ impl MemoStorage for SQLiteStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn del(&self, id: u32) -> Result<bool, Error> {
|
fn del(&self, id: &Uuid) -> Result<bool, Error> {
|
||||||
let count = self
|
let count = self
|
||||||
.conn
|
.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 {
|
if count > 0 {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
} else {
|
} else {
|
||||||
|
@ -229,15 +256,16 @@ impl MemoStorage for SQLiteStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self, id: u32) -> Result<Memo, Error> {
|
fn get(&self, id: &Uuid) -> Result<Memo, Error> {
|
||||||
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 = ? AND done = 0",
|
||||||
params![id],
|
params![id.to_string()],
|
||||||
|row| {
|
|row| {
|
||||||
return Ok(Memo {
|
return Ok(Memo {
|
||||||
id: row.get(0)?,
|
id: Uuid::parse_str(row.get::<usize, String>(0)?.as_str()).unwrap(),
|
||||||
body: row.get(1)?,
|
body: row.get(1)?,
|
||||||
due: row.get(2).unwrap_or(None),
|
due: row.get(2).ok(),
|
||||||
|
done: row.get(3).ok(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
)?);
|
)?);
|
||||||
|
|
Loading…
Reference in a new issue