vibesql_storage/
backend.rs1#![allow(clippy::suspicious_open_options)]
7
8#[cfg(not(target_arch = "wasm32"))]
9use std::io;
10
11use crate::StorageError;
12
13pub trait StorageFile: Send + Sync {
19 fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<usize, StorageError>;
28
29 fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<usize, StorageError>;
38
39 fn sync_all(&mut self) -> Result<(), StorageError>;
44
45 fn sync_data(&mut self) -> Result<(), StorageError>;
50
51 fn size(&self) -> Result<u64, StorageError>;
56}
57
58pub trait StorageBackend: Send + Sync {
64 fn create_file(&self, path: &str) -> Result<Box<dyn StorageFile>, StorageError>;
74
75 fn open_file(&self, path: &str) -> Result<Box<dyn StorageFile>, StorageError>;
83
84 fn delete_file(&self, path: &str) -> Result<(), StorageError>;
92
93 fn file_exists(&self, path: &str) -> bool;
101
102 fn file_size(&self, path: &str) -> Result<u64, StorageError>;
110}
111
112#[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 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 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 pub struct NativeStorage {
181 root: PathBuf,
182 }
183
184 impl NativeStorage {
185 pub fn new<P: AsRef<Path>>(root: P) -> Result<Self, StorageError> {
190 let root = root.as_ref().to_path_buf();
191
192 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 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 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 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 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 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 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 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 assert!(!storage.file_exists("test.db"));
302
303 let mut file = storage.create_file("test.db").unwrap();
305 file.write_at(0, b"test").unwrap();
306 drop(file);
307
308 assert!(storage.file_exists("test.db"));
310
311 let size = storage.file_size("test.db").unwrap();
313 assert_eq!(size, 4);
314
315 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 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 file.write_at(0, b"AAAA").unwrap();
342
343 file.write_at(100, b"BBBB").unwrap();
345
346 let mut buf = vec![0u8; 4];
348 file.read_at(0, &mut buf).unwrap();
349 assert_eq!(&buf, b"AAAA");
350
351 file.read_at(100, &mut buf).unwrap();
353 assert_eq!(&buf, b"BBBB");
354 }
355 }
356}
357
358#[cfg(not(target_arch = "wasm32"))]
360pub use native::{NativeFile, NativeStorage};
361
362#[cfg(target_arch = "wasm32")]
367pub mod opfs;
368
369#[cfg(target_arch = "wasm32")]
371pub use opfs::{MemoryFile, MemoryStorage, OpfsFile, OpfsStorage};