1use crate::core::Project;
7use crate::error::{DevSweepError, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::fs;
11use std::path::{Path, PathBuf};
12use std::time::{Duration, SystemTime, UNIX_EPOCH};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct CachedProject {
17 pub project: Project,
19 pub root_mtime: u64,
21 pub cached_at: u64,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct CachedDirectory {
28 pub mtime: u64,
30 pub project_roots: Vec<PathBuf>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, Default)]
36pub struct ScanCache {
37 pub version: u32,
39 pub updated_at: u64,
41 pub projects: HashMap<PathBuf, CachedProject>,
43 pub directories: HashMap<PathBuf, CachedDirectory>,
45}
46
47impl ScanCache {
48 pub const VERSION: u32 = 1;
50
51 pub const TTL_SECS: u64 = 24 * 60 * 60;
53
54 pub fn new() -> Self {
56 Self {
57 version: Self::VERSION,
58 updated_at: current_timestamp(),
59 projects: HashMap::new(),
60 directories: HashMap::new(),
61 }
62 }
63
64 pub fn get_valid_project(&self, root: &Path) -> Option<&CachedProject> {
66 let cached = self.projects.get(root)?;
67
68 let current_mtime = get_mtime(root).ok()?;
70 if cached.root_mtime != current_mtime {
71 return None; }
73
74 let now = current_timestamp();
76 if now - cached.cached_at > Self::TTL_SECS {
77 return None; }
79
80 Some(cached)
81 }
82
83 pub fn directory_needs_rescan(&self, path: &Path) -> bool {
85 let Some(cached) = self.directories.get(path) else {
86 return true; };
88
89 let Ok(current_mtime) = get_mtime(path) else {
91 return true; };
93
94 cached.mtime != current_mtime
95 }
96
97 pub fn cache_project(&mut self, project: Project) {
99 let root = project.root.clone();
100 let mtime = get_mtime(&root).unwrap_or(0);
101
102 self.projects.insert(root, CachedProject {
103 project,
104 root_mtime: mtime,
105 cached_at: current_timestamp(),
106 });
107 }
108
109 pub fn cache_directory(&mut self, path: PathBuf, project_roots: Vec<PathBuf>) {
111 let mtime = get_mtime(&path).unwrap_or(0);
112 self.directories.insert(path, CachedDirectory {
113 mtime,
114 project_roots,
115 });
116 }
117
118 pub fn touch(&mut self) {
120 self.updated_at = current_timestamp();
121 }
122
123 pub fn is_valid(&self) -> bool {
125 if self.version != Self::VERSION {
127 return false;
128 }
129
130 let now = current_timestamp();
132 now - self.updated_at < Self::TTL_SECS
133 }
134
135 pub fn get_all_valid_projects(&self) -> Vec<Project> {
137 self.projects
138 .iter()
139 .filter_map(|(path, _cached)| {
140 self.get_valid_project(path).map(|c| c.project.clone())
141 })
142 .collect()
143 }
144
145 pub fn project_count(&self) -> usize {
147 self.projects.len()
148 }
149
150 pub fn clear(&mut self) {
152 self.projects.clear();
153 self.directories.clear();
154 self.updated_at = current_timestamp();
155 }
156}
157
158pub fn default_cache_path() -> Result<PathBuf> {
160 let cache_dir = dirs::cache_dir()
161 .ok_or_else(|| DevSweepError::Config("Could not find cache directory".into()))?;
162
163 let devsweep_cache = cache_dir.join("devsweep");
164 if !devsweep_cache.exists() {
165 fs::create_dir_all(&devsweep_cache)?;
166 }
167
168 Ok(devsweep_cache.join("scan_cache.json"))
169}
170
171pub fn load_cache() -> Result<ScanCache> {
173 let path = default_cache_path()?;
174
175 if !path.exists() {
176 return Ok(ScanCache::new());
177 }
178
179 let content = fs::read_to_string(&path)?;
180 let cache: ScanCache = serde_json::from_str(&content)
181 .map_err(|e| DevSweepError::Config(format!("Invalid cache file: {}", e)))?;
182
183 if cache.version != ScanCache::VERSION {
185 return Ok(ScanCache::new()); }
187
188 Ok(cache)
189}
190
191pub fn save_cache(cache: &ScanCache) -> Result<()> {
193 let path = default_cache_path()?;
194 let content = serde_json::to_string_pretty(cache)?;
195 fs::write(&path, content)?;
196 Ok(())
197}
198
199fn get_mtime(path: &Path) -> Result<u64> {
201 let metadata = fs::metadata(path)?;
202 let mtime = metadata.modified()?;
203 Ok(mtime
204 .duration_since(UNIX_EPOCH)
205 .unwrap_or(Duration::ZERO)
206 .as_secs())
207}
208
209fn current_timestamp() -> u64 {
211 SystemTime::now()
212 .duration_since(UNIX_EPOCH)
213 .unwrap_or(Duration::ZERO)
214 .as_secs()
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use tempfile::TempDir;
221
222 #[test]
223 fn test_cache_creation() {
224 let cache = ScanCache::new();
225 assert_eq!(cache.version, ScanCache::VERSION);
226 assert!(cache.projects.is_empty());
227 }
228
229 #[test]
230 fn test_cache_save_load() {
231 let temp = TempDir::new().unwrap();
232 let cache_path = temp.path().join("test_cache.json");
233
234 let mut cache = ScanCache::new();
235 let content = serde_json::to_string(&cache).unwrap();
238 fs::write(&cache_path, &content).unwrap();
239
240 let loaded: ScanCache = serde_json::from_str(&fs::read_to_string(&cache_path).unwrap()).unwrap();
241 assert_eq!(loaded.version, cache.version);
242 }
243}