chore: establish development baseline

This commit is contained in:
2026-06-28 01:27:09 +08:00
commit dd53e3054e
131 changed files with 19327 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "bat-cas-engine"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
[dependencies]
anyhow.workspace = true
thiserror.workspace = true
blake3.workspace = true
serde.workspace = true
serde_json.workspace = true
tracing.workspace = true
async-trait.workspace = true
# 文件系统操作
tokio = { workspace = true, features = ["fs", "io-util"] }
# 数据库(用于引用计数、元数据管理)
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
# Hex 编码
hex = "0.4"
[dev-dependencies]
tokio = { workspace = true, features = ["test-util", "macros"] }
tempfile = "3.14"
+35
View File
@@ -0,0 +1,35 @@
//! CAS Engine 错误类型定义
use thiserror::Error;
/// CAS Engine 错误类型
#[derive(Error, Debug)]
pub enum CasError {
/// IO 错误
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
/// Hash 不匹配
#[error("Hash mismatch: expected {expected}, got {actual}")]
HashMismatch {
/// 期望的 Hash
expected: String,
/// 实际的 Hash
actual: String,
},
/// 对象不存在
#[error("Object not found: {0}")]
ObjectNotFound(String),
/// 数据库错误
#[error("Database error: {0}")]
Database(String),
/// 其他错误
#[error(transparent)]
Other(#[from] anyhow::Error),
}
/// CAS Engine Result 类型
pub type Result<T> = std::result::Result<T, CasError>;
+167
View File
@@ -0,0 +1,167 @@
//! Hash 计算模块
//!
//! 使用 BLAKE3 算法计算内容 Hash
use std::fmt;
use std::str::FromStr;
/// Hash 类型
///
/// 使用 BLAKE3 算法计算的 256 位 Hash
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Hash([u8; 32]);
impl Hash {
/// 从字节数组创建 Hash
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
/// 获取 Hash 的字节表示
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
/// 转换为十六进制字符串
pub fn to_hex(&self) -> String {
hex::encode(self.0)
}
/// 从十六进制字符串解析
pub fn from_hex(s: &str) -> Result<Self, crate::error::CasError> {
let bytes = hex::decode(s).map_err(|_| {
crate::error::CasError::Other(anyhow::anyhow!("Invalid hex string"))
})?;
if bytes.len() != 32 {
return Err(crate::error::CasError::Other(anyhow::anyhow!(
"Hash must be 32 bytes"
)));
}
let mut hash_bytes = [0u8; 32];
hash_bytes.copy_from_slice(&bytes);
Ok(Self(hash_bytes))
}
}
impl fmt::Display for Hash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_hex())
}
}
impl FromStr for Hash {
type Err = crate::error::CasError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_hex(s)
}
}
impl serde::Serialize for Hash {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_hex())
}
}
impl<'de> serde::Deserialize<'de> for Hash {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_hex(&s).map_err(serde::de::Error::custom)
}
}
/// 计算数据的 BLAKE3 Hash
///
/// # 参数
/// - `data`: 要计算 Hash 的数据
///
/// # 返回
/// - 数据的 BLAKE3 Hash
///
/// # 示例
/// ```
/// use bat_cas_engine::hash::compute_hash;
///
/// let data = b"Hello, World!";
/// let hash = compute_hash(data);
/// println!("Hash: {}", hash);
/// ```
pub fn compute_hash(data: &[u8]) -> Hash {
let hash_bytes = blake3::hash(data);
Hash::from_bytes(*hash_bytes.as_bytes())
}
/// 计算文件的 BLAKE3 Hash
///
/// # 参数
/// - `path`: 文件路径
///
/// # 返回
/// - 成功:返回文件的 Hash
/// - 失败:返回错误
pub async fn compute_file_hash(path: &std::path::Path) -> crate::error::Result<Hash> {
let data = tokio::fs::read(path).await?;
Ok(compute_hash(&data))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_hash() {
let data = b"Hello, World!";
let hash = compute_hash(data);
// BLAKE3 Hash 应该是确定性的
let hash2 = compute_hash(data);
assert_eq!(hash, hash2);
}
#[test]
fn test_hash_to_string() {
let data = b"Test data";
let hash = compute_hash(data);
let hex = hash.to_hex();
// 应该是 64 个字符的十六进制字符串
assert_eq!(hex.len(), 64);
}
#[test]
fn test_hash_from_string() {
let data = b"Test data";
let hash = compute_hash(data);
let hex = hash.to_hex();
let parsed = Hash::from_hex(&hex).unwrap();
assert_eq!(hash, parsed);
}
#[test]
fn test_hash_different_data() {
let hash1 = compute_hash(b"Data 1");
let hash2 = compute_hash(b"Data 2");
assert_ne!(hash1, hash2);
}
#[test]
fn test_hash_serialization() {
let data = b"Serialization test";
let hash = compute_hash(data);
let json = serde_json::to_string(&hash).unwrap();
let deserialized: Hash = serde_json::from_str(&json).unwrap();
assert_eq!(hash, deserialized);
}
}
+33
View File
@@ -0,0 +1,33 @@
//! # BAT CAS Engine
//!
//! Content Addressable Storage (CAS) 引擎核心实现
//!
//! 提供基于内容寻址的存储系统,支持:
//! - Hash 去重
//! - 引用计数
//! - 垃圾回收
//! - 多版本共享
//! - 完整性校验
#![warn(missing_docs)]
#![warn(clippy::all)]
pub mod error;
pub mod storage;
pub mod hash;
pub mod refcount;
pub use error::{CasError, Result};
/// CAS Engine 版本号
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version() {
assert!(!VERSION.is_empty());
}
}
+4
View File
@@ -0,0 +1,4 @@
//! 引用计数模块占位
/// 引用计数管理(待实现)
pub struct RefCounter;
+299
View File
@@ -0,0 +1,299 @@
//! CAS 存储接口定义
//!
//! 提供统一的 Content Addressable Storage 抽象接口
use crate::error::Result;
use crate::hash::Hash;
use async_trait::async_trait;
use std::path::Path;
/// CAS 存储接口
///
/// 所有 CAS 存储实现必须实现此 trait
#[async_trait]
pub trait Storage: Send + Sync {
/// 存储对象
///
/// # 参数
/// - `data`: 要存储的数据
///
/// # 返回
/// - 成功:返回对象的 Hash
/// - 失败:返回错误
async fn put(&self, data: &[u8]) -> Result<Hash>;
/// 获取对象
///
/// # 参数
/// - `hash`: 对象的 Hash
///
/// # 返回
/// - 成功:返回对象数据
/// - 失败:如果对象不存在或读取失败,返回错误
async fn get(&self, hash: &Hash) -> Result<Vec<u8>>;
/// 检查对象是否存在
///
/// # 参数
/// - `hash`: 对象的 Hash
///
/// # 返回
/// - 存在返回 true,否则返回 false
async fn exists(&self, hash: &Hash) -> bool;
/// 删除对象
///
/// # 参数
/// - `hash`: 对象的 Hash
///
/// # 返回
/// - 成功:返回 Ok(())
/// - 失败:返回错误
async fn delete(&self, hash: &Hash) -> Result<()>;
/// 获取对象大小
///
/// # 参数
/// - `hash`: 对象的 Hash
///
/// # 返回
/// - 成功:返回对象大小(字节)
/// - 失败:返回错误
async fn size(&self, hash: &Hash) -> Result<u64>;
/// 列出所有对象
///
/// # 返回
/// - 所有对象的 Hash 列表
async fn list(&self) -> Result<Vec<Hash>>;
/// 获取存储统计信息
///
/// # 返回
/// - 存储统计信息
async fn stats(&self) -> Result<StorageStats>;
}
/// 存储统计信息
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct StorageStats {
/// 对象总数
pub object_count: u64,
/// 总存储大小(字节)
pub total_size: u64,
/// 存储路径
pub storage_path: String,
}
/// 文件系统 CAS 存储实现
pub struct FileSystemStorage {
/// 存储根目录
root: std::path::PathBuf,
}
impl FileSystemStorage {
/// 创建新的文件系统存储
///
/// # 参数
/// - `root`: 存储根目录
///
/// # 返回
/// - 成功:返回存储实例
/// - 失败:返回错误
pub async fn new(root: impl AsRef<Path>) -> Result<Self> {
let root = root.as_ref().to_path_buf();
// 创建根目录
tokio::fs::create_dir_all(&root).await.map_err(|e| {
crate::error::CasError::Io(e)
})?;
// 创建 objects 目录
let objects_dir = root.join("objects");
tokio::fs::create_dir_all(&objects_dir).await.map_err(|e| {
crate::error::CasError::Io(e)
})?;
Ok(Self { root })
}
/// 获取对象文件路径
///
/// 使用 Hash 的前两个字符作为子目录,避免单目录文件过多
///
/// 例如:hash = "abcdef1234..." -> objects/ab/cdef1234...
fn object_path(&self, hash: &Hash) -> std::path::PathBuf {
let hash_str = hash.to_string();
let (prefix, suffix) = hash_str.split_at(2);
self.root.join("objects").join(prefix).join(suffix)
}
}
#[async_trait]
impl Storage for FileSystemStorage {
async fn put(&self, data: &[u8]) -> Result<Hash> {
// 计算 Hash
let hash = crate::hash::compute_hash(data);
// 检查是否已存在
if self.exists(&hash).await {
return Ok(hash);
}
// 获取文件路径
let path = self.object_path(&hash);
// 创建父目录
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
// 写入文件
tokio::fs::write(&path, data).await?;
Ok(hash)
}
async fn get(&self, hash: &Hash) -> Result<Vec<u8>> {
let path = self.object_path(hash);
// 读取文件
let data = tokio::fs::read(&path).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
crate::error::CasError::ObjectNotFound(hash.to_string())
} else {
crate::error::CasError::Io(e)
}
})?;
// 验证 Hash
let computed_hash = crate::hash::compute_hash(&data);
if computed_hash != *hash {
return Err(crate::error::CasError::HashMismatch {
expected: hash.to_string(),
actual: computed_hash.to_string(),
});
}
Ok(data)
}
async fn exists(&self, hash: &Hash) -> bool {
let path = self.object_path(hash);
tokio::fs::try_exists(&path).await.unwrap_or(false)
}
async fn delete(&self, hash: &Hash) -> Result<()> {
let path = self.object_path(hash);
tokio::fs::remove_file(&path).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
crate::error::CasError::ObjectNotFound(hash.to_string())
} else {
crate::error::CasError::Io(e)
}
})
}
async fn size(&self, hash: &Hash) -> Result<u64> {
let path = self.object_path(hash);
let metadata = tokio::fs::metadata(&path).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
crate::error::CasError::ObjectNotFound(hash.to_string())
} else {
crate::error::CasError::Io(e)
}
})?;
Ok(metadata.len())
}
async fn list(&self) -> Result<Vec<Hash>> {
let objects_dir = self.root.join("objects");
let mut hashes = Vec::new();
// 遍历所有子目录
let mut read_dir = tokio::fs::read_dir(&objects_dir).await?;
while let Some(entry) = read_dir.next_entry().await? {
let prefix_path = entry.path();
if !prefix_path.is_dir() {
continue;
}
// 遍历子目录中的文件
let mut sub_read_dir = tokio::fs::read_dir(&prefix_path).await?;
while let Some(file_entry) = sub_read_dir.next_entry().await? {
if let Some(file_name) = file_entry.file_name().to_str() {
if let Some(prefix) = prefix_path.file_name().and_then(|p| p.to_str()) {
let hash_str = format!("{}{}", prefix, file_name);
if let Ok(hash) = hash_str.parse() {
hashes.push(hash);
}
}
}
}
}
Ok(hashes)
}
async fn stats(&self) -> Result<StorageStats> {
let hashes = self.list().await?;
let mut total_size = 0u64;
for hash in &hashes {
if let Ok(size) = self.size(hash).await {
total_size += size;
}
}
Ok(StorageStats {
object_count: hashes.len() as u64,
total_size,
storage_path: self.root.display().to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_put_and_get() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = FileSystemStorage::new(temp_dir.path()).await.unwrap();
let data = b"Hello, CAS!";
let hash = storage.put(data).await.unwrap();
let retrieved = storage.get(&hash).await.unwrap();
assert_eq!(data, retrieved.as_slice());
}
#[tokio::test]
async fn test_deduplication() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = FileSystemStorage::new(temp_dir.path()).await.unwrap();
let data = b"Same content";
let hash1 = storage.put(data).await.unwrap();
let hash2 = storage.put(data).await.unwrap();
assert_eq!(hash1, hash2);
}
#[tokio::test]
async fn test_exists() {
let temp_dir = tempfile::tempdir().unwrap();
let storage = FileSystemStorage::new(temp_dir.path()).await.unwrap();
let data = b"Test data";
let hash = storage.put(data).await.unwrap();
assert!(storage.exists(&hash).await);
storage.delete(&hash).await.unwrap();
assert!(!storage.exists(&hash).await);
}
}