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: always keeps `concurrency` requests in flight
99    pub async fn walk_parallel(
100        &self,
101        cid: &Cid,
102        path: &str,
103        concurrency: usize,
104    ) -> Result<Vec<WalkEntry>, HashTreeError> {
105        self.walk_parallel_with_progress(cid, path, concurrency, None)
106            .await
107    }
108
109    /// Walk entire tree with parallel fetching and optional progress counter
110    /// The counter is incremented for each node fetched (not just entries found)
111    ///
112    /// OPTIMIZATION: Blobs are NOT fetched - their metadata (hash, size, link_type)
113    /// comes from the parent node's link, so we just add them directly to entries.
114    /// This avoids downloading file contents during tree traversal.
115    pub async fn walk_parallel_with_progress(
116        &self,
117        cid: &Cid,
118        path: &str,
119        concurrency: usize,
120        progress: Option<&std::sync::atomic::AtomicUsize>,
121    ) -> Result<Vec<WalkEntry>, HashTreeError> {
122        use futures::stream::{FuturesUnordered, StreamExt};
123        use std::collections::VecDeque;
124        use std::sync::atomic::Ordering;
125
126        let mut entries = Vec::new();
127        let mut pending: VecDeque<(Cid, String)> = VecDeque::new();
128        let mut active = FuturesUnordered::new();
129
130        // Seed with root
131        pending.push_back((cid.clone(), path.to_string()));
132
133        loop {
134            // Fill up to concurrency limit from pending queue
135            while active.len() < concurrency {
136                if let Some((node_cid, node_path)) = pending.pop_front() {
137                    let store = &self.store;
138                    let fut = async move {
139                        let data = store
140                            .get(&node_cid.hash)
141                            .await
142                            .map_err(|e| HashTreeError::Store(e.to_string()))?;
143                        Ok::<_, HashTreeError>((node_cid, node_path, data))
144                    };
145                    active.push(fut);
146                } else {
147                    break;
148                }
149            }
150
151            // If nothing active, we're done
152            if active.is_empty() {
153                break;
154            }
155
156            // Wait for any future to complete
157            if let Some(result) = active.next().await {
158                let (node_cid, node_path, data) = result?;
159
160                // Update progress counter
161                if let Some(counter) = progress {
162                    counter.fetch_add(1, Ordering::Relaxed);
163                }
164
165                let data = match data {
166                    Some(d) => d,
167                    None => continue,
168                };
169
170                // Decrypt if key is present
171                let data = if let Some(key) = &node_cid.key {
172                    decrypt_chk(&data, key).map_err(|e| {
173                        HashTreeError::Decryption(format!(
174                            "{} at path '{}' hash {} key {}",
175                            e,
176                            node_path,
177                            hex::encode(node_cid.hash),
178                            hex::encode(key)
179                        ))
180                    })?
181                } else {
182                    data
183                };
184
185                let node = match Self::decode_node_or_blob(&data)? {
186                    Some(node) => node,
187                    None => {
188                        // It's a blob/file - this case only happens for root
189                        entries.push(WalkEntry {
190                            path: node_path,
191                            hash: node_cid.hash,
192                            link_type: LinkType::Blob,
193                            size: data.len() as u64,
194                            key: node_cid.key,
195                        });
196                        continue;
197                    }
198                };
199
200                // It's a directory/file node
201                let node_size: u64 = node.links.iter().map(|l| l.size).sum();
202                entries.push(WalkEntry {
203                    path: node_path.clone(),
204                    hash: node_cid.hash,
205                    link_type: node.node_type,
206                    size: node_size,
207                    key: node_cid.key,
208                });
209
210                // Queue children - but DON'T fetch blobs, just add them directly
211                for link in &node.links {
212                    let child_path = match &link.name {
213                        Some(name) => {
214                            if is_internal_directory_link(&node, link) {
215                                let sub_cid = Cid {
216                                    hash: link.hash,
217                                    key: link.key,
218                                };
219                                pending.push_back((sub_cid, node_path.clone()));
220                                continue;
221                            }
222                            if node_path.is_empty() {
223                                name.clone()
224                            } else {
225                                format!("{}/{}", node_path, name)
226                            }
227                        }
228                        None => node_path.clone(),
229                    };
230
231                    // OPTIMIZATION: If it's a blob, add entry directly without fetching
232                    // The link already contains all the metadata we need
233                    if link.link_type == LinkType::Blob {
234                        entries.push(WalkEntry {
235                            path: child_path,
236                            hash: link.hash,
237                            link_type: LinkType::Blob,
238                            size: link.size,
239                            key: link.key,
240                        });
241                        if let Some(counter) = progress {
242                            counter.fetch_add(1, Ordering::Relaxed);
243                        }
244                        continue;
245                    }
246
247                    // For tree nodes (File/Dir), we need to fetch to see their children
248                    let child_cid = Cid {
249                        hash: link.hash,
250                        key: link.key,
251                    };
252                    pending.push_back((child_cid, child_path));
253                }
254            }
255        }
256
257        Ok(entries)
258    }
259
260    /// Walk tree as stream
261    pub fn walk_stream(
262        &self,
263        cid: Cid,
264        initial_path: String,
265    ) -> Pin<Box<dyn Stream<Item = Result<WalkEntry, HashTreeError>> + Send + '_>> {
266        Box::pin(stream::unfold(
267            WalkStreamState::Init {
268                cid,
269                path: initial_path,
270                tree: self,
271            },
272            |state| async move {
273                match state {
274                    WalkStreamState::Init { cid, path, tree } => {
275                        let data = match tree.store.get(&cid.hash).await {
276                            Ok(Some(d)) => d,
277                            Ok(None) => return None,
278                            Err(e) => {
279                                return Some((
280                                    Err(HashTreeError::Store(e.to_string())),
281                                    WalkStreamState::Done,
282                                ))
283                            }
284                        };
285
286                        // Decrypt if key is present
287                        let data = if let Some(key) = &cid.key {
288                            match decrypt_chk(&data, key) {
289                                Ok(d) => d,
290                                Err(e) => {
291                                    return Some((
292                                        Err(HashTreeError::Decryption(format!(
293                                            "{} at path '{}' hash {} key {}",
294                                            e,
295                                            path,
296                                            hex::encode(cid.hash),
297                                            hex::encode(key)
298                                        ))),
299                                        WalkStreamState::Done,
300                                    ))
301                                }
302                            }
303                        } else {
304                            data
305                        };
306
307                        let node = match Self::decode_node_or_blob(&data) {
308                            Ok(Some(node)) => node,
309                            Ok(None) => {
310                                // Blob data
311                                let entry = WalkEntry {
312                                    path,
313                                    hash: cid.hash,
314                                    link_type: LinkType::Blob,
315                                    size: data.len() as u64,
316                                    key: cid.key,
317                                };
318                                return Some((Ok(entry), WalkStreamState::Done));
319                            }
320                            Err(err) => return Some((Err(err), WalkStreamState::Done)),
321                        };
322
323                        let node_size: u64 = node.links.iter().map(|l| l.size).sum();
324                        let entry = WalkEntry {
325                            path: path.clone(),
326                            hash: cid.hash,
327                            link_type: node.node_type,
328                            size: node_size,
329                            key: cid.key,
330                        };
331
332                        // Create stack with children to process
333                        let mut stack: Vec<WalkStackItem> = Vec::new();
334                        for link in node.links.iter().rev() {
335                            let is_internal = is_internal_directory_link(&node, link);
336                            let child_path = match &link.name {
337                                Some(name) if !is_internal => {
338                                    if path.is_empty() {
339                                        name.clone()
340                                    } else {
341                                        format!("{}/{}", path, name)
342                                    }
343                                }
344                                _ => path.clone(),
345                            };
346                            // Child nodes use their own key from link
347                            stack.push(WalkStackItem {
348                                hash: link.hash,
349                                path: child_path,
350                                key: link.key,
351                            });
352                        }
353
354                        Some((Ok(entry), WalkStreamState::Processing { stack, tree }))
355                    }
356                    WalkStreamState::Processing { mut stack, tree } => {
357                        tree.process_walk_stack(&mut stack).await
358                    }
359                    WalkStreamState::Done => None,
360                }
361            },
362        ))
363    }
364
365    async fn process_walk_stack<'a>(
366        &'a self,
367        stack: &mut Vec<WalkStackItem>,
368    ) -> Option<(Result<WalkEntry, HashTreeError>, WalkStreamState<'a, S>)> {
369        while let Some(item) = stack.pop() {
370            let data = match self.store.get(&item.hash).await {
371                Ok(Some(d)) => d,
372                Ok(None) => continue,
373                Err(e) => {
374                    return Some((
375                        Err(HashTreeError::Store(e.to_string())),
376                        WalkStreamState::Done,
377                    ))
378                }
379            };
380
381            let data = if let Some(key) = &item.key {
382                match decrypt_chk(&data, key) {
383                    Ok(d) => d,
384                    Err(e) => {
385                        return Some((
386                            Err(HashTreeError::Decryption(format!(
387                                "{} at path '{}' hash {} key {}",
388                                e,
389                                item.path,
390                                hex::encode(item.hash),
391                                hex::encode(key)
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}