mockforge_core/snapshots/
manager.rs

1//! Snapshot manager for saving and restoring system states
2//!
3//! The snapshot manager provides functionality to save complete system states
4//! to disk and restore them later, enabling time travel capabilities.
5
6use 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
18/// Snapshot manager for saving and restoring system states
19///
20/// Manages snapshots stored in a directory structure:
21/// `~/.mockforge/snapshots/{workspace_id}/{snapshot_name}/`
22pub struct SnapshotManager {
23    /// Base directory for snapshots
24    base_dir: PathBuf,
25}
26
27impl SnapshotManager {
28    /// Create a new snapshot manager
29    ///
30    /// Defaults to `~/.mockforge/snapshots` if no base directory is provided.
31    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    /// Get the snapshot directory for a workspace
43    fn workspace_dir(&self, workspace_id: &str) -> PathBuf {
44        self.base_dir.join(workspace_id)
45    }
46
47    /// Get the snapshot directory for a specific snapshot
48    fn snapshot_dir(&self, workspace_id: &str, snapshot_name: &str) -> PathBuf {
49        self.workspace_dir(workspace_id).join(snapshot_name)
50    }
51
52    /// Save a snapshot of the current system state
53    ///
54    /// This creates a snapshot directory and saves all specified components.
55    ///
56    /// # Arguments
57    /// * `name` - Name for the snapshot
58    /// * `description` - Optional description
59    /// * `workspace_id` - Workspace identifier
60    /// * `components` - Which components to include
61    /// * `consistency_engine` - Optional consistency engine for unified state
62    /// * `workspace_persistence` - Optional workspace persistence for config
63    /// * `vbr_state` - Optional VBR state (pre-extracted JSON)
64    /// * `recorder_state` - Optional Recorder state (pre-extracted JSON)
65    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    /// Save a snapshot with protocol state exporters
91    ///
92    /// Extended version that accepts a map of protocol state exporters
93    /// for capturing state from multiple protocols.
94    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        // Create snapshot directory
109        let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
110        fs::create_dir_all(&snapshot_dir).await?;
111
112        // Create temporary directory for atomic writes
113        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        // Save unified state if requested
120        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        // Save workspace config if requested
135        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        // Save VBR state if requested
160        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        // Save Recorder state if requested
174        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        // Save protocol states if requested
188        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            // Determine which protocols to save
193            let protocols_to_save: Vec<String> = if components.protocols.is_empty() {
194                // If no specific protocols requested, save all available exporters
195                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                // Try to get state from exporter if available
204                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                    // No exporter available, save placeholder
232                    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        // Calculate checksum and size
247        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        // Write manifest
253        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        // Atomically move temp directory to final location
258        // Remove old snapshot if it exists
259        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        // Move temp to final location
268        if temp_dir.exists() {
269            // Move contents from temp_dir to snapshot_dir
270            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    /// Load a snapshot and restore system state
283    ///
284    /// Restores the specified components from a snapshot.
285    /// Returns the manifest and optionally the VBR and Recorder state as JSON
286    /// (caller is responsible for restoring them to their respective systems).
287    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        // Load manifest
306        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        // Validate checksum
311        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            // Continue anyway, but log warning
315        }
316
317        // Determine which components to restore
318        let components_to_restore = components.unwrap_or_else(|| manifest.components.clone());
319
320        // Restore unified state if requested
321        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        // Restore workspace config if requested
335        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        // Load VBR state if requested (return as JSON for caller to restore)
357        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        // Load Recorder state if requested (return as JSON for caller to restore)
375        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    /// Load a snapshot and restore system state with protocol exporters
399    ///
400    /// Extended version that accepts protocol state exporters to restore
401    /// protocol-specific state from snapshots.
402    ///
403    /// # Arguments
404    /// * `name` - Snapshot name
405    /// * `workspace_id` - Workspace identifier
406    /// * `components` - Which components to restore (uses manifest if None)
407    /// * `consistency_engine` - Optional consistency engine for unified state
408    /// * `workspace_persistence` - Optional workspace persistence for config
409    /// * `protocol_exporters` - Map of protocol exporters for restoring protocol state
410    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        // First load the base snapshot
420        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        // Determine which components to restore
431        let components_to_restore = components.unwrap_or_else(|| manifest.components.clone());
432
433        // Restore protocol states if any exporters provided and protocols were saved
434        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                // Determine which protocols to restore
443                let protocols_to_restore: Vec<String> =
444                    if components_to_restore.protocols.is_empty() {
445                        // If no specific protocols requested, restore all available
446                        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                                            // Skip placeholder/error states
461                                            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    /// List all snapshots for a workspace
530    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            // Skip hidden directories and temp directories
542            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        // Sort by creation date (newest first)
570        snapshots.sort_by(|a, b| b.created_at.cmp(&a.created_at));
571        Ok(snapshots)
572    }
573
574    /// Delete a snapshot
575    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    /// Get snapshot information
591    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    /// Validate snapshot integrity
611    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    /// Calculate checksum and size of snapshot directory
629    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(&current).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                    // Skip temp directories
642                    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                    // Skip manifest.json from checksum calculation (it contains the checksum)
653                    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}