mirror of
https://git.alemi.dev/memo-cli.git
synced 2024-11-22 10:14:51 +01:00
modularized storage, implemented basic json backend
This commit is contained in:
parent
212d41221e
commit
c085d9183e
7 changed files with 532 additions and 311 deletions
|
@ -12,13 +12,16 @@ edition = "2021"
|
||||||
regex = "1.5.5"
|
regex = "1.5.5"
|
||||||
sha2 = "0.10.2"
|
sha2 = "0.10.2"
|
||||||
base64 = "0.13.0"
|
base64 = "0.13.0"
|
||||||
chrono = "0.4.19"
|
|
||||||
colored = "2.0.0"
|
colored = "2.0.0"
|
||||||
rpassword = "6.0.1"
|
rpassword = "6.0.1"
|
||||||
|
tempfile = "3.3.0"
|
||||||
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
|
||||||
notify-rust = "4"
|
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"] }
|
clap = { version = "3.1.6", features = ["derive"] }
|
||||||
rusqlite = { version="0.27.0", features=["bundled-sqlcipher", "chrono", "backup"] }
|
rusqlite = { version="0.27.0", features=["bundled-sqlcipher", "chrono", "backup"] }
|
||||||
dirs = "4.0.0" # cross-platform paths
|
dirs = "4.0.0" # cross-platform paths
|
||||||
|
|
51
src/main.rs
51
src/main.rs
|
@ -1,17 +1,17 @@
|
||||||
|
mod remote;
|
||||||
mod storage;
|
mod storage;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod remote;
|
|
||||||
|
|
||||||
use chrono::{DateTime, Local, Utc};
|
use chrono::{DateTime, Local, Utc};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use const_format::concatcp;
|
use const_format::concatcp;
|
||||||
use git_version::git_version;
|
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 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 GIT_VERSION: &str = git_version!();
|
||||||
const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
|
const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
@ -26,13 +26,15 @@ 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 = "name of db file")]
|
||||||
|
//name: Option<String>,
|
||||||
#[clap(short, long, help = "show memos in a notification")]
|
#[clap(short, long, help = "show memos in a notification")]
|
||||||
notify: bool,
|
notify: bool,
|
||||||
#[clap(long, help = "show completed tasks")]
|
#[clap(long, help = "show completed tasks")]
|
||||||
old: bool,
|
old: bool,
|
||||||
#[clap(short, long, help = "synchronize memo db")]
|
#[clap(short, long, help = "synchronize memo db")]
|
||||||
sync: bool,
|
sync: bool,
|
||||||
#[clap(short, long, help = "location for database file")]
|
#[clap(short, long, help = "specify location for database file")]
|
||||||
path: Option<String>,
|
path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,18 +77,24 @@ fn main() {
|
||||||
db_path = std::path::PathBuf::from(db);
|
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 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.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() {
|
if res.is_ok() {
|
||||||
println!("[v] downloaded remote db");
|
println!("[v] downloaded remote db");
|
||||||
} else {
|
} 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 mut builder = String::new();
|
||||||
let timing = if let Some(state) = storage.get_state().ok() {
|
let timing = if let Some(state) = storage.get_state().ok() {
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
format!("last run: {}", state.last_run.with_timezone(&now.timezone()).format("%a %d/%m %H:%M"))
|
format!(
|
||||||
} else { Local::now().format("%a %d/%m, %H:%M").to_string() };
|
"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 {
|
if args.old {
|
||||||
builder.push_str("Archived memos:\n");
|
builder.push_str("Archived memos:\n");
|
||||||
}
|
}
|
||||||
|
@ -183,19 +199,26 @@ fn main() {
|
||||||
print!("{}", builder);
|
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();
|
storage.set_run_time(Utc::now()).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.sync {
|
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() {
|
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();
|
||||||
} else {
|
} else {
|
||||||
println!("[!] could not upload db : {}", res.err().unwrap().to_string());
|
println!(
|
||||||
|
"[!] could not upload db : {}",
|
||||||
|
res.err().unwrap().to_string()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
112
src/remote.rs
112
src/remote.rs
|
@ -1,19 +1,42 @@
|
||||||
use crate::storage::{Memo, SQLiteStorage, MemoStorage, AuthStorage, open_sqlite_storage};
|
use crate::storage::{MemoError, MemoStorage, AuthStorage};
|
||||||
use std::fs;
|
|
||||||
use std::io::{Write, Read};
|
|
||||||
use rusqlite::params;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
pub trait RemoteSync : AuthStorage+MemoStorage {
|
pub trait RemoteSync : AuthStorage+MemoStorage+Sized {
|
||||||
fn store(&self, hash:&str, server:&str) -> Result<(), ureq::Error>;
|
fn serialize(&self) -> Result<Vec<u8>, MemoError>;
|
||||||
fn fetch(&self, hash:&str, server:&str) -> Result<(), ureq::Error>;
|
fn deserialize(data: Vec<u8>) -> Result<Self, MemoError>;
|
||||||
fn merge(&self, other:&Self) -> Result<(), rusqlite::Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RemoteSync for SQLiteStorage {
|
fn upload(&self, hash: &str, server: &str) -> Result<u16, MemoError> {
|
||||||
|
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<usize, MemoError> {
|
||||||
|
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<u8> = 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<usize, MemoError> { // TODO don't use error specific to rusqlite
|
||||||
|
let mut count = 0;
|
||||||
let mut memo_ids : HashSet<Uuid> = HashSet::new();
|
let mut memo_ids : HashSet<Uuid> = HashSet::new();
|
||||||
for memo in self.all(false)?.iter() {
|
for memo in self.all(false)?.iter() {
|
||||||
memo_ids.insert(memo.id);
|
memo_ids.insert(memo.id);
|
||||||
|
@ -21,67 +44,16 @@ impl RemoteSync for SQLiteStorage {
|
||||||
for memo in self.all(true)?.iter() {
|
for memo in self.all(true)?.iter() {
|
||||||
memo_ids.insert(memo.id);
|
memo_ids.insert(memo.id);
|
||||||
}
|
}
|
||||||
let mut stmt = other.conn.prepare("SELECT * FROM memo")?;
|
|
||||||
let r = stmt.query_map([], |row| {
|
for memo in other.all(true)?.iter() {
|
||||||
let id = Uuid::parse_str(row.get::<usize,String>(0)?.as_str()).unwrap();
|
if memo_ids.contains(&memo.id) {
|
||||||
let m = Memo{
|
self.set(memo)?; // TODO only replace if more recently edited!!
|
||||||
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 {
|
} else {
|
||||||
self.insert(&m)?;
|
self.insert(memo.clone())?;
|
||||||
|
count += 1;
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
})?.count();
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
Ok(count)
|
||||||
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<u8> = 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(());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
296
src/storage.rs
296
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 chrono::{DateTime, Utc};
|
||||||
use rusqlite::{params, Connection, Error};
|
use std::error::Error;
|
||||||
|
use std::collections::LinkedList;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use sha2::{Sha512, Digest};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Memo {
|
pub struct Memo {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub body: String,
|
pub body: String,
|
||||||
|
@ -22,252 +30,94 @@ impl fmt::Display for Memo {
|
||||||
if self.due.is_some() {
|
if self.due.is_some() {
|
||||||
due_str = self.due.unwrap().to_string();
|
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!(
|
return write!(
|
||||||
f,
|
f,
|
||||||
"Memo(id={id}, body={body}, due={due})",
|
"Memo(id={id}, body={body}, due={due}, done={done})",
|
||||||
id = self.id,
|
id = self.id,
|
||||||
body = self.body,
|
body = self.body,
|
||||||
due = due_str,
|
due = due_str,
|
||||||
|
done = done_str
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MemoError {
|
||||||
|
pub cause: &'static str,
|
||||||
|
pub sqlite: Option<rusqlite::Error>,
|
||||||
|
pub io: Option<std::io::Error>,
|
||||||
|
pub ureq: Option<ureq::Error>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<rusqlite::Error> for MemoError {
|
||||||
|
fn from(e: rusqlite::Error) -> Self { Self{cause: "sqlite", sqlite:Some(e), io:None, ureq:None} }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for MemoError {
|
||||||
|
fn from(e: std::io::Error) -> Self { Self{cause: "io", sqlite:None, io:Some(e), ureq:None} }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ureq::Error> for MemoError {
|
||||||
|
fn from(e: ureq::Error) -> Self { Self{cause: "ureq", sqlite:None, io:None, ureq:Some(e)} }
|
||||||
|
}
|
||||||
|
|
||||||
pub trait MemoStorage {
|
pub trait MemoStorage {
|
||||||
fn all(&self, done: bool) -> Result<Vec<Memo>, Error>;
|
fn all(&self, done: bool) -> Result<LinkedList<Memo>, MemoError>;
|
||||||
fn set(&self, memo: &Memo) -> Result<bool, Error>;
|
fn get(&self, id: &Uuid) -> Result<Memo, MemoError>;
|
||||||
fn del(&self, id: &Uuid) -> Result<bool, Error>;
|
fn set(&mut self, memo: &Memo) -> Result<usize, MemoError>;
|
||||||
fn get(&self, id: &Uuid) -> Result<Memo, Error>;
|
fn del(&mut self, id: &Uuid) -> Result<usize, MemoError>;
|
||||||
fn add(&self, body: &str, due: Option<DateTime<Utc>>) -> Result<(), Error>;
|
fn insert(&mut self, m: Memo) -> Result<(), MemoError>;
|
||||||
fn insert(&self, m: &Memo) -> Result<(), Error>;
|
|
||||||
|
fn add(&mut self, body: &str, due: Option<DateTime<Utc>>) -> Result<(), MemoError> {
|
||||||
|
self.insert(Memo {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
body: body.to_string(),
|
||||||
|
due: due,
|
||||||
|
done: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait AuthStorage {
|
pub trait AuthStorage {
|
||||||
fn get_hash(&self) -> Result<String, Error>;
|
fn get_hash(&self) -> Result<String, MemoError>;
|
||||||
fn get_key(&self) -> Result<String, Error>;
|
fn get_key(&self) -> Result<String, MemoError>;
|
||||||
fn set_key(&self, key:&str) -> Result<Option<String>, Error>;
|
fn set_key(&mut self, key: &str) -> Result<Option<String>, MemoError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait StateStorage {
|
pub trait StateStorage {
|
||||||
fn set_state(&self, state:State) -> Result<Option<State>, Error>;
|
fn set_state(&mut self, state: State) -> Result<Option<State>, MemoError>;
|
||||||
fn get_state(&self) -> Result<State, Error>;
|
fn get_state(&self) -> Result<State, MemoError>;
|
||||||
|
|
||||||
fn set_run_time(&self, time:DateTime<Utc>) -> Result<(), Error> {
|
fn set_run_time(&mut self, time: DateTime<Utc>) -> 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
|
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;
|
state.last_run = time;
|
||||||
self.set_state(state)?;
|
self.set_state(state)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_sync_time(&self, time:DateTime<Utc>) -> Result<(), Error> {
|
fn set_sync_time(&mut self, time: DateTime<Utc>) -> Result<(), MemoError> {
|
||||||
let mut state = self.get_state()?;
|
let mut state = self.get_state()?;
|
||||||
state.last_sync = Some(time);
|
state.last_sync = Some(time);
|
||||||
self.set_state(state)?;
|
self.set_state(state)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SQLiteStorage
|
|
||||||
|
|
||||||
pub struct SQLiteStorage {
|
|
||||||
pub conn: Connection, // TODO make back private
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn open_sqlite_storage(path: &str, init: bool) -> Result<SQLiteStorage, Error> {
|
|
||||||
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<String, Error> {
|
|
||||||
return Ok(self.conn.query_row(
|
|
||||||
"SELECT * FROM auth", [],
|
|
||||||
|row| {
|
|
||||||
return Ok(row.get(1)?);
|
|
||||||
},
|
|
||||||
)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_key(&self) -> Result<String, Error> {
|
|
||||||
return Ok(self.conn.query_row(
|
|
||||||
"SELECT * FROM auth", [],
|
|
||||||
|row| {
|
|
||||||
return Ok(row.get(0)?);
|
|
||||||
},
|
|
||||||
)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_key(&self, key: &str) -> Result<Option<String>, 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<State, Error> {
|
|
||||||
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<Option<State>, 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<Vec<Memo>, 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::<usize, String>(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::<usize, String>(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<DateTime<Utc>>) -> 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<bool, Error> {
|
|
||||||
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<bool, Error> {
|
|
||||||
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<Memo, Error> {
|
|
||||||
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::<usize, String>(0)?.as_str()).unwrap(),
|
|
||||||
body: row.get(1)?,
|
|
||||||
due: row.get(2).ok(),
|
|
||||||
done: row.get(3).ok(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
)?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
180
src/storage/json.rs
Normal file
180
src/storage/json.rs
Normal file
|
@ -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<DateTime<Utc>>,
|
||||||
|
last_run: DateTime<Utc>,
|
||||||
|
memo: Vec<Memo>,
|
||||||
|
archive: Vec<Memo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<JsonStorage, std::io::Error> {
|
||||||
|
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<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StateStorage for JsonStorage {
|
||||||
|
fn get_state(&self) -> Result<State, MemoError> {
|
||||||
|
return Ok(
|
||||||
|
State {
|
||||||
|
last_run: self.data.last_run,
|
||||||
|
last_sync: self.data.last_sync,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_state(&mut self, state: State) -> Result<Option<State>, 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<LinkedList<Memo>, MemoError> {
|
||||||
|
let mut results_due : LinkedList<Memo> = LinkedList::new();
|
||||||
|
let mut results_not_due : LinkedList<Memo> = 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<usize, MemoError> {
|
||||||
|
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.remove(index as usize);
|
||||||
|
self.save()?;
|
||||||
|
}
|
||||||
|
return Ok(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, id: &uuid::Uuid) -> Result<Memo, MemoError> {
|
||||||
|
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<Vec<u8>, MemoError> {
|
||||||
|
return Ok(std::fs::read(&self.path).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize(data: Vec<u8>) -> Result<Self, MemoError> {
|
||||||
|
return Ok(JsonStorage{
|
||||||
|
path: "".to_string(),
|
||||||
|
data: serde_json::from_slice(data.as_slice()).unwrap(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
192
src/storage/sqlite.rs
Normal file
192
src/storage/sqlite.rs
Normal file
|
@ -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<NamedTempFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SQLiteStorage {
|
||||||
|
pub fn new(path: &str, init: bool) -> Result<SQLiteStorage, MemoError> {
|
||||||
|
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<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> {
|
||||||
|
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(State {
|
||||||
|
last_run: row.get(0)?,
|
||||||
|
last_sync: row.get(1).ok(),
|
||||||
|
});
|
||||||
|
})?);
|
||||||
|
}
|
||||||
|
|
||||||
|
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_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<LinkedList<Memo>, MemoError> {
|
||||||
|
let mut results_due : LinkedList<Memo> = LinkedList::new();
|
||||||
|
let mut results_not_due : LinkedList<Memo> = 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::<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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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<usize, MemoError> {
|
||||||
|
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<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",
|
||||||
|
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(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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().to_str().unwrap();
|
||||||
|
return Ok(SQLiteStorage{
|
||||||
|
conn: Connection::open(path).unwrap(),
|
||||||
|
path: path.to_string(),
|
||||||
|
tmpfile: Some(tmpfile),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ use crate::storage::Memo;
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use regex::{Error, Regex};
|
use regex::{Error, Regex};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
|
use std::collections::LinkedList;
|
||||||
|
|
||||||
pub trait HumanDisplay {
|
pub trait HumanDisplay {
|
||||||
fn human(&self) -> String;
|
fn human(&self) -> String;
|
||||||
|
@ -91,7 +92,7 @@ pub fn parse_human_duration(input: &str) -> Result<Duration, Error> {
|
||||||
return Ok(Duration::seconds(secs));
|
return Ok(Duration::seconds(secs));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_regex(re: Regex, memos: Vec<Memo>) -> Option<Memo> {
|
pub fn find_by_regex(re: Regex, memos: LinkedList<Memo>) -> Option<Memo> {
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
let mut out: Option<Memo> = None;
|
let mut out: Option<Memo> = None;
|
||||||
for memo in memos {
|
for memo in memos {
|
||||||
|
|
Loading…
Reference in a new issue