[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:
Generated
+12
@@ -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",
|
||||
|
||||
@@ -23,3 +23,5 @@ futures-util = "0.3"
|
||||
bytes = "1"
|
||||
rand = "0.8"
|
||||
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;
|
||||
#[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"))
|
||||
|
||||
Reference in New Issue
Block a user