sps_common/
cache.rs

1// src/utils/cache.rs
2// Handles caching of formula data and downloads
3
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::time::{Duration, SystemTime};
7
8use serde::de::DeserializeOwned;
9use serde::Serialize;
10
11use super::error::{Result, SpsError};
12
13// TODO: Define cache directory structure (e.g., ~/.cache/sp)
14// TODO: Implement functions for storing, retrieving, and clearing cached data.
15
16const CACHE_SUBDIR: &str = "sps";
17// Define how long cache entries are considered valid
18const CACHE_TTL: Duration = Duration::from_secs(24 * 60 * 60); // 24 hours
19
20/// Cache struct to manage cache operations
21pub struct Cache {
22    cache_dir: PathBuf,
23}
24
25impl Cache {
26    pub fn new(cache_dir: &Path) -> Result<Self> {
27        if !cache_dir.exists() {
28            fs::create_dir_all(cache_dir)?;
29        }
30
31        Ok(Self {
32            cache_dir: cache_dir.to_path_buf(),
33        })
34    }
35
36    /// Gets the cache directory path
37    pub fn get_dir(&self) -> &Path {
38        &self.cache_dir
39    }
40
41    /// Stores raw string data in the cache
42    pub fn store_raw(&self, filename: &str, data: &str) -> Result<()> {
43        let path = self.cache_dir.join(filename);
44        tracing::debug!("Saving raw data to cache file: {:?}", path);
45        fs::write(&path, data)?;
46        Ok(())
47    }
48
49    /// Loads raw string data from the cache
50    pub fn load_raw(&self, filename: &str) -> Result<String> {
51        let path = self.cache_dir.join(filename);
52        tracing::debug!("Loading raw data from cache file: {:?}", path);
53
54        if !path.exists() {
55            return Err(SpsError::Cache(format!(
56                "Cache file {filename} does not exist"
57            )));
58        }
59
60        fs::read_to_string(&path).map_err(|e| SpsError::Cache(format!("IO error: {e}")))
61    }
62
63    /// Checks if a cache file exists and is valid (within TTL)
64    pub fn is_cache_valid(&self, filename: &str) -> Result<bool> {
65        let path = self.cache_dir.join(filename);
66        if !path.exists() {
67            return Ok(false);
68        }
69
70        let metadata = fs::metadata(&path)?;
71        let modified_time = metadata.modified()?;
72        let age = SystemTime::now()
73            .duration_since(modified_time)
74            .map_err(|e| SpsError::Cache(format!("System time error: {e}")))?;
75
76        Ok(age <= CACHE_TTL)
77    }
78
79    /// Clears a specific cache file
80    pub fn clear_file(&self, filename: &str) -> Result<()> {
81        let path = self.cache_dir.join(filename);
82        if path.exists() {
83            fs::remove_file(&path)?;
84        }
85        Ok(())
86    }
87
88    /// Clears all cache files
89    pub fn clear_all(&self) -> Result<()> {
90        if self.cache_dir.exists() {
91            fs::remove_dir_all(&self.cache_dir)?;
92            fs::create_dir_all(&self.cache_dir)?;
93        }
94        Ok(())
95    }
96}
97
98/// Gets the path to the application's cache directory, creating it if necessary.
99/// Uses dirs::cache_dir() to find the appropriate system cache location.
100pub fn get_cache_dir() -> Result<PathBuf> {
101    let base_cache_dir = dirs::cache_dir()
102        .ok_or_else(|| SpsError::Cache("Could not determine system cache directory".to_string()))?;
103    let app_cache_dir = base_cache_dir.join(CACHE_SUBDIR);
104
105    if !app_cache_dir.exists() {
106        tracing::debug!("Creating cache directory at {:?}", app_cache_dir);
107        fs::create_dir_all(&app_cache_dir)?;
108    }
109    Ok(app_cache_dir)
110}
111
112/// Constructs the full path for a given cache filename.
113fn get_cache_path(filename: &str) -> Result<PathBuf> {
114    Ok(get_cache_dir()?.join(filename))
115}
116
117/// Saves serializable data to a file in the cache directory.
118/// The data is serialized as JSON.
119pub fn save_to_cache<T: Serialize>(filename: &str, data: &T) -> Result<()> {
120    let path = get_cache_path(filename)?;
121    tracing::debug!("Saving data to cache file: {:?}", path);
122    let file = fs::File::create(&path)?;
123    // Use serde_json::to_writer_pretty for readable cache files (optional)
124    serde_json::to_writer_pretty(file, data)?;
125    Ok(())
126}
127
128/// Loads and deserializes data from a file in the cache directory.
129/// Checks if the cache file exists and is within the TTL (Time To Live).
130pub fn load_from_cache<T: DeserializeOwned>(filename: &str) -> Result<T> {
131    let path = get_cache_path(filename)?;
132    tracing::debug!("Attempting to load from cache file: {:?}", path);
133
134    if !path.exists() {
135        tracing::debug!("Cache file not found.");
136        return Err(SpsError::Cache("Cache file does not exist".to_string()));
137    }
138
139    // Check cache file age
140    let metadata = fs::metadata(&path)?;
141    let modified_time = metadata.modified()?;
142    let age = SystemTime::now()
143        .duration_since(modified_time)
144        .map_err(|e| SpsError::Cache(format!("System time error: {e}")))?;
145
146    if age > CACHE_TTL {
147        tracing::debug!("Cache file expired (age: {:?}, TTL: {:?}).", age, CACHE_TTL);
148        return Err(SpsError::Cache(format!(
149            "Cache file expired ({} > {})",
150            humantime::format_duration(age),
151            humantime::format_duration(CACHE_TTL)
152        )));
153    }
154
155    tracing::debug!("Cache file is valid. Loading");
156    let file = fs::File::open(&path)?;
157    let data: T = serde_json::from_reader(file)?;
158    Ok(data)
159}
160
161/// Clears the entire application cache directory.
162pub fn clear_cache() -> Result<()> {
163    let path = get_cache_dir()?;
164    tracing::debug!("Clearing cache directory: {:?}", path);
165    if path.exists() {
166        fs::remove_dir_all(&path)?;
167    }
168    Ok(())
169}
170
171/// Checks if a specific cache file exists and is valid (within TTL).
172pub fn is_cache_valid(filename: &str) -> Result<bool> {
173    let path = get_cache_path(filename)?;
174    if !path.exists() {
175        return Ok(false);
176    }
177    let metadata = fs::metadata(&path)?;
178    let modified_time = metadata.modified()?;
179    let age = SystemTime::now()
180        .duration_since(modified_time)
181        .map_err(|e| SpsError::Cache(format!("System time error: {e}")))?;
182    Ok(age <= CACHE_TTL)
183}