diff --git a/test/pjdfstest/known_failures.txt b/test/pjdfstest/known_failures.txt index 21f3a8d77..1476890b3 100644 --- a/test/pjdfstest/known_failures.txt +++ b/test/pjdfstest/known_failures.txt @@ -6,11 +6,6 @@ # A failure in any test NOT listed here will cause the CI job to fail, # catching regressions immediately. -# ── Directory nlink count before readdir ──────────────────────────────── -# Directory nlink = 2 + subdirectory count is only accurate after the -# directory children have been cached (readdir). Before that, nlink=2. -tests/rename/24.t - # ── Directory rename permission edge case ────────────────────────────── # Cross-directory rename of a subdirectory with restricted permissions # causes cascading test failures within the test file. diff --git a/weed/mount/inode_to_path.go b/weed/mount/inode_to_path.go index 053d66484..26b4b353e 100644 --- a/weed/mount/inode_to_path.go +++ b/weed/mount/inode_to_path.go @@ -27,6 +27,7 @@ type InodeEntry struct { lastRefresh time.Time updateWindowStart time.Time updateCount int + subdirCount int32 // tracked in-memory for POSIX directory nlink } func (ie *InodeEntry) resetCacheState() { @@ -250,6 +251,55 @@ func (i *InodeToPath) InvalidateChildrenCache(fullpath util.FullPath) { entry.resetCacheState() } +// AdjustSubdirCount adjusts the subdirectory count for a directory inode. +// delta is typically +1 (mkdir) or -1 (rmdir). +func (i *InodeToPath) AdjustSubdirCount(dirPath util.FullPath, delta int32) { + i.Lock() + defer i.Unlock() + inode, found := i.path2inode[dirPath] + if !found { + return + } + entry, found := i.inode2path[inode] + if !found || !entry.isDirectory { + return + } + entry.subdirCount += delta + if entry.subdirCount < 0 { + entry.subdirCount = 0 + } +} + +// GetSubdirCount returns the tracked subdirectory count for a directory. +func (i *InodeToPath) GetSubdirCount(dirPath util.FullPath) int32 { + i.RLock() + defer i.RUnlock() + inode, found := i.path2inode[dirPath] + if !found { + return 0 + } + entry, found := i.inode2path[inode] + if !found || !entry.isDirectory { + return 0 + } + return entry.subdirCount +} + +// SetSubdirCount sets the subdirectory count for a directory (used after readdir). +func (i *InodeToPath) SetSubdirCount(dirPath util.FullPath, count int32) { + i.Lock() + defer i.Unlock() + inode, found := i.path2inode[dirPath] + if !found { + return + } + entry, found := i.inode2path[inode] + if !found || !entry.isDirectory { + return + } + entry.subdirCount = count +} + func (i *InodeToPath) TouchDirectory(fullpath util.FullPath) { i.Lock() defer i.Unlock() diff --git a/weed/mount/weedfs_attr.go b/weed/mount/weedfs_attr.go index 95e39f836..a509673fb 100644 --- a/weed/mount/weedfs_attr.go +++ b/weed/mount/weedfs_attr.go @@ -329,18 +329,11 @@ func (wfs *WFS) applyInMemoryAtime(out *fuse.Attr, inode uint64) { } // 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. +// Uses the in-memory subdirectory count tracked by mkdir/rmdir/rename. 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 + count := wfs.inodeToPath.GetSubdirCount(dirPath) + if count > 0 { + out.Nlink = 2 + uint32(count) } } diff --git a/weed/mount/weedfs_dir_mkrm.go b/weed/mount/weedfs_dir_mkrm.go index b7bf4f747..036bd31a2 100644 --- a/weed/mount/weedfs_dir_mkrm.go +++ b/weed/mount/weedfs_dir_mkrm.go @@ -78,6 +78,7 @@ func (wfs *WFS) Mkdir(cancel <-chan struct{}, in *fuse.MkdirIn, name string, out } wfs.inodeToPath.TouchDirectory(dirFullPath) wfs.touchDirMtimeCtime(dirFullPath) + wfs.inodeToPath.AdjustSubdirCount(dirFullPath, 1) } glog.V(3).Infof("mkdir %s: %v", entryFullPath, err) @@ -155,6 +156,7 @@ func (wfs *WFS) Rmdir(cancel <-chan struct{}, header *fuse.InHeader, name string wfs.inodeToPath.RemovePath(entryFullPath) wfs.inodeToPath.TouchDirectory(dirFullPath) wfs.touchDirMtimeCtime(dirFullPath) + wfs.inodeToPath.AdjustSubdirCount(dirFullPath, -1) return fuse.OK diff --git a/weed/mount/weedfs_rename.go b/weed/mount/weedfs_rename.go index 88c02215c..f72723a85 100644 --- a/weed/mount/weedfs_rename.go +++ b/weed/mount/weedfs_rename.go @@ -326,6 +326,11 @@ func (wfs *WFS) Rename(cancel <-chan struct{}, in *fuse.RenameIn, oldName string wfs.touchDirMtimeCtime(oldDir) if oldDir != newDir { wfs.touchDirMtimeCtime(newDir) + // Adjust subdirectory counts when moving a directory across parents. + if oldEntry != nil && oldEntry.IsDirectory { + wfs.inodeToPath.AdjustSubdirCount(oldDir, -1) + wfs.inodeToPath.AdjustSubdirCount(newDir, 1) + } } return fuse.OK