use crate::{StorageError, StorageResult};
use firecloud_core::{FileId, FileManifest};
#[cfg(test)]
use firecloud_core::{ChunkHash, FileMetadata};
use sled::Db;
use std::path::Path;
use tracing::{debug, info, warn};
const TREE_MANIFESTS: &str = "manifests";
const TREE_FILE_INDEX: &str = "file_index";
pub struct ManifestStore {
db: Db,
}
impl ManifestStore {
pub fn open<P: AsRef<Path>>(path: P) -> StorageResult<Self> {
let db = sled::open(path).map_err(|e| StorageError::Database(e.to_string()))?;
info!("Opened manifest store with Sled");
Ok(Self { db })
}
pub fn put(&self, manifest: &FileManifest) -> StorageResult<()> {
let manifests_tree = self
.db
.open_tree(TREE_MANIFESTS)
.map_err(|e| StorageError::Database(e.to_string()))?;
let index_tree = self
.db
.open_tree(TREE_FILE_INDEX)
.map_err(|e| StorageError::Database(e.to_string()))?;
let key = manifest.metadata.id.0.as_bytes();
let manifest_bytes =
bincode::serialize(manifest).map_err(|e| StorageError::Serialization(e.to_string()))?;
manifests_tree
.insert(key, manifest_bytes)
.map_err(|e| StorageError::Database(e.to_string()))?;
let name_key = manifest.metadata.name.as_bytes();
index_tree
.insert(name_key, key)
.map_err(|e| StorageError::Database(e.to_string()))?;
debug!(
"Stored manifest: {} ({})",
manifest.metadata.id, manifest.metadata.name
);
Ok(())
}
pub fn get(&self, file_id: &FileId) -> StorageResult<Option<FileManifest>> {
let manifests_tree = self
.db
.open_tree(TREE_MANIFESTS)
.map_err(|e| StorageError::Database(e.to_string()))?;
let key = file_id.0.as_bytes();
match manifests_tree
.get(key)
.map_err(|e| StorageError::Database(e.to_string()))?
{
Some(bytes) => {
let manifest: FileManifest = bincode::deserialize(&bytes)
.map_err(|e| StorageError::Serialization(e.to_string()))?;
Ok(Some(manifest))
}
None => Ok(None),
}
}
pub fn get_by_id_str(&self, file_id_str: &str) -> StorageResult<Option<FileManifest>> {
let uuid = uuid::Uuid::parse_str(file_id_str)
.map_err(|e| StorageError::InvalidId(format!("Invalid file ID: {}", e)))?;
let file_id = FileId::from_uuid(uuid);
self.get(&file_id)
}
pub fn get_by_name(&self, name: &str) -> StorageResult<Option<FileManifest>> {
let index_tree = self
.db
.open_tree(TREE_FILE_INDEX)
.map_err(|e| StorageError::Database(e.to_string()))?;
let name_key = name.as_bytes();
match index_tree
.get(name_key)
.map_err(|e| StorageError::Database(e.to_string()))?
{
Some(file_id_bytes) => {
let uuid = uuid::Uuid::from_slice(&file_id_bytes)
.map_err(|e| StorageError::Serialization(e.to_string()))?;
let file_id = FileId::from_uuid(uuid);
self.get(&file_id)
}
None => Ok(None),
}
}
pub fn contains(&self, file_id: &FileId) -> StorageResult<bool> {
let manifests_tree = self
.db
.open_tree(TREE_MANIFESTS)
.map_err(|e| StorageError::Database(e.to_string()))?;
Ok(manifests_tree
.contains_key(file_id.0.as_bytes())
.map_err(|e| StorageError::Database(e.to_string()))?)
}
pub fn delete(&self, file_id: &FileId) -> StorageResult<bool> {
let manifests_tree = self
.db
.open_tree(TREE_MANIFESTS)
.map_err(|e| StorageError::Database(e.to_string()))?;
let index_tree = self
.db
.open_tree(TREE_FILE_INDEX)
.map_err(|e| StorageError::Database(e.to_string()))?;
let key = file_id.0.as_bytes();
if let Some(bytes) = manifests_tree
.get(key)
.map_err(|e| StorageError::Database(e.to_string()))?
{
let manifest: FileManifest = bincode::deserialize(&bytes)
.map_err(|e| StorageError::Serialization(e.to_string()))?;
index_tree
.remove(manifest.metadata.name.as_bytes())
.map_err(|e| StorageError::Database(e.to_string()))?;
}
let existed = manifests_tree
.remove(key)
.map_err(|e| StorageError::Database(e.to_string()))?
.is_some();
if existed {
debug!("Deleted manifest: {}", file_id);
}
Ok(existed)
}
pub fn list(&self) -> StorageResult<Vec<ManifestSummary>> {
let manifests_tree = self
.db
.open_tree(TREE_MANIFESTS)
.map_err(|e| StorageError::Database(e.to_string()))?;
let mut summaries = Vec::new();
for result in manifests_tree.iter() {
let (_, value) = result.map_err(|e| StorageError::Database(e.to_string()))?;
match bincode::deserialize::<FileManifest>(&value) {
Ok(manifest) => {
summaries.push(ManifestSummary {
file_id: manifest.metadata.id,
name: manifest.metadata.name.clone(),
size: manifest.metadata.size,
chunk_count: manifest.chunks.len(),
created_at: manifest.metadata.created_at,
});
}
Err(e) => {
warn!("Failed to deserialize manifest (skipping): {}", e);
continue;
}
}
}
Ok(summaries)
}
pub fn count(&self) -> StorageResult<usize> {
let manifests_tree = self
.db
.open_tree(TREE_MANIFESTS)
.map_err(|e| StorageError::Database(e.to_string()))?;
Ok(manifests_tree.len())
}
pub fn flush(&self) -> StorageResult<()> {
self.db
.flush()
.map_err(|e| StorageError::Database(e.to_string()))?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ManifestSummary {
pub file_id: FileId,
pub name: String,
pub size: u64,
pub chunk_count: usize,
pub created_at: i64,
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_manifest() -> FileManifest {
let metadata = FileMetadata {
id: FileId::new(),
name: "test_file.txt".to_string(),
mime_type: Some("text/plain".to_string()),
size: 1024,
created_at: chrono::Utc::now().timestamp_millis(),
modified_at: chrono::Utc::now().timestamp_millis(),
content_hash: ChunkHash::hash(b"test content"),
};
FileManifest {
metadata,
chunks: vec![
ChunkHash::hash(b"chunk1"),
ChunkHash::hash(b"chunk2"),
],
encrypted_dek: Some(vec![0u8; 32]),
salt: Some(vec![0u8; 16]), version: 1,
}
}
#[test]
fn test_manifest_store_put_get() {
let tmp_dir = TempDir::new().unwrap();
let store = ManifestStore::open(tmp_dir.path().join("manifests")).unwrap();
let manifest = create_test_manifest();
let file_id = manifest.metadata.id;
store.put(&manifest).unwrap();
let retrieved = store.get(&file_id).unwrap().unwrap();
assert_eq!(retrieved.metadata.name, "test_file.txt");
assert_eq!(retrieved.chunks.len(), 2);
}
#[test]
fn test_manifest_store_get_by_name() {
let tmp_dir = TempDir::new().unwrap();
let store = ManifestStore::open(tmp_dir.path().join("manifests")).unwrap();
let manifest = create_test_manifest();
store.put(&manifest).unwrap();
let retrieved = store.get_by_name("test_file.txt").unwrap().unwrap();
assert_eq!(retrieved.metadata.id, manifest.metadata.id);
}
#[test]
fn test_manifest_store_list() {
let tmp_dir = TempDir::new().unwrap();
let store = ManifestStore::open(tmp_dir.path().join("manifests")).unwrap();
for i in 0..3 {
let mut manifest = create_test_manifest();
manifest.metadata.name = format!("file_{}.txt", i);
store.put(&manifest).unwrap();
}
let summaries = store.list().unwrap();
assert_eq!(summaries.len(), 3);
}
}