You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/05/17 09:38:00 UTC
(opendal) branch main updated: feat(binding/golang): expose metadata on list operation (#7534)
This is an automated email from the ASF dual-hosted git repository.
Xuanwo 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 01b60ae15 feat(binding/golang): expose metadata on list operation (#7534)
01b60ae15 is described below
commit 01b60ae15557c8857c09559c3cc359bb83c6b2fa
Author: dentiny <de...@gmail.com>
AuthorDate: Sun May 17 02:37:54 2026 -0700
feat(binding/golang): expose metadata on list operation (#7534)
* [DRAFT] go list
* behavior test
---
bindings/go/README.md | 4 +-
bindings/go/lister.go | 75 +++++++++++++++++----------
bindings/go/string_ownership_test.go | 61 ++++++++++++++++++++++
bindings/go/tests/behavior_tests/list_test.go | 36 +++++++++++++
4 files changed, 146 insertions(+), 30 deletions(-)
diff --git a/bindings/go/README.md b/bindings/go/README.md
index 39e111339..ecdcb2f86 100644
--- a/bindings/go/README.md
+++ b/bindings/go/README.md
@@ -301,9 +301,9 @@ geomean 343.6 12.40 -96.39%
- [x] Writer -- implement as `io.WriteCloser`
- [x] Delete
- [x] CreateDir
-- [ ] Lister
+- [x] Lister
- [x] Entry
- - [ ] Metadata -- Need support from the C binding
+ - [x] Metadata
- [x] Copy
- [x] Rename
- [x] Presign
diff --git a/bindings/go/lister.go b/bindings/go/lister.go
index 420bb9eea..591ab8a1b 100644
--- a/bindings/go/lister.go
+++ b/bindings/go/lister.go
@@ -101,16 +101,12 @@ func (op *Operator) Check() (err error) {
// for lister.Next() {
// entry := lister.Entry()
//
-// meta, err := op.Stat(entry.Path())
-// if err != nil {
-// log.Printf("Error fetching metadata for %s: %v", entry.Path(), err)
-// continue
-// }
-//
// fmt.Printf("Name: %s\n", entry.Name())
-// fmt.Printf("Length: %d\n", meta.ContentLength())
-// fmt.Printf("Last Modified: %s\n", meta.LastModified())
-// fmt.Printf("Is Directory: %v, Is File: %v\n", meta.IsDir(), meta.IsFile())
+// if meta := entry.Metadata(); meta != nil {
+// fmt.Printf("Length: %d\n", meta.ContentLength())
+// fmt.Printf("Last Modified: %s\n", meta.LastModified())
+// fmt.Printf("Is Directory: %v, Is File: %v\n", meta.IsDir(), meta.IsFile())
+// }
// fmt.Println("---")
// }
// if err := lister.Err(); err != nil {
@@ -244,11 +240,6 @@ func (l *Lister) Entry() *Entry {
// An Entry provides basic information about a file or directory encountered
// during a list operation. It contains the path of the item and minimal metadata.
//
-// # Limitations
-//
-// The Entry itself does not contain comprehensive metadata. For detailed
-// metadata information, use the op.Stat() method with the Entry's path.
-//
// # Usage
//
// Entries are typically obtained through iteration of a Lister:
@@ -257,35 +248,42 @@ func (l *Lister) Entry() *Entry {
// entry := lister.Entry()
// // Process the entry
// fmt.Println(entry.Name())
+// if m := entry.Metadata(); m != nil {
+// fmt.Printf("Size: %d\n", m.ContentLength())
+// }
// }
//
-// # Fetching Detailed Metadata
+// # Metadata
//
-// To obtain comprehensive metadata for an Entry, use op.Stat():
-//
-// meta, err := op.Stat(entry.Path())
-// if err != nil {
-// log.Printf("Error fetching metadata: %v", err)
-// return
-// }
-// fmt.Printf("Size: %d, Last Modified: %s\n", meta.ContentLength(), meta.LastModified())
+// Use Metadata() to retrieve metadata populated during the list operation.
//
// # Methods
//
-// Entry provides methods to access basic information:
-// - Path(): Returns the full path of the entry.
+// Entry provides the following methods:
// - Name(): Returns the name of the entry (last component of the path).
+// - Path(): Returns the full path of the entry.
+// - Metadata(): Returns metadata collected during listing, if available.
type Entry struct {
name string
path string
+ meta *Metadata
}
func newEntry(ctx context.Context, inner *opendalEntry) *Entry {
- defer ffiEntryFree.symbol(ctx)(inner)
+ name := ffiEntryName.symbol(ctx)(inner)
+ path := ffiEntryPath.symbol(ctx)(inner)
+
+ var meta *Metadata
+ if m := ffiEntryMetadata.symbol(ctx)(inner); m != nil {
+ meta = newMetadata(ctx, m)
+ }
+
+ ffiEntryFree.symbol(ctx)(inner)
return &Entry{
- name: ffiEntryName.symbol(ctx)(inner),
- path: ffiEntryPath.symbol(ctx)(inner),
+ name: name,
+ path: path,
+ meta: meta,
}
}
@@ -299,6 +297,12 @@ func (e *Entry) Path() string {
return e.path
}
+// Metadata returns the metadata associated with this entry, as reported by the
+// underlying storage service during the list operation.
+func (e *Entry) Metadata() *Metadata {
+ return e.meta
+}
+
var ffiOperatorList = newFFI(ffiOpts{
sym: "opendal_operator_list",
rType: &typeResultList,
@@ -401,3 +405,18 @@ var ffiEntryPath = func() *FFI[func(e *opendalEntry) string] {
}
})
}()
+
+var ffiEntryMetadata = newFFI(ffiOpts{
+ sym: "opendal_entry_metadata",
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall ffiCall) func(e *opendalEntry) *opendalMetadata {
+ return func(e *opendalEntry) *opendalMetadata {
+ var meta *opendalMetadata
+ ffiCall(
+ unsafe.Pointer(&meta),
+ unsafe.Pointer(&e),
+ )
+ return meta
+ }
+})
diff --git a/bindings/go/string_ownership_test.go b/bindings/go/string_ownership_test.go
index bd2b63ab4..65fe7782d 100644
--- a/bindings/go/string_ownership_test.go
+++ b/bindings/go/string_ownership_test.go
@@ -148,6 +148,38 @@ func TestNewEntryCopiesAndFreesOwnedStrings(t *testing.T) {
*(**byte)(rValue) = pathPtr
}))
+ // Mock the new metadata path exercised by newEntry()
+ metaInner := &opendalMetadata{}
+ metaFreed := 0
+ ctx = context.WithValue(ctx, ffiEntryMetadata.opts.sym, func(e *opendalEntry) *opendalMetadata {
+ if e != entryInner {
+ t.Fatalf("ffiEntryMetadata called with unexpected entry")
+ }
+ return metaInner
+ })
+ ctx = context.WithValue(ctx, ffiMetadataFree.opts.sym, func(m *opendalMetadata) {
+ if m != metaInner {
+ t.Fatalf("metadata freed unexpected pointer: %p", m)
+ }
+ metaFreed++
+ })
+ ctx = context.WithValue(ctx, ffiMetaContentLength.opts.sym, func(m *opendalMetadata) uint64 {
+ assertMetadataPointer(t, metaInner, m)
+ return 4096
+ })
+ ctx = context.WithValue(ctx, ffiMetaIsFile.opts.sym, func(m *opendalMetadata) bool {
+ assertMetadataPointer(t, metaInner, m)
+ return true
+ })
+ ctx = context.WithValue(ctx, ffiMetaIsDir.opts.sym, func(m *opendalMetadata) bool {
+ assertMetadataPointer(t, metaInner, m)
+ return false
+ })
+ ctx = context.WithValue(ctx, ffiMetaLastModified.opts.sym, func(m *opendalMetadata) int64 {
+ assertMetadataPointer(t, metaInner, m)
+ return 1700000000000
+ })
+
entry := newEntry(ctx, entryInner)
if entry.Name() != "file.txt" {
t.Fatalf("newEntry().Name() = %q, want file.txt", entry.Name())
@@ -159,6 +191,27 @@ func TestNewEntryCopiesAndFreesOwnedStrings(t *testing.T) {
t.Fatalf("newEntry() freed entry %d times, want 1", entryFreed)
}
assertFreedPointers(t, freed, namePtr, pathPtr)
+
+ // Verify new Metadata() support
+ meta := entry.Metadata()
+ if meta == nil {
+ t.Fatal("newEntry().Metadata() returned nil, expected non-nil")
+ }
+ if meta.ContentLength() != 4096 {
+ t.Fatalf("Metadata().ContentLength() = %d, want 4096", meta.ContentLength())
+ }
+ if !meta.IsFile() {
+ t.Fatal("Metadata().IsFile() = false, want true")
+ }
+ if meta.IsDir() {
+ t.Fatal("Metadata().IsDir() = true, want false")
+ }
+ if meta.LastModified().UnixMilli() != 1700000000000 {
+ t.Fatalf("Metadata().LastModified().UnixMilli() = %d, want 1700000000000", meta.LastModified().UnixMilli())
+ }
+ if metaFreed != 1 {
+ t.Fatalf("newEntry() freed metadata %d times, want 1", metaFreed)
+ }
}
func mustBytePtrFromString(t *testing.T, value string) *byte {
@@ -195,6 +248,14 @@ func assertEntryPointer(t *testing.T, want *opendalEntry, aValues ...unsafe.Poin
}
}
+func assertMetadataPointer(t *testing.T, want *opendalMetadata, got *opendalMetadata) {
+ t.Helper()
+
+ if got != want {
+ t.Fatalf("metadata getter received %p, want %p", got, want)
+ }
+}
+
func assertFreedPointers(t *testing.T, got []*byte, want ...*byte) {
t.Helper()
diff --git a/bindings/go/tests/behavior_tests/list_test.go b/bindings/go/tests/behavior_tests/list_test.go
index 427a731de..29f0436ec 100644
--- a/bindings/go/tests/behavior_tests/list_test.go
+++ b/bindings/go/tests/behavior_tests/list_test.go
@@ -42,6 +42,7 @@ func testsList(cap *opendal.Capability) []behaviorTest {
testListSubDir,
testListNestedDir,
testListDirWithFilePath,
+ testListEntryMetadata,
}
}
@@ -237,6 +238,41 @@ func testListNestedDir(assert *require.Assertions, op *opendal.Operator, fixture
assert.True(meta.IsDir())
}
+func testListEntryMetadata(assert *require.Assertions, op *opendal.Operator, fixture *fixture) {
+ parent := fixture.NewDirPath()
+ assert.Nil(op.CreateDir(parent))
+
+ filePath, content, size := fixture.NewFileWithPath(fmt.Sprintf("%s%s", parent, uuid.NewString()))
+ assert.Nil(op.Write(filePath, content))
+
+ obs, err := op.List(parent)
+ assert.Nil(err)
+ defer obs.Close()
+
+ var sawFile, sawDir bool
+ for obs.Next() {
+ entry := obs.Entry()
+ meta := entry.Metadata()
+ assert.NotNil(meta, "list entry metadata must always be populated for path %s", entry.Path())
+
+ switch entry.Path() {
+ case filePath:
+ sawFile = true
+ assert.True(meta.IsFile(), "expected file for %s", entry.Path())
+ assert.False(meta.IsDir(), "expected file for %s", entry.Path())
+ assert.Equal(uint64(size), meta.ContentLength(),
+ "content length from list metadata must match written file size")
+ case parent:
+ sawDir = true
+ assert.True(meta.IsDir(), "expected dir for %s", entry.Path())
+ assert.False(meta.IsFile(), "expected dir for %s", entry.Path())
+ }
+ }
+ assert.Nil(obs.Error())
+ assert.True(sawFile, "file entry %s must be returned by list", filePath)
+ assert.True(sawDir, "parent dir entry %s must be returned by list", parent)
+}
+
func testListDirWithFilePath(assert *require.Assertions, op *opendal.Operator, fixture *fixture) {
parent := fixture.NewDirPath()
path, content, _ := fixture.NewFileWithPath(fmt.Sprintf("%s%s", parent, uuid.NewString()))