Commit 557408bc authored by Han-Wen Nienhuys's avatar Han-Wen Nienhuys

Move MemUnionFs to termite.

parent 23d26623
......@@ -7,8 +7,7 @@ GOFILES=unionfs.go \
timedcache.go \
cachingfs.go \
autounion.go \
create.go \
memunionfs.go
create.go
DEPS=../fuse
......
package unionfs
import (
"fmt"
"github.com/hanwen/go-fuse/fuse"
"log"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
)
var _ = log.Println
// A unionfs that only uses on-disk backing store for file contents.
type MemUnionFs struct {
fuse.DefaultNodeFileSystem
readonly fuse.FileSystem
backingStore string
connector *fuse.FileSystemConnector
mutex sync.RWMutex
root *memNode
cond *sync.Cond
nextFree int
openWritable int
// All paths that have been renamed or deleted will be marked
// here. After deletion, entries may be recreated, but they
// will be treated as new.
deleted map[string]bool
}
type memNode struct {
fuse.DefaultFsNode
fs *MemUnionFs
// protects mutable data below.
mutex *sync.RWMutex
backing string
original string
changed bool
link string
info os.FileInfo
}
type Result struct {
*os.FileInfo
Original string
Backing string
Link string
}
func (me *MemUnionFs) OnMount(conn *fuse.FileSystemConnector) {
me.connector = conn
}
func (me *MemUnionFs) markCloseWrite() {
me.mutex.Lock()
defer me.mutex.Unlock()
me.openWritable--
if me.openWritable < 0 {
log.Panicf("openWritable Underflow")
}
me.cond.Broadcast()
}
// Reset drops the state of the filesystem back its original.
func (me *MemUnionFs) Reset() {
me.mutex.Lock()
defer me.mutex.Unlock()
me.root.reset("")
for path, _ := range me.deleted {
parent, base := filepath.Split(path)
parent = stripSlash(parent)
last, rest := me.connector.Node(me.root.Inode(), parent)
if len(rest) == 0 {
me.connector.EntryNotify(last, base)
}
}
me.deleted = make(map[string]bool, len(me.deleted))
me.clearBackingStore()
}
func (me *MemUnionFs) Reap() map[string]*Result {
me.mutex.Lock()
defer me.mutex.Unlock()
for me.openWritable > 0 {
me.cond.Wait()
}
m := map[string]*Result{}
for name, _ := range me.deleted {
fi, code := me.readonly.GetAttr(name, nil)
if !code.Ok() {
continue
}
m[name] = &Result{}
if !fi.IsDirectory() {
continue
}
todo := []string{name}
for len(todo) > 0 {
l := len(todo) - 1
n := todo[l]
todo = todo[:l]
s, _ := me.readonly.OpenDir(n, nil)
for e := range s {
full := filepath.Join(n, e.Name)
m[full] = &Result{}
if e.Mode&fuse.S_IFDIR != 0 {
todo = append(todo, full)
}
}
}
}
me.root.reap("", m)
return m
}
func (me *MemUnionFs) clearBackingStore() {
f, err := os.Open(me.backingStore)
if err != nil {
return
}
defer f.Close()
names, err := f.Readdirnames(-1)
if err != nil {
return
}
for _, n := range names {
os.Remove(filepath.Join(me.backingStore, n))
}
}
func (me *MemUnionFs) Update(results map[string]*Result) {
del := []string{}
add := []string{}
for k, v := range results {
if v.FileInfo != nil {
add = append(add, k)
} else {
del = append(del, k)
}
}
sort.Strings(del)
for i := len(del) - 1; i >= 0; i-- {
n := del[i]
dir, base := filepath.Split(n)
dir = strings.TrimRight(dir, "/")
dirNode, rest := me.connector.Node(me.root.Inode(), dir)
if len(rest) > 0 {
continue
}
dirNode.RmChild(base)
me.connector.EntryNotify(dirNode, base)
}
me.mutex.Lock()
notifyNodes := []*fuse.Inode{}
enotifyNodes := []*fuse.Inode{}
enotifyNames := []string{}
sort.Strings(add)
for _, n := range add {
node, rest := me.connector.Node(me.root.Inode(), n)
if len(rest) > 0 {
enotifyNames = append(enotifyNames, rest[0])
enotifyNodes = append(enotifyNodes, node)
continue
}
notifyNodes = append(notifyNodes, node)
mn := node.FsNode().(*memNode)
mn.original = n
mn.changed = false
r := results[n]
mn.info = *r.FileInfo
mn.link = r.Link
}
me.mutex.Unlock()
for _, n := range notifyNodes {
me.connector.FileNotify(n, 0, 0)
}
for i, n := range enotifyNodes {
me.connector.EntryNotify(n, enotifyNames[i])
}
}
func (me *MemUnionFs) getFilename() string {
id := me.nextFree
me.nextFree++
return fmt.Sprintf("%s/%d", me.backingStore, id)
}
func (me *MemUnionFs) Root() fuse.FsNode {
return me.root
}
func (me *MemUnionFs) newNode(isdir bool) *memNode {
n := &memNode{
fs: me,
mutex: &me.mutex,
}
now := time.Nanoseconds()
n.info.Mtime_ns = now
n.info.Atime_ns = now
n.info.Ctime_ns = now
return n
}
func NewMemUnionFs(backingStore string, roFs fuse.FileSystem) *MemUnionFs {
me := &MemUnionFs{}
me.deleted = make(map[string]bool)
me.backingStore = backingStore
me.readonly = roFs
me.root = me.newNode(true)
me.root.info.Mode = fuse.S_IFDIR | 0755
me.cond = sync.NewCond(&me.mutex)
return me
}
func (me *memNode) Deletable() bool {
return !me.changed && me.original == ""
}
func (me *memNode) StatFs() *fuse.StatfsOut {
backingFs := &fuse.LoopbackFileSystem{Root: me.fs.backingStore}
return backingFs.StatFs("")
}
func (me *memNode) touch() {
me.changed = true
me.info.Mtime_ns = time.Nanoseconds()
}
func (me *memNode) ctouch() {
me.changed = true
me.info.Ctime_ns = time.Nanoseconds()
}
func (me *memNode) newNode(isdir bool) *memNode {
n := me.fs.newNode(isdir)
me.Inode().New(isdir, n)
return n
}
func (me *memNode) Readlink(c *fuse.Context) ([]byte, fuse.Status) {
me.mutex.RLock()
defer me.mutex.RUnlock()
return []byte(me.link), fuse.OK
}
func (me *memNode) Lookup(name string, context *fuse.Context) (fi *os.FileInfo, node fuse.FsNode, code fuse.Status) {
me.mutex.RLock()
defer me.mutex.RUnlock()
return me.lookup(name, context)
}
// Must run with mutex held.
func (me *memNode) lookup(name string, context *fuse.Context) (fi *os.FileInfo, node fuse.FsNode, code fuse.Status) {
if me.original == "" && me != me.fs.root {
return nil, nil, fuse.ENOENT
}
fn := filepath.Join(me.original, name)
if _, del := me.fs.deleted[fn]; del {
return nil, nil, fuse.ENOENT
}
fi, code = me.fs.readonly.GetAttr(fn, context)
if !code.Ok() {
return nil, nil, code
}
child := me.newNode(fi.Mode&fuse.S_IFDIR != 0)
child.info = *fi
child.original = fn
if child.info.Mode&fuse.S_IFLNK != 0 {
child.link, _ = me.fs.readonly.Readlink(fn, context)
}
me.Inode().AddChild(name, child.Inode())
return fi, child, fuse.OK
}
func (me *memNode) Mkdir(name string, mode uint32, context *fuse.Context) (fi *os.FileInfo, newNode fuse.FsNode, code fuse.Status) {
me.mutex.Lock()
defer me.mutex.Unlock()
n := me.newNode(true)
n.changed = true
n.info.Mode = mode | fuse.S_IFDIR
me.Inode().AddChild(name, n.Inode())
me.touch()
return &n.info, n, fuse.OK
}
func (me *memNode) Unlink(name string, context *fuse.Context) (code fuse.Status) {
me.mutex.Lock()
defer me.mutex.Unlock()
if me.original != "" || me == me.fs.root {
me.fs.deleted[filepath.Join(me.original, name)] = true
}
ch := me.Inode().RmChild(name)
if ch == nil {
return fuse.ENOENT
}
me.touch()
return fuse.OK
}
func (me *memNode) Rmdir(name string, context *fuse.Context) (code fuse.Status) {
return me.Unlink(name, context)
}
func (me *memNode) Symlink(name string, content string, context *fuse.Context) (fi *os.FileInfo, newNode fuse.FsNode, code fuse.Status) {
me.mutex.Lock()
defer me.mutex.Unlock()
n := me.newNode(false)
n.info.Mode = fuse.S_IFLNK | 0777
n.link = content
n.changed = true
me.Inode().AddChild(name, n.Inode())
me.touch()
return &n.info, n, fuse.OK
}
// Expand the original fs as a tree.
func (me *memNode) materializeSelf() {
me.changed = true
if !me.Inode().IsDir() {
return
}
s, _ := me.fs.readonly.OpenDir(me.original, nil)
for e := range s {
me.lookup(e.Name, nil)
}
me.original = ""
}
func (me *memNode) materialize() {
me.materializeSelf()
for _, n := range me.Inode().FsChildren() {
if n.IsDir() {
n.FsNode().(*memNode).materialize()
}
}
}
func (me *memNode) Rename(oldName string, newParent fuse.FsNode, newName string, context *fuse.Context) (code fuse.Status) {
me.mutex.Lock()
defer me.mutex.Unlock()
ch := me.Inode().RmChild(oldName)
if ch == nil {
return fuse.ENOENT
}
if me.original != "" || me == me.fs.root {
me.fs.deleted[filepath.Join(me.original, oldName)] = true
}
childNode := ch.FsNode().(*memNode)
if childNode.original != "" || childNode == me.fs.root {
childNode.materialize()
childNode.markChanged()
}
newParent.Inode().RmChild(newName)
newParent.Inode().AddChild(newName, ch)
me.touch()
return fuse.OK
}
func (me *memNode) markChanged() {
me.changed = true
for _, n := range me.Inode().FsChildren() {
n.FsNode().(*memNode).markChanged()
}
}
func (me *memNode) Link(name string, existing fuse.FsNode, context *fuse.Context) (fi *os.FileInfo, newNode fuse.FsNode, code fuse.Status) {
me.Inode().AddChild(name, existing.Inode())
fi, code = existing.GetAttr(nil, context)
me.mutex.Lock()
defer me.mutex.Unlock()
me.touch()
return fi, existing, code
}
func (me *memNode) Create(name string, flags uint32, mode uint32, context *fuse.Context) (file fuse.File, fi *os.FileInfo, newNode fuse.FsNode, code fuse.Status) {
me.mutex.Lock()
defer me.mutex.Unlock()
n := me.newNode(false)
n.info.Mode = mode | fuse.S_IFREG
n.changed = true
n.backing = me.fs.getFilename()
f, err := os.Create(n.backing)
if err != nil {
log.Printf("Backing store error %q: %v", n.backing, err)
return nil, nil, nil, fuse.ToStatus(err)
}
me.Inode().AddChild(name, n.Inode())
me.touch()
me.fs.openWritable++
return n.newFile(&fuse.LoopbackFile{File: f}, true), &n.info, n, fuse.OK
}
type memNodeFile struct {
fuse.File
writable bool
node *memNode
}
func (me *memNodeFile) String() string {
return fmt.Sprintf("memUfsFile(%s)", me.File.String())
}
func (me *memNodeFile) InnerFile() fuse.File {
return me.File
}
func (me *memNodeFile) Release() {
// Must do the subfile release first, as that may flush data
// to disk.
me.File.Release()
if me.writable {
me.node.fs.markCloseWrite()
}
}
func (me *memNodeFile) Flush() fuse.Status {
code := me.File.Flush()
if me.writable {
// TODO - should this be in Release?
fi, _ := me.File.GetAttr()
me.node.mutex.Lock()
defer me.node.mutex.Unlock()
me.node.info.Size = fi.Size
me.node.info.Blocks = fi.Blocks
}
return code
}
func (me *memNode) newFile(f fuse.File, writable bool) fuse.File {
return &memNodeFile{
File: f,
writable: writable,
node: me,
}
}
// Must run inside mutex.
func (me *memNode) promote() {
if me.backing == "" {
me.backing = me.fs.getFilename()
destfs := &fuse.LoopbackFileSystem{Root: "/"}
fuse.CopyFile(me.fs.readonly, destfs,
me.original, strings.TrimLeft(me.backing, "/"), nil)
me.original = ""
files := me.Inode().Files(0)
for _, f := range files {
mf := f.File.(*memNodeFile)
inner := mf.File
osFile, err := os.Open(me.backing)
if err != nil {
panic("error opening backing file")
}
mf.File = &fuse.LoopbackFile{File: osFile}
inner.Flush()
inner.Release()
}
}
}
func (me *memNode) Open(flags uint32, context *fuse.Context) (file fuse.File, code fuse.Status) {
me.mutex.Lock()
defer me.mutex.Unlock()
if flags&fuse.O_ANYWRITE != 0 {
me.promote()
me.touch()
}
if me.backing != "" {
f, err := os.OpenFile(me.backing, int(flags), 0666)
if err != nil {
return nil, fuse.ToStatus(err)
}
wr := flags&fuse.O_ANYWRITE != 0
if wr {
me.fs.openWritable++
}
return me.newFile(&fuse.LoopbackFile{File: f}, wr), fuse.OK
}
file, code = me.fs.readonly.Open(me.original, flags, context)
if !code.Ok() {
return nil, code
}
return me.newFile(file, false), fuse.OK
}
func (me *memNode) GetAttr(file fuse.File, context *fuse.Context) (fi *os.FileInfo, code fuse.Status) {
var sz int64
if file != nil {
fi, code := file.GetAttr()
if code.Ok() {
sz = fi.Size
} else {
msg := fmt.Sprintf("File.GetAttr(%s) = %v, %v", file.String(), fi, code)
panic(msg)
}
}
me.mutex.RLock()
defer me.mutex.RUnlock()
info := me.info
if file != nil {
info.Size = sz
}
return &info, fuse.OK
}
func (me *memNode) Truncate(file fuse.File, size uint64, context *fuse.Context) (code fuse.Status) {
me.mutex.Lock()
defer me.mutex.Unlock()
me.promote()
if file != nil {
return file.Truncate(size)
}
me.info.Size = int64(size)
err := os.Truncate(me.backing, int64(size))
me.touch()
return fuse.ToStatus(err)
}
func (me *memNode) Utimens(file fuse.File, atime uint64, mtime uint64, context *fuse.Context) (code fuse.Status) {
me.mutex.Lock()
defer me.mutex.Unlock()
me.info.Atime_ns = int64(atime)
me.info.Mtime_ns = int64(mtime)
me.ctouch()
return fuse.OK
}
func (me *memNode) Chmod(file fuse.File, perms uint32, context *fuse.Context) (code fuse.Status) {
me.mutex.Lock()
defer me.mutex.Unlock()
me.info.Mode = (me.info.Mode &^ 07777) | perms
me.ctouch()
return fuse.OK
}
func (me *memNode) Chown(file fuse.File, uid uint32, gid uint32, context *fuse.Context) (code fuse.Status) {
if context.Uid != 0 {
return fuse.EPERM
}
me.mutex.Lock()
defer me.mutex.Unlock()
me.info.Uid = int(uid)
me.info.Gid = int(gid)
me.ctouch()
return fuse.OK
}
func (me *memNode) OpenDir(context *fuse.Context) (stream chan fuse.DirEntry, code fuse.Status) {
me.mutex.RLock()
defer me.mutex.RUnlock()
ch := map[string]uint32{}
if me.original != "" || me == me.fs.root {
stream, code = me.fs.readonly.OpenDir(me.original, context)
for e := range stream {
fn := filepath.Join(me.original, e.Name)
if !me.fs.deleted[fn] {
ch[e.Name] = e.Mode
}
}
}
for k, n := range me.Inode().FsChildren() {
ch[k] = n.FsNode().(*memNode).info.Mode
}
stream = make(chan fuse.DirEntry, len(ch))
for k, v := range ch {
stream <- fuse.DirEntry{Name: k, Mode: v}
}
close(stream)
return stream, fuse.OK
}
func (me *memNode) reap(path string, results map[string]*Result) {
if me.changed {
info := me.info
results[path] = &Result{
FileInfo: &info,
Link: me.link,
Backing: me.backing,
Original: me.original,
}
}
for n, ch := range me.Inode().FsChildren() {
p := filepath.Join(path, n)
ch.FsNode().(*memNode).reap(p, results)
}
}
func (me *memNode) reset(path string) (entryNotify bool) {
for n, ch := range me.Inode().FsChildren() {
p := filepath.Join(path, n)
mn := ch.FsNode().(*memNode)
if mn.reset(p) {
me.Inode().RmChild(n)
me.fs.connector.EntryNotify(me.Inode(), n)
}
}
if me.backing != "" || me.original != path {
return true
}
if me.changed {
info, code := me.fs.readonly.GetAttr(me.original, nil)
if !code.Ok() {
return true
}
me.info = *info
me.fs.connector.FileNotify(me.Inode(), -1, 0)
if me.Inode().IsDir() {
me.fs.connector.FileNotify(me.Inode(), 0, 0)
}
}
return false
}
package unionfs
import (
"fmt"
"github.com/hanwen/go-fuse/fuse"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"syscall"
"testing"
"time"
)
var _ = fmt.Print
var _ = log.Print
var CheckSuccess = fuse.CheckSuccess
func setupMemUfs(t *testing.T) (workdir string, ufs *MemUnionFs, cleanup func()) {
// Make sure system setting does not affect test.
syscall.Umask(0)
wd, _ := ioutil.TempDir("", "")
err := os.Mkdir(wd+"/mnt", 0700)
fuse.CheckSuccess(err)
err = os.Mkdir(wd+"/backing", 0700)
fuse.CheckSuccess(err)
os.Mkdir(wd+"/ro", 0700)
fuse.CheckSuccess(err)
roFs := fuse.NewLoopbackFileSystem(wd + "/ro")
memFs := NewMemUnionFs(wd+"/backing", roFs)
// We configure timeouts are smaller, so we can check for
// UnionFs's cache consistency.
opts := &fuse.FileSystemOptions{
EntryTimeout: .5 * entryTtl,
AttrTimeout: .5 * entryTtl,
NegativeTimeout: .5 * entryTtl,
PortableInodes: true,
}
state, conn, err := fuse.MountNodeFileSystem(wd+"/mnt", memFs, opts)
CheckSuccess(err)
conn.Debug = fuse.VerboseTest()
state.Debug = fuse.VerboseTest()
go state.Loop()
return wd, memFs, func() {
state.Unmount()
os.RemoveAll(wd)
}
}
func TestMemUnionFsSymlink(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
err := os.Symlink("/foobar", wd+"/mnt/link")
CheckSuccess(err)
val, err := os.Readlink(wd + "/mnt/link")
CheckSuccess(err)
if val != "/foobar" {
t.Errorf("symlink mismatch: %v", val)
}
r := ufs.Reap()
if len(r) != 2 || r["link"] == nil || r["link"].Link != "/foobar" {
t.Errorf("expect 1 symlink reap result: %v", r)
}
}
func TestMemUnionFsSymlinkPromote(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
err := os.Mkdir(wd+"/ro/subdir", 0755)
CheckSuccess(err)
err = os.Symlink("/foobar", wd+"/mnt/subdir/link")
CheckSuccess(err)
r := ufs.Reap()
if len(r) != 2 || r["subdir"] == nil || r["subdir/link"] == nil || r["subdir/link"].Link != "/foobar" {
t.Errorf("expect 1 symlink reap result: %v", r)
}
}
func TestMemUnionFsChtimes(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
writeToFile(wd+"/ro/file", "a")
err := os.Chtimes(wd+"/ro/file", 42e9, 43e9)
CheckSuccess(err)
err = os.Chtimes(wd+"/mnt/file", 82e9, 83e9)
CheckSuccess(err)
fi, err := os.Lstat(wd + "/mnt/file")
if fi.Atime_ns != 82e9 || fi.Mtime_ns != 83e9 {
t.Error("Incorrect timestamp", fi)
}
r := ufs.Reap()
if r["file"] == nil || r["file"].Original == "" {
t.Errorf("expect 1 file reap result: %v", r)
}
}
func TestMemUnionFsChmod(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
ro_fn := wd + "/ro/file"
m_fn := wd + "/mnt/file"
writeToFile(ro_fn, "a")
err := os.Chmod(m_fn, 07070)
CheckSuccess(err)
fi, err := os.Lstat(m_fn)
CheckSuccess(err)
if fi.Mode&07777 != 07070 {
t.Errorf("Unexpected mode found: %o", fi.Mode)
}
r := ufs.Reap()
if r["file"] == nil || r["file"].Original == "" {
t.Errorf("expect 1 file reap result: %v", r)
}
}
func TestMemUnionFsChown(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
ro_fn := wd + "/ro/file"
m_fn := wd + "/mnt/file"
writeToFile(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 TestMemUnionFsDelete(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
writeToFile(wd+"/ro/file", "a")
_, err := os.Lstat(wd + "/mnt/file")
CheckSuccess(err)
err = os.Remove(wd + "/mnt/file")
CheckSuccess(err)
_, err = os.Lstat(wd + "/mnt/file")
if err == nil {
t.Fatal("should have disappeared.")
}
r := ufs.Reap()
if r["file"] == nil || r["file"].FileInfo != nil {
t.Errorf("expect 1 deletion reap result: %v", r)
}
}
func TestMemUnionFsBasic(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
writeToFile(wd+"/mnt/rw", "a")
writeToFile(wd+"/ro/ro1", "a")
writeToFile(wd+"/ro/ro2", "b")
names := dirNames(wd + "/mnt")
expected := map[string]bool{
"rw": true, "ro1": true, "ro2": true,
}
checkMapEq(t, names, expected)
writeToFile(wd+"/mnt/new", "new contents")
contents := readFromFile(wd + "/mnt/new")
if contents != "new contents" {
t.Errorf("read mismatch: '%v'", contents)
}
writeToFile(wd+"/mnt/ro1", "promote me")
remove(wd + "/mnt/new")
names = dirNames(wd + "/mnt")
checkMapEq(t, names, map[string]bool{
"rw": true, "ro1": true, "ro2": true,
})
remove(wd + "/mnt/ro1")
names = dirNames(wd + "/mnt")
checkMapEq(t, names, map[string]bool{
"rw": true, "ro2": true,
})
}
func TestMemUnionFsPromote(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
err := os.Mkdir(wd+"/ro/subdir", 0755)
CheckSuccess(err)
writeToFile(wd+"/ro/subdir/file", "content")
writeToFile(wd+"/mnt/subdir/file", "other-content")
r := ufs.Reap()
if r["subdir/file"] == nil || r["subdir/file"].Backing == "" {
t.Errorf("expect 1 file reap result: %v", r)
}
}
func TestMemUnionFsSubdirCreate(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
err := os.MkdirAll(wd+"/ro/subdir/sub2", 0755)
CheckSuccess(err)
writeToFile(wd+"/mnt/subdir/sub2/file", "other-content")
_, err = os.Lstat(wd + "/mnt/subdir/sub2/file")
CheckSuccess(err)
r := ufs.Reap()
if r["subdir/sub2/file"] == nil || r["subdir/sub2/file"].Backing == "" {
t.Errorf("expect 1 file reap result: %v", r)
}
}
func TestMemUnionFsCreate(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
writeToFile(wd+"/mnt/file.txt", "hello")
_, err := os.Lstat(wd + "/mnt/file.txt")
CheckSuccess(err)
r := ufs.Reap()
if r["file.txt"] == nil || r["file.txt"].Backing == "" {
t.Errorf("expect 1 file reap result: %v", r)
}
}
func TestMemUnionFsOpenUndeletes(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
writeToFile(wd+"/ro/file", "X")
err := os.Remove(wd + "/mnt/file")
CheckSuccess(err)
writeToFile(wd+"/mnt/file", "X")
_, err = os.Lstat(wd + "/mnt/file")
}
func TestMemUnionFsMkdir(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
dirname := wd + "/mnt/subdir"
err := os.Mkdir(dirname, 0755)
CheckSuccess(err)
err = os.Remove(dirname)
CheckSuccess(err)
r := ufs.Reap()
if len(r) > 2 || r[""] == nil || r["subdir"] != nil {
t.Errorf("expect 1 file reap result: %v", r)
}
}
func TestMemUnionFsMkdirPromote(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
dirname := wd + "/ro/subdir/subdir2"
err := os.MkdirAll(dirname, 0755)
CheckSuccess(err)
err = os.Mkdir(wd+"/mnt/subdir/subdir2/dir3", 0755)
CheckSuccess(err)
r := ufs.Reap()
if r["subdir/subdir2/dir3"] == nil || r["subdir/subdir2/dir3"].FileInfo.Mode&fuse.S_IFDIR == 0 {
t.Errorf("expect 1 file reap result: %v", r)
}
}
func TestMemUnionFsRmdirMkdir(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
err := os.Mkdir(wd+"/ro/subdir", 0755)
CheckSuccess(err)
dirname := wd + "/mnt/subdir"
err = os.Remove(dirname)
CheckSuccess(err)
err = os.Mkdir(dirname, 0755)
CheckSuccess(err)
}
func TestMemUnionFsLink(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
content := "blabla"
fn := wd + "/ro/file"
err := ioutil.WriteFile(fn, []byte(content), 0666)
CheckSuccess(err)
err = os.Link(wd+"/mnt/file", wd+"/mnt/linked")
CheckSuccess(err)
fi2, err := os.Lstat(wd + "/mnt/linked")
CheckSuccess(err)
fi1, err := os.Lstat(wd + "/mnt/file")
CheckSuccess(err)
if fi1.Ino != fi2.Ino {
t.Errorf("inode numbers should be equal for linked files %v, %v", fi1.Ino, fi2.Ino)
}
c, err := ioutil.ReadFile(wd + "/mnt/linked")
if string(c) != content {
t.Errorf("content mismatch got %q want %q", string(c), content)
}
}
func TestMemUnionFsCreateLink(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
content := "blabla"
fn := wd + "/mnt/file"
err := ioutil.WriteFile(fn, []byte(content), 0666)
CheckSuccess(err)
err = os.Link(wd+"/mnt/file", wd+"/mnt/linked")
CheckSuccess(err)
}
func TestMemUnionFsTruncate(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
writeToFile(wd+"/ro/file", "hello")
os.Truncate(wd+"/mnt/file", 2)
content := readFromFile(wd + "/mnt/file")
if content != "he" {
t.Errorf("unexpected content %v", content)
}
}
func TestMemUnionFsCopyChmod(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
contents := "hello"
fn := wd + "/mnt/y"
err := ioutil.WriteFile(fn, []byte(contents), 0644)
CheckSuccess(err)
err = os.Chmod(fn, 0755)
CheckSuccess(err)
fi, err := os.Lstat(fn)
CheckSuccess(err)
if fi.Mode&0111 == 0 {
t.Errorf("1st attr error %o", fi.Mode)
}
time.Sleep(entryTtl * 1.1e9)
fi, err = os.Lstat(fn)
CheckSuccess(err)
if fi.Mode&0111 == 0 {
t.Errorf("uncached attr error %o", fi.Mode)
}
}
func TestMemUnionFsTruncateTimestamp(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
contents := "hello"
fn := wd + "/mnt/y"
err := ioutil.WriteFile(fn, []byte(contents), 0644)
CheckSuccess(err)
time.Sleep(0.2e9)
truncTs := time.Nanoseconds()
err = os.Truncate(fn, 3)
CheckSuccess(err)
fi, err := os.Lstat(fn)
CheckSuccess(err)
if abs(truncTs-fi.Mtime_ns) > 0.1e9 {
t.Error("timestamp drift", truncTs, fi.Mtime_ns)
}
}
func TestMemUnionFsRemoveAll(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
err := os.MkdirAll(wd+"/ro/dir/subdir", 0755)
CheckSuccess(err)
contents := "hello"
fn := wd + "/ro/dir/subdir/y"
err = ioutil.WriteFile(fn, []byte(contents), 0644)
CheckSuccess(err)
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)
}
}
}
func TestMemUnionFsRmRf(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
err := os.MkdirAll(wd+"/ro/dir/subdir", 0755)
CheckSuccess(err)
contents := "hello"
fn := wd + "/ro/dir/subdir/y"
err = ioutil.WriteFile(fn, []byte(contents), 0644)
CheckSuccess(err)
bin, err := exec.LookPath("rm")
CheckSuccess(err)
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)
}
}
}
func TestMemUnionFsDeletedGetAttr(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
err := ioutil.WriteFile(wd+"/ro/file", []byte("blabla"), 0644)
CheckSuccess(err)
f, err := os.Open(wd + "/mnt/file")
CheckSuccess(err)
defer f.Close()
err = os.Remove(wd + "/mnt/file")
CheckSuccess(err)
if fi, err := f.Stat(); err != nil || !fi.IsRegular() {
t.Fatalf("stat returned error or non-file: %v %v", err, fi)
}
}
func TestMemUnionFsDoubleOpen(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
err := ioutil.WriteFile(wd+"/ro/file", []byte("blablabla"), 0644)
CheckSuccess(err)
roFile, err := os.Open(wd + "/mnt/file")
CheckSuccess(err)
defer roFile.Close()
rwFile, err := os.OpenFile(wd+"/mnt/file", os.O_WRONLY|os.O_TRUNC, 0666)
CheckSuccess(err)
defer rwFile.Close()
output, err := ioutil.ReadAll(roFile)
CheckSuccess(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))
CheckSuccess(err)
b := make([]byte, 100)
roFile.Seek(0, 0)
n, err := roFile.Read(b)
CheckSuccess(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 TestMemUnionFsUpdate(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
err := ioutil.WriteFile(wd+"/ro/file1", []byte("blablabla"), 0644)
CheckSuccess(err)
_, err = os.Lstat(wd + "/mnt/file1")
CheckSuccess(err)
if fi, _ := os.Lstat(wd + "/mnt/file2"); fi != nil {
t.Fatal("file2 should not exist", fi)
}
if fi, _ := os.Lstat(wd + "/mnt/symlink"); fi != nil {
t.Fatal("symlink should not exist", fi)
}
err = os.Remove(wd + "/ro/file1")
CheckSuccess(err)
err = ioutil.WriteFile(wd+"/ro/file2", []byte("foobar"), 0644)
CheckSuccess(err)
err = os.Symlink("target", wd+"/ro/symlink")
CheckSuccess(err)
// Still have cached attributes.
fi, err := os.Lstat(wd + "/mnt/file1")
CheckSuccess(err)
if fi, _ := os.Lstat(wd + "/mnt/file2"); fi != nil {
t.Fatal("file2 should not exist")
}
if fi, _ := os.Lstat(wd + "/mnt/symlink"); fi != nil {
t.Fatal("symlink should not exist", fi)
}
roF2, err := os.Lstat(wd + "/ro/file2")
CheckSuccess(err)
roSymlinkFi, err := os.Lstat(wd + "/ro/symlink")
CheckSuccess(err)
updates := map[string]*Result{
"file1": &Result{
nil, "", "", "",
},
"file2": &Result{
roF2, "", "", "",
},
"symlink": &Result{
roSymlinkFi, "", "", "target",
},
}
ufs.Update(updates)
// Cached attributes flushed.
if fi, _ := os.Lstat(wd + "/mnt/file1"); fi != nil {
t.Fatal("file1 should have disappeared", fi)
}
fi, err = os.Lstat(wd + "/mnt/file2")
CheckSuccess(err)
if roF2.Mtime_ns != fi.Mtime_ns {
t.Fatalf("file2 attribute mismatch: got %v want %v", fi, roF2)
}
val, err := os.Readlink(wd + "/mnt/symlink")
CheckSuccess(err)
if val != "target" {
t.Error("symlink value got %q want %v", val, "target")
}
}
func TestMemUnionFsFdLeak(t *testing.T) {
beforeEntries, err := ioutil.ReadDir("/proc/self/fd")
CheckSuccess(err)
wd, _, clean := setupMemUfs(t)
err = ioutil.WriteFile(wd+"/ro/file", []byte("blablabla"), 0644)
CheckSuccess(err)
contents, err := ioutil.ReadFile(wd + "/mnt/file")
CheckSuccess(err)
err = ioutil.WriteFile(wd+"/mnt/file", contents, 0644)
CheckSuccess(err)
clean()
afterEntries, err := ioutil.ReadDir("/proc/self/fd")
CheckSuccess(err)
if len(afterEntries) != len(beforeEntries) {
t.Errorf("/proc/self/fd changed size: after %v before %v", len(beforeEntries), len(afterEntries))
}
}
func TestMemUnionFsStatFs(t *testing.T) {
wd, _, clean := setupMemUfs(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 TestMemUnionFsFlushSize(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
fn := wd + "/mnt/file"
f, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE, 0644)
CheckSuccess(err)
fi, err := f.Stat()
CheckSuccess(err)
n, err := f.Write([]byte("hello"))
CheckSuccess(err)
f.Close()
fi, err = os.Lstat(fn)
CheckSuccess(err)
if fi.Size != int64(n) {
t.Errorf("got %d from Stat().Size, want %d", fi.Size, n)
}
}
func TestMemUnionFsFlushRename(t *testing.T) {
wd, _, clean := setupMemUfs(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)
CheckSuccess(err)
fi, err := f.Stat()
CheckSuccess(err)
n, err := f.Write([]byte("hello"))
CheckSuccess(err)
f.Close()
dst := wd + "/mnt/file"
err = os.Rename(fn, dst)
CheckSuccess(err)
fi, err = os.Lstat(dst)
CheckSuccess(err)
if fi.Size != int64(n) {
t.Errorf("got %d from Stat().Size, want %d", fi.Size, n)
}
}
func TestMemUnionFsTruncGetAttr(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
c := []byte("hello")
f, err := os.OpenFile(wd+"/mnt/file", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
CheckSuccess(err)
_, err = f.Write(c)
CheckSuccess(err)
err = f.Close()
CheckSuccess(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 TestMemUnionFsRenameDirBasic(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
err := os.MkdirAll(wd+"/ro/dir/subdir", 0755)
CheckSuccess(err)
err = os.Rename(wd+"/mnt/dir", wd+"/mnt/renamed")
CheckSuccess(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.IsDirectory() {
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)
}
r := ufs.Reap()
if r["dir"] == nil || r["dir"].FileInfo != nil || r["renamed/subdir"] == nil || !r["renamed/subdir"].FileInfo.IsDirectory() {
t.Errorf("Reap should del dir, and add renamed/subdir: %v", r)
}
if err = os.Mkdir(wd+"/mnt/dir", 0755); err != nil {
t.Errorf("mkdir should succeed %v", err)
}
}
func TestMemUnionFsRenameDirAllSourcesGone(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
err := os.MkdirAll(wd+"/ro/dir", 0755)
CheckSuccess(err)
err = ioutil.WriteFile(wd+"/ro/dir/file.txt", []byte{42}, 0644)
CheckSuccess(err)
err = os.Rename(wd+"/mnt/dir", wd+"/mnt/renamed")
CheckSuccess(err)
r := ufs.Reap()
if r["dir"] == nil || r["dir"].FileInfo != nil || r["dir/file.txt"] == nil || r["dir/file.txt"].FileInfo != nil {
t.Errorf("Expected 2 deletion entries in %v", r)
}
}
func TestMemUnionFsRenameDirWithDeletions(t *testing.T) {
wd, _, clean := setupMemUfs(t)
defer clean()
err := os.MkdirAll(wd+"/ro/dir/subdir", 0755)
CheckSuccess(err)
err = ioutil.WriteFile(wd+"/ro/dir/file.txt", []byte{42}, 0644)
CheckSuccess(err)
err = ioutil.WriteFile(wd+"/ro/dir/subdir/file.txt", []byte{42}, 0644)
CheckSuccess(err)
if fi, _ := os.Lstat(wd + "/mnt/dir/subdir/file.txt"); fi == nil || !fi.IsRegular() {
t.Fatalf("%s/mnt/dir/subdir/file.txt should be file: %v", wd, fi)
}
err = os.Remove(wd + "/mnt/dir/file.txt")
CheckSuccess(err)
err = os.Rename(wd+"/mnt/dir", wd+"/mnt/renamed")
CheckSuccess(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.IsDirectory() {
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 TestMemUnionGc(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
writeToFile(wd+"/mnt/file1", "other-content")
writeToFile(wd+"/mnt/file2", "other-content")
err := os.Remove(wd + "/mnt/file1")
CheckSuccess(err)
ufs.Reset()
entries, err := ioutil.ReadDir(wd + "/backing")
CheckSuccess(err)
if len(entries) != 0 {
t.Fatalf("should have 1 file after backing store gc: %v", entries)
}
}
func testEq(t *testing.T, got interface{}, want interface{}, expectEq bool) {
gots := fmt.Sprintf("%v", got)
wants := fmt.Sprintf("%v", want)
if (gots == wants) != expectEq {
op := "must differ from"
if expectEq {
op = "want"
}
t.Fatalf("Got %s %s %s.", gots, op, wants)
}
}
func TestMemUnionResetAttr(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
ioutil.WriteFile(wd+"/ro/fileattr", []byte{42}, 0644)
before, _ := os.Lstat(wd + "/mnt/fileattr")
err := os.Chmod(wd+"/mnt/fileattr", 0606)
CheckSuccess(err)
after, _ := os.Lstat(wd + "/mnt/fileattr")
testEq(t, after, before, false)
ufs.Reset()
afterReset, _ := os.Lstat(wd + "/mnt/fileattr")
testEq(t, afterReset, before, true)
}
func TestMemUnionResetContent(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
ioutil.WriteFile(wd+"/ro/filecontents", []byte{42}, 0644)
before, _ := ioutil.ReadFile(wd + "/mnt/filecontents")
ioutil.WriteFile(wd+"/mnt/filecontents", []byte{43}, 0644)
after, _ := ioutil.ReadFile(wd + "/mnt/filecontents")
testEq(t, after, before, false)
ufs.Reset()
afterReset, err := ioutil.ReadFile(wd + "/mnt/filecontents")
CheckSuccess(err)
testEq(t, afterReset, before, true)
}
func TestMemUnionResetDelete(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
ioutil.WriteFile(wd+"/ro/todelete", []byte{42}, 0644)
before, _ := os.Lstat(wd + "/mnt/todelete")
before.Ino = 0
os.Remove(wd + "/mnt/todelete")
after, _ := os.Lstat(wd + "/mnt/todelete")
testEq(t, after, before, false)
ufs.Reset()
afterReset, _ := os.Lstat(wd + "/mnt/todelete")
afterReset.Ino = 0
testEq(t, afterReset, before, true)
}
func clearInodes(infos []*os.FileInfo) {
for _, i := range infos {
i.Ino = 0
}
}
func TestMemUnionResetDirEntry(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
os.Mkdir(wd+"/ro/dir", 0755)
ioutil.WriteFile(wd+"/ro/dir/todelete", []byte{42}, 0644)
before, _ := ioutil.ReadDir(wd + "/mnt/dir")
clearInodes(before)
ioutil.WriteFile(wd+"/mnt/dir/newfile", []byte{42}, 0644)
os.Remove(wd + "/mnt/dir/todelete")
after, _ := ioutil.ReadDir(wd + "/mnt/dir")
clearInodes(after)
testEq(t, fuse.OsFileInfos(after), fuse.OsFileInfos(before), false)
ufs.Reset()
reset, _ := ioutil.ReadDir(wd + "/mnt/dir")
clearInodes(reset)
testEq(t, fuse.OsFileInfos(reset), fuse.OsFileInfos(before), true)
}
func TestMemUnionResetRename(t *testing.T) {
wd, ufs, clean := setupMemUfs(t)
defer clean()
os.Mkdir(wd+"/ro/dir", 0755)
ioutil.WriteFile(wd+"/ro/dir/movesrc", []byte{42}, 0644)
before, _ := ioutil.ReadDir(wd + "/mnt/dir")
clearInodes(before)
os.Rename(wd+"/mnt/dir/movesrc", wd+"/mnt/dir/dest")
after, _ := ioutil.ReadDir(wd + "/mnt/dir")
clearInodes(after)
testEq(t, fuse.OsFileInfos(after), fuse.OsFileInfos(before), false)
ufs.Reset()
reset, _ := ioutil.ReadDir(wd + "/mnt/dir")
clearInodes(reset)
testEq(t, fuse.OsFileInfos(reset), fuse.OsFileInfos(before), true)
}
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