fraiseql_server/backup/
storage.rs1use std::path::{Path, PathBuf};
4
5#[async_trait::async_trait]
7pub trait BackupStorage: Send + Sync {
8 async fn store(&self, backup_id: &str, data: &[u8]) -> Result<u64, StorageError>;
14
15 async fn retrieve(&self, backup_id: &str) -> Result<Vec<u8>, StorageError>;
17
18 async fn delete(&self, backup_id: &str) -> Result<(), StorageError>;
20
21 async fn list(&self) -> Result<Vec<String>, StorageError>;
23
24 async fn get_size(&self, backup_id: &str) -> Result<u64, StorageError>;
26
27 async fn exists(&self, backup_id: &str) -> Result<bool, StorageError>;
29}
30
31#[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
53pub struct LocalFileStorage {
55 base_path: PathBuf,
56}
57
58impl LocalFileStorage {
59 pub fn new(base_path: impl AsRef<Path>) -> Result<Self, StorageError> {
61 let path = base_path.as_ref().to_path_buf();
62
63 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 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}