Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions bindings/c/include/opendal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*
Expand Down
3 changes: 2 additions & 1 deletion bindings/c/src/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions bindings/c/src/operator_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -299,6 +303,8 @@ impl From<core::Capability> 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,
Expand Down
36 changes: 36 additions & 0 deletions bindings/c/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand All @@ -120,6 +126,8 @@ impl opendal_list_options {
recursive: false,
limit: 0,
start_after: std::ptr::null_mut(),
versions: false,
deleted: false,
}))
}

Expand Down Expand Up @@ -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.
Expand Down
60 changes: 60 additions & 0 deletions bindings/go/lister.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions bindings/go/operator_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
34 changes: 34 additions & 0 deletions bindings/go/string_ownership_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
78 changes: 78 additions & 0 deletions bindings/go/tests/behavior_tests/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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")
}
Loading