This is an automated email from the ASF dual-hosted git repository.
erickguan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git
The following commit(s) were added to refs/heads/main by this push:
new b6a0a7a9c fix(services/onedrive): build correct children URL when
listing root (#7720)
b6a0a7a9c is described below
commit b6a0a7a9c60d050c8ba22a532fb51007483effae
Author: tonghuaroot (童话) <[email protected]>
AuthorDate: Sun Jun 21 13:00:12 2026 +0800
fix(services/onedrive): build correct children URL when listing root (#7720)
* fix(services/onedrive): build correct children URL when listing root
* docs(services/onedrive): tighten root list-request comment
---
core/Cargo.lock | 1 +
core/services/onedrive/Cargo.toml | 1 +
core/services/onedrive/src/core.rs | 167 +++++++++++++++++++++++++++++++++++
core/services/onedrive/src/lister.rs | 25 ++----
4 files changed, 176 insertions(+), 18 deletions(-)
diff --git a/core/Cargo.lock b/core/Cargo.lock
index b5b4ed5b6..457874deb 100644
--- a/core/Cargo.lock
+++ b/core/Cargo.lock
@@ -7324,6 +7324,7 @@ name = "opendal-service-onedrive"
version = "0.57.0"
dependencies = [
"bytes",
+ "futures",
"http 1.4.2",
"log",
"mea",
diff --git a/core/services/onedrive/Cargo.toml
b/core/services/onedrive/Cargo.toml
index 79024e42d..e3bf9f06d 100644
--- a/core/services/onedrive/Cargo.toml
+++ b/core/services/onedrive/Cargo.toml
@@ -41,4 +41,5 @@ serde_json = { workspace = true }
tokio = { workspace = true, features = ["time"] }
[dev-dependencies]
+futures = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
diff --git a/core/services/onedrive/src/core.rs
b/core/services/onedrive/src/core.rs
index 7920c4414..ee2cebba1 100644
--- a/core/services/onedrive/src/core.rs
+++ b/core/services/onedrive/src/core.rs
@@ -269,6 +269,42 @@ impl OneDriveCore {
Ok(decoded_response.value)
}
+ pub(crate) fn onedrive_list_request(
+ &self,
+ path: &str,
+ limit: Option<usize>,
+ ) -> Result<Request<Buffer>> {
+ let item_url = self.onedrive_item_url(path, true);
+ // Root is addressed as `root/children`, not the path form
`root:/children`.
+ let mut url = if item_url == Self::DRIVE_ROOT_URL {
+ format!("{item_url}/children?{GENERAL_SELECT_PARAM}")
+ } else {
+ format!("{item_url}:/children?{GENERAL_SELECT_PARAM}")
+ };
+ if let Some(limit) = limit {
+ url += &format!("&$top={limit}");
+ }
+
+ Request::get(&url)
+ .extension(Operation::List)
+ .extension(ServiceOperation("ListChildren"))
+ .body(Buffer::new())
+ .map_err(new_request_build_error)
+ }
+
+ pub(crate) async fn onedrive_list(
+ &self,
+ ctx: &OperationContext,
+ path: &str,
+ limit: Option<usize>,
+ ) -> Result<Response<Buffer>> {
+ let mut request = self.onedrive_list_request(path, limit)?;
+
+ self.sign(ctx, &mut request).await?;
+
+ ctx.http_transport().send(request).await
+ }
+
pub(crate) async fn onedrive_get_next_list_page(
&self,
ctx: &OperationContext,
@@ -750,6 +786,137 @@ impl OneDriveSigner {
}
}
+#[cfg(test)]
+mod tests {
+ use bytes::Bytes;
+ use futures::stream;
+ use http::StatusCode;
+ use opendal_core::raw::oio::List;
+
+ use super::super::lister::OneDriveLister;
+ use super::*;
+
+ const ROOT_STAT_RESPONSE: &str =
r#"{"id":"0","name":"root","lastModifiedDateTime":"2026-01-01T00:00:00Z","eTag":"aTag","size":0,"parentReference":{"path":"","driveId":"d","id":"p"},"folder":{"childCount":1}}"#;
+ const ROOT_CHILDREN_RESPONSE: &str =
r#"{"value":[{"id":"1","name":"test.txt","lastModifiedDateTime":"2026-01-01T00:00:00Z","eTag":"aTag","size":5,"parentReference":{"path":"/drive/root:","driveId":"d","id":"p"},"file":{"mimeType":"text/plain"}}]}"#;
+
+ #[derive(Clone)]
+ struct MockHttpTransport;
+
+ impl HttpTransport for MockHttpTransport {
+ 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}/children?{GENERAL_SELECT_PARAM}") {
+ (StatusCode::OK, ROOT_CHILDREN_RESPONSE)
+ } else if url == root_url {
+ (StatusCode::OK, ROOT_STAT_RESPONSE)
+ } else {
+ (
+ StatusCode::NOT_FOUND,
+ r#"{"error":{"code":"itemNotFound","message":"Item not
found"}}"#,
+ )
+ };
+
+ let data = Bytes::from_static(body.as_bytes());
+ 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())
+ }
+ }
+
+ fn test_ctx() -> OperationContext {
+
OperationContext::new().with_http_transport(HttpTransporter::new(MockHttpTransport))
+ }
+
+ fn test_core(root: &str) -> Arc<OneDriveCore> {
+ let info = ServiceInfo::new("onedrive", root, "");
+
+ let mut signer = OneDriveSigner::new();
+ signer.access_token = "token".to_string();
+ signer.expires_in = Timestamp::MAX;
+
+ Arc::new(OneDriveCore {
+ info,
+ capability: Capability::default(),
+ root: root.to_string(),
+ signer: Arc::new(Mutex::new(signer)),
+ })
+ }
+
+ #[test]
+ fn list_request_for_root_targets_drive_root_children() {
+ let core = test_core("/");
+ let request = core.onedrive_list_request("/", None).unwrap();
+ assert_eq!(
+ request.uri().to_string(),
+ format!(
+ "{}/children?{}",
+ OneDriveCore::DRIVE_ROOT_URL,
+ GENERAL_SELECT_PARAM
+ )
+ );
+ }
+
+ #[test]
+ fn list_request_for_nested_path_uses_path_addressing() {
+ let core = test_core("/");
+ let request = core.onedrive_list_request("foo/", Some(10)).unwrap();
+ assert_eq!(
+ request.uri().to_string(),
+ format!(
+ "{}:/foo:/children?{}&$top=10",
+ OneDriveCore::DRIVE_ROOT_URL,
+ GENERAL_SELECT_PARAM
+ )
+ );
+ }
+
+ #[test]
+ fn list_request_for_root_under_custom_root_uses_path_addressing() {
+ let core = test_core("/base/");
+ let request = core.onedrive_list_request("", None).unwrap();
+ assert_eq!(
+ request.uri().to_string(),
+ format!(
+ "{}:/base:/children?{}",
+ OneDriveCore::DRIVE_ROOT_URL,
+ GENERAL_SELECT_PARAM
+ )
+ );
+ }
+
+ #[tokio::test]
+ async fn list_root_returns_entries() {
+ let core = test_core("/");
+ let ctx = test_ctx();
+ let lister = OneDriveLister::new(
+ "/".to_string(),
+ core,
+ ctx,
+ Capability::default(),
+ &OpList::default(),
+ );
+ let mut lister = oio::PageLister::new(lister);
+
+ let mut entries = Vec::new();
+ while let Some(entry) = lister.next().await.unwrap() {
+ entries.push(entry);
+ }
+
+ assert_eq!(entries.len(), 2);
+ assert_eq!(entries[0].mode(), EntryMode::DIR);
+ assert_eq!(entries[1].path(), "test.txt");
+ assert_eq!(entries[1].mode(), EntryMode::FILE);
+ }
+}
+
mod error {
use http::Response;
use http::StatusCode;
diff --git a/core/services/onedrive/src/lister.rs
b/core/services/onedrive/src/lister.rs
index 58700616f..f5d88bc18 100644
--- a/core/services/onedrive/src/lister.rs
+++ b/core/services/onedrive/src/lister.rs
@@ -25,7 +25,6 @@ use opendal_core::*;
use super::core::OneDriveCore;
use super::core::parse_error;
-use super::graph_model::GENERAL_SELECT_PARAM;
use super::graph_model::GraphApiOneDriveListResponse;
use super::graph_model::ItemType;
@@ -59,26 +58,16 @@ impl OneDriveLister {
impl oio::PageList for OneDriveLister {
async fn next_page(&self, ctx: &mut oio::PageContext) -> Result<()> {
- let request_url = if ctx.token.is_empty() {
- let base = format!(
- "{}:/children?{}",
- self.core.onedrive_item_url(&self.path, true),
- GENERAL_SELECT_PARAM
- );
- if let Some(limit) = self.op.limit() {
- base + &format!("&$top={limit}")
- } else {
- base
- }
+ let response = if ctx.token.is_empty() {
+ self.core
+ .onedrive_list(&self.ctx, &self.path, self.op.limit())
+ .await?
} else {
- ctx.token.clone()
+ self.core
+ .onedrive_get_next_list_page(&self.ctx, &ctx.token)
+ .await?
};
- let response = self
- .core
- .onedrive_get_next_list_page(&self.ctx, &request_url)
- .await?;
-
let status_code = response.status();
if !status_code.is_success() {
if status_code == http::StatusCode::NOT_FOUND {