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")
+ }
+}