fix(mount): report correct nlink for directories (#9023)

fix(mount): report correct nlink for directories (2 + subdirectory count)

POSIX requires directory nlink = 2 (for . and ..) + number of
subdirectories. Previously SeaweedFS reported nlink=1 for all dirs.

- Set nlink baseline to 2 for directories in setAttrByPbEntry,
  setAttrByFilerEntry, and setRootAttr
- Add applyDirNlink() that counts subdirectories from the local
  metacache and sets nlink = 2 + count
- Call it from GetAttr and Lookup for directory entries

When the metacache has no entries (before readdir), nlink=2 is used
as a safe POSIX-compliant default.
This commit is contained in:
Chris Lu authored and GitHub committed 2026-04-10 14:05:27 -07:00
1 parent 2a7ec8d033
commit 39e76b8e94
2 files changed
+32 -4

No files matched your search

+28 -4
View File
@@ -17,15 +17,19 @@ func (wfs *WFS) GetAttr(cancel <-chan struct{}, input *fuse.GetAttrIn, out *fuse
glog.V(4).Infof("GetAttr %v", input.NodeId)
if input.NodeId == 1 {
wfs.setRootAttr(out)
wfs.applyDirNlink(&out.Attr, util.FullPath(wfs.option.FilerMountRootPath))
return fuse.OK
}
inode := input.NodeId
_, _, entry, status := wfs.maybeReadEntry(inode)
path, _, entry, status := wfs.maybeReadEntry(inode)
if status == fuse.OK {
out.AttrValid = 1
wfs.setAttrByPbEntry(&out.Attr, inode, entry, true)
wfs.applyInMemoryAtime(&out.Attr, inode)
if entry.IsDirectory {
wfs.applyDirNlink(&out.Attr, path)
}
return status
} else {
if fh, found := wfs.fhMap.FindFileHandle(inode); found {
@@ -177,7 +181,7 @@ func (wfs *WFS) setRootAttr(out *fuse.AttrOut) {
out.Ctime = now
out.Atime = now
out.Mode = toSyscallType(os.ModeDir) | uint32(wfs.option.MountMode)
out.Nlink = 1
out.Nlink = 2
}
func (wfs *WFS) setAttrByPbEntry(out *fuse.Attr, inode uint64, entry *filer_pb.Entry, calculateSize bool) {
@@ -209,7 +213,9 @@ func (wfs *WFS) setAttrByPbEntry(out *fuse.Attr, inode uint64, entry *filer_pb.E
out.Atimensec = uint32(entry.Attributes.MtimeNs)
// In-memory atime overlay is applied by the caller via applyInMemoryAtime.
out.Mode = toSyscallMode(os.FileMode(entry.Attributes.FileMode))
if entry.HardLinkCounter > 0 {
if entry.IsDirectory {
out.Nlink = 2
} else if entry.HardLinkCounter > 0 {
out.Nlink = uint32(entry.HardLinkCounter)
} else {
out.Nlink = 1
@@ -239,7 +245,9 @@ func (wfs *WFS) setAttrByFilerEntry(out *fuse.Attr, inode uint64, entry *filer.E
out.Ctimensec = uint32(entry.Attr.Mtime.Nanosecond())
}
out.Mode = toSyscallMode(entry.Attr.Mode)
if entry.HardLinkCounter > 0 {
if entry.IsDirectory() {
out.Nlink = 2
} else if entry.HardLinkCounter > 0 {
out.Nlink = uint32(entry.HardLinkCounter)
} else {
out.Nlink = 1
@@ -318,6 +326,22 @@ func (wfs *WFS) applyInMemoryAtime(out *fuse.Attr, inode uint64) {
wfs.atimeMu.Unlock()
}
// applyDirNlink sets nlink = 2 + number_of_subdirectories for a directory.
// Only counts from the local metacache to avoid expensive filer queries.
// When the cache has no entries (e.g. before readdir), keeps nlink=2.
func (wfs *WFS) applyDirNlink(out *fuse.Attr, dirPath util.FullPath) {
var subdirCount uint32
wfs.metaCache.ListDirectoryEntries(context.Background(), dirPath, "", false, 100000, func(entry *filer.Entry) (bool, error) {
if entry.IsDirectory() {
subdirCount++
}
return true, nil
})
if subdirCount > 0 {
out.Nlink = 2 + subdirCount
}
}
func chmod(existing uint32, mode uint32) uint32 {
return existing&^07777 | mode&07777
}
+4
View File
@@ -44,6 +44,10 @@ func (wfs *WFS) Lookup(cancel <-chan struct{}, header *fuse.InHeader, name strin
wfs.outputFilerEntry(out, inode, localEntry)
if localEntry.IsDirectory() {
wfs.applyDirNlink(&out.Attr, fullFilePath)
}
return fuse.OK
}