Commit f4cec619 authored by Han-Wen Nienhuys's avatar Han-Wen Nienhuys

unionfs: remove library

This library was created as a mechanism to overlay a writable checkout
for Git on top of Google's readonly SrcFS filesystem. This use case
has long disappeared, and the unionfs library appears unused
otherwise. Due to its reliance on timestamps and delays, it is very
susceptible to flakiness in tests.

Fixing this seems hard or maybe even impossible, so just remove the
library.

Change-Id: I6c594a724a0579b5c4c3aada76cd7013195fda94
parent d8187fce
// 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 (
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"sync"
"syscall"
"time"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
)
type knownFs struct {
unionFS pathfs.FileSystem
nodeFS *pathfs.PathNodeFs
}
// Creates unions for all files under a given directory,
// walking the tree and looking for directories D which have a
// D/READONLY symlink.
//
// A union for A/B/C will placed under directory A-B-C.
type autoUnionFs struct {
pathfs.FileSystem
debug bool
lock sync.RWMutex
zombies map[string]bool
knownFileSystems map[string]knownFs
nameRootMap map[string]string
root string
nodeFs *pathfs.PathNodeFs
options *AutoUnionFsOptions
}
type AutoUnionFsOptions struct {
UnionFsOptions
nodefs.Options
pathfs.PathNodeFsOptions
// If set, run updateKnownFses() after mounting.
UpdateOnMount bool
// If set hides the _READONLY file.
HideReadonly bool
// Expose this version in /status/gounionfs_version
Version string
}
const (
_READONLY = "READONLY"
_STATUS = "status"
_CONFIG = "config"
_DEBUG = "debug"
_ROOT = "root"
_VERSION = "gounionfs_version"
_SCAN_CONFIG = ".scan_config"
)
func NewAutoUnionFs(directory string, options AutoUnionFsOptions) pathfs.FileSystem {
if options.HideReadonly {
options.HiddenFiles = append(options.HiddenFiles, _READONLY)
}
a := &autoUnionFs{
knownFileSystems: make(map[string]knownFs),
nameRootMap: make(map[string]string),
zombies: make(map[string]bool),
options: &options,
FileSystem: pathfs.NewDefaultFileSystem(),
}
directory, err := filepath.Abs(directory)
if err != nil {
panic("filepath.Abs returned err")
}
a.root = directory
return a
}
func (fs *autoUnionFs) String() string {
return fmt.Sprintf("autoUnionFs(%s)", fs.root)
}
func (fs *autoUnionFs) OnMount(nodeFs *pathfs.PathNodeFs) {
fs.nodeFs = nodeFs
if fs.options.UpdateOnMount {
time.AfterFunc(100*time.Millisecond, func() { fs.updateKnownFses() })
}
}
func (fs *autoUnionFs) addAutomaticFs(roots []string) {
relative := strings.TrimLeft(strings.Replace(roots[0], fs.root, "", -1), "/")
name := strings.Replace(relative, "/", "-", -1)
if fs.getUnionFs(name) == nil {
fs.addFs(name, roots)
}
}
func (fs *autoUnionFs) createFs(name string, roots []string) fuse.Status {
fs.lock.Lock()
defer fs.lock.Unlock()
if fs.zombies[name] {
log.Printf("filesystem named %q is being removed", name)
return fuse.EBUSY
}
for workspace, root := range fs.nameRootMap {
if root == roots[0] && workspace != name {
log.Printf("Already have a union FS for directory %s in workspace %s",
roots[0], workspace)
return fuse.EBUSY
}
}
known := fs.knownFileSystems[name]
if known.unionFS != nil {
log.Println("Already have a workspace:", name)
return fuse.EBUSY
}
ufs, err := NewUnionFsFromRoots(roots, &fs.options.UnionFsOptions, true)
if err != nil {
log.Println("Could not create UnionFs:", err)
return fuse.EPERM
}
log.Printf("Adding workspace %v for roots %v", name, ufs.String())
nfs := pathfs.NewPathNodeFs(ufs, &fs.options.PathNodeFsOptions)
code := fs.nodeFs.Mount(name, nfs.Root(), &fs.options.Options)
if code.Ok() {
fs.knownFileSystems[name] = knownFs{
ufs,
nfs,
}
fs.nameRootMap[name] = roots[0]
}
return code
}
func (fs *autoUnionFs) rmFs(name string) (code fuse.Status) {
fs.lock.Lock()
defer fs.lock.Unlock()
if fs.zombies[name] {
return fuse.ENOENT
}
known := fs.knownFileSystems[name]
if known.unionFS == nil {
return fuse.ENOENT
}
root := fs.nameRootMap[name]
delete(fs.knownFileSystems, name)
delete(fs.nameRootMap, name)
fs.zombies[name] = true
fs.lock.Unlock()
code = fs.nodeFs.Unmount(name)
fs.lock.Lock()
delete(fs.zombies, name)
if !code.Ok() {
// Reinstate.
log.Printf("Unmount failed for %s. Code %v", name, code)
fs.knownFileSystems[name] = known
fs.nameRootMap[name] = root
}
return code
}
func (fs *autoUnionFs) addFs(name string, roots []string) (code fuse.Status) {
if name == _CONFIG || name == _STATUS || name == _SCAN_CONFIG {
return fuse.EINVAL
}
return fs.createFs(name, roots)
}
func (fs *autoUnionFs) getRoots(path string) []string {
ro := filepath.Join(path, _READONLY)
fi, err := os.Lstat(ro)
fiDir, errDir := os.Stat(ro)
if err != nil || errDir != nil {
return nil
}
if fi.Mode()&os.ModeSymlink != 0 && fiDir.IsDir() {
// TODO - should recurse and chain all READONLYs
// together.
return []string{path, ro}
}
return nil
}
func (fs *autoUnionFs) visit(path string, fi os.FileInfo, err error) error {
if fi != nil && fi.IsDir() {
roots := fs.getRoots(path)
if roots != nil {
fs.addAutomaticFs(roots)
}
}
return nil
}
func (fs *autoUnionFs) updateKnownFses() {
// We unroll the first level of entries in the root manually in order
// to allow symbolic links on that level.
directoryEntries, err := ioutil.ReadDir(fs.root)
if err == nil {
for _, dir := range directoryEntries {
if dir.IsDir() || dir.Mode()&os.ModeSymlink != 0 {
path := filepath.Join(fs.root, dir.Name())
dir, _ = os.Stat(path)
fs.visit(path, dir, nil)
filepath.Walk(path,
func(path string, fi os.FileInfo, err error) error {
return fs.visit(path, fi, err)
})
}
}
}
}
func (fs *autoUnionFs) Readlink(path string, context *fuse.Context) (out string, code fuse.Status) {
comps := strings.Split(path, string(filepath.Separator))
if comps[0] == _STATUS && comps[1] == _ROOT {
return fs.root, fuse.OK
}
if comps[0] != _CONFIG {
return "", fuse.ENOENT
}
name := comps[1]
fs.lock.RLock()
defer fs.lock.RUnlock()
root, ok := fs.nameRootMap[name]
if ok {
return root, fuse.OK
}
return "", fuse.ENOENT
}
func (fs *autoUnionFs) getUnionFs(name string) pathfs.FileSystem {
fs.lock.RLock()
defer fs.lock.RUnlock()
return fs.knownFileSystems[name].unionFS
}
func (fs *autoUnionFs) Symlink(pointedTo string, linkName string, context *fuse.Context) (code fuse.Status) {
comps := strings.Split(linkName, "/")
if len(comps) != 2 {
return fuse.EPERM
}
if comps[0] == _CONFIG {
roots := fs.getRoots(pointedTo)
if roots == nil {
return fuse.Status(syscall.ENOTDIR)
}
name := comps[1]
return fs.addFs(name, roots)
}
return fuse.EPERM
}
func (fs *autoUnionFs) Unlink(path string, context *fuse.Context) (code fuse.Status) {
comps := strings.Split(path, "/")
if len(comps) != 2 {
return fuse.EPERM
}
if comps[0] == _CONFIG && comps[1] != _SCAN_CONFIG {
code = fs.rmFs(comps[1])
} else {
code = fuse.ENOENT
}
return code
}
// Must define this, because ENOSYS will suspend all GetXAttr calls.
func (fs *autoUnionFs) GetXAttr(name string, attr string, context *fuse.Context) ([]byte, fuse.Status) {
return nil, fuse.ENOATTR
}
func (fs *autoUnionFs) GetAttr(path string, context *fuse.Context) (*fuse.Attr, fuse.Status) {
a := &fuse.Attr{
Owner: *fuse.CurrentOwner(),
}
if path == "" || path == _CONFIG || path == _STATUS {
a.Mode = fuse.S_IFDIR | 0755
return a, fuse.OK
}
if path == filepath.Join(_STATUS, _VERSION) {
a.Mode = fuse.S_IFREG | 0644
a.Size = uint64(len(fs.options.Version))
return a, fuse.OK
}
if path == filepath.Join(_STATUS, _DEBUG) {
a.Mode = fuse.S_IFREG | 0644
a.Size = uint64(len(fs.DebugData()))
return a, fuse.OK
}
if path == filepath.Join(_STATUS, _ROOT) {
a.Mode = syscall.S_IFLNK | 0644
return a, fuse.OK
}
if path == filepath.Join(_CONFIG, _SCAN_CONFIG) {
a.Mode = fuse.S_IFREG | 0644
return a, fuse.OK
}
comps := strings.Split(path, string(filepath.Separator))
if len(comps) > 1 && comps[0] == _CONFIG {
fs := fs.getUnionFs(comps[1])
if fs == nil {
return nil, fuse.ENOENT
}
a.Mode = syscall.S_IFLNK | 0644
return a, fuse.OK
}
return nil, fuse.ENOENT
}
func (fs *autoUnionFs) StatusDir() (stream []fuse.DirEntry, status fuse.Status) {
stream = make([]fuse.DirEntry, 0, 10)
stream = []fuse.DirEntry{
{Name: _VERSION, Mode: fuse.S_IFREG | 0644},
{Name: _DEBUG, Mode: fuse.S_IFREG | 0644},
{Name: _ROOT, Mode: syscall.S_IFLNK | 0644},
}
return stream, fuse.OK
}
func (fs *autoUnionFs) DebugData() string {
conn := fs.nodeFs.Connector()
if conn.Server() == nil {
return "autoUnionFs.mountState not set"
}
setting := conn.Server().KernelSettings()
msg := fmt.Sprintf(
"Version: %v\n"+
"Bufferpool: %v\n"+
"Kernel: %v\n",
fs.options.Version,
conn.Server().DebugData(),
&setting)
if conn != nil {
msg += fmt.Sprintf("Live inodes: %d\n", conn.InodeHandleCount())
}
return msg
}
func (fs *autoUnionFs) Open(path string, flags uint32, context *fuse.Context) (nodefs.File, fuse.Status) {
if path == filepath.Join(_STATUS, _DEBUG) {
if flags&fuse.O_ANYWRITE != 0 {
return nil, fuse.EPERM
}
return nodefs.NewDataFile([]byte(fs.DebugData())), fuse.OK
}
if path == filepath.Join(_STATUS, _VERSION) {
if flags&fuse.O_ANYWRITE != 0 {
return nil, fuse.EPERM
}
return nodefs.NewDataFile([]byte(fs.options.Version)), fuse.OK
}
if path == filepath.Join(_CONFIG, _SCAN_CONFIG) {
if flags&fuse.O_ANYWRITE != 0 {
fs.updateKnownFses()
}
return nodefs.NewDevNullFile(), fuse.OK
}
return nil, fuse.ENOENT
}
func (fs *autoUnionFs) Truncate(name string, offset uint64, context *fuse.Context) (code fuse.Status) {
if name != filepath.Join(_CONFIG, _SCAN_CONFIG) {
log.Println("Huh? Truncating unsupported write file", name)
return fuse.EPERM
}
return fuse.OK
}
func (fs *autoUnionFs) OpenDir(name string, context *fuse.Context) (stream []fuse.DirEntry, status fuse.Status) {
switch name {
case _STATUS:
return fs.StatusDir()
case _CONFIG:
case "/":
name = ""
case "":
default:
log.Printf("Argh! Don't know how to list dir %v", name)
return nil, fuse.ENOSYS
}
fs.lock.RLock()
defer fs.lock.RUnlock()
stream = make([]fuse.DirEntry, 0, len(fs.knownFileSystems)+5)
if name == _CONFIG {
for k := range fs.knownFileSystems {
stream = append(stream, fuse.DirEntry{
Name: k,
Mode: syscall.S_IFLNK | 0644,
})
}
}
if name == "" {
stream = append(stream, fuse.DirEntry{
Name: _CONFIG,
Mode: uint32(fuse.S_IFDIR | 0755),
},
fuse.DirEntry{
Name: _STATUS,
Mode: uint32(fuse.S_IFDIR | 0755),
})
}
return stream, status
}
func (fs *autoUnionFs) StatFs(name string) *fuse.StatfsOut {
return &fuse.StatfsOut{}
}
// 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 (
"io/ioutil"
"os"
"testing"
"time"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
"github.com/hanwen/go-fuse/v2/internal/testutil"
)
const entryTTL = 100 * time.Millisecond
var testAOpts = AutoUnionFsOptions{
UnionFsOptions: testOpts,
Options: nodefs.Options{
EntryTimeout: entryTTL,
AttrTimeout: entryTTL,
NegativeTimeout: 0,
Debug: testutil.VerboseTest(),
LookupKnownChildren: true,
},
HideReadonly: true,
Version: "version",
}
func init() {
testAOpts.Options.Debug = testutil.VerboseTest()
}
func WriteFile(t *testing.T, name string, contents string) {
err := ioutil.WriteFile(name, []byte(contents), 0644)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
}
func setup(t *testing.T) (workdir string, server *fuse.Server, cleanup func()) {
wd := testutil.TempDir()
err := os.Mkdir(wd+"/mnt", 0700)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
err = os.Mkdir(wd+"/store", 0700)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
os.Mkdir(wd+"/ro", 0700)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
WriteFile(t, wd+"/ro/file1", "file1")
WriteFile(t, wd+"/ro/file2", "file2")
fs := NewAutoUnionFs(wd+"/store", testAOpts)
nfs := pathfs.NewPathNodeFs(fs, nil)
state, _, err := nodefs.MountRoot(wd+"/mnt", nfs.Root(), &testAOpts.Options)
if err != nil {
t.Fatalf("MountNodeFileSystem failed: %v", err)
}
go state.Serve()
state.WaitMount()
return wd, state, func() {
state.Unmount()
os.RemoveAll(wd)
}
}
func TestDebug(t *testing.T) {
wd, _, clean := setup(t)
defer clean()
c, err := ioutil.ReadFile(wd + "/mnt/status/debug")
if err != nil {
t.Fatalf("ReadFile failed: %v", err)
}
if len(c) == 0 {
t.Fatal("No debug found.")
}
}
func TestVersion(t *testing.T) {
wd, _, clean := setup(t)
defer clean()
c, err := ioutil.ReadFile(wd + "/mnt/status/gounionfs_version")
if err != nil {
t.Fatalf("ReadFile failed: %v", err)
}
if len(c) == 0 {
t.Fatal("No version found.")
}
}
func TestAutoFsSymlink(t *testing.T) {
wd, server, clean := setup(t)
defer clean()
err := os.Mkdir(wd+"/store/backing1", 0755)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
err = os.Symlink(wd+"/ro", wd+"/store/backing1/READONLY")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
err = os.Symlink(wd+"/store/backing1", wd+"/mnt/config/manual1")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
fi, err := os.Lstat(wd + "/mnt/manual1/file1")
if err != nil {
t.Fatalf("Lstat failed: %v", err)
}
entries, err := ioutil.ReadDir(wd + "/mnt")
if err != nil {
t.Fatalf("ReadDir failed: %v", err)
}
if len(entries) != 3 {
t.Error("readdir mismatch", entries)
}
err = os.Remove(wd + "/mnt/config/manual1")
if err != nil {
t.Fatalf("Remove failed: %v", err)
}
scan := wd + "/mnt/config/" + _SCAN_CONFIG
err = ioutil.WriteFile(scan, []byte("something"), 0644)
if err != nil {
t.Error("error writing:", err)
}
// If FUSE supports invalid inode notifications we expect this node to be gone. Otherwise we'll just make sure that it's not reachable.
if server.KernelSettings().SupportsNotify(fuse.NOTIFY_INVAL_INODE) {
fi, _ = os.Lstat(wd + "/mnt/manual1")
if fi != nil {
t.Error("Should not have file:", fi)
}
} else {
entries, err = ioutil.ReadDir(wd + "/mnt")
if err != nil {
t.Fatalf("ReadDir failed: %v", err)
}
for _, e := range entries {
if e.Name() == "manual1" {
t.Error("Should not have entry: ", e)
}
}
}
_, err = os.Lstat(wd + "/mnt/backing1/file1")
if err != nil {
t.Fatalf("Lstat failed: %v", err)
}
}
func TestDetectSymlinkedDirectories(t *testing.T) {
wd, _, clean := setup(t)
defer clean()
err := os.Mkdir(wd+"/backing1", 0755)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
err = os.Symlink(wd+"/ro", wd+"/backing1/READONLY")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
err = os.Symlink(wd+"/backing1", wd+"/store/backing1")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
scan := wd + "/mnt/config/" + _SCAN_CONFIG
err = ioutil.WriteFile(scan, []byte("something"), 0644)
if err != nil {
t.Error("error writing:", err)
}
_, err = os.Lstat(wd + "/mnt/backing1")
if err != nil {
t.Fatalf("Lstat failed: %v", err)
}
}
func TestExplicitScan(t *testing.T) {
wd, _, clean := setup(t)
defer clean()
err := os.Mkdir(wd+"/store/backing1", 0755)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
os.Symlink(wd+"/ro", wd+"/store/backing1/READONLY")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
fi, _ := os.Lstat(wd + "/mnt/backing1")
if fi != nil {
t.Error("Should not have file:", fi)
}
scan := wd + "/mnt/config/" + _SCAN_CONFIG
_, err = os.Lstat(scan)
if err != nil {
t.Error(".scan_config missing:", err)
}
err = ioutil.WriteFile(scan, []byte("something"), 0644)
if err != nil {
t.Error("error writing:", err)
}
_, err = os.Lstat(wd + "/mnt/backing1")
if err != nil {
t.Error("Should have workspace backing1:", err)
}
}
func TestCreationChecks(t *testing.T) {
wd, _, clean := setup(t)
defer clean()
err := os.Mkdir(wd+"/store/foo", 0755)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
os.Symlink(wd+"/ro", wd+"/store/foo/READONLY")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
err = os.Mkdir(wd+"/store/ws2", 0755)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
os.Symlink(wd+"/ro", wd+"/store/ws2/READONLY")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
err = os.Symlink(wd+"/store/foo", wd+"/mnt/config/bar")
if err != nil {
t.Fatalf("Symlink failed: %v", err)
}
err = os.Symlink(wd+"/store/foo", wd+"/mnt/config/foo")
code := fuse.ToStatus(err)
if code != fuse.EBUSY {
t.Error("Should return EBUSY", err)
}
err = os.Symlink(wd+"/store/ws2", wd+"/mnt/config/config")
code = fuse.ToStatus(err)
if code != fuse.EINVAL {
t.Error("Should return EINVAL", err)
}
}
// 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 (
"fmt"
"log"
"strings"
"time"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
)
const _XATTRSEP = "@XATTR@"
type attrResponse struct {
*fuse.Attr
fuse.Status
}
type xattrResponse struct {
data []byte
fuse.Status
}
type dirResponse struct {
entries []fuse.DirEntry
fuse.Status
}
type linkResponse struct {
linkContent string
fuse.Status
}
// Caches filesystem metadata.
type cachingFileSystem struct {
pathfs.FileSystem
attributes *TimedCache
dirs *TimedCache
links *TimedCache
xattr *TimedCache
}
func readDir(fs pathfs.FileSystem, name string) *dirResponse {
origStream, code := fs.OpenDir(name, nil)
r := &dirResponse{nil, code}
if !code.Ok() {
return r
}
r.entries = origStream
return r
}
func getAttr(fs pathfs.FileSystem, name string) *attrResponse {
a, code := fs.GetAttr(name, nil)
return &attrResponse{
Attr: a,
Status: code,
}
}
func getXAttr(fs pathfs.FileSystem, nameAttr string) *xattrResponse {
ns := strings.SplitN(nameAttr, _XATTRSEP, 2)
a, code := fs.GetXAttr(ns[0], ns[1], nil)
return &xattrResponse{
data: a,
Status: code,
}
}
func readLink(fs pathfs.FileSystem, name string) *linkResponse {
a, code := fs.Readlink(name, nil)
return &linkResponse{
linkContent: a,
Status: code,
}
}
func NewCachingFileSystem(fs pathfs.FileSystem, ttl time.Duration) pathfs.FileSystem {
c := new(cachingFileSystem)
c.FileSystem = fs
c.attributes = NewTimedCache(func(n string) (interface{}, bool) {
a := getAttr(fs, n)
return a, a.Ok()
}, ttl)
c.dirs = NewTimedCache(func(n string) (interface{}, bool) {
d := readDir(fs, n)
return d, d.Ok()
}, ttl)
c.links = NewTimedCache(func(n string) (interface{}, bool) {
l := readLink(fs, n)
return l, l.Ok()
}, ttl)
c.xattr = NewTimedCache(func(n string) (interface{}, bool) {
l := getXAttr(fs, n)
return l, l.Ok()
}, ttl)
return c
}
func (fs *cachingFileSystem) DropCache() {
for _, c := range []*TimedCache{fs.attributes, fs.dirs, fs.links, fs.xattr} {
c.DropAll(nil)
}
}
func (fs *cachingFileSystem) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.Status) {
if name == _DROP_CACHE {
return &fuse.Attr{
Mode: fuse.S_IFREG | 0777,
}, fuse.OK
}
r := fs.attributes.Get(name).(*attrResponse)
return r.Attr, r.Status
}
func (fs *cachingFileSystem) GetXAttr(name string, attr string, context *fuse.Context) ([]byte, fuse.Status) {
key := name + _XATTRSEP + attr
r := fs.xattr.Get(key).(*xattrResponse)
return r.data, r.Status
}
func (fs *cachingFileSystem) Readlink(name string, context *fuse.Context) (string, fuse.Status) {
r := fs.links.Get(name).(*linkResponse)
return r.linkContent, r.Status
}
func (fs *cachingFileSystem) OpenDir(name string, context *fuse.Context) (stream []fuse.DirEntry, status fuse.Status) {
r := fs.dirs.Get(name).(*dirResponse)
return r.entries, r.Status
}
func (fs *cachingFileSystem) String() string {
return fmt.Sprintf("cachingFileSystem(%v)", fs.FileSystem)
}
func (fs *cachingFileSystem) Open(name string, flags uint32, context *fuse.Context) (f nodefs.File, status fuse.Status) {
if flags&fuse.O_ANYWRITE != 0 && name == _DROP_CACHE {
log.Println("Dropping cache for", fs)
fs.DropCache()
}
return fs.FileSystem.Open(name, flags, context)
}
// 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 (
"os"
"syscall"
"testing"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
"github.com/hanwen/go-fuse/v2/internal/testutil"
)
func modeMapEq(m1, m2 map[string]uint32) bool {
if len(m1) != len(m2) {
return false
}
for k, v := range m1 {
val, ok := m2[k]
if !ok || val != v {
return false
}
}
return true
}
func TestCachingFs(t *testing.T) {
wd := testutil.TempDir()
defer os.RemoveAll(wd)
fs := pathfs.NewLoopbackFileSystem(wd)
cfs := NewCachingFileSystem(fs, 0)
os.Mkdir(wd+"/orig", 0755)
fi, code := cfs.GetAttr("orig", nil)
if !code.Ok() {
t.Fatal("GetAttr failure", code)
}
if !fi.IsDir() {
t.Error("unexpected attr", fi)
}
os.Symlink("orig", wd+"/symlink")
val, code := cfs.Readlink("symlink", nil)
if val != "orig" {
t.Error("unexpected readlink", val)
}
if !code.Ok() {
t.Error("code !ok ", code)
}
stream, code := cfs.OpenDir("", nil)
if !code.Ok() {
t.Fatal("Readdir fail", code)
}
results := make(map[string]uint32)
for _, v := range stream {
results[v.Name] = v.Mode &^ 07777
}
expected := map[string]uint32{
"symlink": syscall.S_IFLNK,
"orig": fuse.S_IFDIR,
}
if !modeMapEq(results, expected) {
t.Error("Unexpected readdir result", results, expected)
}
}
// 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 (
"os"
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
)
func NewUnionFsFromRoots(roots []string, opts *UnionFsOptions, roCaching bool) (pathfs.FileSystem, error) {
fses := make([]pathfs.FileSystem, 0)
for i, r := range roots {
var fs pathfs.FileSystem
fi, err := os.Stat(r)
if err != nil {
return nil, err
}
if fi.IsDir() {
fs = pathfs.NewLoopbackFileSystem(r)
}
if fs == nil {
return nil, err
}
if i > 0 && roCaching {
fs = NewCachingFileSystem(fs, 0)
}
fses = append(fses, fs)
}
return NewUnionFs(fses, *opts)
}
// 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 (
"sync"
"time"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
)
// newDirnameMap reads the contents of the given directory. On error,
// returns a nil map. This forces reloads in the dirCache until we
// succeed.
func newDirnameMap(fs pathfs.FileSystem, dir string) map[string]struct{} {
stream, code := fs.OpenDir(dir, nil)
if code == fuse.ENOENT {
// The directory not existing is not an error.
return map[string]struct{}{}
}
if !code.Ok() {
return nil
}
result := make(map[string]struct{})
for _, e := range stream {
if e.Mode&fuse.S_IFREG != 0 {
result[e.Name] = struct{}{}
}
}
return result
}
// dirCache caches names in a directory for some time.
//
// If called when the cache is expired, the filenames are read afresh in
// the background.
type dirCache struct {
dir string
ttl time.Duration
fs pathfs.FileSystem
// Protects data below.
lock sync.RWMutex
// If nil, you may call refresh() to schedule a new one.
names map[string]struct{}
updateRunning bool
}
func (c *dirCache) setMap(newMap map[string]struct{}) {
c.lock.Lock()
defer c.lock.Unlock()
c.names = newMap
c.updateRunning = false
_ = time.AfterFunc(c.ttl,
func() { c.DropCache() })
}
func (c *dirCache) DropCache() {
c.lock.Lock()
defer c.lock.Unlock()
c.names = nil
}
// Try to refresh: if another update is already running, do nothing,
// otherwise, read the directory and set it.
func (c *dirCache) maybeRefresh() {
c.lock.Lock()
defer c.lock.Unlock()
if c.updateRunning {
return
}
c.updateRunning = true
go func() {
newmap := newDirnameMap(c.fs, c.dir)
c.setMap(newmap)
}()
}
func (c *dirCache) RemoveEntry(name string) {
c.lock.Lock()
defer c.lock.Unlock()
if c.names == nil {
go c.maybeRefresh()
return
}
delete(c.names, name)
}
func (c *dirCache) AddEntry(name string) {
c.lock.Lock()
defer c.lock.Unlock()
if c.names == nil {
go c.maybeRefresh()
return
}
c.names[name] = struct{}{}
}
func newDirCache(fs pathfs.FileSystem, dir string, ttl time.Duration) *dirCache {
dc := new(dirCache)
dc.dir = dir
dc.fs = fs
dc.ttl = ttl
return dc
}
func (c *dirCache) HasEntry(name string) (mapPresent bool, found bool) {
c.lock.RLock()
defer c.lock.RUnlock()
if c.names == nil {
go c.maybeRefresh()
return false, false
}
_, ok := c.names[name]
return true, ok
}
// 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 (
"sync"
"time"
)
type cacheEntry struct {
data interface{}
// expiry is the absolute timestamp of the expiry.
expiry time.Time
}
// TimedCache caches the result of fetch() for some time. It is
// thread-safe. Calls of fetch() do no happen inside a critical
// section, so when multiple concurrent Get()s happen for the same
// key, multiple fetch() calls may be issued for the same key.
type TimedCacheFetcher func(name string) (value interface{}, cacheable bool)
type TimedCache struct {
fetch TimedCacheFetcher
// ttl is the duration of the cache.
ttl time.Duration
cacheMapMutex sync.RWMutex
cacheMap map[string]*cacheEntry
PurgeTimer *time.Timer
}
// Creates a new cache with the given TTL. If TTL <= 0, the caching is
// indefinite.
func NewTimedCache(fetcher TimedCacheFetcher, ttl time.Duration) *TimedCache {
l := new(TimedCache)
l.ttl = ttl
l.fetch = fetcher
l.cacheMap = make(map[string]*cacheEntry)
return l
}
func (c *TimedCache) Get(name string) interface{} {
c.cacheMapMutex.RLock()
info, ok := c.cacheMap[name]
c.cacheMapMutex.RUnlock()
valid := ok && (c.ttl <= 0 || info.expiry.After(time.Now()))
if valid {
return info.data
}
return c.GetFresh(name)
}
func (c *TimedCache) Set(name string, val interface{}) {
c.cacheMapMutex.Lock()
defer c.cacheMapMutex.Unlock()
c.cacheMap[name] = &cacheEntry{
data: val,
expiry: time.Now().Add(c.ttl),
}
}
func (c *TimedCache) DropEntry(name string) {
c.cacheMapMutex.Lock()
defer c.cacheMapMutex.Unlock()
delete(c.cacheMap, name)
}
func (c *TimedCache) GetFresh(name string) interface{} {
data, ok := c.fetch(name)
if ok {
c.Set(name, data)
}
return data
}
// Drop all expired entries.
func (c *TimedCache) Purge() {
keys := make([]string, 0, len(c.cacheMap))
now := time.Now()
c.cacheMapMutex.Lock()
defer c.cacheMapMutex.Unlock()
for k, v := range c.cacheMap {
if now.After(v.expiry) {
keys = append(keys, k)
}
}
for _, k := range keys {
delete(c.cacheMap, k)
}
}
func (c *TimedCache) DropAll(names []string) {
c.cacheMapMutex.Lock()
defer c.cacheMapMutex.Unlock()
if names == nil {
c.cacheMap = make(map[string]*cacheEntry, len(c.cacheMap))
} else {
for _, nm := range names {
delete(c.cacheMap, nm)
}
}
}
// 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 (
"testing"
"time"
)
func TestTimedCacheUncacheable(t *testing.T) {
fetchCount := 0
fetch := func(n string) (interface{}, bool) {
fetchCount++
i := int(n[0])
return &i, false
}
cache := NewTimedCache(fetch, 0)
v := cache.Get("n").(*int)
w := cache.Get("n").(*int)
if *v != int('n') || *w != *v {
t.Errorf("value mismatch: got %d, %d want %d", *v, *w, int('n'))
}
if fetchCount != 2 {
t.Fatalf("Should have fetched twice: %d", fetchCount)
}
}
func TestTimedCache(t *testing.T) {
fetchCount := 0
fetch := func(n string) (interface{}, bool) {
fetchCount++
i := int(n[0])
return &i, true
}
// This fails with 1e6 on some Opteron CPUs.
ttl := 100 * time.Millisecond
cache := NewTimedCache(fetch, ttl)
v := cache.Get("n").(*int)
if *v != int('n') {
t.Errorf("value mismatch: got %d, want %d", *v, int('n'))
}
if fetchCount != 1 {
t.Errorf("fetch count mismatch: got %d want 1", fetchCount)
}
// The cache update is async.
time.Sleep(time.Duration(ttl / 10))
w := cache.Get("n")
if v != w {
t.Errorf("Huh, inconsistent: 1st = %v != 2nd = %v", v, w)
}
if fetchCount > 1 {
t.Errorf("fetch count fail: %d > 1", fetchCount)
}
time.Sleep(time.Duration(ttl * 2))
cache.Purge()
w = cache.Get("n")
if fetchCount == 1 {
t.Error("Did not fetch again. Purge unsuccessful?")
}
}
// 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/v2/fuse"
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
"github.com/hanwen/go-fuse/v2/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)
// 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)
}
// Update in-memory cache as last step, so we avoid caching a
// state from before the storage update.
fs.deletionCache.RemoveEntry(path.Base(marker))
}
func (fs *unionFS) putDeletion(name string) (code fuse.Status) {
code = fs.createDeletionStore()
if !code.Ok() {
return code
}
marker := fs.deletionPath(name)
// 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))
}
// Update the in-memory deletion cache as the last step,
// to ensure that the new state stays in memory
fs.deletionCache.AddEntry(path.Base(marker))
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() {
newAttr := *r.attr
r.attr = &newAttr
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()
newAttr := *r.attr
r.attr = &newAttr
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
}
newAttr := *r.attr
r.attr = &newAttr
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
}
newAttr := *r.attr
r.attr = &newAttr
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]struct{}
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]struct{}{}
} 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) fuse.Status {
srcResult := fs.getBranch(src)
if !srcResult.code.Ok() {
return srcResult.code
}
if srcResult.attr.IsDir() {
return fs.renameDirectory(srcResult, src, dst, context)
}
if srcResult.branch > 0 {
if code := fs.Promote(src, srcResult, context); !code.Ok() {
return code
}
}
if code := fs.promoteDirsTo(dst); !code.Ok() {
return code
}
if code := fs.fileSystems[0].Rename(src, dst, context); !code.Ok() {
return code
}
fs.removeDeletion(dst)
// Rename is racy; avoid racing with unionFsFile.Release().
fs.branchCache.DropEntry(dst)
srcResult = fs.branchCache.GetFresh(src).(branchResult)
if srcResult.branch > 0 {
return fs.putDeletion(src)
}
return fuse.OK
}
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
}
// 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 (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"testing"
"time"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
"github.com/hanwen/go-fuse/v2/internal/testutil"
"github.com/hanwen/go-fuse/v2/posixtest"
)
func TestFilePathHash(t *testing.T) {
got := filePathHash("xyz/abc")
want := "34d52a6371ee5c79-abc"
if got != want {
t.Errorf("got %q want %q", got, want)
}
}
var testOpts = UnionFsOptions{
DeletionCacheTTL: entryTTL,
DeletionDirName: "DELETIONS",
BranchCacheTTL: entryTTL,
HiddenFiles: []string{"hidden"},
}
func setRecursiveWritable(t *testing.T, dir string, writable bool) {
err := filepath.Walk(
dir,
func(path string, fi os.FileInfo, err error) error {
var newMode uint32
if writable {
newMode = uint32(fi.Mode().Perm()) | 0200
} else {
newMode = uint32(fi.Mode().Perm()) &^ 0222
}
if fi.Mode()|os.ModeSymlink != 0 {
return nil
}
return os.Chmod(path, os.FileMode(newMode))
})
if err != nil {
t.Fatalf("Walk: %v", err)
}
}
// Creates a temporary dir "wd" with 3 directories:
// mnt ... overlayed (unionfs) mount
// rw .... modifiable data
// ro .... read-only data
func setupUfs(t *testing.T) (wd string, cleanup func()) {
// Make sure system setting does not affect test.
syscall.Umask(0)
wd = testutil.TempDir()
err := os.Mkdir(wd+"/mnt", 0700)
if err != nil {
t.Fatalf("Mkdir: %v", err)
}
err = os.Mkdir(wd+"/rw", 0700)
if err != nil {
t.Fatalf("Mkdir: %v", err)
}
os.Mkdir(wd+"/ro", 0700)
if err != nil {
t.Fatalf("Mkdir: %v", err)
}
fses := []pathfs.FileSystem{
pathfs.NewLoopbackFileSystem(wd + "/rw"),
NewCachingFileSystem(pathfs.NewLoopbackFileSystem(wd+"/ro"), 0),
}
ufs, err := NewUnionFs(fses, testOpts)
if err != nil {
t.Fatalf("NewUnionFs: %v", err)
}
// We configure timeouts are smaller, so we can check for
// UnionFs's cache consistency.
opts := &nodefs.Options{
EntryTimeout: entryTTL / 2,
AttrTimeout: entryTTL / 2,
NegativeTimeout: entryTTL / 2,
PortableInodes: true,
Debug: testutil.VerboseTest(),
LookupKnownChildren: true,
}
pathfs := pathfs.NewPathNodeFs(ufs,
&pathfs.PathNodeFsOptions{ClientInodes: true,
Debug: opts.Debug,
})
state, _, err := nodefs.MountRoot(wd+"/mnt", pathfs.Root(), opts)
if err != nil {
t.Fatalf("MountNodeFileSystem: %v", err)
}
go state.Serve()
state.WaitMount()
return wd, func() {
err := state.Unmount()
if err != nil {
return
}
setRecursiveWritable(t, wd, true)
os.RemoveAll(wd)
}
}
func readFromFile(t *testing.T, path string) string {
b, err := ioutil.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
return string(b)
}
func dirNames(t *testing.T, path string) map[string]bool {
f, err := os.Open(path)
if err != nil {
t.Fatalf("Open: %v", err)
}
result := make(map[string]bool)
names, err := f.Readdirnames(-1)
if err != nil {
t.Fatalf("Readdirnames: %v", err)
}
err = f.Close()
if err != nil {
t.Fatalf("Close: %v", err)
}
for _, nm := range names {
result[nm] = true
}
return result
}
func checkMapEq(t *testing.T, m1, m2 map[string]bool) {
if !mapEq(m1, m2) {
msg := fmt.Sprintf("mismatch: got %v != expect %v", m1, m2)
panic(msg)
}
}
func mapEq(m1, m2 map[string]bool) bool {
if len(m1) != len(m2) {
return false
}
for k, v := range m1 {
val, ok := m2[k]
if !ok || val != v {
return false
}
}
return true
}
func fileExists(path string) bool {
f, err := os.Lstat(path)
return err == nil && f != nil
}
func TestUnionFsAutocreateDeletionDir(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := os.Remove(wd + "/rw/DELETIONS")
if err != nil {
t.Fatalf("Remove: %v", err)
}
err = os.Mkdir(wd+"/mnt/dir", 0755)
if err != nil {
t.Fatalf("Mkdir: %v", err)
}
_, err = ioutil.ReadDir(wd + "/mnt/dir")
if err != nil {
t.Fatalf("ReadDir: %v", err)
}
}
func TestUnionFsSymlink(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
posixtest.SymlinkReadlink(t, wd+"/mnt")
}
func TestUnionFsSymlinkPromote(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := os.Mkdir(wd+"/ro/subdir", 0755)
if err != nil {
t.Fatalf("Mkdir: %v", err)
}
err = os.Symlink("/foobar", wd+"/mnt/subdir/link")
if err != nil {
t.Fatalf("Symlink: %v", err)
}
}
func TestUnionFsChtimes(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
WriteFile(t, wd+"/ro/file", "a")
err := os.Chtimes(wd+"/ro/file", time.Unix(42, 0), time.Unix(43, 0))
if err != nil {
t.Fatalf("Chtimes: %v", err)
}
err = os.Chtimes(wd+"/mnt/file", time.Unix(82, 0), time.Unix(83, 0))
if err != nil {
t.Fatalf("Chtimes: %v", err)
}
fi, err := os.Lstat(wd + "/mnt/file")
attr := &fuse.Attr{}
attr.FromStat(fuse.ToStatT(fi))
if attr.Atime != 82 || attr.Mtime != 83 {
t.Error("Incorrect timestamp", fi)
}
}
func TestUnionFsChmod(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
ro_fn := wd + "/ro/file"
m_fn := wd + "/mnt/file"
WriteFile(t, ro_fn, "a")
err := os.Chmod(m_fn, 00070)
if err != nil {
t.Fatalf("Chmod: %v", err)
}
fi, err := os.Lstat(m_fn)
if err != nil {
t.Fatalf("Lstat: %v", err)
}
if fi.Mode()&07777 != 00270 {
t.Errorf("Unexpected mode found: %o", uint32(fi.Mode().Perm()))
}
_, err = os.Lstat(wd + "/rw/file")
if err != nil {
t.Errorf("File not promoted")
}
}
func TestUnionFsChown(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
ro_fn := wd + "/ro/file"
m_fn := wd + "/mnt/file"
WriteFile(t, ro_fn, "a")
err := os.Chown(m_fn, 0, 0)
code := fuse.ToStatus(err)
if code != fuse.EPERM {
t.Error("Unexpected error code", code, err)
}
}
func TestUnionFsDelete(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
WriteFile(t, wd+"/ro/file", "a")
_, err := os.Lstat(wd + "/mnt/file")
if err != nil {
t.Fatalf("Lstat: %v", err)
}
err = os.Remove(wd + "/mnt/file")
if err != nil {
t.Fatalf("Remove: %v", err)
}
_, err = os.Lstat(wd + "/mnt/file")
if err == nil {
t.Fatal("should have disappeared.")
}
delPath := wd + "/rw/" + testOpts.DeletionDirName
names := dirNames(t, delPath)
if len(names) != 1 {
t.Fatal("Should have 1 deletion", names)
}
for k := range names {
c, err := ioutil.ReadFile(delPath + "/" + k)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if string(c) != "file" {
t.Fatal("content mismatch", string(c))
}
}
}
func TestUnionFsBasic(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
WriteFile(t, wd+"/rw/rw", "a")
WriteFile(t, wd+"/ro/ro1", "a")
WriteFile(t, wd+"/ro/ro2", "b")
names := dirNames(t, wd+"/mnt")
expected := map[string]bool{
"rw": true, "ro1": true, "ro2": true,
}
checkMapEq(t, names, expected)
WriteFile(t, wd+"/mnt/new", "new contents")
if !fileExists(wd + "/rw/new") {
t.Errorf("missing file in rw layer: %s", wd+"/rw/new")
}
contents := readFromFile(t, wd+"/mnt/new")
if contents != "new contents" {
t.Errorf("read mismatch: '%v'", contents)
}
WriteFile(t, wd+"/mnt/ro1", "promote me")
if !fileExists(wd + "/rw/ro1") {
t.Errorf("missing file in rw layer: %s", wd+"/mnt/ro1")
}
err := os.Remove(wd + "/mnt/new")
if err != nil {
t.Fatalf("Remove: %v", err)
}
names = dirNames(t, wd+"/mnt")
checkMapEq(t, names, map[string]bool{
"rw": true, "ro1": true, "ro2": true,
})
names = dirNames(t, wd+"/rw")
checkMapEq(t, names, map[string]bool{
testOpts.DeletionDirName: true,
"rw": true, "ro1": true,
})
names = dirNames(t, wd+"/rw/"+testOpts.DeletionDirName)
if len(names) != 0 {
t.Errorf("Expected 0 entry in %v", names)
}
err = os.Remove(wd + "/mnt/ro1")
if err != nil {
t.Fatalf("Remove: %v", err)
}
names = dirNames(t, wd+"/mnt")
checkMapEq(t, names, map[string]bool{
"rw": true, "ro2": true,
})
names = dirNames(t, wd+"/rw")
checkMapEq(t, names, map[string]bool{
"rw": true, testOpts.DeletionDirName: true,
})
names = dirNames(t, wd+"/rw/"+testOpts.DeletionDirName)
if len(names) != 1 {
t.Errorf("Expected 1 entry in %v", names)
}
}
func TestUnionFsPromote(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := os.Mkdir(wd+"/ro/subdir", 0755)
if err != nil {
t.Fatalf("Mkdir: %v", err)
}
WriteFile(t, wd+"/ro/subdir/file", "content")
WriteFile(t, wd+"/mnt/subdir/file", "other-content")
}
func TestUnionFsCreate(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := os.MkdirAll(wd+"/ro/subdir/sub2", 0755)
if err != nil {
t.Fatalf("MkdirAll: %v", err)
}
WriteFile(t, wd+"/mnt/subdir/sub2/file", "other-content")
_, err = os.Lstat(wd + "/mnt/subdir/sub2/file")
if err != nil {
t.Fatalf("Lstat: %v", err)
}
}
func TestUnionFsOpenUndeletes(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
WriteFile(t, wd+"/ro/file", "X")
err := os.Remove(wd + "/mnt/file")
if err != nil {
t.Fatalf("Remove: %v", err)
}
WriteFile(t, wd+"/mnt/file", "X")
_, err = os.Lstat(wd + "/mnt/file")
if err != nil {
t.Fatalf("Lstat: %v", err)
}
}
func TestUnionFsMkdir(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
posixtest.MkdirRmdir(t, wd+"/mnt")
}
func TestUnionFsMkdirPromote(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
dirname := wd + "/ro/subdir/subdir2"
err := os.MkdirAll(dirname, 0755)
if err != nil {
t.Fatalf("MkdirAll: %v", err)
}
err = os.Mkdir(wd+"/mnt/subdir/subdir2/dir3", 0755)
if err != nil {
t.Fatalf("Mkdir: %v", err)
}
fi, _ := os.Lstat(wd + "/rw/subdir/subdir2/dir3")
if err != nil {
t.Fatalf("Lstat: %v", err)
}
if fi == nil || !fi.IsDir() {
t.Error("is not a directory: ", fi)
}
}
func TestUnionFsRmdirMkdir(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := os.Mkdir(wd+"/ro/subdir", 0755)
if err != nil {
t.Fatalf("Mkdir: %v", err)
}
dirname := wd + "/mnt/subdir"
err = os.Remove(dirname)
if err != nil {
t.Fatalf("Remove: %v", err)
}
err = os.Mkdir(dirname, 0755)
if err != nil {
t.Fatalf("Mkdir: %v", err)
}
}
func TestUnionFsRename(t *testing.T) {
type Config struct {
f1_ro bool
f1_rw bool
f2_ro bool
f2_rw bool
}
configs := make([]Config, 0)
for i := 0; i < 16; i++ {
c := Config{i&0x1 != 0, i&0x2 != 0, i&0x4 != 0, i&0x8 != 0}
if !(c.f1_ro || c.f1_rw) {
continue
}
configs = append(configs, c)
}
for i, c := range configs {
t.Run(fmt.Sprintf("config %d", i), func(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
if c.f1_ro {
WriteFile(t, wd+"/ro/file1", "c1")
}
if c.f1_rw {
WriteFile(t, wd+"/rw/file1", "c2")
}
if c.f2_ro {
WriteFile(t, wd+"/ro/file2", "c3")
}
if c.f2_rw {
WriteFile(t, wd+"/rw/file2", "c4")
}
err := os.Rename(wd+"/mnt/file1", wd+"/mnt/file2")
if err != nil {
t.Fatalf("Rename: %v", err)
}
_, err = os.Lstat(wd + "/mnt/file1")
if err == nil {
t.Errorf("Should have lost file1")
}
_, err = os.Lstat(wd + "/mnt/file2")
if err != nil {
t.Errorf("Should have gotten file2: %v", err)
}
err = os.Rename(wd+"/mnt/file2", wd+"/mnt/file1")
if err != nil {
t.Fatalf("Rename: %v", err)
}
_, err = os.Lstat(wd + "/mnt/file2")
if err == nil {
t.Errorf("Should have lost file2")
}
_, err = os.Lstat(wd + "/mnt/file1")
if err != nil {
t.Errorf("Should have gotten file1: %v", err)
}
})
}
}
func TestUnionFsRenameDirBasic(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := os.MkdirAll(wd+"/ro/dir/subdir", 0755)
if err != nil {
t.Fatalf("MkdirAll: %v", err)
}
err = os.Rename(wd+"/mnt/dir", wd+"/mnt/renamed")
if err != nil {
t.Fatalf("Rename: %v", err)
}
if fi, _ := os.Lstat(wd + "/mnt/dir"); fi != nil {
t.Fatalf("%s/mnt/dir should have disappeared: %v", wd, fi)
}
if fi, _ := os.Lstat(wd + "/mnt/renamed"); fi == nil || !fi.IsDir() {
t.Fatalf("%s/mnt/renamed should be directory: %v", wd, fi)
}
entries, err := ioutil.ReadDir(wd + "/mnt/renamed")
if err != nil || len(entries) != 1 || entries[0].Name() != "subdir" {
t.Errorf("readdir(%s/mnt/renamed) should have one entry: %v, err %v", wd, entries, err)
}
if err = os.Mkdir(wd+"/mnt/dir", 0755); err != nil {
t.Errorf("mkdir should succeed %v", err)
}
}
func TestUnionFsRenameDirAllSourcesGone(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := os.MkdirAll(wd+"/ro/dir", 0755)
if err != nil {
t.Fatalf("MkdirAll: %v", err)
}
err = ioutil.WriteFile(wd+"/ro/dir/file.txt", []byte{42}, 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
setRecursiveWritable(t, wd+"/ro", false)
err = os.Rename(wd+"/mnt/dir", wd+"/mnt/renamed")
if err != nil {
t.Fatalf("Rename: %v", err)
}
names := dirNames(t, wd+"/rw/"+testOpts.DeletionDirName)
if len(names) != 2 {
t.Errorf("Expected 2 entries in %v", names)
}
}
func TestUnionFsRenameDirWithDeletions(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := os.MkdirAll(wd+"/ro/dir/subdir", 0755)
if err != nil {
t.Fatalf("MkdirAll: %v", err)
}
err = ioutil.WriteFile(wd+"/ro/dir/file.txt", []byte{42}, 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
err = ioutil.WriteFile(wd+"/ro/dir/subdir/file.txt", []byte{42}, 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
setRecursiveWritable(t, wd+"/ro", false)
if fi, _ := os.Lstat(wd + "/mnt/dir/subdir/file.txt"); fi == nil || fi.Mode()&os.ModeType != 0 {
t.Fatalf("%s/mnt/dir/subdir/file.txt should be file: %v", wd, fi)
}
err = os.Remove(wd + "/mnt/dir/file.txt")
if err != nil {
t.Fatalf("Remove: %v", err)
}
err = os.Rename(wd+"/mnt/dir", wd+"/mnt/renamed")
if err != nil {
t.Fatalf("Rename: %v", err)
}
if fi, _ := os.Lstat(wd + "/mnt/dir/subdir/file.txt"); fi != nil {
t.Fatalf("%s/mnt/dir/subdir/file.txt should have disappeared: %v", wd, fi)
}
if fi, _ := os.Lstat(wd + "/mnt/dir"); fi != nil {
t.Fatalf("%s/mnt/dir should have disappeared: %v", wd, fi)
}
if fi, _ := os.Lstat(wd + "/mnt/renamed"); fi == nil || !fi.IsDir() {
t.Fatalf("%s/mnt/renamed should be directory: %v", wd, fi)
}
if fi, _ := os.Lstat(wd + "/mnt/renamed/file.txt"); fi != nil {
t.Fatalf("%s/mnt/renamed/file.txt should have disappeared %#v", wd, fi)
}
if err = os.Mkdir(wd+"/mnt/dir", 0755); err != nil {
t.Errorf("mkdir should succeed %v", err)
}
if fi, _ := os.Lstat(wd + "/mnt/dir/subdir"); fi != nil {
t.Fatalf("%s/mnt/dir/subdir should have disappeared %#v", wd, fi)
}
}
func TestUnionFsRenameSymlink(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := os.Symlink("linktarget", wd+"/ro/link")
if err != nil {
t.Fatalf("Symlink: %v", err)
}
err = os.Rename(wd+"/mnt/link", wd+"/mnt/renamed")
if err != nil {
t.Fatalf("Rename: %v", err)
}
if fi, _ := os.Lstat(wd + "/mnt/link"); fi != nil {
t.Fatalf("%s/mnt/link should have disappeared: %v", wd, fi)
}
if fi, _ := os.Lstat(wd + "/mnt/renamed"); fi == nil || fi.Mode()&os.ModeSymlink == 0 {
t.Fatalf("%s/mnt/renamed should be link: %v", wd, fi)
}
if link, err := os.Readlink(wd + "/mnt/renamed"); err != nil || link != "linktarget" {
t.Fatalf("readlink(%s/mnt/renamed) should point to 'linktarget': %v, err %v", wd, link, err)
}
}
func TestUnionFsWritableDir(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
dirname := wd + "/ro/subdir"
err := os.Mkdir(dirname, 0555)
if err != nil {
t.Fatalf("Mkdir: %v", err)
}
setRecursiveWritable(t, wd+"/ro", false)
fi, err := os.Lstat(wd + "/mnt/subdir")
if err != nil {
t.Fatalf("Lstat: %v", err)
}
if fi.Mode().Perm()&0222 == 0 {
t.Errorf("unexpected permission %o", fi.Mode().Perm())
}
}
func TestUnionFsWriteAccess(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
fn := wd + "/ro/file"
// No write perms.
err := ioutil.WriteFile(fn, []byte("foo"), 0444)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
setRecursiveWritable(t, wd+"/ro", false)
err = syscall.Access(wd+"/mnt/file", fuse.W_OK)
if err != nil {
if err != nil {
t.Fatalf("Access: %v", err)
}
}
}
func TestUnionFsLink(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
content := "blabla"
fn := wd + "/ro/file"
err := ioutil.WriteFile(fn, []byte(content), 0666)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
setRecursiveWritable(t, wd+"/ro", false)
err = os.Link(wd+"/mnt/file", wd+"/mnt/linked")
if err != nil {
t.Fatalf("Link: %v", err)
}
fi2, err := os.Lstat(wd + "/mnt/linked")
if err != nil {
t.Fatalf("Lstat: %v", err)
}
fi1, err := os.Lstat(wd + "/mnt/file")
if err != nil {
t.Fatalf("Lstat: %v", err)
}
s1 := fuse.ToStatT(fi1)
s2 := fuse.ToStatT(fi2)
if s1.Ino != s2.Ino {
t.Errorf("inode numbers should be equal for linked files %v, %v", s1.Ino, s2.Ino)
}
c, err := ioutil.ReadFile(wd + "/mnt/linked")
if string(c) != content {
t.Errorf("content mismatch got %q want %q", string(c), content)
}
}
func TestUnionFsTruncate(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
WriteFile(t, wd+"/ro/file", "hello")
setRecursiveWritable(t, wd+"/ro", false)
os.Truncate(wd+"/mnt/file", 2)
content := readFromFile(t, wd+"/mnt/file")
if content != "he" {
t.Errorf("unexpected content %v", content)
}
content2 := readFromFile(t, wd+"/rw/file")
if content2 != content {
t.Errorf("unexpected rw content %v", content2)
}
}
func TestUnionFsCopyChmod(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
contents := "hello"
fn := wd + "/mnt/y"
err := ioutil.WriteFile(fn, []byte(contents), 0644)
if err != nil {
t.Fatalf("WriteFile(%v): %v", fn, err)
}
err = os.Chmod(fn, 0755)
if err != nil {
t.Fatalf("Chmod(%v): %v", fn, err)
}
fi, err := os.Lstat(fn)
if err != nil {
t.Fatalf("Lstat(%v): %v", fn, err)
}
if fi.Mode()&0111 == 0 {
t.Errorf("Lstat(%v): got mode %o, want some +x bit", fn, fi.Mode())
}
time.Sleep(entryTTL)
fi, err = os.Lstat(fn)
if err != nil {
t.Fatalf("Lstat(%v) after sleep: %v", fn, err)
}
if fi.Mode()&0111 == 0 {
t.Errorf("Lstat(%v) after sleep: mode %o", fn, fi.Mode())
}
}
func abs(dt int64) int64 {
if dt >= 0 {
return dt
}
return -dt
}
func TestUnionFsTruncateTimestamp(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
contents := "hello"
fn := wd + "/mnt/y"
err := ioutil.WriteFile(fn, []byte(contents), 0644)
if err != nil {
t.Fatalf("WriteFile(%v): %v", fn, err)
}
time.Sleep(200 * time.Millisecond)
truncTs := time.Now()
err = os.Truncate(fn, 3)
if err != nil {
t.Fatalf("Truncate(%v): %v", fn, err)
}
fi, err := os.Lstat(fn)
if err != nil {
t.Fatalf("Lstat(%v): %v", fn, err)
}
if truncTs.Sub(fi.ModTime()) > 100*time.Millisecond {
t.Errorf("after Truncate: got TS %v, want %v", fi.ModTime(), truncTs)
}
}
func TestUnionFsRemoveAll(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := os.MkdirAll(wd+"/ro/dir/subdir", 0755)
if err != nil {
t.Fatalf("MkdirAll: %v", err)
}
contents := "hello"
fn := wd + "/ro/dir/subdir/y"
err = ioutil.WriteFile(fn, []byte(contents), 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
setRecursiveWritable(t, wd+"/ro", false)
err = os.RemoveAll(wd + "/mnt/dir")
if err != nil {
t.Error("Should delete all")
}
for _, f := range []string{"dir/subdir/y", "dir/subdir", "dir"} {
if fi, _ := os.Lstat(filepath.Join(wd, "mount", f)); fi != nil {
t.Errorf("file %s should have disappeared: %v", f, fi)
}
}
names, err := Readdirnames(wd + "/rw/DELETIONS")
if err != nil {
t.Fatalf("Readdirnames: %v", err)
}
if len(names) != 3 {
t.Fatal("unexpected names", names)
}
}
func ProgramVersion(bin string) (major, minor int64, err error) {
cmd := exec.Command(bin, "--version")
buf := &bytes.Buffer{}
cmd.Stdout = buf
if err := cmd.Run(); err != nil {
return 0, 0, err
}
lines := strings.Split(buf.String(), "\n")
if len(lines) < 1 {
return 0, 0, fmt.Errorf("no output")
}
matches := regexp.MustCompile(".* ([0-9]+)\\.([0-9]+)").FindStringSubmatch(lines[0])
if matches == nil {
return 0, 0, fmt.Errorf("no match for %q", lines[0])
}
major, err = strconv.ParseInt(matches[1], 10, 64)
if err != nil {
return 0, 0, err
}
minor, err = strconv.ParseInt(matches[2], 10, 64)
if err != nil {
return 0, 0, err
}
return major, minor, nil
}
func TestUnionFsRmRf(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := os.MkdirAll(wd+"/ro/dir/subdir", 0755)
if err != nil {
t.Fatalf("MkdirAll: %v", err)
}
contents := "hello"
fn := wd + "/ro/dir/subdir/y"
err = ioutil.WriteFile(fn, []byte(contents), 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
setRecursiveWritable(t, wd+"/ro", false)
bin, err := exec.LookPath("rm")
if err != nil {
t.Fatalf("LookPath: %v", err)
}
maj, min, err := ProgramVersion(bin)
if err != nil {
t.Logf("ProgramVersion: %v", err)
}
if maj < 8 { // assuming GNU coreutils.
t.Skipf("Skipping test; GNU rm %d.%d is not POSIX compliant.", maj, min)
}
names, _ := Readdirnames(wd + "/mnt/dir")
t.Logf("Contents of %s/mnt/dir: %s", wd, strings.Join(names, ", "))
cmd := exec.Command(bin, "-rf", wd+"/mnt/dir")
err = cmd.Run()
if err != nil {
t.Fatal("rm -rf returned error:", err)
}
for _, f := range []string{"dir/subdir/y", "dir/subdir", "dir"} {
if fi, _ := os.Lstat(filepath.Join(wd, "mount", f)); fi != nil {
t.Errorf("file %s should have disappeared: %v", f, fi)
}
}
names, err = Readdirnames(wd + "/rw/DELETIONS")
if err != nil {
t.Fatalf("Readdirnames: %v", err)
}
if len(names) != 3 {
t.Fatal("unexpected names", names)
}
}
func Readdirnames(dir string) ([]string, error) {
f, err := os.Open(dir)
if err != nil {
return nil, err
}
defer f.Close()
return f.Readdirnames(-1)
}
func TestUnionFsDropDeletionCache(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := ioutil.WriteFile(wd+"/ro/file", []byte("bla"), 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
setRecursiveWritable(t, wd+"/ro", false)
_, err = os.Lstat(wd + "/mnt/file")
if err != nil {
t.Fatalf("Lstat: %v", err)
}
err = os.Remove(wd + "/mnt/file")
if err != nil {
t.Fatalf("Remove: %v", err)
}
fi, _ := os.Lstat(wd + "/mnt/file")
if fi != nil {
t.Fatal("Lstat() should have failed", fi)
}
names, err := Readdirnames(wd + "/rw/DELETIONS")
if err != nil {
t.Fatalf("Readdirnames: %v", err)
}
if len(names) != 1 {
t.Fatal("unexpected names", names)
}
os.Remove(wd + "/rw/DELETIONS/" + names[0])
fi, _ = os.Lstat(wd + "/mnt/file")
if fi != nil {
t.Fatal("Lstat() should have failed", fi)
}
// Expire kernel entry.
time.Sleep((6 * entryTTL) / 10)
err = ioutil.WriteFile(wd+"/mnt/.drop_cache", []byte(""), 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
_, err = os.Lstat(wd + "/mnt/file")
if err != nil {
t.Fatal("Lstat() should have succeeded", err)
}
}
func TestUnionFsDropCache(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := ioutil.WriteFile(wd+"/ro/file", []byte("bla"), 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
_, err = os.Lstat(wd + "/mnt/.drop_cache")
if err != nil {
t.Fatalf("Lstat: %v", err)
}
names, err := Readdirnames(wd + "/mnt")
if err != nil {
t.Fatalf("Readdirnames: %v", err)
}
if len(names) != 1 || names[0] != "file" {
t.Fatal("unexpected names", names)
}
err = ioutil.WriteFile(wd+"/ro/file2", []byte("blabla"), 0644)
names2, err := Readdirnames(wd + "/mnt")
if err != nil {
t.Fatalf("Readdirnames: %v", err)
}
if len(names2) != len(names) {
t.Fatal("mismatch", names2)
}
err = ioutil.WriteFile(wd+"/mnt/.drop_cache", []byte("does not matter"), 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
names2, err = Readdirnames(wd + "/mnt")
if len(names2) != 2 {
t.Fatal("mismatch 2", names2)
}
}
type disappearingFS struct {
pathfs.FileSystem
normal pathfs.FileSystem
nop pathfs.FileSystem
visible bool
visibleChan chan bool
}
func (d *disappearingFS) fs() pathfs.FileSystem {
select {
case v := <-d.visibleChan:
d.visible = v
if v {
d.FileSystem = d.normal
} else {
d.FileSystem = d.nop
}
default:
}
return d.FileSystem
}
func (d *disappearingFS) GetAttr(name string, context *fuse.Context) (a *fuse.Attr, s fuse.Status) {
return d.fs().GetAttr(name, context)
}
func (d *disappearingFS) OpenDir(name string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) {
return d.fs().OpenDir(name, context)
}
func newDisappearingFS(fs, nop pathfs.FileSystem) *disappearingFS {
return &disappearingFS{
visibleChan: make(chan bool, 1),
visible: true,
normal: fs,
nop: nop,
FileSystem: fs,
}
}
func TestUnionFsDisappearing(t *testing.T) {
// This init is like setupUfs, but we want access to the
// writable Fs.
wd := testutil.TempDir()
defer os.RemoveAll(wd)
err := os.Mkdir(wd+"/mnt", 0700)
if err != nil {
t.Fatalf("Mkdir: %v", err)
}
err = os.Mkdir(wd+"/rw", 0700)
if err != nil {
t.Fatalf("Mkdir: %v", err)
}
os.Mkdir(wd+"/ro", 0700)
if err != nil {
t.Fatalf("Mkdir: %v", err)
}
wrFs := newDisappearingFS(pathfs.NewLoopbackFileSystem(wd+"/rw"),
pathfs.NewLoopbackFileSystem("/dev/null"))
var fses []pathfs.FileSystem
fses = append(fses, pathfs.NewLockingFileSystem(wrFs))
fses = append(fses, pathfs.NewLoopbackFileSystem(wd+"/ro"))
ufs, err := NewUnionFs(fses, testOpts)
if err != nil {
t.Fatalf("NewUnionFs: %v", err)
}
opts := &nodefs.Options{
EntryTimeout: entryTTL,
AttrTimeout: entryTTL,
NegativeTimeout: entryTTL,
Debug: testutil.VerboseTest(),
LookupKnownChildren: true,
}
nfs := pathfs.NewPathNodeFs(ufs, nil)
state, _, err := nodefs.MountRoot(wd+"/mnt", nfs.Root(), opts)
if err != nil {
t.Fatalf("MountNodeFileSystem: %v", err)
}
defer state.Unmount()
go state.Serve()
state.WaitMount()
err = ioutil.WriteFile(wd+"/ro/file", []byte("blabla"), 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
setRecursiveWritable(t, wd+"/ro", false)
err = os.Remove(wd + "/mnt/file")
if err != nil {
t.Fatalf("Remove: %v", err)
}
wrFs.visibleChan <- false
time.Sleep((3 * entryTTL) / 2)
_, err = ioutil.ReadDir(wd + "/mnt")
if err == nil {
t.Fatal("Readdir should have failed")
}
err = ioutil.WriteFile(wd+"/mnt/file2", []byte("blabla"), 0644)
if err == nil {
t.Fatal("write should have failed")
}
// Wait for the caches to purge, and then restore.
time.Sleep((3 * entryTTL) / 2)
wrFs.visibleChan <- true
_, err = ioutil.ReadDir(wd + "/mnt")
if err != nil {
t.Fatal("Readdir should succeed", err)
}
err = ioutil.WriteFile(wd+"/mnt/file2", []byte("blabla"), 0644)
if err != nil {
t.Fatal("write should succeed", err)
}
}
func TestUnionFsDeletedGetAttr(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := ioutil.WriteFile(wd+"/ro/file", []byte("blabla"), 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
setRecursiveWritable(t, wd+"/ro", false)
f, err := os.Open(wd + "/mnt/file")
if err != nil {
t.Fatalf("Open: %v", err)
}
defer f.Close()
err = os.Remove(wd + "/mnt/file")
if err != nil {
t.Fatalf("Remove: %v", err)
}
if fi, err := f.Stat(); err != nil || fi.Mode()&os.ModeType != 0 {
t.Fatalf("stat returned error or non-file: %v %v", err, fi)
}
}
func TestUnionFsDoubleOpen(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := ioutil.WriteFile(wd+"/ro/file", []byte("blablabla"), 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
setRecursiveWritable(t, wd+"/ro", false)
roFile, err := os.Open(wd + "/mnt/file")
if err != nil {
t.Fatalf("Open: %v", err)
}
defer roFile.Close()
rwFile, err := os.OpenFile(wd+"/mnt/file", os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
t.Fatalf("OpenFile: %v", err)
}
defer rwFile.Close()
output, err := ioutil.ReadAll(roFile)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
if len(output) != 0 {
t.Errorf("After r/w truncation, r/o file should be empty too: %q", string(output))
}
want := "hello"
_, err = rwFile.Write([]byte(want))
if err != nil {
t.Fatalf("Write: %v", err)
}
b := make([]byte, 100)
roFile.Seek(0, 0)
n, err := roFile.Read(b)
if err != nil {
t.Fatalf("Read: %v", err)
}
b = b[:n]
if string(b) != "hello" {
t.Errorf("r/w and r/o file are not synchronized: got %q want %q", string(b), want)
}
}
func TestUnionFsStatFs(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
s1 := syscall.Statfs_t{}
err := syscall.Statfs(wd+"/mnt", &s1)
if err != nil {
t.Fatal("statfs mnt", err)
}
if s1.Bsize == 0 {
t.Fatal("Expect blocksize > 0")
}
}
func TestUnionFsFlushSize(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
fn := wd + "/mnt/file"
f, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
t.Fatalf("OpenFile: %v", err)
}
fi, err := f.Stat()
if err != nil {
t.Fatalf("Stat: %v", err)
}
n, err := f.Write([]byte("hello"))
if err != nil {
t.Fatalf("Write: %v", err)
}
f.Close()
fi, err = os.Lstat(fn)
if err != nil {
t.Fatalf("Lstat: %v", err)
}
if fi.Size() != int64(n) {
t.Errorf("got %d from Stat().Size, want %d", fi.Size(), n)
}
}
func TestUnionFsFlushRename(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := ioutil.WriteFile(wd+"/mnt/file", []byte("x"), 0644)
fn := wd + "/mnt/tmp"
f, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
t.Fatalf("OpenFile: %v", err)
}
fi, err := f.Stat()
if err != nil {
t.Fatalf("Stat: %v", err)
}
n, err := f.Write([]byte("hello"))
if err != nil {
t.Fatalf("Write: %v", err)
}
f.Close()
dst := wd + "/mnt/file"
err = os.Rename(fn, dst)
if err != nil {
t.Fatalf("Rename: %v", err)
}
fi, err = os.Lstat(dst)
if err != nil {
t.Fatalf("Lstat: %v", err)
}
if fi.Size() != int64(n) {
t.Errorf("got %d from Stat().Size, want %d", fi.Size(), n)
}
}
func TestUnionFsTruncGetAttr(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
c := []byte("hello")
f, err := os.OpenFile(wd+"/mnt/file", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
if err != nil {
t.Fatalf("OpenFile: %v", err)
}
_, err = f.Write(c)
if err != nil {
t.Fatalf("Write: %v", err)
}
err = f.Close()
if err != nil {
t.Fatalf("Close: %v", err)
}
fi, err := os.Lstat(wd + "/mnt/file")
if fi.Size() != int64(len(c)) {
t.Fatalf("Length mismatch got %d want %d", fi.Size(), len(c))
}
}
func TestUnionFsPromoteDirTimeStamp(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := os.Mkdir(wd+"/ro/subdir", 0750)
if err != nil {
t.Fatalf("Mkdir: %v", err)
}
err = ioutil.WriteFile(wd+"/ro/subdir/file", []byte("hello"), 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
setRecursiveWritable(t, wd+"/ro", false)
err = os.Chmod(wd+"/mnt/subdir/file", 0060)
if err != nil {
t.Fatalf("Chmod: %v", err)
}
fRo, err := os.Lstat(wd + "/ro/subdir")
if err != nil {
t.Fatalf("Lstat: %v", err)
}
fRw, err := os.Lstat(wd + "/rw/subdir")
if err != nil {
t.Fatalf("Lstat: %v", err)
}
// TODO - need to update timestamps after promoteDirsTo calls,
// not during.
if false && fRo.ModTime().Equal(fRw.ModTime()) {
t.Errorf("Changed timestamps on promoted subdir: ro %v rw %v", fRo.ModTime(), fRw.ModTime())
}
if fRo.Mode().Perm()|0200 != fRw.Mode().Perm() {
t.Errorf("Changed mode ro: %v, rw: %v", fRo.Mode(), fRw.Mode())
}
}
func TestUnionFsCheckHiddenFiles(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
err := ioutil.WriteFile(wd+"/ro/hidden", []byte("bla"), 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
err = ioutil.WriteFile(wd+"/ro/not_hidden", []byte("bla"), 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
setRecursiveWritable(t, wd+"/ro", false)
fi, _ := os.Lstat(wd + "/mnt/hidden")
if fi != nil {
t.Fatal("Lstat() should have failed", fi)
}
_, err = os.Lstat(wd + "/mnt/not_hidden")
if err != nil {
t.Fatalf("Lstat: %v", err)
}
names, err := Readdirnames(wd + "/mnt")
if err != nil {
t.Fatalf("Readdirnames: %v", err)
}
if len(names) != 1 || names[0] != "not_hidden" {
t.Fatal("unexpected names", names)
}
}
func TestUnionFSBarf(t *testing.T) {
wd, clean := setupUfs(t)
defer clean()
if err := os.Mkdir(wd+"/mnt/dir", 0755); err != nil {
t.Fatalf("os.Mkdir: %v", err)
}
if err := os.Mkdir(wd+"/mnt/dir2", 0755); err != nil {
t.Fatalf("os.Mkdir: %v", err)
}
if err := ioutil.WriteFile(wd+"/rw/dir/file", []byte("bla"), 0644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if _, err := os.Lstat(wd + "/mnt/dir/file"); err != nil {
t.Fatalf("Lstat: %v", err)
}
if err := os.Rename(wd+"/rw/dir/file", wd+"/rw/file"); err != nil {
t.Fatalf("os.Rename: %v", err)
}
if err := os.Rename(wd+"/mnt/file", wd+"/mnt/dir2/file"); err != nil {
t.Fatalf("os.Rename: %v", err)
}
}
// 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 (
"os"
"sync/atomic"
"testing"
"time"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
"github.com/hanwen/go-fuse/v2/fuse/pathfs"
"github.com/hanwen/go-fuse/v2/internal/testutil"
)
type TestFS struct {
pathfs.FileSystem
xattrRead int64
}
func (fs *TestFS) GetAttr(path string, context *fuse.Context) (*fuse.Attr, fuse.Status) {
switch path {
case "":
return &fuse.Attr{Mode: fuse.S_IFDIR | 0755}, fuse.OK
case "file":
return &fuse.Attr{Mode: fuse.S_IFREG | 0755}, fuse.OK
}
return nil, fuse.ENOENT
}
func (fs *TestFS) GetXAttr(path string, name string, context *fuse.Context) ([]byte, fuse.Status) {
if path == "file" && name == "user.attr" {
atomic.AddInt64(&fs.xattrRead, 1)
return []byte{42}, fuse.OK
}
return nil, fuse.ENOATTR
}
func TestXAttrCaching(t *testing.T) {
wd := testutil.TempDir()
defer os.RemoveAll(wd)
os.Mkdir(wd+"/mnt", 0700)
err := os.Mkdir(wd+"/rw", 0700)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
rwFS := pathfs.NewLoopbackFileSystem(wd + "/rw")
roFS := &TestFS{
FileSystem: pathfs.NewDefaultFileSystem(),
}
ufs, err := NewUnionFs([]pathfs.FileSystem{rwFS,
NewCachingFileSystem(roFS, entryTTL)}, testOpts)
if err != nil {
t.Fatalf("NewUnionFs: %v", err)
}
opts := &nodefs.Options{
EntryTimeout: entryTTL / 2,
AttrTimeout: entryTTL / 2,
NegativeTimeout: entryTTL / 2,
Debug: testutil.VerboseTest(),
LookupKnownChildren: true,
}
pathfs := pathfs.NewPathNodeFs(ufs,
&pathfs.PathNodeFsOptions{ClientInodes: true,
Debug: testutil.VerboseTest()})
server, _, err := nodefs.MountRoot(wd+"/mnt", pathfs.Root(), opts)
if err != nil {
t.Fatalf("MountNodeFileSystem failed: %v", err)
}
defer server.Unmount()
go server.Serve()
server.WaitMount()
start := time.Now()
if fi, err := os.Lstat(wd + "/mnt"); err != nil || !fi.IsDir() {
t.Fatalf("root not readable: %v, %v", err, fi)
}
buf := make([]byte, 1024)
n, err := Getxattr(wd+"/mnt/file", "user.attr", buf)
if err != nil {
t.Fatalf("Getxattr: %v", err)
}
want := "\x2a"
got := string(buf[:n])
if got != want {
t.Fatalf("Got %q want %q", got, err)
}
time.Sleep(entryTTL / 3)
n, err = Getxattr(wd+"/mnt/file", "user.attr", buf)
if err != nil {
t.Fatalf("Getxattr: %v", err)
}
got = string(buf[:n])
if got != want {
t.Fatalf("Got %q want %q", got, err)
}
time.Sleep(entryTTL / 3)
// Make sure that an interceding Getxattr() to a filesystem that doesn't implement GetXAttr() doesn't affect future calls.
Getxattr(wd, "whatever", buf)
n, err = Getxattr(wd+"/mnt/file", "user.attr", buf)
if err != nil {
t.Fatalf("Getxattr: %v", err)
}
got = string(buf[:n])
if got != want {
t.Fatalf("Got %q want %q", got, err)
}
if time.Now().Sub(start) >= entryTTL {
// If we run really slowly, this test will spuriously
// fail.
t.Skip("test took too long.")
}
actual := atomic.LoadInt64(&roFS.xattrRead)
if actual != 1 {
t.Errorf("got xattrRead=%d, want 1", actual)
}
}
// 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 (
"syscall"
"unsafe"
)
// Darwin doesn't have support for syscall.Getxattr() so we pull it into its own file and implement it by hand on Darwin.
func Getxattr(path string, attr string, dest []byte) (sz int, err error) {
var _p0 *byte
_p0, err = syscall.BytePtrFromString(path)
if err != nil {
return
}
var _p1 *byte
_p1, err = syscall.BytePtrFromString(attr)
if err != nil {
return
}
var _p2 unsafe.Pointer
if len(dest) > 0 {
_p2 = unsafe.Pointer(&dest[0])
} else {
var _zero uintptr
_p2 = unsafe.Pointer(&_zero)
}
r0, _, e1 := syscall.Syscall6(syscall.SYS_GETXATTR, uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(_p1)), uintptr(_p2), uintptr(len(dest)), 0, 0)
sz = int(r0)
if e1 != 0 {
err = e1
}
return
}
// 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 (
"syscall"
)
// Darwin doesn't have support for syscall.Getxattr() so we pull it into its own file and implement it by hand on Darwin.
func Getxattr(path string, attr string, dest []byte) (sz int, err error) {
return syscall.Getxattr(path, attr, dest)
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment