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