This is an automated email from the ASF dual-hosted git repository.

zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new a5c6bc9d2 fix(io): skip subdirectory markers in blobfs WalkDir (#2187)
a5c6bc9d2 is described below

commit a5c6bc9d26bbaaaea25af025e4012ebd4d958d87
Author: Revanth <[email protected]>
AuthorDate: Mon Oct 19 10:21:40 2026 -0500

    fix(io): skip subdirectory markers in blobfs WalkDir (#2187)
---
 io/gocloud/blobfs/blob.go      | 39 ++++++++++----------------------------
 io/gocloud/blobfs/blob_test.go | 43 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 53 insertions(+), 29 deletions(-)

diff --git a/io/gocloud/blobfs/blob.go b/io/gocloud/blobfs/blob.go
index 5988870aa..19a6474b4 100644
--- a/io/gocloud/blobfs/blob.go
+++ b/io/gocloud/blobfs/blob.go
@@ -409,26 +409,6 @@ func walkedURIPath(location ObjectLocation, walked string) 
string {
        return location.uriPrefix + walked
 }
 
-// isDirectoryMarker reports whether dirEntry is the marker object for the
-// path passed to WalkDir
-func isDirectoryMarker(walkRootKey string, dirEntry fs.DirEntry) bool {
-       // Directory entries and nil entries are never the marker object itself.
-       if dirEntry == nil || dirEntry.IsDir() {
-               return false
-       }
-
-       info, err := dirEntry.Info()
-       if err != nil {
-               return false
-       }
-
-       // blob fs is the only implementation that uses directory markers
-       // so we can use a type assertion to check
-       obj, ok := info.Sys().(*blob.ListObject)
-
-       return ok && obj.Key == directoryMarker(walkRootKey)
-}
-
 // walkFS walks a bucket for fs.WalkDir, treating any key with children as a 
directory,
 // so the 0-byte directory blobs of ADLS hierarchical namespaces (OneLake) 
aren't files.
 type walkFS struct{ bfs *FileIO }
@@ -484,6 +464,7 @@ func (w walkFS) ReadDir(name string) ([]fs.DirEntry, error) 
{
 
        var files []*blob.ListObject
        dirs := map[string]bool{}
+       hasMarker := false
        iter := w.bfs.List(&blob.ListOptions{Prefix: prefix, Delimiter: "/"})
        for {
                obj, err := iter.Next(w.bfs.ctx)
@@ -494,6 +475,13 @@ func (w walkFS) ReadDir(name string) ([]fs.DirEntry, 
error) {
                        return nil, blobErrToFsErr("readdir", name, err)
                }
 
+               if obj.Key == prefix {
+                       // The "name/" marker of the directory being read, not 
a child of it.
+                       hasMarker = true
+
+                       continue
+               }
+
                if obj.IsDir {
                        dirs[strings.TrimSuffix(strings.TrimPrefix(obj.Key, 
prefix), "/")] = true
                } else {
@@ -513,8 +501,6 @@ func (w walkFS) ReadDir(name string) ([]fs.DirEntry, error) 
{
                        continue
                }
 
-               // Name entries the way gocloud's fs.FS does, so a "dir/" 
marker keeps
-               // the name it had before walkFS.
                entries = append(entries, walkFileEntry{blobFileInfo{
                        name:    pathpkg.Base(obj.Key),
                        size:    obj.Size,
@@ -524,7 +510,8 @@ func (w walkFS) ReadDir(name string) ([]fs.DirEntry, error) 
{
                }})
        }
 
-       if len(entries) == 0 && name != "." {
+       // A directory holding only its marker is empty, not missing.
+       if len(entries) == 0 && name != "." && !hasMarker {
                return nil, &fs.PathError{Op: "readdir", Path: name, Err: 
fs.ErrNotExist}
        }
 
@@ -567,12 +554,6 @@ func (bfs *FileIO) WalkDir(root string, fn fs.WalkDirFunc) 
error {
        }
 
        return fs.WalkDir(walkFS{bfs}, walkPath, func(path string, d 
fs.DirEntry, err error) error {
-               // Skip the marker for the path passed to WalkDir. Otherwise 
walking
-               // "warehouse/ns" reports "warehouse/ns/" as a child file.
-               if err == nil && isDirectoryMarker(walkPath, d) {
-                       return nil
-               }
-
                if location.hasAuthority {
                        path = walkedURIPath(location, path)
                }
diff --git a/io/gocloud/blobfs/blob_test.go b/io/gocloud/blobfs/blob_test.go
index 125a2f042..272833207 100644
--- a/io/gocloud/blobfs/blob_test.go
+++ b/io/gocloud/blobfs/blob_test.go
@@ -893,6 +893,49 @@ func TestBlobFileIOWalkDirSkipsDirectoryMarker(t 
*testing.T) {
        assert.Equal(t, []string{"s3://test-bucket/warehouse/ns"}, paths)
 }
 
+func TestBlobFileIOWalkDirSkipsSubdirectoryMarkers(t *testing.T) {
+       ctx := context.Background()
+
+       bucket := memblob.OpenBucket(nil)
+       defer bucket.Close()
+
+       // "tbl/empty/" is a marker with no children, so it is an empty 
directory.
+       for _, k := range []string{"tbl/", "tbl/data/", "tbl/empty/"} {
+               require.NoError(t, bucket.WriteAll(ctx, k, nil, nil))
+       }
+       require.NoError(t, bucket.WriteAll(ctx, "tbl/data/a.parquet", 
[]byte("a"), nil))
+       require.NoError(t, bucket.WriteAll(ctx, 
"tbl/metadata/v1.metadata.json", []byte("b"), nil))
+
+       bfs := testBlobFileIO(ctx, "mybucket", bucket)
+
+       var dirs, files []string
+       err := bfs.WalkDir("s3://mybucket/tbl", func(path string, d 
fs.DirEntry, err error) error {
+               if err != nil {
+                       return err
+               }
+
+               if d.IsDir() {
+                       dirs = append(dirs, path)
+               } else {
+                       files = append(files, path)
+               }
+
+               return nil
+       })
+       require.NoError(t, err)
+
+       assert.Equal(t, []string{
+               "s3://mybucket/tbl",
+               "s3://mybucket/tbl/data",
+               "s3://mybucket/tbl/empty",
+               "s3://mybucket/tbl/metadata",
+       }, dirs)
+       assert.Equal(t, []string{
+               "s3://mybucket/tbl/data/a.parquet",
+               "s3://mybucket/tbl/metadata/v1.metadata.json",
+       }, files)
+}
+
 func TestBlobFileIORemoveAll(t *testing.T) {
        ctx := context.Background()
        bucket := memblob.OpenBucket(nil)

Reply via email to