// Copyright 2016 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 unionfs import ( "crypto/md5" "fmt" "log" "os" "path" "path/filepath" "strings" "sync" "syscall" "time" "github.com/hanwen/go-fuse/fuse" "github.com/hanwen/go-fuse/fuse/nodefs" "github.com/hanwen/go-fuse/fuse/pathfs" ) func filePathHash(path string) string { dir, base := filepath.Split(path) h := md5.New() h.Write([]byte(dir)) return fmt.Sprintf("%x-%s", h.Sum(nil)[:8], base) } /* UnionFs implements a user-space union file system, which is stateless but efficient even if the writable branch is on NFS. Assumptions: * It uses a list of branches, the first of which (index 0) is thought to be writable, and the rest read-only. * It assumes that the number of deleted files is small relative to the total tree size. Implementation notes. * It overlays arbitrary writable FileSystems with any number of readonly FileSystems. * Deleting a file will put a file named /DELETIONS/HASH-OF-FULL-FILENAME into the writable overlay, containing the full filename itself. This is optimized for NFS usage: we want to minimize the number of NFS operations, which are slow. By putting all whiteouts in one place, we can cheaply fetch the list of all deleted files. Even without caching on our side, the kernel's negative dentry cache can answer is-deleted queries quickly. */ type unionFS struct { pathfs.FileSystem // The same, but as interfaces. fileSystems []pathfs.FileSystem // A file-existence cache. deletionCache *dirCache // A file -> branch cache. branchCache *TimedCache // Map of files to hide. hiddenFiles map[string]bool options *UnionFsOptions nodeFs *pathfs.PathNodeFs } type UnionFsOptions struct { BranchCacheTTL time.Duration DeletionCacheTTL time.Duration DeletionDirName string HiddenFiles []string } const ( _DROP_CACHE = ".drop_cache" ) func NewUnionFs(fileSystems []pathfs.FileSystem, options UnionFsOptions) (pathfs.FileSystem, error) { g := &unionFS{ options: &options, fileSystems: fileSystems, FileSystem: pathfs.NewDefaultFileSystem(), } writable := g.fileSystems[0] code := g.createDeletionStore() if !code.Ok() { return nil, fmt.Errorf("could not create deletion path %v: %v", options.DeletionDirName, code) } g.deletionCache = newDirCache(writable, options.DeletionDirName, options.DeletionCacheTTL) g.branchCache = NewTimedCache( func(n string) (interface{}, bool) { return g.getBranchAttrNoCache(n), true }, options.BranchCacheTTL) g.hiddenFiles = make(map[string]bool) for _, name := range options.HiddenFiles { g.hiddenFiles[name] = true } return g, nil } func (fs *unionFS) OnMount(nodeFs *pathfs.PathNodeFs) { fs.nodeFs = nodeFs } //////////////// // Deal with all the caches. // The isDeleted() method tells us if a path has a marker in the deletion store. // It may return an error code if the store could not be accessed. func (fs *unionFS) isDeleted(name string) (deleted bool, code fuse.Status) { marker := fs.deletionPath(name) haveCache, found := fs.deletionCache.HasEntry(filepath.Base(marker)) if haveCache { return found, fuse.OK } _, code = fs.fileSystems[0].GetAttr(marker, nil) if code == fuse.OK { return true, code } if code == fuse.ENOENT { return false, fuse.OK } log.Printf("error accessing deletion marker %s: %v", marker, code) return false, fuse.Status(syscall.EROFS) } func (fs *unionFS) createDeletionStore() (code fuse.Status) { writable := fs.fileSystems[0] fi, code := writable.GetAttr(fs.options.DeletionDirName, nil) if code == fuse.ENOENT { code = writable.Mkdir(fs.options.DeletionDirName, 0755, nil) if code.Ok() { fi, code = writable.GetAttr(fs.options.DeletionDirName, nil) } } if !code.Ok() || !fi.IsDir() { code = fuse.Status(syscall.EROFS) } return code } func (fs *unionFS) getBranch(name string) branchResult { name = stripSlash(name) r := fs.branchCache.Get(name) return r.(branchResult) } func (fs *unionFS) setBranch(name string, r branchResult) { if !r.valid() { log.Panicf("entry %q setting illegal branchResult %v", name, r) } fs.branchCache.Set(name, r) } type branchResult struct { attr *fuse.Attr code fuse.Status branch int } func (r *branchResult) valid() bool { return (r.branch >= 0 && r.attr != nil && r.code.Ok()) || (r.branch < 0 && r.attr == nil && !r.code.Ok()) } func (fs branchResult) String() string { return fmt.Sprintf("{%v %v branch %d}", fs.attr, fs.code, fs.branch) } func (fs *unionFS) getBranchAttrNoCache(name string) branchResult { name = stripSlash(name) parent, base := path.Split(name) parent = stripSlash(parent) parentBranch := 0 if base != "" { parentBranch = fs.getBranch(parent).branch } for i, fs := range fs.fileSystems { if i < parentBranch { continue } a, s := fs.GetAttr(name, nil) if s.Ok() { if i > 0 { // Needed to make hardlinks work. a.Ino = 0 } return branchResult{ attr: a, code: s, branch: i, } } else { if s != fuse.ENOENT { log.Printf("getattr: %v: Got error %v from branch %v", name, s, i) } } } return branchResult{nil, fuse.ENOENT, -1} } //////////////// // Deletion. func (fs *unionFS) deletionPath(name string) string { return filepath.Join(fs.options.DeletionDirName, filePathHash(name)) } func (fs *unionFS) removeDeletion(name string) { marker := fs.deletionPath(name) fs.deletionCache.RemoveEntry(path.Base(marker)) // os.Remove tries to be smart and issues a Remove() and // Rmdir() sequentially. We want to skip the 2nd system call, // so use syscall.Unlink() directly. code := fs.fileSystems[0].Unlink(marker, nil) if !code.Ok() && code != fuse.ENOENT { log.Printf("error unlinking %s: %v", marker, code) } } func (fs *unionFS) putDeletion(name string) (code fuse.Status) { code = fs.createDeletionStore() if !code.Ok() { return code } marker := fs.deletionPath(name) fs.deletionCache.AddEntry(path.Base(marker)) // Is there a WriteStringToFileOrDie ? writable := fs.fileSystems[0] fi, code := writable.GetAttr(marker, nil) if code.Ok() && fi.Size == uint64(len(name)) { return fuse.OK } var f nodefs.File if code == fuse.ENOENT { f, code = writable.Create(marker, uint32(os.O_TRUNC|os.O_WRONLY), 0644, nil) } else { writable.Chmod(marker, 0644, nil) f, code = writable.Open(marker, uint32(os.O_TRUNC|os.O_WRONLY), nil) } if !code.Ok() { log.Printf("could not create deletion file %v: %v", marker, code) return fuse.EPERM } defer f.Release() defer f.Flush() n, code := f.Write([]byte(name), 0) if int(n) != len(name) || !code.Ok() { panic(fmt.Sprintf("Error for writing %v: %v, %v (exp %v) %v", name, marker, n, len(name), code)) } return fuse.OK } //////////////// // Promotion. func (fs *unionFS) Promote(name string, srcResult branchResult, context *fuse.Context) (code fuse.Status) { writable := fs.fileSystems[0] sourceFs := fs.fileSystems[srcResult.branch] // Promote directories. fs.promoteDirsTo(name) if srcResult.attr.IsRegular() { code = pathfs.CopyFile(sourceFs, writable, name, name, context) if code.Ok() { code = writable.Chmod(name, srcResult.attr.Mode&07777|0200, context) } if code.Ok() { aTime := srcResult.attr.AccessTime() mTime := srcResult.attr.ModTime() code = writable.Utimens(name, &aTime, &mTime, context) } files := fs.nodeFs.AllFiles(name, 0) for _, fileWrapper := range files { if !code.Ok() { break } var uf *unionFsFile f := fileWrapper.File for f != nil { ok := false uf, ok = f.(*unionFsFile) if ok { break } f = f.InnerFile() } if uf == nil { panic("no unionFsFile found inside") } if uf.layer > 0 { uf.layer = 0 f := uf.File uf.File, code = fs.fileSystems[0].Open(name, fileWrapper.OpenFlags, context) f.Flush() f.Release() } } } else if srcResult.attr.IsSymlink() { link := "" link, code = sourceFs.Readlink(name, context) if !code.Ok() { log.Println("can't read link in source fs", name) } else { code = writable.Symlink(link, name, context) } } else if srcResult.attr.IsDir() { code = writable.Mkdir(name, srcResult.attr.Mode&07777|0200, context) } else { log.Println("Unknown file type:", srcResult.attr) return fuse.ENOSYS } if !code.Ok() { fs.branchCache.GetFresh(name) return code } else { r := fs.getBranch(name) r.branch = 0 fs.setBranch(name, r) } return fuse.OK } //////////////////////////////////////////////////////////////// // Below: implement interface for a FileSystem. func (fs *unionFS) Link(orig string, newName string, context *fuse.Context) (code fuse.Status) { origResult := fs.getBranch(orig) code = origResult.code if code.Ok() && origResult.branch > 0 { code = fs.Promote(orig, origResult, context) } if code.Ok() && origResult.branch > 0 { // Hairy: for the link to be hooked up to the existing // inode, PathNodeFs must see a client inode for the // original. We force a refresh of the attribute (so // the Ino is filled in.), and then force PathNodeFs // to see the Inode number. fs.branchCache.GetFresh(orig) inode := fs.nodeFs.Node(orig) var a fuse.Attr inode.Node().GetAttr(&a, nil, nil) } if code.Ok() { code = fs.promoteDirsTo(newName) } if code.Ok() { code = fs.fileSystems[0].Link(orig, newName, context) } if code.Ok() { fs.removeDeletion(newName) fs.branchCache.GetFresh(newName) } return code } func (fs *unionFS) Rmdir(path string, context *fuse.Context) (code fuse.Status) { r := fs.getBranch(path) if r.code != fuse.OK { return r.code } if !r.attr.IsDir() { return fuse.Status(syscall.ENOTDIR) } stream, code := fs.OpenDir(path, context) found := false for _ = range stream { found = true } if found { return fuse.Status(syscall.ENOTEMPTY) } if r.branch > 0 { code = fs.putDeletion(path) return code } code = fs.fileSystems[0].Rmdir(path, context) if code != fuse.OK { return code } r = fs.branchCache.GetFresh(path).(branchResult) if r.branch > 0 { code = fs.putDeletion(path) } return code } func (fs *unionFS) Mkdir(path string, mode uint32, context *fuse.Context) (code fuse.Status) { deleted, code := fs.isDeleted(path) if !code.Ok() { return code } if !deleted { r := fs.getBranch(path) if r.code != fuse.ENOENT { return fuse.Status(syscall.EEXIST) } } code = fs.promoteDirsTo(path) if code.Ok() { code = fs.fileSystems[0].Mkdir(path, mode, context) } if code.Ok() { fs.removeDeletion(path) attr := &fuse.Attr{ Mode: fuse.S_IFDIR | mode, } fs.setBranch(path, branchResult{attr, fuse.OK, 0}) } var stream []fuse.DirEntry stream, code = fs.OpenDir(path, context) if code.Ok() { // This shouldn't happen, but let's be safe. for _, entry := range stream { fs.putDeletion(filepath.Join(path, entry.Name)) } } return code } func (fs *unionFS) Symlink(pointedTo string, linkName string, context *fuse.Context) (code fuse.Status) { code = fs.promoteDirsTo(linkName) if code.Ok() { code = fs.fileSystems[0].Symlink(pointedTo, linkName, context) } if code.Ok() { fs.removeDeletion(linkName) fs.branchCache.GetFresh(linkName) } return code } func (fs *unionFS) Truncate(path string, size uint64, context *fuse.Context) (code fuse.Status) { if path == _DROP_CACHE { return fuse.OK } r := fs.getBranch(path) if r.branch > 0 { code = fs.Promote(path, r, context) r.branch = 0 } if code.Ok() { code = fs.fileSystems[0].Truncate(path, size, context) } if code.Ok() { r.attr.Size = size now := time.Now() r.attr.SetTimes(nil, &now, &now) fs.setBranch(path, r) } return code } func (fs *unionFS) Utimens(name string, atime *time.Time, mtime *time.Time, context *fuse.Context) (code fuse.Status) { name = stripSlash(name) r := fs.getBranch(name) code = r.code if code.Ok() && r.branch > 0 { code = fs.Promote(name, r, context) r.branch = 0 } if code.Ok() { code = fs.fileSystems[0].Utimens(name, atime, mtime, context) } if code.Ok() { now := time.Now() r.attr.SetTimes(atime, mtime, &now) fs.setBranch(name, r) } return code } func (fs *unionFS) Chown(name string, uid uint32, gid uint32, context *fuse.Context) (code fuse.Status) { name = stripSlash(name) r := fs.getBranch(name) if r.attr == nil || r.code != fuse.OK { return r.code } if os.Geteuid() != 0 { return fuse.EPERM } if r.attr.Uid != uid || r.attr.Gid != gid { if r.branch > 0 { code := fs.Promote(name, r, context) if code != fuse.OK { return code } r.branch = 0 } fs.fileSystems[0].Chown(name, uid, gid, context) } r.attr.Uid = uid r.attr.Gid = gid now := time.Now() r.attr.SetTimes(nil, nil, &now) fs.setBranch(name, r) return fuse.OK } func (fs *unionFS) Chmod(name string, mode uint32, context *fuse.Context) (code fuse.Status) { name = stripSlash(name) r := fs.getBranch(name) if r.attr == nil { return r.code } if r.code != fuse.OK { return r.code } permMask := uint32(07777) // Always be writable. oldMode := r.attr.Mode & permMask if oldMode != mode { if r.branch > 0 { code := fs.Promote(name, r, context) if code != fuse.OK { return code } r.branch = 0 } fs.fileSystems[0].Chmod(name, mode, context) } r.attr.Mode = (r.attr.Mode &^ permMask) | mode now := time.Now() r.attr.SetTimes(nil, nil, &now) fs.setBranch(name, r) return fuse.OK } func (fs *unionFS) Access(name string, mode uint32, context *fuse.Context) (code fuse.Status) { // We always allow writing. mode = mode &^ fuse.W_OK if name == "" || name == _DROP_CACHE { return fuse.OK } r := fs.getBranch(name) if r.branch >= 0 { return fs.fileSystems[r.branch].Access(name, mode, context) } return fuse.ENOENT } func (fs *unionFS) Unlink(name string, context *fuse.Context) (code fuse.Status) { r := fs.getBranch(name) if r.branch == 0 { code = fs.fileSystems[0].Unlink(name, context) if code != fuse.OK { return code } r = fs.branchCache.GetFresh(name).(branchResult) } if r.branch > 0 { // It would be nice to do the putDeletion async. code = fs.putDeletion(name) } return code } func (fs *unionFS) Readlink(name string, context *fuse.Context) (out string, code fuse.Status) { r := fs.getBranch(name) if r.branch >= 0 { return fs.fileSystems[r.branch].Readlink(name, context) } return "", fuse.ENOENT } func stripSlash(fn string) string { return strings.TrimRight(fn, string(filepath.Separator)) } func (fs *unionFS) promoteDirsTo(filename string) fuse.Status { dirName, _ := filepath.Split(filename) dirName = stripSlash(dirName) var todo []string var results []branchResult for dirName != "" { r := fs.getBranch(dirName) if !r.code.Ok() { log.Println("path component does not exist", filename, dirName) } if !r.attr.IsDir() { log.Println("path component is not a directory.", dirName, r) return fuse.EPERM } if r.branch == 0 { break } todo = append(todo, dirName) results = append(results, r) dirName, _ = filepath.Split(dirName) dirName = stripSlash(dirName) } for i := range todo { j := len(todo) - i - 1 d := todo[j] r := results[j] code := fs.fileSystems[0].Mkdir(d, r.attr.Mode&07777|0200, nil) if code != fuse.OK { log.Println("Error creating dir leading to path", d, code, fs.fileSystems[0]) return fuse.EPERM } aTime := r.attr.AccessTime() mTime := r.attr.ModTime() fs.fileSystems[0].Utimens(d, &aTime, &mTime, nil) r.branch = 0 fs.setBranch(d, r) } return fuse.OK } func (fs *unionFS) Create(name string, flags uint32, mode uint32, context *fuse.Context) (fuseFile nodefs.File, code fuse.Status) { writable := fs.fileSystems[0] code = fs.promoteDirsTo(name) if code != fuse.OK { return nil, code } fuseFile, code = writable.Create(name, flags, mode, context) if code.Ok() { fuseFile = fs.newUnionFsFile(fuseFile, 0) fs.removeDeletion(name) now := time.Now() a := fuse.Attr{ Mode: fuse.S_IFREG | mode, } a.SetTimes(nil, &now, &now) fs.setBranch(name, branchResult{&a, fuse.OK, 0}) } return fuseFile, code } func (fs *unionFS) GetAttr(name string, context *fuse.Context) (a *fuse.Attr, s fuse.Status) { _, hidden := fs.hiddenFiles[name] if hidden { return nil, fuse.ENOENT } if name == _DROP_CACHE { return &fuse.Attr{ Mode: fuse.S_IFREG | 0777, }, fuse.OK } if name == fs.options.DeletionDirName { return nil, fuse.ENOENT } isDel, s := fs.isDeleted(name) if !s.Ok() { return nil, s } if isDel { return nil, fuse.ENOENT } r := fs.getBranch(name) if r.branch < 0 { return nil, fuse.ENOENT } fi := *r.attr // Make everything appear writable. fi.Mode |= 0200 return &fi, r.code } func (fs *unionFS) GetXAttr(name string, attr string, context *fuse.Context) ([]byte, fuse.Status) { if name == _DROP_CACHE { return nil, fuse.ENOATTR } r := fs.getBranch(name) if r.branch >= 0 { return fs.fileSystems[r.branch].GetXAttr(name, attr, context) } return nil, fuse.ENOENT } func (fs *unionFS) OpenDir(directory string, context *fuse.Context) (stream []fuse.DirEntry, status fuse.Status) { dirBranch := fs.getBranch(directory) if dirBranch.branch < 0 { return nil, fuse.ENOENT } // We could try to use the cache, but we have a delay, so // might as well get the fresh results async. var wg sync.WaitGroup var deletions map[string]bool wg.Add(1) go func() { deletions = newDirnameMap(fs.fileSystems[0], fs.options.DeletionDirName) wg.Done() }() entries := make([]map[string]uint32, len(fs.fileSystems)) for i := range fs.fileSystems { entries[i] = make(map[string]uint32) } statuses := make([]fuse.Status, len(fs.fileSystems)) for i, l := range fs.fileSystems { if i >= dirBranch.branch { wg.Add(1) go func(j int, pfs pathfs.FileSystem) { ch, s := pfs.OpenDir(directory, context) statuses[j] = s for _, v := range ch { entries[j][v.Name] = v.Mode } wg.Done() }(i, l) } } wg.Wait() if deletions == nil { _, code := fs.fileSystems[0].GetAttr(fs.options.DeletionDirName, context) if code == fuse.ENOENT { deletions = map[string]bool{} } else { return nil, fuse.Status(syscall.EROFS) } } results := entries[0] // TODO(hanwen): should we do anything with the return // statuses? for i, m := range entries { if statuses[i] != fuse.OK { continue } if i == 0 { // We don't need to further process the first // branch: it has no deleted files. continue } for k, v := range m { _, ok := results[k] if ok { continue } deleted := deletions[filePathHash(filepath.Join(directory, k))] if !deleted { results[k] = v } } } if directory == "" { delete(results, fs.options.DeletionDirName) for name, _ := range fs.hiddenFiles { delete(results, name) } } stream = make([]fuse.DirEntry, 0, len(results)) for k, v := range results { stream = append(stream, fuse.DirEntry{ Name: k, Mode: v, }) } return stream, fuse.OK } // recursivePromote promotes path, and if a directory, everything // below that directory. It returns a list of all promoted paths, in // full, including the path itself. func (fs *unionFS) recursivePromote(path string, pathResult branchResult, context *fuse.Context) (names []string, code fuse.Status) { names = []string{} if pathResult.branch > 0 { code = fs.Promote(path, pathResult, context) } if code.Ok() { names = append(names, path) } if code.Ok() && pathResult.attr != nil && pathResult.attr.IsDir() { var stream []fuse.DirEntry stream, code = fs.OpenDir(path, context) for _, e := range stream { if !code.Ok() { break } subnames := []string{} p := filepath.Join(path, e.Name) r := fs.getBranch(p) subnames, code = fs.recursivePromote(p, r, context) names = append(names, subnames...) } } if !code.Ok() { names = nil } return names, code } func (fs *unionFS) renameDirectory(srcResult branchResult, srcDir string, dstDir string, context *fuse.Context) (code fuse.Status) { names := []string{} if code.Ok() { names, code = fs.recursivePromote(srcDir, srcResult, context) } if code.Ok() { code = fs.promoteDirsTo(dstDir) } if code.Ok() { writable := fs.fileSystems[0] code = writable.Rename(srcDir, dstDir, context) } if code.Ok() { for _, srcName := range names { relative := strings.TrimLeft(srcName[len(srcDir):], string(filepath.Separator)) dst := filepath.Join(dstDir, relative) fs.removeDeletion(dst) srcResult := fs.getBranch(srcName) srcResult.branch = 0 fs.setBranch(dst, srcResult) srcResult = fs.branchCache.GetFresh(srcName).(branchResult) if srcResult.branch > 0 { code = fs.putDeletion(srcName) } } } return code } func (fs *unionFS) Rename(src string, dst string, context *fuse.Context) (code fuse.Status) { srcResult := fs.getBranch(src) code = srcResult.code if code.Ok() { code = srcResult.code } if srcResult.attr.IsDir() { return fs.renameDirectory(srcResult, src, dst, context) } if code.Ok() && srcResult.branch > 0 { code = fs.Promote(src, srcResult, context) } if code.Ok() { code = fs.promoteDirsTo(dst) } if code.Ok() { code = fs.fileSystems[0].Rename(src, dst, context) } if code.Ok() { fs.removeDeletion(dst) // Rename is racy; avoid racing with unionFsFile.Release(). fs.branchCache.DropEntry(dst) srcResult := fs.branchCache.GetFresh(src) if srcResult.(branchResult).branch > 0 { code = fs.putDeletion(src) } } return code } func (fs *unionFS) DropBranchCache(names []string) { fs.branchCache.DropAll(names) } func (fs *unionFS) DropDeletionCache() { fs.deletionCache.DropCache() } func (fs *unionFS) DropSubFsCaches() { for _, fs := range fs.fileSystems { a, code := fs.GetAttr(_DROP_CACHE, nil) if code.Ok() && a.IsRegular() { f, _ := fs.Open(_DROP_CACHE, uint32(os.O_WRONLY), nil) if f != nil { f.Flush() f.Release() } } } } func (fs *unionFS) Open(name string, flags uint32, context *fuse.Context) (fuseFile nodefs.File, status fuse.Status) { if name == _DROP_CACHE { if flags&fuse.O_ANYWRITE != 0 { log.Println("Forced cache drop on", fs) fs.DropBranchCache(nil) fs.DropDeletionCache() fs.DropSubFsCaches() fs.nodeFs.ForgetClientInodes() } return nodefs.NewDevNullFile(), fuse.OK } r := fs.getBranch(name) if r.branch < 0 { // This should not happen, as a GetAttr() should have // already verified existence. log.Println("UnionFs: open of non-existent file:", name) return nil, fuse.ENOENT } if flags&fuse.O_ANYWRITE != 0 && r.branch > 0 { code := fs.Promote(name, r, context) if code != fuse.OK { return nil, code } r.branch = 0 now := time.Now() r.attr.SetTimes(nil, &now, nil) fs.setBranch(name, r) } fuseFile, status = fs.fileSystems[r.branch].Open(name, uint32(flags), context) if fuseFile != nil { fuseFile = fs.newUnionFsFile(fuseFile, r.branch) } return fuseFile, status } func (fs *unionFS) String() string { names := []string{} for _, fs := range fs.fileSystems { names = append(names, fs.String()) } return fmt.Sprintf("UnionFs(%v)", names) } func (fs *unionFS) StatFs(name string) *fuse.StatfsOut { return fs.fileSystems[0].StatFs("") } type unionFsFile struct { nodefs.File ufs *unionFS node *nodefs.Inode layer int } func (fs *unionFsFile) String() string { return fmt.Sprintf("unionFsFile(%s)", fs.File.String()) } func (fs *unionFS) newUnionFsFile(f nodefs.File, branch int) *unionFsFile { return &unionFsFile{ File: f, ufs: fs, layer: branch, } } func (fs *unionFsFile) InnerFile() (file nodefs.File) { return fs.File } // We can't hook on Release. Release has no response, so it is not // ordered wrt any following calls. func (fs *unionFsFile) Flush() (code fuse.Status) { code = fs.File.Flush() path := fs.ufs.nodeFs.Path(fs.node) fs.ufs.branchCache.GetFresh(path) return code } func (fs *unionFsFile) SetInode(node *nodefs.Inode) { fs.node = node } func (fs *unionFsFile) GetAttr(out *fuse.Attr) fuse.Status { code := fs.File.GetAttr(out) if code.Ok() { out.Mode |= 0200 } return code }