You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by de...@apache.org on 2026/06/01 16:07:33 UTC

(opendal) branch main updated: feat(lister.go): Add opendal_list_options_set_limit and opendal_list_options_set_start_after (#7649)

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

dentiny 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 773eadbd6 feat(lister.go): Add opendal_list_options_set_limit and opendal_list_options_set_start_after (#7649)
773eadbd6 is described below

commit 773eadbd651234926efad30b99e9d2c7f5f8673d
Author: PoAn Yang <pa...@apache.org>
AuthorDate: Tue Jun 2 01:07:26 2026 +0900

    feat(lister.go): Add opendal_list_options_set_limit and opendal_list_options_set_start_after (#7649)
---
 bindings/c/include/opendal.h                  | 33 ++++++++++++-
 bindings/c/src/operator.rs                    | 13 ++++++
 bindings/c/src/types.rs                       | 67 +++++++++++++++++++++++++--
 bindings/go/lister.go                         | 62 ++++++++++++++++++++++++-
 bindings/go/string_ownership_test.go          | 36 ++++++++++++++
 bindings/go/tests/behavior_tests/list_test.go | 61 ++++++++++++++++++++++++
 6 files changed, 265 insertions(+), 7 deletions(-)

diff --git a/bindings/c/include/opendal.h b/bindings/c/include/opendal.h
index 5a040ef92..ef06e3589 100644
--- a/bindings/c/include/opendal.h
+++ b/bindings/c/include/opendal.h
@@ -468,19 +468,29 @@ typedef struct opendal_result_list {
  * \brief The options for the list operation.
  *
  * This struct carries the options for the list operation, including whether to
- * list recursively. Use `opendal_list_options_new()` to construct and
- * `opendal_list_options_free()` to free.
+ * list recursively, an optional result limit, and an optional start-after key.
+ * Use `opendal_list_options_new()` to construct and `opendal_list_options_free()` to free.
  *
  * @see opendal_operator_list_with
  * @see opendal_list_options_new
  * @see opendal_list_options_free
  * @see opendal_list_options_set_recursive
+ * @see opendal_list_options_set_limit
+ * @see opendal_list_options_set_start_after
  */
 typedef struct opendal_list_options {
   /**
    * Whether to list recursively under the prefix; default false.
    */
   bool recursive;
+  /**
+   * Optional hint for maximum results per request; 0 means unset.
+   */
+  uintptr_t limit;
+  /**
+   * Optional key to start listing from; NULL means unset.
+   */
+  char *start_after;
 } opendal_list_options;
 
 /**
@@ -1678,6 +1688,25 @@ struct opendal_list_options *opendal_list_options_new(void);
  */
 void opendal_list_options_set_recursive(struct opendal_list_options *opts, bool recursive);
 
+/**
+ * \brief Set the limit option.
+ *
+ * @param opts The opendal_list_options to modify.
+ * @param limit Maximum number of results per request; 0 means unset.
+ */
+void opendal_list_options_set_limit(struct opendal_list_options *opts, uintptr_t limit);
+
+/**
+ * \brief Set the start_after option.
+ *
+ * Passes the specified key to the underlying service to start listing from.
+ *
+ * @param opts The opendal_list_options to modify.
+ * @param start_after The key to start listing from; NULL to unset.
+ */
+void opendal_list_options_set_start_after(struct opendal_list_options *opts,
+                                          const char *start_after);
+
 /**
  * \brief Free the heap memory used by opendal_list_options.
  *
diff --git a/bindings/c/src/operator.rs b/bindings/c/src/operator.rs
index 4b3a058d1..d62225a0a 100644
--- a/bindings/c/src/operator.rs
+++ b/bindings/c/src/operator.rs
@@ -802,8 +802,21 @@ pub unsafe extern "C" fn opendal_operator_list_with(
         core::options::ListOptions::default()
     } else {
         let o = &*opts;
+        let limit = if o.limit == 0 { None } else { Some(o.limit) };
+        let start_after = if o.start_after.is_null() {
+            None
+        } else {
+            Some(
+                std::ffi::CStr::from_ptr(o.start_after)
+                    .to_str()
+                    .expect("malformed start_after")
+                    .to_owned(),
+            )
+        };
         core::options::ListOptions {
             recursive: o.recursive,
+            limit,
+            start_after,
             ..Default::default()
         }
     };
diff --git a/bindings/c/src/types.rs b/bindings/c/src/types.rs
index 1b63f24ce..c13e73ec7 100644
--- a/bindings/c/src/types.rs
+++ b/bindings/c/src/types.rs
@@ -16,7 +16,7 @@
 // under the License.
 
 use std::collections::HashMap;
-use std::ffi::{c_void, CString};
+use std::ffi::{c_void, CStr, CString};
 use std::os::raw::c_char;
 
 use opendal::Buffer;
@@ -87,17 +87,23 @@ impl opendal_bytes {
 /// \brief The options for the list operation.
 ///
 /// This struct carries the options for the list operation, including whether to
-/// list recursively. Use `opendal_list_options_new()` to construct and
-/// `opendal_list_options_free()` to free.
+/// list recursively, an optional result limit, and an optional start-after key.
+/// Use `opendal_list_options_new()` to construct and `opendal_list_options_free()` to free.
 ///
 /// @see opendal_operator_list_with
 /// @see opendal_list_options_new
 /// @see opendal_list_options_free
 /// @see opendal_list_options_set_recursive
+/// @see opendal_list_options_set_limit
+/// @see opendal_list_options_set_start_after
 #[repr(C)]
 pub struct opendal_list_options {
     /// Whether to list recursively under the prefix; default false.
     pub recursive: bool,
+    /// Optional hint for maximum results per request; 0 means unset.
+    pub limit: usize,
+    /// Optional key to start listing from; NULL means unset.
+    pub start_after: *mut c_char,
 }
 
 impl opendal_list_options {
@@ -108,7 +114,11 @@ impl opendal_list_options {
     /// @see opendal_list_options_free
     #[no_mangle]
     pub extern "C" fn opendal_list_options_new() -> *mut Self {
-        Box::into_raw(Box::new(Self { recursive: false }))
+        Box::into_raw(Box::new(Self {
+            recursive: false,
+            limit: 0,
+            start_after: std::ptr::null_mut(),
+        }))
     }
 
     /// \brief Set the recursive option.
@@ -125,12 +135,61 @@ impl opendal_list_options {
         }
     }
 
+    /// \brief Set the limit option.
+    ///
+    /// @param opts The opendal_list_options to modify.
+    /// @param limit Maximum number of results per request; 0 means unset.
+    #[no_mangle]
+    pub unsafe extern "C" fn opendal_list_options_set_limit(
+        opts: *mut opendal_list_options,
+        limit: usize,
+    ) {
+        if !opts.is_null() {
+            (*opts).limit = limit;
+        }
+    }
+
+    /// \brief Set the start_after option.
+    ///
+    /// Passes the specified key to the underlying service to start listing from.
+    ///
+    /// @param opts The opendal_list_options to modify.
+    /// @param start_after The key to start listing from; NULL to unset.
+    #[no_mangle]
+    pub unsafe extern "C" fn opendal_list_options_set_start_after(
+        opts: *mut opendal_list_options,
+        start_after: *const c_char,
+    ) {
+        if opts.is_null() {
+            return;
+        }
+
+        let o = &mut *opts;
+        // Free any previous value.
+        if !o.start_after.is_null() {
+            drop(CString::from_raw(o.start_after));
+            o.start_after = std::ptr::null_mut();
+        }
+        if !start_after.is_null() {
+            let s = CStr::from_ptr(start_after)
+                .to_str()
+                .expect("malformed start_after")
+                .to_owned();
+            o.start_after = CString::new(s).unwrap().into_raw();
+        }
+    }
+
     /// \brief Free the heap memory used by opendal_list_options.
     ///
     /// @param opts The opendal_list_options to free.
     #[no_mangle]
     pub unsafe extern "C" fn opendal_list_options_free(opts: *mut opendal_list_options) {
         if !opts.is_null() {
+            let o = &mut *opts;
+            if !o.start_after.is_null() {
+                drop(CString::from_raw(o.start_after));
+                o.start_after = std::ptr::null_mut();
+            }
             drop(Box::from_raw(opts));
         }
     }
diff --git a/bindings/go/lister.go b/bindings/go/lister.go
index fc6fd2b33..bf54c6652 100644
--- a/bindings/go/lister.go
+++ b/bindings/go/lister.go
@@ -86,9 +86,30 @@ func ListWithRecursive(recursive bool) WithListFn {
 	}
 }
 
+// ListWithLimit sets the maximum number of results per request hint for the list operation.
+//
+// This is a hint to the backend; the actual number of results may differ.
+// A value of 0 means no limit is set.
+func ListWithLimit(limit uint) WithListFn {
+	return func(o *listOptions) {
+		o.limit = limit
+	}
+}
+
+// ListWithStartAfter sets the start-after key for the list operation.
+//
+// Passes the specified key to the underlying service to start listing from.
+func ListWithStartAfter(startAfter string) WithListFn {
+	return func(o *listOptions) {
+		o.startAfter = &startAfter
+	}
+}
+
 // listOptions holds the options for a list operation.
 type listOptions struct {
-	recursive bool
+	recursive  bool
+	limit      uint
+	startAfter *string
 }
 
 // List returns a Lister to iterate over entries that start with the given path.
@@ -139,6 +160,12 @@ func (op *Operator) List(path string, opts ...WithListFn) (*Lister, error) {
 	cOpts := ffiListOptionsNew.symbol(op.ctx)()
 	defer ffiListOptionsFree.symbol(op.ctx)(cOpts)
 	ffiListOptionsSetRecursive.symbol(op.ctx)(cOpts, o.recursive)
+	if o.limit > 0 {
+		ffiListOptionsSetLimit.symbol(op.ctx)(cOpts, o.limit)
+	}
+	if o.startAfter != nil {
+		ffiListOptionsSetStartAfter.symbol(op.ctx)(cOpts, *o.startAfter)
+	}
 	listerInner, err := ffiOperatorListWith.symbol(op.ctx)(op.inner, path, cOpts)
 	if err != nil {
 		return nil, err
@@ -349,6 +376,39 @@ var ffiListOptionsSetRecursive = newFFI(ffiOpts{
 	}
 })
 
+var ffiListOptionsSetLimit = newFFI(ffiOpts{
+	sym:    "opendal_list_options_set_limit",
+	rType:  &ffi.TypeVoid,
+	aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer},
+}, func(_ context.Context, ffiCall ffiCall) func(opts *opendalListOptions, limit uint) {
+	return func(opts *opendalListOptions, limit uint) {
+		l := uintptr(limit)
+		ffiCall(
+			nil,
+			unsafe.Pointer(&opts),
+			unsafe.Pointer(&l),
+		)
+	}
+})
+
+var ffiListOptionsSetStartAfter = newFFI(ffiOpts{
+	sym:    "opendal_list_options_set_start_after",
+	rType:  &ffi.TypeVoid,
+	aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer},
+}, func(_ context.Context, ffiCall ffiCall) func(opts *opendalListOptions, startAfter string) {
+	return func(opts *opendalListOptions, startAfter string) {
+		bytePtr, err := BytePtrFromString(startAfter)
+		if err != nil {
+			return
+		}
+		ffiCall(
+			nil,
+			unsafe.Pointer(&opts),
+			unsafe.Pointer(&bytePtr),
+		)
+	}
+})
+
 var ffiListOptionsFree = newFFI(ffiOpts{
 	sym:    "opendal_list_options_free",
 	rType:  &ffi.TypeVoid,
diff --git a/bindings/go/string_ownership_test.go b/bindings/go/string_ownership_test.go
index 400ece17c..edfae9d4e 100644
--- a/bindings/go/string_ownership_test.go
+++ b/bindings/go/string_ownership_test.go
@@ -659,6 +659,42 @@ func TestListWithRecursiveFalse(t *testing.T) {
 	}
 }
 
+func TestListWithLimitSetsValue(t *testing.T) {
+	o := &listOptions{}
+	ListWithLimit(100)(o)
+	if o.limit != 100 {
+		t.Fatalf("ListWithLimit(100): limit = %d, want 100", o.limit)
+	}
+}
+
+func TestListWithLimitOverwrite(t *testing.T) {
+	o := &listOptions{}
+	ListWithLimit(50)(o)
+	ListWithLimit(200)(o)
+	if o.limit != 200 {
+		t.Fatalf("ListWithLimit overwrite: limit = %d, want 200", o.limit)
+	}
+}
+
+func TestListWithStartAfterSetsValue(t *testing.T) {
+	o := &listOptions{}
+	ListWithStartAfter("some/key")(o)
+	if o.startAfter == nil {
+		t.Fatal("ListWithStartAfter: startAfter = nil, want non-nil")
+	}
+	if *o.startAfter != "some/key" {
+		t.Fatalf("ListWithStartAfter: startAfter = %q, want some/key", *o.startAfter)
+	}
+}
+
+func TestListWithStartAfterEmptyString(t *testing.T) {
+	o := &listOptions{}
+	ListWithStartAfter("")(o)
+	if o.startAfter == nil {
+		t.Fatal("ListWithStartAfter(empty): startAfter = nil, want non-nil pointer to empty string")
+	}
+}
+
 func TestFfiOperatorListWithReturnType(t *testing.T) {
 	if ffiOperatorListWith.opts.rType != &typeResultList {
 		t.Fatalf("ffiOperatorListWith rType = %v, want typeResultList", ffiOperatorListWith.opts.rType)
diff --git a/bindings/go/tests/behavior_tests/list_test.go b/bindings/go/tests/behavior_tests/list_test.go
index 5fc09e27e..2950ba1c6 100644
--- a/bindings/go/tests/behavior_tests/list_test.go
+++ b/bindings/go/tests/behavior_tests/list_test.go
@@ -48,6 +48,12 @@ func testsList(cap *opendal.Capability) []behaviorTest {
 	if cap.ListWithRecursive() {
 		tests = append(tests, testListWithRecursive)
 	}
+	if cap.ListWithLimit() {
+		tests = append(tests, testListWithLimit)
+	}
+	if cap.ListWithStartAfter() {
+		tests = append(tests, testListWithStartAfter)
+	}
 	return tests
 }
 
@@ -354,3 +360,58 @@ func testListWithRecursive(assert *require.Assertions, op *opendal.Operator, fix
 	assert.Contains(paths, fileMid, "recursive list must include file in sub-directory")
 	assert.Contains(paths, fileDeep, "recursive list must include file in deep directory")
 }
+
+func testListWithLimit(assert *require.Assertions, op *opendal.Operator, fixture *fixture) {
+	parent := fixture.NewDirPath()
+	assert.Nil(op.CreateDir(parent))
+
+	// Write 5 files.
+	for range 5 {
+		path, content, _ := fixture.NewFileWithPath(fmt.Sprintf("%s%s", parent, uuid.NewString()))
+		assert.Nil(op.Write(path, content))
+	}
+
+	// List with limit=2; the operation must succeed without error.
+	obs, err := op.List(parent, opendal.ListWithLimit(2))
+	assert.Nil(err)
+	defer obs.Close()
+
+	var paths []string
+	for obs.Next() {
+		paths = append(paths, obs.Entry().Path())
+	}
+	assert.Nil(obs.Error())
+	// At least one entry must be returned (limit is a hint, not a hard cap for all backends).
+	assert.NotEmpty(paths, "list with limit must return at least one entry")
+}
+
+func testListWithStartAfter(assert *require.Assertions, op *opendal.Operator, fixture *fixture) {
+	parent := fixture.NewDirPath()
+	assert.Nil(op.CreateDir(parent))
+
+	// Write files with predictable sorted names.
+	var filePaths []string
+	for i := range 5 {
+		name := fmt.Sprintf("%sfile-%02d", parent, i)
+		filePaths = append(filePaths, name)
+		assert.Nil(op.Write(name, []byte("content")))
+	}
+	slices.Sort(filePaths)
+
+	// Start listing from the second file (index 1).
+	pivotName := strings.TrimPrefix(filePaths[1], "/")
+	obs, err := op.List(parent, opendal.ListWithStartAfter(pivotName))
+	assert.Nil(err)
+	defer obs.Close()
+
+	var paths []string
+	for obs.Next() {
+		paths = append(paths, obs.Entry().Path())
+	}
+	assert.Nil(obs.Error())
+
+	// Files after the pivot must appear.
+	for _, p := range filePaths[2:] {
+		assert.Contains(paths, p, "start_after must include entries after pivot")
+	}
+}