1use crate::{ConfigEntry, Environment, Result, StorageError, VersionEntry};
4use std::collections::HashMap;
5use std::fs::{self, File};
6use std::io::{Read, Write};
7use std::path::{Path, PathBuf};
8use std::sync::{Arc, RwLock};
9use uuid::Uuid;
10
11#[derive(Clone)]
13pub struct FileStorage {
14 base_path: PathBuf,
15 index: Arc<RwLock<HashMap<String, ConfigEntry>>>,
17}
18
19impl FileStorage {
20 pub fn new(base_path: impl AsRef<Path>) -> Result<Self> {
22 let base_path = base_path.as_ref().to_path_buf();
23
24 fs::create_dir_all(&base_path)?;
26 fs::create_dir_all(base_path.join("configs"))?;
27 fs::create_dir_all(base_path.join("versions"))?;
28
29 let storage = Self {
30 base_path,
31 index: Arc::new(RwLock::new(HashMap::new())),
32 };
33
34 storage.rebuild_index()?;
36
37 Ok(storage)
38 }
39
40 fn rebuild_index(&self) -> Result<()> {
42 let configs_dir = self.base_path.join("configs");
43 if !configs_dir.exists() {
44 return Ok(());
45 }
46
47 let mut index = self.index.write().unwrap();
48 index.clear();
49
50 for entry in fs::read_dir(&configs_dir)? {
51 let entry = entry?;
52 let path = entry.path();
53
54 if path.extension().and_then(|s| s.to_str()) == Some("json") {
55 if let Ok(config) = self.load_config_from_file(&path) {
56 let key = self.make_key(&config.namespace, &config.key, config.environment);
57 index.insert(key, config);
58 }
59 }
60 }
61
62 Ok(())
63 }
64
65 fn make_key(&self, namespace: &str, key: &str, env: Environment) -> String {
67 format!("{}::{}::{}", namespace, key, env)
68 }
69
70 fn config_file_path(&self, namespace: &str, key: &str, env: Environment) -> PathBuf {
72 let safe_namespace = namespace.replace('/', "_");
73 let safe_key = key.replace('/', "_");
74 let filename = format!("{}_{}_{}.json", safe_namespace, safe_key, env);
75 self.base_path.join("configs").join(filename)
76 }
77
78 fn version_file_path(&self, version_id: Uuid) -> PathBuf {
80 self.base_path
81 .join("versions")
82 .join(format!("{}.json", version_id))
83 }
84
85 fn load_config_from_file(&self, path: &Path) -> Result<ConfigEntry> {
87 let mut file = File::open(path)?;
88 let mut contents = String::new();
89 file.read_to_string(&mut contents)?;
90
91 serde_json::from_str(&contents)
92 .map_err(|e| StorageError::SerializationError(e.to_string()))
93 }
94
95 fn write_config_atomically(&self, config: &ConfigEntry) -> Result<()> {
97 let path = self.config_file_path(&config.namespace, &config.key, config.environment);
98
99 let json = serde_json::to_string_pretty(config)
101 .map_err(|e| StorageError::SerializationError(e.to_string()))?;
102
103 let temp_path = path.with_extension("tmp");
105 {
106 let mut temp_file = File::create(&temp_path)?;
107 temp_file.write_all(json.as_bytes())?;
108 temp_file.sync_all()?; }
110
111 fs::rename(&temp_path, &path)?;
113
114 Ok(())
115 }
116
117 pub fn set(&self, config: ConfigEntry) -> Result<()> {
119 self.write_config_atomically(&config)?;
121
122 let key = self.make_key(&config.namespace, &config.key, config.environment);
124 let mut index = self.index.write().unwrap();
125 index.insert(key, config);
126
127 Ok(())
128 }
129
130 pub fn get(
132 &self,
133 namespace: &str,
134 key: &str,
135 env: Environment,
136 ) -> Result<Option<ConfigEntry>> {
137 let storage_key = self.make_key(namespace, key, env);
138 let index = self.index.read().unwrap();
139
140 Ok(index.get(&storage_key).cloned())
141 }
142
143 pub fn list(&self, namespace: &str, env: Environment) -> Result<Vec<ConfigEntry>> {
145 let index = self.index.read().unwrap();
146 let prefix = format!("{}::", namespace);
147 let suffix = format!("::{}", env);
148
149 let configs: Vec<ConfigEntry> = index
150 .iter()
151 .filter(|(k, _)| k.starts_with(&prefix) && k.ends_with(&suffix))
152 .map(|(_, v)| v.clone())
153 .collect();
154
155 Ok(configs)
156 }
157
158 pub fn delete(&self, namespace: &str, key: &str, env: Environment) -> Result<bool> {
160 let storage_key = self.make_key(namespace, key, env);
161
162 let mut index = self.index.write().unwrap();
164 let removed = index.remove(&storage_key).is_some();
165
166 if removed {
167 let path = self.config_file_path(namespace, key, env);
169 if path.exists() {
170 fs::remove_file(path)?;
171 }
172 }
173
174 Ok(removed)
175 }
176
177 pub fn store_version(&self, version: VersionEntry) -> Result<()> {
179 let version_id = Uuid::new_v4();
180 let path = self.version_file_path(version_id);
181
182 let json = serde_json::to_string_pretty(&version)
183 .map_err(|e| StorageError::SerializationError(e.to_string()))?;
184
185 let mut file = File::create(path)?;
186 file.write_all(json.as_bytes())?;
187 file.sync_all()?;
188
189 Ok(())
190 }
191
192 pub fn get_versions(
194 &self,
195 namespace: &str,
196 key: &str,
197 env: Environment,
198 ) -> Result<Vec<VersionEntry>> {
199 let versions_dir = self.base_path.join("versions");
200 if !versions_dir.exists() {
201 return Ok(Vec::new());
202 }
203
204 let mut versions = Vec::new();
205
206 for entry in fs::read_dir(&versions_dir)? {
207 let entry = entry?;
208 let path = entry.path();
209
210 if path.extension().and_then(|s| s.to_str()) == Some("json") {
211 if let Ok(mut file) = File::open(&path) {
212 let mut contents = String::new();
213 if file.read_to_string(&mut contents).is_ok() {
214 if let Ok(version) = serde_json::from_str::<VersionEntry>(&contents) {
215 if version.namespace == namespace
216 && version.key == key
217 && version.environment == env
218 {
219 versions.push(version);
220 }
221 }
222 }
223 }
224 }
225 }
226
227 versions.sort_by(|a, b| b.version.cmp(&a.version));
229
230 Ok(versions)
231 }
232
233 pub fn export_all(&self, export_path: impl AsRef<Path>) -> Result<usize> {
235 let export_path = export_path.as_ref();
236 fs::create_dir_all(export_path)?;
237
238 let index = self.index.read().unwrap();
239 let count = index.len();
240
241 for config in index.values() {
242 let filename = format!(
243 "{}_{}_{}_{}.json",
244 config.namespace.replace('/', "_"),
245 config.key.replace('/', "_"),
246 config.environment,
247 config.id
248 );
249 let path = export_path.join(filename);
250
251 let json = serde_json::to_string_pretty(config)
252 .map_err(|e| StorageError::SerializationError(e.to_string()))?;
253
254 let mut file = File::create(path)?;
255 file.write_all(json.as_bytes())?;
256 }
257
258 Ok(count)
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use crate::ConfigValue;
266 use tempfile::TempDir;
267
268 #[test]
269 fn test_file_storage_creation() {
270 let temp_dir = TempDir::new().unwrap();
271 let storage = FileStorage::new(temp_dir.path()).unwrap();
272
273 assert!(temp_dir.path().join("configs").exists());
274 assert!(temp_dir.path().join("versions").exists());
275 }
276
277 #[test]
278 fn test_set_and_get() {
279 let temp_dir = TempDir::new().unwrap();
280 let storage = FileStorage::new(temp_dir.path()).unwrap();
281
282 let entry = ConfigEntry::new(
283 "test/namespace",
284 "config.key",
285 ConfigValue::String("test value".to_string()),
286 Environment::Development,
287 );
288
289 storage.set(entry.clone()).unwrap();
290
291 let retrieved = storage
292 .get("test/namespace", "config.key", Environment::Development)
293 .unwrap()
294 .unwrap();
295
296 assert_eq!(retrieved.namespace, entry.namespace);
297 assert_eq!(retrieved.key, entry.key);
298 }
299
300 #[test]
301 fn test_list_configs() {
302 let temp_dir = TempDir::new().unwrap();
303 let storage = FileStorage::new(temp_dir.path()).unwrap();
304
305 let entry1 = ConfigEntry::new(
306 "test/ns",
307 "key1",
308 ConfigValue::String("val1".to_string()),
309 Environment::Development,
310 );
311 let entry2 = ConfigEntry::new(
312 "test/ns",
313 "key2",
314 ConfigValue::String("val2".to_string()),
315 Environment::Development,
316 );
317
318 storage.set(entry1).unwrap();
319 storage.set(entry2).unwrap();
320
321 let configs = storage.list("test/ns", Environment::Development).unwrap();
322 assert_eq!(configs.len(), 2);
323 }
324
325 #[test]
326 fn test_delete() {
327 let temp_dir = TempDir::new().unwrap();
328 let storage = FileStorage::new(temp_dir.path()).unwrap();
329
330 let entry = ConfigEntry::new(
331 "test/ns",
332 "key",
333 ConfigValue::String("val".to_string()),
334 Environment::Development,
335 );
336
337 storage.set(entry).unwrap();
338 assert!(storage
339 .get("test/ns", "key", Environment::Development)
340 .unwrap()
341 .is_some());
342
343 let deleted = storage.delete("test/ns", "key", Environment::Development).unwrap();
344 assert!(deleted);
345
346 assert!(storage
347 .get("test/ns", "key", Environment::Development)
348 .unwrap()
349 .is_none());
350 }
351
352 #[test]
353 fn test_persistence() {
354 let temp_dir = TempDir::new().unwrap();
355
356 let entry = ConfigEntry::new(
357 "test/ns",
358 "key",
359 ConfigValue::String("val".to_string()),
360 Environment::Production,
361 );
362
363 {
365 let storage = FileStorage::new(temp_dir.path()).unwrap();
366 storage.set(entry.clone()).unwrap();
367 }
368
369 {
371 let storage = FileStorage::new(temp_dir.path()).unwrap();
372 let retrieved = storage
373 .get("test/ns", "key", Environment::Production)
374 .unwrap()
375 .unwrap();
376 assert_eq!(retrieved.key, entry.key);
377 }
378 }
379}