Skip to main content

http_cache/managers/
cacache.rs

1use std::path::PathBuf;
2
3use crate::{CacheManager, HttpResponse, Result};
4
5use http_cache_semantics::CachePolicy;
6use serde::{Deserialize, Serialize};
7
8/// Implements [`CacheManager`] with [`cacache`](https://github.com/zkat/cacache-rs) as the backend.
9#[derive(Clone)]
10pub struct CACacheManager {
11    /// Directory where the cache will be stored.
12    pub path: PathBuf,
13    /// Options for removing cache entries.
14    pub remove_opts: cacache::RemoveOpts,
15}
16
17impl std::fmt::Debug for CACacheManager {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        f.debug_struct("CACacheManager").field("path", &self.path).finish()
20    }
21}
22
23// Modern store format (postcard) - includes metadata field
24#[cfg(feature = "postcard")]
25#[derive(Debug, Deserialize, Serialize)]
26struct Store {
27    response: HttpResponse,
28    policy: CachePolicy,
29}
30
31// Legacy store format (bincode) - HttpResponse without metadata field
32// The metadata field was added alongside postcard, so bincode cache
33// data will never contain it.
34#[cfg(all(feature = "bincode", not(feature = "postcard")))]
35#[derive(Debug, Deserialize, Serialize)]
36struct Store {
37    response: LegacyHttpResponse,
38    policy: CachePolicy,
39}
40
41#[cfg(all(feature = "bincode", not(feature = "postcard")))]
42use crate::{HttpHeaders, HttpVersion, Url};
43
44#[cfg(all(feature = "bincode", not(feature = "postcard")))]
45#[derive(Debug, Clone, Deserialize, Serialize)]
46struct LegacyHttpResponse {
47    body: Vec<u8>,
48    #[cfg(feature = "http-headers-compat")]
49    headers: std::collections::HashMap<String, String>,
50    #[cfg(not(feature = "http-headers-compat"))]
51    headers: std::collections::HashMap<String, Vec<String>>,
52    status: u16,
53    url: Url,
54    version: HttpVersion,
55}
56
57#[cfg(all(feature = "bincode", not(feature = "postcard")))]
58impl From<LegacyHttpResponse> for HttpResponse {
59    fn from(legacy: LegacyHttpResponse) -> Self {
60        #[cfg(feature = "http-headers-compat")]
61        let headers = HttpHeaders::Legacy(legacy.headers);
62        #[cfg(not(feature = "http-headers-compat"))]
63        let headers = HttpHeaders::Modern(legacy.headers);
64
65        HttpResponse {
66            body: legacy.body,
67            headers,
68            status: legacy.status,
69            url: legacy.url,
70            version: legacy.version,
71            metadata: None,
72        }
73    }
74}
75
76#[cfg(all(feature = "bincode", not(feature = "postcard")))]
77impl From<HttpResponse> for LegacyHttpResponse {
78    fn from(response: HttpResponse) -> Self {
79        #[cfg(feature = "http-headers-compat")]
80        let headers = match response.headers {
81            HttpHeaders::Legacy(h) => h,
82            HttpHeaders::Modern(h) => {
83                h.into_iter().map(|(k, v)| (k, v.join(", "))).collect()
84            }
85        };
86        #[cfg(not(feature = "http-headers-compat"))]
87        let headers = match response.headers {
88            HttpHeaders::Modern(h) => h,
89        };
90
91        LegacyHttpResponse {
92            body: response.body,
93            headers,
94            status: response.status,
95            url: response.url,
96            version: response.version,
97        }
98    }
99}
100
101#[allow(dead_code)]
102impl CACacheManager {
103    /// Creates a new [`CACacheManager`] with the given path.
104    pub fn new(path: PathBuf, remove_fully: bool) -> Self {
105        Self {
106            path,
107            remove_opts: cacache::RemoveOpts::new().remove_fully(remove_fully),
108        }
109    }
110
111    /// Clears out the entire cache.
112    pub async fn clear(&self) -> Result<()> {
113        cacache::clear(&self.path).await?;
114        Ok(())
115    }
116}
117
118#[async_trait::async_trait]
119impl CacheManager for CACacheManager {
120    async fn get(
121        &self,
122        cache_key: &str,
123    ) -> Result<Option<(HttpResponse, CachePolicy)>> {
124        let store: Store = match cacache::read(&self.path, cache_key).await {
125            Ok(d) => {
126                #[cfg(feature = "postcard")]
127                {
128                    postcard::from_bytes(&d)?
129                }
130                #[cfg(all(feature = "bincode", not(feature = "postcard")))]
131                {
132                    bincode::deserialize(&d)?
133                }
134            }
135            Err(_e) => {
136                return Ok(None);
137            }
138        };
139
140        #[cfg(feature = "postcard")]
141        {
142            Ok(Some((store.response, store.policy)))
143        }
144        #[cfg(all(feature = "bincode", not(feature = "postcard")))]
145        {
146            Ok(Some((store.response.into(), store.policy)))
147        }
148    }
149
150    async fn put(
151        &self,
152        cache_key: String,
153        response: HttpResponse,
154        policy: CachePolicy,
155    ) -> Result<HttpResponse> {
156        #[cfg(feature = "postcard")]
157        let data = Store { response, policy };
158        #[cfg(all(feature = "bincode", not(feature = "postcard")))]
159        let data = Store { response: response.into(), policy };
160
161        #[cfg(feature = "postcard")]
162        let bytes = postcard::to_allocvec(&data)?;
163        #[cfg(all(feature = "bincode", not(feature = "postcard")))]
164        let bytes = bincode::serialize(&data)?;
165
166        cacache::write(&self.path, cache_key, bytes).await?;
167
168        #[cfg(feature = "postcard")]
169        {
170            Ok(data.response)
171        }
172        #[cfg(all(feature = "bincode", not(feature = "postcard")))]
173        {
174            Ok(data.response.into())
175        }
176    }
177
178    async fn delete(&self, cache_key: &str) -> Result<()> {
179        self.remove_opts.clone().remove(&self.path, cache_key).await?;
180        Ok(())
181    }
182}