1use super::{FilesystemStateManager, StateManager, StateOptions, StateSnapshot, VersionedState};
4use crate::error::{Mecha10Error, Result};
5use crate::prelude::now_micros;
6use async_trait::async_trait;
7use serde::{de::DeserializeOwned, Serialize};
8use std::path::Path;
9use tokio::fs;
10
11async fn filenames_with_suffix(dir: &Path, suffix: &str, dir_label: &str) -> Result<Vec<String>> {
19 let mut names = Vec::new();
20 let mut entries = fs::read_dir(dir).await.map_err(|e| Mecha10Error::IoError {
21 message: format!("Failed to read {}: {}", dir_label, e),
22 suggestion: "Check directory permissions".to_string(),
23 })?;
24
25 while let Some(entry) = entries.next_entry().await.map_err(|e| Mecha10Error::IoError {
26 message: format!("Failed to read directory entry: {}", e),
27 suggestion: "Check directory permissions".to_string(),
28 })? {
29 let path = entry.path();
30 if let Some(filename) = path.file_name() {
31 let filename = filename.to_string_lossy().into_owned();
32 if filename.ends_with(suffix) {
33 names.push(filename);
34 }
35 }
36 }
37
38 Ok(names)
39}
40
41#[async_trait]
42impl StateManager for FilesystemStateManager {
43 async fn save_with_options<T: Serialize + Send + Sync>(
44 &self,
45 node_id: &str,
46 state: &T,
47 options: StateOptions,
48 ) -> Result<()> {
49 let versioned = if let Some(ttl) = options.ttl {
50 VersionedState::with_ttl(state, options.version, ttl)
51 } else {
52 VersionedState::new(state, options.version)
53 };
54
55 let path = self.state_path(node_id);
56
57 if options.create_backup && path.exists() {
58 let backup_path = path.with_extension("state.json.backup");
59 fs::copy(&path, backup_path).await.ok();
60 }
61
62 let json = serde_json::to_string_pretty(&versioned).map_err(|e| Mecha10Error::SerializationError {
63 message: format!("Failed to serialize state: {}", e),
64 suggestion: "Ensure state type implements Serialize".to_string(),
65 })?;
66
67 fs::write(&path, json).await.map_err(|e| Mecha10Error::IoError {
68 message: format!("Failed to write state file: {}", e),
69 suggestion: "Check file permissions".to_string(),
70 })?;
71
72 Ok(())
73 }
74
75 async fn load_versioned<T: DeserializeOwned + Send + Sync>(
76 &self,
77 node_id: &str,
78 ) -> Result<Option<VersionedState<T>>> {
79 let path = self.state_path(node_id);
80
81 if !path.exists() {
82 return Ok(None);
83 }
84
85 let json = fs::read_to_string(&path).await.map_err(|e| Mecha10Error::IoError {
86 message: format!("Failed to read state file: {}", e),
87 suggestion: "Check file permissions".to_string(),
88 })?;
89
90 let versioned: VersionedState<T> =
91 serde_json::from_str(&json).map_err(|e| Mecha10Error::SerializationError {
92 message: format!("Failed to deserialize state: {}", e),
93 suggestion: "State may be corrupted or type mismatch".to_string(),
94 })?;
95
96 if versioned.is_expired() {
97 return Ok(None);
98 }
99
100 Ok(Some(versioned))
101 }
102
103 async fn delete(&self, node_id: &str) -> Result<()> {
104 let path = self.state_path(node_id);
105
106 if path.exists() {
107 fs::remove_file(&path).await.map_err(|e| Mecha10Error::IoError {
108 message: format!("Failed to delete state file: {}", e),
109 suggestion: "Check file permissions".to_string(),
110 })?;
111 }
112
113 Ok(())
114 }
115
116 async fn exists(&self, node_id: &str) -> Result<bool> {
117 Ok(self.state_path(node_id).exists())
118 }
119
120 async fn list_nodes(&self) -> Result<Vec<String>> {
121 let filenames = filenames_with_suffix(&self.base_path, ".state.json", "state directory").await?;
122
123 Ok(filenames
124 .into_iter()
125 .map(|filename| filename.trim_end_matches(".state.json").to_string())
126 .collect())
127 }
128
129 async fn size(&self, node_id: &str) -> Result<Option<usize>> {
130 let path = self.state_path(node_id);
131
132 if !path.exists() {
133 return Ok(None);
134 }
135
136 let metadata = fs::metadata(&path).await.map_err(|e| Mecha10Error::IoError {
137 message: format!("Failed to get file metadata: {}", e),
138 suggestion: "Check file permissions".to_string(),
139 })?;
140
141 Ok(Some(metadata.len() as usize))
142 }
143
144 async fn create_snapshot(&self, node_id: &str, description: Option<String>) -> Result<StateSnapshot> {
145 let state_path = self.state_path(node_id);
146
147 if !state_path.exists() {
148 return Err(Mecha10Error::Configuration(format!("No state for node: {}", node_id)));
149 }
150
151 let snapshot_id = uuid::Uuid::new_v4().to_string();
152
153 let snapshots_dir = self.base_path.join("snapshots");
154 fs::create_dir_all(&snapshots_dir)
155 .await
156 .map_err(|e| Mecha10Error::IoError {
157 message: format!("Failed to create snapshots directory: {}", e),
158 suggestion: "Check directory permissions".to_string(),
159 })?;
160
161 let snapshot_path = self.snapshot_path(&snapshot_id);
162 fs::copy(&state_path, &snapshot_path)
163 .await
164 .map_err(|e| Mecha10Error::IoError {
165 message: format!("Failed to create snapshot: {}", e),
166 suggestion: "Check file permissions".to_string(),
167 })?;
168
169 let size_bytes = self.size(node_id).await?.unwrap_or(0);
170
171 let snapshot = StateSnapshot {
172 id: snapshot_id.clone(),
173 node_id: node_id.to_string(),
174 timestamp: now_micros(),
175 description,
176 size_bytes,
177 };
178
179 let meta_path = self.snapshot_meta_path(&snapshot_id);
180 let meta_json = serde_json::to_string_pretty(&snapshot).unwrap();
181 fs::write(&meta_path, meta_json)
182 .await
183 .map_err(|e| Mecha10Error::IoError {
184 message: format!("Failed to write snapshot metadata: {}", e),
185 suggestion: "Check file permissions".to_string(),
186 })?;
187
188 Ok(snapshot)
189 }
190
191 async fn restore_snapshot(&self, snapshot_id: &str) -> Result<()> {
192 let snapshot_path = self.snapshot_path(snapshot_id);
193 let meta_path = self.snapshot_meta_path(snapshot_id);
194
195 if !snapshot_path.exists() || !meta_path.exists() {
196 return Err(Mecha10Error::Configuration(format!(
197 "Snapshot not found: {}",
198 snapshot_id
199 )));
200 }
201
202 let meta_json = fs::read_to_string(&meta_path)
203 .await
204 .map_err(|e| Mecha10Error::IoError {
205 message: format!("Failed to read snapshot metadata: {}", e),
206 suggestion: "Check file permissions".to_string(),
207 })?;
208
209 let snapshot: StateSnapshot =
210 serde_json::from_str(&meta_json).map_err(|e| Mecha10Error::SerializationError {
211 message: format!("Failed to deserialize snapshot metadata: {}", e),
212 suggestion: "Snapshot metadata may be corrupted".to_string(),
213 })?;
214
215 let state_path = self.state_path(&snapshot.node_id);
216 fs::copy(&snapshot_path, &state_path)
217 .await
218 .map_err(|e| Mecha10Error::IoError {
219 message: format!("Failed to restore snapshot: {}", e),
220 suggestion: "Check file permissions".to_string(),
221 })?;
222
223 Ok(())
224 }
225
226 async fn list_snapshots(&self, node_id: &str) -> Result<Vec<StateSnapshot>> {
227 let snapshots_dir = self.base_path.join("snapshots");
228
229 if !snapshots_dir.exists() {
230 return Ok(Vec::new());
231 }
232
233 let mut snapshots = Vec::new();
234 let filenames = filenames_with_suffix(&snapshots_dir, ".meta.json", "snapshots directory").await?;
235
236 for filename in filenames {
237 let json = fs::read_to_string(snapshots_dir.join(filename)).await.ok();
238 if let Some(json) = json {
239 if let Ok(snapshot) = serde_json::from_str::<StateSnapshot>(&json) {
240 if snapshot.node_id == node_id {
241 snapshots.push(snapshot);
242 }
243 }
244 }
245 }
246
247 snapshots.sort_by_key(|s| s.timestamp);
248 snapshots.reverse();
249
250 Ok(snapshots)
251 }
252
253 async fn delete_snapshot(&self, snapshot_id: &str) -> Result<()> {
254 let snapshot_path = self.snapshot_path(snapshot_id);
255 let meta_path = self.snapshot_meta_path(snapshot_id);
256
257 if snapshot_path.exists() {
258 fs::remove_file(&snapshot_path).await.ok();
259 }
260
261 if meta_path.exists() {
262 fs::remove_file(&meta_path).await.ok();
263 }
264
265 Ok(())
266 }
267
268 async fn compare_and_swap<T: Serialize + DeserializeOwned + Send + Sync>(
269 &self,
270 node_id: &str,
271 expected: &T,
272 new: &T,
273 ) -> Result<bool> {
274 let path = self.state_path(node_id);
275
276 if !path.exists() {
277 return Ok(false);
278 }
279
280 let json = fs::read_to_string(&path).await.map_err(|e| Mecha10Error::IoError {
281 message: format!("Failed to read state file: {}", e),
282 suggestion: "Check file permissions".to_string(),
283 })?;
284
285 let current: VersionedState<T> = serde_json::from_str(&json).map_err(|e| Mecha10Error::SerializationError {
286 message: format!("Failed to deserialize for CAS: {}", e),
287 suggestion: "State may be corrupted".to_string(),
288 })?;
289
290 let expected_json = serde_json::to_string(expected).unwrap();
291 let current_json = serde_json::to_string(¤t.data).unwrap();
292
293 if expected_json == current_json {
294 let versioned = VersionedState::new(new, current.version + 1);
295 let new_json = serde_json::to_string_pretty(&versioned).unwrap();
296 fs::write(&path, new_json).await.map_err(|e| Mecha10Error::IoError {
297 message: format!("Failed to write state file: {}", e),
298 suggestion: "Check file permissions".to_string(),
299 })?;
300 Ok(true)
301 } else {
302 Ok(false)
303 }
304 }
305
306 async fn clear_expired(&self) -> Result<usize> {
307 let nodes = self.list_nodes().await?;
308 let mut count = 0;
309
310 for node_id in nodes {
311 if let Ok(Some(versioned)) = self.load_versioned::<serde_json::Value>(&node_id).await {
312 if versioned.is_expired() {
313 self.delete(&node_id).await?;
314 count += 1;
315 }
316 }
317 }
318
319 Ok(count)
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[tokio::test]
328 async fn filenames_with_suffix_returns_only_matching_names() {
329 let dir = tempfile::tempdir().expect("tempdir");
330 fs::write(dir.path().join("a.state.json"), b"{}").await.unwrap();
331 fs::write(dir.path().join("b.state.json"), b"{}").await.unwrap();
332 fs::write(dir.path().join("c.meta.json"), b"{}").await.unwrap();
333 fs::write(dir.path().join("d.txt"), b"ignored").await.unwrap();
334
335 let mut names = filenames_with_suffix(dir.path(), ".state.json", "state directory")
336 .await
337 .unwrap();
338 names.sort();
339
340 assert_eq!(names, vec!["a.state.json".to_string(), "b.state.json".to_string()]);
341 }
342
343 #[tokio::test]
344 async fn filenames_with_suffix_returns_empty_for_empty_dir() {
345 let dir = tempfile::tempdir().expect("tempdir");
346
347 let names = filenames_with_suffix(dir.path(), ".meta.json", "snapshots directory")
348 .await
349 .unwrap();
350
351 assert!(names.is_empty());
352 }
353
354 #[tokio::test]
355 async fn filenames_with_suffix_errors_on_missing_dir() {
356 let dir = tempfile::tempdir().expect("tempdir");
357 let missing = dir.path().join("does-not-exist");
358
359 let err = filenames_with_suffix(&missing, ".state.json", "state directory")
360 .await
361 .unwrap_err();
362
363 assert!(err.to_string().contains("state directory"));
364 }
365}