From 8318020387fec43dc332f25dbdf98e2c6df85fa7 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Mon, 21 Nov 2016 20:52:32 +0000 Subject: [PATCH] Implement mount2 with go-fuse This passes the tests and works efficiently with the non sequential vfs ReadAt fix. --- cmd/all/all.go | 1 + cmd/mount2/file.go | 154 ++++++++++++ cmd/mount2/fs.go | 131 +++++++++++ cmd/mount2/mount.go | 277 ++++++++++++++++++++++ cmd/mount2/mount_test.go | 13 ++ cmd/mount2/mount_unsupported.go | 7 + cmd/mount2/node.go | 400 ++++++++++++++++++++++++++++++++ 7 files changed, 983 insertions(+) create mode 100644 cmd/mount2/file.go create mode 100644 cmd/mount2/fs.go create mode 100644 cmd/mount2/mount.go create mode 100644 cmd/mount2/mount_test.go create mode 100644 cmd/mount2/mount_unsupported.go create mode 100644 cmd/mount2/node.go diff --git a/cmd/all/all.go b/cmd/all/all.go index 00108cdfe..894e04b1c 100644 --- a/cmd/all/all.go +++ b/cmd/all/all.go @@ -36,6 +36,7 @@ import ( _ "github.com/rclone/rclone/cmd/memtest" _ "github.com/rclone/rclone/cmd/mkdir" _ "github.com/rclone/rclone/cmd/mount" + _ "github.com/rclone/rclone/cmd/mount2" _ "github.com/rclone/rclone/cmd/move" _ "github.com/rclone/rclone/cmd/moveto" _ "github.com/rclone/rclone/cmd/ncdu" diff --git a/cmd/mount2/file.go b/cmd/mount2/file.go new file mode 100644 index 000000000..d460c7b73 --- /dev/null +++ b/cmd/mount2/file.go @@ -0,0 +1,154 @@ +// +build linux darwin,amd64 + +package mount2 + +import ( + "context" + "fmt" + "io" + "os" + "syscall" + + fusefs "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/rclone/rclone/fs/log" + "github.com/rclone/rclone/vfs" +) + +// FileHandle is a resource identifier for opened files. Usually, a +// FileHandle should implement some of the FileXxxx interfaces. +// +// All of the FileXxxx operations can also be implemented at the +// InodeEmbedder level, for example, one can implement NodeReader +// instead of FileReader. +// +// FileHandles are useful in two cases: First, if the underlying +// storage systems needs a handle for reading/writing. This is the +// case with Unix system calls, which need a file descriptor (See also +// the function `NewLoopbackFile`). Second, it is useful for +// implementing files whose contents are not tied to an inode. For +// example, a file like `/proc/interrupts` has no fixed content, but +// changes on each open call. This means that each file handle must +// have its own view of the content; this view can be tied to a +// FileHandle. Files that have such dynamic content should return the +// FOPEN_DIRECT_IO flag from their `Open` method. See directio_test.go +// for an example. +type FileHandle struct { + h vfs.Handle +} + +// Create a new FileHandle +func newFileHandle(h vfs.Handle) *FileHandle { + return &FileHandle{ + h: h, + } +} + +// Check interface satistfied +var _ fusefs.FileHandle = (*FileHandle)(nil) + +// The String method is for debug printing. +func (f *FileHandle) String() string { + return fmt.Sprintf("fh=%p(%s)", f, f.h.Node().Path()) +} + +// Read data from a file. The data should be returned as +// ReadResult, which may be constructed from the incoming +// `dest` buffer. +func (f *FileHandle) Read(ctx context.Context, dest []byte, off int64) (res fuse.ReadResult, errno syscall.Errno) { + var n int + var err error + defer log.Trace(f, "off=%d", off)("n=%d, off=%d, errno=%v", &n, &off, &errno) + n, err = f.h.ReadAt(dest, off) + if err == io.EOF { + err = nil + } + return fuse.ReadResultData(dest[:n]), translateError(err) +} + +var _ fusefs.FileReader = (*FileHandle)(nil) + +// Write the data into the file handle at given offset. After +// returning, the data will be reused and may not referenced. +func (f *FileHandle) Write(ctx context.Context, data []byte, off int64) (written uint32, errno syscall.Errno) { + var n int + var err error + defer log.Trace(f, "off=%d", off)("n=%d, off=%d, errno=%v", &n, &off, &errno) + if f.h.Node().VFS().Opt.CacheMode < vfs.CacheModeWrites || f.h.Node().Mode()&os.ModeAppend == 0 { + n, err = f.h.WriteAt(data, off) + } else { + n, err = f.h.Write(data) + } + return uint32(n), translateError(err) +} + +var _ fusefs.FileWriter = (*FileHandle)(nil) + +// Flush is called for the close(2) call on a file descriptor. In case +// of a descriptor that was duplicated using dup(2), it may be called +// more than once for the same FileHandle. +func (f *FileHandle) Flush(ctx context.Context) syscall.Errno { + return translateError(f.h.Flush()) +} + +var _ fusefs.FileFlusher = (*FileHandle)(nil) + +// Release is called to before a FileHandle is forgotten. The +// kernel ignores the return value of this method, +// so any cleanup that requires specific synchronization or +// could fail with I/O errors should happen in Flush instead. +func (f *FileHandle) Release(ctx context.Context) syscall.Errno { + return translateError(f.h.Release()) +} + +var _ fusefs.FileReleaser = (*FileHandle)(nil) + +// Fsync is a signal to ensure writes to the Inode are flushed +// to stable storage. +func (f *FileHandle) Fsync(ctx context.Context, flags uint32) (errno syscall.Errno) { + return translateError(f.h.Sync()) +} + +var _ fusefs.FileFsyncer = (*FileHandle)(nil) + +// Getattr reads attributes for an Inode. The library will ensure that +// Mode and Ino are set correctly. For files that are not opened with +// FOPEN_DIRECTIO, Size should be set so it can be read correctly. If +// returning zeroed permissions, the default behavior is to change the +// mode of 0755 (directory) or 0644 (files). This can be switched off +// with the Options.NullPermissions setting. If blksize is unset, 4096 +// is assumed, and the 'blocks' field is set accordingly. +func (f *FileHandle) Getattr(ctx context.Context, out *fuse.AttrOut) (errno syscall.Errno) { + defer log.Trace(f, "")("attr=%v, errno=%v", &out, &errno) + setAttrOut(f.h.Node(), out) + return 0 +} + +var _ fusefs.FileGetattrer = (*FileHandle)(nil) + +// Setattr sets attributes for an Inode. +func (f *FileHandle) Setattr(ctx context.Context, in *fuse.SetAttrIn, out *fuse.AttrOut) (errno syscall.Errno) { + defer log.Trace(f, "in=%v", in)("attr=%v, errno=%v", &out, &errno) + var err error + setAttrOut(f.h.Node(), out) + size, ok := in.GetSize() + if ok { + err = f.h.Truncate(int64(size)) + if err != nil { + return translateError(err) + } + out.Attr.Size = size + } + mtime, ok := in.GetMTime() + if ok { + err = f.h.Node().SetModTime(mtime) + if err != nil { + return translateError(err) + } + out.Attr.Mtime = uint64(mtime.Unix()) + out.Attr.Mtimensec = uint32(mtime.Nanosecond()) + } + return 0 +} + +var _ fusefs.FileSetattrer = (*FileHandle)(nil) diff --git a/cmd/mount2/fs.go b/cmd/mount2/fs.go new file mode 100644 index 000000000..f672b48b5 --- /dev/null +++ b/cmd/mount2/fs.go @@ -0,0 +1,131 @@ +// FUSE main Fs + +// +build linux darwin,amd64 + +package mount2 + +import ( + "os" + "syscall" + + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/pkg/errors" + "github.com/rclone/rclone/cmd/mountlib" + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/log" + "github.com/rclone/rclone/vfs" + "github.com/rclone/rclone/vfs/vfsflags" +) + +// FS represents the top level filing system +type FS struct { + VFS *vfs.VFS + f fs.Fs +} + +// NewFS creates a pathfs.FileSystem from the fs.Fs passed in +func NewFS(f fs.Fs) *FS { + fsys := &FS{ + VFS: vfs.New(f, &vfsflags.Opt), + f: f, + } + return fsys +} + +// Root returns the root node +func (f *FS) Root() (node *Node, err error) { + defer log.Trace("", "")("node=%+v, err=%v", &node, &err) + root, err := f.VFS.Root() + if err != nil { + return nil, err + } + return newNode(f, root), nil +} + +// SetDebug if called, provide debug output through the log package. +func (f *FS) SetDebug(debug bool) { + fs.Debugf(f.f, "SetDebug %v", debug) +} + +// get the Mode from a vfs Node +func getMode(node os.FileInfo) uint32 { + Mode := node.Mode().Perm() + if node.IsDir() { + Mode |= fuse.S_IFDIR + } else { + Mode |= fuse.S_IFREG + } + return uint32(Mode) +} + +// fill in attr from node +func setAttr(node vfs.Node, attr *fuse.Attr) { + Size := uint64(node.Size()) + const BlockSize = 512 + Blocks := (Size + BlockSize - 1) / BlockSize + modTime := node.ModTime() + // set attributes + vfs := node.VFS() + attr.Owner.Gid = vfs.Opt.UID + attr.Owner.Uid = vfs.Opt.GID + attr.Mode = getMode(node) + attr.Size = Size + attr.Nlink = 1 + attr.Blocks = Blocks + // attr.Blksize = BlockSize // not supported in freebsd/darwin, defaults to 4k if not set + s := uint64(modTime.Unix()) + ns := uint32(modTime.Nanosecond()) + attr.Atime = s + attr.Atimensec = ns + attr.Mtime = s + attr.Mtimensec = ns + attr.Ctime = s + attr.Ctimensec = ns + //attr.Rdev +} + +// fill in AttrOut from node +func setAttrOut(node vfs.Node, out *fuse.AttrOut) { + setAttr(node, &out.Attr) + out.SetTimeout(mountlib.AttrTimeout) +} + +// fill in EntryOut from node +func setEntryOut(node vfs.Node, out *fuse.EntryOut) { + setAttr(node, &out.Attr) + out.SetEntryTimeout(mountlib.AttrTimeout) + out.SetAttrTimeout(mountlib.AttrTimeout) +} + +// Translate errors from mountlib into Syscall error numbers +func translateError(err error) syscall.Errno { + if err == nil { + return 0 + } + switch errors.Cause(err) { + case vfs.OK: + return 0 + case vfs.ENOENT: + return syscall.ENOENT + case vfs.EEXIST: + return syscall.EEXIST + case vfs.EPERM: + return syscall.EPERM + case vfs.ECLOSED: + return syscall.EBADF + case vfs.ENOTEMPTY: + return syscall.ENOTEMPTY + case vfs.ESPIPE: + return syscall.ESPIPE + case vfs.EBADF: + return syscall.EBADF + case vfs.EROFS: + return syscall.EROFS + case vfs.ENOSYS: + return syscall.ENOSYS + case vfs.EINVAL: + return syscall.EINVAL + } + fs.Errorf(nil, "IO error: %v", err) + return syscall.EIO +} diff --git a/cmd/mount2/mount.go b/cmd/mount2/mount.go new file mode 100644 index 000000000..fc6362639 --- /dev/null +++ b/cmd/mount2/mount.go @@ -0,0 +1,277 @@ +// Package mount implents a FUSE mounting system for rclone remotes. + +// +build linux darwin,amd64 + +package mount2 + +import ( + "fmt" + "log" + "os" + "os/signal" + "runtime" + "syscall" + + fusefs "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/okzk/sdnotify" + "github.com/pkg/errors" + "github.com/rclone/rclone/cmd/mountlib" + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/lib/atexit" + "github.com/rclone/rclone/vfs" +) + +func init() { + mountlib.NewMountCommand("mount2", Mount) +} + +// mountOptions configures the options from the command line flags +// +// man mount.fuse for more info and note the -o flag for other options +func mountOptions(fsys *FS, f fs.Fs) (mountOpts *fuse.MountOptions) { + device := f.Name() + ":" + f.Root() + mountOpts = &fuse.MountOptions{ + AllowOther: mountlib.AllowOther, + FsName: device, + Name: "rclone", + DisableXAttrs: true, + Debug: mountlib.DebugFUSE, + MaxReadAhead: int(mountlib.MaxReadAhead), + + // RememberInodes: true, + // SingleThreaded: true, + + /* + AllowOther bool + + // Options are passed as -o string to fusermount. + Options []string + + // Default is _DEFAULT_BACKGROUND_TASKS, 12. This numbers + // controls the allowed number of requests that relate to + // async I/O. Concurrency for synchronous I/O is not limited. + MaxBackground int + + // Write size to use. If 0, use default. This number is + // capped at the kernel maximum. + MaxWrite int + + // Max read ahead to use. If 0, use default. This number is + // capped at the kernel maximum. + MaxReadAhead int + + // If IgnoreSecurityLabels is set, all security related xattr + // requests will return NO_DATA without passing through the + // user defined filesystem. You should only set this if you + // file system implements extended attributes, and you are not + // interested in security labels. + IgnoreSecurityLabels bool // ignoring labels should be provided as a fusermount mount option. + + // If RememberInodes is set, we will never forget inodes. + // This may be useful for NFS. + RememberInodes bool + + // Values shown in "df -T" and friends + // First column, "Filesystem" + FsName string + + // Second column, "Type", will be shown as "fuse." + Name + Name string + + // If set, wrap the file system in a single-threaded locking wrapper. + SingleThreaded bool + + // If set, return ENOSYS for Getxattr calls, so the kernel does not issue any + // Xattr operations at all. + DisableXAttrs bool + + // If set, print debugging information. + Debug bool + + // If set, ask kernel to forward file locks to FUSE. If using, + // you must implement the GetLk/SetLk/SetLkw methods. + EnableLocks bool + + // If set, ask kernel not to do automatic data cache invalidation. + // The filesystem is fully responsible for invalidating data cache. + ExplicitDataCacheControl bool + */ + + } + var opts []string + // FIXME doesn't work opts = append(opts, fmt.Sprintf("max_readahead=%d", maxReadAhead)) + if mountlib.AllowNonEmpty { + opts = append(opts, "nonempty") + } + if mountlib.AllowOther { + opts = append(opts, "allow_other") + } + if mountlib.AllowRoot { + opts = append(opts, "allow_root") + } + if mountlib.DefaultPermissions { + opts = append(opts, "default_permissions") + } + if fsys.VFS.Opt.ReadOnly { + opts = append(opts, "ro") + } + if mountlib.WritebackCache { + log.Printf("FIXME --write-back-cache not supported") + // FIXME opts = append(opts,fuse.WritebackCache()) + } + // Some OS X only options + if runtime.GOOS == "darwin" { + opts = append(opts, + // VolumeName sets the volume name shown in Finder. + fmt.Sprintf("volname=%s", device), + + // NoAppleXattr makes OSXFUSE disallow extended attributes with the + // prefix "com.apple.". This disables persistent Finder state and + // other such information. + "noapplexattr", + + // NoAppleDouble makes OSXFUSE disallow files with names used by OS X + // to store extended attributes on file systems that do not support + // them natively. + // + // Such file names are: + // + // ._* + // .DS_Store + "noappledouble", + ) + } + mountOpts.Options = opts + return mountOpts +} + +// mount the file system +// +// The mount point will be ready when this returns. +// +// returns an error, and an error channel for the serve process to +// report an error when fusermount is called. +func mount(f fs.Fs, mountpoint string) (*vfs.VFS, <-chan error, func() error, error) { + fs.Debugf(f, "Mounting on %q", mountpoint) + + fsys := NewFS(f) + // nodeFsOpts := &fusefs.PathNodeFsOptions{ + // ClientInodes: false, + // Debug: mountlib.DebugFUSE, + // } + // nodeFs := fusefs.NewPathNodeFs(fsys, nodeFsOpts) + + //mOpts := fusefs.NewOptions() // default options + // FIXME + // mOpts.EntryTimeout = 10 * time.Second + // mOpts.AttrTimeout = 10 * time.Second + // mOpts.NegativeTimeout = 10 * time.Second + //mOpts.Debug = mountlib.DebugFUSE + + //conn := fusefs.NewFileSystemConnector(nodeFs.Root(), mOpts) + mountOpts := mountOptions(fsys, f) + + // FIXME fill out + opts := fusefs.Options{ + MountOptions: *mountOpts, + EntryTimeout: &mountlib.AttrTimeout, + AttrTimeout: &mountlib.AttrTimeout, + // UID + // GID + } + + root, err := fsys.Root() + if err != nil { + return nil, nil, nil, err + } + + rawFS := fusefs.NewNodeFS(root, &opts) + server, err := fuse.NewServer(rawFS, mountpoint, &opts.MountOptions) + if err != nil { + return nil, nil, nil, err + } + + //mountOpts := &fuse.MountOptions{} + //server, err := fusefs.Mount(mountpoint, fsys, &opts) + // server, err := fusefs.Mount(mountpoint, root, &opts) + // if err != nil { + // return nil, nil, nil, err + // } + + umount := func() error { + // Shutdown the VFS + fsys.VFS.Shutdown() + return server.Unmount() + } + + // serverSettings := server.KernelSettings() + // fs.Debugf(f, "Server settings %+v", serverSettings) + + // Serve the mount point in the background returning error to errChan + errs := make(chan error, 1) + go func() { + server.Serve() + errs <- nil + }() + + fs.Debugf(f, "Waiting for the mount to start...") + err = server.WaitMount() + if err != nil { + return nil, nil, nil, err + } + + fs.Debugf(f, "Mount started") + return fsys.VFS, errs, umount, nil +} + +// Mount mounts the remote at mountpoint. +// +// If noModTime is set then it +func Mount(f fs.Fs, mountpoint string) error { + // Mount it + vfs, errChan, unmount, err := mount(f, mountpoint) + if err != nil { + return errors.Wrap(err, "failed to mount FUSE fs") + } + + sigInt := make(chan os.Signal, 1) + signal.Notify(sigInt, syscall.SIGINT, syscall.SIGTERM) + sigHup := make(chan os.Signal, 1) + signal.Notify(sigHup, syscall.SIGHUP) + atexit.Register(func() { + _ = unmount() + }) + + if err := sdnotify.Ready(); err != nil && err != sdnotify.ErrSdNotifyNoSocket { + return errors.Wrap(err, "failed to notify systemd") + } + +waitloop: + for { + select { + // umount triggered outside the app + case err = <-errChan: + break waitloop + // Program abort: umount + case <-sigInt: + err = unmount() + break waitloop + // user sent SIGHUP to clear the cache + case <-sigHup: + root, err := vfs.Root() + if err != nil { + fs.Errorf(f, "Error reading root: %v", err) + } else { + root.ForgetAll() + } + } + } + + _ = sdnotify.Stopping() + if err != nil { + return errors.Wrap(err, "failed to umount FUSE fs") + } + + return nil +} diff --git a/cmd/mount2/mount_test.go b/cmd/mount2/mount_test.go new file mode 100644 index 000000000..32bd83b7d --- /dev/null +++ b/cmd/mount2/mount_test.go @@ -0,0 +1,13 @@ +// +build linux darwin,amd64 + +package mount2 + +import ( + "testing" + + "github.com/rclone/rclone/cmd/mountlib/mounttest" +) + +func TestMount(t *testing.T) { + mounttest.RunTests(t, mount) +} diff --git a/cmd/mount2/mount_unsupported.go b/cmd/mount2/mount_unsupported.go new file mode 100644 index 000000000..7adf1e72f --- /dev/null +++ b/cmd/mount2/mount_unsupported.go @@ -0,0 +1,7 @@ +// Build for mount for unsupported platforms to stop go complaining +// about "no buildable Go source files " + +// +build !linux +// +build !darwin !amd64 + +package mount2 diff --git a/cmd/mount2/node.go b/cmd/mount2/node.go new file mode 100644 index 000000000..3f00cdcec --- /dev/null +++ b/cmd/mount2/node.go @@ -0,0 +1,400 @@ +// +build linux darwin,amd64 + +package mount2 + +import ( + "context" + "os" + "path" + "syscall" + + fusefs "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/rclone/rclone/cmd/mountlib" + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/log" + "github.com/rclone/rclone/vfs" +) + +// Node represents a directory or file +type Node struct { + fusefs.Inode + node vfs.Node + fsys *FS +} + +// Node types must be InodeEmbedders +var _ fusefs.InodeEmbedder = (*Node)(nil) + +// newNode creates a new fusefs.Node from a vfs Node +func newNode(fsys *FS, node vfs.Node) *Node { + return &Node{ + node: node, + fsys: fsys, + } +} + +// String used for pretty printing. +func (n *Node) String() string { + return n.node.Path() +} + +// lookup a Node in a directory +func (n *Node) lookupVfsNodeInDir(leaf string) (vfsNode vfs.Node, errno syscall.Errno) { + dir, ok := n.node.(*vfs.Dir) + if !ok { + return nil, syscall.ENOTDIR + } + vfsNode, err := dir.Stat(leaf) + return vfsNode, translateError(err) +} + +// // lookup a Dir given a path +// func (n *Node) lookupDir(path string) (dir *vfs.Dir, code fuse.Status) { +// node, code := fsys.lookupVfsNodeInDir(path) +// if !code.Ok() { +// return nil, code +// } +// dir, ok := n.(*vfs.Dir) +// if !ok { +// return nil, fuse.ENOTDIR +// } +// return dir, fuse.OK +// } + +// // lookup a parent Dir given a path returning the dir and the leaf +// func (n *Node) lookupParentDir(filePath string) (leaf string, dir *vfs.Dir, code fuse.Status) { +// parentDir, leaf := path.Split(filePath) +// dir, code = fsys.lookupDir(parentDir) +// return leaf, dir, code +// } + +// Statfs implements statistics for the filesystem that holds this +// Inode. If not defined, the `out` argument will zeroed with an OK +// result. This is because OSX filesystems must Statfs, or the mount +// will not work. +func (n *Node) Statfs(ctx context.Context, out *fuse.StatfsOut) syscall.Errno { + defer log.Trace(n, "")("out=%+v", &out) + out = new(fuse.StatfsOut) + const blockSize = 4096 + const fsBlocks = (1 << 50) / blockSize + out.Blocks = fsBlocks // Total data blocks in file system. + out.Bfree = fsBlocks // Free blocks in file system. + out.Bavail = fsBlocks // Free blocks in file system if you're not root. + out.Files = 1E9 // Total files in file system. + out.Ffree = 1E9 // Free files in file system. + out.Bsize = blockSize // Block size + out.NameLen = 255 // Maximum file name length? + out.Frsize = blockSize // Fragment size, smallest addressable data size in the file system. + mountlib.ClipBlocks(&out.Blocks) + mountlib.ClipBlocks(&out.Bfree) + mountlib.ClipBlocks(&out.Bavail) + return 0 +} + +var _ = (fusefs.NodeStatfser)((*Node)(nil)) + +// Getattr reads attributes for an Inode. The library will ensure that +// Mode and Ino are set correctly. For files that are not opened with +// FOPEN_DIRECTIO, Size should be set so it can be read correctly. If +// returning zeroed permissions, the default behavior is to change the +// mode of 0755 (directory) or 0644 (files). This can be switched off +// with the Options.NullPermissions setting. If blksize is unset, 4096 +// is assumed, and the 'blocks' field is set accordingly. +func (n *Node) Getattr(ctx context.Context, f fusefs.FileHandle, out *fuse.AttrOut) syscall.Errno { + setAttrOut(n.node, out) + return 0 +} + +var _ = (fusefs.NodeGetattrer)((*Node)(nil)) + +// Setattr sets attributes for an Inode. +func (n *Node) Setattr(ctx context.Context, f fusefs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) (errno syscall.Errno) { + defer log.Trace(n, "in=%v", in)("out=%#v, errno=%v", &out, &errno) + var err error + setAttrOut(n.node, out) + size, ok := in.GetSize() + if ok { + err = n.node.Truncate(int64(size)) + if err != nil { + return translateError(err) + } + out.Attr.Size = size + } + mtime, ok := in.GetMTime() + if ok { + err = n.node.SetModTime(mtime) + if err != nil { + return translateError(err) + } + out.Attr.Mtime = uint64(mtime.Unix()) + out.Attr.Mtimensec = uint32(mtime.Nanosecond()) + } + return 0 +} + +var _ = (fusefs.NodeSetattrer)((*Node)(nil)) + +// Open opens an Inode (of regular file type) for reading. It +// is optional but recommended to return a FileHandle. +func (n *Node) Open(ctx context.Context, flags uint32) (fh fusefs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + defer log.Trace(n, "flags=%#o", flags)("errno=%v", &errno) + // fuse flags are based off syscall flags as are os flags, so + // should be compatible + handle, err := n.node.Open(int(flags)) + if err != nil { + return nil, 0, translateError(err) + } + // If size unknown then use direct io to read + if entry := n.node.DirEntry(); entry != nil && entry.Size() < 0 { + fuseFlags |= fuse.FOPEN_DIRECT_IO + } + return newFileHandle(handle), fuseFlags, 0 +} + +var _ = (fusefs.NodeOpener)((*Node)(nil)) + +// Lookup should find a direct child of a directory by the child's name. If +// the entry does not exist, it should return ENOENT and optionally +// set a NegativeTimeout in `out`. If it does exist, it should return +// attribute data in `out` and return the Inode for the child. A new +// inode can be created using `Inode.NewInode`. The new Inode will be +// added to the FS tree automatically if the return status is OK. +// +// If a directory does not implement NodeLookuper, the library looks +// for an existing child with the given name. +// +// The input to a Lookup is {parent directory, name string}. +// +// Lookup, if successful, must return an *Inode. Once the Inode is +// returned to the kernel, the kernel can issue further operations, +// such as Open or Getxattr on that node. +// +// A successful Lookup also returns an EntryOut. Among others, this +// contains file attributes (mode, size, mtime, etc.). +// +// FUSE supports other operations that modify the namespace. For +// example, the Symlink, Create, Mknod, Link methods all create new +// children in directories. Hence, they also return *Inode and must +// populate their fuse.EntryOut arguments. +func (n *Node) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (inode *fusefs.Inode, errno syscall.Errno) { + defer log.Trace(n, "name=%q", name)("inode=%v, attr=%v, errno=%v", &inode, &out, &errno) + vfsNode, errno := n.lookupVfsNodeInDir(name) + if errno != 0 { + return nil, errno + } + newNode := &Node{ + node: vfsNode, + fsys: n.fsys, + } + + // FIXME + // out.SetEntryTimeout(dt time.Duration) + // out.SetAttrTimeout(dt time.Duration) + setEntryOut(vfsNode, out) + + return n.NewInode(ctx, newNode, fusefs.StableAttr{Mode: out.Attr.Mode}), 0 +} + +var _ = (fusefs.NodeLookuper)((*Node)(nil)) + +// Opendir opens a directory Inode for reading its +// contents. The actual reading is driven from Readdir, so +// this method is just for performing sanity/permission +// checks. The default is to return success. +func (n *Node) Opendir(ctx context.Context) syscall.Errno { + if !n.node.IsDir() { + return syscall.ENOTDIR + } + return 0 +} + +var _ = (fusefs.NodeOpendirer)((*Node)(nil)) + +type dirStream struct { + nodes []os.FileInfo + i int +} + +// HasNext indicates if there are further entries. HasNext +// might be called on already closed streams. +func (ds *dirStream) HasNext() bool { + return ds.i < len(ds.nodes) +} + +// Next retrieves the next entry. It is only called if HasNext +// has previously returned true. The Errno return may be used to +// indicate I/O errors +func (ds *dirStream) Next() (de fuse.DirEntry, errno syscall.Errno) { + // defer log.Trace(nil, "")("de=%+v, errno=%v", &de, &errno) + fi := ds.nodes[ds.i] + de = fuse.DirEntry{ + // Mode is the file's mode. Only the high bits (eg. S_IFDIR) + // are considered. + Mode: getMode(fi), + + // Name is the basename of the file in the directory. + Name: path.Base(fi.Name()), + + // Ino is the inode number. + Ino: 0, // FIXME + } + ds.i++ + return de, 0 +} + +// Close releases resources related to this directory +// stream. +func (ds *dirStream) Close() { +} + +var _ fusefs.DirStream = (*dirStream)(nil) + +// Readdir opens a stream of directory entries. +// +// Readdir essentiallly returns a list of strings, and it is allowed +// for Readdir to return different results from Lookup. For example, +// you can return nothing for Readdir ("ls my-fuse-mount" is empty), +// while still implementing Lookup ("ls my-fuse-mount/a-specific-file" +// shows a single file). +// +// If a directory does not implement NodeReaddirer, a list of +// currently known children from the tree is returned. This means that +// static in-memory file systems need not implement NodeReaddirer. +func (n *Node) Readdir(ctx context.Context) (ds fusefs.DirStream, errno syscall.Errno) { + defer log.Trace(n, "")("ds=%v, errno=%v", &ds, &errno) + if !n.node.IsDir() { + return nil, syscall.ENOTDIR + } + fh, err := n.node.Open(os.O_RDONLY) + if err != nil { + return nil, translateError(err) + } + defer func() { + closeErr := fh.Close() + if errno == 0 && closeErr != nil { + errno = translateError(closeErr) + } + }() + items, err := fh.Readdir(-1) + if err != nil { + return nil, translateError(err) + } + return &dirStream{ + nodes: items, + }, 0 +} + +var _ = (fusefs.NodeReaddirer)((*Node)(nil)) + +// Mkdir is similar to Lookup, but must create a directory entry and Inode. +// Default is to return EROFS. +func (n *Node) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (inode *fusefs.Inode, errno syscall.Errno) { + defer log.Trace(name, "mode=0%o", mode)("inode=%v, errno=%v", &inode, &errno) + dir, ok := n.node.(*vfs.Dir) + if !ok { + return nil, syscall.ENOTDIR + } + newDir, err := dir.Mkdir(name) + if err != nil { + return nil, translateError(err) + } + newNode := newNode(n.fsys, newDir) + setEntryOut(newNode.node, out) + newInode := n.NewInode(ctx, newNode, fusefs.StableAttr{Mode: out.Attr.Mode}) + return newInode, 0 +} + +var _ = (fusefs.NodeMkdirer)((*Node)(nil)) + +// Create is similar to Lookup, but should create a new +// child. It typically also returns a FileHandle as a +// reference for future reads/writes. +// Default is to return EROFS. +func (n *Node) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (node *fusefs.Inode, fh fusefs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + defer log.Trace(n, "name=%q, flags=%#o, mode=%#o", name, flags, mode)("node=%v, fh=%v, flags=%#o, errno=%v", &node, &fh, &fuseFlags, &errno) + dir, ok := n.node.(*vfs.Dir) + if !ok { + return nil, nil, 0, syscall.ENOTDIR + } + // translate the fuse flags to os flags + osFlags := int(flags) | os.O_CREATE + file, err := dir.Create(name, osFlags) + if err != nil { + return nil, nil, 0, translateError(err) + } + handle, err := file.Open(osFlags) + if err != nil { + return nil, nil, 0, translateError(err) + } + fh = newFileHandle(handle) + // FIXME + // fh = &fusefs.WithFlags{ + // File: fh, + // //FuseFlags: fuse.FOPEN_NONSEEKABLE, + // OpenFlags: flags, + // } + + // Find the created node + vfsNode, errno := n.lookupVfsNodeInDir(name) + if errno != 0 { + return nil, nil, 0, errno + } + setEntryOut(vfsNode, out) + newNode := newNode(n.fsys, vfsNode) + fs.Debugf(nil, "attr=%#v", out.Attr) + newInode := n.NewInode(ctx, newNode, fusefs.StableAttr{Mode: out.Attr.Mode}) + return newInode, fh, 0, 0 +} + +var _ = (fusefs.NodeCreater)((*Node)(nil)) + +// Unlink should remove a child from this directory. If the +// return status is OK, the Inode is removed as child in the +// FS tree automatically. Default is to return EROFS. +func (n *Node) Unlink(ctx context.Context, name string) (errno syscall.Errno) { + defer log.Trace(n, "name=%q", name)("errno=%v", &errno) + vfsNode, errno := n.lookupVfsNodeInDir(name) + if errno != 0 { + return errno + } + return translateError(vfsNode.Remove()) +} + +var _ = (fusefs.NodeUnlinker)((*Node)(nil)) + +// Rmdir is like Unlink but for directories. +// Default is to return EROFS. +func (n *Node) Rmdir(ctx context.Context, name string) (errno syscall.Errno) { + defer log.Trace(n, "name=%q", name)("errno=%v", &errno) + vfsNode, errno := n.lookupVfsNodeInDir(name) + if errno != 0 { + return errno + } + return translateError(vfsNode.Remove()) +} + +var _ = (fusefs.NodeRmdirer)((*Node)(nil)) + +// Rename should move a child from one directory to a different +// one. The change is effected in the FS tree if the return status is +// OK. Default is to return EROFS. +func (n *Node) Rename(ctx context.Context, oldName string, newParent fusefs.InodeEmbedder, newName string, flags uint32) (errno syscall.Errno) { + defer log.Trace(n, "oldName=%q, newParent=%v, newName=%q", oldName, newParent, newName)("errno=%v", &errno) + oldDir, ok := n.node.(*vfs.Dir) + if !ok { + return syscall.ENOTDIR + } + newParentNode, ok := newParent.(*Node) + if !ok { + fs.Errorf(n, "newParent was not a *Node") + return syscall.EIO + } + newDir, ok := newParentNode.node.(*vfs.Dir) + if !ok { + return syscall.ENOTDIR + } + return translateError(oldDir.Rename(oldName, newName, newDir)) +} + +var _ = (fusefs.NodeRenamer)((*Node)(nil))