mockforge_core/snapshots/
manager.rs1use crate::consistency::ConsistencyEngine;
7use crate::snapshots::types::{SnapshotComponents, SnapshotManifest, SnapshotMetadata};
8use crate::Result;
9use chrono::{DateTime, Utc};
10use sha2::{Digest, Sha256};
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13use tokio::fs;
14use tracing::{debug, error, info, warn};
15
16pub struct SnapshotManager {
21 base_dir: PathBuf,
23}
24
25impl SnapshotManager {
26 pub fn new(base_dir: Option<PathBuf>) -> Self {
30 let base_dir = base_dir.unwrap_or_else(|| {
31 dirs::home_dir()
32 .unwrap_or_else(|| PathBuf::from("."))
33 .join(".mockforge")
34 .join("snapshots")
35 });
36
37 Self { base_dir }
38 }
39
40 fn workspace_dir(&self, workspace_id: &str) -> PathBuf {
42 self.base_dir.join(workspace_id)
43 }
44
45 fn snapshot_dir(&self, workspace_id: &str, snapshot_name: &str) -> PathBuf {
47 self.workspace_dir(workspace_id).join(snapshot_name)
48 }
49
50 pub async fn save_snapshot(
54 &self,
55 name: String,
56 description: Option<String>,
57 workspace_id: String,
58 components: SnapshotComponents,
59 consistency_engine: Option<&ConsistencyEngine>,
60 ) -> Result<SnapshotManifest> {
62 info!("Saving snapshot '{}' for workspace '{}'", name, workspace_id);
63
64 let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
66 fs::create_dir_all(&snapshot_dir).await?;
67
68 let temp_dir = snapshot_dir.join(".tmp");
70 fs::create_dir_all(&temp_dir).await?;
71
72 let mut manifest =
73 SnapshotManifest::new(name.clone(), workspace_id.clone(), components.clone());
74
75 if components.unified_state {
77 if let Some(engine) = consistency_engine {
78 let unified_state = engine.get_state(&workspace_id).await;
79 if let Some(state) = unified_state {
80 let state_path = temp_dir.join("unified_state.json");
81 let state_json = serde_json::to_string_pretty(&state)?;
82 fs::write(&state_path, &state_json).await?;
83 debug!("Saved unified state to {}", state_path.display());
84 } else {
85 warn!("No unified state found for workspace {}", workspace_id);
86 }
87 }
88 }
89
90 if components.workspace_config {
92 let config_path = temp_dir.join("workspace_config.yaml");
94 let empty_config = serde_yaml::to_string(&serde_json::json!({}))?;
95 fs::write(&config_path, empty_config).await?;
96 debug!("Saved workspace config placeholder to {}", config_path.display());
97 }
98
99 if !components.protocols.is_empty() || components.protocols.is_empty() {
101 let protocols_dir = temp_dir.join("protocols");
102 fs::create_dir_all(&protocols_dir).await?;
103
104 if let Some(_engine) = consistency_engine {
105 let protocols: Vec<String> = if components.protocols.is_empty() {
107 vec![
108 "http".to_string(),
109 "graphql".to_string(),
110 "grpc".to_string(),
111 "websocket".to_string(),
112 "tcp".to_string(),
113 ]
114 } else {
115 components.protocols.clone()
116 };
117
118 for protocol_name in protocols {
119 let protocol_path = protocols_dir.join(format!("{}.json", protocol_name));
121 let empty_state = serde_json::json!({});
122 fs::write(&protocol_path, serde_json::to_string_pretty(&empty_state)?).await?;
123 }
124 }
125 }
126
127 let (size, checksum) = self.calculate_snapshot_checksum(&temp_dir).await?;
129 manifest.size_bytes = size;
130 manifest.checksum = checksum;
131 manifest.description = description;
132
133 let manifest_path = temp_dir.join("manifest.json");
135 let manifest_json = serde_json::to_string_pretty(&manifest)?;
136 fs::write(&manifest_path, &manifest_json).await?;
137
138 if snapshot_dir.exists() && snapshot_dir != temp_dir {
141 let old_backup = snapshot_dir.with_extension("old");
142 if old_backup.exists() {
143 fs::remove_dir_all(&old_backup).await?;
144 }
145 fs::rename(&snapshot_dir, &old_backup).await?;
146 }
147
148 if temp_dir.exists() {
150 let mut entries = fs::read_dir(&temp_dir).await?;
152 while let Some(entry) = entries.next_entry().await? {
153 let dest = snapshot_dir.join(entry.file_name());
154 fs::rename(entry.path(), &dest).await?;
155 }
156 fs::remove_dir(&temp_dir).await?;
157 }
158
159 info!("Snapshot '{}' saved successfully ({} bytes)", name, size);
160 Ok(manifest)
161 }
162
163 pub async fn load_snapshot(
167 &self,
168 name: String,
169 workspace_id: String,
170 components: Option<SnapshotComponents>,
171 consistency_engine: Option<&ConsistencyEngine>,
172 ) -> Result<SnapshotManifest> {
173 info!("Loading snapshot '{}' for workspace '{}'", name, workspace_id);
174
175 let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
176 if !snapshot_dir.exists() {
177 return Err(crate::Error::from(format!(
178 "Snapshot '{}' not found for workspace '{}'",
179 name, workspace_id
180 )));
181 }
182
183 let manifest_path = snapshot_dir.join("manifest.json");
185 let manifest_json = fs::read_to_string(&manifest_path).await?;
186 let manifest: SnapshotManifest = serde_json::from_str(&manifest_json)?;
187
188 let (size, checksum) = self.calculate_snapshot_checksum(&snapshot_dir).await?;
190 if checksum != manifest.checksum {
191 warn!("Snapshot checksum mismatch: expected {}, got {}", manifest.checksum, checksum);
192 }
194
195 let components_to_restore = components.unwrap_or_else(|| manifest.components.clone());
197
198 if components_to_restore.unified_state && manifest.components.unified_state {
200 if let Some(engine) = consistency_engine {
201 let state_path = snapshot_dir.join("unified_state.json");
202 if state_path.exists() {
203 let state_json = fs::read_to_string(&state_path).await?;
204 let unified_state: crate::consistency::UnifiedState =
205 serde_json::from_str(&state_json)?;
206 engine.restore_state(unified_state).await?;
207 debug!("Restored unified state from {}", state_path.display());
208 }
209 }
210 }
211
212 if components_to_restore.workspace_config && manifest.components.workspace_config {
214 let config_path = snapshot_dir.join("workspace_config.yaml");
215 if config_path.exists() {
216 debug!("Loaded workspace config from {}", config_path.display());
218 }
219 }
220
221 info!("Snapshot '{}' loaded successfully", name);
222 Ok(manifest)
223 }
224
225 pub async fn list_snapshots(&self, workspace_id: &str) -> Result<Vec<SnapshotMetadata>> {
227 let workspace_dir = self.workspace_dir(workspace_id);
228 if !workspace_dir.exists() {
229 return Ok(Vec::new());
230 }
231
232 let mut snapshots = Vec::new();
233 let mut entries = fs::read_dir(&workspace_dir).await?;
234
235 while let Some(entry) = entries.next_entry().await? {
236 let snapshot_name = entry.file_name().to_string_lossy().to_string();
237 if snapshot_name.starts_with('.') {
239 continue;
240 }
241
242 let manifest_path = entry.path().join("manifest.json");
243 if manifest_path.exists() {
244 match fs::read_to_string(&manifest_path).await {
245 Ok(manifest_json) => {
246 match serde_json::from_str::<SnapshotManifest>(&manifest_json) {
247 Ok(manifest) => {
248 snapshots.push(SnapshotMetadata::from(manifest));
249 }
250 Err(e) => {
251 warn!(
252 "Failed to parse manifest for snapshot {}: {}",
253 snapshot_name, e
254 );
255 }
256 }
257 }
258 Err(e) => {
259 warn!("Failed to read manifest for snapshot {}: {}", snapshot_name, e);
260 }
261 }
262 }
263 }
264
265 snapshots.sort_by(|a, b| b.created_at.cmp(&a.created_at));
267 Ok(snapshots)
268 }
269
270 pub async fn delete_snapshot(&self, name: String, workspace_id: String) -> Result<()> {
272 info!("Deleting snapshot '{}' for workspace '{}'", name, workspace_id);
273 let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
274 if snapshot_dir.exists() {
275 fs::remove_dir_all(&snapshot_dir).await?;
276 info!("Snapshot '{}' deleted successfully", name);
277 } else {
278 return Err(crate::Error::from(format!(
279 "Snapshot '{}' not found for workspace '{}'",
280 name, workspace_id
281 )));
282 }
283 Ok(())
284 }
285
286 pub async fn get_snapshot_info(
288 &self,
289 name: String,
290 workspace_id: String,
291 ) -> Result<SnapshotManifest> {
292 let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
293 let manifest_path = snapshot_dir.join("manifest.json");
294 if !manifest_path.exists() {
295 return Err(crate::Error::from(format!(
296 "Snapshot '{}' not found for workspace '{}'",
297 name, workspace_id
298 )));
299 }
300
301 let manifest_json = fs::read_to_string(&manifest_path).await?;
302 let manifest: SnapshotManifest = serde_json::from_str(&manifest_json)?;
303 Ok(manifest)
304 }
305
306 pub async fn validate_snapshot(&self, name: String, workspace_id: String) -> Result<bool> {
308 let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
309 let manifest_path = snapshot_dir.join("manifest.json");
310 if !manifest_path.exists() {
311 return Err(crate::Error::from(format!(
312 "Snapshot '{}' not found for workspace '{}'",
313 name, workspace_id
314 )));
315 }
316
317 let manifest_json = fs::read_to_string(&manifest_path).await?;
318 let manifest: SnapshotManifest = serde_json::from_str(&manifest_json)?;
319
320 let (_, checksum) = self.calculate_snapshot_checksum(&snapshot_dir).await?;
321 Ok(checksum == manifest.checksum)
322 }
323
324 async fn calculate_snapshot_checksum(&self, dir: &Path) -> Result<(u64, String)> {
326 let mut hasher = Sha256::new();
327 let mut total_size = 0u64;
328
329 let mut stack = vec![dir.to_path_buf()];
330 while let Some(current) = stack.pop() {
331 let mut entries = fs::read_dir(¤t).await?;
332 while let Some(entry) = entries.next_entry().await? {
333 let path = entry.path();
334 let metadata = fs::metadata(&path).await?;
335
336 if metadata.is_dir() {
337 if path
339 .file_name()
340 .and_then(|n| n.to_str())
341 .map(|s| s.starts_with('.'))
342 .unwrap_or(false)
343 {
344 continue;
345 }
346 stack.push(path);
347 } else if metadata.is_file() {
348 if path
350 .file_name()
351 .and_then(|n| n.to_str())
352 .map(|s| s == "manifest.json")
353 .unwrap_or(false)
354 {
355 continue;
356 }
357
358 let file_size = metadata.len();
359 total_size += file_size;
360
361 let content = fs::read(&path).await?;
362 hasher.update(&content);
363 hasher
364 .update(path.file_name().unwrap_or_default().to_string_lossy().as_bytes());
365 }
366 }
367 }
368
369 let checksum = format!("sha256:{:x}", hasher.finalize());
370 Ok((total_size, checksum))
371 }
372}
373
374impl Default for SnapshotManager {
375 fn default() -> Self {
376 Self::new(None)
377 }
378}