mirror of
https://github.com/rclone/rclone.git
synced 2026-06-10 09:24:33 -04:00
mount2: fix empty directory listings on re-read
With cmd/mount2, reading a directory more than once returned the correct entries on the first read but nothing on subsequent reads. Plain `ls` triggers this: it does lseek(fd, 0, SEEK_SET) to rewind the directory before a second getdents. go-fuse v2.9.0 rewinds a directory stream by calling Seekdir on the FileSeekdirer interface. dirStream did not implement it, so go-fuse returned ENOTSUP and produced an empty listing on every read after the first. This implements Seekdir on dirStream: a rewind to offset 0 resets the stream to the start, restoring correct listings on re-read. Non-zero offsets are uncommon for in-memory listings and still return ENOTSUP, matching go-fuse's own default. A compile-time interface assertion is added so signature drift on future go-fuse updates is caught at build time. Before: second and subsequent reads of a directory returned no entries. After: directories list correctly on every read. See: https://github.com/hanwen/go-fuse/issues/549 Co-authored-by: Nick Craig-Wood <nick@craig-wood.com>
This commit is contained in:
committed by
GitHub
parent
2dbad62a11
commit
00bd00d83d
@@ -271,7 +271,20 @@ func (ds *dirStream) Next() (de fuse.DirEntry, errno syscall.Errno) {
|
||||
func (ds *dirStream) Close() {
|
||||
}
|
||||
|
||||
// Seekdir implements fusefs.FileSeekdirer so go-fuse can rewind the directory
|
||||
// stream when the kernel calls lseek(fd, 0, SEEK_SET) before a second getdents.
|
||||
// Without this, go-fuse returns ENOTSUP and ls returns empty on every call after
|
||||
// the first. See: https://github.com/hanwen/go-fuse/issues/549
|
||||
func (ds *dirStream) Seekdir(_ context.Context, off uint64) syscall.Errno {
|
||||
if off == 0 {
|
||||
ds.i = 0
|
||||
return 0
|
||||
}
|
||||
return syscall.ENOTSUP
|
||||
}
|
||||
|
||||
var _ fusefs.DirStream = (*dirStream)(nil)
|
||||
var _ fusefs.FileSeekdirer = (*dirStream)(nil)
|
||||
|
||||
// Readdir opens a stream of directory entries.
|
||||
//
|
||||
|
||||
13
vfs/vfstest/dir_non_unix.go
Normal file
13
vfs/vfstest/dir_non_unix.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build !linux && !darwin && !freebsd
|
||||
|
||||
package vfstest
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestDirRewind checks that re-reading a rewound directory works
|
||||
func TestDirRewind(t *testing.T) {
|
||||
t.Skip("not supported on " + runtime.GOOS)
|
||||
}
|
||||
75
vfs/vfstest/dir_unix.go
Normal file
75
vfs/vfstest/dir_unix.go
Normal file
@@ -0,0 +1,75 @@
|
||||
//go:build linux || darwin || freebsd
|
||||
|
||||
package vfstest
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// countDirFd reads the directory referred to by fd to end-of-stream using
|
||||
// raw getdents syscalls and returns the number of entries (excluding "." and
|
||||
// ".." which ParseDirent skips).
|
||||
//
|
||||
// This deliberately uses syscall.ReadDirent rather than (*os.File).Readdir so
|
||||
// that it exercises the kernel readdir path directly on a single file
|
||||
// descriptor, the same way an in-process rewinddir does.
|
||||
func countDirFd(t *testing.T, fd int) int {
|
||||
buf := make([]byte, 8192)
|
||||
var names []string
|
||||
for {
|
||||
n, err := syscall.ReadDirent(fd, buf)
|
||||
require.NoError(t, err)
|
||||
if n <= 0 {
|
||||
break
|
||||
}
|
||||
_, _, names = syscall.ParseDirent(buf[:n], -1, names)
|
||||
}
|
||||
return len(names)
|
||||
}
|
||||
|
||||
// TestDirRewind checks that re-reading a directory after rewinding the
|
||||
// directory stream (lseek(fd, 0, SEEK_SET), as rewinddir(3) does) returns the
|
||||
// same entries as the first read.
|
||||
//
|
||||
// This reproduces the bug where the mount2 backend returned an empty listing
|
||||
// on every read after the first because go-fuse v2.9.0 rewinds a directory by
|
||||
// calling Seekdir, which dirStream did not implement. See PR #9469 and
|
||||
// https://github.com/hanwen/go-fuse/issues/549
|
||||
func TestDirRewind(t *testing.T) {
|
||||
run.skipIfVFS(t)
|
||||
run.skipIfNoFUSE(t)
|
||||
|
||||
run.mkdir(t, "dir")
|
||||
run.createFile(t, "dir/f1", "1")
|
||||
run.createFile(t, "dir/f2", "2")
|
||||
run.createFile(t, "dir/f3", "3")
|
||||
run.checkDir(t, "dir/|dir/f1 1|dir/f2 1|dir/f3 1")
|
||||
|
||||
fh, err := run.os.Open(run.path("dir"))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = fh.Close()
|
||||
}()
|
||||
fd := int(fh.Fd())
|
||||
|
||||
first := countDirFd(t, fd)
|
||||
assert.Equal(t, 3, first, "first read should see all entries")
|
||||
|
||||
// rewinddir == lseek(fd, 0, SEEK_SET)
|
||||
_, err = syscall.Seek(fd, 0, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
second := countDirFd(t, fd)
|
||||
assert.Equal(t, first, second, "re-read after rewind should match first read")
|
||||
|
||||
require.NoError(t, fh.Close())
|
||||
run.rm(t, "dir/f1")
|
||||
run.rm(t, "dir/f2")
|
||||
run.rm(t, "dir/f3")
|
||||
run.rmdir(t, "dir")
|
||||
run.checkDir(t, "")
|
||||
}
|
||||
@@ -86,6 +86,7 @@ func RunTests(t *testing.T, useVFS bool, minimumRequiredCacheMode vfscommon.Cach
|
||||
t.Run("TestDirRenameEmptyDir", TestDirRenameEmptyDir)
|
||||
t.Run("TestDirRenameFullDir", TestDirRenameFullDir)
|
||||
t.Run("TestDirModTime", TestDirModTime)
|
||||
t.Run("TestDirRewind", TestDirRewind)
|
||||
if enableCacheTests {
|
||||
t.Run("TestDirCacheFlush", TestDirCacheFlush)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user