whiteout/storage/
local.rs1use 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}