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()))