func (n *Node) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
	out.Mode = n.getMode(n.isLeaf)
	out.Size = uint64(len(n.content))
	out.Ino = n.inodeHash(n.path)
	now := time.Now()
	out.SetTimes(&now, &now, &now)
	out.Uid = uid
	out.Gid = gid
	return fs.OK
}
Getattr outputs file attributes
TODO: how to invalidate them?
package fs
import (
	"context"
	"hash/fnv"
	"os/user"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"
	"github.com/hanwen/go-fuse/v2/fs"
	"github.com/hanwen/go-fuse/v2/fuse"
	"github.com/polyrabbit/etcdfs/etcd"
	"github.com/sirupsen/logrus"
	v3 "go.etcd.io/etcd/v3/clientv3"
)
var uid, gid uint32
func init() {
	u, err := user.Current()
	if err != nil {
		panic(err)
	}
	uid32, _ := strconv.ParseUint(u.Uid, 10, 32)
	gid32, _ := strconv.ParseUint(u.Gid, 10, 32)
	uid = uint32(uid32)
	gid = uint32(gid32)
}
type Node struct {
	fs.Inode
	client *etcd.Client
	isLeaf bool    
	path   string  
	rwMu    sync.RWMutex  
	content []byte        
}
func NewRoot(client *etcd.Client) *Node {
	return &Node{
		client: client,
		isLeaf: false,
	}
}
func (n *Node) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
	parent := n.resolve("")
	logrus.WithField("path", parent).Debug("Node Readdir")
	entrySet := make(map[string]fuse.DirEntry)
	opts := []v3.OpOption{v3.WithRange(v3.GetPrefixRangeEnd(parent)),
		v3.WithSort(v3.SortByKey, v3.SortAscend), v3.WithLimit(500)}
	nextGroup := parent
	for {
		keys, moreKeys, err := n.client.ListKeys(ctx, nextGroup, opts...)
		if err != nil {
			logrus.WithError(err).WithField("path", parent).Errorf("Failed to list keys from etcd")
			return nil, syscall.EIO
		}
		var lastName string
		for _, key := range keys {
			nextLevel, hasMore := n.nextHierarchyLevel(key, parent)
			lastName = nextLevel
			if _, exist := entrySet[nextLevel]; exist {
				continue
			}
			entrySet[nextLevel] = fuse.DirEntry{
				Mode: n.getMode(!hasMore),
				Name: nextLevel,
				Ino:  n.inodeHash(nextLevel),
			}
		}
		nextGroup = v3.GetPrefixRangeEnd(n.resolve(lastName))  
		if !moreKeys || len(keys) == 0 {
			break
		}
		if len(entrySet) > 1000 {
			logrus.Warn("Already fetched more than 1000 entries, skipping the rest for performance reason...")
			break
		}
	}
	entries := make([]fuse.DirEntry, 0, len(entrySet))
	for _, e := range entrySet {
		entries = append(entries, e)
	}
	return fs.NewListDirStream(entries), fs.OK
}
func (n *Node) nextHierarchyLevel(path, parent string) (string, bool) {
	baseName := strings.TrimPrefix(path, parent)
	hierarchies := strings.SplitN(baseName, string(filepath.Separator), 2)
	return filepath.Clean(hierarchies[0]), len(hierarchies) >= 2
}
func (n *Node) resolve(fileName string) string {
	return n.path + string(filepath.Separator) + fileName
}
func (n *Node) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
	fullPath := n.resolve(name)
	logrus.WithField("path", fullPath).Debug("Node Lookup")
	keys, _, err := n.client.ListKeys(ctx, fullPath, v3.WithLimit(1))
	if err != nil {
		logrus.WithError(err).WithField("path", fullPath).Errorf("Failed to list keys from etcd")
		return nil, syscall.EIO
	}
	if len(keys) == 0 {
		return nil, syscall.ENOENT
	}
	key := keys[0]
	child := Node{
		path:   fullPath,
		client: n.client,
	}
	if key == fullPath {
		child.isLeaf = true
	} else if strings.HasPrefix(key, fullPath+string(filepath.Separator)) {
		child.isLeaf = false
	} else {
		return nil, syscall.ENOENT
	}
	return n.NewInode(ctx, &child, fs.StableAttr{Mode: child.getMode(child.isLeaf), Ino: n.inodeHash(child.path)}), fs.OK
}
func (n *Node) getMode(isLeaf bool) uint32 {
	if isLeaf {
		return 0644 | uint32(syscall.S_IFREG)
	} else {
		return 0755 | uint32(syscall.S_IFDIR)
	}
}