1use anyhow::Result;
8use hashtree_blossom::BlossomClient;
9use hashtree_config::detect_local_daemon_url;
10use hashtree_core::{to_hex, Cid, HashTree, HashTreeConfig, Link};
11use nostr::Keys;
12use std::collections::{HashSet, VecDeque};
13use std::sync::Arc;
14use std::time::Duration;
15use tracing::debug;
16
17use crate::config::Config as CliConfig;
18use crate::storage::HashtreeStore;
19use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
20
21fn child_cid(parent: &Cid, link: &Link) -> Cid {
22 let inherits_parent_key = link
23 .name
24 .as_deref()
25 .map(|name| {
26 name.starts_with("_chunk_")
27 || (name.starts_with('_') && name.chars().count() == 2 && link.link_type.is_tree())
28 })
29 .unwrap_or(false);
30
31 Cid {
32 hash: link.hash,
33 key: link.key.or(if inherits_parent_key {
34 parent.key
35 } else {
36 None
37 }),
38 }
39}
40
41#[derive(Debug, Default)]
42pub struct FetchProgress {
43 chunks_fetched: AtomicUsize,
44 bytes_fetched: AtomicU64,
45}
46
47#[derive(Debug, Clone, Copy, Default)]
48pub struct FetchProgressSnapshot {
49 pub chunks_fetched: usize,
50 pub bytes_fetched: u64,
51}
52
53impl FetchProgress {
54 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub fn snapshot(&self) -> FetchProgressSnapshot {
59 FetchProgressSnapshot {
60 chunks_fetched: self.chunks_fetched.load(Ordering::Relaxed),
61 bytes_fetched: self.bytes_fetched.load(Ordering::Relaxed),
62 }
63 }
64
65 fn record_chunk(&self, byte_len: usize) {
66 self.chunks_fetched.fetch_add(1, Ordering::Relaxed);
67 self.bytes_fetched
68 .fetch_add(byte_len as u64, Ordering::Relaxed);
69 }
70}
71
72#[derive(Clone)]
74pub struct FetchConfig {
75 pub blossom_timeout: Duration,
77}
78
79impl Default for FetchConfig {
80 fn default() -> Self {
81 Self {
82 blossom_timeout: Duration::from_millis(10000),
83 }
84 }
85}
86
87pub struct Fetcher {
89 blossom: BlossomClient,
90}
91
92impl Fetcher {
93 pub fn new(config: FetchConfig) -> Self {
96 let keys = Keys::generate();
98 let blossom = BlossomClient::new(keys).with_timeout(config.blossom_timeout);
99 let blossom = with_local_daemon_read(blossom);
100
101 Self { blossom }
102 }
103
104 pub fn with_keys(config: FetchConfig, keys: Keys) -> Self {
106 let blossom = BlossomClient::new(keys).with_timeout(config.blossom_timeout);
107 let blossom = with_local_daemon_read(blossom);
108
109 Self { blossom }
110 }
111
112 pub fn blossom(&self) -> &BlossomClient {
114 &self.blossom
115 }
116
117 pub async fn fetch_chunk(&self, hash_hex: &str) -> Result<Vec<u8>> {
119 let short_hash = if hash_hex.len() >= 12 {
120 &hash_hex[..12]
121 } else {
122 hash_hex
123 };
124
125 debug!("Trying Blossom for {}", short_hash);
126 match self.blossom.download(hash_hex).await {
127 Ok(data) => {
128 debug!("Got {} from Blossom ({} bytes)", short_hash, data.len());
129 Ok(data)
130 }
131 Err(e) => {
132 debug!("Blossom download failed for {}: {}", short_hash, e);
133 Err(anyhow::anyhow!(
134 "Failed to fetch {} from any source: {}",
135 short_hash,
136 e
137 ))
138 }
139 }
140 }
141
142 pub async fn fetch_chunk_with_store(
144 &self,
145 store: &HashtreeStore,
146 hash: &[u8; 32],
147 ) -> Result<Vec<u8>> {
148 if let Some(data) = store.get_chunk(hash)? {
150 return Ok(data);
151 }
152
153 let hash_hex = to_hex(hash);
155 let data = self.fetch_chunk(&hash_hex).await?;
156 store.put_cached_blob(&data)?;
157 Ok(data)
158 }
159
160 pub async fn fetch_tree(
163 &self,
164 store: &HashtreeStore,
165 root_hash: &[u8; 32],
166 ) -> Result<(usize, u64)> {
167 self.fetch_cid_tree(store, &Cid::public(*root_hash)).await
168 }
169
170 pub async fn fetch_cid_tree(
172 &self,
173 store: &HashtreeStore,
174 root_cid: &Cid,
175 ) -> Result<(usize, u64)> {
176 self.fetch_cid_tree_with_progress(store, root_cid, None)
177 .await
178 }
179
180 pub async fn fetch_cid_tree_with_progress(
182 &self,
183 store: &HashtreeStore,
184 root_cid: &Cid,
185 progress: Option<&FetchProgress>,
186 ) -> Result<(usize, u64)> {
187 self.fetch_cid_tree_parallel_with_progress(store, root_cid, 1, progress)
188 .await
189 }
190
191 pub async fn fetch_tree_parallel(
195 &self,
196 store: &HashtreeStore,
197 root_hash: &[u8; 32],
198 concurrency: usize,
199 ) -> Result<(usize, u64)> {
200 self.fetch_cid_tree_parallel(store, &Cid::public(*root_hash), concurrency)
201 .await
202 }
203
204 pub async fn fetch_cid_tree_parallel(
206 &self,
207 store: &HashtreeStore,
208 root_cid: &Cid,
209 concurrency: usize,
210 ) -> Result<(usize, u64)> {
211 self.fetch_cid_tree_parallel_with_progress(store, root_cid, concurrency, None)
212 .await
213 }
214
215 pub async fn fetch_cid_tree_parallel_with_progress(
217 &self,
218 store: &HashtreeStore,
219 root_cid: &Cid,
220 concurrency: usize,
221 progress: Option<&FetchProgress>,
222 ) -> Result<(usize, u64)> {
223 use futures::stream::{FuturesUnordered, StreamExt};
224
225 let chunks_fetched = Arc::new(AtomicUsize::new(0));
226 let bytes_fetched = Arc::new(AtomicU64::new(0));
227 let mut queued: HashSet<[u8; 32]> = HashSet::new();
228 let mut pending: VecDeque<Cid> = VecDeque::new();
229
230 pending.push_back(root_cid.clone());
231 queued.insert(root_cid.hash);
232
233 let mut active = FuturesUnordered::new();
234 let tree = HashTree::new(HashTreeConfig::new(store.store_arc()).public());
235
236 loop {
237 while active.len() < concurrency {
239 if let Some(cid) = pending.pop_front() {
240 if store.blob_exists(&cid.hash).unwrap_or(false) {
241 if let Some(node) = tree.get_node(&cid).await? {
242 for link in node.links {
243 let child = child_cid(&cid, &link);
244 if queued.insert(child.hash) {
245 pending.push_back(child);
246 }
247 }
248 }
249 continue;
250 }
251
252 let hash_hex = to_hex(&cid.hash);
253 let blossom = self.blossom.clone();
254 let fut = async move {
255 let data = blossom.download(&hash_hex).await;
256 (cid, data)
257 };
258 active.push(fut);
259 } else {
260 break;
261 }
262 }
263
264 if active.is_empty() {
266 break;
267 }
268
269 if let Some((cid, result)) = active.next().await {
271 match result {
272 Ok(data) => {
273 store.put_cached_blob(&data)?;
275 if let Some(progress) = progress {
276 progress.record_chunk(data.len());
277 }
278 chunks_fetched.fetch_add(1, Ordering::Relaxed);
279 bytes_fetched.fetch_add(data.len() as u64, Ordering::Relaxed);
280
281 if let Some(node) = tree.get_node(&cid).await? {
282 for link in node.links {
283 let child = child_cid(&cid, &link);
284 if queued.insert(child.hash) {
285 pending.push_back(child);
286 }
287 }
288 }
289 }
290 Err(e) => {
291 debug!("Failed to fetch {}: {}", to_hex(&cid.hash), e);
292 }
294 }
295 }
296 }
297
298 Ok((
299 chunks_fetched.load(Ordering::Relaxed),
300 bytes_fetched.load(Ordering::Relaxed),
301 ))
302 }
303
304 pub async fn fetch_file(
307 &self,
308 store: &HashtreeStore,
309 hash: &[u8; 32],
310 ) -> Result<Option<Vec<u8>>> {
311 if let Some(content) = store.get_file(hash)? {
313 return Ok(Some(content));
314 }
315
316 self.fetch_tree(store, hash).await?;
318
319 store.get_file(hash)
321 }
322
323 pub async fn fetch_directory(
325 &self,
326 store: &HashtreeStore,
327 hash: &[u8; 32],
328 ) -> Result<Option<crate::storage::DirectoryListing>> {
329 if let Ok(Some(listing)) = store.get_directory_listing(hash) {
331 return Ok(Some(listing));
332 }
333
334 self.fetch_tree(store, hash).await?;
336
337 store.get_directory_listing(hash)
339 }
340
341 pub async fn upload(&self, data: &[u8]) -> Result<String> {
343 self.blossom
344 .upload(data)
345 .await
346 .map_err(|e| anyhow::anyhow!("Blossom upload failed: {}", e))
347 }
348
349 pub async fn upload_if_missing(&self, data: &[u8]) -> Result<(String, bool)> {
351 self.blossom
352 .upload_if_missing(data)
353 .await
354 .map_err(|e| anyhow::anyhow!("Blossom upload failed: {}", e))
355 }
356}
357
358fn with_local_daemon_read(blossom: BlossomClient) -> BlossomClient {
359 let bind_address = CliConfig::load().ok().map(|cfg| cfg.server.bind_address);
360 let local_url = detect_local_daemon_url(bind_address.as_deref());
361 let Some(local_url) = local_url else {
362 return blossom;
363 };
364
365 let mut servers = blossom.read_servers().to_vec();
366 if servers.iter().any(|server| server == &local_url) {
367 return blossom;
368 }
369 servers.insert(0, local_url);
370 blossom.with_read_servers(servers)
371}