Use new FUSE API

Change-Id: I6b42407d92304fdd6ca14611235904bc04e62847
diff --git a/cmd/slothfs-gitilesfs/main.go b/cmd/slothfs-gitilesfs/main.go
index 2e9bfa1..30b3feb 100644
--- a/cmd/slothfs-gitilesfs/main.go
+++ b/cmd/slothfs-gitilesfs/main.go
@@ -24,7 +24,7 @@
 	"github.com/google/slothfs/cache"
 	"github.com/google/slothfs/fs"
 	"github.com/google/slothfs/gitiles"
-	"github.com/hanwen/go-fuse/fuse/nodefs"
+	fusefs "github.com/hanwen/go-fuse/fs"
 )
 
 func main() {
@@ -64,12 +64,15 @@
 	}
 
 	root := fs.NewGitilesConfigFSRoot(cache, repoService, &opts)
-	server, _, err := nodefs.MountRoot(mntDir, root, &nodefs.Options{
-		EntryTimeout:    time.Hour,
-		NegativeTimeout: time.Hour,
-		AttrTimeout:     time.Hour,
-		Debug:           *debug,
-	})
+	h := time.Hour
+	fuseOpts := &fusefs.Options{
+		EntryTimeout:    &h,
+		NegativeTimeout: &h,
+		AttrTimeout:     &h,
+	}
+	fuseOpts.Debug = *debug
+
+	server, err := fusefs.Mount(mntDir, root, fuseOpts)
 	if err != nil {
 		log.Fatalf("MountFileSystem: %v", err)
 	}
diff --git a/cmd/slothfs-hostfs/main.go b/cmd/slothfs-hostfs/main.go
index 68a80b6..1fa0436 100644
--- a/cmd/slothfs-hostfs/main.go
+++ b/cmd/slothfs-hostfs/main.go
@@ -24,7 +24,7 @@
 	"github.com/google/slothfs/cache"
 	"github.com/google/slothfs/fs"
 	"github.com/google/slothfs/gitiles"
-	"github.com/hanwen/go-fuse/fuse/nodefs"
+	fusefs "github.com/hanwen/go-fuse/fs"
 )
 
 func main() {
@@ -57,12 +57,14 @@
 		log.Fatalf("NewService: %v", err)
 	}
 
-	server, _, err := nodefs.MountRoot(mntDir, root, &nodefs.Options{
-		EntryTimeout:    time.Hour,
-		NegativeTimeout: time.Hour,
-		AttrTimeout:     time.Hour,
-		Debug:           *debug,
-	})
+	h := time.Hour
+	fuseOpts := &fusefs.Options{
+		EntryTimeout:    &h,
+		NegativeTimeout: &h,
+		AttrTimeout:     &h,
+	}
+	fuseOpts.Debug = *debug
+	server, err := fusefs.Mount(mntDir, root, fuseOpts)
 	if err != nil {
 		log.Fatalf("MountFileSystem: %v", err)
 	}
diff --git a/fs/fixture_test.go b/fs/fixture_test.go
index 92d41f7..0a1cdbc 100644
--- a/fs/fixture_test.go
+++ b/fs/fixture_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 Google Inc. All rights reserved.
+// Copyright 2019 Google Inc. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -23,8 +23,8 @@
 
 	"github.com/google/slothfs/cache"
 	"github.com/google/slothfs/gitiles"
+	"github.com/hanwen/go-fuse/fs"
 	"github.com/hanwen/go-fuse/fuse"
-	"github.com/hanwen/go-fuse/fuse/nodefs"
 )
 
 type testFixture struct {
@@ -34,7 +34,7 @@
 	cache      *cache.Cache
 	testServer *testServer
 	service    *gitiles.Service
-	root       nodefs.Node
+	root       fs.InodeEmbedder
 }
 
 func (f *testFixture) cleanup() {
@@ -52,52 +52,45 @@
 	if err != nil {
 		return nil, err
 	}
-
 	fixture := &testFixture{dir: d}
-
 	fixture.cache, err = cache.NewCache(filepath.Join(d, "/cache"), cache.Options{})
 	if err != nil {
 		return nil, err
 	}
-
 	fixture.testServer, err = newTestServer()
 	if err != nil {
 		return nil, err
 	}
-
 	fixture.service, err = gitiles.NewService(gitiles.Options{
 		Address: fmt.Sprintf("http://%s", fixture.testServer.addr),
 	})
 	if err != nil {
 		return nil, err
 	}
-
 	return fixture, nil
 }
 
