From 67b2351b5b42fa462f737b10b4ce2a4bb8ab617c Mon Sep 17 00:00:00 2001 From: PoAn Yang Date: Tue, 2 Jun 2026 20:58:36 +0900 Subject: [PATCH] feat(lister.go): Add opendal_list_options_set_versions and opendal_list_options_set_deleted Signed-off-by: PoAn Yang --- bindings/c/include/opendal.h | 34 ++++++++ bindings/c/src/operator.rs | 3 +- bindings/c/src/operator_info.rs | 6 ++ bindings/c/src/types.rs | 36 +++++++++ bindings/go/lister.go | 60 ++++++++++++++ bindings/go/operator_info.go | 8 ++ bindings/go/string_ownership_test.go | 34 ++++++++ bindings/go/tests/behavior_tests/list_test.go | 78 +++++++++++++++++++ bindings/go/types.go | 4 + 9 files changed, 262 insertions(+), 1 deletion(-) diff --git a/bindings/c/include/opendal.h b/bindings/c/include/opendal.h index 9c492d6e4db1..d08668d141b3 100644 --- a/bindings/c/include/opendal.h +++ b/bindings/c/include/opendal.h @@ -708,6 +708,8 @@ typedef struct opendal_result_list { * @see opendal_list_options_set_recursive * @see opendal_list_options_set_limit * @see opendal_list_options_set_start_after + * @see opendal_list_options_set_versions + * @see opendal_list_options_set_deleted */ typedef struct opendal_list_options { /** @@ -722,6 +724,14 @@ typedef struct opendal_list_options { * Optional key to start listing from; NULL means unset. */ char *start_after; + /** + * Include object versions when supported by version-aware backends; default false. + */ + bool versions; + /** + * Include delete markers when supported by version-aware backends; default false. + */ + bool deleted; } opendal_list_options; /** @@ -925,6 +935,14 @@ typedef struct opendal_capability { * If backend supports list without delimiter. */ bool list_with_recursive; + /** + * If backend supports list with versions. + */ + bool list_with_versions; + /** + * If backend supports list with deleted. + */ + bool list_with_deleted; /** * If operator supports presign. */ @@ -2106,6 +2124,22 @@ void opendal_list_options_set_limit(struct opendal_list_options *opts, uintptr_t void opendal_list_options_set_start_after(struct opendal_list_options *opts, const char *start_after); +/** + * \brief Set the versions option. + * + * @param opts The opendal_list_options to modify. + * @param versions Whether to include object versions. + */ +void opendal_list_options_set_versions(struct opendal_list_options *opts, bool versions); + +/** + * \brief Set the deleted option. + * + * @param opts The opendal_list_options to modify. + * @param deleted Whether to include delete markers. + */ +void opendal_list_options_set_deleted(struct opendal_list_options *opts, bool deleted); + /** * \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 6b5b2f2ecbad..00ae40968e76 100644 --- a/bindings/c/src/operator.rs +++ b/bindings/c/src/operator.rs @@ -1032,7 +1032,8 @@ pub unsafe extern "C" fn opendal_operator_list_with( recursive: o.recursive, limit, start_after, - ..Default::default() + versions: o.versions, + deleted: o.deleted, } }; match op.deref().lister_options(path, list_opts) { diff --git a/bindings/c/src/operator_info.rs b/bindings/c/src/operator_info.rs index 48aa8ee707bb..4e0e79be8094 100644 --- a/bindings/c/src/operator_info.rs +++ b/bindings/c/src/operator_info.rs @@ -148,6 +148,10 @@ pub struct opendal_capability { pub list_with_start_after: bool, /// If backend supports list without delimiter. pub list_with_recursive: bool, + /// If backend supports list with versions. + pub list_with_versions: bool, + /// If backend supports list with deleted. + pub list_with_deleted: bool, /// If operator supports presign. pub presign: bool, @@ -299,6 +303,8 @@ impl From for opendal_capability { list_with_limit: value.list_with_limit, list_with_start_after: value.list_with_start_after, list_with_recursive: value.list_with_recursive, + list_with_versions: value.list_with_versions, + list_with_deleted: value.list_with_deleted, presign: value.presign, presign_read: value.presign_read, presign_stat: value.presign_stat, diff --git a/bindings/c/src/types.rs b/bindings/c/src/types.rs index 9114bf2dad33..80f508723e9e 100644 --- a/bindings/c/src/types.rs +++ b/bindings/c/src/types.rs @@ -98,6 +98,8 @@ impl opendal_bytes { /// @see opendal_list_options_set_recursive /// @see opendal_list_options_set_limit /// @see opendal_list_options_set_start_after +/// @see opendal_list_options_set_versions +/// @see opendal_list_options_set_deleted #[repr(C)] pub struct opendal_list_options { /// Whether to list recursively under the prefix; default false. @@ -106,6 +108,10 @@ pub struct opendal_list_options { pub limit: usize, /// Optional key to start listing from; NULL means unset. pub start_after: *mut c_char, + /// Include object versions when supported by version-aware backends; default false. + pub versions: bool, + /// Include delete markers when supported by version-aware backends; default false. + pub deleted: bool, } impl opendal_list_options { @@ -120,6 +126,8 @@ impl opendal_list_options { recursive: false, limit: 0, start_after: std::ptr::null_mut(), + versions: false, + deleted: false, })) } @@ -181,6 +189,34 @@ impl opendal_list_options { } } + /// \brief Set the versions option. + /// + /// @param opts The opendal_list_options to modify. + /// @param versions Whether to include object versions. + #[no_mangle] + pub unsafe extern "C" fn opendal_list_options_set_versions( + opts: *mut opendal_list_options, + versions: bool, + ) { + if !opts.is_null() { + (*opts).versions = versions; + } + } + + /// \brief Set the deleted option. + /// + /// @param opts The opendal_list_options to modify. + /// @param deleted Whether to include delete markers. + #[no_mangle] + pub unsafe extern "C" fn opendal_list_options_set_deleted( + opts: *mut opendal_list_options, + deleted: bool, + ) { + if !opts.is_null() { + (*opts).deleted = deleted; + } + } + /// \brief Free the heap memory used by opendal_list_options. /// /// @param opts The opendal_list_options to free. diff --git a/bindings/go/lister.go b/bindings/go/lister.go index bf54c665270b..8a3fd9bb2874 100644 --- a/bindings/go/lister.go +++ b/bindings/go/lister.go @@ -105,11 +105,33 @@ func ListWithStartAfter(startAfter string) WithListFn { } } +// ListWithVersions sets the versions flag for the list operation. +// +// When versions is true, the list operation will include all object versions. +// This option is only meaningful on version-aware backends. +func ListWithVersions(versions bool) WithListFn { + return func(o *listOptions) { + o.versions = versions + } +} + +// ListWithDeleted sets the deleted flag for the list operation. +// +// When deleted is true, the list operation will include delete markers. +// This option is only meaningful on version-aware backends. +func ListWithDeleted(deleted bool) WithListFn { + return func(o *listOptions) { + o.deleted = deleted + } +} + // listOptions holds the options for a list operation. type listOptions struct { recursive bool limit uint startAfter *string + versions bool + deleted bool } // List returns a Lister to iterate over entries that start with the given path. @@ -166,6 +188,8 @@ func (op *Operator) List(path string, opts ...WithListFn) (*Lister, error) { if o.startAfter != nil { ffiListOptionsSetStartAfter.symbol(op.ctx)(cOpts, *o.startAfter) } + ffiListOptionsSetVersions.symbol(op.ctx)(cOpts, o.versions) + ffiListOptionsSetDeleted.symbol(op.ctx)(cOpts, o.deleted) listerInner, err := ffiOperatorListWith.symbol(op.ctx)(op.inner, path, cOpts) if err != nil { return nil, err @@ -409,6 +433,42 @@ var ffiListOptionsSetStartAfter = newFFI(ffiOpts{ } }) +var ffiListOptionsSetVersions = newFFI(ffiOpts{ + sym: "opendal_list_options_set_versions", + rType: &ffi.TypeVoid, + aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypeUint8}, +}, func(_ context.Context, ffiCall ffiCall) func(opts *opendalListOptions, versions bool) { + return func(opts *opendalListOptions, versions bool) { + var v uint8 + if versions { + v = 1 + } + ffiCall( + nil, + unsafe.Pointer(&opts), + unsafe.Pointer(&v), + ) + } +}) + +var ffiListOptionsSetDeleted = newFFI(ffiOpts{ + sym: "opendal_list_options_set_deleted", + rType: &ffi.TypeVoid, + aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypeUint8}, +}, func(_ context.Context, ffiCall ffiCall) func(opts *opendalListOptions, deleted bool) { + return func(opts *opendalListOptions, deleted bool) { + var d uint8 + if deleted { + d = 1 + } + ffiCall( + nil, + unsafe.Pointer(&opts), + unsafe.Pointer(&d), + ) + } +}) + var ffiListOptionsFree = newFFI(ffiOpts{ sym: "opendal_list_options_free", rType: &ffi.TypeVoid, diff --git a/bindings/go/operator_info.go b/bindings/go/operator_info.go index fa27164a3850..c8d842ebcd61 100644 --- a/bindings/go/operator_info.go +++ b/bindings/go/operator_info.go @@ -271,6 +271,14 @@ func (c *Capability) ListWithRecursive() bool { return c.inner.listWithRecursive == 1 } +func (c *Capability) ListWithVersions() bool { + return c.inner.listWithVersions == 1 +} + +func (c *Capability) ListWithDeleted() bool { + return c.inner.listWithDeleted == 1 +} + func (c *Capability) Presign() bool { return c.inner.presign == 1 } diff --git a/bindings/go/string_ownership_test.go b/bindings/go/string_ownership_test.go index 2c40d19c5b66..b87e9898e721 100644 --- a/bindings/go/string_ownership_test.go +++ b/bindings/go/string_ownership_test.go @@ -831,6 +831,40 @@ func TestListWithStartAfterEmptyString(t *testing.T) { } } +func TestListWithVersionsTrue(t *testing.T) { + o := &listOptions{} + ListWithVersions(true)(o) + if !o.versions { + t.Fatalf("ListWithVersions(true): versions = false, want true") + } +} + +func TestListWithVersionsFalse(t *testing.T) { + o := &listOptions{} + ListWithVersions(true)(o) + ListWithVersions(false)(o) + if o.versions { + t.Fatalf("ListWithVersions(false): versions = true, want false") + } +} + +func TestListWithDeletedTrue(t *testing.T) { + o := &listOptions{} + ListWithDeleted(true)(o) + if !o.deleted { + t.Fatalf("ListWithDeleted(true): deleted = false, want true") + } +} + +func TestListWithDeletedFalse(t *testing.T) { + o := &listOptions{} + ListWithDeleted(true)(o) + ListWithDeleted(false)(o) + if o.deleted { + t.Fatalf("ListWithDeleted(false): deleted = true, want false") + } +} + 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 2950ba1c6ad5..c681ba85607c 100644 --- a/bindings/go/tests/behavior_tests/list_test.go +++ b/bindings/go/tests/behavior_tests/list_test.go @@ -54,6 +54,12 @@ func testsList(cap *opendal.Capability) []behaviorTest { if cap.ListWithStartAfter() { tests = append(tests, testListWithStartAfter) } + if isCapEnabled(cap.ListWithVersions, "list_with_versions") { + tests = append(tests, testListWithVersions) + } + if isCapEnabled(cap.ListWithDeleted, "list_with_deleted") { + tests = append(tests, testListWithDeleted) + } return tests } @@ -415,3 +421,75 @@ func testListWithStartAfter(assert *require.Assertions, op *opendal.Operator, fi assert.Contains(paths, p, "start_after must include entries after pivot") } } + +func testListWithVersions(assert *require.Assertions, op *opendal.Operator, fixture *fixture) { + parent := fixture.NewDirPath() + path, _, _ := fixture.NewFileWithPath(fmt.Sprintf("%s%s", parent, uuid.NewString())) + + assert.Nil(op.Write(path, []byte("version-1")), "first write must succeed") + assert.Nil(op.Write(path, []byte("version-2")), "second write must succeed") + + obs, err := op.List(path, opendal.ListWithVersions(true)) + assert.Nil(err, "list with versions must succeed") + defer obs.Close() + + var count int + var currentCount int + for obs.Next() { + entry := obs.Entry() + if entry.Path() == path { + count++ + meta := entry.Metadata() + version, ok := meta.Version() + assert.True(ok, "version metadata must be present for list with versions") + assert.NotEmpty(version, "each version entry must have a version ID") + if curr, ok := meta.IsCurrent(); ok && curr { + currentCount++ + } + } + } + assert.Nil(obs.Error()) + assert.GreaterOrEqual(count, 2, "list with versions must return at least 2 entries for the same path") + assert.Equal(1, currentCount, "exactly one version entry should be current") +} + +func testListWithDeleted(assert *require.Assertions, op *opendal.Operator, fixture *fixture) { + parent := fixture.NewDirPath() + path, content, _ := fixture.NewFileWithPath(fmt.Sprintf("%s%s", parent, uuid.NewString())) + + assert.Nil(op.Write(path, content), "write must succeed") + + obs, err := op.List(path, opendal.ListWithDeleted(true)) + assert.Nil(err, "list with deleted must succeed before deletion") + defer obs.Close() + var beforeCount int + for obs.Next() { + if obs.Entry().Path() == path { + beforeCount++ + } + } + assert.Nil(obs.Error()) + assert.Equal(1, beforeCount, "active file must appear exactly once before deletion") + + assert.Nil(op.Delete(path), "delete must succeed") + + obs2, err := op.List(path, opendal.ListWithDeleted(true)) + assert.Nil(err, "list with deleted must succeed after deletion") + defer obs2.Close() + var foundDeleteMarker bool + for obs2.Next() { + entry := obs2.Entry() + if entry.Path() == path { + meta := entry.Metadata() + if meta != nil && meta.IsDeleted() { + version, ok := meta.Version() + assert.True(ok, "delete marker must have a version ID") + assert.NotEmpty(version, "delete marker must have a version ID") + foundDeleteMarker = true + break + } + } + } + assert.Nil(obs2.Error()) + assert.True(foundDeleteMarker, "delete marker must be found after deletion") +} diff --git a/bindings/go/types.go b/bindings/go/types.go index f057dfa61204..b74908a66549 100644 --- a/bindings/go/types.go +++ b/bindings/go/types.go @@ -190,6 +190,8 @@ var ( &ffi.TypeUint8, // list_with_limit &ffi.TypeUint8, // list_with_start_after &ffi.TypeUint8, // list_with_recursive + &ffi.TypeUint8, // list_with_versions + &ffi.TypeUint8, // list_with_deleted &ffi.TypeUint8, // presign &ffi.TypeUint8, // presign_read &ffi.TypeUint8, // presign_stat @@ -245,6 +247,8 @@ type opendalCapability struct { listWithLimit uint8 listWithStartAfter uint8 listWithRecursive uint8 + listWithVersions uint8 + listWithDeleted uint8 presign uint8 presignRead uint8 presignStat uint8