llm_config_storage/
file.rs

1//! File-based storage backend with atomic operations
2
3use 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/// File-based storage backend
12#[derive(Clone)]
13pub struct FileStorage {
14    base_path: PathBuf,
15    /// In-memory index for fast lookups
16    index: Arc<RwLock<HashMap<String, ConfigEntry>>>,
17}
18
19impl FileStorage {
20    /// Create a new file storage at the given path
21    pub fn new(base_path: impl AsRef<Path>) -> Result<Self> {
22        let base_path = base_path.as_ref().to_path_buf();
23
24        // Create directories
25        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        // Load existing configs into index
35        storage.rebuild_index()?;
36
37        Ok(storage)
38    }
39
40    /// Rebuild the index from disk
41    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    /// Make a storage key from namespace, key, and environment
66    fn make_key(&self, namespace: &str, key: &str, env: Environment) -> String {
67        format!("{}::{}::{}", namespace, key, env)
68    }
69
70    /// Get the file path for a config entry
71    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    /// Get the file path for a version entry
79    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    /// Load a config from a file
86    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    /// Atomically write a config to a file
96    fn write_config_atomically(&self, config: &ConfigEntry) -> Result<()> {
97        let path = self.config_file_path(&config.namespace, &config.key, config.environment);
98
99        // Serialize to JSON
100        let json = serde_json::to_string_pretty(config)
101            .map_err(|e| StorageError::SerializationError(e.to_string()))?;
102
103        // Write to temporary file first
104        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()?; // Ensure data is written to disk
109        }
110
111        // Atomic rename
112        fs::rename(&temp_path, &path)?;
113
114        Ok(())
115    }
116
117    /// Store a configuration
118    pub fn set(&self, config: ConfigEntry) -> Result<()> {
119        // Write to disk atomically
120        self.write_config_atomically(&config)?;
121
122        // Update index
123        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    /// Get a configuration
131    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    /// List all configurations in a namespace
144    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    /// Delete a configuration
159    pub fn delete(&self, namespace: &str, key: &str, env: Environment) -> Result<bool> {
160        let storage_key = self.make_key(namespace, key, env);
161
162        // Remove from index
163        let mut index = self.index.write().unwrap();
164        let removed = index.remove(&storage_key).is_some();
165
166        if removed {
167            // Delete file
168            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    /// Store a version entry
178    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    /// Get version history for a config
193    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        // Sort by version number descending
228        versions.sort_by(|a, b| b.version.cmp(&a.version));
229
230        Ok(versions)
231    }
232
233    /// Export all configurations to a directory
234    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        // Create storage, add entry, drop it
364        {
365            let storage = FileStorage::new(temp_dir.path()).unwrap();
366            storage.set(entry.clone()).unwrap();
367        }
368
369        // Create new storage instance and verify entry exists
370        {
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}