whiteout/storage/
local.rs

1use anyhow::{Context, Result};
2use std::collections::HashMap;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use super::{StorageData, StorageEntry};
7
8#[derive(Debug, Clone)]
9pub struct LocalStorage {
10    root_path: PathBuf,
11    storage_path: PathBuf,
12}
13
14impl LocalStorage {
15    pub fn new(project_root: impl AsRef<Path>) -> Result<Self> {
16        let root_path = project_root.as_ref().to_path_buf();
17        let storage_path = root_path.join(".whiteout").join("local.toml");
18        
19        Ok(Self {
20            root_path,
21            storage_path,
22        })
23    }
24
25    pub fn init(project_root: impl AsRef<Path>) -> Result<()> {
26        let whiteout_dir = project_root.as_ref().join(".whiteout");
27        fs::create_dir_all(&whiteout_dir).context("Failed to create .whiteout directory")?;
28        
29        let gitignore_path = whiteout_dir.join(".gitignore");
30        if !gitignore_path.exists() {
31            fs::write(&gitignore_path, "local.toml\n*.bak\n")
32                .context("Failed to create .gitignore")?;
33        }
34        
35        let storage_path = whiteout_dir.join("local.toml");
36        if !storage_path.exists() {
37            let initial_data = StorageData {
38                version: "0.1.0".to_string(),
39                entries: HashMap::new(),
40            };
41            let content = toml::to_string_pretty(&initial_data)
42                .context("Failed to serialize initial storage")?;
43            fs::write(&storage_path, content).context("Failed to write initial storage")?;
44        }
45        
46        Ok(())
47    }
48
49    pub fn store_value(
50        &self,
51        file_path: &Path,
52        key: &str,
53        value: &str,
54    ) -> Result<()> {
55        let storage_key = self.make_storage_key(file_path, key);
56        
57        let entry = StorageEntry {
58            file_path: file_path.to_path_buf(),
59            key: key.to_string(),
60            value: value.to_string(),
61            encrypted: false,
62            timestamp: chrono::Utc::now(),
63        };
64        
65        let mut data = self.load_data()?;
66        data.entries.insert(storage_key, entry);
67        
68        let content = toml::to_string_pretty(&data)
69            .context("Failed to serialize storage")?;
70        
71        fs::create_dir_all(self.storage_path.parent().unwrap())
72            .context("Failed to create storage directory")?;
73        
74        fs::write(&self.storage_path, content)
75            .context("Failed to write storage file")?;
76        
77        Ok(())
78    }
79
80    pub fn get_value(&self, file_path: &Path, key: &str) -> Result<String> {
81        let storage_key = self.make_storage_key(file_path, key);
82        let data = self.load_data()?;
83        
84        data.entries
85            .get(&storage_key)
86            .map(|e| e.value.clone())
87            .ok_or_else(|| anyhow::anyhow!("Value not found for key: {}", storage_key))
88    }
89
90    pub fn remove_value(&self, file_path: &Path, key: &str) -> Result<()> {
91        let storage_key = self.make_storage_key(file_path, key);
92        
93        let mut data = self.load_data()?;
94        data.entries.remove(&storage_key);
95        
96        let content = toml::to_string_pretty(&data)
97            .context("Failed to serialize storage")?;
98        
99        fs::write(&self.storage_path, content)
100            .context("Failed to write storage file")?;
101        
102        Ok(())
103    }
104
105    pub fn list_values(&self, file_path: Option<&Path>) -> Result<Vec<StorageEntry>> {
106        let data = self.load_data()?;
107        Ok(data
108            .entries
109            .values()
110            .filter(|e| {
111                file_path.map_or(true, |fp| e.file_path == fp)
112            })
113            .cloned()
114            .collect())
115    }
116    
117    fn load_data(&self) -> Result<StorageData> {
118        if self.storage_path.exists() {
119            let content = fs::read_to_string(&self.storage_path)
120                .context("Failed to read storage file")?;
121            toml::from_str(&content).context("Failed to parse storage file")
122        } else {
123            Ok(StorageData {
124                version: "0.1.0".to_string(),
125                entries: HashMap::new(),
126            })
127        }
128    }
129
130    fn make_storage_key(&self, file_path: &Path, key: &str) -> String {
131        let relative_path = file_path
132            .strip_prefix(&self.root_path)
133            .unwrap_or(file_path);
134        
135        format!("{}::{}", relative_path.display(), key)
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use tempfile::TempDir;
143
144    #[test]
145    fn test_storage_init() -> Result<()> {
146        let temp_dir = TempDir::new()?;
147        LocalStorage::init(temp_dir.path())?;
148        
149        assert!(temp_dir.path().join(".whiteout").exists());
150        assert!(temp_dir.path().join(".whiteout/.gitignore").exists());
151        assert!(temp_dir.path().join(".whiteout/local.toml").exists());
152        
153        Ok(())
154    }
155
156    #[test]
157    fn test_store_and_get_value() -> Result<()> {
158        let temp_dir = TempDir::new()?;
159        LocalStorage::init(temp_dir.path())?;
160        let storage = LocalStorage::new(temp_dir.path())?;
161        
162        let file_path = Path::new("test.rs");
163        storage.store_value(file_path, "test_key", "test_value")?;
164        
165        let value = storage.get_value(file_path, "test_key")?;
166        assert_eq!(value, "test_value");
167        
168        Ok(())
169    }
170
171    #[test]
172    fn test_remove_value() -> Result<()> {
173        let temp_dir = TempDir::new()?;
174        LocalStorage::init(temp_dir.path())?;
175        let storage = LocalStorage::new(temp_dir.path())?;
176        
177        let file_path = Path::new("test.rs");
178        storage.store_value(file_path, "test_key", "test_value")?;
179        storage.remove_value(file_path, "test_key")?;
180        
181        assert!(storage.get_value(file_path, "test_key").is_err());
182        
183        Ok(())
184    }
185}