use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::blob_store::BlobStore;
pub trait ShardStorage: Send + Sync {
fn shard_path(&self, shard_id: usize) -> PathBuf;
fn write_root_file(&self, name: &str, data: &[u8]) -> Result<(), String>;
fn read_root_file(&self, name: &str) -> Result<Vec<u8>, String>;
fn root_file_exists(&self, name: &str) -> bool;
fn count_shards(&self) -> usize {
let mut count = 0;
while self.shard_path(count).exists() {
count += 1;
}
count
}
fn ensure_shard_dir(&self, shard_id: usize) -> Result<(), String> {
let path = self.shard_path(shard_id);
std::fs::create_dir_all(&path)
.map_err(|e| format!("cannot create shard_{shard_id} dir: {e}"))
}
}
pub struct FsShardStorage {
base_path: PathBuf,
}
impl FsShardStorage {
pub fn new(base_path: impl Into<PathBuf>) -> Result<Self, String> {
let base_path = base_path.into();
std::fs::create_dir_all(&base_path)
.map_err(|e| format!("cannot create base dir: {e}"))?;
Ok(Self { base_path })
}
pub fn base_path(&self) -> &Path {
&self.base_path
}
}
impl ShardStorage for FsShardStorage {
fn shard_path(&self, shard_id: usize) -> PathBuf {
self.base_path.join(format!("shard_{shard_id}"))
}
fn write_root_file(&self, name: &str, data: &[u8]) -> Result<(), String> {
std::fs::write(self.base_path.join(name), data)
.map_err(|e| format!("cannot write {name}: {e}"))
}
fn read_root_file(&self, name: &str) -> Result<Vec<u8>, String> {
std::fs::read(self.base_path.join(name))
.map_err(|e| format!("cannot read {name}: {e}"))
}
fn root_file_exists(&self, name: &str) -> bool {
self.base_path.join(name).exists()
}
}
pub struct BlobShardStorage<S: BlobStore> {
store: Arc<S>,
index_name: String,
cache_base: Option<PathBuf>,
}
impl<S: BlobStore> BlobShardStorage<S> {
pub fn new(store: Arc<S>, index_name: impl Into<String>, cache_base: Option<PathBuf>) -> Self {
Self {
store,
index_name: index_name.into(),
cache_base,
}
}
pub fn shard_namespace(&self, shard_id: usize) -> String {
format!("{}/shard_{shard_id}", self.index_name)
}
pub fn store(&self) -> &S {
&self.store
}
}
impl<S: BlobStore> ShardStorage for BlobShardStorage<S> {
fn shard_path(&self, shard_id: usize) -> PathBuf {
match &self.cache_base {
Some(base) => base.join(format!("shard_{shard_id}")),
None => PathBuf::from(format!("/tmp/lucistore_cache/shard_{shard_id}")),
}
}
fn write_root_file(&self, name: &str, data: &[u8]) -> Result<(), String> {
self.store.save(&self.index_name, name, data)
.map_err(|e| format!("cannot write {name} to blob store: {e}"))
}
fn read_root_file(&self, name: &str) -> Result<Vec<u8>, String> {
self.store.load(&self.index_name, name)
.map_err(|e| format!("cannot read {name} from blob store: {e}"))
}
fn root_file_exists(&self, name: &str) -> bool {
self.store.exists(&self.index_name, name).unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fs_shard_storage() {
let tmp = std::env::temp_dir().join("lucistore_test_fs_shard");
let _ = std::fs::remove_dir_all(&tmp);
let storage = FsShardStorage::new(&tmp).unwrap();
assert_eq!(storage.shard_path(0), tmp.join("shard_0"));
assert_eq!(storage.shard_path(3), tmp.join("shard_3"));
storage.write_root_file("config.json", b"{\"shards\":4}").unwrap();
assert!(storage.root_file_exists("config.json"));
let data = storage.read_root_file("config.json").unwrap();
assert_eq!(data, b"{\"shards\":4}");
storage.ensure_shard_dir(0).unwrap();
assert!(storage.shard_path(0).exists());
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn test_blob_shard_storage() {
use crate::blob_store::MemBlobStore;
let store = Arc::new(MemBlobStore::new());
let storage = BlobShardStorage::new(store.clone(), "test_idx", None);
storage.write_root_file("config.json", b"{}").unwrap();
assert!(storage.root_file_exists("config.json"));
assert_eq!(storage.read_root_file("config.json").unwrap(), b"{}");
assert_eq!(storage.shard_namespace(2), "test_idx/shard_2");
}
}