[feat](trx-frontend-http): add BookmarkStore backed by pickledb

Add pickledb and dirs dependencies. Introduce BookmarkStore wrapping
PickleDb behind Arc<RwLock<>> 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 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-01 19:16:50 +01:00
parent 31238098ca
commit 34f6b42330
4 changed files with 114 additions and 0 deletions
Generated
+12
View File
@@ -1586,6 +1586,16 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 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]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@@ -2503,8 +2513,10 @@ dependencies = [
"actix-web", "actix-web",
"actix-ws", "actix-ws",
"bytes", "bytes",
"dirs",
"futures-util", "futures-util",
"hex", "hex",
"pickledb",
"rand 0.8.5", "rand 0.8.5",
"serde", "serde",
"serde_json", "serde_json",
@@ -23,3 +23,5 @@ futures-util = "0.3"
bytes = "1" bytes = "1"
rand = "0.8" rand = "0.8"
hex = "0.4" hex = "0.4"
pickledb = "0.5"
dirs = "6"
@@ -0,0 +1,94 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// 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<u64>,
pub comment: String,
pub category: String,
pub decoders: Vec<String>,
}
pub struct BookmarkStore {
db: Arc<RwLock<PickleDb>>,
}
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<Bookmark> {
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::<Bookmark>()
} else {
None
}
})
.collect()
}
pub fn get(&self, id: &str) -> Option<Bookmark> {
let db = self.db.read().unwrap_or_else(|e| e.into_inner());
db.get::<Bookmark>(&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)
}
}
@@ -8,6 +8,8 @@ mod api;
pub mod audio; pub mod audio;
#[path = "auth.rs"] #[path = "auth.rs"]
pub mod auth; pub mod auth;
#[path = "bookmarks.rs"]
pub mod bookmarks;
#[path = "status.rs"] #[path = "status.rs"]
pub mod status; pub mod status;
@@ -82,6 +84,9 @@ fn build_server(
let rig_tx = web::Data::new(rig_tx); let rig_tx = web::Data::new(rig_tx);
let clients = web::Data::new(Arc::new(AtomicUsize::new(0))); 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 // Extract auth config values before moving context
let same_site = match context.http_auth_cookie_same_site.as_str() { let same_site = match context.http_auth_cookie_same_site.as_str() {
"Strict" => SameSite::Strict, "Strict" => SameSite::Strict,
@@ -120,6 +125,7 @@ fn build_server(
.app_data(clients.clone()) .app_data(clients.clone())
.app_data(context_data.clone()) .app_data(context_data.clone())
.app_data(auth_state.clone()) .app_data(auth_state.clone())
.app_data(bookmark_store.clone())
.wrap( .wrap(
DefaultHeaders::new() DefaultHeaders::new()
.add(("Referrer-Policy", "same-origin")) .add(("Referrer-Policy", "same-origin"))