Skip to main content

nodedb_lite/nodedb/core/
snapshot.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Native snapshot export and writable restore APIs.
4
5use std::path::Path;
6use std::sync::Arc;
7
8use nodedb_types::error::{NodeDbError, NodeDbResult};
9
10use crate::config::LiteConfig;
11use crate::storage::encryption::Encryption;
12use crate::storage::pagedb_storage::PagedbStorageDefault;
13
14use super::types::NodeDbLite;
15
16impl NodeDbLite<PagedbStorageDefault> {
17    /// Flush pending state and export a native snapshot to a new directory.
18    ///
19    /// Quiesce concurrent writes for one application-level point in time.
20    /// PageDB pins its published state and blocks storage writes during export.
21    /// This API is unavailable on WASM.
22    pub async fn snapshot_to(&self, path: impl AsRef<Path>) -> NodeDbResult<()> {
23        self.flush().await?;
24        self.storage
25            .export_snapshot(path.as_ref())
26            .await
27            .map_err(NodeDbError::from)
28    }
29
30    /// Restore a native snapshot into a new writable destination and retain Lite replica identity.
31    ///
32    /// Supply the source encryption mode and KDF costs. Passphrase restores create a fresh destination salt.
33    /// PageDB forks its storage identity through rekeying. Restore always uses fail-closed corruption handling.
34    /// This API is unavailable on WASM.
35    pub async fn restore_from(
36        source: impl AsRef<Path>,
37        destination: impl AsRef<Path>,
38        encryption: Encryption,
39        mut config: LiteConfig,
40    ) -> NodeDbResult<Arc<Self>> {
41        config.corruption_policy = crate::storage::corruption::CorruptionPolicy::FailClosed;
42        let destination = destination.as_ref();
43        let storage =
44            PagedbStorageDefault::restore_snapshot(source.as_ref(), destination, encryption)
45                .await
46                .map_err(NodeDbError::from)?;
47        Self::open_with_config(storage, config)
48            .await
49            .map_err(|error| {
50                NodeDbError::storage(format!(
51                    "snapshot writable fork remains at {} but Lite reopen returned: {error}",
52                    destination.display()
53                ))
54            })
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use nodedb_client::NodeDb;
62
63    #[tokio::test]
64    async fn incomplete_vector_state_stops_snapshot_before_destination_creation() {
65        let dir = tempfile::tempdir().unwrap();
66        let db = NodeDbLite::open_at_path_with_config(
67            dir.path().join("source"),
68            Encryption::Plaintext,
69            LiteConfig {
70                auto_flush_ms: 0,
71                sync_enabled: false,
72                ..LiteConfig::default()
73            },
74        )
75        .await
76        .unwrap();
77        db.vector_insert("vectors", "v1", &[1.0, 0.0], None)
78            .await
79            .unwrap();
80        crate::engine::vector::durable::remove(&*db.storage, "vectors", "v1")
81            .await
82            .unwrap();
83        let destination = dir.path().join("snapshot");
84        assert!(db.snapshot_to(&destination).await.is_err());
85        assert!(!destination.exists());
86    }
87}