From 34f6b42330a47e8d37fce24daccf3792f5c5e2cf Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 1 Mar 2026 19:16:50 +0100 Subject: [PATCH] [feat](trx-frontend-http): add BookmarkStore backed by pickledb Add pickledb and dirs dependencies. Introduce BookmarkStore wrapping PickleDb behind Arc> with list/get/insert/upsert/remove ops. Keys stored as "bm:{id}" in ~/.config/trx-rs/bookmarks.db; falls back to ./bookmarks.db when config dir is unavailable. Wire BookmarkStore into build_server() as actix-web app_data so all request handlers can share the store. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Stan Grams --- Cargo.lock | 12 +++ .../trx-frontend/trx-frontend-http/Cargo.toml | 2 + .../trx-frontend-http/src/bookmarks.rs | 94 +++++++++++++++++++ .../trx-frontend-http/src/server.rs | 6 ++ 4 files changed, 114 insertions(+) create mode 100644 src/trx-client/trx-frontend/trx-frontend-http/src/bookmarks.rs diff --git a/Cargo.lock b/Cargo.lock index 83ed796..b1c84be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1586,6 +1586,16 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pickledb" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c53a5ade47760e8cc4986bdc5e72daeffaaaee64cbc374f9cfe0a00c1cd87b1f" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2503,8 +2513,10 @@ dependencies = [ "actix-web", "actix-ws", "bytes", + "dirs", "futures-util", "hex", + "pickledb", "rand 0.8.5", "serde", "serde_json", diff --git a/src/trx-client/trx-frontend/trx-frontend-http/Cargo.toml b/src/trx-client/trx-frontend/trx-frontend-http/Cargo.toml index 1f7cacb..dc58bbc 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/Cargo.toml +++ b/src/trx-client/trx-frontend/trx-frontend-http/Cargo.toml @@ -23,3 +23,5 @@ futures-util = "0.3" bytes = "1" rand = "0.8" hex = "0.4" +pickledb = "0.5" +dirs = "6" diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/bookmarks.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/bookmarks.rs new file mode 100644 index 0000000..9aec2b2 --- /dev/null +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/bookmarks.rs @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use std::path::{Path, PathBuf}; +use std::sync::{Arc, RwLock}; + +use pickledb::{PickleDb, PickleDbDumpPolicy, SerializationMethod}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Bookmark { + pub id: String, + pub name: String, + pub freq_hz: u64, + pub mode: String, + pub bandwidth_hz: Option, + pub comment: String, + pub category: String, + pub decoders: Vec, +} + +pub struct BookmarkStore { + db: Arc>, +} + +impl BookmarkStore { + /// Open (or create) the bookmark store at `path`. + pub fn open(path: &Path) -> Self { + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + let db = if path.exists() { + PickleDb::load(path, PickleDbDumpPolicy::AutoDump, SerializationMethod::Json) + .unwrap_or_else(|_| { + PickleDb::new(path, PickleDbDumpPolicy::AutoDump, SerializationMethod::Json) + }) + } else { + PickleDb::new(path, PickleDbDumpPolicy::AutoDump, SerializationMethod::Json) + }; + Self { + db: Arc::new(RwLock::new(db)), + } + } + + /// Returns the platform default path: `~/.config/trx-rs/bookmarks.db`. + /// Falls back to `./bookmarks.db` when the config dir is unavailable. + pub fn default_path() -> PathBuf { + dirs::config_dir() + .map(|p| p.join("trx-rs").join("bookmarks.db")) + .unwrap_or_else(|| PathBuf::from("bookmarks.db")) + } + + pub fn list(&self) -> Vec { + let db = self.db.read().unwrap_or_else(|e| e.into_inner()); + db.iter() + .filter_map(|kv| { + if kv.get_key().starts_with("bm:") { + kv.get_value::() + } else { + None + } + }) + .collect() + } + + pub fn get(&self, id: &str) -> Option { + let db = self.db.read().unwrap_or_else(|e| e.into_inner()); + db.get::(&format!("bm:{id}")) + } + + /// Insert a new bookmark. Returns false if the DB write fails. + pub fn insert(&self, bm: &Bookmark) -> bool { + let mut db = self.db.write().unwrap_or_else(|e| e.into_inner()); + db.set(&format!("bm:{}", bm.id), bm).is_ok() + } + + /// Update an existing bookmark by id. Returns false if not found. + pub fn upsert(&self, id: &str, bm: &Bookmark) -> bool { + let mut db = self.db.write().unwrap_or_else(|e| e.into_inner()); + let key = format!("bm:{id}"); + if db.exists(&key) { + db.set(&key, bm).is_ok() + } else { + false + } + } + + /// Remove a bookmark by id. Returns false if not found. + pub fn remove(&self, id: &str) -> bool { + let mut db = self.db.write().unwrap_or_else(|e| e.into_inner()); + db.rem(&format!("bm:{id}")).unwrap_or(false) + } +} diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/server.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/server.rs index 3a3f707..1622628 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/server.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/server.rs @@ -8,6 +8,8 @@ mod api; pub mod audio; #[path = "auth.rs"] pub mod auth; +#[path = "bookmarks.rs"] +pub mod bookmarks; #[path = "status.rs"] pub mod status; @@ -82,6 +84,9 @@ fn build_server( let rig_tx = web::Data::new(rig_tx); let clients = web::Data::new(Arc::new(AtomicUsize::new(0))); + let bookmark_path = bookmarks::BookmarkStore::default_path(); + let bookmark_store = web::Data::new(Arc::new(bookmarks::BookmarkStore::open(&bookmark_path))); + // Extract auth config values before moving context let same_site = match context.http_auth_cookie_same_site.as_str() { "Strict" => SameSite::Strict, @@ -120,6 +125,7 @@ fn build_server( .app_data(clients.clone()) .app_data(context_data.clone()) .app_data(auth_state.clone()) + .app_data(bookmark_store.clone()) .wrap( DefaultHeaders::new() .add(("Referrer-Policy", "same-origin"))