1use crate::consistency::ConsistencyEngine;
7use crate::snapshots::state_exporter::ProtocolStateExporter;
8use crate::snapshots::types::{SnapshotComponents, SnapshotManifest, SnapshotMetadata};
9use crate::workspace_persistence::WorkspacePersistence;
10use crate::Result;
11use sha2::{Digest, Sha256};
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15use tokio::fs;
16use tracing::{debug, info, warn};
17
18pub struct SnapshotManager {
23 base_dir: PathBuf,
25}
26
27impl SnapshotManager {
28 pub fn new(base_dir: Option<PathBuf>) -> Self {
32 let base_dir = base_dir.unwrap_or_else(|| {
33 dirs::home_dir()
34 .unwrap_or_else(|| PathBuf::from("."))
35 .join(".mockforge")
36 .join("snapshots")
37 });
38
39 Self { base_dir }
40 }
41
42 fn workspace_dir(&self, workspace_id: &str) -> PathBuf {
44 self.base_dir.join(workspace_id)
45 }
46
47 fn snapshot_dir(&self, workspace_id: &str, snapshot_name: &str) -> PathBuf {
49 self.workspace_dir(workspace_id).join(snapshot_name)
50 }
51
52 pub async fn save_snapshot(
66 &self,
67 name: String,
68 description: Option<String>,
69 workspace_id: String,
70 components: SnapshotComponents,
71 consistency_engine: Option<&ConsistencyEngine>,
72 workspace_persistence: Option<&WorkspacePersistence>,
73 vbr_state: Option<serde_json::Value>,
74 recorder_state: Option<serde_json::Value>,
75 ) -> Result<SnapshotManifest> {
76 self.save_snapshot_with_exporters(
77 name,
78 description,
79 workspace_id,
80 components,
81 consistency_engine,
82 workspace_persistence,
83 vbr_state,
84 recorder_state,
85 HashMap::new(),
86 )
87 .await
88 }
89
90 pub async fn save_snapshot_with_exporters(
95 &self,
96 name: String,
97 description: Option<String>,
98 workspace_id: String,
99 components: SnapshotComponents,
100 consistency_engine: Option<&ConsistencyEngine>,
101 workspace_persistence: Option<&WorkspacePersistence>,
102 vbr_state: Option<serde_json::Value>,
103 recorder_state: Option<serde_json::Value>,
104 protocol_exporters: HashMap<String, Arc<dyn ProtocolStateExporter>>,
105 ) -> Result<SnapshotManifest> {
106 info!("Saving snapshot '{}' for workspace '{}'", name, workspace_id);
107
108 let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
110 fs::create_dir_all(&snapshot_dir).await?;
111
112 let temp_dir = snapshot_dir.join(".tmp");
114 fs::create_dir_all(&temp_dir).await?;
115
116 let mut manifest =
117 SnapshotManifest::new(name.clone(), workspace_id.clone(), components.clone());
118
119 if components.unified_state {
121 if let Some(engine) = consistency_engine {
122 let unified_state = engine.get_state(&workspace_id).await;
123 if let Some(state) = unified_state {
124 let state_path = temp_dir.join("unified_state.json");
125 let state_json = serde_json::to_string_pretty(&state)?;
126 fs::write(&state_path, &state_json).await?;
127 debug!("Saved unified state to {}", state_path.display());
128 } else {
129 warn!("No unified state found for workspace {}", workspace_id);
130 }
131 }
132 }
133
134 if components.workspace_config {
136 let config_path = temp_dir.join("workspace_config.yaml");
137 if let Some(persistence) = workspace_persistence {
138 match persistence.load_workspace(&workspace_id).await {
139 Ok(workspace) => {
140 let config_yaml = serde_yaml::to_string(&workspace).map_err(|e| {
141 crate::Error::generic(format!("Failed to serialize workspace: {}", e))
142 })?;
143 fs::write(&config_path, config_yaml).await?;
144 debug!("Saved workspace config to {}", config_path.display());
145 }
146 Err(e) => {
147 warn!("Failed to load workspace config for snapshot: {}. Saving empty config.", e);
148 let empty_config = serde_yaml::to_string(&serde_json::json!({}))?;
149 fs::write(&config_path, empty_config).await?;
150 }
151 }
152 } else {
153 warn!("Workspace persistence not provided, saving empty workspace config");
154 let empty_config = serde_yaml::to_string(&serde_json::json!({}))?;
155 fs::write(&config_path, empty_config).await?;
156 }
157 }
158
159 if components.vbr_state {
161 let vbr_path = temp_dir.join("vbr_state.json");
162 if let Some(state) = vbr_state {
163 let state_json = serde_json::to_string_pretty(&state)?;
164 fs::write(&vbr_path, state_json).await?;
165 debug!("Saved VBR state to {}", vbr_path.display());
166 } else {
167 warn!("VBR state requested but not provided, saving empty state");
168 let empty_state = serde_json::json!({});
169 fs::write(&vbr_path, serde_json::to_string_pretty(&empty_state)?).await?;
170 }
171 }
172
173 if components.recorder_state {
175 let recorder_path = temp_dir.join("recorder_state.json");
176 if let Some(state) = recorder_state {
177 let state_json = serde_json::to_string_pretty(&state)?;
178 fs::write(&recorder_path, state_json).await?;
179 debug!("Saved Recorder state to {}", recorder_path.display());
180 } else {
181 warn!("Recorder state requested but not provided, saving empty state");
182 let empty_state = serde_json::json!({});
183 fs::write(&recorder_path, serde_json::to_string_pretty(&empty_state)?).await?;
184 }
185 }
186
187 if !components.protocols.is_empty() || !protocol_exporters.is_empty() {
189 let protocols_dir = temp_dir.join("protocols");
190 fs::create_dir_all(&protocols_dir).await?;
191
192 let protocols_to_save: Vec<String> = if components.protocols.is_empty() {
194 protocol_exporters.keys().cloned().collect()
196 } else {
197 components.protocols.clone()
198 };
199
200 for protocol_name in protocols_to_save {
201 let protocol_path = protocols_dir.join(format!("{}.json", protocol_name));
202
203 if let Some(exporter) = protocol_exporters.get(&protocol_name) {
205 match exporter.export_state().await {
206 Ok(state) => {
207 let summary = exporter.state_summary().await;
208 fs::write(&protocol_path, serde_json::to_string_pretty(&state)?)
209 .await?;
210 debug!(
211 "Saved {} protocol state to {}: {}",
212 protocol_name,
213 protocol_path.display(),
214 summary
215 );
216 }
217 Err(e) => {
218 warn!(
219 "Failed to export {} protocol state: {}. Saving empty state.",
220 protocol_name, e
221 );
222 let empty_state = serde_json::json!({
223 "error": format!("Failed to export state: {}", e),
224 "protocol": protocol_name
225 });
226 fs::write(&protocol_path, serde_json::to_string_pretty(&empty_state)?)
227 .await?;
228 }
229 }
230 } else {
231 debug!(
233 "No exporter available for protocol {}, saving placeholder",
234 protocol_name
235 );
236 let placeholder_state = serde_json::json!({
237 "protocol": protocol_name,
238 "state": "no_exporter_available"
239 });
240 fs::write(&protocol_path, serde_json::to_string_pretty(&placeholder_state)?)
241 .await?;
242 }
243 }
244 }
245
246 let (size, checksum) = self.calculate_snapshot_checksum(&temp_dir).await?;
248 manifest.size_bytes = size;
249 manifest.checksum = checksum;
250 manifest.description = description;
251
252 let manifest_path = temp_dir.join("manifest.json");
254 let manifest_json = serde_json::to_string_pretty(&manifest)?;
255 fs::write(&manifest_path, &manifest_json).await?;
256
257 if snapshot_dir.exists() && snapshot_dir != temp_dir {
260 let old_backup = snapshot_dir.with_extension("old");
261 if old_backup.exists() {
262 fs::remove_dir_all(&old_backup).await?;
263 }
264 fs::rename(&snapshot_dir, &old_backup).await?;
265 }
266
267 if temp_dir.exists() {
269 let mut entries = fs::read_dir(&temp_dir).await?;
271 while let Some(entry) = entries.next_entry().await? {
272 let dest = snapshot_dir.join(entry.file_name());
273 fs::rename(entry.path(), &dest).await?;
274 }
275 fs::remove_dir(&temp_dir).await?;
276 }
277
278 info!("Snapshot '{}' saved successfully ({} bytes)", name, size);
279 Ok(manifest)
280 }
281
282 pub async fn load_snapshot(
288 &self,
289 name: String,
290 workspace_id: String,
291 components: Option<SnapshotComponents>,
292 consistency_engine: Option<&ConsistencyEngine>,
293 workspace_persistence: Option<&WorkspacePersistence>,
294 ) -> Result<(SnapshotManifest, Option<serde_json::Value>, Option<serde_json::Value>)> {
295 info!("Loading snapshot '{}' for workspace '{}'", name, workspace_id);
296
297 let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
298 if !snapshot_dir.exists() {
299 return Err(crate::Error::from(format!(
300 "Snapshot '{}' not found for workspace '{}'",
301 name, workspace_id
302 )));
303 }
304
305 let manifest_path = snapshot_dir.join("manifest.json");
307 let manifest_json = fs::read_to_string(&manifest_path).await?;
308 let manifest: SnapshotManifest = serde_json::from_str(&manifest_json)?;
309
310 let (size, checksum) = self.calculate_snapshot_checksum(&snapshot_dir).await?;
312 if checksum != manifest.checksum {
313 warn!("Snapshot checksum mismatch: expected {}, got {}", manifest.checksum, checksum);
314 }
316
317 let components_to_restore = components.unwrap_or_else(|| manifest.components.clone());
319
320 if components_to_restore.unified_state && manifest.components.unified_state {
322 if let Some(engine) = consistency_engine {
323 let state_path = snapshot_dir.join("unified_state.json");
324 if state_path.exists() {
325 let state_json = fs::read_to_string(&state_path).await?;
326 let unified_state: crate::consistency::UnifiedState =
327 serde_json::from_str(&state_json)?;
328 engine.restore_state(unified_state).await?;
329 debug!("Restored unified state from {}", state_path.display());
330 }
331 }
332 }
333
334 if components_to_restore.workspace_config && manifest.components.workspace_config {
336 let config_path = snapshot_dir.join("workspace_config.yaml");
337 if config_path.exists() {
338 if let Some(persistence) = workspace_persistence {
339 let config_yaml = fs::read_to_string(&config_path).await?;
340 let workspace: crate::workspace::Workspace = serde_yaml::from_str(&config_yaml)
341 .map_err(|e| {
342 crate::Error::generic(format!("Failed to deserialize workspace: {}", e))
343 })?;
344 persistence.save_workspace(&workspace).await?;
345 debug!("Restored workspace config from {}", config_path.display());
346 } else {
347 warn!(
348 "Workspace persistence not provided, skipping workspace config restoration"
349 );
350 }
351 } else {
352 warn!("Workspace config file not found in snapshot: {}", config_path.display());
353 }
354 }
355
356 let vbr_state = if components_to_restore.vbr_state && manifest.components.vbr_state {
358 let vbr_path = snapshot_dir.join("vbr_state.json");
359 if vbr_path.exists() {
360 let vbr_json = fs::read_to_string(&vbr_path).await?;
361 let state: serde_json::Value = serde_json::from_str(&vbr_json).map_err(|e| {
362 crate::Error::generic(format!("Failed to parse VBR state: {}", e))
363 })?;
364 debug!("Loaded VBR state from {}", vbr_path.display());
365 Some(state)
366 } else {
367 warn!("VBR state file not found in snapshot: {}", vbr_path.display());
368 None
369 }
370 } else {
371 None
372 };
373
374 let recorder_state =
376 if components_to_restore.recorder_state && manifest.components.recorder_state {
377 let recorder_path = snapshot_dir.join("recorder_state.json");
378 if recorder_path.exists() {
379 let recorder_json = fs::read_to_string(&recorder_path).await?;
380 let state: serde_json::Value =
381 serde_json::from_str(&recorder_json).map_err(|e| {
382 crate::Error::generic(format!("Failed to parse Recorder state: {}", e))
383 })?;
384 debug!("Loaded Recorder state from {}", recorder_path.display());
385 Some(state)
386 } else {
387 warn!("Recorder state file not found in snapshot: {}", recorder_path.display());
388 None
389 }
390 } else {
391 None
392 };
393
394 info!("Snapshot '{}' loaded successfully", name);
395 Ok((manifest, vbr_state, recorder_state))
396 }
397
398 pub async fn load_snapshot_with_exporters(
411 &self,
412 name: String,
413 workspace_id: String,
414 components: Option<SnapshotComponents>,
415 consistency_engine: Option<&ConsistencyEngine>,
416 workspace_persistence: Option<&WorkspacePersistence>,
417 protocol_exporters: HashMap<String, Arc<dyn ProtocolStateExporter>>,
418 ) -> Result<(SnapshotManifest, Option<serde_json::Value>, Option<serde_json::Value>)> {
419 let (manifest, vbr_state, recorder_state) = self
421 .load_snapshot(
422 name.clone(),
423 workspace_id.clone(),
424 components.clone(),
425 consistency_engine,
426 workspace_persistence,
427 )
428 .await?;
429
430 let components_to_restore = components.unwrap_or_else(|| manifest.components.clone());
432
433 if !protocol_exporters.is_empty()
435 && (!components_to_restore.protocols.is_empty()
436 || !manifest.components.protocols.is_empty())
437 {
438 let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
439 let protocols_dir = snapshot_dir.join("protocols");
440
441 if protocols_dir.exists() {
442 let protocols_to_restore: Vec<String> =
444 if components_to_restore.protocols.is_empty() {
445 manifest.components.protocols.clone()
447 } else {
448 components_to_restore.protocols.clone()
449 };
450
451 for protocol_name in protocols_to_restore {
452 let protocol_path = protocols_dir.join(format!("{}.json", protocol_name));
453
454 if protocol_path.exists() {
455 if let Some(exporter) = protocol_exporters.get(&protocol_name) {
456 match fs::read_to_string(&protocol_path).await {
457 Ok(state_json) => {
458 match serde_json::from_str::<serde_json::Value>(&state_json) {
459 Ok(state) => {
460 if state.get("state")
462 == Some(&serde_json::json!("no_exporter_available"))
463 {
464 debug!(
465 "Skipping {} protocol restore - no exporter was available during save",
466 protocol_name
467 );
468 continue;
469 }
470 if state.get("error").is_some() {
471 warn!(
472 "Skipping {} protocol restore - state contains error from save",
473 protocol_name
474 );
475 continue;
476 }
477
478 match exporter.import_state(state).await {
479 Ok(_) => {
480 debug!(
481 "Restored {} protocol state from {}",
482 protocol_name,
483 protocol_path.display()
484 );
485 }
486 Err(e) => {
487 warn!(
488 "Failed to restore {} protocol state: {}",
489 protocol_name, e
490 );
491 }
492 }
493 }
494 Err(e) => {
495 warn!(
496 "Failed to parse {} protocol state: {}",
497 protocol_name, e
498 );
499 }
500 }
501 }
502 Err(e) => {
503 warn!(
504 "Failed to read {} protocol state file: {}",
505 protocol_name, e
506 );
507 }
508 }
509 } else {
510 debug!(
511 "No exporter provided for protocol {}, skipping restore",
512 protocol_name
513 );
514 }
515 } else {
516 debug!(
517 "Protocol state file not found for {}: {}",
518 protocol_name,
519 protocol_path.display()
520 );
521 }
522 }
523 }
524 }
525
526 Ok((manifest, vbr_state, recorder_state))
527 }
528
529 pub async fn list_snapshots(&self, workspace_id: &str) -> Result<Vec<SnapshotMetadata>> {
531 let workspace_dir = self.workspace_dir(workspace_id);
532 if !workspace_dir.exists() {
533 return Ok(Vec::new());
534 }
535
536 let mut snapshots = Vec::new();
537 let mut entries = fs::read_dir(&workspace_dir).await?;
538
539 while let Some(entry) = entries.next_entry().await? {
540 let snapshot_name = entry.file_name().to_string_lossy().to_string();
541 if snapshot_name.starts_with('.') {
543 continue;
544 }
545
546 let manifest_path = entry.path().join("manifest.json");
547 if manifest_path.exists() {
548 match fs::read_to_string(&manifest_path).await {
549 Ok(manifest_json) => {
550 match serde_json::from_str::<SnapshotManifest>(&manifest_json) {
551 Ok(manifest) => {
552 snapshots.push(SnapshotMetadata::from(manifest));
553 }
554 Err(e) => {
555 warn!(
556 "Failed to parse manifest for snapshot {}: {}",
557 snapshot_name, e
558 );
559 }
560 }
561 }
562 Err(e) => {
563 warn!("Failed to read manifest for snapshot {}: {}", snapshot_name, e);
564 }
565 }
566 }
567 }
568
569 snapshots.sort_by(|a, b| b.created_at.cmp(&a.created_at));
571 Ok(snapshots)
572 }
573
574 pub async fn delete_snapshot(&self, name: String, workspace_id: String) -> Result<()> {
576 info!("Deleting snapshot '{}' for workspace '{}'", name, workspace_id);
577 let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
578 if snapshot_dir.exists() {
579 fs::remove_dir_all(&snapshot_dir).await?;
580 info!("Snapshot '{}' deleted successfully", name);
581 } else {
582 return Err(crate::Error::from(format!(
583 "Snapshot '{}' not found for workspace '{}'",
584 name, workspace_id
585 )));
586 }
587 Ok(())
588 }
589
590 pub async fn get_snapshot_info(
592 &self,
593 name: String,
594 workspace_id: String,
595 ) -> Result<SnapshotManifest> {
596 let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
597 let manifest_path = snapshot_dir.join("manifest.json");
598 if !manifest_path.exists() {
599 return Err(crate::Error::from(format!(
600 "Snapshot '{}' not found for workspace '{}'",
601 name, workspace_id
602 )));
603 }
604
605 let manifest_json = fs::read_to_string(&manifest_path).await?;
606 let manifest: SnapshotManifest = serde_json::from_str(&manifest_json)?;
607 Ok(manifest)
608 }
609
610 pub async fn validate_snapshot(&self, name: String, workspace_id: String) -> Result<bool> {
612 let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
613 let manifest_path = snapshot_dir.join("manifest.json");
614 if !manifest_path.exists() {
615 return Err(crate::Error::from(format!(
616 "Snapshot '{}' not found for workspace '{}'",
617 name, workspace_id
618 )));
619 }
620
621 let manifest_json = fs::read_to_string(&manifest_path).await?;
622 let manifest: SnapshotManifest = serde_json::from_str(&manifest_json)?;
623
624 let (_, checksum) = self.calculate_snapshot_checksum(&snapshot_dir).await?;
625 Ok(checksum == manifest.checksum)
626 }
627
628 async fn calculate_snapshot_checksum(&self, dir: &Path) -> Result<(u64, String)> {
630 let mut hasher = Sha256::new();
631 let mut total_size = 0u64;
632
633 let mut stack = vec![dir.to_path_buf()];
634 while let Some(current) = stack.pop() {
635 let mut entries = fs::read_dir(¤t).await?;
636 while let Some(entry) = entries.next_entry().await? {
637 let path = entry.path();
638 let metadata = fs::metadata(&path).await?;
639
640 if metadata.is_dir() {
641 if path
643 .file_name()
644 .and_then(|n| n.to_str())
645 .map(|s| s.starts_with('.'))
646 .unwrap_or(false)
647 {
648 continue;
649 }
650 stack.push(path);
651 } else if metadata.is_file() {
652 if path
654 .file_name()
655 .and_then(|n| n.to_str())
656 .map(|s| s == "manifest.json")
657 .unwrap_or(false)
658 {
659 continue;
660 }
661
662 let file_size = metadata.len();
663 total_size += file_size;
664
665 let content = fs::read(&path).await?;
666 hasher.update(&content);
667 hasher
668 .update(path.file_name().unwrap_or_default().to_string_lossy().as_bytes());
669 }
670 }
671 }
672
673 let checksum = format!("sha256:{:x}", hasher.finalize());
674 Ok((total_size, checksum))
675 }
676}
677
678impl Default for SnapshotManager {
679 fn default() -> Self {
680 Self::new(None)
681 }
682}