firecloud-storage 0.2.0

Chunking, compression, and local storage for FireCloud distributed storage
Documentation
//! File manifest storage using Sled
//!
//! Persists FileManifest to local storage, enabling:
//! - Upload: Store manifest after chunking a file
//! - Download: Retrieve manifest by file_id to reconstruct files
//! - Share: Serve manifests to peers via P2P protocol

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};

/// Tree names for manifest data
const TREE_MANIFESTS: &str = "manifests";
const TREE_FILE_INDEX: &str = "file_index"; // name -> file_id mapping

/// Local manifest store backed by Sled
pub struct ManifestStore {
    db: Db,
}

impl ManifestStore {
    /// Open or create a manifest store at the given path
    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 })
    }

    /// Store a file manifest
    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()))?;

        // Use file_id UUID bytes as key
        let key = manifest.metadata.id.0.as_bytes();

        // Serialize and store manifest
        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()))?;

        // Also index by filename for lookup
        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(())
    }

    /// Retrieve a manifest by file ID
    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),
        }
    }

    /// Retrieve a manifest by file ID string (UUID format)
    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)
    }

    /// Retrieve a manifest by filename
    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) => {
                // file_id_bytes is UUID 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),
        }
    }

    /// Check if a manifest exists
    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()))?)
    }

    /// Delete a manifest
    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()))?;

        // Get manifest first to find filename for index removal
        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()))?;

            // Remove from name index
            index_tree
                .remove(manifest.metadata.name.as_bytes())
                .map_err(|e| StorageError::Database(e.to_string()))?;
        }

        // Remove manifest
        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)
    }

    /// List all stored manifests (returns file_id, name, size, chunk_count)
    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()))?;
            
            // Try to deserialize manifest, skip corrupted entries
            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) => {
                    // Log warning but continue iteration (old schema or corrupted data)
                    warn!("Failed to deserialize manifest (skipping): {}", e);
                    continue;
                }
            }
        }

        Ok(summaries)
    }

    /// Get total number of manifests stored
    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())
    }

    /// Flush all pending writes to disk
    pub fn flush(&self) -> StorageResult<()> {
        self.db
            .flush()
            .map_err(|e| StorageError::Database(e.to_string()))?;
        Ok(())
    }
}

/// Summary info for listing manifests
#[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]),  // Add salt field for Phase 3 compatibility
            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
        store.put(&manifest).unwrap();

        // Retrieve by ID
        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();

        // Retrieve by name
        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();

        // Store multiple manifests
        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);
    }
}