cstats_core/cache/
mod.rs

1//! Caching functionality for cstats
2
3use std::path::PathBuf;
4use std::time::{Duration, SystemTime};
5
6use serde::{Deserialize, Serialize};
7use tokio::fs;
8
9use crate::{config::CacheConfig, Result};
10
11/// Cache entry with metadata
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CacheEntry<T> {
14    /// The cached data
15    pub data: T,
16
17    /// When the entry was created
18    pub created_at: SystemTime,
19
20    /// When the entry expires
21    pub expires_at: SystemTime,
22
23    /// Optional metadata
24    pub metadata: Option<std::collections::HashMap<String, String>>,
25}
26
27/// File-based cache implementation
28#[derive(Debug, Clone)]
29pub struct FileCache {
30    config: CacheConfig,
31}
32
33impl FileCache {
34    /// Create a new file cache with the given configuration
35    pub fn new(config: CacheConfig) -> Self {
36        Self { config }
37    }
38
39    /// Initialize the cache directory
40    pub async fn init(&self) -> Result<()> {
41        fs::create_dir_all(&self.config.cache_dir).await?;
42        Ok(())
43    }
44
45    /// Store data in the cache
46    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    /// Retrieve data from the cache
65    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        // Check if entry has expired
79        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    /// Remove an entry from the cache
88    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    /// Clear all entries from the cache
97    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    /// Check if a key exists in the cache and is not expired
106    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    /// Get cache statistics
120    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                // Check if expired
133                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    /// Generate the file path for a cache key
154    fn cache_path(&self, key: &str) -> PathBuf {
155        // Use a hash of the key to avoid filesystem issues with special characters
156        let key_hash = format!("{:x}", md5::compute(key.as_bytes()));
157        self.config.cache_dir.join(format!("{}.json", key_hash))
158    }
159}
160
161/// Cache statistics
162#[derive(Debug, Clone)]
163pub struct CacheStats {
164    /// Total size of cache in bytes
165    pub total_size_bytes: u64,
166
167    /// Number of cache entries
168    pub entry_count: u32,
169
170    /// Number of expired entries
171    pub expired_count: u32,
172
173    /// Cache directory path
174    pub cache_dir: PathBuf,
175}