1use std::collections::HashMap;
28use std::time::SystemTime;
29use chrono::{DateTime, Local};
30use crate::config::CacheConfig;
31use crate::object::CacheObject;
32use crate::{CacheError, CacheResult};
33
34fn time_format(time: SystemTime, format: &str) -> String {
35 let datetime: DateTime<Local> = time.into();
36 datetime.format(format).to_string()
37}
38
39pub struct Cache {
41 config: CacheConfig,
42 objects: HashMap<String, CacheObject>,
43 next_id: u32,
44}
45
46impl Cache {
47 pub fn new(config: CacheConfig) -> Self {
55 Cache {
56 config,
57 objects: HashMap::new(),
58 next_id: 1,
59 }
60 }
61
62 pub fn create(&mut self, name: &str, custom_config: Option<&str>) -> CacheResult<CacheObject> {
71 validate_name(name)?;
72
73 let id = self.next_id;
74 self.next_id += 1;
75
76 let mut merged_config = self.config.clone();
77
78 if let Some(config_str) = custom_config {
79 match serde_json::from_str::<CacheConfig>(config_str) {
80 Ok(custom) => {
81 if !custom.path.windows.is_empty() {
82 merged_config.path.windows = custom.path.windows.clone();
83 }
84 if !custom.path.linux.is_empty() {
85 merged_config.path.linux = custom.path.linux.clone();
86 }
87
88 if !custom.format.filename.is_empty() {
89 merged_config.format.filename = custom.format.filename.clone();
90 }
91 if !custom.format.time.is_empty() {
92 merged_config.format.time = custom.format.time.clone();
93 }
94 }
95 Err(e) => return Err(CacheError::ConfigParse(e.to_string())),
96 }
97 }
98
99 let cache_path = if cfg!(windows) {
100 self.expand_path(&merged_config.path.windows)
101 } else {
102 self.expand_path(&merged_config.path.linux)
103 };
104
105 let filename = merged_config.format.filename
106 .replace("{name}", name)
107 .replace("{id}", &id.to_string())
108 .replace("{time}", &time_format(SystemTime::now(), &merged_config.format.time));
109
110 let full_path = std::path::PathBuf::from(&cache_path).join(&filename);
111
112 #[cfg(windows)]
113 let full_path = std::path::PathBuf::from(
114 full_path.to_string_lossy().replace('/', "\\")
115 );
116
117 if let Some(parent) = full_path.parent() {
119 std::fs::create_dir_all(parent).map_err(|e| {
120 CacheError::InvalidPath(format!("Failed to create cache directory: {}", e))
121 })?;
122 }
123
124 let cache_object = CacheObject::new(
125 name.to_string(),
126 full_path,
127 id
128 );
129
130 self.objects.insert(name.to_string(), cache_object.clone());
131
132 Ok(cache_object)
133 }
134
135 pub fn expand_path(&self, path: &str) -> String {
137 let mut expanded = path.to_string();
138
139 if cfg!(windows) && path.contains("%") {
141 expanded = self.expand_windows_env_vars(path);
142 }
143
144 if expanded.starts_with('~') {
146 if let Some(home) = dirs::home_dir() {
147 expanded = home.to_string_lossy().to_string() + &expanded[1..];
148 }
149 }
150
151 #[cfg(windows)]
152 {
153 expanded = expanded.replace('/', "\\");
154 }
155
156 expanded
157 }
158
159 fn expand_windows_env_vars(&self, path: &str) -> String {
161 use std::env;
162 let mut result = path.to_string();
163
164 if let Ok(temp) = env::var("TEMP") {
166 result = result.replace("%temp%", &temp);
167 }
168 if let Ok(tmp) = env::var("TMP") {
169 result = result.replace("%tmp%", &tmp);
170 }
171 if let Ok(appdata) = env::var("APPDATA") {
172 result = result.replace("%appdata%", &appdata);
173 }
174 if let Ok(localappdata) = env::var("LOCALAPPDATA") {
175 result = result.replace("%localappdata%", &localappdata);
176 }
177 if let Ok(userprofile) = env::var("USERPROFILE") {
178 result = result.replace("%userprofile%", &userprofile);
179 }
180
181 result
182 }
183
184 pub fn get(&self, name: &str) -> CacheResult<CacheObject> {
192 self.objects.get(name)
193 .cloned()
194 .ok_or_else(|| CacheError::NotFound(format!("Cache object '{}' not found", name)))
195 }
196
197 pub fn len(&self) -> usize {
202 self.objects.len()
203 }
204
205 pub fn is_empty(&self) -> bool {
210 self.objects.is_empty()
211 }
212
213 pub fn remove(&mut self, name: &str) -> CacheResult<()> {
221 if let Some(cache_obj) = self.objects.remove(name) {
222 cache_obj.delete()?;
223 }
224 Ok(())
225 }
226
227 #[deprecated(note = "Due to being deprecated in its lifecycle, this function only returns 0; please use the Cache::clear()")]
232 pub fn cleanup(&mut self) -> CacheResult<u32> {
233 let count = self.objects.len() as u32;
234 self.clear()?;
235 Ok(count)
236 }
237
238 pub fn clear(&mut self) -> CacheResult<()> {
243 let mut errors = Vec::new();
244
245 for (name, cache_obj) in &self.objects {
246 if let Err(e) = cache_obj.delete() {
247 errors.push(format!("Failed to delete cache object '{}': {}", name, e));
248 }
249 }
250
251 self.objects.clear();
252
253 if !errors.is_empty() {
254 return Err(CacheError::Generic(format!(
255 "Errors occurred while clearing cache: {}",
256 errors.join("; ")
257 )));
258 }
259
260 Ok(())
261 }
262
263 pub fn set_config(&mut self, config: CacheConfig) {
268 self.config = config;
269 }
270
271 pub fn get_config(&self) -> CacheConfig {
276 self.config.clone()
277 }
278
279 pub fn iter(&self) -> impl Iterator<Item = &CacheObject> {
284 self.objects.values()
285 }
286}
287
288fn validate_name(name: &str) -> CacheResult<()> {
296 if name.is_empty() {
297 return Err(CacheError::InvalidName(
298 "Cache name cannot be empty".to_string()
299 ));
300 }
301
302 if name.contains("..") || name.contains(std::path::MAIN_SEPARATOR) ||
303 name.contains('/') || name.contains('\\') {
304 return Err(CacheError::InvalidName(
305 "Invalid cache name: contains path components".to_string()
306 ));
307 }
308
309 #[cfg(windows)]
310 {
311 let reserved_names = ["CON", "PRN", "AUX", "NUL",
312 "COM1", "COM2", "COM3", "COM4", "COM5",
313 "COM6", "COM7", "COM8", "COM9",
314 "LPT1", "LPT2", "LPT3", "LPT4", "LPT5",
315 "LPT6", "LPT7", "LPT8", "LPT9"];
316
317 let uppercase_name = name.to_uppercase();
318 for reserved in &reserved_names {
319 if uppercase_name == *reserved || uppercase_name.starts_with(&format!("{}.", reserved)) {
320 return Err(CacheError::InvalidName(
321 format!("Cache name '{}' is a reserved system name", name)
322 ));
323 }
324 }
325 }
326
327 if name.len() > 255 {
328 return Err(CacheError::InvalidName(
329 "Cache name too long (max 255 characters)".to_string()
330 ));
331 }
332
333 Ok(())
334}