lychee_lib/types/cookies.rs
1use crate::{ErrorKind, Result};
2use log::info;
3use reqwest_cookie_store::{CookieStore as ReqwestCookieStore, CookieStoreMutex};
4use std::io::ErrorKind as IoErrorKind;
5use std::{path::PathBuf, sync::Arc};
6
7/// A wrapper around `reqwest_cookie_store::CookieStore`
8///
9/// We keep track of the file path of the cookie store and
10/// implement `PartialEq` to compare cookie jars by their path
11#[derive(Debug, Clone)]
12pub struct CookieJar {
13 pub(crate) path: PathBuf,
14 pub(crate) inner: Arc<CookieStoreMutex>,
15}
16
17impl CookieJar {
18 /// Load a cookie store from a file
19 ///
20 /// Currently only JSON files are supported
21 ///
22 /// # Errors
23 ///
24 /// This function will return an error if
25 /// - the file cannot be opened (except for `NotFound`) or
26 /// - if the file is not valid JSON in either new or legacy format
27 pub fn load(path: PathBuf) -> Result<Self> {
28 match std::fs::File::open(&path).map(std::io::BufReader::new) {
29 Ok(mut reader) => {
30 info!("Loading cookies from {}", path.display());
31
32 // Try loading with new format first, fall back to legacy format
33 #[allow(clippy::single_match_else)]
34 let store = match cookie_store::serde::json::load(&mut reader) {
35 Ok(store) => store,
36 Err(_) => {
37 // Reopen file for legacy format attempt
38 let reader = std::fs::File::open(&path).map(std::io::BufReader::new)?;
39 #[allow(deprecated)]
40 ReqwestCookieStore::load_json(reader).map_err(|e| {
41 ErrorKind::Cookies(format!("Failed to load cookies: {e}"))
42 })?
43 }
44 };
45
46 Ok(Self {
47 path,
48 inner: Arc::new(CookieStoreMutex::new(store)),
49 })
50 }
51 // Create a new cookie store if the file does not exist
52 Err(e) if e.kind() == IoErrorKind::NotFound => Ok(Self {
53 path,
54 inner: Arc::new(CookieStoreMutex::new(ReqwestCookieStore::default())),
55 }),
56 // Propagate other IO errors (like permission denied) to the caller
57 Err(e) => Err(e.into()),
58 }
59 }
60
61 /// Save the cookie store to file as JSON
62 /// This will overwrite the file, which was loaded if any
63 ///
64 /// # Errors
65 ///
66 /// This function will return an error if
67 /// - the cookie store is locked or
68 /// - the file cannot be opened or
69 /// - if the file cannot be written to or
70 /// - if the file cannot be serialized to JSON
71 pub fn save(&self) -> Result<()> {
72 info!("Saving cookies to {}", self.path.display());
73 // Create parent directories if they don't exist
74 if let Some(parent) = self.path.parent() {
75 std::fs::create_dir_all(parent)?;
76 }
77 let mut file = std::fs::File::create(&self.path)?;
78 let store = self
79 .inner
80 .lock()
81 .map_err(|e| ErrorKind::Cookies(format!("Failed to lock cookie store: {e}")))?;
82 cookie_store::serde::json::save(&store, &mut file)
83 .map_err(|e| ErrorKind::Cookies(format!("Failed to save cookies: {e}")))
84 }
85}
86
87impl std::ops::Deref for CookieJar {
88 type Target = Arc<CookieStoreMutex>;
89 fn deref(&self) -> &Self::Target {
90 &self.inner
91 }
92}
93
94impl PartialEq for CookieJar {
95 fn eq(&self, other: &Self) -> bool {
96 // Assume that the cookie jar is the same if the path is the same
97 // Comparing the cookie stores directly is not possible because the
98 // `CookieStore` struct does not implement `Eq`
99 self.path == other.path
100 }
101}