Skip to main content

rabia_persistence/
file_system.rs

1use async_trait::async_trait;
2use rabia_core::{persistence::PersistenceLayer, RabiaError, Result};
3use std::path::{Path, PathBuf};
4use tokio::fs;
5
6/// Simple file-based persistence implementation.
7///
8/// This implementation stores the state in a single file on disk. It provides
9/// persistent storage that survives process restarts.
10#[derive(Debug, Clone)]
11pub struct FileSystemPersistence {
12    state_file_path: PathBuf,
13}
14
15impl FileSystemPersistence {
16    /// Create a new file-based persistence instance.
17    ///
18    /// # Arguments
19    /// * `data_dir` - Directory path where the state file will be stored
20    ///
21    /// # Returns
22    /// * A new `FileSystemPersistence` instance
23    ///
24    /// # Errors
25    /// * Returns error if the data directory cannot be created
26    pub async fn new<P: AsRef<Path>>(data_dir: P) -> Result<Self> {
27        let data_dir = data_dir.as_ref();
28
29        // Create data directory if it doesn't exist
30        if !data_dir.exists() {
31            fs::create_dir_all(data_dir).await.map_err(|e| {
32                RabiaError::persistence(format!("Failed to create data directory: {}", e))
33            })?;
34        }
35
36        let state_file_path = data_dir.join("state.dat");
37
38        Ok(Self { state_file_path })
39    }
40
41    /// Create a new file-based persistence instance (synchronous).
42    ///
43    /// This is a convenience method that blocks on the async `new` method.
44    ///
45    /// # Arguments
46    /// * `data_dir` - Directory path where the state file will be stored
47    ///
48    /// # Returns
49    /// * A new `FileSystemPersistence` instance
50    ///
51    /// # Errors
52    /// * Returns error if the data directory cannot be created
53    pub fn new_sync<P: AsRef<Path>>(data_dir: P) -> Result<Self> {
54        let runtime = tokio::runtime::Runtime::new()
55            .map_err(|e| RabiaError::internal(format!("Failed to create runtime: {}", e)))?;
56        runtime.block_on(Self::new(data_dir))
57    }
58}
59
60#[async_trait]
61impl PersistenceLayer for FileSystemPersistence {
62    async fn save_state(&self, state: &[u8]) -> Result<()> {
63        // Write to a temporary file first, then atomically move to final location
64        let temp_file_path = self.state_file_path.with_extension("tmp");
65
66        fs::write(&temp_file_path, state).await.map_err(|e| {
67            RabiaError::persistence(format!("Failed to write state to temp file: {}", e))
68        })?;
69
70        // Atomically replace the old file with the new one
71        fs::rename(&temp_file_path, &self.state_file_path)
72            .await
73            .map_err(|e| {
74                RabiaError::persistence(format!("Failed to rename temp file to state file: {}", e))
75            })?;
76
77        Ok(())
78    }
79
80    async fn load_state(&self) -> Result<Option<Vec<u8>>> {
81        if !self.state_file_path.exists() {
82            return Ok(None);
83        }
84
85        match fs::read(&self.state_file_path).await {
86            Ok(data) => Ok(Some(data)),
87            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
88            Err(e) => Err(RabiaError::persistence(format!(
89                "Failed to read state file: {}",
90                e
91            ))),
92        }
93    }
94}