Recursive (no-delimiter) listing of objects under `base`, with a hard cap on the number of objects considered. Returns `(entries, truncated)` where `entries` holds `(relative_key, size_bytes)` tuples relative to `base`'s S3 prefix, and `truncated` is `true` when the cap was reached before the listing completed. The listing-prefix marker itself is filtered out. Used as the foundation for both [`W
(
&self,
base: &WorkspacePath,
max_objects: usize,
)
| 705 | /// [`WorkspaceSearch::grep`]. Always paginates through continuation |
| 706 | /// tokens to avoid silently dropping objects past the first page. |
| 707 | async fn list_recursive_under( |
| 708 | &self, |
| 709 | base: &WorkspacePath, |
| 710 | max_objects: usize, |
| 711 | ) -> Result<(Vec<(String, u64)>, bool)> { |
| 712 | let prefix = self.list_prefix_for(base); |
| 713 | let mut entries: Vec<(String, u64)> = Vec::new(); |
| 714 | let mut continuation: Option<String> = None; |
| 715 | let mut truncated = false; |
| 716 | |
| 717 | loop { |
| 718 | let mut req = self |
| 719 | .client |
| 720 | .list_objects_v2() |
| 721 | .bucket(&self.bucket) |
| 722 | .prefix(&prefix); |
| 723 | if let Some(t) = continuation.as_ref() { |
| 724 | req = req.continuation_token(t); |
| 725 | } |
| 726 | let start = std::time::Instant::now(); |
| 727 | let send_result = req.send().await; |
| 728 | emit_s3_call_event( |
| 729 | "s3.list_objects_v2_recursive", |
| 730 | &self.bucket, |
| 731 | &prefix, |
| 732 | send_result |
| 733 | .as_ref() |
| 734 | .ok() |
| 735 | .map_or(0, |r| r.contents().len() as u64), |
| 736 | send_result.is_ok(), |
| 737 | start.elapsed(), |
| 738 | ); |
| 739 | let resp = send_result.map_err(|e| classify_list_error(&self.bucket, &prefix, e))?; |
| 740 | |
| 741 | for obj in resp.contents() { |
| 742 | if entries.len() >= max_objects { |
| 743 | truncated = true; |
| 744 | return Ok((entries, truncated)); |
| 745 | } |
| 746 | let Some(key) = obj.key() else { continue }; |
| 747 | if key == prefix { |
| 748 | continue; |
| 749 | } |
| 750 | let Some(rel) = key.strip_prefix(&prefix) else { |
| 751 | continue; |
| 752 | }; |
| 753 | if rel.is_empty() { |
| 754 | continue; |
| 755 | } |
| 756 | let size = obj.size().unwrap_or(0).max(0) as u64; |
| 757 | entries.push((rel.to_string(), size)); |
| 758 | } |
| 759 | |
| 760 | if resp.is_truncated().unwrap_or(false) { |
| 761 | continuation = resp.next_continuation_token().map(|s| s.to_string()); |
| 762 | if continuation.is_none() { |
| 763 | break; |
| 764 | } |
no test coverage detected