(&self, path: &WorkspacePath)
| 547 | } |
| 548 | |
| 549 | async fn list_dir(&self, path: &WorkspacePath) -> WorkspaceResult<Vec<WorkspaceDirEntry>> { |
| 550 | let prefix = self.list_prefix_for(path); |
| 551 | let mut entries: Vec<WorkspaceDirEntry> = Vec::new(); |
| 552 | // `total_listed` counts every Content/CommonPrefix the server returned |
| 553 | // including the prefix marker (the zero-byte "<prefix>/" object some |
| 554 | // tools create to denote an empty directory). We use it to distinguish |
| 555 | // "prefix exists but has no children" from "prefix never existed" so |
| 556 | // `ls` on a missing path on S3 errors like it does on local FS. |
| 557 | let mut total_listed: usize = 0; |
| 558 | let mut continuation: Option<String> = None; |
| 559 | |
| 560 | loop { |
| 561 | let mut req = self |
| 562 | .client |
| 563 | .list_objects_v2() |
| 564 | .bucket(&self.bucket) |
| 565 | .prefix(&prefix) |
| 566 | .delimiter("/"); |
| 567 | if let Some(token) = continuation.as_ref() { |
| 568 | req = req.continuation_token(token); |
| 569 | } |
| 570 | |
| 571 | let start = std::time::Instant::now(); |
| 572 | let send_result = req.send().await; |
| 573 | emit_s3_call_event( |
| 574 | "s3.list_objects_v2", |
| 575 | &self.bucket, |
| 576 | &prefix, |
| 577 | send_result.as_ref().ok().map_or(0, |r| { |
| 578 | r.contents().len() as u64 + r.common_prefixes().len() as u64 |
| 579 | }), |
| 580 | send_result.is_ok(), |
| 581 | start.elapsed(), |
| 582 | ); |
| 583 | let resp = send_result.map_err(|e| classify_list_error(&self.bucket, &prefix, e))?; |
| 584 | |
| 585 | // CommonPrefixes → directories |
| 586 | for cp in resp.common_prefixes() { |
| 587 | total_listed += 1; |
| 588 | if let Some(p) = cp.prefix() { |
| 589 | // p looks like "<prefix><name>/"; extract <name> |
| 590 | if let Some(name) = strip_dir_name(p, &prefix) { |
| 591 | entries.push(WorkspaceDirEntry { |
| 592 | name, |
| 593 | kind: WorkspaceFileType::Directory, |
| 594 | size: 0, |
| 595 | }); |
| 596 | } |
| 597 | } |
| 598 | } |
| 599 | |
| 600 | // Contents → files |
| 601 | for obj in resp.contents() { |
| 602 | total_listed += 1; |
| 603 | let Some(key) = obj.key() else { continue }; |
| 604 | // Skip the prefix marker itself (key == prefix exactly). |
| 605 | if key == prefix { |
| 606 | continue; |
nothing calls this directly
no test coverage detected