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}