1use std::fs;
2use std::io::{Read, Write};
3use std::path::{Path, PathBuf};
4
5use tracing::{debug, warn};
6
7use crate::api::config::ConfigInfo;
8use crate::api::naming::Service;
9use crate::common::{build_config_key, build_service_key, md5_hash};
10
11pub struct FileCache {
13 cache_dir: PathBuf,
14}
15
16impl FileCache {
17 pub fn new(cache_dir: impl AsRef<Path>) -> std::io::Result<Self> {
19 let cache_dir = cache_dir.as_ref().to_path_buf();
20 fs::create_dir_all(&cache_dir)?;
21 fs::create_dir_all(cache_dir.join("config"))?;
22 fs::create_dir_all(cache_dir.join("naming"))?;
23 Ok(Self { cache_dir })
24 }
25
26 fn config_dir(&self) -> PathBuf {
28 self.cache_dir.join("config")
29 }
30
31 fn naming_dir(&self) -> PathBuf {
33 self.cache_dir.join("naming")
34 }
35
36 fn encode_key(key: &str) -> String {
38 md5_hash(key)
40 }
41
42 pub fn save_config(&self, config: &ConfigInfo) -> std::io::Result<()> {
46 let key = build_config_key(&config.data_id, &config.group, &config.tenant);
47 let filename = Self::encode_key(&key);
48 let path = self.config_dir().join(&filename);
49
50 let json = serde_json::to_string(&ConfigCacheEntry {
51 data_id: config.data_id.clone(),
52 group: config.group.clone(),
53 tenant: config.tenant.clone(),
54 content: config.content.clone(),
55 md5: config.md5.clone(),
56 last_modified: config.last_modified,
57 content_type: config.content_type.clone(),
58 })
59 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
60
61 let mut file = fs::File::create(&path)?;
62 file.write_all(json.as_bytes())?;
63 debug!("Saved config to cache: {}", key);
64 Ok(())
65 }
66
67 pub fn load_config(&self, data_id: &str, group: &str, tenant: &str) -> Option<ConfigInfo> {
69 let key = build_config_key(data_id, group, tenant);
70 let filename = Self::encode_key(&key);
71 let path = self.config_dir().join(&filename);
72
73 if !path.exists() {
74 return None;
75 }
76
77 match fs::File::open(&path) {
78 Ok(mut file) => {
79 let mut contents = String::new();
80 if file.read_to_string(&mut contents).is_err() {
81 return None;
82 }
83
84 match serde_json::from_str::<ConfigCacheEntry>(&contents) {
85 Ok(entry) => {
86 debug!("Loaded config from cache: {}", key);
87 Some(ConfigInfo {
88 data_id: entry.data_id,
89 group: entry.group,
90 tenant: entry.tenant,
91 content: entry.content,
92 md5: entry.md5,
93 last_modified: entry.last_modified,
94 content_type: entry.content_type,
95 })
96 }
97 Err(e) => {
98 warn!("Failed to parse cached config {}: {}", key, e);
99 None
100 }
101 }
102 }
103 Err(e) => {
104 warn!("Failed to read cached config {}: {}", key, e);
105 None
106 }
107 }
108 }
109
110 pub fn remove_config(&self, data_id: &str, group: &str, tenant: &str) -> std::io::Result<()> {
112 let key = build_config_key(data_id, group, tenant);
113 let filename = Self::encode_key(&key);
114 let path = self.config_dir().join(&filename);
115
116 if path.exists() {
117 fs::remove_file(&path)?;
118 debug!("Removed config from cache: {}", key);
119 }
120 Ok(())
121 }
122
123 pub fn save_service(&self, namespace: &str, service: &Service) -> std::io::Result<()> {
127 let key = build_service_key(&service.name, &service.group_name, namespace);
128 let filename = Self::encode_key(&key);
129 let path = self.naming_dir().join(&filename);
130
131 let json = serde_json::to_string(service)
132 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
133
134 let mut file = fs::File::create(&path)?;
135 file.write_all(json.as_bytes())?;
136 debug!("Saved service to cache: {}", key);
137 Ok(())
138 }
139
140 pub fn load_service(
142 &self,
143 namespace: &str,
144 group_name: &str,
145 service_name: &str,
146 ) -> Option<Service> {
147 let key = build_service_key(service_name, group_name, namespace);
148 let filename = Self::encode_key(&key);
149 let path = self.naming_dir().join(&filename);
150
151 if !path.exists() {
152 return None;
153 }
154
155 match fs::File::open(&path) {
156 Ok(mut file) => {
157 let mut contents = String::new();
158 if file.read_to_string(&mut contents).is_err() {
159 return None;
160 }
161
162 match serde_json::from_str::<Service>(&contents) {
163 Ok(service) => {
164 debug!("Loaded service from cache: {}", key);
165 Some(service)
166 }
167 Err(e) => {
168 warn!("Failed to parse cached service {}: {}", key, e);
169 None
170 }
171 }
172 }
173 Err(e) => {
174 warn!("Failed to read cached service {}: {}", key, e);
175 None
176 }
177 }
178 }
179
180 pub fn remove_service(
182 &self,
183 namespace: &str,
184 group_name: &str,
185 service_name: &str,
186 ) -> std::io::Result<()> {
187 let key = build_service_key(service_name, group_name, namespace);
188 let filename = Self::encode_key(&key);
189 let path = self.naming_dir().join(&filename);
190
191 if path.exists() {
192 fs::remove_file(&path)?;
193 debug!("Removed service from cache: {}", key);
194 }
195 Ok(())
196 }
197
198 pub fn clear(&self) -> std::io::Result<()> {
200 for entry in fs::read_dir(self.config_dir())?.flatten() {
202 fs::remove_file(entry.path())?;
203 }
204
205 for entry in fs::read_dir(self.naming_dir())?.flatten() {
207 fs::remove_file(entry.path())?;
208 }
209
210 debug!("Cleared file cache");
211 Ok(())
212 }
213}
214
215#[derive(serde::Serialize, serde::Deserialize)]
217struct ConfigCacheEntry {
218 data_id: String,
219 group: String,
220 tenant: String,
221 content: String,
222 md5: String,
223 last_modified: i64,
224 content_type: String,
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use tempfile::tempdir;
231
232 #[test]
233 fn test_config_cache() {
234 let dir = tempdir().unwrap();
235 let cache = FileCache::new(dir.path()).unwrap();
236
237 let mut config = ConfigInfo::new("test-data-id", "test-group", "test-tenant");
238 config.content = "test content".to_string();
239 config.md5 = md5_hash(&config.content);
240
241 cache.save_config(&config).unwrap();
243
244 let loaded = cache
246 .load_config("test-data-id", "test-group", "test-tenant")
247 .unwrap();
248 assert_eq!(loaded.content, "test content");
249
250 cache
252 .remove_config("test-data-id", "test-group", "test-tenant")
253 .unwrap();
254 assert!(cache
255 .load_config("test-data-id", "test-group", "test-tenant")
256 .is_none());
257 }
258
259 #[test]
260 fn test_service_cache() {
261 let dir = tempdir().unwrap();
262 let cache = FileCache::new(dir.path()).unwrap();
263
264 let service = Service::new("test-service", "test-group");
265
266 cache.save_service("test-namespace", &service).unwrap();
268
269 let loaded = cache
271 .load_service("test-namespace", "test-group", "test-service")
272 .unwrap();
273 assert_eq!(loaded.name, "test-service");
274
275 cache
277 .remove_service("test-namespace", "test-group", "test-service")
278 .unwrap();
279 assert!(cache
280 .load_service("test-namespace", "test-group", "test-service")
281 .is_none());
282 }
283}