use std::fs;
use std::path::{Path, PathBuf};
use crate::error::{IoError, Result};
#[derive(Debug, Clone)]
pub struct DirectoryStore {
root: PathBuf,
}
impl DirectoryStore {
pub fn open<P: AsRef<Path>>(root: P) -> Result<Self> {
let root = root.as_ref().to_path_buf();
if !root.exists() {
fs::create_dir_all(&root).map_err(|e| {
IoError::FileError(format!(
"Failed to create store directory {}: {e}",
root.display()
))
})?;
}
Ok(Self { root })
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn get(&self, key: &str) -> Result<Vec<u8>> {
let path = self.key_to_path(key);
fs::read(&path).map_err(|e| {
IoError::FileNotFound(format!("Store key '{}' ({}): {e}", key, path.display()))
})
}
pub fn set(&self, key: &str, value: &[u8]) -> Result<()> {
let path = self.key_to_path(key);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| {
IoError::FileError(format!(
"Failed to create directory {}: {e}",
parent.display()
))
})?;
}
fs::write(&path, value)
.map_err(|e| IoError::FileError(format!("Failed to write {}: {e}", path.display())))
}
pub fn delete(&self, key: &str) -> Result<()> {
let path = self.key_to_path(key);
if path.exists() {
fs::remove_file(&path).map_err(|e| {
IoError::FileError(format!("Failed to delete {}: {e}", path.display()))
})?;
}
Ok(())
}
pub fn exists(&self, key: &str) -> bool {
self.key_to_path(key).exists()
}
pub fn list_prefix(&self, prefix: &str) -> Result<Vec<String>> {
let base = self.key_to_path(prefix);
if !base.exists() {
return Ok(Vec::new());
}
let mut result = Vec::new();
self.collect_keys(&base, &self.root, &mut result)?;
Ok(result)
}
pub fn list_dir(&self, prefix: &str) -> Result<Vec<String>> {
let base = self.key_to_path(prefix);
if !base.is_dir() {
return Ok(Vec::new());
}
let mut result = Vec::new();
let entries = fs::read_dir(&base).map_err(|e| {
IoError::FileError(format!("Failed to read dir {}: {e}", base.display()))
})?;
for entry in entries {
let entry =
entry.map_err(|e| IoError::FileError(format!("Failed to read dir entry: {e}")))?;
let rel = entry
.path()
.strip_prefix(&self.root)
.map_err(|e| IoError::FileError(format!("Strip prefix error: {e}")))?
.to_string_lossy()
.replace('\\', "/");
result.push(rel);
}
result.sort();
Ok(result)
}
fn key_to_path(&self, key: &str) -> PathBuf {
let mut path = self.root.clone();
for component in key.split('/') {
if !component.is_empty() {
path.push(component);
}
}
path
}
fn collect_keys(&self, dir: &Path, root: &Path, out: &mut Vec<String>) -> Result<()> {
if dir.is_file() {
let rel = dir
.strip_prefix(root)
.map_err(|e| IoError::FileError(format!("Strip prefix error: {e}")))?
.to_string_lossy()
.replace('\\', "/");
out.push(rel);
return Ok(());
}
if !dir.is_dir() {
return Ok(());
}
let entries = fs::read_dir(dir).map_err(|e| {
IoError::FileError(format!("Failed to read dir {}: {e}", dir.display()))
})?;
for entry in entries {
let entry =
entry.map_err(|e| IoError::FileError(format!("Failed to read dir entry: {e}")))?;
self.collect_keys(&entry.path(), root, out)?;
}
Ok(())
}
}
pub fn chunk_key_v2(prefix: &str, coords: &[u64], sep: &str) -> String {
let coord_str: Vec<String> = coords.iter().map(|c| c.to_string()).collect();
let joined = coord_str.join(sep);
if prefix.is_empty() {
joined
} else {
format!("{prefix}/{joined}")
}
}
pub fn chunk_key_v3(prefix: &str, coords: &[u64], separator: &str) -> String {
let coord_str: Vec<String> = coords.iter().map(|c| c.to_string()).collect();
let joined = coord_str.join(separator);
if prefix.is_empty() {
format!("c/{joined}")
} else {
format!("{prefix}/c/{joined}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_directory_store_write_read() {
let dir = std::env::temp_dir().join("zarr_store_test_wr");
let _ = fs::remove_dir_all(&dir);
let store = DirectoryStore::open(&dir).expect("open store");
store
.set("group/.zgroup", b"{\"zarr_format\":2}")
.expect("write");
let data = store.get("group/.zgroup").expect("read");
assert_eq!(data, b"{\"zarr_format\":2}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_directory_store_exists_delete() {
let dir = std::env::temp_dir().join("zarr_store_test_ed");
let _ = fs::remove_dir_all(&dir);
let store = DirectoryStore::open(&dir).expect("open store");
assert!(!store.exists("foo"));
store.set("foo", b"bar").expect("write");
assert!(store.exists("foo"));
store.delete("foo").expect("delete");
assert!(!store.exists("foo"));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_directory_store_list_prefix() {
let dir = std::env::temp_dir().join("zarr_store_test_lp");
let _ = fs::remove_dir_all(&dir);
let store = DirectoryStore::open(&dir).expect("open store");
store.set("arr/.zarray", b"{}").expect("w1");
store.set("arr/0.0", b"chunk1").expect("w2");
store.set("arr/0.1", b"chunk2").expect("w3");
let mut keys = store.list_prefix("arr").expect("list");
keys.sort();
assert_eq!(keys.len(), 3);
assert!(keys.contains(&"arr/.zarray".to_string()));
assert!(keys.contains(&"arr/0.0".to_string()));
assert!(keys.contains(&"arr/0.1".to_string()));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_directory_store_nested_keys() {
let dir = std::env::temp_dir().join("zarr_store_test_nk");
let _ = fs::remove_dir_all(&dir);
let store = DirectoryStore::open(&dir).expect("open store");
store.set("a/b/c/d", b"deep").expect("write deep");
assert!(store.exists("a/b/c/d"));
let data = store.get("a/b/c/d").expect("read");
assert_eq!(data, b"deep");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_chunk_key_v2() {
assert_eq!(chunk_key_v2("data", &[0, 1, 2], "."), "data/0.1.2");
assert_eq!(chunk_key_v2("", &[3, 4], "."), "3.4");
assert_eq!(chunk_key_v2("arr", &[0], "."), "arr/0");
}
#[test]
fn test_chunk_key_v3() {
assert_eq!(chunk_key_v3("data", &[0, 1, 2], "/"), "data/c/0/1/2");
assert_eq!(chunk_key_v3("", &[3, 4], "/"), "c/3/4");
}
#[test]
fn test_get_missing_key_returns_error() {
let dir = std::env::temp_dir().join("zarr_store_test_miss");
let _ = fs::remove_dir_all(&dir);
let store = DirectoryStore::open(&dir).expect("open store");
let result = store.get("nonexistent");
assert!(result.is_err());
let _ = fs::remove_dir_all(&dir);
}
}