Skip to main content

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    #[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    /// Save a snapshot with protocol state exporters
92    ///
93    /// Extended version that accepts a map of protocol state exporters
94    /// for capturing state from multiple protocols.
95    #[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        // Create snapshot directory
111        let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
112        fs::create_dir_all(&snapshot_dir).await?;
113
114        // Create temporary directory for atomic writes
115        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        // Save unified state if requested
122        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        // Save workspace config if requested
137        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::io_with_context("serialize workspace", e.to_string())
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        // Save VBR state if requested
162        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        // Save Recorder state if requested
176        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        // Save protocol states if requested
190        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            // Determine which protocols to save
195            let protocols_to_save: Vec<String> = if components.protocols.is_empty() {
196                // If no specific protocols requested, save all available exporters
197                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                // Try to get state from exporter if available
206                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                    // No exporter available, save placeholder
234                    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        // Calculate checksum and size
249        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        // Write manifest
255        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        // Atomically move temp directory to final location
260        // Remove old snapshot if it exists
261        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        // Move temp to final location
270        if temp_dir.exists() {
271            // Move contents from temp_dir to snapshot_dir
272            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    /// Load a snapshot and restore system state
285    ///
286    /// Restores the specified components from a snapshot.
287    /// Returns the manifest and optionally the VBR and Recorder state as JSON
288    /// (caller is responsible for restoring them to their respective systems).
289    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        // Load manifest
308        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        // Validate checksum
313        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            // Continue anyway, but log warning
317        }
318
319        // Determine which components to restore
320        let components_to_restore = components.unwrap_or_else(|| manifest.components.clone());
321
322        // Restore unified state if requested
323        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        // Restore workspace config if requested
337        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::io_with_context("deserialize workspace", e.to_string())
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        // Load VBR state if requested (return as JSON for caller to restore)
359        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)
364                    .map_err(|e| crate::Error::io_with_context("parse VBR state", e.to_string()))?;
365                debug!("Loaded VBR state from {}", vbr_path.display());
366                Some(state)
367            } else {
368                warn!("VBR state file not found in snapshot: {}", vbr_path.display());
369                None
370            }
371        } else {
372            None
373        };
374
375        // Load Recorder state if requested (return as JSON for caller to restore)
376        let recorder_state =
377            if components_to_restore.recorder_state && manifest.components.recorder_state {
378                let recorder_path = snapshot_dir.join("recorder_state.json");
379                if recorder_path.exists() {
380                    let recorder_json = fs::read_to_string(&recorder_path).await?;
381                    let state: serde_json::Value =
382                        serde_json::from_str(&recorder_json).map_err(|e| {
383                            crate::Error::io_with_context("parse Recorder state", e.to_string())
384                        })?;
385                    debug!("Loaded Recorder state from {}", recorder_path.display());
386                    Some(state)
387                } else {
388                    warn!("Recorder state file not found in snapshot: {}", recorder_path.display());
389                    None
390                }
391            } else {
392                None
393            };
394
395        info!("Snapshot '{}' loaded successfully", name);
396        Ok((manifest, vbr_state, recorder_state))
397    }
398
399    /// Load a snapshot and restore system state with protocol exporters
400    ///
401    /// Extended version that accepts protocol state exporters to restore
402    /// protocol-specific state from snapshots.
403    ///
404    /// # Arguments
405    /// * `name` - Snapshot name
406    /// * `workspace_id` - Workspace identifier
407    /// * `components` - Which components to restore (uses manifest if None)
408    /// * `consistency_engine` - Optional consistency engine for unified state
409    /// * `workspace_persistence` - Optional workspace persistence for config
410    /// * `protocol_exporters` - Map of protocol exporters for restoring protocol state
411    pub async fn load_snapshot_with_exporters(
412        &self,
413        name: String,
414        workspace_id: String,
415        components: Option<SnapshotComponents>,
416        consistency_engine: Option<&ConsistencyEngine>,
417        workspace_persistence: Option<&WorkspacePersistence>,
418        protocol_exporters: HashMap<String, Arc<dyn ProtocolStateExporter>>,
419    ) -> Result<(SnapshotManifest, Option<serde_json::Value>, Option<serde_json::Value>)> {
420        // First load the base snapshot
421        let (manifest, vbr_state, recorder_state) = self
422            .load_snapshot(
423                name.clone(),
424                workspace_id.clone(),
425                components.clone(),
426                consistency_engine,
427                workspace_persistence,
428            )
429            .await?;
430
431        // Determine which components to restore
432        let components_to_restore = components.unwrap_or_else(|| manifest.components.clone());
433
434        // Restore protocol states if any exporters provided and protocols were saved
435        if !protocol_exporters.is_empty()
436            && (!components_to_restore.protocols.is_empty()
437                || !manifest.components.protocols.is_empty())
438        {
439            let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
440            let protocols_dir = snapshot_dir.join("protocols");
441
442            if protocols_dir.exists() {
443                // Determine which protocols to restore
444                let protocols_to_restore: Vec<String> =
445                    if components_to_restore.protocols.is_empty() {
446                        // If no specific protocols requested, restore all available
447                        manifest.components.protocols.clone()
448                    } else {
449                        components_to_restore.protocols.clone()
450                    };
451
452                for protocol_name in protocols_to_restore {
453                    let protocol_path = protocols_dir.join(format!("{}.json", protocol_name));
454
455                    if protocol_path.exists() {
456                        if let Some(exporter) = protocol_exporters.get(&protocol_name) {
457                            match fs::read_to_string(&protocol_path).await {
458                                Ok(state_json) => {
459                                    match serde_json::from_str::<serde_json::Value>(&state_json) {
460                                        Ok(state) => {
461                                            // Skip placeholder/error states
462                                            if state.get("state")
463                                                == Some(&serde_json::json!("no_exporter_available"))
464                                            {
465                                                debug!(
466                                                    "Skipping {} protocol restore - no exporter was available during save",
467                                                    protocol_name
468                                                );
469                                                continue;
470                                            }
471                                            if state.get("error").is_some() {
472                                                warn!(
473                                                    "Skipping {} protocol restore - state contains error from save",
474                                                    protocol_name
475                                                );
476                                                continue;
477                                            }
478
479                                            match exporter.import_state(state).await {
480                                                Ok(_) => {
481                                                    debug!(
482                                                        "Restored {} protocol state from {}",
483                                                        protocol_name,
484                                                        protocol_path.display()
485                                                    );
486                                                }
487                                                Err(e) => {
488                                                    warn!(
489                                                        "Failed to restore {} protocol state: {}",
490                                                        protocol_name, e
491                                                    );
492                                                }
493                                            }
494                                        }
495                                        Err(e) => {
496                                            warn!(
497                                                "Failed to parse {} protocol state: {}",
498                                                protocol_name, e
499                                            );
500                                        }
501                                    }
502                                }
503                                Err(e) => {
504                                    warn!(
505                                        "Failed to read {} protocol state file: {}",
506                                        protocol_name, e
507                                    );
508                                }
509                            }
510                        } else {
511                            debug!(
512                                "No exporter provided for protocol {}, skipping restore",
513                                protocol_name
514                            );
515                        }
516                    } else {
517                        debug!(
518                            "Protocol state file not found for {}: {}",
519                            protocol_name,
520                            protocol_path.display()
521                        );
522                    }
523                }
524            }
525        }
526
527        Ok((manifest, vbr_state, recorder_state))
528    }
529
530    /// List all snapshots for a workspace
531    pub async fn list_snapshots(&self, workspace_id: &str) -> Result<Vec<SnapshotMetadata>> {
532        let workspace_dir = self.workspace_dir(workspace_id);
533        if !workspace_dir.exists() {
534            return Ok(Vec::new());
535        }
536
537        let mut snapshots = Vec::new();
538        let mut entries = fs::read_dir(&workspace_dir).await?;
539
540        while let Some(entry) = entries.next_entry().await? {
541            let snapshot_name = entry.file_name().to_string_lossy().to_string();
542            // Skip hidden directories and temp directories
543            if snapshot_name.starts_with('.') {
544                continue;
545            }
546
547            let manifest_path = entry.path().join("manifest.json");
548            if manifest_path.exists() {
549                match fs::read_to_string(&manifest_path).await {
550                    Ok(manifest_json) => {
551                        match serde_json::from_str::<SnapshotManifest>(&manifest_json) {
552                            Ok(manifest) => {
553                                snapshots.push(SnapshotMetadata::from(manifest));
554                            }
555                            Err(e) => {
556                                warn!(
557                                    "Failed to parse manifest for snapshot {}: {}",
558                                    snapshot_name, e
559                                );
560                            }
561                        }
562                    }
563                    Err(e) => {
564                        warn!("Failed to read manifest for snapshot {}: {}", snapshot_name, e);
565                    }
566                }
567            }
568        }
569
570        // Sort by creation date (newest first)
571        snapshots.sort_by(|a, b| b.created_at.cmp(&a.created_at));
572        Ok(snapshots)
573    }
574
575    /// Delete a snapshot
576    pub async fn delete_snapshot(&self, name: String, workspace_id: String) -> Result<()> {
577        info!("Deleting snapshot '{}' for workspace '{}'", name, workspace_id);
578        let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
579        if snapshot_dir.exists() {
580            fs::remove_dir_all(&snapshot_dir).await?;
581            info!("Snapshot '{}' deleted successfully", name);
582        } else {
583            return Err(crate::Error::from(format!(
584                "Snapshot '{}' not found for workspace '{}'",
585                name, workspace_id
586            )));
587        }
588        Ok(())
589    }
590
591    /// Get snapshot information
592    pub async fn get_snapshot_info(
593        &self,
594        name: String,
595        workspace_id: String,
596    ) -> Result<SnapshotManifest> {
597        let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
598        let manifest_path = snapshot_dir.join("manifest.json");
599        if !manifest_path.exists() {
600            return Err(crate::Error::from(format!(
601                "Snapshot '{}' not found for workspace '{}'",
602                name, workspace_id
603            )));
604        }
605
606        let manifest_json = fs::read_to_string(&manifest_path).await?;
607        let manifest: SnapshotManifest = serde_json::from_str(&manifest_json)?;
608        Ok(manifest)
609    }
610
611    /// Validate snapshot integrity
612    pub async fn validate_snapshot(&self, name: String, workspace_id: String) -> Result<bool> {
613        let snapshot_dir = self.snapshot_dir(&workspace_id, &name);
614        let manifest_path = snapshot_dir.join("manifest.json");
615        if !manifest_path.exists() {
616            return Err(crate::Error::from(format!(
617                "Snapshot '{}' not found for workspace '{}'",
618                name, workspace_id
619            )));
620        }
621
622        let manifest_json = fs::read_to_string(&manifest_path).await?;
623        let manifest: SnapshotManifest = serde_json::from_str(&manifest_json)?;
624
625        let (_, checksum) = self.calculate_snapshot_checksum(&snapshot_dir).await?;
626        Ok(checksum == manifest.checksum)
627    }
628
629    /// Calculate checksum and size of snapshot directory
630    async fn calculate_snapshot_checksum(&self, dir: &Path) -> Result<(u64, String)> {
631        let mut hasher = Sha256::new();
632        let mut total_size = 0u64;
633
634        let mut stack = vec![dir.to_path_buf()];
635        while let Some(current) = stack.pop() {
636            let mut entries = fs::read_dir(&current).await?;
637            while let Some(entry) = entries.next_entry().await? {
638                let path = entry.path();
639                let metadata = fs::metadata(&path).await?;
640
641                if metadata.is_dir() {
642                    // Skip temp directories
643                    if path
644                        .file_name()
645                        .and_then(|n| n.to_str())
646                        .map(|s| s.starts_with('.'))
647                        .unwrap_or(false)
648                    {
649                        continue;
650                    }
651                    stack.push(path);
652                } else if metadata.is_file() {
653                    // Skip manifest.json from checksum calculation (it contains the checksum)
654                    if path
655                        .file_name()
656                        .and_then(|n| n.to_str())
657                        .map(|s| s == "manifest.json")
658                        .unwrap_or(false)
659                    {
660                        continue;
661                    }
662
663                    let file_size = metadata.len();
664                    total_size += file_size;
665
666                    let content = fs::read(&path).await?;
667                    hasher.update(&content);
668                    hasher
669                        .update(path.file_name().unwrap_or_default().to_string_lossy().as_bytes());
670                }
671            }
672        }
673
674        let checksum = format!("sha256:{:x}", hasher.finalize());
675        Ok((total_size, checksum))
676    }
677}
678
679impl Default for SnapshotManager {
680    fn default() -> Self {
681        Self::new(None)
682    }
683}