kubelwagen/vendor/github.com/hanwen/go-fuse/unionfs/unionfs.go
2018-03-31 17:24:57 -07:00

1032 lines
24 KiB
Go

// 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
}