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::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        // 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::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        // 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).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        // Load Recorder state if requested (return as JSON for caller to restore)
377        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    /// Load a snapshot and restore system state with protocol exporters
401    ///
402    /// Extended version that accepts protocol state exporters to restore
403    /// protocol-specific state from snapshots.
404    ///
405    /// # Arguments
406    /// * `name` - Snapshot name
407    /// * `workspace_id` - Workspace identifier
408    /// * `components` - Which components to restore (uses manifest if None)
409    /// * `consistency_engine` - Optional consistency engine for unified state
410    /// * `workspace_persistence` - Optional workspace persistence for config
411    /// * `protocol_exporters` - Map of protocol exporters for restoring protocol state
412    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        // First load the base snapshot
422        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        // Determine which components to restore
433        let components_to_restore = components.unwrap_or_else(|| manifest.components.clone());
434
435        // Restore protocol states if any exporters provided and protocols were saved
436        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                // Determine which protocols to restore
445                let protocols_to_restore: Vec<String> =
446                    if components_to_restore.protocols.is_empty() {
447                        // If no specific protocols requested, restore all available
448                        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                                            // Skip placeholder/error states
463                                            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    /// List all snapshots for a workspace
532    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            // Skip hidden directories and temp directories
544            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        // Sort by creation date (newest first)
572        snapshots.sort_by(|a, b| b.created_at.cmp(&a.created_at));
573        Ok(snapshots)
574    }
575
576    /// Delete a snapshot
577    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    /// Get snapshot information
593    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    /// Validate snapshot integrity
613    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    /// Calculate checksum and size of snapshot directory
631    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(&current).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                    // Skip temp directories
644                    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                    // Skip manifest.json from checksum calculation (it contains the checksum)
655                    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}