salvo_captcha/storage/
cacache_storage.rs1use std::{
13 path::{Path, PathBuf},
14 time::{Duration, SystemTime},
15};
16
17use crate::CaptchaStorage;
18
19#[derive(Debug, Clone)]
23pub struct CacacheStorage {
24 cache_dir: PathBuf,
26}
27
28impl CacacheStorage {
29 pub fn new(cache_dir: impl Into<PathBuf>) -> Self {
31 Self {
32 cache_dir: cache_dir.into(),
33 }
34 }
35
36 pub fn cache_dir(&self) -> &Path {
38 &self.cache_dir
39 }
40}
41
42impl CaptchaStorage for CacacheStorage {
43 type Error = cacache::Error;
44
45 async fn store_answer(&self, answer: String) -> Result<String, Self::Error> {
46 let token = uuid::Uuid::new_v4();
47 log::info!("Storing captcha answer to cacache for token: {token}");
48 cacache::write(&self.cache_dir, token.to_string(), answer.as_bytes()).await?;
49 Ok(token.to_string())
50 }
51
52 async fn get_answer(&self, token: &str) -> Result<Option<String>, Self::Error> {
53 log::info!("Getting captcha answer from cacache for token: {token}");
54 match cacache::read(&self.cache_dir, token).await {
55 Ok(answer) => {
56 log::info!("Captcha answer is exist in cacache for token: {token}");
57 Ok(Some(
58 String::from_utf8(answer)
59 .expect("All the stored captcha answer should be utf8"),
60 ))
61 }
62 Err(cacache::Error::EntryNotFound(_, _)) => {
63 log::info!("Captcha answer is not exist in cacache for token: {token}");
64 Ok(None)
65 }
66 Err(err) => {
67 log::error!("Failed to get captcha answer from cacache for token: {token}");
68 Err(err)
69 }
70 }
71 }
72
73 async fn clear_expired(&self, expired_after: Duration) -> Result<(), Self::Error> {
74 let now = SystemTime::now()
75 .duration_since(std::time::UNIX_EPOCH)
76 .expect("SystemTime before UNIX EPOCH!")
77 .as_millis();
78 let expired_after = expired_after.as_millis();
79
80 let expr_keys = cacache::index::ls(&self.cache_dir).filter_map(|meta| {
81 if let Ok(meta) = meta {
82 if now >= (meta.time + expired_after) {
83 return Some(meta.key);
84 }
85 }
86 None
87 });
88
89 for key in expr_keys {
90 cacache::RemoveOpts::new()
91 .remove_fully(true)
92 .remove(&self.cache_dir, &key)
93 .await
94 .ok();
95 }
96 Ok(())
97 }
98
99 async fn clear_by_token(&self, token: &str) -> Result<(), Self::Error> {
100 log::info!("Clearing captcha token from cacache: {token}");
101 let remove_opts = cacache::RemoveOpts::new().remove_fully(true);
102 remove_opts.remove(&self.cache_dir, token).await
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[tokio::test]
111 async fn cacache_store_captcha() {
112 let storage = CacacheStorage::new(
113 tempfile::tempdir()
114 .expect("failed to create temp file")
115 .path()
116 .to_owned(),
117 );
118
119 let token = storage
120 .store_answer("answer".to_owned())
121 .await
122 .expect("failed to store captcha");
123 assert_eq!(
124 storage
125 .get_answer(&token)
126 .await
127 .expect("failed to get captcha answer"),
128 Some("answer".to_owned())
129 );
130 }
131
132 #[tokio::test]
133 async fn cacache_clear_expired() {
134 let storage = CacacheStorage::new(
135 tempfile::tempdir()
136 .expect("failed to create temp file")
137 .path()
138 .to_owned(),
139 );
140
141 let token = storage
142 .store_answer("answer".to_owned())
143 .await
144 .expect("failed to store captcha");
145 storage
146 .clear_expired(Duration::from_secs(0))
147 .await
148 .expect("failed to clear expired captcha");
149 assert!(storage
150 .get_answer(&token)
151 .await
152 .expect("failed to get captcha answer")
153 .is_none());
154 }
155
156 #[tokio::test]
157 async fn cacache_clear_by_token() {
158 let storage = CacacheStorage::new(
159 tempfile::tempdir()
160 .expect("failed to create temp file")
161 .path()
162 .to_owned(),
163 );
164
165 let token = storage
166 .store_answer("answer".to_owned())
167 .await
168 .expect("failed to store captcha");
169 storage
170 .clear_by_token(&token)
171 .await
172 .expect("failed to clear captcha by token");
173 assert!(storage
174 .get_answer(&token)
175 .await
176 .expect("failed to get captcha answer")
177 .is_none());
178 }
179
180 #[tokio::test]
181 async fn cacache_is_token_exist() {
182 let storage = CacacheStorage::new(
183 tempfile::tempdir()
184 .expect("failed to create temp file")
185 .path()
186 .to_owned(),
187 );
188
189 let token = storage
190 .store_answer("answer".to_owned())
191 .await
192 .expect("failed to store captcha");
193 assert!(storage
194 .get_answer(&token)
195 .await
196 .expect("failed to check if token is exist")
197 .is_some());
198 assert!(storage
199 .get_answer("token")
200 .await
201 .expect("failed to check if token is exist")
202 .is_none());
203 }
204
205 #[tokio::test]
206 async fn cacache_get_answer() {
207 let storage = CacacheStorage::new(
208 tempfile::tempdir()
209 .expect("failed to create temp file")
210 .path()
211 .to_owned(),
212 );
213
214 let token = storage
215 .store_answer("answer".to_owned())
216 .await
217 .expect("failed to store captcha");
218 assert_eq!(
219 storage
220 .get_answer(&token)
221 .await
222 .expect("failed to get captcha answer"),
223 Some("answer".to_owned())
224 );
225 assert!(storage
226 .get_answer("token")
227 .await
228 .expect("failed to get captcha answer")
229 .is_none());
230 }
231
232 #[tokio::test]
233 async fn cacache_cache_dir() {
234 let cache_dir = tempfile::tempdir()
235 .expect("failed to create temp file")
236 .path()
237 .to_owned();
238 let storage = CacacheStorage::new(cache_dir.clone());
239 assert_eq!(storage.cache_dir(), &cache_dir);
240 }
241
242 #[tokio::test]
243 async fn cacache_clear_expired_with_expired_after() {
244 let storage = CacacheStorage::new(
245 tempfile::tempdir()
246 .expect("failed to create temp file")
247 .path()
248 .to_owned(),
249 );
250
251 let token = storage
252 .store_answer("answer".to_owned())
253 .await
254 .expect("failed to store captcha");
255 storage
256 .clear_expired(Duration::from_secs(1))
257 .await
258 .expect("failed to clear expired captcha");
259 assert_eq!(
260 storage
261 .get_answer(&token)
262 .await
263 .expect("failed to get captcha answer"),
264 Some("answer".to_owned())
265 );
266 tokio::time::sleep(Duration::from_secs(1)).await;
267 storage
268 .clear_expired(Duration::from_secs(1))
269 .await
270 .expect("failed to clear expired captcha");
271 assert!(storage
272 .get_answer(&token)
273 .await
274 .expect("failed to get captcha answer")
275 .is_none());
276 }
277}