diff --git a/plugins/http/src/commands.rs b/plugins/http/src/commands.rs index 18953121..e0aa9d3b 100644 --- a/plugins/http/src/commands.rs +++ b/plugins/http/src/commands.rs @@ -266,7 +266,7 @@ pub async fn fetch( #[cfg(feature = "cookies")] { - builder = builder.cookie_provider(state.cookies_jar.clone()); + builder = builder.cookie_provider(Arc::new(state.cookies_jar)); } let mut request = builder.build()?.request(method.clone(), url); diff --git a/plugins/http/src/lib.rs b/plugins/http/src/lib.rs index 921f7969..11331f99 100644 --- a/plugins/http/src/lib.rs +++ b/plugins/http/src/lib.rs @@ -4,8 +4,6 @@ //! Access the HTTP client written in Rust. -use std::io::Cursor; - pub use reqwest; use tauri::{ plugin::{Builder, TauriPlugin}, @@ -22,20 +20,17 @@ mod scope; pub(crate) struct Http { #[cfg(feature = "cookies")] - cookies_jar_path: std::path::PathBuf, - #[cfg(feature = "cookies")] - cookies_jar: std::sync::Arc, + cookies_jar: crate::reqwest_cookie_store::CookieStoreMutex, } pub fn init() -> TauriPlugin { Builder::::new("http") .setup(|app, _| { #[cfg(feature = "cookies")] - let (cookies_jar_path, cookies_jar) = { + let cookies_jar = { use crate::reqwest_cookie_store::*; use std::fs::File; use std::io::BufReader; - use std::sync::Arc; let cache_dir = app.path().app_cache_dir()?; std::fs::create_dir_all(&cache_dir)?; @@ -48,22 +43,18 @@ pub fn init() -> TauriPlugin { .open(&path)?; let reader = BufReader::new(file); - let store = CookieStoreMutex::load(reader) - .or_else(|_e| { - #[cfg(feature = "tracing")] - tracing::warn!( - "failed to load cookie store: {_e}, falling back to empty store" - ); - CookieStoreMutex::load(Cursor::new("[]".to_string())) - }) - .map_err(|e| e.to_string())?; - - (path, Arc::new(store)) + let store = CookieStoreMutex::load(path.clone(), reader).unwrap_or_else(|_e| { + #[cfg(feature = "tracing")] + tracing::warn!( + "failed to load cookie store: {_e}, falling back to empty store" + ); + CookieStoreMutex::new(path, Default::default()) + }); + + store }; let state = Http { - #[cfg(feature = "cookies")] - cookies_jar_path, #[cfg(feature = "cookies")] cookies_jar, }; @@ -75,14 +66,11 @@ pub fn init() -> TauriPlugin { .on_event(|app, event| { #[cfg(feature = "cookies")] if let tauri::RunEvent::Exit = event { - use std::fs::File; - use std::io::BufWriter; - let state = app.state::(); - if let Ok(file) = File::create(&state.cookies_jar_path) { - let mut writer = BufWriter::new(file); - let _ = state.cookies_jar.save(&mut writer); + if let Err(_e) = state.cookies_jar.save() { + #[cfg(feature = "tracing")] + tracing::error!("failed to save cookie jar: {_e}"); } } }) diff --git a/plugins/http/src/reqwest_cookie_store.rs b/plugins/http/src/reqwest_cookie_store.rs index dab24cfe..8ce39ce8 100644 --- a/plugins/http/src/reqwest_cookie_store.rs +++ b/plugins/http/src/reqwest_cookie_store.rs @@ -4,7 +4,12 @@ // taken from https://github.com/pfernie/reqwest_cookie_store/blob/2ec4afabcd55e24d3afe3f0626ee6dc97bed938d/src/lib.rs -use std::sync::{Mutex, MutexGuard, PoisonError}; +use std::{ + fs::File, + io::BufWriter, + path::PathBuf, + sync::{Arc, Mutex, MutexGuard, PoisonError}, +}; use cookie_store::{CookieStore, RawCookie, RawCookieParseError}; use reqwest::header::HeaderValue; @@ -41,47 +46,61 @@ fn cookies(cookie_store: &CookieStore, url: &url::Url) -> Option { /// A [`cookie_store::CookieStore`] wrapped internally by a [`std::sync::Mutex`], suitable for use in /// async/concurrent contexts. -#[derive(Debug, Serialize, Deserialize)] -pub struct CookieStoreMutex(Mutex); - -impl Default for CookieStoreMutex { - /// Create a new, empty [`CookieStoreMutex`] - fn default() -> Self { - CookieStoreMutex::new(CookieStore::default()) - } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CookieStoreMutex { + pub path: PathBuf, + store: Arc>, } impl CookieStoreMutex { /// Create a new [`CookieStoreMutex`] from an existing [`cookie_store::CookieStore`]. - pub const fn new(cookie_store: CookieStore) -> CookieStoreMutex { - CookieStoreMutex(Mutex::new(cookie_store)) + pub fn new(path: PathBuf, cookie_store: CookieStore) -> CookieStoreMutex { + CookieStoreMutex { + path, + store: Arc::new(Mutex::new(cookie_store)), + } } /// Lock and get a handle to the contained [`cookie_store::CookieStore`]. pub fn lock( &self, ) -> Result, PoisonError>> { - self.0.lock() + self.store.lock() } - pub fn load(reader: R) -> cookie_store::Result { - cookie_store::serde::load(reader, |c| serde_json::from_str(c)).map(CookieStoreMutex::new) + pub fn load( + path: PathBuf, + reader: R, + ) -> cookie_store::Result { + cookie_store::serde::load(reader, |c| serde_json::from_str(c)) + .map(|store| CookieStoreMutex::new(path, store)) } - pub fn save(&self, writer: &mut W) -> cookie_store::Result<()> { + pub fn save(&self) -> cookie_store::Result<()> { + let file = File::create(&self.path)?; + let mut writer = BufWriter::new(file); let store = self.lock().expect("poisoned cookie jar mutex"); - cookie_store::serde::save(&store, writer, serde_json::to_string) + cookie_store::serde::save(&store, &mut writer, serde_json::to_string) } } impl reqwest::cookie::CookieStore for CookieStoreMutex { fn set_cookies(&self, cookie_headers: &mut dyn Iterator, url: &url::Url) { - let mut store = self.0.lock().unwrap(); + let mut store = self.store.lock().unwrap(); set_cookies(&mut store, cookie_headers, url); + + // try to persist cookies immediately asynchronously + let cookies_jar = self.clone(); + tauri::async_runtime::spawn(async move { + if let Err(_e) = cookies_jar.save() { + #[cfg(feature = "tracing")] + tracing::error!("failed to save cookie jar: {_e}"); + } + }); } fn cookies(&self, url: &url::Url) -> Option { - let store = self.0.lock().unwrap(); + let store = self.store.lock().unwrap(); cookies(&store, url) } }