hopr_chain_indexer/snapshot/
mod.rs

1//! Fast synchronization using database snapshots.
2//!
3//! This module enables HOPR nodes to synchronize quickly with the network by downloading
4//! and installing pre-built database snapshots instead of processing all historical blockchain logs.
5//!
6//! # Features
7//!
8//! - **HTTP/HTTPS Downloads**: Secure download with retry logic and progress tracking
9//! - **Local File Support**: Direct installation from local `file://` URLs
10//! - **Archive Extraction**: Safe tar.xz extraction with path traversal protection
11//! - **Database Validation**: SQLite integrity checks and content verification
12//! - **Disk Space Management**: Cross-platform space validation before operations
13//! - **Comprehensive Errors**: Actionable error messages with recovery suggestions
14//!
15//! # URL Support
16//!
17//! - `https://example.com/snapshot.tar.xz` - Remote HTTP/HTTPS downloads
18//! - `file:///path/to/snapshot.tar.xz` - Local file system access
19//!
20//! # Example
21//!
22//! ```no_run
23//! use std::path::Path;
24//! use hopr_chain_indexer::snapshot::{SnapshotResult, SnapshotManager};
25//!
26//! # async fn example(db: impl hopr_db_sql::HoprDbGeneralModelOperations + Clone + Send + Sync + 'static) -> SnapshotResult<()> {
27//! let manager = SnapshotManager::with_db(db)?;
28//! let info = manager
29//!     .download_and_setup_snapshot(
30//!         "https://snapshots.hoprnet.org/logs.tar.xz",
31//!         Path::new("/data/hopr")
32//!     )
33//!     .await?;
34//!
35//! println!("Installed snapshot: {} logs, latest block {}", info.log_count, info.latest_block.unwrap_or(0));
36//! # Ok(())
37//! # }
38//! ```
39
40pub mod download;
41pub mod error;
42pub mod extract;
43pub mod validate;
44
45#[cfg(test)]
46pub(crate) mod test_utils;
47
48// Re-export commonly used types
49use std::{fs, path::Path};
50
51pub use error::{SnapshotError, SnapshotResult};
52use hopr_db_sql::HoprDbGeneralModelOperations;
53use tracing::{debug, error, info};
54pub use validate::SnapshotInfo;
55
56use crate::snapshot::{download::SnapshotDownloader, extract::SnapshotExtractor, validate::SnapshotValidator};
57
58/// Trait for implementing the snapshot installation step.
59///
60/// This trait abstracts the final installation step of the snapshot workflow,
61/// allowing different implementations for production (database integration)
62/// and testing (filesystem copy) scenarios.
63#[async_trait::async_trait]
64trait SnapshotInstaller {
65    /// Installs the validated snapshot from the temporary directory.
66    ///
67    /// # Arguments
68    /// * `temp_dir` - Directory containing extracted and validated snapshot files
69    /// * `data_dir` - Target directory for installation
70    /// * `extracted_files` - List of files that were extracted from the archive
71    ///
72    /// # Returns
73    /// Result indicating success or failure of installation
74    async fn install_snapshot(
75        &self,
76        temp_dir: &Path,
77        data_dir: &Path,
78        extracted_files: &[String],
79    ) -> SnapshotResult<()>;
80}
81
82/// Shared snapshot workflow implementation.
83///
84/// Contains the common download → extract → validate → install workflow
85/// shared between SnapshotManager and TestSnapshotManager.
86pub(crate) struct SnapshotWorkflow {
87    downloader: SnapshotDownloader,
88    extractor: SnapshotExtractor,
89    validator: SnapshotValidator,
90}
91
92impl SnapshotWorkflow {
93    /// Creates a new snapshot workflow with default components.
94    fn new() -> Result<Self, SnapshotError> {
95        Ok(Self {
96            downloader: SnapshotDownloader::new()?,
97            extractor: SnapshotExtractor::new(),
98            validator: SnapshotValidator::new(),
99        })
100    }
101
102    /// Executes the complete snapshot workflow.
103    ///
104    /// Downloads, extracts, validates, and installs a snapshot using the provided installer.
105    async fn execute_workflow<I: SnapshotInstaller>(
106        &self,
107        installer: &I,
108        url: &str,
109        data_dir: &Path,
110        use_temp_subdir: bool,
111    ) -> SnapshotResult<SnapshotInfo> {
112        info!("Starting snapshot download and setup from: {}", url);
113
114        // Create temporary directory - either as subdirectory or using tempfile
115        let (temp_dir, _temp_guard) = if use_temp_subdir {
116            let temp_dir = data_dir.join("snapshot_temp");
117            fs::create_dir_all(&temp_dir)?;
118            (temp_dir, None)
119        } else {
120            let temp_guard = tempfile::tempdir_in(data_dir)?;
121            let temp_dir = temp_guard.path().to_path_buf();
122            (temp_dir, Some(temp_guard))
123        };
124
125        // Download snapshot
126        let archive_path = temp_dir.join("snapshot.tar.xz");
127        self.downloader.download_snapshot(url, &archive_path).await?;
128
129        // Extract snapshot
130        let extracted_files = self.extractor.extract_snapshot(&archive_path, &temp_dir).await?;
131        debug!("Extracted snapshot files: {:?}", extracted_files);
132
133        // Validate extracted database
134        let db_path = temp_dir.join("hopr_logs.db");
135        let snapshot_info = self.validator.validate_snapshot(&db_path).await?;
136
137        // Install using the provided installer
138        installer
139            .install_snapshot(&temp_dir, data_dir, &extracted_files)
140            .await?;
141
142        // Cleanup temporary directory if we created it manually
143        if use_temp_subdir {
144            if let Err(e) = fs::remove_dir_all(&temp_dir) {
145                error!("Failed to cleanup temp directory: {}", e);
146            }
147        }
148        // tempfile cleanup is automatic via Drop
149
150        info!("Snapshot setup completed successfully");
151        Ok(snapshot_info)
152    }
153}
154
155/// Coordinates snapshot download, extraction, validation, and database integration.
156///
157/// The main interface for snapshot operations in production environments.
158/// Manages the complete workflow from download to database installation.
159///
160/// # Architecture
161///
162/// - [`SnapshotDownloader`] - HTTP/HTTPS and file:// URL handling with retry logic
163/// - [`SnapshotExtractor`] - Secure tar.xz extraction with path validation
164/// - [`SnapshotValidator`] - SQLite integrity and content verification
165/// - Database integration via [`HoprDbGeneralModelOperations::import_logs_db`]
166pub struct SnapshotManager<Db>
167where
168    Db: HoprDbGeneralModelOperations + Clone + Send + Sync + 'static,
169{
170    db: Db,
171    workflow: SnapshotWorkflow,
172}
173
174impl<Db> SnapshotManager<Db>
175where
176    Db: HoprDbGeneralModelOperations + Clone + Send + Sync + 'static,
177{
178    /// Creates a snapshot manager with database integration.
179    ///
180    /// # Arguments
181    ///
182    /// * `db` - Database instance implementing [`HoprDbGeneralModelOperations`]
183    ///
184    /// # Example
185    ///
186    /// ```no_run
187    /// # use hopr_chain_indexer::snapshot::{SnapshotResult, SnapshotManager};
188    ///
189    /// # fn example(db: impl hopr_db_sql::HoprDbGeneralModelOperations + Clone + Send + Sync + 'static) -> SnapshotResult<()> {
190    /// let manager = SnapshotManager::with_db(db)?;
191    /// # Ok(())
192    /// # }
193    /// ```
194    pub fn with_db(db: Db) -> Result<Self, SnapshotError> {
195        Ok(Self {
196            db,
197            workflow: SnapshotWorkflow::new()?,
198        })
199    }
200
201    /// Downloads, extracts, validates, and installs a snapshot.
202    ///
203    /// Performs the complete snapshot setup workflow:
204    /// 1. Downloads archive from URL (HTTP/HTTPS/file://)
205    /// 2. Extracts tar.xz archive safely
206    /// 3. Validates database integrity
207    /// 4. Installs via [`HoprDbGeneralModelOperations::import_logs_db`]
208    /// 5. Cleans up temporary files
209    ///
210    /// # Arguments
211    ///
212    /// * `url` - Snapshot URL (`https://`, `http://`, or `file://` scheme)
213    /// * `data_dir` - Target directory for temporary files during installation
214    ///
215    /// # Returns
216    ///
217    /// [`SnapshotInfo`] containing log count, block count, and metadata on success
218    ///
219    /// # Errors
220    ///
221    /// Returns [`SnapshotError`] for network failures, validation errors, or installation issues
222    ///
223    /// # Examples
224    ///
225    /// ```no_run
226    /// # use std::path::Path;
227    /// # use hopr_chain_indexer::snapshot::SnapshotManager;
228    /// # async fn example(manager: SnapshotManager<impl hopr_db_sql::HoprDbGeneralModelOperations + Clone + Send + Sync + 'static>) -> Result<(), Box<dyn std::error::Error>> {
229    /// // Download from HTTPS
230    /// let info = manager
231    ///     .download_and_setup_snapshot("https://snapshots.hoprnet.org/logs.tar.xz", Path::new("/data"))
232    ///     .await?;
233    ///
234    /// // Use local file
235    /// let info = manager
236    ///     .download_and_setup_snapshot("file:///backups/snapshot.tar.xz", Path::new("/data"))
237    ///     .await?;
238    /// # Ok(())
239    /// # }
240    /// ```
241    pub async fn download_and_setup_snapshot(&self, url: &str, data_dir: &Path) -> SnapshotResult<SnapshotInfo> {
242        self.workflow.execute_workflow(self, url, data_dir, true).await
243    }
244}
245
246#[async_trait::async_trait]
247impl<Db> SnapshotInstaller for SnapshotManager<Db>
248where
249    Db: HoprDbGeneralModelOperations + Clone + Send + Sync + 'static,
250{
251    async fn install_snapshot(
252        &self,
253        temp_dir: &Path,
254        _data_dir: &Path,
255        _extracted_files: &[String],
256    ) -> SnapshotResult<()> {
257        // Update database using the imported logs database
258        self.db
259            .clone()
260            .import_logs_db(temp_dir.to_path_buf())
261            .await
262            .map_err(|e| SnapshotError::Installation(e.to_string()))?;
263
264        Ok(())
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use tempfile::TempDir;
271
272    use super::{test_utils::*, *};
273
274    #[tokio::test]
275    async fn test_snapshot_manager_integration() {
276        let temp_dir = TempDir::new().unwrap();
277
278        // Create test archive
279        let archive_path = create_test_archive(&temp_dir, None).await.unwrap();
280
281        // Use TestSnapshotManager for testing
282        let manager = TestSnapshotManager::new().expect("Failed to create TestSnapshotManager");
283        let data_dir = temp_dir.path().join("data");
284        fs::create_dir_all(&data_dir).unwrap();
285
286        // Test file:// URL using TestSnapshotManager
287        let file_url = format!("file://{}", archive_path.display());
288        let result = manager.download_and_setup_snapshot(&file_url, &data_dir).await;
289
290        assert!(result.is_ok(), "TestSnapshotManager should handle file:// URLs");
291        let info = result.unwrap();
292        assert_eq!(info.log_count, 2);
293
294        // Verify the database file was installed
295        assert!(data_dir.join("hopr_logs.db").exists());
296    }
297
298    #[tokio::test]
299    async fn test_snapshot_manager_with_data_directory() {
300        let temp_dir = TempDir::new().unwrap();
301        let data_dir = temp_dir.path().join("hopr_data");
302        fs::create_dir_all(&data_dir).unwrap();
303
304        // Create a test archive
305        let archive_path = create_test_archive(&temp_dir, None).await.unwrap();
306
307        // Test file:// URL support using TestSnapshotManager
308        let manager = TestSnapshotManager::new().expect("Failed to create TestSnapshotManager");
309
310        // Test with file:// URL for local testing
311        let file_url = format!("file://{}", archive_path.display());
312
313        // Test the full workflow through TestSnapshotManager
314        let result = manager.download_and_setup_snapshot(&file_url, &data_dir).await;
315        assert!(result.is_ok(), "TestSnapshotManager should handle complete workflow");
316
317        let info = result.unwrap();
318        assert_eq!(info.log_count, 2);
319
320        // Verify the database file exists in the data directory
321        assert!(data_dir.join("hopr_logs.db").exists());
322
323        // Also test individual component access
324        let downloader = manager.workflow.downloader;
325        let downloaded_archive = data_dir.join("test_download.tar.xz");
326        let download_result = downloader.download_snapshot(&file_url, &downloaded_archive).await;
327        assert!(download_result.is_ok(), "file:// URL download should succeed");
328        assert!(downloaded_archive.exists(), "Downloaded archive should exist");
329    }
330}