vibesql_storage/
backend.rs

1//! Storage Backend Abstraction for Cross-Platform Storage
2//!
3//! This module provides a trait-based abstraction over filesystem operations,
4//! enabling vibesql to run on different platforms including WebAssembly with OPFS.
5
6#![allow(clippy::suspicious_open_options)]
7
8#[cfg(not(target_arch = "wasm32"))]
9use std::io;
10
11use crate::StorageError;
12
13/// Trait for platform-specific file operations
14///
15/// Implementations provide file I/O for different platforms:
16/// - `NativeFile`: Standard filesystem (Linux, macOS, Windows)
17/// - `OpfsFile`: Origin Private File System (WebAssembly/browsers)
18pub trait StorageFile: Send + Sync {
19    /// Read data from the file at a specific offset
20    ///
21    /// # Arguments
22    /// * `offset` - Byte offset from start of file
23    /// * `buf` - Buffer to read data into
24    ///
25    /// # Returns
26    /// Number of bytes read, or error if operation fails
27    fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<usize, StorageError>;
28
29    /// Write data to the file at a specific offset
30    ///
31    /// # Arguments
32    /// * `offset` - Byte offset from start of file
33    /// * `buf` - Data to write
34    ///
35    /// # Returns
36    /// Number of bytes written, or error if operation fails
37    fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<usize, StorageError>;
38
39    /// Ensure all data is persisted to storage
40    ///
41    /// # Returns
42    /// Ok on success, error if sync fails
43    fn sync_all(&mut self) -> Result<(), StorageError>;
44
45    /// Ensure file data (not metadata) is persisted to storage
46    ///
47    /// # Returns
48    /// Ok on success, error if sync fails
49    fn sync_data(&mut self) -> Result<(), StorageError>;
50
51    /// Get the current size of the file in bytes
52    ///
53    /// # Returns
54    /// Size in bytes, or error if operation fails
55    fn size(&self) -> Result<u64, StorageError>;
56}
57
58/// Trait for platform-specific storage backend
59///
60/// Implementations provide filesystem operations for different platforms:
61/// - `NativeStorage`: Standard filesystem using std::fs
62/// - `OpfsStorage`: Browser-based Origin Private File System
63pub trait StorageBackend: Send + Sync {
64    /// Create a new file at the specified path
65    ///
66    /// If the file already exists, it will be truncated.
67    ///
68    /// # Arguments
69    /// * `path` - Path to create the file
70    ///
71    /// # Returns
72    /// File handle, or error if creation fails
73    fn create_file(&self, path: &str) -> Result<Box<dyn StorageFile>, StorageError>;
74
75    /// Open an existing file, or create it if it doesn't exist
76    ///
77    /// # Arguments
78    /// * `path` - Path to open/create
79    ///
80    /// # Returns
81    /// File handle, or error if operation fails
82    fn open_file(&self, path: &str) -> Result<Box<dyn StorageFile>, StorageError>;
83
84    /// Delete a file at the specified path
85    ///
86    /// # Arguments
87    /// * `path` - Path to delete
88    ///
89    /// # Returns
90    /// Ok on success, error if deletion fails or file doesn't exist
91    fn delete_file(&self, path: &str) -> Result<(), StorageError>;
92
93    /// Check if a file exists at the specified path
94    ///
95    /// # Arguments
96    /// * `path` - Path to check
97    ///
98    /// # Returns
99    /// true if file exists, false otherwise
100    fn file_exists(&self, path: &str) -> bool;
101
102    /// Get the size of a file in bytes
103    ///
104    /// # Arguments
105    /// * `path` - Path to check
106    ///
107    /// # Returns
108    /// Size in bytes, or error if file doesn't exist
109    fn file_size(&self, path: &str) -> Result<u64, StorageError>;
110}
111
112/// Native filesystem storage implementation using std::fs
113///
114/// This backend provides storage using the standard library's filesystem operations.
115/// It works on all platforms that support std::fs (Linux, macOS, Windows).
116#[cfg(not(target_arch = "wasm32"))]
117pub mod native {
118    #[cfg(target_arch = "wasm32")]
119    use std::sync::Mutex;
120    use std::{
121        fs::{File, OpenOptions},
122        io::{Read, Seek, SeekFrom, Write},
123        path::{Path, PathBuf},
124    };
125
126    #[cfg(not(target_arch = "wasm32"))]
127    use parking_lot::Mutex;
128
129    use super::*;
130
131    /// Native file implementation using std::fs::File
132    pub struct NativeFile {
133        file: Mutex<File>,
134    }
135
136    impl StorageFile for NativeFile {
137        fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<usize, StorageError> {
138            let mut file = self.file.lock();
139
140            file.seek(SeekFrom::Start(offset)).map_err(|e| StorageError::IoError(e.to_string()))?;
141
142            match file.read_exact(buf) {
143                Ok(()) => Ok(buf.len()),
144                Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
145                    // Try to read what we can
146                    file.seek(SeekFrom::Start(offset))
147                        .map_err(|e| StorageError::IoError(e.to_string()))?;
148                    file.read(buf).map_err(|e| StorageError::IoError(e.to_string()))
149                }
150                Err(e) => Err(StorageError::IoError(e.to_string())),
151            }
152        }
153
154        fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<usize, StorageError> {
155            let mut file = self.file.lock();
156
157            file.seek(SeekFrom::Start(offset)).map_err(|e| StorageError::IoError(e.to_string()))?;
158
159            file.write_all(buf).map_err(|e| StorageError::IoError(e.to_string()))?;
160            Ok(buf.len())
161        }
162
163        fn sync_all(&mut self) -> Result<(), StorageError> {
164            let file = self.file.lock();
165            file.sync_all().map_err(|e| StorageError::IoError(e.to_string()))
166        }
167
168        fn sync_data(&mut self) -> Result<(), StorageError> {
169            let file = self.file.lock();
170            file.sync_data().map_err(|e| StorageError::IoError(e.to_string()))
171        }
172
173        fn size(&self) -> Result<u64, StorageError> {
174            let file = self.file.lock();
175            Ok(file.metadata().map_err(|e| StorageError::IoError(e.to_string()))?.len())
176        }
177    }
178
179    /// Native storage backend using std::fs
180    pub struct NativeStorage {
181        root: PathBuf,
182    }
183
184    impl NativeStorage {
185        /// Create a new native storage backend
186        ///
187        /// # Arguments
188        /// * `root` - Root directory for all files (will be created if it doesn't exist)
189        pub fn new<P: AsRef<Path>>(root: P) -> Result<Self, StorageError> {
190            let root = root.as_ref().to_path_buf();
191
192            // Create root directory if it doesn't exist
193            if !root.exists() {
194                std::fs::create_dir_all(&root).map_err(|e| StorageError::IoError(e.to_string()))?;
195            }
196
197            Ok(NativeStorage { root })
198        }
199
200        /// Get the full path for a relative path
201        fn full_path(&self, path: &str) -> PathBuf {
202            self.root.join(path)
203        }
204    }
205
206    impl StorageBackend for NativeStorage {
207        fn create_file(&self, path: &str) -> Result<Box<dyn StorageFile>, StorageError> {
208            let full_path = self.full_path(path);
209
210            // Create parent directories if needed
211            if let Some(parent) = full_path.parent() {
212                std::fs::create_dir_all(parent)
213                    .map_err(|e| StorageError::IoError(e.to_string()))?;
214            }
215
216            // Open with read+write permissions and truncate if exists
217            let file = OpenOptions::new()
218                .read(true)
219                .write(true)
220                .create(true)
221                .truncate(true)
222                .open(&full_path)
223                .map_err(|e| StorageError::IoError(e.to_string()))?;
224
225            Ok(Box::new(NativeFile { file: Mutex::new(file) }))
226        }
227
228        fn open_file(&self, path: &str) -> Result<Box<dyn StorageFile>, StorageError> {
229            let full_path = self.full_path(path);
230
231            // Create parent directories if needed
232            if let Some(parent) = full_path.parent() {
233                std::fs::create_dir_all(parent)
234                    .map_err(|e| StorageError::IoError(e.to_string()))?;
235            }
236
237            let file = OpenOptions::new()
238                .read(true)
239                .write(true)
240                .create(true)
241                .open(&full_path)
242                .map_err(|e| StorageError::IoError(e.to_string()))?;
243
244            Ok(Box::new(NativeFile { file: Mutex::new(file) }))
245        }
246
247        fn delete_file(&self, path: &str) -> Result<(), StorageError> {
248            let full_path = self.full_path(path);
249            std::fs::remove_file(&full_path).map_err(|e| StorageError::IoError(e.to_string()))
250        }
251
252        fn file_exists(&self, path: &str) -> bool {
253            self.full_path(path).exists()
254        }
255
256        fn file_size(&self, path: &str) -> Result<u64, StorageError> {
257            let full_path = self.full_path(path);
258            let metadata =
259                std::fs::metadata(&full_path).map_err(|e| StorageError::IoError(e.to_string()))?;
260            Ok(metadata.len())
261        }
262    }
263
264    #[cfg(test)]
265    mod tests {
266        use tempfile::TempDir;
267
268        use super::*;
269
270        #[test]
271        fn test_native_file_operations() {
272            let temp_dir = TempDir::new().unwrap();
273            let storage = NativeStorage::new(temp_dir.path()).unwrap();
274
275            // Create and write to file
276            let mut file = storage.create_file("test.db").unwrap();
277
278            let data = b"Hello, Storage!";
279            let written = file.write_at(0, data).unwrap();
280            assert_eq!(written, data.len());
281
282            file.sync_all().unwrap();
283
284            // Read back
285            let mut buf = vec![0u8; data.len()];
286            let read = file.read_at(0, &mut buf).unwrap();
287            assert_eq!(read, data.len());
288            assert_eq!(&buf, data);
289
290            // Check size
291            let size = file.size().unwrap();
292            assert_eq!(size, data.len() as u64);
293        }
294
295        #[test]
296        fn test_native_storage_operations() {
297            let temp_dir = TempDir::new().unwrap();
298            let storage = NativeStorage::new(temp_dir.path()).unwrap();
299
300            // File shouldn't exist initially
301            assert!(!storage.file_exists("test.db"));
302
303            // Create file
304            let mut file = storage.create_file("test.db").unwrap();
305            file.write_at(0, b"test").unwrap();
306            drop(file);
307
308            // Now it should exist
309            assert!(storage.file_exists("test.db"));
310
311            // Check size
312            let size = storage.file_size("test.db").unwrap();
313            assert_eq!(size, 4);
314
315            // Delete file
316            storage.delete_file("test.db").unwrap();
317            assert!(!storage.file_exists("test.db"));
318        }
319
320        #[test]
321        fn test_native_storage_with_subdirectories() {
322            let temp_dir = TempDir::new().unwrap();
323            let storage = NativeStorage::new(temp_dir.path()).unwrap();
324
325            // Create file in subdirectory (should auto-create parent dirs)
326            let mut file = storage.create_file("subdir/nested/test.db").unwrap();
327            file.write_at(0, b"nested").unwrap();
328            drop(file);
329
330            assert!(storage.file_exists("subdir/nested/test.db"));
331        }
332
333        #[test]
334        fn test_read_write_at_different_offsets() {
335            let temp_dir = TempDir::new().unwrap();
336            let storage = NativeStorage::new(temp_dir.path()).unwrap();
337
338            let mut file = storage.create_file("test.db").unwrap();
339
340            // Write at offset 0
341            file.write_at(0, b"AAAA").unwrap();
342
343            // Write at offset 100
344            file.write_at(100, b"BBBB").unwrap();
345
346            // Read at offset 0
347            let mut buf = vec![0u8; 4];
348            file.read_at(0, &mut buf).unwrap();
349            assert_eq!(&buf, b"AAAA");
350
351            // Read at offset 100
352            file.read_at(100, &mut buf).unwrap();
353            assert_eq!(&buf, b"BBBB");
354        }
355    }
356}
357
358/// Re-export native storage for convenience
359#[cfg(not(target_arch = "wasm32"))]
360pub use native::{NativeFile, NativeStorage};
361
362/// OPFS storage implementation for WebAssembly browsers
363///
364/// This backend provides storage using the Origin Private File System API,
365/// enabling persistent storage in web browsers.
366#[cfg(target_arch = "wasm32")]
367pub mod opfs;
368
369/// Re-export OPFS storage for convenience
370#[cfg(target_arch = "wasm32")]
371pub use opfs::{MemoryFile, MemoryStorage, OpfsFile, OpfsStorage};