Skip to main content

wax/
cache.rs

1use std::path::{Path, PathBuf};
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use tokio::fs;
7
8use crate::error::Result;
9
10#[derive(Debug, Clone, Default)]
11pub struct CacheStats {
12    pub hits: usize,
13    pub misses: usize,
14}
15
16#[derive(Debug, Serialize, Deserialize)]
17struct CacheEntry {
18    url: String,
19    fetched_at: u64,
20    body: String,
21}
22
23#[derive(Debug, Clone)]
24pub struct Cache {
25    root: PathBuf,
26}
27
28impl Cache {
29    pub async fn new(root: PathBuf) -> Result<Self> {
30        fs::create_dir_all(&root).await?;
31        Ok(Self { root })
32    }
33
34    pub fn root(&self) -> &Path {
35        &self.root
36    }
37
38    pub async fn get(&self, url: &str, max_age_ms: u64) -> Result<Option<String>> {
39        let path = self.entry_path(url);
40        let Ok(raw) = fs::read_to_string(path).await else {
41            return Ok(None);
42        };
43
44        let entry: CacheEntry = serde_json::from_str(&raw)?;
45        let now = now_ms();
46        if now.saturating_sub(entry.fetched_at) > max_age_ms {
47            return Ok(None);
48        }
49
50        Ok(Some(entry.body))
51    }
52
53    pub async fn put(&self, url: &str, body: &str) -> Result<()> {
54        let entry = CacheEntry {
55            url: url.to_string(),
56            fetched_at: now_ms(),
57            body: body.to_string(),
58        };
59
60        let raw = serde_json::to_string(&entry)?;
61        fs::write(self.entry_path(url), raw).await?;
62        Ok(())
63    }
64
65    pub async fn stats(&self) -> Result<(usize, u64)> {
66        let mut entries = 0usize;
67        let mut bytes = 0u64;
68        let mut read_dir = fs::read_dir(&self.root).await?;
69
70        while let Some(item) = read_dir.next_entry().await? {
71            let meta = item.metadata().await?;
72            if meta.is_file() {
73                entries += 1;
74                bytes += meta.len();
75            }
76        }
77
78        Ok((entries, bytes))
79    }
80
81    pub async fn clear(&self) -> Result<()> {
82        let mut read_dir = fs::read_dir(&self.root).await?;
83        while let Some(item) = read_dir.next_entry().await? {
84            if item.metadata().await?.is_file() {
85                fs::remove_file(item.path()).await?;
86            }
87        }
88        Ok(())
89    }
90
91    fn entry_path(&self, url: &str) -> PathBuf {
92        let mut hasher = Sha256::new();
93        hasher.update(url.as_bytes());
94        let digest = format!("{:x}", hasher.finalize());
95        self.root.join(format!("{digest}.json"))
96    }
97}
98
99fn now_ms() -> u64 {
100    SystemTime::now()
101        .duration_since(UNIX_EPOCH)
102        .unwrap_or_default()
103        .as_millis() as u64
104}