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}