mirror of
https://git.alemi.dev/memo-cli.git
synced 2024-11-25 06:44:49 +01:00
245 lines
5.7 KiB
Rust
245 lines
5.7 KiB
Rust
use chrono::{DateTime, Utc};
|
|
use rusqlite::{params, Connection, Error};
|
|
use std::fmt;
|
|
|
|
pub struct Memo {
|
|
pub id: u32,
|
|
pub body: String,
|
|
pub due: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
pub struct State {
|
|
pub last_run: DateTime<Utc>,
|
|
pub last_sync: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
impl fmt::Display for Memo {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let mut due_str = "null".to_string();
|
|
if self.due.is_some() {
|
|
due_str = self.due.unwrap().to_string();
|
|
}
|
|
return write!(
|
|
f,
|
|
"Memo(id={id}, body={body}, due={due})",
|
|
id = self.id,
|
|
body = self.body,
|
|
due = due_str,
|
|
);
|
|
}
|
|
}
|
|
|
|
pub trait MemoStorage {
|
|
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 del(&self, id: u32) -> Result<bool, Error>;
|
|
fn get(&self, id: u32) -> Result<Memo, Error>;
|
|
}
|
|
|
|
pub trait AuthStorage {
|
|
fn get_key(&self) -> Result<String, Error>;
|
|
fn set_key(&self, key:&str) -> Result<Option<String>, Error>;
|
|
}
|
|
|
|
pub trait StateStorage {
|
|
fn set_state(&self, state:State) -> Result<Option<State>, Error>;
|
|
fn get_state(&self) -> Result<State, Error>;
|
|
|
|
fn set_run_time(&self, time:DateTime<Utc>) -> 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
|
|
state.last_run = time;
|
|
self.set_state(state)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn set_sync_time(&self, time:DateTime<Utc>) -> Result<(), Error> {
|
|
let mut state = self.get_state()?;
|
|
state.last_sync = Some(time);
|
|
self.set_state(state)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// SQLiteStorage
|
|
|
|
pub struct SQLiteStorage {
|
|
conn: Connection,
|
|
}
|
|
|
|
pub fn open_sqlite_storage(path: &str) -> Result<SQLiteStorage, Error> {
|
|
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
|
|
);",
|
|
[],
|
|
)?;
|
|
return Ok(SQLiteStorage { conn: connection });
|
|
}
|
|
|
|
impl AuthStorage for SQLiteStorage {
|
|
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();
|
|
self.conn.execute("DELETE FROM auth;", [])?;
|
|
self.conn.execute(
|
|
"INSERT INTO auth VALUES (?);",
|
|
params![key]
|
|
)?;
|
|
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: row.get(0)?,
|
|
body: row.get(1)?,
|
|
due: row.get(2)?,
|
|
});
|
|
}
|
|
}
|
|
|
|
{
|
|
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: row.get(0)?,
|
|
body: row.get(1)?,
|
|
due: row.get(2)?,
|
|
});
|
|
}
|
|
}
|
|
|
|
return Ok(results);
|
|
}
|
|
|
|
fn add(&self, body: &str, due: Option<DateTime<Utc>>) -> 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(());
|
|
}
|
|
|
|
fn set(&self, memo: &Memo) -> Result<bool, Error> {
|
|
let count = self.conn.execute(
|
|
"UPDATE memo SET body = ?, due = ? WHERE id = ?",
|
|
params![memo.body, memo.due, memo.id],
|
|
)?;
|
|
if count > 0 {
|
|
return Ok(true);
|
|
} else {
|
|
return Ok(false);
|
|
}
|
|
}
|
|
|
|
fn del(&self, id: u32) -> Result<bool, Error> {
|
|
let count = self
|
|
.conn
|
|
.execute("UPDATE memo SET done = ? WHERE id = ?", params![Utc::now(), id])?;
|
|
if count > 0 {
|
|
return Ok(true);
|
|
} else {
|
|
return Ok(false);
|
|
}
|
|
}
|
|
|
|
fn get(&self, id: u32) -> Result<Memo, Error> {
|
|
return Ok(self.conn.query_row(
|
|
"SELECT * FROM memo WHERE id = ? AND done = 0",
|
|
params![id],
|
|
|row| {
|
|
return Ok(Memo {
|
|
id: row.get(0)?,
|
|
body: row.get(1)?,
|
|
due: row.get(2).unwrap_or(None),
|
|
});
|
|
},
|
|
)?);
|
|
}
|
|
}
|