Clean up key/value store, add convenience wrapper.

This commit is contained in:
nobody 2024-07-27 15:16:44 +02:00 committed by murmeldin
parent 08878169bc
commit 19be226f11
2 changed files with 116 additions and 79 deletions

View file

@ -1,12 +1,23 @@
use rusqlite::{params, Connection, Result};
// use std::collections::HashMap;
use rusqlite::{Connection, Result, params};
use std::ops::Index;
use std::cell::Cell;
use std::collections::HashSet;
/// Simple SQLite-backed key/value store.
///
/// All failing writes will log a message, any errors will set `has_errors`.
///
/// There's `Index` support for convenience (`&kvstore["foo"]`),
/// but **it will leak data.**
/// (That's perfectly fine when used for getting data once for the duration of
/// the program, just use `.get()` for stuff that changes.)
pub struct KeyValueStore {
conn: Connection,
has_write_errors: Cell<bool>,
}
impl KeyValueStore {
// Initialize the database and create the table if it doesn't exist
/// Open or create a DB with a `kv_store` table storing all tuples.
pub fn new(db_path: &str) -> Result<Self> {
let conn = Connection::open(db_path)?;
conn.execute(
@ -16,73 +27,95 @@ impl KeyValueStore {
)",
[],
)?;
Ok(KeyValueStore { conn })
Ok(Self { conn, has_write_errors: Cell::new(false), })
}
// Insert or update a key-value pair
pub fn set(&self, key: &str, value: &str) -> Result<()> {
self.conn.execute(
"INSERT INTO kv_store (key, value) VALUES (?1, ?2)
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
params![key, value],
)?;
Ok(())
/// Report if any write errors occurred.
pub fn has_errors(&self) -> bool {
self.has_write_errors.get()
}
/// Get value at `key`. (Use `.unwrap_or(default)` & friends to work with the Result.)
pub fn get(&self, key: &str) -> Result<String> {
let mut stmt = self.conn.prepare("SELECT value FROM kv_store WHERE key = ?1")?;
let mut rows = stmt.query(params![key])?;
if let Some(row) = rows.next()? {
let result = row.get(0)?;
Ok(result)
} else {
Err(rusqlite::Error::QueryReturnedNoRows)
}
}
/// If `key` is absent from the DB, write `key`/`value` pair (else leave unchanged).
///
/// Will panic on error. (The idea is to call this a bunch of times at boot,
/// to populate the DB if missing / newly created, and not use it after.)
pub fn default(&self, key: &str, value: &str) {
self.conn.execute(
"INSERT INTO kv_store (key, value) VALUES (?1, ?2)
ON CONFLICT(key) DO NOTHING",
params![key, value],
).ok();
).expect(&format!("Failed to write default at key: {}", key));
}
// Retrieve a value by key
pub fn get(&self, key: &str) -> Result<Option<String>> {
let mut stmt = self.conn.prepare("SELECT value FROM kv_store WHERE key = ?1")?;
let mut rows = stmt.query(params![key])?;
if let Some(row) = rows.next()? {
let value: String = row.get(0)?;
Ok(Some(value))
} else {
Ok(None)
/// Write a `key`/`value` pair to the DB.
///
/// Errors log the failed write and set `has_errors`.
pub fn set(&self, key: &str, value: &str) -> Result<()> {
match self.conn.execute(
"INSERT INTO kv_store (key, value) VALUES (?1, ?2)
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
params![key, value],
) {
Ok(_) => Ok(()),
Err(e) => {
eprintln!("Failed DB write: ({}, {})", key, value);
self.has_write_errors.set(true);
Err(e)
}
}
}
// Delete a key-value pair
/// Delete the key-value pair associated with `key`.
///
/// Errors log the failed write and set `has_errors`.
pub fn delete(&self, key: &str) -> Result<()> {
self.conn.execute("DELETE FROM kv_store WHERE key = ?1", params![key])?;
match self.conn.execute("DELETE FROM kv_store WHERE key = ?1", params![key]) {
Ok(_) => Ok(()),
Err(e) => {
eprintln!("Failed DB write: ({}, NULL)", key);
self.has_write_errors.set(true);
Err(e)
}
}
}
/// Dump the current DB contents to stderr.
///
/// For all keys listed in `redacted_keys`, no value is printed, only the presence is reported.
pub fn dump_redacting(&self, redacted_keys: &[&str]) -> Result<()> {
let exclude_set: HashSet<&str> = redacted_keys.iter().cloned().collect();
let mut stmt = self.conn.prepare("SELECT key, value FROM kv_store")?;
let mut rows = stmt.query(params![])?;
while let Some(row) = rows.next()? {
let key: String = row.get(0)?;
if exclude_set.contains(key.as_str()) {
eprintln!("{} = REDACTED", key);
} else {
let value: String = row.get(1)?;
eprintln!("{} = {}", key, value);
}
}
Ok(())
}
}
impl Index<&str> for KeyValueStore {
type Output = str;
/*
fn main() -> Result<()> {
let kv_store = KeyValueStore::new("kv_store.db")?;
// Set some key-value pairs
kv_store.set("last_week_pad", "pad_id_123")?;
kv_store.set("email_id", "email_id_456")?;
// Retrieve and print values
if let Some(value) = kv_store.get("last_week_pad")? {
println!("Last week's pad ID: {}", value);
} else {
println!("Key not found");
fn index(&self, key: &str) -> &Self::Output {
Box::leak(Box::new(self.get(key).unwrap())).as_str()
}
if let Some(value) = kv_store.get("email_id")? {
println!("Email ID: {}", value);
} else {
println!("Key not found");
}
// Delete a key-value pair
kv_store.delete("last_week_pad")?;
Ok(())
}
*/

View file

@ -28,7 +28,7 @@ mod create_new_pads;
pub mod variables_and_settings;
use chrono::{Datelike, Local, NaiveDate, Weekday};
use regex::{Regex};
use regex::Regex;
use uuid::Uuid;
use reqwest::Client;
use std::error::Error;
@ -41,7 +41,6 @@ use pandoc;
// MAIL START
use lettre::{Message, SmtpTransport, Transport};
use lettre::message::{header, SinglePart};
use lettre::message::header::MessageId;
use lettre::transport::smtp::authentication::Credentials;
// MAIL END
@ -56,20 +55,25 @@ const PLENUM_TEMPLATE_URL: &str = variables_and_settings::PLENUM_TEMPLATE_URL;
const FALLBACK_TEMPLATE: &str = variables_and_settings::FALLBACK_TEMPLATE;
const TESTING_MODE: EMailOperationStates = EMailOperationStates::Test;
fn kv_defaults (kv: &KV) {
kv.default("template-url", "https://md.berlin.ccc.de/plenum-template");
kv.default("hedgedoc-server-url","https://md.berlin.ccc.de/");
kv.default("hedgedoc-template-name","plenum-template");
// hedgedoc-last-id, hedgedoc-next-id have no defaults
kv.default("email-server","mail.berlin.ccc.de");
kv.default("email-user","plenum-bot@berlin.ccc.de");
kv.default("email-name","Plenumsbot");
// add email-pass and email-to manually
// email-last-message-id has no default
kv.default("email-signature", "\n\n[Diese Nachricht wurde automatisiert vom Plenumsbot erstellt und ist daher ohne Unterschrift gültig.]");
}
#[tokio::main]
async fn main() {
// BEGIN ANKÜNDIGUNGSSCRIPT
async fn main() -> Result<(), Box<dyn Error>> {
// config
let config = KV::new("plenum_config.sqlite").unwrap();
kv_defaults(&config);
println!("[BEGINNING] Aktuelle Situation der Datenbank: \"current_pad_link\": https://md.berlin.ccc.de/{}, \"zukünftiges-plenumspad\" https://md.berlin.ccc.de/{}", config.get("aktuelles-plenumspad").unwrap().unwrap(), config.get("zukünftiges-plenumspad").unwrap_or_else(|error|Option::from(String::from("error"))).unwrap_or(String::from("error")));
let hedgedoc_server = &config["hedgedoc-server-url"];
println!("[START]\nAktueller Zustand der DB:");
config.dump_redacting(&["email-pass","matrix-pass"]).ok();
// Dienstage diesen Monat
let all_tuesdays: Vec<Option<NaiveDate>> = get_tuesdays(0);
@ -116,10 +120,10 @@ async fn main() {
let yesterday_was_plenum: bool = check_if_plenum(nächster_plenumtermin.clone(), yesterday);
// Pad-Links aus der Datenbank laden:
let pad_id: String = config.get("aktuelles-plenumspad").unwrap().unwrap();
let current_pad_link: String = format!("https://md.berlin.ccc.de/{}", pad_id);
let future_pad_id:String = config.get("zukünftiges-plenumspad").unwrap_or_else(|error|Option::from(String::from("error"))).unwrap_or(String::from("error"));
let future_pad_link: String = format!("https://md.berlin.ccc.de/{}", future_pad_id);
let pad_id = &config["aktuelles-plenumspad"];
let current_pad_link: String = format!("{}{}", hedgedoc_server, pad_id);
let future_pad_id = &config["zukünftiges-plenumspad"];
let future_pad_link: String = format!("{}{}", hedgedoc_server, future_pad_id);
let mut message_id: String = String::from("FEHLER"); // message id initialisieren
@ -183,10 +187,10 @@ async fn main() {
message_id = mail_versenden(message, betreff).expect("Mail mit Plenumsprotokoll wurde versucht zu senden, konnte aber nicht gesendet werden!");
pad_ins_wiki(old_pad_content_without_top_instructions);
}
println!("[END] Aktuelle Situation der Datenbank: \"current_pad_link\": https://md.berlin.ccc.de/{}, \"zukünftiges-plenumspad\" https://md.berlin.ccc.de/{}", config.get("aktuelles-plenumspad").unwrap().unwrap(), config.get("zukünftiges-plenumspad").unwrap_or_else(|error|Option::from(String::from("error"))).unwrap_or(String::from("error")));
println!("[ENDE]\nAktueller Zustand der DB:");
config.dump_redacting(&["email-pass","matrix-pass"]).ok();
if config.has_errors() { Err("There were errors.".into()) } else { Ok(()) }
}
@ -320,7 +324,7 @@ fn replace_placeholders(template: &str, übernächster_plenumtermin: String, üb
}
fn rotate (future_pad_id: &str, kv: &KV) {
let next_plenum_pad = kv.get("zukünftiges-plenumspad").unwrap_or_else(|error | None);
let next_plenum_pad = kv.get("zukünftiges-plenumspad").ok();
if next_plenum_pad == None {
kv.set("zukünftiges-plenumspad", &future_pad_id).expect("Fehler beim Beschreiben der Datenbank mit neuem Plenumslink!"); // Beispiel: aktuelles-plenumspad: Ok(Some("eCH24zXGS9S8Stg5xI3aRg"))
} else {