agent_diva_files/
storage.rs1use crate::backend::{LocalStorageBackend, StorageBackend};
10use crate::config::FileConfig;
11use crate::Result;
12use std::path::{Path, PathBuf};
13
14pub struct FileStorage {
19 backend: Box<dyn StorageBackend>,
20 #[allow(dead_code)]
21 config: FileConfig,
22}
23
24#[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 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 pub fn with_backend(config: FileConfig, backend: Box<dyn StorageBackend>) -> Self {
57 Self { backend, config }
58 }
59
60 pub async fn initialize(&self) -> Result<()> {
62 self.backend.initialize().await
63 }
64
65 pub async fn store_data(&self, hash: &str, data: &[u8]) -> Result<PathBuf> {
69 self.backend.write(hash, data).await
70 }
71
72 pub async fn read_data(&self, relative_path: &Path) -> Result<Vec<u8>> {
74 self.backend.read(relative_path).await
75 }
76
77 pub async fn delete_data(&self, relative_path: &Path) -> Result<()> {
79 self.backend.delete(relative_path).await
80 }
81
82 pub async fn exists(&self, hash: &str) -> bool {
84 self.backend.exists(hash).await
85 }
86
87 pub fn full_path(&self, relative_path: &Path) -> PathBuf {
89 self.backend.full_path(relative_path)
90 }
91
92 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, })
100 }
101
102 pub fn backend(&self) -> &dyn StorageBackend {
104 self.backend.as_ref()
105 }
106}
107
108pub 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
121pub 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); 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 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}