diff --git a/nodefs/api.go b/nodefs/api.go new file mode 100644 index 0000000000000000000000000000000000000000..66d7347cca9bcbbb450af715847d023104e55afc --- /dev/null +++ b/nodefs/api.go @@ -0,0 +1,169 @@ +// 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 provides infrastructure to build tree-organized filesystems. +// +// A tree-organized filesystem is similar to UNIX or Plan 9 filesystem: it +// consists of nodes with each node being either a file or a directory. Files +// are located at tree leafs. A directory node can have other nodes as its +// children and refer to each child by name unique through the directory. +// There can be several paths leading from tree root to a particular node, +// known as hard-linking, for example +// +// root +// / \ +// dir1 dir2 +// \ / +// file +// +// A /-separated string path describes location of a node in the tree. For example +// +// /dir1/file +// +// describes path root → dir1 → file. +// +// Each node is associated with integer ID uniquely identifying the node +// throughout filesystem. The tree-level structure of any filesystem is +// expressed through index-nodes (also known as "inode", see Inode) which +// describe parent/child relation in between nodes and node-ID association. +// +// A particular filesystem should provide nodes with filesystem operations +// implemented as defined by Node interface. When filesystem is mounted, its +// root Node is associated with root of the tree, and the tree is further build +// lazily when nodefs infrastructure needs to lookup children of nodes to +// process client requests. For every new node, the filesystem infrastructure +// automatically builds new index node and links it in the filesystem tree. +// InodeOf can be used to get particular Inode associated with a Node. +// +// XXX ^^^ inodes cleaned on cache clean (FORGET). +// +// XXX describe how to mount. +// +// XXX node example with Lookup. +// +// XXX describe how to pre-add nodes to tree. +// +package nodefs + +import ( + "context" + "time" + + "github.com/hanwen/go-fuse/fuse" +) + +// InodeOf returns index-node associated with filesystem node. +// +// The identity of the Inode does not change over the lifetime of +// the node object. +func InodeOf(node Node) *Inode { + return node.inode() +} + +/* +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? + +Every Node implementation must directly or indirectly embed DefaultNode. +*/ +type Node interface { + // setInode and inode are used by nodefs internally to link Inode to a Node. + // + // When a new Node instance is created, e.g. on Lookup, it has nil Inode. + // Nodefs infrastructure will notice this and associate created Node with new Inode. + // + // See InodeOf for public API to retrieve an inode from Node. + inode() *Inode + setInode(*Inode) (set bool) + + // Lookup should find a direct child of the node by child name. + // + // VFS makes sure to call Lookup only once for particular (node, name) + // pair. + 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) (node *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 +} diff --git a/nodefs/bridge.go b/nodefs/bridge.go new file mode 100644 index 0000000000000000000000000000000000000000..76efcbe82255f0ae3e876c6175fe6c94845090b0 --- /dev/null +++ b/nodefs/bridge.go @@ -0,0 +1,439 @@ +// 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" + "sync" + "time" + + "github.com/hanwen/go-fuse/fuse" +) + +type mapEntry struct { + generation uint64 + inode *Inode +} + +type fileEntry struct { + file File + + // space to hold directory stuff +} + +type rawBridge struct { + fuse.RawFileSystem + + options Options + root *Inode + + mu sync.Mutex + nodes []mapEntry + free []uint64 + + files []fileEntry + freeFiles []uint64 +} + +func NewNodeFS(root Node, opts *Options) fuse.RawFileSystem { + bridge := &rawBridge{ + RawFileSystem: fuse.NewDefaultRawFileSystem(), + } + + if opts != nil { + bridge.options = *opts + } else { + oneSec := time.Second + bridge.options.EntryTimeout = &oneSec + bridge.options.AttrTimeout = &oneSec + } + + bridge.root = &Inode{ + nodeID: 1, + lookupCount: 1, + mode: fuse.S_IFDIR, + children: make(map[string]*Inode), + parents: nil, + node: root, + bridge: bridge, + } + root.setInode(bridge.root) + bridge.nodes = append(bridge.nodes, + mapEntry{}, + // ID 1 is always the root. + mapEntry{inode: bridge.root}) + + // Fh 0 means no file handle. + bridge.files = []fileEntry{{}} + return bridge +} + +func (b *rawBridge) inode(id uint64, fh uint64) (*Inode, fileEntry) { + b.mu.Lock() + defer b.mu.Unlock() + return b.nodes[id].inode, b.files[fh] +} + +func (b *rawBridge) Lookup(header *fuse.InHeader, name string, out *fuse.EntryOut) (status fuse.Status) { + parent, _ := b.inode(header.NodeId, 0) + + child, code := parent.node.Lookup(context.TODO(), name, out) + if !code.Ok() { + if b.options.NegativeTimeout != nil { + out.SetEntryTimeout(*b.options.NegativeTimeout) + } + return code + } + + b.mu.Lock() + defer b.mu.Unlock() + + lockNodes(parent, child) + parent.setEntry(name, child) + unlockNodes(parent, child) + + if child.nodeID == 0 { + b.registerInode(child) + } + + out.NodeId = child.nodeID + out.Generation = b.nodes[child.nodeID].generation + + if b.options.AttrTimeout != nil { + out.SetAttrTimeout(*b.options.AttrTimeout) + } + if b.options.EntryTimeout != nil { + out.SetEntryTimeout(*b.options.EntryTimeout) + } + + return fuse.OK +} + +func (b *rawBridge) registerInode(child *Inode) { + if l := len(b.free); l > 0 { + last := b.free[l-1] + b.free = b.free[:l-1] + + child.nodeID = last + b.nodes[last].inode = child + b.nodes[last].generation++ + } else { + last := len(b.nodes) + b.nodes = append(b.nodes, mapEntry{ + inode: child, + }) + child.nodeID = uint64(last) + } +} + +func (b *rawBridge) Create(input *fuse.CreateIn, name string, out *fuse.CreateOut) (code fuse.Status) { + ctx := context.TODO() + parent, _ := b.inode(input.NodeId, 0) + child, f, flags, code := parent.node.Create(ctx, name, input.Flags, input.Mode) + if !code.Ok() { + if b.options.NegativeTimeout != nil { + out.SetEntryTimeout(*b.options.NegativeTimeout) + } + return code + } + + b.mu.Lock() + defer b.mu.Unlock() + + b.registerInode(child) + + lockNodes(parent, child) + parent.setEntry(name, child) + unlockNodes(parent, child) + + out.NodeId = child.nodeID + out.Generation = b.nodes[child.nodeID].generation + + if b.options.AttrTimeout != nil { + out.SetAttrTimeout(*b.options.AttrTimeout) + } + if b.options.EntryTimeout != nil { + out.SetEntryTimeout(*b.options.EntryTimeout) + } + + out.Fh = b.registerFile(f) + out.OpenFlags = flags + + f.GetAttr(ctx, &out.Attr) + return fuse.OK +} + +func (b *rawBridge) Forget(nodeid, nlookup uint64) { + b.mu.Lock() + defer b.mu.Unlock() + n := b.nodes[nodeid].inode + n.lookupCount -= nlookup + if n.lookupCount == 0 { + n.clearChildren() + n.clearParents() + + b.free = append(b.free, nodeid) + b.nodes[nodeid].inode = nil + } + +} + +func (b *rawBridge) SetDebug(debug bool) {} + +func (b *rawBridge) GetAttr(input *fuse.GetAttrIn, out *fuse.AttrOut) (code fuse.Status) { + n, fEntry := b.inode(input.NodeId, input.Fh()) + f := fEntry.file + + // nosubmit - FATTR_FH vs FUSE_GETATTR_FH ? + if input.Flags()&fuse.FUSE_GETATTR_FH == 0 { + f = nil + } + + dest := &out.Attr + code = n.node.GetAttr(context.TODO(), f, dest) + if out.Nlink == 0 { + // With Nlink == 0, newer kernels will refuse link + // operations. + out.Nlink = 1 + } + + // NOSUBMIT attr timeout + return code +} + +func (b *rawBridge) SetAttr(input *fuse.SetAttrIn, out *fuse.AttrOut) (code fuse.Status) { + + ctx := context.TODO() + + n, fEntry := b.inode(input.NodeId, input.Fh) + f := fEntry.file + if input.Valid&fuse.FATTR_FH == 0 { + f = nil + } + + if input.Valid&fuse.FATTR_MODE != 0 { + permissions := uint32(07777) & input.Mode + code = n.node.Chmod(ctx, f, permissions) + } + + if code.Ok() && (input.Valid&(fuse.FATTR_UID|fuse.FATTR_GID) != 0) { + var uid uint32 = ^uint32(0) // means "do not change" in chown(2) + var gid uint32 = ^uint32(0) + if input.Valid&fuse.FATTR_UID != 0 { + uid = input.Uid + } + if input.Valid&fuse.FATTR_GID != 0 { + gid = input.Gid + } + code = n.node.Chown(ctx, f, uid, gid) + } + + if code.Ok() && input.Valid&fuse.FATTR_SIZE != 0 { + code = n.node.Truncate(ctx, f, input.Size) + } + + if code.Ok() && (input.Valid&(fuse.FATTR_ATIME|fuse.FATTR_MTIME|fuse.FATTR_ATIME_NOW|fuse.FATTR_MTIME_NOW) != 0) { + now := time.Now() + var atime *time.Time + var mtime *time.Time + + if input.Valid&fuse.FATTR_ATIME != 0 { + if input.Valid&fuse.FATTR_ATIME_NOW != 0 { + atime = &now + } else { + t := time.Unix(int64(input.Atime), int64(input.Atimensec)) + atime = &t + } + } + + if input.Valid&fuse.FATTR_MTIME != 0 { + if input.Valid&fuse.FATTR_MTIME_NOW != 0 { + mtime = &now + } else { + t := time.Unix(int64(input.Mtime), int64(input.Mtimensec)) + mtime = &t + } + } + + code = n.node.Utimens(ctx, f, atime, mtime) + } + + if !code.Ok() { + return code + } + + // Must call GetAttr(); the filesystem may override some of + // the changes we effect here. + attr := &out.Attr + code = n.node.GetAttr(ctx, f, attr) + + // TODO - attr timout? + return code +} + +func (b *rawBridge) Mknod(input *fuse.MknodIn, name string, out *fuse.EntryOut) (code fuse.Status) { + return fuse.ENOSYS +} + +func (b *rawBridge) Mkdir(input *fuse.MkdirIn, name string, out *fuse.EntryOut) (code fuse.Status) { + return fuse.ENOSYS +} + +func (b *rawBridge) Unlink(header *fuse.InHeader, name string) (code fuse.Status) { + return fuse.ENOSYS +} + +func (b *rawBridge) Rmdir(header *fuse.InHeader, name string) (code fuse.Status) { + return fuse.ENOSYS +} + +func (b *rawBridge) Rename(input *fuse.RenameIn, oldName string, newName string) (code fuse.Status) { + return fuse.ENOSYS +} + +func (b *rawBridge) Link(input *fuse.LinkIn, filename string, out *fuse.EntryOut) (code fuse.Status) { + return fuse.ENOSYS +} + +func (b *rawBridge) Symlink(header *fuse.InHeader, pointedTo string, linkName string, out *fuse.EntryOut) (code fuse.Status) { + return fuse.ENOSYS +} + +func (b *rawBridge) Readlink(header *fuse.InHeader) (out []byte, code fuse.Status) { + return nil, fuse.ENOSYS +} + +func (b *rawBridge) Access(input *fuse.AccessIn) (code fuse.Status) { + return fuse.ENOSYS +} + +// Extended attributes. +func (b *rawBridge) GetXAttrSize(header *fuse.InHeader, attr string) (sz int, code fuse.Status) { + return 0, fuse.ENOSYS +} + +func (b *rawBridge) GetXAttrData(header *fuse.InHeader, attr string) (data []byte, code fuse.Status) { + return nil, fuse.ENOSYS +} + +func (b *rawBridge) ListXAttr(header *fuse.InHeader) (attributes []byte, code fuse.Status) { + return nil, fuse.ENOSYS +} + +func (b *rawBridge) SetXAttr(input *fuse.SetXAttrIn, attr string, data []byte) fuse.Status { + return fuse.ENOSYS +} + +func (b *rawBridge) RemoveXAttr(header *fuse.InHeader, attr string) (code fuse.Status) { + return +} + +func (b *rawBridge) Open(input *fuse.OpenIn, out *fuse.OpenOut) (status fuse.Status) { + n, _ := b.inode(input.NodeId, 0) + // NOSUBMIT: what about the mode argument? + f, flags, code := n.node.Open(context.TODO(), input.Flags) + if !code.Ok() { + return code + } + + b.mu.Lock() + defer b.mu.Unlock() + + out.Fh = b.registerFile(f) + out.OpenFlags = flags + return fuse.OK +} + +func (b *rawBridge) registerFile(f File) uint64 { + var fh uint64 + if len(b.freeFiles) > 0 { + last := uint64(len(b.freeFiles) - 1) + fh = b.freeFiles[last] + b.freeFiles = b.freeFiles[:last] + } else { + fh = uint64(len(b.files)) + b.files = append(b.files, fileEntry{}) + } + + b.files[fh].file = f + return fh +} + +func (b *rawBridge) Read(input *fuse.ReadIn, buf []byte) (fuse.ReadResult, fuse.Status) { + n, f := b.inode(input.NodeId, input.Fh) + return n.node.Read(context.TODO(), f.file, buf, int64(input.Offset)) +} + +func (b *rawBridge) GetLk(input *fuse.LkIn, out *fuse.LkOut) (code fuse.Status) { + n, f := b.inode(input.NodeId, input.Fh) + return n.node.GetLk(context.TODO(), f.file, input.Owner, &input.Lk, input.LkFlags, &out.Lk) +} + +func (b *rawBridge) SetLk(input *fuse.LkIn) (code fuse.Status) { + n, f := b.inode(input.NodeId, input.Fh) + return n.node.SetLk(context.TODO(), f.file, input.Owner, &input.Lk, input.LkFlags) +} + +func (b *rawBridge) SetLkw(input *fuse.LkIn) (code fuse.Status) { + n, f := b.inode(input.NodeId, input.Fh) + return n.node.SetLkw(context.TODO(), f.file, input.Owner, &input.Lk, input.LkFlags) +} + +func (b *rawBridge) Release(input *fuse.ReleaseIn) { + n, f := b.inode(input.NodeId, input.Fh) + n.node.Release(context.TODO(), f.file) + + if input.Fh > 0 { + b.mu.Lock() + defer b.mu.Unlock() + b.files[input.Fh].file = nil + b.freeFiles = append(b.freeFiles, input.Fh) + } +} + +func (b *rawBridge) Write(input *fuse.WriteIn, data []byte) (written uint32, code fuse.Status) { + n, f := b.inode(input.NodeId, input.Fh) + return n.node.Write(context.TODO(), f.file, data, int64(input.Offset)) +} + +func (b *rawBridge) Flush(input *fuse.FlushIn) fuse.Status { + n, f := b.inode(input.NodeId, input.Fh) + return n.node.Flush(context.TODO(), f.file) +} + +func (b *rawBridge) Fsync(input *fuse.FsyncIn) (code fuse.Status) { + return fuse.ENOSYS +} + +func (b *rawBridge) Fallocate(input *fuse.FallocateIn) (code fuse.Status) { + n, f := b.inode(input.NodeId, input.Fh) + return n.node.Allocate(context.TODO(), f.file, input.Offset, input.Length, input.Mode) +} + +func (b *rawBridge) OpenDir(input *fuse.OpenIn, out *fuse.OpenOut) (status fuse.Status) { + return +} + +func (b *rawBridge) ReadDir(input *fuse.ReadIn, out *fuse.DirEntryList) fuse.Status { + return fuse.ENOSYS +} + +func (b *rawBridge) ReadDirPlus(input *fuse.ReadIn, out *fuse.DirEntryList) fuse.Status { + return fuse.ENOSYS +} + +func (b *rawBridge) ReleaseDir(input *fuse.ReleaseIn) { + return +} + +func (b *rawBridge) FsyncDir(input *fuse.FsyncIn) (code fuse.Status) { + return +} + +func (b *rawBridge) StatFs(input *fuse.InHeader, out *fuse.StatfsOut) (code fuse.Status) { + return +} +func (b *rawBridge) Init(*fuse.Server) { +} diff --git a/nodefs/default.go b/nodefs/default.go new file mode 100644 index 0000000000000000000000000000000000000000..16355b4af3804e49ce3853a67ca2ec9a50e5aefe --- /dev/null +++ b/nodefs/default.go @@ -0,0 +1,204 @@ +// 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" + "sync/atomic" + "time" + "unsafe" + + "github.com/hanwen/go-fuse/fuse" +) + +// DefaultNode provides common base Node functionality. +// +// It must be embedded in any Node implementation. +type DefaultNode struct { + inode_ *Inode +} + +// set/retrieve inode. +// +// node -> inode association, can be simultaneously tried to be set, if for e.g. +// +// root +// / \ +// dir1 dir2 +// \ / +// file +// +// dir1.Lookup("file") and dir2.Lookup("file") are executed simultaneously. +// +// We use atomics so that only one set can win +// +// To read node.inode atomic.LoadPointer is used, however it is not expensive +// since it translates to regular MOVQ on amd64. + +func (dn *DefaultNode) setInode(inode *Inode) bool { + return atomic.CompareAndSwapPointer( + (*unsafe.Pointer)(unsafe.Pointer(&dn.inode_)), + nil, unsafe.Pointer(inode)) +} + +func (dn *DefaultNode) inode() *Inode { + return (*Inode)(atomic.LoadPointer( + (*unsafe.Pointer)(unsafe.Pointer(&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 +} diff --git a/nodefs/files.go b/nodefs/files.go new file mode 100644 index 0000000000000000000000000000000000000000..7b28a8b2add5d3ed8e230bf3b8fe90334eff8bd6 --- /dev/null +++ b/nodefs/files.go @@ -0,0 +1,166 @@ +// 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 +} diff --git a/nodefs/files_linux.go b/nodefs/files_linux.go new file mode 100644 index 0000000000000000000000000000000000000000..dcf8699e77b021d85b1695ea86d0fe772fe5e511 --- /dev/null +++ b/nodefs/files_linux.go @@ -0,0 +1,36 @@ +// 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) +} diff --git a/nodefs/inode.go b/nodefs/inode.go new file mode 100644 index 0000000000000000000000000000000000000000..2c82a48d2fe2dd2160a6b313458a61d24821a3c2 --- /dev/null +++ b/nodefs/inode.go @@ -0,0 +1,333 @@ +// 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. + + // the following fields protected by bridge.mu + + // ID of the inode; 0 if inode was forgotten. + // forgotten inodes are unlinked from parent and children, but could be + // still not yet removed from bridge.nodes . + lookupCount uint64 + nodeID uint64 + + // mu protects the following mutable fields. When locking + // multiple Inodes, locks must be acquired using + // lockNodes/unlockNodes + mu sync.Mutex + + // changeCounter increments every time the below mutable state + // (lookupCount, nodeID, children, parents) is modified. + // + // This is used in places where we have to relock inode into inode + // group lock, and after locking the group we have to check if inode + // did not changed, and if it changed - retry the operation. + changeCounter uint32 + + children map[string]*Inode + parents map[parentData]struct{} +} + +// newInode creates creates new inode pointing to node. +// +// node -> inode association is NOT set. +// the inode is _not_ yet has +func newInode(node Node, mode uint32) *Inode { + inode := &Inode{ + mode: mode ^ 07777, + node: node, + parents: make(map[parentData]struct{}), + } + if mode&fuse.S_IFDIR != 0 { + inode.children = make(map[string]*Inode) + } + return inode +} + +// sortNodes rearranges inode group in consistent order. +// +// The nodes are ordered by their in-RAM address, which gives consistency +// property: for any A and B inodes, sortNodes will either always order A < B, +// or always order A > B. +// +// See lockNodes where this property is used to avoid deadlock when taking +// locks on inode group. +func sortNodes(ns []*Inode) { + sort.Slice(ns, func(i, j int) bool { + return uintptr(unsafe.Pointer(ns[i])) < uintptr(unsafe.Pointer(ns[j])) + }) +} + +// lockNodes locks group of inodes. +// +// It always lock the inodes in the same order - to avoid deadlocks. +// It also avoids locking an inode more than once, if it was specified multiple times. +// An example when an inode might be given multiple times is if dir/a and dir/b +// are hardlinked to the same inode and the caller needs to take locks on dir children. +// +// It is valid to give nil nodes - those are simply ignored. +func lockNodes(ns ...*Inode) { + sortNodes(ns) + + // The default value nil prevents trying to lock nil nodes. + var nprev *Inode + for _, n := range ns { + if n != nprev { + n.mu.Lock() + nprev = n + } + } +} + +// unlockNodes releases locks taken by lockNodes. +func unlockNodes(ns ...*Inode) { + // we don't need to unlock in the same order that was used in lockNodes. + // however it still helps to have nodes sorted to avoid duplicates. + sortNodes(ns) + + var nprev *Inode + for _, n := range ns { + if n != nprev { + n.mu.Unlock() + nprev = n + } + } +} + +// 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 +} + +// setEntry does `iparent[name] = ichild` linking. +// +// setEntry must not be called simultaneously for any of iparent or ichild. +// This, for example could be satisfied if both iparent and ichild are locked, +// but it could be also valid if only iparent is locked and ichild was just +// created and only one goroutine keeps referencing it. +// +// XXX also ichild.lookupCount++ ? +func (iparent *Inode) setEntry(name string, ichild *Inode) { + ichild.parents[parentData{name, iparent}] = struct{}{} + iparent.children[name] = ichild + ichild.changeCounter++ + iparent.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 + } + } + + // XXX not right - we cannot fully clear our children, because they can + // be also children of another directory. + // + // XXX also not right - the kernel can send FORGET(idir) but keep + // references to children inodes. + 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) + } + if node.setInode(ch) { + return ch + } + + return node.inode() +} diff --git a/nodefs/loopback.go b/nodefs/loopback.go new file mode 100644 index 0000000000000000000000000000000000000000..612016c2cc878a419233f87c1ab47ae1fb8b8977 --- /dev/null +++ b/nodefs/loopback.go @@ -0,0 +1,117 @@ +// 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 := InodeOf(n).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 := InodeOf(n).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 +} diff --git a/nodefs/simple_test.go b/nodefs/simple_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f897e373e2cffa3e24cca1d8a6de6c1e046c9ad5 --- /dev/null +++ b/nodefs/simple_test.go @@ -0,0 +1,218 @@ +// 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/ioutil" + "os" + "path/filepath" + "runtime" + "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 == "linux" { + 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) + } +}