1use std::path::PathBuf;
4use std::time::{Duration, SystemTime};
5
6use serde::{Deserialize, Serialize};
7use tokio::fs;
8
9use crate::{config::CacheConfig, Result};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CacheEntry<T> {
14 pub data: T,
16
17 pub created_at: SystemTime,
19
20 pub expires_at: SystemTime,
22
23 pub metadata: Option<std::collections::HashMap<String, String>>,
25}
26
27#[derive(Debug, Clone)]
29pub struct FileCache {
30 config: CacheConfig,
31}
32
33impl FileCache {
34 pub fn new(config: CacheConfig) -> Self {
36 Self { config }
37 }
38
39 pub async fn init(&self) -> Result<()> {
41 fs::create_dir_all(&self.config.cache_dir).await?;
42 Ok(())
43 }
44
45 pub async fn store<T>(&self, key: &str, data: T) -> Result<()>
47 where
48 T: Serialize,
49 {
50 let entry = CacheEntry {
51 data,
52 created_at: SystemTime::now(),
53 expires_at: SystemTime::now() + Duration::from_secs(self.config.ttl_seconds),
54 metadata: None,
55 };
56
57 let path = self.cache_path(key);
58 let content = serde_json::to_string(&entry)?;
59 fs::write(path, content).await?;
60
61 Ok(())
62 }
63
64 pub async fn get<T>(&self, key: &str) -> Result<Option<T>>
66 where
67 T: for<'de> Deserialize<'de>,
68 {
69 let path = self.cache_path(key);
70
71 if !path.exists() {
72 return Ok(None);
73 }
74
75 let content = fs::read_to_string(path).await?;
76 let entry: CacheEntry<T> = serde_json::from_str(&content)?;
77
78 if entry.expires_at < SystemTime::now() {
80 self.remove(key).await?;
81 return Ok(None);
82 }
83
84 Ok(Some(entry.data))
85 }
86
87 pub async fn remove(&self, key: &str) -> Result<()> {
89 let path = self.cache_path(key);
90 if path.exists() {
91 fs::remove_file(path).await?;
92 }
93 Ok(())
94 }
95
96 pub async fn clear(&self) -> Result<()> {
98 if self.config.cache_dir.exists() {
99 fs::remove_dir_all(&self.config.cache_dir).await?;
100 fs::create_dir_all(&self.config.cache_dir).await?;
101 }
102 Ok(())
103 }
104
105 pub async fn exists(&self, key: &str) -> Result<bool> {
107 let path = self.cache_path(key);
108
109 if !path.exists() {
110 return Ok(false);
111 }
112
113 let content = fs::read_to_string(path).await?;
114 let entry: CacheEntry<serde_json::Value> = serde_json::from_str(&content)?;
115
116 Ok(entry.expires_at > SystemTime::now())
117 }
118
119 pub async fn stats(&self) -> Result<CacheStats> {
121 let mut total_size = 0u64;
122 let mut entry_count = 0u32;
123 let mut expired_count = 0u32;
124
125 let mut entries = fs::read_dir(&self.config.cache_dir).await?;
126 while let Some(entry) = entries.next_entry().await? {
127 if entry.file_type().await?.is_file() {
128 entry_count += 1;
129 let metadata = entry.metadata().await?;
130 total_size += metadata.len();
131
132 if let Ok(content) = fs::read_to_string(entry.path()).await {
134 if let Ok(cache_entry) =
135 serde_json::from_str::<CacheEntry<serde_json::Value>>(&content)
136 {
137 if cache_entry.expires_at < SystemTime::now() {
138 expired_count += 1;
139 }
140 }
141 }
142 }
143 }
144
145 Ok(CacheStats {
146 total_size_bytes: total_size,
147 entry_count,
148 expired_count,
149 cache_dir: self.config.cache_dir.clone(),
150 })
151 }
152
153 fn cache_path(&self, key: &str) -> PathBuf {
155 let key_hash = format!("{:x}", md5::compute(key.as_bytes()));
157 self.config.cache_dir.join(format!("{}.json", key_hash))
158 }
159}
160
161#[derive(Debug, Clone)]
163pub struct CacheStats {
164 pub total_size_bytes: u64,
166
167 pub entry_count: u32,
169
170 pub expired_count: u32,
172
173 pub cache_dir: PathBuf,
175}