From 2a15ea46a460dcf798aafaea59a4f89da81af2a2 Mon Sep 17 00:00:00 2001 From: Phil Denhoff Date: Mon, 21 Sep 2026 23:56:51 -0700 Subject: [PATCH] feat(opds): stop sharing when the active library changes Opening or creating a library while sharing is active now stops sharing. The running server reads live state, so without this a switch would silently start serving the new library - a privacy surprise, and the books a reader has cached would no longer match their ids. --- crates/citadel-opds/src/credential_store.rs | 24 ++++----------------- crates/citadel-opds/src/password.rs | 3 +-- src-tauri/src/libs/calibre/mod.rs | 14 ++++++++++-- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/crates/citadel-opds/src/credential_store.rs b/crates/citadel-opds/src/credential_store.rs index df871879..b1a7b1ed 100644 --- a/crates/citadel-opds/src/credential_store.rs +++ b/crates/citadel-opds/src/credential_store.rs @@ -82,11 +82,7 @@ impl OpdsCredentialStore { /// Disk is the source of truth; memory mirrors the last successful disk /// operation. pub fn status(&self) -> OpdsCredentialStatus { - let credentials = self - .inner - .credentials - .read() - .expect("OPDS credential store poisoned"); + let credentials = self.read(); OpdsCredentialStatus { configured: credentials.is_some(), username: credentials @@ -96,22 +92,14 @@ impl OpdsCredentialStore { } pub fn get(&self) -> Option { - self.inner - .credentials - .read() - .expect("OPDS credential store poisoned") - .clone() + self.read().clone() } pub fn set(&self, credentials: StoredOpdsCredentials) -> io::Result<()> { if let Some(path) = &self.inner.path { persist(path, &credentials)?; } - *self - .inner - .credentials - .write() - .expect("OPDS credential store poisoned") = Some(credentials); + *self.write() = Some(credentials); Ok(()) } @@ -123,11 +111,7 @@ impl OpdsCredentialStore { Err(error) => return Err(error), } } - *self - .inner - .credentials - .write() - .expect("OPDS credential store poisoned") = None; + *self.write() = None; Ok(()) } } diff --git a/crates/citadel-opds/src/password.rs b/crates/citadel-opds/src/password.rs index a953e977..ccbc3e62 100644 --- a/crates/citadel-opds/src/password.rs +++ b/crates/citadel-opds/src/password.rs @@ -1,8 +1,7 @@ //! The generated-password chunk shape and algorithm. use super::words::WORD_POOL; -use rand_core::{OsRng, RngCore}; -use serde::{Deserialize, Serialize}; +use serde::Serialize; /// Returned once when credentials are generated; the plaintext is never /// stored, so this is the only chance to copy it into a reader. diff --git a/src-tauri/src/libs/calibre/mod.rs b/src-tauri/src/libs/calibre/mod.rs index c392fb88..880f81f6 100644 --- a/src-tauri/src/libs/calibre/mod.rs +++ b/src-tauri/src/libs/calibre/mod.rs @@ -26,13 +26,23 @@ pub struct CalibreClientConfig { #[tauri::command] #[specta::specta] -pub fn init_client( +pub async fn init_client( handle: tauri::AppHandle, - state: tauri::State, + state: tauri::State<'_, CitadelState>, library_path: String, ) -> Result { use tauri::Manager; + // Stop BEFORE the swap: the running server reads live state, so draining + // after the swap would serve the new library under the old share. If the + // new library then fails to open, sharing stays off - the right failure + // direction (better a dead share than a wrong one). Re-opening the + // already-active library keeps sharing. + let same_library = state.get_library_path().as_deref() == Some(library_path.as_str()); + if !same_library { + handle.state::().stop().await; + } + state.init_library(library_path.clone())?; // Full-resolution covers are served over the asset protocol straight from