Skip to main content

agent_diva_files/
storage.rs

1//! Storage backend for file management
2//!
3//! Provides content-addressed storage using SHA256 hashes.
4//! Files are stored in a directory structure based on hash prefixes.
5//!
6//! This module is now backed by the `StorageBackend` trait from `backend.rs`.
7//! The default implementation uses `LocalStorageBackend`.
8
9use crate::backend::{LocalStorageBackend, StorageBackend};
10use crate::config::FileConfig;
11use crate::Result;
12use std::path::{Path, PathBuf};
13
14/// File storage using a pluggable backend
15///
16/// This struct wraps a `StorageBackend` implementation and provides
17/// higher-level operations like index management.
18pub struct FileStorage {
19    backend: Box<dyn StorageBackend>,
20    #[allow(dead_code)]
21    config: FileConfig,
22}
23
24/// Storage statistics
25#[derive(Debug, Clone, Default)]
26pub struct StorageStats {
27    pub total_files: usize,
28    pub total_size: u64,
29    pub total_refs: usize,
30}
31
32impl FileStorage {
33    /// Create a new file storage with the default local backend
34    pub fn new(config: FileConfig) -> Self {
35        let data_dir = config.data_dir();
36        let backend = Box::new(LocalStorageBackend::new(data_dir));
37        Self { backend, config }
38    }
39
40    /// Create a file storage with a custom backend
41    ///
42    /// This allows using different storage backends like S3, Azure Blob, etc.
43    ///
44    /// # Example
45    /// ```rust
46    /// use agent_diva_files::{FileStorage, FileConfig};
47    /// use agent_diva_files::backend::LocalStorageBackend;
48    /// use std::path::PathBuf;
49    ///
50    /// # fn example() {
51    /// let config = FileConfig::default();
52    /// let backend = LocalStorageBackend::new(PathBuf::from("/custom/path"));
53    /// let storage = FileStorage::with_backend(config, Box::new(backend));
54    /// # }
55    /// ```
56    pub fn with_backend(config: FileConfig, backend: Box<dyn StorageBackend>) -> Self {
57        Self { backend, config }
58    }
59
60    /// Initialize the storage backend
61    pub async fn initialize(&self) -> Result<()> {
62        self.backend.initialize().await
63    }
64
65    /// Store file data and return the relative path
66    ///
67    /// The hash is used as the key for content-addressed storage.
68    pub async fn store_data(&self, hash: &str, data: &[u8]) -> Result<PathBuf> {
69        self.backend.write(hash, data).await
70    }
71
72    /// Read file data by relative path
73    pub async fn read_data(&self, relative_path: &Path) -> Result<Vec<u8>> {
74        self.backend.read(relative_path).await
75    }
76
77    /// Delete file data by relative path
78    pub async fn delete_data(&self, relative_path: &Path) -> Result<()> {
79        self.backend.delete(relative_path).await
80    }
81
82    /// Check if a file exists by hash
83    pub async fn exists(&self, hash: &str) -> bool {
84        self.backend.exists(hash).await
85    }
86
87    /// Get the full path for a relative path
88    pub fn full_path(&self, relative_path: &Path) -> PathBuf {
89        self.backend.full_path(relative_path)
90    }
91
92    /// Get storage statistics
93    pub async fn stats(&self) -> Result<StorageStats> {
94        let backend_stats = self.backend.stats().await?;
95        Ok(StorageStats {
96            total_files: backend_stats.total_objects,
97            total_size: backend_stats.total_size,
98            total_refs: 0, // This is populated from the index, not the backend
99        })
100    }
101
102    /// Get a reference to the underlying backend
103    pub fn backend(&self) -> &dyn StorageBackend {
104        self.backend.as_ref()
105    }
106}
107
108/// Convert hash to storage path
109///
110/// Example: "abcdef123..." -> "data/ab/cdef123..."
111pub fn hash_to_path(hash: &str) -> PathBuf {
112    if hash.len() < 4 {
113        return PathBuf::from(hash);
114    }
115
116    let prefix = &hash[0..2];
117    let rest = &hash[2..];
118    PathBuf::from(prefix).join(rest)
119}
120
121/// Compute SHA256 hash of data
122pub fn compute_hash(data: &[u8]) -> String {
123    use sha2::{Digest, Sha256};
124    let mut hasher = Sha256::new();
125    hasher.update(data);
126    hex::encode(hasher.finalize())
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use tempfile::TempDir;
133
134    #[test]
135    fn test_hash_to_path() {
136        assert_eq!(hash_to_path("abcdef123456"), PathBuf::from("ab/cdef123456"));
137        assert_eq!(hash_to_path("ab"), PathBuf::from("ab"));
138        assert_eq!(hash_to_path("a"), PathBuf::from("a"));
139    }
140
141    #[test]
142    fn test_compute_hash() {
143        let data = b"hello world";
144        let hash = compute_hash(data);
145        assert_eq!(hash.len(), 64); // SHA256 is 64 hex chars
146        assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
147    }
148
149    #[tokio::test]
150    async fn test_file_storage_with_backend() {
151        let temp_dir = TempDir::new().unwrap();
152        let config = FileConfig::with_path(temp_dir.path().to_path_buf());
153        let storage = FileStorage::new(config);
154
155        storage.initialize().await.unwrap();
156
157        let data = b"test content";
158        let hash = compute_hash(data);
159        let path = storage.store_data(&hash, data).await.unwrap();
160
161        assert!(storage.exists(&hash).await);
162
163        let read_data = storage.read_data(&path).await.unwrap();
164        assert_eq!(read_data, data);
165    }
166
167    #[tokio::test]
168    async fn test_file_storage_with_custom_backend() {
169        let temp_dir = TempDir::new().unwrap();
170        let config = FileConfig::with_path(temp_dir.path().to_path_buf());
171
172        // Create custom backend
173        let custom_backend = LocalStorageBackend::new(temp_dir.path().join("custom"));
174        let storage = FileStorage::with_backend(config, Box::new(custom_backend));
175
176        storage.initialize().await.unwrap();
177
178        let data = b"custom backend test";
179        let hash = compute_hash(data);
180        let path = storage.store_data(&hash, data).await.unwrap();
181
182        assert!(storage.exists(&hash).await);
183
184        let read_data = storage.read_data(&path).await.unwrap();
185        assert_eq!(read_data, data);
186    }
187}