Skip to content

Commit

Permalink
fs: new directory API
Browse files Browse the repository at this point in the history
The new API follows the pattern of the other file API:

InodeEmbedder can implement a OpendirHandle method, which returns a
FileHandle. Directories can implement the following APIs,

 * FileSyncdirer
 * FileReaddirenter
 * FileReleasedirer
 * FileSeekdirer

along with the opened directory, FOPEN_* flags may be returned. In a
follow-up change, we'll demonstrate how to use FOPEN_CACHE_DIR for
directory caching.

This change is backward compatible in a compilation sense, but support
for seeking in DirStreams has been removed: it was incorrect (it made
the assumption that the dir offset was monotonically increasing),
inefficient and complicated.

Change-Id: I662713321f0f812e92e4059ee11e8b1427c6aa0f
  • Loading branch information
hanwen committed Sep 23, 2024
1 parent 531a685 commit e885cea
Show file tree
Hide file tree
Showing 6 changed files with 366 additions and 104 deletions.
30 changes: 30 additions & 0 deletions fs/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,36 @@ type FileAllocater interface {
Allocate(ctx context.Context, off uint64, size uint64, mode uint32) syscall.Errno
}

// Opens a directory. This supersedes NodeOpendirer, allowing to pass
// back flags (eg. FOPEN_CACHE_DIR).
type NodeOpendirHandler interface {
OpendirHandle(ctx context.Context, flags uint32) (fh FileHandle, fuseFlags uint32, errno syscall.Errno)
}

// FileReaddirenter is a directory that supports reading.
type FileReaddirenter interface {
// Read a single directory entry.
Readdirent(ctx context.Context) (*fuse.DirEntry, syscall.Errno)
}

// FileFsyncer is a directory that supports fsyncdir.
type FileFsyncdirer interface {
Fsyncdir(ctx context.Context, flags uint32) syscall.Errno
}

// FileSeekdirer is directory that supports seeking. `off` is an
// opaque uint64 value, where only the value 0 is reserved for the
// start of the stream. (See https://lwn.net/Articles/544520/ for
// background).
type FileSeekdirer interface {
Seekdir(ctx context.Context, off uint64) syscall.Errno
}

// FileReleasedirer is a directory that supports a cleanup operation.
type FileReleasedirer interface {
Releasedir(ctx context.Context, releaseFlags uint32)
}

