nostr_double_ratchet_runtime/
file_storage.rs1use crate::{Result, StorageAdapter};
2use std::collections::HashMap;
3use std::fs;
4use std::path::PathBuf;
5use std::sync::Mutex;
6
7pub struct FileStorageAdapter {
8 base_path: PathBuf,
9}
10
11impl FileStorageAdapter {
12 pub fn new(base_path: PathBuf) -> Result<Self> {
13 fs::create_dir_all(&base_path)
14 .map_err(|e| crate::Error::Storage(format!("Failed to create directory: {}", e)))?;
15 Ok(Self { base_path })
16 }
17
18 fn sanitize_key(key: &str) -> String {
19 key.replace(['/', '\\', ':'], "_")
20 }
21
22 fn key_to_path(&self, key: &str) -> PathBuf {
23 let sanitized = Self::sanitize_key(key);
24 self.base_path.join(format!("{}.json", sanitized))
25 }
26}
27
28impl StorageAdapter for FileStorageAdapter {
29 fn get(&self, key: &str) -> Result<Option<String>> {
30 let path = self.key_to_path(key);
31
32 if !path.exists() {
33 return Ok(None);
34 }
35
36 match fs::read_to_string(&path) {
37 Ok(contents) => Ok(Some(contents)),
38 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
39 Err(e) => Err(crate::Error::Storage(format!("Failed to read file: {}", e))),
40 }
41 }
42
43 fn put(&self, key: &str, value: String) -> Result<()> {
44 let path = self.key_to_path(key);
45
46 if let Some(parent) = path.parent() {
47 fs::create_dir_all(parent).map_err(|e| {
48 crate::Error::Storage(format!("Failed to create parent dir: {}", e))
49 })?;
50 }
51
52 let tmp_path = path.with_extension(format!("json.{}.tmp", uuid::Uuid::new_v4()));
55 fs::write(&tmp_path, value)
56 .map_err(|e| crate::Error::Storage(format!("Failed to write file: {}", e)))?;
57
58 #[cfg(windows)]
59 {
60 if path.exists() {
62 fs::remove_file(&path).map_err(|e| {
63 crate::Error::Storage(format!("Failed to replace existing file: {}", e))
64 })?;
65 }
66 }
67
68 fs::rename(&tmp_path, &path)
69 .map_err(|e| crate::Error::Storage(format!("Failed to write file: {}", e)))?;
70
71 Ok(())
72 }
73
74 fn del(&self, key: &str) -> Result<()> {
75 let path = self.key_to_path(key);
76
77 match fs::remove_file(&path) {
78 Ok(()) => Ok(()),
79 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
80 Err(e) => Err(crate::Error::Storage(format!(
81 "Failed to delete file: {}",
82 e
83 ))),
84 }
85 }
86
87 fn list(&self, prefix: &str) -> Result<Vec<String>> {
88 let mut keys = Vec::new();
89
90 let sanitized_prefix = FileStorageAdapter::sanitize_key(prefix);
94
95 let entries = fs::read_dir(&self.base_path)
96 .map_err(|e| crate::Error::Storage(format!("Failed to read directory: {}", e)))?;
97
98 for entry in entries {
99 let entry = entry
100 .map_err(|e| crate::Error::Storage(format!("Failed to read dir entry: {}", e)))?;
101
102 let file_name = entry.file_name();
103 let file_name_str = file_name.to_string_lossy();
104
105 if !file_name_str.ends_with(".json") {
106 continue;
107 }
108
109 let key = file_name_str
110 .strip_suffix(".json")
111 .unwrap_or(&file_name_str)
112 .to_string();
113
114 if prefix.is_empty() {
115 keys.push(key);
116 continue;
117 }
118
119 if key.starts_with(&sanitized_prefix) {
120 let remainder = key.strip_prefix(&sanitized_prefix).unwrap_or("");
124 keys.push(format!("{}{}", prefix, remainder));
125 }
126 }
127
128 Ok(keys)
129 }
130}
131
132pub struct DebouncedFileStorage {
133 adapter: FileStorageAdapter,
134 pending_writes: Mutex<HashMap<String, String>>,
135 last_flush: Mutex<std::time::Instant>,
136 flush_interval: std::time::Duration,
137}
138
139impl DebouncedFileStorage {
140 pub fn new(base_path: PathBuf, flush_interval_ms: u64) -> Result<Self> {
141 Ok(Self {
142 adapter: FileStorageAdapter::new(base_path)?,
143 pending_writes: Mutex::new(HashMap::new()),
144 last_flush: Mutex::new(std::time::Instant::now()),
145 flush_interval: std::time::Duration::from_millis(flush_interval_ms),
146 })
147 }
148
149 pub fn flush(&self) -> Result<()> {
150 let mut pending = self.pending_writes.lock().unwrap();
151 for (key, value) in pending.drain() {
152 self.adapter.put(&key, value)?;
153 }
154 *self.last_flush.lock().unwrap() = std::time::Instant::now();
155 Ok(())
156 }
157
158 fn maybe_flush(&self) -> Result<()> {
159 let last = *self.last_flush.lock().unwrap();
160 let pending_count = self.pending_writes.lock().unwrap().len();
161
162 if last.elapsed() >= self.flush_interval && pending_count > 0 {
163 self.flush()?;
164 }
165 Ok(())
166 }
167}
168
169impl StorageAdapter for DebouncedFileStorage {
170 fn get(&self, key: &str) -> Result<Option<String>> {
171 let pending = self.pending_writes.lock().unwrap();
172 if let Some(value) = pending.get(key) {
173 return Ok(Some(value.clone()));
174 }
175 drop(pending);
176 self.adapter.get(key)
177 }
178
179 fn put(&self, key: &str, value: String) -> Result<()> {
180 self.pending_writes
181 .lock()
182 .unwrap()
183 .insert(key.to_string(), value);
184 self.maybe_flush()
185 }
186
187 fn del(&self, key: &str) -> Result<()> {
188 self.pending_writes.lock().unwrap().remove(key);
189 self.adapter.del(key)
190 }
191
192 fn list(&self, prefix: &str) -> Result<Vec<String>> {
193 let mut keys = self.adapter.list(prefix)?;
194 let pending = self.pending_writes.lock().unwrap();
195
196 for key in pending.keys() {
197 if key.starts_with(prefix) && !keys.contains(key) {
198 keys.push(key.clone());
199 }
200 }
201
202 Ok(keys)
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use crate::StoredUserRecord;
210 use tempfile::TempDir;
211
212 #[test]
213 fn test_file_storage_adapter_basic() {
214 let temp_dir = TempDir::new().unwrap();
215 let adapter = FileStorageAdapter::new(temp_dir.path().to_path_buf()).unwrap();
216
217 assert!(adapter.get("test-key").unwrap().is_none());
218
219 adapter.put("test-key", "test-value".to_string()).unwrap();
220 assert_eq!(
221 adapter.get("test-key").unwrap(),
222 Some("test-value".to_string())
223 );
224
225 adapter.del("test-key").unwrap();
226 assert!(adapter.get("test-key").unwrap().is_none());
227 }
228
229 #[test]
230 fn test_file_storage_adapter_list() {
231 let temp_dir = TempDir::new().unwrap();
232 let adapter = FileStorageAdapter::new(temp_dir.path().to_path_buf()).unwrap();
233
234 adapter.put("user/alice", "data1".to_string()).unwrap();
235 adapter.put("user/bob", "data2".to_string()).unwrap();
236 adapter.put("invite/charlie", "data3".to_string()).unwrap();
237
238 let user_keys = adapter.list("user/").unwrap();
239 assert_eq!(user_keys.len(), 2);
240 assert!(user_keys.contains(&"user/alice".to_string()));
241 assert!(user_keys.contains(&"user/bob".to_string()));
242
243 let all_keys = adapter.list("").unwrap();
244 assert_eq!(all_keys.len(), 3);
245 }
246
247 #[test]
248 fn test_file_storage_adapter_json() {
249 let temp_dir = TempDir::new().unwrap();
250 let adapter = FileStorageAdapter::new(temp_dir.path().to_path_buf()).unwrap();
251
252 let user_record = StoredUserRecord {
253 user_id: "test-user".to_string(),
254 devices: vec![],
255 known_device_identities: vec![],
256 };
257
258 let json = serde_json::to_string(&user_record).unwrap();
259 adapter.put("user/test", json.clone()).unwrap();
260
261 let retrieved = adapter.get("user/test").unwrap().unwrap();
262 let parsed: StoredUserRecord = serde_json::from_str(&retrieved).unwrap();
263
264 assert_eq!(parsed.user_id, "test-user");
265 }
266
267 #[test]
268 fn test_debounced_storage() {
269 let temp_dir = TempDir::new().unwrap();
270 let storage = DebouncedFileStorage::new(temp_dir.path().to_path_buf(), 1000).unwrap();
271
272 storage.put("key1", "value1".to_string()).unwrap();
273
274 assert_eq!(storage.get("key1").unwrap(), Some("value1".to_string()));
275
276 assert!(storage.pending_writes.lock().unwrap().contains_key("key1"));
277
278 storage.flush().unwrap();
279
280 assert!(storage.pending_writes.lock().unwrap().is_empty());
281 assert_eq!(
282 storage.adapter.get("key1").unwrap(),
283 Some("value1".to_string())
284 );
285 }
286}