tatara_engine/p2p/
cache.rs1use anyhow::Result;
2use std::collections::{HashMap, HashSet};
3use std::path::{Path, PathBuf};
4use tokio::sync::RwLock;
5use tracing::{debug, info, warn};
6
7use super::chunk::{Chunk, ChunkManifest};
8
9pub struct DataCache {
16 dir: PathBuf,
17 index: RwLock<HashSet<String>>,
19 manifests: RwLock<HashMap<String, ChunkManifest>>,
21}
22
23impl DataCache {
24 pub async fn new(dir: &Path) -> Result<Self> {
25 tokio::fs::create_dir_all(dir.join("manifests")).await?;
26 tokio::fs::create_dir_all(dir.join("chunks")).await?;
27
28 let cache = Self {
29 dir: dir.to_path_buf(),
30 index: RwLock::new(HashSet::new()),
31 manifests: RwLock::new(HashMap::new()),
32 };
33
34 cache.rebuild_index().await?;
35 Ok(cache)
36 }
37
38 pub async fn put_chunk(&self, chunk: &Chunk) -> Result<()> {
40 if !chunk.verify() {
41 anyhow::bail!("Chunk verification failed: {}", chunk.hash);
42 }
43
44 let path = self.chunk_path(&chunk.hash);
45 if let Some(parent) = path.parent() {
46 tokio::fs::create_dir_all(parent).await?;
47 }
48
49 tokio::fs::write(&path, &chunk.data).await?;
50 self.index.write().await.insert(chunk.hash.clone());
51
52 debug!(hash = %chunk.hash, size = chunk.size, "Stored chunk");
53 Ok(())
54 }
55
56 pub async fn get_chunk(&self, hash: &str) -> Result<Option<Chunk>> {
58 if !self.has_chunk(hash).await {
59 return Ok(None);
60 }
61
62 let path = self.chunk_path(hash);
63 let data = tokio::fs::read(&path).await?;
64
65 let chunk = Chunk {
66 hash: hash.to_string(),
67 data,
68 size: 0, };
70
71 if !chunk.verify() {
73 warn!(hash = hash, "Cached chunk failed verification — removing");
74 let _ = tokio::fs::remove_file(&path).await;
75 self.index.write().await.remove(hash);
76 return Ok(None);
77 }
78
79 Ok(Some(Chunk {
80 size: chunk.data.len(),
81 ..chunk
82 }))
83 }
84
85 pub async fn has_chunk(&self, hash: &str) -> bool {
87 self.index.read().await.contains(hash)
88 }
89
90 pub async fn put_manifest(&self, manifest: &ChunkManifest) -> Result<()> {
92 let path = self
93 .dir
94 .join("manifests")
95 .join(format!("{}.json", manifest.root_hash));
96 let data = serde_json::to_string_pretty(manifest)?;
97 tokio::fs::write(&path, data).await?;
98 self.manifests
99 .write()
100 .await
101 .insert(manifest.root_hash.clone(), manifest.clone());
102
103 debug!(
104 root_hash = %manifest.root_hash,
105 chunks = manifest.chunks.len(),
106 content_type = %manifest.content_type,
107 "Stored manifest"
108 );
109 Ok(())
110 }
111
112 pub async fn get_manifest(&self, root_hash: &str) -> Option<ChunkManifest> {
114 self.manifests.read().await.get(root_hash).cloned()
115 }
116
117 pub async fn local_chunks(&self) -> Vec<String> {
119 self.index.read().await.iter().cloned().collect()
120 }
121
122 pub async fn local_manifests(&self) -> Vec<ChunkManifest> {
124 self.manifests.read().await.values().cloned().collect()
125 }
126
127 pub async fn manifest_completeness(&self, manifest: &ChunkManifest) -> (usize, usize) {
129 let index = self.index.read().await;
130 let have = manifest
131 .chunks
132 .iter()
133 .filter(|h| index.contains(*h))
134 .count();
135 (have, manifest.chunks.len())
136 }
137
138 pub async fn store_data(
140 &self,
141 data: &[u8],
142 content_type: &str,
143 label: &str,
144 ) -> Result<ChunkManifest> {
145 let (manifest, chunks) = ChunkManifest::from_data(data, content_type, label);
146
147 for chunk in &chunks {
148 self.put_chunk(chunk).await?;
149 }
150 self.put_manifest(&manifest).await?;
151
152 info!(
153 root_hash = %manifest.root_hash,
154 chunks = chunks.len(),
155 total_size = manifest.total_size,
156 "Data stored in p2p cache"
157 );
158
159 Ok(manifest)
160 }
161
162 pub async fn retrieve_data(&self, manifest: &ChunkManifest) -> Result<Option<Vec<u8>>> {
164 let mut chunks = Vec::with_capacity(manifest.chunks.len());
165
166 for hash in &manifest.chunks {
167 match self.get_chunk(hash).await? {
168 Some(chunk) => chunks.push(chunk),
169 None => return Ok(None), }
171 }
172
173 manifest
174 .reassemble(&chunks)
175 .map(Some)
176 .map_err(|e| anyhow::anyhow!("Reassembly failed: {}", e))
177 }
178
179 pub async fn size_bytes(&self) -> u64 {
181 let mut total = 0u64;
182 let chunks_dir = self.dir.join("chunks");
183
184 if let Ok(mut entries) = tokio::fs::read_dir(&chunks_dir).await {
185 while let Ok(Some(shard)) = entries.next_entry().await {
186 if let Ok(mut shard_entries) = tokio::fs::read_dir(shard.path()).await {
187 while let Ok(Some(entry)) = shard_entries.next_entry().await {
188 if let Ok(meta) = entry.metadata().await {
189 total += meta.len();
190 }
191 }
192 }
193 }
194 }
195
196 total
197 }
198
199 fn chunk_path(&self, hash: &str) -> PathBuf {
200 let shard = &hash[..2.min(hash.len())];
201 self.dir.join("chunks").join(shard).join(hash)
202 }
203
204 async fn rebuild_index(&self) -> Result<()> {
205 let mut index = self.index.write().await;
206 let mut manifests = self.manifests.write().await;
207
208 let chunks_dir = self.dir.join("chunks");
210 if let Ok(mut entries) = tokio::fs::read_dir(&chunks_dir).await {
211 while let Ok(Some(shard)) = entries.next_entry().await {
212 if let Ok(mut shard_entries) = tokio::fs::read_dir(shard.path()).await {
213 while let Ok(Some(entry)) = shard_entries.next_entry().await {
214 if let Some(hash) = entry.file_name().to_str() {
215 index.insert(hash.to_string());
216 }
217 }
218 }
219 }
220 }
221
222 let manifests_dir = self.dir.join("manifests");
224 if let Ok(mut entries) = tokio::fs::read_dir(&manifests_dir).await {
225 while let Ok(Some(entry)) = entries.next_entry().await {
226 if let Some(name) = entry.file_name().to_str() {
227 if name.ends_with(".json") {
228 if let Ok(data) = tokio::fs::read_to_string(entry.path()).await {
229 if let Ok(manifest) = serde_json::from_str::<ChunkManifest>(&data) {
230 manifests.insert(manifest.root_hash.clone(), manifest);
231 }
232 }
233 }
234 }
235 }
236 }
237
238 info!(
239 chunks = index.len(),
240 manifests = manifests.len(),
241 "P2P cache index rebuilt"
242 );
243
244 Ok(())
245 }
246}