Commit 9d7cb89b authored by Han-Wen Nienhuys's avatar Han-Wen Nienhuys

nodefs: sketch for a better NodeFS API

Credits: navytux, rfjakob for general feedback.


Next:
- Opendir

TODO
- context for PID/caller

Open questions: grep for NOSUBMIT/TODO
- how many interfaces?
- explode SetAttr ?
parent dc73e9f1
// Copyright 2019 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package nodefs
import (
"context"
"time"
"github.com/hanwen/go-fuse/fuse"
)
/*
NOSUBMIT: how to structure?
- one interface per method?
- one interface for files (getattr, read/write), one for dirs (lookup, opendir), one shared?
- one giant interface?
- use raw types as args rather than mimicking Golang signatures?
*/
type Node interface {
// setInode links the Inode to a Node.
setInode(*Inode)
// Inode must return a non-nil associated inode structure. The
// identity of the Inode may not change over the lifetime of
// the object.
Inode() *Inode
// Lookup finds a child Inode. If a new Inode must be created,
// the inode does not have to be added to the tree.
Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*Inode, fuse.Status)
Open(ctx context.Context, flags uint32) (fh File, fuseFlags uint32, code fuse.Status)
Create(ctx context.Context, name string, flags uint32, mode uint32) (inode *Inode, fh File, fuseFlags uint32, code fuse.Status)
Read(ctx context.Context, f File, dest []byte, off int64) (fuse.ReadResult, fuse.Status)
Write(ctx context.Context, f File, data []byte, off int64) (written uint32, code fuse.Status)
// File locking
GetLk(ctx context.Context, f File, owner uint64, lk *fuse.FileLock, flags uint32, out *fuse.FileLock) (code fuse.Status)
SetLk(ctx context.Context, f File, owner uint64, lk *fuse.FileLock, flags uint32) (code fuse.Status)
SetLkw(ctx context.Context, f File, owner uint64, lk *fuse.FileLock, flags uint32) (code fuse.Status)
// Flush is called for close() call on a file descriptor. In
// case of duplicated descriptor, it may be called more than
// once for a file.
Flush(ctx context.Context, f File) fuse.Status
// This is called to before the file handle is forgotten. This
// method has no return value, so nothing can synchronizes on
// the call. Any cleanup that requires specific synchronization or
// could fail with I/O errors should happen in Flush instead.
Release(ctx context.Context, f File)
// The methods below may be called on closed files, due to
// concurrency. In that case, you should return EBADF.
GetAttr(ctx context.Context, f File, out *fuse.Attr) fuse.Status
/*
NOSUBMIT - fold into a setattr method, or expand methods?
Decoding SetAttr is a bit of a PITA, but if we use fuse
types as args, we can't take apart SetAttr for the caller
*/
Truncate(ctx context.Context, f File, size uint64) fuse.Status
Chown(ctx context.Context, f File, uid uint32, gid uint32) fuse.Status
Chmod(ctx context.Context, f File, perms uint32) fuse.Status
Utimens(ctx context.Context, f File, atime *time.Time, mtime *time.Time) fuse.Status
Allocate(ctx context.Context, f File, off uint64, size uint64, mode uint32) (code fuse.Status)
}
type File interface {
Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, fuse.Status)
Write(ctx context.Context, data []byte, off int64) (written uint32, code fuse.Status)
// File locking
GetLk(ctx context.Context, owner uint64, lk *fuse.FileLock, flags uint32, out *fuse.FileLock) (code fuse.Status)
SetLk(ctx context.Context, owner uint64, lk *fuse.FileLock, flags uint32) (code fuse.Status)
SetLkw(ctx context.Context, owner uint64, lk *fuse.FileLock, flags uint32) (code fuse.Status)
// Flush is called for close() call on a file descriptor. In
// case of duplicated descriptor, it may be called more than
// once for a file.
Flush(ctx context.Context) fuse.Status
// This is called to before the file handle is forgotten. This
// method has no return value, so nothing can synchronizes on
// the call. Any cleanup that requires specific synchronization or
// could fail with I/O errors should happen in Flush instead.
Release(ctx context.Context)
// The methods below may be called on closed files, due to
// concurrency. In that case, you should return EBADF.
// TODO - fold into a setattr method?
GetAttr(ctx context.Context, out *fuse.Attr) fuse.Status
Truncate(ctx context.Context, size uint64) fuse.Status
Chown(ctx context.Context, uid uint32, gid uint32) fuse.Status
Chmod(ctx context.Context, perms uint32) fuse.Status
Utimens(ctx context.Context, atime *time.Time, mtime *time.Time) fuse.Status
Allocate(ctx context.Context, off uint64, size uint64, mode uint32) (code fuse.Status)
}
type Options struct {
Debug bool
EntryTimeout *time.Duration
AttrTimeout *time.Duration
NegativeTimeout *time.Duration
}
This diff is collapsed.
// Copyright 2019 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package nodefs
import (
"context"
"time"
"github.com/hanwen/go-fuse/fuse"
)
// DefaultNode must be embedded in a Node implementation.
type DefaultNode struct {
inode *Inode
}
func (dn *DefaultNode) setInode(n *Inode) {
dn.inode = n
}
func (dn *DefaultNode) Inode() *Inode {
return dn.inode
}
func (n *DefaultNode) Read(ctx context.Context, f File, dest []byte, off int64) (fuse.ReadResult, fuse.Status) {
if f != nil {
return f.Read(ctx, dest, off)
}
return nil, fuse.ENOSYS
}
func (n *DefaultNode) Write(ctx context.Context, f File, data []byte, off int64) (written uint32, code fuse.Status) {
if f != nil {
return f.Write(ctx, data, off)
}
return 0, fuse.ENOSYS
}
func (n *DefaultNode) GetLk(ctx context.Context, f File, owner uint64, lk *fuse.FileLock, flags uint32, out *fuse.FileLock) (code fuse.Status) {
if f != nil {
return f.GetLk(ctx, owner, lk, flags, out)
}
return fuse.ENOSYS
}
func (n *DefaultNode) SetLk(ctx context.Context, f File, owner uint64, lk *fuse.FileLock, flags uint32) (code fuse.Status) {
if f != nil {
return f.SetLk(ctx, owner, lk, flags)
}
return fuse.ENOSYS
}
func (n *DefaultNode) SetLkw(ctx context.Context, f File, owner uint64, lk *fuse.FileLock, flags uint32) (code fuse.Status) {
if f != nil {
return f.SetLkw(ctx, owner, lk, flags)
}
return fuse.ENOSYS
}
func (n *DefaultNode) Flush(ctx context.Context, f File) fuse.Status {
if f != nil {
return f.Flush(ctx)
}
return fuse.ENOSYS
}
func (n *DefaultNode) Release(ctx context.Context, f File) {
if f != nil {
f.Release(ctx)
}
}
func (n *DefaultNode) Allocate(ctx context.Context, f File, off uint64, size uint64, mode uint32) (code fuse.Status) {
if f != nil {
return f.Allocate(ctx, off, size, mode)
}
return fuse.ENOSYS
}
func (n *DefaultNode) GetAttr(ctx context.Context, f File, out *fuse.Attr) fuse.Status {
if f != nil {
f.GetAttr(ctx, out)
}
return fuse.ENOSYS
}
func (n *DefaultNode) Truncate(ctx context.Context, f File, size uint64) fuse.Status {
if f != nil {
return f.Truncate(ctx, size)
}
return fuse.ENOSYS
}
func (n *DefaultNode) Chown(ctx context.Context, f File, uid uint32, gid uint32) fuse.Status {
if f != nil {
return f.Chown(ctx, uid, gid)
}
return fuse.ENOSYS
}
func (n *DefaultNode) Chmod(ctx context.Context, f File, perms uint32) fuse.Status {
if f != nil {
return f.Chmod(ctx, perms)
}
return fuse.ENOSYS
}
func (n *DefaultNode) Utimens(ctx context.Context, f File, atime *time.Time, mtime *time.Time) fuse.Status {
if f != nil {
return f.Utimens(ctx, atime, mtime)
}
return fuse.ENOSYS
}
type DefaultFile struct {
}
func (f *DefaultFile) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, fuse.Status) {
return nil, fuse.ENOSYS
}
func (f *DefaultFile) Write(ctx context.Context, data []byte, off int64) (written uint32, code fuse.Status) {
return 0, fuse.ENOSYS
}
func (f *DefaultFile) GetLk(ctx context.Context, owner uint64, lk *fuse.FileLock, flags uint32, out *fuse.FileLock) (code fuse.Status) {
return fuse.ENOSYS
}
func (f *DefaultFile) SetLk(ctx context.Context, owner uint64, lk *fuse.FileLock, flags uint32) (code fuse.Status) {
return fuse.ENOSYS
}
func (f *DefaultFile) SetLkw(ctx context.Context, owner uint64, lk *fuse.FileLock, flags uint32) (code fuse.Status) {
return fuse.ENOSYS
}
func (f *DefaultFile) Flush(ctx context.Context) fuse.Status {
return fuse.ENOSYS
}
func (f *DefaultFile) Release(ctx context.Context) {
}
func (f *DefaultFile) GetAttr(ctx context.Context, out *fuse.Attr) fuse.Status {
return fuse.ENOSYS
}
func (f *DefaultFile) Truncate(ctx context.Context, size uint64) fuse.Status {
return fuse.ENOSYS
}
func (f *DefaultFile) Chown(ctx context.Context, uid uint32, gid uint32) fuse.Status {
return fuse.ENOSYS
}
func (f *DefaultFile) Chmod(ctx context.Context, perms uint32) fuse.Status {
return fuse.ENOSYS
}
func (f *DefaultFile) Utimens(ctx context.Context, atime *time.Time, mtime *time.Time) fuse.Status {
return fuse.ENOSYS
}
func (f *DefaultFile) Allocate(ctx context.Context, off uint64, size uint64, mode uint32) (code fuse.Status) {
return fuse.ENOSYS
}
// Copyright 2019 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package nodefs
import (
"context"
// "time"
"os"
"sync"
"syscall"
"github.com/hanwen/go-fuse/fuse"
)
// LoopbackFile delegates all operations back to an underlying os.File.
func NewLoopbackFile(f *os.File) File {
return &loopbackFile{File: f}
}
type loopbackFile struct {
File *os.File
// os.File is not threadsafe. Although fd themselves are
// constant during the lifetime of an open file, the OS may
// reuse the fd number after it is closed. When open races
// with another close, they may lead to confusion as which
// file gets written in the end.
mu sync.Mutex
}
func (f *loopbackFile) Read(ctx context.Context, buf []byte, off int64) (res fuse.ReadResult, code fuse.Status) {
f.mu.Lock()
// This is not racy by virtue of the kernel properly
// synchronizing the open/write/close.
r := fuse.ReadResultFd(f.File.Fd(), off, len(buf))
f.mu.Unlock()
return r, fuse.OK
}
func (f *loopbackFile) Write(ctx context.Context, data []byte, off int64) (uint32, fuse.Status) {
f.mu.Lock()
n, err := f.File.WriteAt(data, off)
f.mu.Unlock()
return uint32(n), fuse.ToStatus(err)
}
func (f *loopbackFile) Release(ctx context.Context) {
f.mu.Lock()
f.File.Close()
f.mu.Unlock()
}
func (f *loopbackFile) Flush(ctx context.Context) fuse.Status {
f.mu.Lock()
// Since Flush() may be called for each dup'd fd, we don't
// want to really close the file, we just want to flush. This
// is achieved by closing a dup'd fd.
newFd, err := syscall.Dup(int(f.File.Fd()))
f.mu.Unlock()
if err != nil {
return fuse.ToStatus(err)
}
err = syscall.Close(newFd)
return fuse.ToStatus(err)
}
func (f *loopbackFile) Fsync(ctx context.Context, flags int) (code fuse.Status) {
f.mu.Lock()
r := fuse.ToStatus(syscall.Fsync(int(f.File.Fd())))
f.mu.Unlock()
return r
}
const (
F_OFD_GETLK = 36
F_OFD_SETLK = 37
F_OFD_SETLKW = 38
)
func (f *loopbackFile) GetLk(ctx context.Context, owner uint64, lk *fuse.FileLock, flags uint32, out *fuse.FileLock) (code fuse.Status) {
flk := syscall.Flock_t{}
lk.ToFlockT(&flk)
code = fuse.ToStatus(syscall.FcntlFlock(f.File.Fd(), F_OFD_GETLK, &flk))
out.FromFlockT(&flk)
return
}
func (f *loopbackFile) SetLk(ctx context.Context, owner uint64, lk *fuse.FileLock, flags uint32) (code fuse.Status) {
return f.setLock(ctx, owner, lk, flags, false)
}
func (f *loopbackFile) SetLkw(ctx context.Context, owner uint64, lk *fuse.FileLock, flags uint32) (code fuse.Status) {
return f.setLock(ctx, owner, lk, flags, true)
}
func (f *loopbackFile) setLock(ctx context.Context, owner uint64, lk *fuse.FileLock, flags uint32, blocking bool) (code fuse.Status) {
if (flags & fuse.FUSE_LK_FLOCK) != 0 {
var op int
switch lk.Typ {
case syscall.F_RDLCK:
op = syscall.LOCK_SH
case syscall.F_WRLCK:
op = syscall.LOCK_EX
case syscall.F_UNLCK:
op = syscall.LOCK_UN
default:
return fuse.EINVAL
}
if !blocking {
op |= syscall.LOCK_NB
}
return fuse.ToStatus(syscall.Flock(int(f.File.Fd()), op))
} else {
flk := syscall.Flock_t{}
lk.ToFlockT(&flk)
var op int
if blocking {
op = F_OFD_SETLKW
} else {
op = F_OFD_SETLK
}
return fuse.ToStatus(syscall.FcntlFlock(f.File.Fd(), op, &flk))
}
}
func (f *loopbackFile) Truncate(ctx context.Context, size uint64) fuse.Status {
f.mu.Lock()
r := fuse.ToStatus(syscall.Ftruncate(int(f.File.Fd()), int64(size)))
f.mu.Unlock()
return r
}
func (f *loopbackFile) Chmod(ctx context.Context, mode uint32) fuse.Status {
f.mu.Lock()
r := fuse.ToStatus(f.File.Chmod(os.FileMode(mode)))
f.mu.Unlock()
return r
}
func (f *loopbackFile) Chown(ctx context.Context, uid uint32, gid uint32) fuse.Status {
f.mu.Lock()
r := fuse.ToStatus(f.File.Chown(int(uid), int(gid)))
f.mu.Unlock()
return r
}
func (f *loopbackFile) GetAttr(ctx context.Context, a *fuse.Attr) fuse.Status {
st := syscall.Stat_t{}
f.mu.Lock()
err := syscall.Fstat(int(f.File.Fd()), &st)
f.mu.Unlock()
if err != nil {
return fuse.ToStatus(err)
}
a.FromStat(&st)
return fuse.OK
}
// Copyright 2019 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package nodefs
import (
"context"
"syscall"
"time"
"github.com/hanwen/go-fuse/fuse"
)
func (f *loopbackFile) Allocate(ctx context.Context, off uint64, sz uint64, mode uint32) fuse.Status {
f.mu.Lock()
err := syscall.Fallocate(int(f.File.Fd()), mode, int64(off), int64(sz))
f.mu.Unlock()
if err != nil {
return fuse.ToStatus(err)
}
return fuse.OK
}
// Utimens - file handle based version of loopbackFileSystem.Utimens()
func (f *loopbackFile) Utimens(ctx context.Context, a *time.Time, m *time.Time) fuse.Status {
var ts [2]syscall.Timespec
ts[0] = fuse.UtimeToTimespec(a)
ts[1] = fuse.UtimeToTimespec(m)
f.mu.Lock()
var err error
// NOSUBMIT
// err := futimens(int(f.File.Fd()), &ts)
f.mu.Unlock()
return fuse.ToStatus(err)
}
// Copyright 2019 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package nodefs
import (
"log"
"sort"
"strings"
"sync"
"unsafe"
"github.com/hanwen/go-fuse/fuse"
)
var _ = log.Println
type parentData struct {
name string
parent *Inode
}
// Inode is a node in VFS tree. Inodes are one-to-one mapped to Node
// instances, which is the extension interface for file systems. One
// can create fully-formed trees of Inodes ahead of time by creating
// "persistent" Inodes.
type Inode struct {
// The filetype bits from the mode.
mode uint32
opaqueID uint64
node Node
bridge *rawBridge
// Following data is mutable.
// Protected by bridge.mu
lookupCount uint64
nodeID uint64
// mu protects the following mutable fields. When locking
// multiple Inodes, locks must be acquired using
// lockNodes/unlockNodes
mu sync.Mutex
// incremented every time the 'children' or 'parents' field is changed.
changeCounter uint32
children map[string]*Inode
parents map[parentData]struct{}
}
func sortNodes(ns []*Inode) {
sort.Slice(ns, func(i, j int) bool {
return uintptr(unsafe.Pointer(ns[i])) < uintptr(unsafe.Pointer(ns[j]))
})
}
func lockNodes(ns ...*Inode) {
sortNodes(ns)
for _, n := range ns {
n.mu.Lock()
}
}
func unlockNodes(ns ...*Inode) {
sortNodes(ns)
for _, n := range ns {
n.mu.Unlock()
}
}
// Forgotten returns true if the kernel holds no references to this
// inode. This can be used for background cleanup tasks, since the
// kernel has no way of reviving forgotten nodes by its own
// initiative.
func (n *Inode) Forgotten() bool {
n.bridge.mu.Lock()
defer n.bridge.mu.Unlock()
return n.lookupCount == 0
}
// Node returns the Node object implementing the file system operations.
func (n *Inode) Node() Node {
return n.node
}
// Path returns a path string to the inode relative to the root.
func (n *Inode) Path(root *Inode) string {
var segments []string
p := n
for p != nil && p != root {
var pd parentData
// We don't try to take all locks at the same time, because
// the caller won't use the "path" string under lock anyway.
p.mu.Lock()
for pd = range p.parents {
break
}
p.mu.Unlock()
if pd.parent == nil {
break
}
segments = append(segments, pd.name)
p = pd.parent
}
if p == nil {
// NOSUBMIT - should replace rather than append?
segments = append(segments, ".deleted")
}
i := 0
j := len(segments) - 1
for i < j {
segments[i], segments[j] = segments[j], segments[i]
i++
j--
}
path := strings.Join(segments, "/")
return path
}
// Finds a child with the given name and filetype. Returns nil if not
// found.
func (n *Inode) FindChildByMode(name string, mode uint32) *Inode {
mode ^= 07777
n.mu.Lock()
defer n.mu.Unlock()
ch := n.children[name]
if ch != nil && ch.mode == mode {
return ch
}
return nil
}
// Finds a child with the given name and ID. Returns nil if not found.
func (n *Inode) FindChildByOpaqueID(name string, opaqueID uint64) *Inode {
n.mu.Lock()
defer n.mu.Unlock()
ch := n.children[name]
if ch != nil && ch.opaqueID == opaqueID {
return ch
}
return nil
}
func (n *Inode) addLookup(name string, child *Inode) {
child.lookupCount++
child.parents[parentData{name, n}] = struct{}{}
n.children[name] = child
child.changeCounter++
n.changeCounter++
}
func (n *Inode) clearParents() {
for {
lockme := []*Inode{n}
n.mu.Lock()
ts := n.changeCounter
for p := range n.parents {
lockme = append(lockme, p.parent)
}
n.mu.Unlock()
lockNodes(lockme...)
success := false
if ts == n.changeCounter {
for p := range n.parents {
delete(p.parent.children, p.name)
p.parent.changeCounter++
}
n.parents = map[parentData]struct{}{}
n.changeCounter++
success = true
}
unlockNodes(lockme...)
if success {
return
}
}
}
func (n *Inode) clearChildren() {
if n.mode != fuse.S_IFDIR {
return
}
var lockme []*Inode
for {
lockme = append(lockme[:0], n)
n.mu.Lock()
ts := n.changeCounter
for _, ch := range n.children {
lockme = append(lockme, ch)
}
n.mu.Unlock()
lockNodes(lockme...)
success := false
if ts == n.changeCounter {
for nm, ch := range n.children {
delete(ch.parents, parentData{nm, n})
ch.changeCounter++
}
n.children = map[string]*Inode{}
n.changeCounter++
success = true
}
unlockNodes(lockme...)
if success {
break
}
}
for _, ch := range lockme {
if ch != n {
ch.clearChildren()
}
}
}
// NewPersistentInode returns an Inode with a LookupCount == 1, ie. the
// node will only get garbage collected if the kernel issues a forget
// on any of its parents.
func (n *Inode) NewPersistentInode(node Node, mode uint32, opaque uint64) *Inode {
ch := n.NewInode(node, mode, opaque)
ch.lookupCount++
return ch
}
// NewInode returns an inode for the given Node. The mode should be
// standard mode argument (eg. S_IFDIR). The opaqueID argument can be
// used to signal changes in the tree structure during lookup (see
// FindChildByOpaqueID). For a loopback file system, the inode number
// of the underlying file is a good candidate.
func (n *Inode) NewInode(node Node, mode uint32, opaqueID uint64) *Inode {
ch := &Inode{
mode: mode ^ 07777,
node: node,
bridge: n.bridge,
parents: make(map[parentData]struct{}),
}
if mode&fuse.S_IFDIR != 0 {
ch.children = make(map[string]*Inode)
}
node.setInode(ch)
return ch
}
// Copyright 2019 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package nodefs
import (
"context"
"os"
"path/filepath"
"syscall"
"github.com/hanwen/go-fuse/fuse"
)
type loopbackRoot struct {
loopbackNode
root string
}
func (n *loopbackRoot) GetAttr(ctx context.Context, f File, out *fuse.Attr) fuse.Status {
var err error = nil
st := syscall.Stat_t{}
err = syscall.Stat(n.root, &st)
if err != nil {
return fuse.ToStatus(err)
}
out.FromStat(&st)
return fuse.OK
}
type loopbackNode struct {
DefaultNode
rootNode *loopbackRoot
}
func (n *loopbackNode) path() string {
path := n.Inode().Path(nil)
return filepath.Join(n.rootNode.root, path)
}
func (n *loopbackNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*Inode, fuse.Status) {
p := filepath.Join(n.path(), name)
st := syscall.Stat_t{}
err := syscall.Lstat(p, &st)
if err != nil {
return nil, fuse.ToStatus(err)
}
out.Attr.FromStat(&st)
ch := n.Inode().FindChildByOpaqueID(name, out.Attr.Ino)
if ch != nil {
return ch, fuse.OK
}
node := &loopbackNode{rootNode: n.rootNode}
ch = n.Inode().NewInode(node, out.Attr.Mode, out.Attr.Ino)
return ch, fuse.OK
}
func (n *loopbackNode) Create(ctx context.Context, name string, flags uint32, mode uint32) (inode *Inode, fh File, fuseFlags uint32, code fuse.Status) {
p := filepath.Join(n.path(), name)
f, err := os.OpenFile(p, int(flags)|os.O_CREATE, os.FileMode(mode))
if err != nil {
return nil, nil, 0, fuse.ToStatus(err)
}
st := syscall.Stat_t{}
if err := syscall.Fstat(int(f.Fd()), &st); err != nil {
f.Close()
return nil, nil, 0, fuse.ToStatus(err)
}
node := &loopbackNode{rootNode: n.rootNode}
ch := n.Inode().NewInode(node, st.Mode, st.Ino)
return ch, NewLoopbackFile(f), 0, fuse.OK
}
func (n *loopbackNode) Open(ctx context.Context, flags uint32) (fh File, fuseFlags uint32, code fuse.Status) {
p := n.path()
f, err := os.OpenFile(p, int(flags), 0)
if err != nil {
return nil, 0, fuse.ToStatus(err)
}
return NewLoopbackFile(f), 0, fuse.OK
}
func (n *loopbackNode) GetAttr(ctx context.Context, f File, out *fuse.Attr) fuse.Status {
if f != nil {
return f.GetAttr(ctx, out)
}
p := n.path()
var err error = nil
st := syscall.Stat_t{}
err = syscall.Lstat(p, &st)
if err != nil {
return fuse.ToStatus(err)
}
out.FromStat(&st)
return fuse.OK
}
func NewLoopback(root string) Node {
n := &loopbackRoot{
root: root,
}
n.rootNode = n
return n
}
// Copyright 2019 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package nodefs
import (
"bytes"
"io"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
"github.com/hanwen/go-fuse/fuse"
"github.com/hanwen/go-fuse/internal/testutil"
)
type testCase struct {
*testing.T
dir string
origDir string
mntDir string
rawFS fuse.RawFileSystem
server *fuse.Server
}
func (tc *testCase) writeOrig(path, content string, mode os.FileMode) {
if err := ioutil.WriteFile(filepath.Join(tc.origDir, path), []byte(content), mode); err != nil {
tc.Fatal(err)
}
}
func (tc *testCase) Clean() {
if err := tc.server.Unmount(); err != nil {
tc.Fatal(err)
}
if err := os.RemoveAll(tc.dir); err != nil {
tc.Fatal(err)
}
}
func newTestCase(t *testing.T) *testCase {
tc := &testCase{
dir: testutil.TempDir(),
T: t,
}
tc.origDir = tc.dir + "/orig"
tc.mntDir = tc.dir + "/mnt"
if err := os.Mkdir(tc.origDir, 0755); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(tc.mntDir, 0755); err != nil {
t.Fatal(err)
}
loopback := NewLoopback(tc.origDir)
oneSec := time.Second
tc.rawFS = NewNodeFS(loopback, &Options{
Debug: testutil.VerboseTest(),
EntryTimeout: &oneSec,
AttrTimeout: &oneSec,
})
var err error
tc.server, err = fuse.NewServer(tc.rawFS, tc.mntDir,
&fuse.MountOptions{
Debug: testutil.VerboseTest(),
})
if err != nil {
t.Fatal(err)
}
go tc.server.Serve()
if err := tc.server.WaitMount(); err != nil {
t.Fatal(err)
}
return tc
}
func TestBasic(t *testing.T) {
tc := newTestCase(t)
defer tc.Clean()
tc.writeOrig("file", "hello", 0644)
fi, err := os.Lstat(tc.mntDir + "/file")
if err != nil {
t.Fatalf("Lstat: %v", err)
}
if fi.Size() != 5 {
t.Errorf("got size %d want 5", fi.Size())
}
stat := fuse.ToStatT(fi)
if got, want := stat.Mode, uint32(fuse.S_IFREG|0644); got != want {
t.Errorf("got mode %o, want %o", got, want)
}
}
func TestFile(t *testing.T) {
tc := newTestCase(t)
defer tc.Clean()
content := []byte("hello world")
fn := tc.mntDir + "/file"
if err := ioutil.WriteFile(fn, content, 0755); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if got, err := ioutil.ReadFile(fn); err != nil {
t.Fatalf("ReadFile: %v", err)
} else if bytes.Compare(got, content) != 0 {
t.Errorf("got %q, want %q", got, content)
}
f, err := os.Open(fn)
if err != nil {
t.Fatalf("Open: %v", err)
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
t.Fatalf("Fstat: %v", err)
} else if int(fi.Size()) != len(content) {
t.Errorf("got size %d want 5", fi.Size())
}
stat := fuse.ToStatT(fi)
if got, want := stat.Mode, uint32(fuse.S_IFREG|0755); got != want {
t.Errorf("Fstat: got mode %o, want %o", got, want)
}
if err := f.Close(); err != nil {
t.Errorf("Close: %v", err)
}
}
func TestFileTruncate(t *testing.T) {
tc := newTestCase(t)
defer tc.Clean()
content := []byte("hello world")
if err := ioutil.WriteFile(tc.origDir+"/file", content, 0755); err != nil {
t.Fatalf("WriteFile: %v", err)
}
f, err := os.OpenFile(tc.mntDir+"/file", os.O_RDWR, 0644)
if err != nil {
t.Fatalf("Open: %v", err)
}
defer f.Close()
const trunc = 5
if err := f.Truncate(5); err != nil {
t.Errorf("Truncate: %v", err)
}
if err := f.Close(); err != nil {
t.Errorf("Close: %v", err)
}
if got, err := ioutil.ReadFile(tc.origDir + "/file"); err != nil {
t.Fatalf("ReadFile: %v", err)
} else if want := content[:trunc]; bytes.Compare(got, want) != 0 {
t.Errorf("got %q, want %q", got, want)
}
}
func TestFileFdLeak(t *testing.T) {
tc := newTestCase(t)
defer func() {
if tc != nil {
tc.Clean()
}
}()
content := []byte("hello world")
if err := ioutil.WriteFile(tc.origDir+"/file", content, 0755); err != nil {
t.Fatalf("WriteFile: %v", err)
}
for i := 0; i < 100; i++ {
if _, err := ioutil.ReadFile(tc.mntDir + "/file"); err != nil {
t.Fatalf("ReadFile: %v", err)
}
}
if runtime.GOOS == "line" {
infos, err := ioutil.ReadDir("/proc/self/fd")
if err != nil {
t.Errorf("ReadDir %v", err)
}
if len(infos) > 15 {
t.Errorf("found %d open file descriptors for 100x ReadFile", len(infos))
}
}
tc.Clean()
bridge := tc.rawFS.(*rawBridge)
tc = nil
if got := len(bridge.files); got > 3 {
t.Errorf("found %d used file handles, should be <= 3", got)
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment