#![allow(clippy::suspicious_open_options)]
#[cfg(not(target_arch = "wasm32"))]
use std::io;
use crate::StorageError;
pub trait StorageFile: Send + Sync {
fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<usize, StorageError>;
fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<usize, StorageError>;
fn sync_all(&mut self) -> Result<(), StorageError>;
fn sync_data(&mut self) -> Result<(), StorageError>;
fn size(&self) -> Result<u64, StorageError>;
}
pub trait StorageBackend: Send + Sync {
fn create_file(&self, path: &str) -> Result<Box<dyn StorageFile>, StorageError>;
fn open_file(&self, path: &str) -> Result<Box<dyn StorageFile>, StorageError>;
fn delete_file(&self, path: &str) -> Result<(), StorageError>;
fn file_exists(&self, path: &str) -> bool;
fn file_size(&self, path: &str) -> Result<u64, StorageError>;
}
#[cfg(not(target_arch = "wasm32"))]
pub mod native {
#[cfg(target_arch = "wasm32")]
use std::sync::Mutex;
use std::{
fs::{File, OpenOptions},
io::{Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
};
#[cfg(not(target_arch = "wasm32"))]
use parking_lot::Mutex;
use super::*;
pub struct NativeFile {
file: Mutex<File>,
}
impl StorageFile for NativeFile {
fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<usize, StorageError> {
let mut file = self.file.lock();
file.seek(SeekFrom::Start(offset)).map_err(|e| StorageError::IoError(e.to_string()))?;
match file.read_exact(buf) {
Ok(()) => Ok(buf.len()),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
file.seek(SeekFrom::Start(offset))
.map_err(|e| StorageError::IoError(e.to_string()))?;
file.read(buf).map_err(|e| StorageError::IoError(e.to_string()))
}
Err(e) => Err(StorageError::IoError(e.to_string())),
}
}
fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<usize, StorageError> {
let mut file = self.file.lock();
file.seek(SeekFrom::Start(offset)).map_err(|e| StorageError::IoError(e.to_string()))?;
file.write_all(buf).map_err(|e| StorageError::IoError(e.to_string()))?;
Ok(buf.len())
}
fn sync_all(&mut self) -> Result<(), StorageError> {
let file = self.file.lock();
file.sync_all().map_err(|e| StorageError::IoError(e.to_string()))
}
fn sync_data(&mut self) -> Result<(), StorageError> {
let file = self.file.lock();
file.sync_data().map_err(|e| StorageError::IoError(e.to_string()))
}
fn size(&self) -> Result<u64, StorageError> {
let file = self.file.lock();
Ok(file.metadata().map_err(|e| StorageError::IoError(e.to_string()))?.len())
}
}
pub struct NativeStorage {
root: PathBuf,
}
impl NativeStorage {
pub fn new<P: AsRef<Path>>(root: P) -> Result<Self, StorageError> {
let root = root.as_ref().to_path_buf();
if !root.exists() {
std::fs::create_dir_all(&root).map_err(|e| StorageError::IoError(e.to_string()))?;
}
Ok(NativeStorage { root })
}
fn full_path(&self, path: &str) -> PathBuf {
self.root.join(path)
}
}
impl StorageBackend for NativeStorage {
fn create_file(&self, path: &str) -> Result<Box<dyn StorageFile>, StorageError> {
let full_path = self.full_path(path);
if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| StorageError::IoError(e.to_string()))?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&full_path)
.map_err(|e| StorageError::IoError(e.to_string()))?;
Ok(Box::new(NativeFile { file: Mutex::new(file) }))
}
fn open_file(&self, path: &str) -> Result<Box<dyn StorageFile>, StorageError> {
let full_path = self.full_path(path);
if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| StorageError::IoError(e.to_string()))?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&full_path)
.map_err(|e| StorageError::IoError(e.to_string()))?;
Ok(Box::new(NativeFile { file: Mutex::new(file) }))
}
fn delete_file(&self, path: &str) -> Result<(), StorageError> {
let full_path = self.full_path(path);
std::fs::remove_file(&full_path).map_err(|e| StorageError::IoError(e.to_string()))
}
fn file_exists(&self, path: &str) -> bool {
self.full_path(path).exists()
}
fn file_size(&self, path: &str) -> Result<u64, StorageError> {
let full_path = self.full_path(path);
let metadata =
std::fs::metadata(&full_path).map_err(|e| StorageError::IoError(e.to_string()))?;
Ok(metadata.len())
}
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use super::*;
#[test]
fn test_native_file_operations() {
let temp_dir = TempDir::new().unwrap();
let storage = NativeStorage::new(temp_dir.path()).unwrap();
let mut file = storage.create_file("test.db").unwrap();
let data = b"Hello, Storage!";
let written = file.write_at(0, data).unwrap();
assert_eq!(written, data.len());
file.sync_all().unwrap();
let mut buf = vec![0u8; data.len()];
let read = file.read_at(0, &mut buf).unwrap();
assert_eq!(read, data.len());
assert_eq!(&buf, data);
let size = file.size().unwrap();
assert_eq!(size, data.len() as u64);
}
#[test]
fn test_native_storage_operations() {
let temp_dir = TempDir::new().unwrap();
let storage = NativeStorage::new(temp_dir.path()).unwrap();
assert!(!storage.file_exists("test.db"));
let mut file = storage.create_file("test.db").unwrap();
file.write_at(0, b"test").unwrap();
drop(file);
assert!(storage.file_exists("test.db"));
let size = storage.file_size("test.db").unwrap();
assert_eq!(size, 4);
storage.delete_file("test.db").unwrap();
assert!(!storage.file_exists("test.db"));
}
#[test]
fn test_native_storage_with_subdirectories() {
let temp_dir = TempDir::new().unwrap();
let storage = NativeStorage::new(temp_dir.path()).unwrap();
let mut file = storage.create_file("subdir/nested/test.db").unwrap();
file.write_at(0, b"nested").unwrap();
drop(file);
assert!(storage.file_exists("subdir/nested/test.db"));
}
#[test]
fn test_read_write_at_different_offsets() {
let temp_dir = TempDir::new().unwrap();
let storage = NativeStorage::new(temp_dir.path()).unwrap();
let mut file = storage.create_file("test.db").unwrap();
file.write_at(0, b"AAAA").unwrap();
file.write_at(100, b"BBBB").unwrap();
let mut buf = vec![0u8; 4];
file.read_at(0, &mut buf).unwrap();
assert_eq!(&buf, b"AAAA");
file.read_at(100, &mut buf).unwrap();
assert_eq!(&buf, b"BBBB");
}
}
}
#[cfg(not(target_arch = "wasm32"))]
pub use native::{NativeFile, NativeStorage};
#[cfg(target_arch = "wasm32")]
pub mod opfs;
#[cfg(target_arch = "wasm32")]
pub use opfs::{MemoryFile, MemoryStorage, OpfsFile, OpfsStorage};