use std::fmt;
use std::path::{Path, PathBuf};
use chronix_core::{NamespaceId, ShardId};
use crate::storage::error::Result;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SegmentPath {
namespace: NamespaceId,
shard_id: ShardId,
segment_name: String,
}
impl SegmentPath {
pub fn new(
namespace: NamespaceId,
shard_id: ShardId,
segment_name: impl Into<String>,
) -> crate::storage::error::Result<Self> {
let name = segment_name.into();
if name.is_empty()
|| name.contains('/')
|| name.contains('\\')
|| name.contains("..")
|| name == "."
{
return Err(crate::storage::error::StorageError::InvalidPath {
detail: format!(
"invalid segment name '{name}': must not be empty or contain path separators / '..' components"
),
});
}
Ok(Self {
namespace,
shard_id,
segment_name: name,
})
}
#[must_use]
pub fn namespace(&self) -> &NamespaceId {
&self.namespace
}
#[must_use]
pub fn shard_id(&self) -> ShardId {
self.shard_id
}
#[must_use]
pub fn segment_name(&self) -> &str {
&self.segment_name
}
#[must_use]
pub fn to_fs_path(&self, data_dir: &Path) -> PathBuf {
data_dir
.join(format!("ns_{}", self.namespace.as_str()))
.join(format!("shard_{}", self.shard_id.0))
.join(&self.segment_name)
}
}
impl fmt::Display for SegmentPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"ns_{}/shard_{}/{}",
self.namespace.as_str(),
self.shard_id.0,
self.segment_name
)
}
}
pub trait StorageBackend: Send + Sync + 'static {
fn put_segment(
&self,
path: &SegmentPath,
data: &[u8],
) -> impl std::future::Future<Output = Result<()>> + Send;
fn get_segment(
&self,
path: &SegmentPath,
) -> impl std::future::Future<Output = Result<Vec<u8>>> + Send;
fn get_range(
&self,
path: &SegmentPath,
offset: u64,
length: usize,
) -> impl std::future::Future<Output = Result<Vec<u8>>> + Send;
fn delete_segment(
&self,
path: &SegmentPath,
) -> impl std::future::Future<Output = Result<()>> + Send;
fn list_segments(
&self,
namespace: &NamespaceId,
shard_id: &ShardId,
) -> impl std::future::Future<Output = Result<Vec<SegmentPath>>> + Send;
fn exists(&self, path: &SegmentPath) -> impl std::future::Future<Output = Result<bool>> + Send;
}
#[cfg(test)]
mod tests {
use super::*;
fn default_ns() -> NamespaceId {
NamespaceId::default_namespace()
}
#[test]
fn segment_path_display() {
let path = SegmentPath::new(default_ns(), ShardId(42), "segment_001.csx").unwrap();
assert_eq!(path.to_string(), "ns_default/shard_42/segment_001.csx");
}
#[test]
fn segment_path_to_fs_path() {
let path = SegmentPath::new(default_ns(), ShardId(7), "seg.csx").unwrap();
let fs_path = path.to_fs_path(Path::new("/data"));
assert_eq!(fs_path, PathBuf::from("/data/ns_default/shard_7/seg.csx"));
}
#[test]
fn segment_path_accessors() {
let path = SegmentPath::new(default_ns(), ShardId(3), "test.csx").unwrap();
assert_eq!(path.namespace(), &default_ns());
assert_eq!(path.shard_id(), ShardId(3));
assert_eq!(path.segment_name(), "test.csx");
}
#[test]
fn segment_path_equality() {
let a = SegmentPath::new(default_ns(), ShardId(1), "seg.csx").unwrap();
let b = SegmentPath::new(default_ns(), ShardId(1), "seg.csx").unwrap();
let c = SegmentPath::new(default_ns(), ShardId(2), "seg.csx").unwrap();
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn segment_path_different_namespaces_differ() {
let ns_a = NamespaceId::new("tenant-a").unwrap();
let ns_b = NamespaceId::new("tenant-b").unwrap();
let a = SegmentPath::new(ns_a, ShardId(1), "seg.csx").unwrap();
let b = SegmentPath::new(ns_b, ShardId(1), "seg.csx").unwrap();
assert_ne!(a, b);
assert_ne!(
a.to_fs_path(Path::new("/data")),
b.to_fs_path(Path::new("/data"))
);
}
#[test]
fn segment_path_rejects_invalid_names() {
assert!(SegmentPath::new(default_ns(), ShardId(1), "").is_err());
assert!(SegmentPath::new(default_ns(), ShardId(1), "a/b").is_err());
assert!(SegmentPath::new(default_ns(), ShardId(1), "a\\b").is_err());
assert!(SegmentPath::new(default_ns(), ShardId(1), "a..b").is_err());
assert!(SegmentPath::new(default_ns(), ShardId(1), ".").is_err());
assert!(SegmentPath::new(default_ns(), ShardId(1), "segment_001.csx").is_ok());
}
}