Skip to main content

fraiseql_server/backup/
storage.rs

1//! Backup storage backends.
2
3use std::path::{Path, PathBuf};
4
5/// Storage backend trait.
6#[async_trait::async_trait]
7pub trait BackupStorage: Send + Sync {
8    /// Store backup data.
9    ///
10    /// # Arguments
11    /// * `backup_id` - Unique backup identifier
12    /// * `data` - Backup content bytes
13    async fn store(&self, backup_id: &str, data: &[u8]) -> Result<u64, StorageError>;
14
15    /// Retrieve backup data.
16    async fn retrieve(&self, backup_id: &str) -> Result<Vec<u8>, StorageError>;
17
18    /// Delete backup data.
19    async fn delete(&self, backup_id: &str) -> Result<(), StorageError>;
20
21    /// List all backup IDs.
22    async fn list(&self) -> Result<Vec<String>, StorageError>;
23
24    /// Get backup size in bytes.
25    async fn get_size(&self, backup_id: &str) -> Result<u64, StorageError>;
26
27    /// Check if backup exists.
28    async fn exists(&self, backup_id: &str) -> Result<bool, StorageError>;
29}
30
31/// Storage backend errors.
32#[derive(Debug, Clone)]
33pub enum StorageError {
34    IoError { message: String },
35    NotFound { backup_id: String },
36    PermissionDenied { message: String },
37    Other { message: String },
38}
39
40impl std::fmt::Display for StorageError {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        match self {
43            Self::IoError { message } => write!(f, "IO error: {}", message),
44            Self::NotFound { backup_id } => write!(f, "Backup not found: {}", backup_id),
45            Self::PermissionDenied { message } => write!(f, "Permission denied: {}", message),
46            Self::Other { message } => write!(f, "Storage error: {}", message),
47        }
48    }
49}
50
51impl std::error::Error for StorageError {}
52
53/// Local filesystem storage backend.
54pub struct LocalFileStorage {
55    base_path: PathBuf,
56}
57
58impl LocalFileStorage {
59    /// Create new local file storage.
60    pub fn new(base_path: impl AsRef<Path>) -> Result<Self, StorageError> {
61        let path = base_path.as_ref().to_path_buf();
62
63        // Create directory if it doesn't exist
64        std::fs::create_dir_all(&path).map_err(|e| StorageError::IoError {
65            message: format!("Failed to create backup directory: {}", e),
66        })?;
67
68        Ok(Self { base_path: path })
69    }
70
71    /// Get full path for a backup ID.
72    fn get_path(&self, backup_id: &str) -> PathBuf {
73        self.base_path.join(format!("{}.backup", backup_id))
74    }
75}
76
77#[async_trait::async_trait]
78impl BackupStorage for LocalFileStorage {
79    async fn store(&self, backup_id: &str, data: &[u8]) -> Result<u64, StorageError> {
80        let path = self.get_path(backup_id);
81
82        tokio::fs::write(&path, data).await.map_err(|e| StorageError::IoError {
83            message: format!("Failed to write backup: {}", e),
84        })?;
85
86        Ok(data.len() as u64)
87    }
88
89    async fn retrieve(&self, backup_id: &str) -> Result<Vec<u8>, StorageError> {
90        let path = self.get_path(backup_id);
91
92        if !path.exists() {
93            return Err(StorageError::NotFound {
94                backup_id: backup_id.to_string(),
95            });
96        }
97
98        tokio::fs::read(&path).await.map_err(|e| StorageError::IoError {
99            message: format!("Failed to read backup: {}", e),
100        })
101    }
102
103    async fn delete(&self, backup_id: &str) -> Result<(), StorageError> {
104        let path = self.get_path(backup_id);
105
106        if path.exists() {
107            tokio::fs::remove_file(&path).await.map_err(|e| StorageError::IoError {
108                message: format!("Failed to delete backup: {}", e),
109            })?;
110        }
111
112        Ok(())
113    }
114
115    async fn list(&self) -> Result<Vec<String>, StorageError> {
116        let mut entries = Vec::new();
117
118        let mut dir =
119            tokio::fs::read_dir(&self.base_path).await.map_err(|e| StorageError::IoError {
120                message: format!("Failed to list backups: {}", e),
121            })?;
122
123        while let Some(entry) = dir.next_entry().await.map_err(|e| StorageError::IoError {
124            message: format!("Failed to read backup entry: {}", e),
125        })? {
126            if let Some(name) = entry.file_name().to_str() {
127                if name.ends_with(".backup") {
128                    let backup_id = name.strip_suffix(".backup").unwrap_or(name).to_string();
129                    entries.push(backup_id);
130                }
131            }
132        }
133
134        Ok(entries)
135    }
136
137    async fn get_size(&self, backup_id: &str) -> Result<u64, StorageError> {
138        let path = self.get_path(backup_id);
139
140        if !path.exists() {
141            return Err(StorageError::NotFound {
142                backup_id: backup_id.to_string(),
143            });
144        }
145
146        let metadata = tokio::fs::metadata(&path).await.map_err(|e| StorageError::IoError {
147            message: format!("Failed to get backup size: {}", e),
148        })?;
149
150        Ok(metadata.len())
151    }
152
153    async fn exists(&self, backup_id: &str) -> Result<bool, StorageError> {
154        let path = self.get_path(backup_id);
155        Ok(path.exists())
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use tempfile::TempDir;
162
163    use super::*;
164
165    #[tokio::test]
166    async fn test_local_storage_store_retrieve() {
167        let tmpdir = TempDir::new().unwrap();
168        let storage = LocalFileStorage::new(tmpdir.path()).unwrap();
169
170        let data = b"test backup data";
171        let size = storage.store("test-backup-1", data).await.unwrap();
172        assert_eq!(size, data.len() as u64);
173
174        let retrieved = storage.retrieve("test-backup-1").await.unwrap();
175        assert_eq!(retrieved, data);
176    }
177
178    #[tokio::test]
179    async fn test_local_storage_delete() {
180        let tmpdir = TempDir::new().unwrap();
181        let storage = LocalFileStorage::new(tmpdir.path()).unwrap();
182
183        storage.store("test-backup-2", b"data").await.unwrap();
184        assert!(storage.exists("test-backup-2").await.unwrap());
185
186        storage.delete("test-backup-2").await.unwrap();
187        assert!(!storage.exists("test-backup-2").await.unwrap());
188    }
189
190    #[tokio::test]
191    async fn test_local_storage_list() {
192        let tmpdir = TempDir::new().unwrap();
193        let storage = LocalFileStorage::new(tmpdir.path()).unwrap();
194
195        storage.store("backup-1", b"data1").await.unwrap();
196        storage.store("backup-2", b"data2").await.unwrap();
197
198        let list = storage.list().await.unwrap();
199        assert_eq!(list.len(), 2);
200        assert!(list.contains(&"backup-1".to_string()));
201        assert!(list.contains(&"backup-2".to_string()));
202    }
203}