Skip to main content

hashtree_core/hashtree/
walk.rs

1use super::*;
2use crate::directory::is_internal_directory_link;
3
4impl<S: Store> HashTree<S> {
5    fn decode_node_or_blob(data: &[u8]) -> Result<Option<TreeNode>, HashTreeError> {
6        match decode_tree_node(data) {
7            Ok(node) => Ok(Some(node)),
8            Err(err) if is_tree_node(data) => Err(HashTreeError::Codec(err)),
9            Err(_) => Ok(None),
10        }
11    }
12
13    /// Walk entire tree depth-first (returns Vec)
14    pub async fn walk(&self, cid: &Cid, path: &str) -> Result<Vec<WalkEntry>, HashTreeError> {
15        let mut entries = Vec::new();
16        self.walk_recursive(cid, path, &mut entries).await?;
17        Ok(entries)
18    }
19
20    async fn walk_recursive(
21        &self,
22        cid: &Cid,
23        path: &str,
24        entries: &mut Vec<WalkEntry>,
25    ) -> Result<(), HashTreeError> {
26        let data = match self
27            .store
28            .get(&cid.hash)
29            .await
30            .map_err(|e| HashTreeError::Store(e.to_string()))?
31        {
32            Some(d) => d,
33            None => return Ok(()),
34        };
35
36        // Decrypt if key is present
37        let data = if let Some(key) = &cid.key {
38            decrypt_chk(&data, key).map_err(|e| HashTreeError::Decryption(e.to_string()))?
39        } else {
40            data
41        };
42
43        let node = match Self::decode_node_or_blob(&data)? {
44            Some(node) => node,
45            None => {
46                entries.push(WalkEntry {
47                    path: path.to_string(),
48                    hash: cid.hash,
49                    link_type: LinkType::Blob,
50                    size: data.len() as u64,
51                    key: cid.key,
52                });
53                return Ok(());
54            }
55        };
56
57        let node_size: u64 = node.links.iter().map(|l| l.size).sum();
58        entries.push(WalkEntry {
59            path: path.to_string(),
60            hash: cid.hash,
61            link_type: node.node_type,
62            size: node_size,
63            key: cid.key,
64        });
65
66        for link in &node.links {
67            let child_path = match &link.name {
68                Some(name) => {
69                    if is_internal_directory_link(&node, link) {
70                        let sub_cid = Cid {
71                            hash: link.hash,
72                            key: link.key,
73                        };
74                        Box::pin(self.walk_recursive(&sub_cid, path, entries)).await?;
75                        continue;
76                    }
77                    if path.is_empty() {
78                        name.clone()
79                    } else {
80                        format!("{}/{}", path, name)
81                    }
82                }
83                None => path.to_string(),
84            };
85
86            // Child nodes use their own key from link
87            let child_cid = Cid {
88                hash: link.hash,
89                key: link.key,
90            };
91            Box::pin(self.walk_recursive(&child_cid, &child_path, entries)).await?;
92        }
93
94        Ok(())
95    }
96
97    /// Walk entire tree with parallel fetching
98    /// Uses a work-stealing approach: keeps up to `concurrency` requests in flight.
99    /// Zero concurrency runs serially.
100    pub async fn walk_parallel(
101        &self,
102        cid: &Cid,
103        path: &str,
104        concurrency: usize,
105    ) -> Result<Vec<WalkEntry>, HashTreeError> {
106        self.walk_parallel_with_progress(cid, path, concurrency, None)
107            .await
108    }
109
110    /// Walk entire tree with parallel fetching and optional progress counter
111    /// The counter is incremented for each node fetched (not just entries found).
112    /// Zero concurrency runs serially.
113    ///
114    /// OPTIMIZATION: Blobs are NOT fetched - their metadata (hash, size, link_type)
115    /// comes from the parent node's link, so we just add them directly to entries.
116    /// This avoids downloading file contents during tree traversal.
117    pub async fn walk_parallel_with_progress(
118        &self,
119        cid: &Cid,
120        path: &str,
121        concurrency: usize,
122        progress: Option<&std::sync::atomic::AtomicUsize>,
123    ) -> Result<Vec<WalkEntry>, HashTreeError> {
124        use futures::stream::{FuturesUnordered, StreamExt};
125        use std::collections::VecDeque;
126        use std::sync::atomic::Ordering;
127
128        let mut entries = Vec::new();
129        let concurrency = concurrency.max(1);
130        let mut pending: VecDeque<(Cid, String)> = VecDeque::new();
131        let mut active = FuturesUnordered::new();
132
133        // Seed with root
134        pending.push_back((cid.clone(), path.to_string()));
135
136        loop {
137            // Fill up to concurrency limit from pending queue
138            while active.len() < concurrency {
139                if let Some((node_cid, node_path)) = pending.pop_front() {
140                    let store = &self.store;
141                    let fut = async move {
142                        let data = store
143                            .get(&node_cid.hash)
144                            .await
145                            .map_err(|e| HashTreeError::Store(e.to_string()))?;
146                        Ok::<_, HashTreeError>((node_cid, node_path, data))
147                    };
148                    active.push(fut);
149                } else {
150                    break;
151                }
152            }
153
154            // If nothing active, we're done
155            if active.is_empty() {
156                break;
157            }
158
159            // Wait for any future to complete
160            if let Some(result) = active.next().await {
161                let (node_cid, node_path, data) = result?;
162
163                // Update progress counter
164                if let Some(counter) = progress {
165                    counter.fetch_add(1, Ordering::Relaxed);
166                }
167
168                let data = match data {
169                    Some(d) => d,
170                    None => continue,
171                };
172
173                // Decrypt if key is present
174                let data = if let Some(key) = &node_cid.key {
175                    decrypt_chk(&data, key).map_err(|e| {
176                        HashTreeError::Decryption(format!(
177                            "{} at path '{}' hash {}",
178                            e,
179                            node_path,
180                            hex::encode(node_cid.hash)
181                        ))
182                    })?
183                } else {
184                    data
185                };
186
187                let node = match Self::decode_node_or_blob(&data)? {
188                    Some(node) => node,
189                    None => {
190                        // It's a blob/file - this case only happens for root
191                        entries.push(WalkEntry {
192                            path: node_path,
193                            hash: node_cid.hash,
194                            link_type: LinkType::Blob,
195                            size: data.len() as u64,
196                            key: node_cid.key,
197                        });
198                        continue;
199                    }
200                };
201
202                // It's a directory/file node
203                let node_size: u64 = node.links.iter().map(|l| l.size).sum();
204                entries.push(WalkEntry {
205                    path: node_path.clone(),
206                    hash: node_cid.hash,
207                    link_type: node.node_type,
208                    size: node_size,
209                    key: node_cid.key,
210                });
211
212                // Queue children - but DON'T fetch blobs, just add them directly
213                for link in &node.links {
214                    let child_path = match &link.name {
215                        Some(name) => {
216                            if is_internal_directory_link(&node, link) {
217                                let sub_cid = Cid {
218                                    hash: link.hash,
219                                    key: link.key,
220                                };
221                                pending.push_back((sub_cid, node_path.clone()));
222                                continue;
223                            }
224                            if node_path.is_empty() {
225                                name.clone()
226                            } else {
227                                format!("{}/{}", node_path, name)
228                            }
229                        }
230                        None => node_path.clone(),
231                    };
232
233                    // OPTIMIZATION: If it's a blob, add entry directly without fetching
234                    // The link already contains all the metadata we need
235                    if link.link_type == LinkType::Blob {
236                        entries.push(WalkEntry {
237                            path: child_path,
238                            hash: link.hash,
239                            link_type: LinkType::Blob,
240                            size: link.size,
241                            key: link.key,
242                        });
243                        if let Some(counter) = progress {
244                            counter.fetch_add(1, Ordering::Relaxed);
245                        }
246                        continue;
247                    }
248
249                    // For tree nodes (File/Dir), we need to fetch to see their children
250                    let child_cid = Cid {
251                        hash: link.hash,
252                        key: link.key,
253                    };
254                    pending.push_back((child_cid, child_path));
255                }
256            }
257        }
258
259        Ok(entries)
260    }
261
262    /// Walk tree as stream
263    pub fn walk_stream(
264        &self,
265        cid: Cid,
266        initial_path: String,
267    ) -> Pin<Box<dyn Stream<Item = Result<WalkEntry, HashTreeError>> + Send + '_>> {
268        Box::pin(stream::unfold(
269            WalkStreamState::Init {
270                cid,
271                path: initial_path,
272                tree: self,
273            },
274            |state| async move {
275                match state {
276                    WalkStreamState::Init { cid, path, tree } => {
277                        let data = match tree.store.get(&cid.hash).await {
278                            Ok(Some(d)) => d,
279                            Ok(None) => return None,
280                            Err(e) => {
281                                return Some((
282                                    Err(HashTreeError::Store(e.to_string())),
283                                    WalkStreamState::Done,
284                                ))
285                            }
286                        };
287
288                        // Decrypt if key is present
289                        let data = if let Some(key) = &cid.key {
290                            match decrypt_chk(&data, key) {
291                                Ok(d) => d,
292                                Err(e) => {
293                                    return Some((
294                                        Err(HashTreeError::Decryption(format!(
295                                            "{} at path '{}' hash {}",
296                                            e,
297                                            path,
298                                            hex::encode(cid.hash)
299                                        ))),
300                                        WalkStreamState::Done,
301                                    ))
302                                }
303                            }
304                        } else {
305                            data
306                        };
307
308                        let node = match Self::decode_node_or_blob(&data) {
309                            Ok(Some(node)) => node,
310                            Ok(None) => {
311                                // Blob data
312                                let entry = WalkEntry {
313                                    path,
314                                    hash: cid.hash,
315                                    link_type: LinkType::Blob,
316                                    size: data.len() as u64,
317                                    key: cid.key,
318                                };
319                                return Some((Ok(entry), WalkStreamState::Done));
320                            }
321                            Err(err) => return Some((Err(err), WalkStreamState::Done)),
322                        };
323
324                        let node_size: u64 = node.links.iter().map(|l| l.size).sum();
325                        let entry = WalkEntry {
326                            path: path.clone(),
327                            hash: cid.hash,
328                            link_type: node.node_type,
329                            size: node_size,
330                            key: cid.key,
331                        };
332
333                        // Create stack with children to process
334                        let mut stack: Vec<WalkStackItem> = Vec::new();
335                        for link in node.links.iter().rev() {
336                            let is_internal = is_internal_directory_link(&node, link);
337                            let child_path = match &link.name {
338                                Some(name) if !is_internal => {
339                                    if path.is_empty() {
340                                        name.clone()
341                                    } else {
342                                        format!("{}/{}", path, name)
343                                    }
344                                }
345                                _ => path.clone(),
346                            };
347                            // Child nodes use their own key from link
348                            stack.push(WalkStackItem {
349                                hash: link.hash,
350                                path: child_path,
351                                key: link.key,
352                            });
353                        }
354
355                        Some((Ok(entry), WalkStreamState::Processing { stack, tree }))
356                    }
357                    WalkStreamState::Processing { mut stack, tree } => {
358                        tree.process_walk_stack(&mut stack).await
359                    }
360                    WalkStreamState::Done => None,
361                }
362            },
363        ))
364    }
365
366    async fn process_walk_stack<'a>(
367        &'a self,
368        stack: &mut Vec<WalkStackItem>,
369    ) -> Option<(Result<WalkEntry, HashTreeError>, WalkStreamState<'a, S>)> {
370        while let Some(item) = stack.pop() {
371            let data = match self.store.get(&item.hash).await {
372                Ok(Some(d)) => d,
373                Ok(None) => continue,
374                Err(e) => {
375                    return Some((
376                        Err(HashTreeError::Store(e.to_string())),
377                        WalkStreamState::Done,
378                    ))
379                }
380            };
381
382            let data = if let Some(key) = &item.key {
383                match decrypt_chk(&data, key) {
384                    Ok(d) => d,
385                    Err(e) => {
386                        return Some((
387                            Err(HashTreeError::Decryption(format!(
388                                "{} at path '{}' hash {}",
389                                e,
390                                item.path,
391                                hex::encode(item.hash)
392                            ))),
393                            WalkStreamState::Done,
394                        ))
395                    }
396                }
397            } else {
398                data
399            };
400
401            let node = match Self::decode_node_or_blob(&data) {
402                Ok(Some(node)) => node,
403                Ok(None) => {
404                    // Blob data
405                    let entry = WalkEntry {
406                        path: item.path,
407                        hash: item.hash,
408                        link_type: LinkType::Blob,
409                        size: data.len() as u64,
410                        key: item.key,
411                    };
412                    return Some((
413                        Ok(entry),
414                        WalkStreamState::Processing {
415                            stack: std::mem::take(stack),
416                            tree: self,
417                        },
418                    ));
419                }
420                Err(err) => return Some((Err(err), WalkStreamState::Done)),
421            };
422
423            let node_size: u64 = node.links.iter().map(|l| l.size).sum();
424            let entry = WalkEntry {
425                path: item.path.clone(),
426                hash: item.hash,
427                link_type: node.node_type,
428                size: node_size,
429                key: item.key,
430            };
431
432            // Push children to stack
433            for link in node.links.iter().rev() {
434                let is_internal = is_internal_directory_link(&node, link);
435                let child_path = match &link.name {
436                    Some(name) if !is_internal => {
437                        if item.path.is_empty() {
438                            name.clone()
439                        } else {
440                            format!("{}/{}", item.path, name)
441                        }
442                    }
443                    _ => item.path.clone(),
444                };
445                stack.push(WalkStackItem {
446                    hash: link.hash,
447                    path: child_path,
448                    key: link.key,
449                });
450            }
451
452            return Some((
453                Ok(entry),
454                WalkStreamState::Processing {
455                    stack: std::mem::take(stack),
456                    tree: self,
457                },
458            ));
459        }
460        None
461    }
462}
463
464struct WalkStackItem {
465    hash: Hash,
466    path: String,
467    key: Option<[u8; 32]>,
468}
469
470enum WalkStreamState<'a, S: Store> {
471    Init {
472        cid: Cid,
473        path: String,
474        tree: &'a HashTree<S>,
475    },
476    Processing {
477        stack: Vec<WalkStackItem>,
478        tree: &'a HashTree<S>,
479    },
480    Done,
481}