-func (f *testFixture) mount(root nodefs.Node) error {
+func (f *testFixture) mount(root fs.InodeEmbedder) error {
 	f.mntDir = filepath.Join(f.dir, "mnt")
 	if err := os.Mkdir(f.mntDir, 0755); err != nil {
 		return err
 	}
-
-	fuseOpts := &nodefs.Options{
-		EntryTimeout:    time.Hour,
-		NegativeTimeout: time.Hour,
-		AttrTimeout:     time.Hour,
+	t := time.Hour
+	fuseOpts := &fs.Options{
+		EntryTimeout:    &t,
+		NegativeTimeout: &t,
+		AttrTimeout:     &t,
 	}
-
+	fuseOpts.Debug = true
 	var err error
-	f.server, _, err = nodefs.MountRoot(f.mntDir, root, fuseOpts)
+	f.server, err = fs.Mount(f.mntDir, root, fuseOpts)
 	if err != nil {
 		return err
 	}
-
 	if fuseDebug {
 		f.server.SetDebug(true)
 	}
 	go f.server.Serve()
-
 	f.root = root
 	return nil
 }
diff --git a/fs/gitilesconfigfs.go b/fs/gitilesconfigfs.go
index 19ae65a..dad2b6a 100644
--- a/fs/gitilesconfigfs.go
+++ b/fs/gitilesconfigfs.go
@@ -15,31 +15,28 @@
 package fs
 
 import (
+	"context"
 	"encoding/hex"
 	"fmt"
 	"log"
+	"syscall"
 
 	"gopkg.in/src-d/go-git.v4/plumbing"
 
 	"github.com/google/slothfs/cache"
 	"github.com/google/slothfs/gitiles"
+	"github.com/hanwen/go-fuse/fs"
 	"github.com/hanwen/go-fuse/fuse"
-	"github.com/hanwen/go-fuse/fuse/nodefs"
 )
 
 type gitilesConfigFSRoot struct {
-	nodefs.Node
+	fs.Inode
 
-	fsConn  *nodefs.FileSystemConnector
 	cache   *cache.Cache
 	service *gitiles.RepoService
 	options GitilesOptions
 }
 
-func (r *gitilesConfigFSRoot) OnMount(fsConn *nodefs.FileSystemConnector) {
-	r.fsConn = fsConn
-}
-
 func parseID(s string) (*plumbing.Hash, error) {
 	b, err := hex.DecodeString(s)
 	if err != nil || len(b) != 20 {
@@ -51,14 +48,16 @@
 	return &h, nil
 }
 
-func (r *gitilesConfigFSRoot) Lookup(out *fuse.Attr, name string, context *fuse.Context) (*nodefs.Inode, fuse.Status) {
+var _ = (fs.NodeLookuper)((*gitilesConfigFSRoot)(nil))
+
+func (r *gitilesConfigFSRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
 	id, err := parseID(name)
 	if err != nil {
-		return nil, fuse.ENOENT
+		return nil, syscall.ENOENT
 	}
 
-	if ch := r.Inode().GetChild(name); ch != nil {
-		return ch, fuse.OK
+	if ch := r.GetChild(name); ch != nil {
+		return ch, 0
 	}
 
 	tree, err := r.cache.Tree.Get(id)
@@ -66,7 +65,7 @@
 		tree, err = r.service.GetTree(id.String(), "/", true)
 		if err != nil {
 			log.Printf("GetTree(%s): %v", id, err)
-			return nil, fuse.EIO
+			return nil, syscall.EIO
 		}
 
 		if err := r.cache.Tree.Add(id, tree); err != nil {
@@ -79,17 +78,18 @@
 		GitilesOptions: r.options,
 	}
 	newRoot := NewGitilesRoot(r.cache, tree, r.service, gro)
-	ch := r.Inode().NewChild(id.String(), true, newRoot)
-	out.Mode = fuse.S_IFDIR | 0755
+	ch := r.NewPersistentInode(
+		ctx,
+		newRoot,
+		fs.StableAttr{Mode: syscall.S_IFDIR})
 
-	newRoot.OnMount(r.fsConn)
-	return ch, fuse.OK
+	return ch, 0
 }
 
 // NewGitilesConfigFSRoot returns a root node for a filesystem that lazily
 // instantiates a repository if you access any subdirectory named by a
 // 40-byte hex SHA1.
-func NewGitilesConfigFSRoot(c *cache.Cache, service *gitiles.RepoService, options *GitilesOptions) nodefs.Node {
+func NewGitilesConfigFSRoot(c *cache.Cache, service *gitiles.RepoService, options *GitilesOptions) fs.InodeEmbedder {
 	// TODO(hanwen): nodefs.Node has an OnForget(), but it will
 	// never trigger for directories that have children. That
 	// means that we effectively never drop old trees. We can fix
@@ -97,7 +97,6 @@
 	// a periodic removal of all subtrees trees. Since the FS is
 	// read-only that should cause no ill effects.
 	return &gitilesConfigFSRoot{
-		Node:    nodefs.NewDefaultNode(),
 		cache:   c,
 		service: service,
 		options: *options,
diff --git a/fs/gitilesfs.go b/fs/gitilesfs.go
index dd96b08..394e4ee 100644
--- a/fs/gitilesfs.go
+++ b/fs/gitilesfs.go
@@ -15,6 +15,7 @@
 package fs
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -22,6 +23,7 @@
 	"log"
 	"os"
 	"path/filepath"
+	"strings"
 	"sync"
 	"sync/atomic"
 	"syscall"
@@ -32,14 +34,14 @@
 
 	"github.com/google/slothfs/cache"
 	"github.com/google/slothfs/gitiles"
+	"github.com/hanwen/go-fuse/fs"
 	"github.com/hanwen/go-fuse/fuse"
-	"github.com/hanwen/go-fuse/fuse/nodefs"
 )
 
 // gitilesRoot is the root for a FUSE filesystem backed by a Gitiles
 // service.
 type gitilesRoot struct {
-	nodefs.Node
+	fs.Inode
 
 	nodeCache *nodeCache
 
@@ -59,37 +61,9 @@
 	fetching     map[plumbing.Hash]bool
 }
 
-type linkNode struct {
-	nodefs.Node
-	linkTarget []byte
-}
-
-func (n *linkNode) Deletable() bool { return false }
-
-func newLinkNode(target string) *linkNode {
-	return &linkNode{
-		Node:       nodefs.NewDefaultNode(),
-		linkTarget: []byte(target),
-	}
-}
-
-func (n *linkNode) GetAttr(out *fuse.Attr, file nodefs.File, context *fuse.Context) (code fuse.Status) {
-	out.Size = uint64(len(n.linkTarget))
-	out.Mode = fuse.S_IFLNK
-
-	t := time.Unix(1, 0)
-	out.SetTimes(nil, &t, nil)
-
-	return fuse.OK
-}
-
-func (n *linkNode) Readlink(c *fuse.Context) ([]byte, fuse.Status) {
-	return n.linkTarget, fuse.OK
-}
-
 // gitilesNode represents a read-only blob in the FUSE filesystem.
 type gitilesNode struct {
-	nodefs.Node
+	fs.Inode
 
 	root *gitilesRoot
 
@@ -110,24 +84,15 @@
 	readCount uint32
 }
 
-func (n *gitilesNode) Deletable() bool {
-	return false
+var _ = (fs.NodeReadlinker)((*gitilesNode)(nil))
+
+func (n *gitilesNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) {
+	return n.linkTarget, 0
 }
 
-func (n *gitilesNode) Utimens(file nodefs.File, atime *time.Time, mtime *time.Time, context *fuse.Context) (code fuse.Status) {
-	if mtime != nil {
-		n.mtimeMu.Lock()
-		n.mtime = *mtime
-		n.mtimeMu.Unlock()
-	}
-	return fuse.OK
-}
+var _ = (fs.NodeGetattrer)((*gitilesNode)(nil))
 
-func (n *gitilesNode) Readlink(c *fuse.Context) ([]byte, fuse.Status) {
-	return n.linkTarget, fuse.OK
-}
-
-func (n *gitilesNode) GetAttr(out *fuse.Attr, file nodefs.File, context *fuse.Context) (code fuse.Status) {
+func (n *gitilesNode) Getattr(ctx context.Context, h fs.FileHandle, out *fuse.AttrOut) (code syscall.Errno) {
 	out.Size = uint64(n.size)
 	out.Mode = n.mode
 
@@ -136,57 +101,86 @@
 	n.mtimeMu.Unlock()
 
 	out.SetTimes(nil, &t, nil)
-	return fuse.OK
+	return 0
+}
+
+var _ = (fs.NodeSetattrer)((*gitilesNode)(nil))
+
+func (n *gitilesNode) Setattr(ctx context.Context, h fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) (code syscall.Errno) {
+	if 0 != in.Valid&(fuse.FATTR_MODE|
+		fuse.FATTR_UID|
+		fuse.FATTR_GID|
+		fuse.FATTR_SIZE|
+		fuse.FATTR_LOCKOWNER|
+		fuse.FATTR_CTIME) {
+		return syscall.ENOTSUP
+	}
+	if mt, ok := in.GetMTime(); ok {
+		n.mtimeMu.Lock()
+		n.mtime = mt
+		n.mtimeMu.Unlock()
+
+		return n.Getattr(ctx, h, out)
+	}
+	return 0
 }
 
 const xattrName = "user.gitsha1"
 
-func (n *gitilesNode) GetXAttr(attribute string, context *fuse.Context) (data []byte, code fuse.Status) {
+var _ = (fs.NodeGetxattrer)((*gitilesNode)(nil))
+
+func (n *gitilesNode) Getxattr(ctx context.Context, attribute string, dest []byte) (uint32, syscall.Errno) {
 	if attribute != xattrName {
-		return nil, fuse.ENODATA
+		return 0, syscall.ENODATA
 	}
-	return []byte(n.id.String()), fuse.OK
+	sz := copy(dest, n.id.String())
+	return uint32(sz), 0
 }
 
-func (n *gitilesNode) ListXAttr(context *fuse.Context) (attrs []string, code fuse.Status) {
-	return []string{xattrName}, fuse.OK
+var _ = (fs.NodeListxattrer)((*gitilesNode)(nil))
+
+func (n *gitilesNode) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) {
+	sz := copy(dest, xattrName)
+	dest[sz] = 0
+	return uint32(sz + 1), 0
 }
 
-func (n *gitilesNode) Open(flags uint32, context *fuse.Context) (file nodefs.File, code fuse.Status) {
+var _ = (fs.NodeOpener)((*gitilesNode)(nil))
+
+func (n *gitilesNode) Open(ctx context.Context, flags uint32) (h fs.FileHandle, fuseFlags uint32, code syscall.Errno) {
 	if n.root.handleLessIO {
 		// We say ENOSYS so FUSE on Linux uses handle-less I/O.
-		return nil, fuse.ENOSYS
+		return nil, 0, syscall.ENOSYS
 	}
 
 	f, err := n.root.openFile(n.id, n.clone)
 	if err != nil {
-		return nil, fuse.ToStatus(err)
+		return nil, 0, fs.ToErrno(err)
 	}
 
-	return &nodefs.WithFlags{
-		File:      nodefs.NewLoopbackFile(f),
-		FuseFlags: fuse.FOPEN_KEEP_CACHE,
-	}, fuse.OK
+	return fs.NewLoopbackFile(int(f.Fd())), fuse.FOPEN_KEEP_CACHE, 0
 }
 
-func (n *gitilesNode) Read(file nodefs.File, dest []byte, off int64, context *fuse.Context) (fuse.ReadResult, fuse.Status) {
+var _ = (fs.NodeReader)((*gitilesNode)(nil))
+
+func (n *gitilesNode) Read(ctx context.Context, file fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
 	if off == 0 {
 		atomic.AddUint32(&n.readCount, 1)
 	}
 
 	if n.root.handleLessIO {
-		return n.handleLessRead(file, dest, off, context)
+		return n.handleLessRead(file, dest, off)
 	}
 
-	return file.Read(dest, off)
+	return file.(fs.FileReader).Read(ctx, dest, off)
 }
 
-func (n *gitilesNode) handleLessRead(file nodefs.File, dest []byte, off int64, context *fuse.Context) (fuse.ReadResult, fuse.Status) {
+func (n *gitilesNode) handleLessRead(file fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
 	// TODO(hanwen): for large files this is not efficient. Should
 	// have a cache of open file handles.
 	f, err := n.root.openFile(n.id, n.clone)
 	if err != nil {
-		return nil, fuse.ToStatus(err)
+		return nil, fs.ToErrno(err)
 	}
 
 	m, err := f.ReadAt(dest, off)
@@ -194,7 +188,7 @@
 		err = nil
 	}
 	f.Close()
-	return fuse.ReadResultData(dest[:m]), fuse.ToStatus(err)
+	return fuse.ReadResultData(dest[:m]), fs.ToErrno(err)
 }
 
 // openFile returns a file handle for the given blob. If `clone` is
@@ -290,37 +284,36 @@
 
 // dataNode makes arbitrary data available as a file.
 type dataNode struct {
-	nodefs.Node
+	fs.Inode
 	data []byte
 }
 
-func (n *dataNode) GetAttr(out *fuse.Attr, file nodefs.File, context *fuse.Context) (code fuse.Status) {
+var _ = (fs.NodeGetattrer)((*dataNode)(nil))
+
+func (n *dataNode) Getattr(ctx context.Context, file fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
 	out.Size = uint64(len(n.data))
 	out.Mode = fuse.S_IFREG | 0644
 	t := time.Unix(1, 0)
 	out.SetTimes(nil, &t, nil)
 
-	return fuse.OK
+	return 0
 }
 
-func (n *dataNode) Open(flags uint32, content *fuse.Context) (nodefs.File, fuse.Status) {
-	return nodefs.NewDataFile(n.data), fuse.OK
+var _ = (fs.NodeOpener)((*gitilesNode)(nil))
+
+func (n *dataNode) Open(ctx context.Context, flags uint32) (fs.FileHandle, syscall.Errno) {
+	return fs.MemRegularFile{Data: n.data}, 0
 }
 
-func (n *dataNode) GetXAttr(attribute string, context *fuse.Context) (data []byte, code fuse.Status) {
-	return nil, fuse.ENODATA
-}
+var _ = (fs.NodeGetxattrer)((*gitilesNode)(nil))
 
-func (n *dataNode) Deletable() bool { return false }
-
-func newDataNode(c []byte) nodefs.Node {
-	return &dataNode{nodefs.NewDefaultNode(), c}
+func (n *dataNode) GetXAttr(ctx context.Context, attribute string) (data []byte, code syscall.Errno) {
+	return nil, syscall.ENODATA
 }
 
 // NewGitilesRoot returns the root node for a file system.
-func NewGitilesRoot(c *cache.Cache, tree *gitiles.Tree, service *gitiles.RepoService, options GitilesRevisionOptions) nodefs.Node {
+func NewGitilesRoot(c *cache.Cache, tree *gitiles.Tree, service *gitiles.RepoService, options GitilesRevisionOptions) *gitilesRoot {
 	r := &gitilesRoot{
-		Node:         newDirNode(),
 		service:      service,
 		nodeCache:    newNodeCache(),
 		cache:        c,
@@ -335,63 +328,39 @@
 	return r
 }
 
-func (r *gitilesRoot) Deletable() bool { return false }
+var _ = (fs.NodeGetxattrer)((*gitilesRoot)(nil))
 
-func (r *gitilesRoot) GetXAttr(attribute string, context *fuse.Context) (data []byte, code fuse.Status) {
-	return nil, fuse.ENODATA
+func (r *gitilesRoot) Getxattr(ctx context.Context, attribute string, data []byte) (sz uint32, code syscall.Errno) {
+	return 0, syscall.ENODATA
 }
 
-func (r *gitilesRoot) OnMount(fsConn *nodefs.FileSystemConnector) {
-	if err := r.onMount(fsConn); err != nil {
-		log.Printf("onMount: %v", err)
-		for k := range r.Inode().Children() {
-			r.Inode().RmChild(k)
+func (r *gitilesRoot) pathTo(dir string) *fs.Inode {
+	p := &r.Inode
+	for _, c := range strings.Split(dir, "/") {
+		if len(c) == 0 {
+			continue
 		}
-		r.Inode().NewChild("ERROR", false, newDataNode([]byte(err.Error())))
+		ch := p.GetChild(c)
+		if ch == nil {
+			ch = p.NewPersistentInode(context.Background(),
+				&fs.Inode{},
+				fs.StableAttr{Mode: syscall.S_IFDIR})
+			p.AddChild(c, ch, true)
+		}
+		p = ch
 	}
+	return p
 }
 
-type dirNode struct {
-	nodefs.Node
-}
+var _ = (fs.NodeOnAdder)((*gitilesRoot)(nil))
 
-// Implement Utimens so we don't create spurious "not implemented"
-// messages when directory targets for symlinks are touched.
-func (n *dirNode) Utimens(file nodefs.File, atime *time.Time, mtime *time.Time, context *fuse.Context) (code fuse.Status) {
-	return fuse.OK
-}
-
-func (n *dirNode) GetAttr(out *fuse.Attr, file nodefs.File, context *fuse.Context) (code fuse.Status) {
-	out.Mode = fuse.S_IFDIR | 0755
-	t := time.Unix(1, 0)
-	out.SetTimes(nil, &t, nil)
-	return fuse.OK
-}
-
-func (n *dirNode) Deletable() bool {
-	return false
-}
-
-func newDirNode() nodefs.Node {
-	return &dirNode{nodefs.NewDefaultNode()}
-}
-
-func (r *gitilesRoot) pathTo(fsConn *nodefs.FileSystemConnector, dir string) *nodefs.Inode {
-	parent, left := fsConn.Node(r.Inode(), dir)
-	for _, l := range left {
-		ch := parent.NewChild(l, true, newDirNode())
-		parent = ch
-	}
-	return parent
-}
-
-func (r *gitilesRoot) onMount(fsConn *nodefs.FileSystemConnector) error {
+func (r *gitilesRoot) OnAdd(ctx context.Context) {
 	for _, e := range r.tree.Entries {
 		if e.Type == "commit" {
 			// TODO(hanwen): support submodules.  For now,
 			// we pretend we are plain git, which also
 			// leaves an empty directory in the place of a submodule.
-			r.pathTo(fsConn, e.Name)
+			r.pathTo(e.Name)
 			continue
 		}
 		if e.Type != "blob" {
@@ -401,10 +370,10 @@
 		p := e.Name
 		dir, base := filepath.Split(p)
 
-		parent := r.pathTo(fsConn, dir)
+		parent := r.pathTo(dir)
 		id, err := parseID(e.ID)
 		if err != nil {
-			return err
+			return
 		}
 
 		// Determine if file should trigger a clone.
@@ -422,7 +391,6 @@
 		n := r.nodeCache.get(id, xbit)
 		if n == nil {
 			n = &gitilesNode{
-				Node:  nodefs.NewDefaultNode(),
 				id:    *id,
 				mode:  uint32(e.Mode),
 				clone: clone,
@@ -436,35 +404,45 @@
 				n.size = int64(*e.Size)
 			}
 
+			mode := uint32(syscall.S_IFREG)
 			if e.Target != nil {
 				n.linkTarget = []byte(*e.Target)
 				n.size = int64(len(n.linkTarget))
+				mode = syscall.S_IFLNK
 			}
 
 			r.shaMap[*id] = p
-			parent.NewChild(base, false, n)
+
+			ch := parent.NewPersistentInode(ctx, n, fs.StableAttr{Mode: mode})
+			parent.AddChild(base, ch, true)
 			r.nodeCache.add(n)
 		} else {
-			parent.AddChild(base, n.Inode())
+			parent.AddChild(base, n.EmbeddedInode(), true)
 		}
 
 	}
 
-	slothfsNode := r.Inode().NewChild(".slothfs", true, newDirNode())
-	slothfsNode.NewChild("treeID", false, newDataNode([]byte(r.tree.ID)))
+	slothfsNode := r.NewPersistentInode(ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
+	r.AddChild(".slothfs", slothfsNode, true)
+	idFile := r.NewPersistentInode(ctx, &fs.MemRegularFile{
+		Data: []byte(r.tree.ID)}, fs.StableAttr{Mode: syscall.S_IFREG})
+
+	slothfsNode.AddChild("treeID", idFile, false)
 
 	treeContent, err := json.MarshalIndent(r.tree, "", " ")
 	if err != nil {
 		log.Panicf("json.Marshal: %v", err)
 	}
+	jsonFile := r.NewPersistentInode(ctx, &fs.MemRegularFile{
+		Data: treeContent}, fs.StableAttr{Mode: syscall.S_IFREG})
 
-	slothfsNode.NewChild("tree.json", false, newDataNode([]byte(treeContent)))
+	slothfsNode.AddChild("tree.json", jsonFile, false)
 
 	// We don't need the tree data anymore.
 	r.tree = nil
 
-	if fsConn.Server().KernelSettings().Flags&fuse.CAP_NO_OPEN_SUPPORT != 0 {
-		r.handleLessIO = true
-	}
-	return nil
+	// XXX
+	//	if fsConn.Server().KernelSettings().Flags&fuse.CAP_NO_OPEN_SUPPORT != 0 {
+	//		r.handleLessIO = true
+	//	}
 }
diff --git a/fs/gitilesfs_test.go b/fs/gitilesfs_test.go
index f444292..ddac371 100644
--- a/fs/gitilesfs_test.go
+++ b/fs/gitilesfs_test.go
@@ -296,12 +296,12 @@
 		t.Fatal("mount", err)
 	}
 
-	ch1 := fs.Inode().GetChild("AUTHORS")
+	ch1 := fs.GetChild("AUTHORS")
 	if ch1 == nil {
 		t.Fatalf("node for AUTHORS not found")
 	}
 
-	ch2 := fs.Inode().GetChild("AUTHORS2")
+	ch2 := fs.GetChild("AUTHORS2")
 	if ch2 == nil {
 		t.Fatalf("node for AUTHORS2 not found")
 	}
@@ -309,7 +309,7 @@
 	if ch1 != ch2 {
 		t.Error("equal blobs did not share inodes.")
 	}
-	ch3 := fs.Inode().GetChild("AUTHORSx")
+	ch3 := fs.GetChild("AUTHORSx")
 	if ch1 == ch3 {
 		t.Error("blob with different modes shared inode.")
 	}
@@ -430,7 +430,7 @@
 	}
 
 	// TODO(hanwen): is this race-detector sane?
-	ch := fs.Inode().GetChild("testcase")
+	ch := fs.GetChild("testcase")
 	if ch == nil {
 		t.Fatalf("node for testcase/ not found")
 	}
@@ -439,9 +439,9 @@
 		t.Fatalf("node for addprefix.mk not found")
 	}
 
-	giNode, ok := ch.Node().(*gitilesNode)
+	giNode, ok := ch.Operations().(*gitilesNode)
 	if !ok {
-		t.Fatalf("got node type %T, want *gitilesNode", ch.Node())
+		t.Fatalf("got node type %T, want *gitilesNode", ch.Operations())
 	}
 
 	if giNode.clone {
@@ -477,14 +477,14 @@
 		}
 	}
 
-	ch := fs.Inode().GetChild("AUTHORS")
+	ch := fs.GetChild("AUTHORS")
 	if ch == nil {
 		t.Fatalf("node for AUTHORS not found")
 	}
 
-	giNode, ok := ch.Node().(*gitilesNode)
+	giNode, ok := ch.Operations().(*gitilesNode)
 	if !ok {
-		t.Fatalf("got node type %T, want *gitilesNode", ch.Node())
+		t.Fatalf("got node type %T, want *gitilesNode", ch.Operations())
 	}
 
 	if c := atomic.LoadUint32(&giNode.readCount); c != 1 {
diff --git a/fs/gitileshostfs.go b/fs/gitileshostfs.go
index 7c83e6d..b283831 100644
--- a/fs/gitileshostfs.go
+++ b/fs/gitileshostfs.go
@@ -15,18 +15,20 @@
 package fs
 
 import (
+	"context"
 	"fmt"
 	"path/filepath"
 	"sort"
 	"strings"
+	"syscall"
 
 	"github.com/google/slothfs/cache"
 	"github.com/google/slothfs/gitiles"
-	"github.com/hanwen/go-fuse/fuse/nodefs"
+	"github.com/hanwen/go-fuse/fs"
 )
 
 type hostFS struct {
-	nodefs.Node
+	fs.Inode
 
 	cache        *cache.Cache
 	service      *gitiles.Service
@@ -60,7 +62,6 @@
 	}
 
 	return &hostFS{
-		Node:         nodefs.NewDefaultNode(),
 		projects:     projMap,
 		cloneOptions: cloneOptions,
 		service:      service,
@@ -68,34 +69,38 @@
 	}, nil
 }
 
-func (h *hostFS) OnMount(fsConn *nodefs.FileSystemConnector) {
+var _ = (fs.NodeOnAdder)((*hostFS)(nil))
+
+func (h *hostFS) OnAdd(ctx context.Context) {
 	var keys []string
 	for k := range parents(h.projects) {
 		keys = append(keys, k)
 	}
 	sort.Strings(keys)
 
-	nodes := map[string]*nodefs.Inode{
-		"": h.Inode(),
+	nodes := map[string]*fs.Inode{
+		"": h.EmbeddedInode(),
 	}
 
 	for _, k := range keys {
 		if k == "." {
 			continue
 		}
+		// XXX should loop ?
 		d, nm := filepath.Split(k)
 		d = strings.TrimSuffix(d, "/")
 		parent := nodes[d]
 
-		var node nodefs.Node
+		var node fs.InodeEmbedder
 		if p := h.projects[k]; p != nil {
 			node = h.newProjectNode(parent, p)
 			delete(h.projects, k)
-			node.OnMount(fsConn)
 		} else {
-			node = newDirNode()
+			node = &fs.Inode{}
 		}
-		ch := parent.NewChild(nm, true, node)
+
+		ch := parent.NewPersistentInode(ctx, node, fs.StableAttr{Mode: syscall.S_IFDIR})
+		parent.AddChild(nm, ch, true)
 		nodes[k] = ch
 	}
 
@@ -105,13 +110,13 @@
 
 		parent := nodes[d]
 		node := h.newProjectNode(parent, p)
-		node.OnMount(fsConn)
 
-		parent.NewChild(nm, true, node)
+		ch := parent.NewPersistentInode(ctx, node, fs.StableAttr{Mode: syscall.S_IFDIR})
+		parent.AddChild(nm, ch, true)
 	}
 }
 
-func (h *hostFS) newProjectNode(parent *nodefs.Inode, proj *gitiles.Project) nodefs.Node {
+func (h *hostFS) newProjectNode(parent *fs.Inode, proj *gitiles.Project) fs.InodeEmbedder {
 	repoService := h.service.NewRepoService(proj.Name)
 	opts := GitilesOptions{
 		CloneURL:    proj.CloneURL,