// Options sets options for the entire filesystem
type Options struct {
// MountOptions contain the options for mounting the fuse server
Expand Down
191 changes: 95 additions & 96 deletions fs/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ type fileEntry struct {
mu sync.Mutex

// Directory
dirStream DirStream
hasOverflow bool
overflow fuse.DirEntry
overflowErrno syscall.Errno
Expand Down Expand Up @@ -821,7 +820,8 @@ func (b *rawBridge) releaseBackingIDRef(n *Inode) {
}
}

// registerFile hands out a file handle. Must have bridge.mu
// registerFile hands out a file handle. Must have bridge.mu. Flags are the open flags
// (eg. syscall.O_EXCL).
func (b *rawBridge) registerFile(n *Inode, f FileHandle, flags uint32) *fileEntry {
fe := &fileEntry{}
if len(b.freeFiles) > 0 {
Expand Down Expand Up @@ -919,12 +919,9 @@ func (b *rawBridge) ReleaseDir(input *fuse.ReleaseIn) {
n, f := b.releaseFileEntry(input.NodeId, input.Fh)
f.wg.Wait()

f.mu.Lock()
if f.dirStream != nil {
f.dirStream.Close()
f.dirStream = nil
if frd, ok := f.file.(FileReleasedirer); ok {
frd.Releasedir(context.Background(), input.ReleaseFlags)
}
f.mu.Unlock()

b.mu.Lock()
defer b.mu.Unlock()
Expand Down Expand Up @@ -1006,79 +1003,59 @@ func (b *rawBridge) Fallocate(cancel <-chan struct{}, input *fuse.FallocateIn) f
func (b *rawBridge) OpenDir(cancel <-chan struct{}, input *fuse.OpenIn, out *fuse.OpenOut) fuse.Status {
n, _ := b.inode(input.NodeId, 0)

if od, ok := n.ops.(NodeOpendirer); ok {
errno := od.Opendir(&fuse.Context{Caller: input.Caller, Cancel: cancel})
if errno != 0 {
return errnoToStatus(errno)
}
}
var fh FileHandle
var fuseFlags uint32
var errno syscall.Errno

b.mu.Lock()
defer b.mu.Unlock()
fe := b.registerFile(n, nil, 0)
out.Fh = uint64(fe.fh)
return fuse.OK
}
ctx := &fuse.Context{Caller: input.Caller, Cancel: cancel}

// setStream makes sure `f.dirStream` and associated state variables are set and
// seeks to offset requested in `input`. Caller must hold `f.mu`.
// The `eof` return value shows if `f.dirStream` ended before the requested
// offset was reached.
func (b *rawBridge) setStream(cancel <-chan struct{}, input *fuse.ReadIn, inode *Inode, f *fileEntry) (errno syscall.Errno, eof bool) {
// Get a new directory stream in the following cases:
// 1) f.dirStream == nil ............ First READDIR[PLUS] on this file handle.
// 2) input.Offset == 0 ............. Start reading the directory again from
// the beginning (user called rewinddir(3) or lseek(2)).
// 3) input.Offset < f.nextOffset ... Seek back (user called seekdir(3) or lseek(2)).
if f.dirStream == nil || input.Offset == 0 || input.Offset < f.dirOffset {
if f.dirStream != nil {
f.dirStream.Close()
f.dirStream = nil
}
str, errno := b.getStream(&fuse.Context{Caller: input.Caller, Cancel: cancel}, inode)
if errno != 0 {
return errno, false
}
nod, _ := n.ops.(NodeOpendirer)
nrd, _ := n.ops.(NodeReaddirer)

f.dirOffset = 0
f.hasOverflow = false
f.dirStream = str
}
if odh, ok := n.ops.(NodeOpendirHandler); ok {
fh, fuseFlags, errno = odh.OpendirHandle(ctx, input.Flags)

// Seek forward?
for f.dirOffset < input.Offset {
f.hasOverflow = false
if !f.dirStream.HasNext() {
// Seek past end of directory. This is not an error, but the
// user will get an empty directory listing.
return 0, true
}
de, errno := f.dirStream.Next()
if errno != 0 {
return errno, true
return errnoToStatus(errno)
}
} else {
if nod != nil {
errno = nod.Opendir(ctx)
if errno != 0 {
return errnoToStatus(errno)
}
}
if de.Off == 0 {
de.Off = f.dirOffset + 1

var ctor func(context.Context) (DirStream, syscall.Errno)
if nrd != nil {
ctor = func(ctx context.Context) (DirStream, syscall.Errno) {
return nrd.Readdir(ctx)
}
} else {
ctor = func(ctx context.Context) (DirStream, syscall.Errno) {
return n.childrenAsDirstream(), 0
}
}
f.dirOffset = de.Off
fh = &dirStreamAsFile{creator: ctor}
}

return 0, false
b.mu.Lock()
defer b.mu.Unlock()
fe := b.registerFile(n, fh, 0)
out.Fh = uint64(fe.fh)
out.OpenFlags = fuseFlags
return fuse.OK
}

func (b *rawBridge) getStream(ctx context.Context, inode *Inode) (DirStream, syscall.Errno) {
if rd, ok := inode.ops.(NodeReaddirer); ok {
return rd.Readdir(ctx)
}

lst := inode.childrenList()
func (n *Inode) childrenAsDirstream() DirStream {
lst := n.childrenList()
r := make([]fuse.DirEntry, 0, len(lst))
for _, e := range lst {
r = append(r, fuse.DirEntry{Mode: e.Inode.Mode(),
Name: e.Name,
Ino: e.Inode.StableAttr().Ino})
}
return NewListDirStream(r), 0
return NewListDirStream(r)
}

func (b *rawBridge) ReadDirPlus(cancel <-chan struct{}, input *fuse.ReadIn, out *fuse.DirEntryList) fuse.Status {
Expand All @@ -1095,53 +1072,72 @@ func (b *rawBridge) readDirMaybeLookup(cancel <-chan struct{}, input *fuse.ReadI
f.mu.Lock()
defer f.mu.Unlock()

defer func() { f.dirOffset = out.Offset }()

errno, eof := b.setStream(cancel, input, n, f)
if errno != 0 {
return errnoToStatus(errno)
} else if eof {
return fuse.OK
ctx := &fuse.Context{Caller: input.Caller, Cancel: cancel}
if input.Offset != f.dirOffset {
if sd, ok := f.file.(FileSeekdirer); ok {
errno := sd.Seekdir(ctx, input.Offset)
if errno != 0 {
return errnoToStatus(errno)
}
f.dirOffset = input.Offset
f.overflowErrno = 0
f.hasOverflow = false
} else {
return fuse.ENOTSUP
}
}

ctx := &fuse.Context{Caller: input.Caller, Cancel: cancel}
defer func() {
f.dirOffset = out.Offset
}()

fre, ok := f.file.(FileReaddirenter)
if !ok {
return fuse.OK
}
getdent := fre.Readdirent
first := true
for f.dirStream.HasNext() || f.hasOverflow {
var e fuse.DirEntry
var errno syscall.Errno

for {
var de *fuse.DirEntry
var errno syscall.Errno
if f.hasOverflow {
e = f.overflow
errno = f.overflowErrno
f.hasOverflow = false
if f.overflowErrno != 0 {
return errnoToStatus(f.overflowErrno)
}
de = &f.overflow
} else {
e, errno = f.dirStream.Next()
de, errno = getdent(ctx)
if errno != 0 {
if first {
return errnoToStatus(errno)
} else {
f.hasOverflow = true
f.overflowErrno = errno
return fuse.OK
}
}
}

if errno != 0 {
if first {
return errnoToStatus(errno)
} else {
f.overflowErrno = errno
f.hasOverflow = true
return fuse.OK
}
if de == nil {
break
}

first = false

if !lookup {
if !out.AddDirEntry(e) {
f.overflow = e
if !out.AddDirEntry(*de) {
f.overflow = *de
f.hasOverflow = true
return fuse.OK
}
continue
}

entryOut := out.AddDirLookupEntry(e)
entryOut := out.AddDirLookupEntry(*de)
if entryOut == nil {
f.overflow = e
f.overflow = *de
f.hasOverflow = true
return fuse.OK
}
Expand All @@ -1151,20 +1147,20 @@ func (b *rawBridge) readDirMaybeLookup(cancel <-chan struct{}, input *fuse.ReadI
// The values in EntryOut are ignored by Linux
// (see fuse_direntplus_link() in linux/fs/fuse/readdir.c), so leave
// them at zero-value.
if e.Name == "." || e.Name == ".." {
if de.Name == "." || de.Name == ".." {
continue
}

child, errno := b.lookup(ctx, n, e.Name, entryOut)
child, errno := b.lookup(ctx, n, de.Name, entryOut)
if errno != 0 {
if b.options.NegativeTimeout != nil {
entryOut.SetEntryTimeout(*b.options.NegativeTimeout)
}
} else {
child, _ = b.addNewChild(n, e.Name, child, nil, 0, entryOut)
child, _ = b.addNewChild(n, de.Name, child, nil, 0, entryOut)
child.setEntryOut(entryOut)
b.setEntryOutTimeout(entryOut)
if e.Mode&syscall.S_IFMT != child.stableAttr.Mode&syscall.S_IFMT {
if de.Mode&syscall.S_IFMT != child.stableAttr.Mode&syscall.S_IFMT {
// The file type has changed behind our back. Use the new value.
out.FixMode(child.stableAttr.Mode)
}
Expand All @@ -1176,9 +1172,12 @@ func (b *rawBridge) readDirMaybeLookup(cancel <-chan struct{}, input *fuse.ReadI
}

func (b *rawBridge) FsyncDir(cancel <-chan struct{}, input *fuse.FsyncIn) fuse.Status {
n, _ := b.inode(input.NodeId, input.Fh)
if fs, ok := n.ops.(NodeFsyncer); ok {
return errnoToStatus(fs.Fsync(&fuse.Context{Caller: input.Caller, Cancel: cancel}, nil, input.FsyncFlags))
n, f := b.inode(input.NodeId, input.Fh)
ctx := &fuse.Context{Caller: input.Caller, Cancel: cancel}
if fsd, ok := f.file.(FileFsyncdirer); ok {
return errnoToStatus(fsd.Fsyncdir(ctx, input.FsyncFlags))
} else if fs, ok := n.ops.(NodeFsyncer); ok {
return errnoToStatus(fs.Fsync(ctx, f.file, input.FsyncFlags))
}

return fuse.ENOTSUP
Expand Down
Loading

0 comments on commit e885cea

Please sign in to comment.