You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by er...@apache.org on 2026/08/15 07:17:12 UTC

(opendal) 07/07: fix(services/onedrive): honor list start after

This is an automated email from the ASF dual-hosted git repository.

erickguan pushed a commit to branch fix/onedrive-behavior-followups
in repository https://gitbox.apache.org/repos/asf/opendal.git

commit 50cd01b267cf616c64d03b07685d37ebdc7e45e8
Author: Erick Guan <29...@users.noreply.github.com>
AuthorDate: Sat Aug 15 14:02:36 2026 +0800

    fix(services/onedrive): honor list start after
---
 core/services/onedrive/src/backend.rs |  4 +-
 core/services/onedrive/src/core.rs    | 87 +++++++++++++++++++++++++++++++++++
 core/services/onedrive/src/lister.rs  | 16 ++++++-
 3 files changed, 104 insertions(+), 3 deletions(-)

diff --git a/core/services/onedrive/src/backend.rs b/core/services/onedrive/src/backend.rs
index f8c29f182..62098a5ca 100644
--- a/core/services/onedrive/src/backend.rs
+++ b/core/services/onedrive/src/backend.rs
@@ -149,6 +149,7 @@ impl Builder for OnedriveBuilder {
 
             list: true,
             list_with_limit: true,
+            list_with_start_after: true,
             list_with_versions: true,
 
             shared: true,
@@ -362,12 +363,13 @@ mod tests {
     use super::OnedriveBuilder;
 
     #[test]
-    fn capability_does_not_advertise_versioned_stat() {
+    fn capabilities_match_supported_version_listing() {
         let backend = OnedriveBuilder::default()
             .access_token("test-token")
             .build()
             .unwrap();
 
         assert!(!backend.capability().stat_with_version);
+        assert!(backend.capability().list_with_start_after);
     }
 }
diff --git a/core/services/onedrive/src/core.rs b/core/services/onedrive/src/core.rs
index dde05b848..a1349ff05 100644
--- a/core/services/onedrive/src/core.rs
+++ b/core/services/onedrive/src/core.rs
@@ -1002,6 +1002,44 @@ mod tests {
         assert_eq!(entries[2].metadata().version(), Some("1.0"));
     }
 
+    #[tokio::test]
+    async fn list_with_versions_respects_start_after() {
+        let core = test_core("/");
+        let ctx = OperationContext::new()
+            .with_http_transport(HttpTransporter::new(StartAfterMockHttpTransport));
+        let lister = OneDriveLister::new(
+            "dir/".to_string(),
+            core,
+            ctx,
+            Capability {
+                list_with_start_after: true,
+                list_with_versions: true,
+                ..Default::default()
+            },
+            &OpList::new()
+                .with_versions(true)
+                .with_start_after("dir/file-2"),
+        );
+        let mut lister = oio::PageLister::new(lister);
+
+        let mut paths = Vec::new();
+        while let Some(entry) = lister.next().await.unwrap() {
+            paths.push(entry.path().to_string());
+        }
+
+        assert_eq!(
+            paths,
+            [
+                "dir/file-3",
+                "dir/file-3",
+                "dir/file-4",
+                "dir/file-4",
+                "dir/file-5",
+                "dir/file-5",
+            ]
+        );
+    }
+
     #[derive(Clone)]
     struct VersionMockHttpTransport;
 
@@ -1034,6 +1072,55 @@ mod tests {
                 .unwrap())
         }
     }
+
+    #[derive(Clone)]
+    struct StartAfterMockHttpTransport;
+
+    impl HttpTransport for StartAfterMockHttpTransport {
+        async fn fetch(&self, req: Request<Buffer>) -> Result<Response<HttpBody>> {
+            let url = req.uri().to_string();
+            let root_url = OneDriveCore::DRIVE_ROOT_URL;
+
+            let (status, body) = if url
+                == format!("{root_url}:/dir:/children?{GENERAL_SELECT_PARAM}")
+            {
+                let items = (0..6)
+                    .map(|index| {
+                        format!(
+                            r#"{{"id":"{index}","name":"file-{index}","lastModifiedDateTime":"2026-01-01T00:00:00Z","eTag":"tag-{index}","size":5,"parentReference":{{"path":"/drive/root:/dir","driveId":"d","id":"p"}},"file":{{"mimeType":"text/plain"}}}}"#
+                        )
+                    })
+                    .collect::<Vec<_>>()
+                    .join(",");
+                (StatusCode::OK, format!(r#"{{"value":[{items}]}}"#))
+            } else if url == format!("{root_url}:/dir") {
+                (
+                    StatusCode::OK,
+                    r#"{"id":"dir","name":"dir","lastModifiedDateTime":"2026-01-01T00:00:00Z","eTag":"dir-tag","size":0,"parentReference":{"path":"/drive/root:","driveId":"d","id":"p"},"folder":{"childCount":6}}"#.to_string(),
+                )
+            } else if url.starts_with(&format!("{root_url}:/dir/file-"))
+                && url.ends_with(&format!(":/versions?{VERSION_SELECT_PARAM}"))
+            {
+                (StatusCode::OK, VERSION_RESPONSE.to_string())
+            } else {
+                (
+                    StatusCode::NOT_FOUND,
+                    r#"{"error":{"code":"itemNotFound","message":"Item not found"}}"#.to_string(),
+                )
+            };
+
+            let data = Bytes::from(body);
+            let size = data.len() as u64;
+            Ok(Response::builder()
+                .status(status)
+                .header(header::CONTENT_LENGTH, size)
+                .body(HttpBody::new(
+                    stream::iter(vec![Ok(Buffer::from(data))]),
+                    Some(size),
+                ))
+                .unwrap())
+        }
+    }
 }
 
 mod error {
diff --git a/core/services/onedrive/src/lister.rs b/core/services/onedrive/src/lister.rs
index b001d72eb..ab7bb540b 100644
--- a/core/services/onedrive/src/lister.rs
+++ b/core/services/onedrive/src/lister.rs
@@ -54,6 +54,12 @@ impl OneDriveLister {
             op: args.clone(),
         }
     }
+
+    fn is_after_start(&self, path: &str) -> bool {
+        self.op
+            .start_after()
+            .is_none_or(|start_after| path > start_after)
+    }
 }
 
 impl oio::PageList for OneDriveLister {
@@ -100,8 +106,10 @@ impl oio::PageList for OneDriveLister {
 
             // skip `list_with_versions` intentionally because a folder doesn't have versions
 
-            let entry = oio::Entry::new(&path, meta);
-            ctx.entries.push_back(entry);
+            if self.is_after_start(&path) {
+                let entry = oio::Entry::new(&path, meta);
+                ctx.entries.push_back(entry);
+            }
         }
 
         if let Some(next_link) = decoded_response.next_link {
@@ -129,6 +137,10 @@ impl oio::PageList for OneDriveLister {
                 normalized_path.push('/');
             }
 
+            if !self.is_after_start(&normalized_path) {
+                continue;
+            }
+
             let mut meta = Metadata::new(entry_mode)
                 .with_etag(drive_item.e_tag)
                 .with_content_length(drive_item.size.max(0) as u64);