use crate::persistence::error::{PersistenceError, PersistenceResult};
use std::path::{Path, PathBuf};
pub trait BlobStore: Send + Sync {
fn put(&self, key: &[u8], blob: &[u8]) -> PersistenceResult<()>;
fn get(&self, key: &[u8]) -> PersistenceResult<Option<Vec<u8>>>;
fn delete(&self, key: &[u8]) -> PersistenceResult<()>;
fn exists(&self, key: &[u8]) -> PersistenceResult<bool> {
self.get(key).map(|opt| opt.is_some())
}
}
pub struct FileBlobStore {
base_path: PathBuf,
}
impl FileBlobStore {
pub fn new<P: AsRef<Path>>(base_path: P) -> PersistenceResult<Self> {
let base_path = base_path.as_ref().to_path_buf();
std::fs::create_dir_all(&base_path).map_err(PersistenceError::Io)?;
Ok(Self { base_path })
}
fn key_to_path(&self, key: &[u8]) -> PathBuf {
let hex_key = hex::encode(key);
if hex_key.len() >= 4 {
let dir1 = &hex_key[0..2];
let dir2 = &hex_key[2..4];
self.base_path.join(dir1).join(dir2).join(&hex_key)
} else {
self.base_path.join(&hex_key)
}
}
}
impl BlobStore for FileBlobStore {
fn put(&self, key: &[u8], blob: &[u8]) -> PersistenceResult<()> {
let path = self.key_to_path(key);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(PersistenceError::Io)?;
}
let temp_path = path.with_extension(".tmp");
std::fs::write(&temp_path, blob).map_err(PersistenceError::Io)?;
std::fs::rename(&temp_path, &path).map_err(PersistenceError::Io)?;
Ok(())
}
fn get(&self, key: &[u8]) -> PersistenceResult<Option<Vec<u8>>> {
let path = self.key_to_path(key);
match std::fs::read(&path) {
Ok(blob) => Ok(Some(blob)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(PersistenceError::Io(e)),
}
}
fn delete(&self, key: &[u8]) -> PersistenceResult<()> {
let path = self.key_to_path(key);
match std::fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), Err(e) => Err(PersistenceError::Io(e)),
}
}
}
#[cfg(all(test, feature = "persistence"))]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_file_blob_store() {
let temp_dir = TempDir::new().unwrap();
let store = FileBlobStore::new(temp_dir.path()).unwrap();
let key = b"test_key";
let blob = b"test blob content";
store.put(key, blob).unwrap();
let retrieved = store.get(key).unwrap().unwrap();
assert_eq!(retrieved, blob);
assert!(store.exists(key).unwrap());
store.delete(key).unwrap();
assert!(!store.exists(key).unwrap());
assert!(store.get(key).unwrap().is_none());
}
#[test]
fn test_key_to_path_structure() {
let temp_dir = TempDir::new().unwrap();
let store = FileBlobStore::new(temp_dir.path()).unwrap();
let key = b"abcdefghijklmnop"; let path = store.key_to_path(key);
assert!(path.to_string_lossy().contains("61/62"));
assert!(path
.file_name()
.unwrap()
.to_string_lossy()
.starts_with("6162"));
}
}