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

Open source UnionFS, a FUSE union mount filesystem.

parent 4e6239d4
# Use "gomake install" to build and install this package.
include $(GOROOT)/src/Make.inc
TARG=unionfs
GOFILES=main.go
DEPS=../../fuse ../../unionfs
include $(GOROOT)/src/Make.cmd
package main
import (
"flag"
"fmt"
"github.com/hanwen/go-fuse/fuse"
"github.com/hanwen/go-fuse/unionfs"
"os"
)
func main() {
debug := flag.Bool("debug", false, "debug on")
threaded := flag.Bool("threaded", true, "debug on")
delcache_ttl := flag.Float64("deletion_cache_ttl", 5.0, "Deletion cache TTL in seconds.")
branchcache_ttl := flag.Float64("branchcache_ttl", 5.0, "Branch cache TTL in seconds.")
deldirname := flag.String(
"deletion_dirname", "GOUNIONFS_DELETIONS", "Directory name to use for deletions.")
flag.Parse()
if len(flag.Args()) < 2 {
fmt.Println("Usage:\n main MOUNTPOINT BASEDIR")
os.Exit(2)
}
mountpoint := flag.Arg(0)
ufsOptions := unionfs.UnionFsOptions{
DeletionCacheTTLSecs: *delcache_ttl,
BranchCacheTTLSecs: *branchcache_ttl,
DeletionDirName: *deldirname,
}
options := unionfs.AutoUnionFsOptions{
UnionFsOptions: ufsOptions,
}
gofs := unionfs.NewAutoUnionFs(flag.Arg(1), options)
conn := fuse.NewPathFileSystemConnector(gofs)
mountState := fuse.NewMountState(conn)
mountState.Debug = *debug
fmt.Printf("Mounting...\n")
err := mountState.Mount(mountpoint)
if err != nil {
fmt.Printf("MountFuse fail: %v\n", err)
os.Exit(1)
}
fmt.Printf("Mounted!\n")
mountState.Loop(*threaded)
}
include $(GOROOT)/src/Make.inc
TARG=github.com/hanwen/go-fuse/unionfs
GOFILES=unionfs.go \
dircache.go \
timedcache.go \
cachingfs.go \
autounion.go \
DEPS=../fuse
include $(GOROOT)/src/Make.pkg
package unionfs
import (
"fmt"
"github.com/hanwen/go-fuse/fuse"
"log"
"os"
"path/filepath"
"strings"
"syscall"
"sync"
"time"
)
// 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 {
fuse.DefaultPathFilesystem
lock sync.RWMutex
knownFilesystems map[string]*UnionFs
root string
connector *fuse.PathFileSystemConnector
options *AutoUnionFsOptions
}
type AutoUnionFsOptions struct {
UnionFsOptions
}
func NewAutoUnionFs(directory string, options AutoUnionFsOptions) *AutoUnionFs {
a := new(AutoUnionFs)
a.knownFilesystems = make(map[string]*UnionFs)
a.options = &options
a.root = directory
return a
}
func (me *AutoUnionFs) Mount(connector *fuse.PathFileSystemConnector) fuse.Status {
me.connector = connector
time.AfterFunc(0.1e9, func() { me.updateKnownFses() })
return fuse.OK
}
func (me *AutoUnionFs) addFs(roots []string) {
relative := strings.TrimLeft(strings.Replace(roots[0], me.root, "", -1), "/")
name := strings.Replace(relative, "/", "-", -1)
if name == "config" || name == "status" {
log.Println("Illegal name for overlay", roots)
return
}
me.lock.Lock()
var gofs *UnionFs
if me.knownFilesystems[name] == nil {
log.Println("Adding UnionFs for roots", roots)
gofs = NewUnionfs(roots, me.options.UnionFsOptions)
me.knownFilesystems[name] = gofs
}
me.lock.Unlock()
if gofs != nil {
me.connector.Mount("/"+name, gofs)
}
}
// TODO - should hide these methods.
func (me *AutoUnionFs) VisitDir(path string, f *os.FileInfo) bool {
ro := filepath.Join(path, "READONLY")
fi, err := os.Lstat(ro)
if err == nil && fi.IsSymlink() {
// TODO - should recurse and chain all READONLYs
// together.
me.addFs([]string{path, ro})
}
return true
}
func (me *AutoUnionFs) VisitFile(path string, f *os.FileInfo) {
}
func (me *AutoUnionFs) updateKnownFses() {
log.Println("Looking for new filesystems")
filepath.Walk(me.root, me, nil)
}
func (me *AutoUnionFs) Readlink(path string) (out string, code fuse.Status) {
comps := strings.Split(path, filepath.SeparatorString, -1)
if comps[0] != "config" {
return "", fuse.ENOENT
}
name := comps[1]
me.lock.RLock()
defer me.lock.RUnlock()
fs := me.knownFilesystems[name]
if fs == nil {
return "", fuse.ENOENT
}
return fs.Roots()[0], fuse.OK
}
func (me *AutoUnionFs) GetAttr(path string) (*fuse.Attr, fuse.Status) {
if path == "" || path == "config" || path == "status" {
a := &fuse.Attr{
Mode: fuse.S_IFDIR | 0755,
}
return a, fuse.OK
}
if path == "status/gounionfs_version" {
a := &fuse.Attr{
Mode: fuse.S_IFREG | 0644,
}
return a, fuse.OK
}
comps := strings.Split(path, filepath.SeparatorString, -1)
me.lock.RLock()
defer me.lock.RUnlock()
if len(comps) > 1 && comps[0] == "config" {
fs := me.knownFilesystems[comps[1]]
if fs == nil {
return nil, fuse.ENOENT
}
a := &fuse.Attr{
Mode: syscall.S_IFLNK | 0644,
}
return a, fuse.OK
}
if me.knownFilesystems[path] != nil {
return &fuse.Attr{
Mode: fuse.S_IFDIR | 0755,
},fuse.OK
}
return nil, fuse.ENOENT
}
func (me *AutoUnionFs) StatusDir() (stream chan fuse.DirEntry, status fuse.Status) {
stream = make(chan fuse.DirEntry, 1)
stream <- fuse.DirEntry{
Name: "gounionfs_version",
Mode: fuse.S_IFREG | 0644,
}
close(stream)
return stream, fuse.OK
}
func (me *AutoUnionFs) OpenDir(name string) (stream chan fuse.DirEntry, status fuse.Status) {
switch name {
case "status":
return me.StatusDir()
case "config":
me.updateKnownFses()
case "/":
name = ""
case "":
default:
panic(fmt.Sprintf("Don't know how to list dir %v", name))
}
me.lock.RLock()
defer me.lock.RUnlock()
stream = make(chan fuse.DirEntry, len(me.knownFilesystems)+5)
for k, _ := range me.knownFilesystems {
mode := fuse.S_IFDIR | 0755
if name == "config" {
mode = syscall.S_IFLNK | 0644
}
stream <- fuse.DirEntry{
Name: k,
Mode: uint32(mode),
}
}
if name == "" {
stream <- fuse.DirEntry{
Name: "config",
Mode: uint32(fuse.S_IFDIR | 0755),
}
stream <- fuse.DirEntry{
Name: "status",
Mode: uint32(fuse.S_IFDIR | 0755),
}
}
close(stream)
return stream, status
}
package unionfs
import (
"github.com/hanwen/go-fuse/fuse"
"sync"
)
type attrResponse struct {
attr *fuse.Attr
code fuse.Status
}
type dirResponse struct {
entries []fuse.DirEntry
code fuse.Status
}
type linkResponse struct {
linkContent string
code fuse.Status
}
// Caches readdir and getattr()
type CachingFileSystem struct {
fuse.WrappingPathFilesystem
attributesLock sync.RWMutex
attributes map[string]attrResponse
dirsLock sync.RWMutex
dirs map[string]dirResponse
linksLock sync.RWMutex
links map[string]linkResponse
}
func NewCachingFileSystem(pfs fuse.PathFilesystem) *CachingFileSystem {
c := new(CachingFileSystem)
c.Original = pfs
c.attributes = make(map[string]attrResponse)
c.dirs = make(map[string]dirResponse)
c.links = make(map[string]linkResponse)
return c
}
func (me *CachingFileSystem) GetAttr(name string) (*fuse.Attr, fuse.Status) {
me.attributesLock.RLock()
v, ok := me.attributes[name]
me.attributesLock.RUnlock()
if ok {
return v.attr, v.code
}
var r attrResponse
r.attr, r.code = me.Original.GetAttr(name)
// TODO - could do async.
me.attributesLock.Lock()
me.attributes[name] = r
me.attributesLock.Unlock()
return r.attr, r.code
}
func (me *CachingFileSystem) Readlink(name string) (string, fuse.Status) {
me.linksLock.RLock()
v, ok := me.links[name]
me.linksLock.RUnlock()
if ok {
return v.linkContent, v.code
}
v.linkContent, v.code = me.Original.Readlink(name)
// TODO - could do async.
me.linksLock.Lock()
me.links[name] = v
me.linksLock.Unlock()
return v.linkContent, v.code
}
func (me *CachingFileSystem) OpenDir(name string) (stream chan fuse.DirEntry, status fuse.Status) {
me.dirsLock.RLock()
v, ok := me.dirs[name]
me.dirsLock.RUnlock()
if !ok {
origStream, code := me.Original.OpenDir(name)
if code != fuse.OK {
return nil, code
}
v.code = code
for {
d := <-origStream
if d.Name == "" {
break
}
v.entries = append(v.entries, d)
}
me.dirsLock.Lock()
me.dirs[name] = v
me.dirsLock.Unlock()
}
stream = make(chan fuse.DirEntry)
go func() {
for _, d := range v.entries {
stream <- d
}
stream <- fuse.DirEntry{}
}()
return stream, v.code
}
package unionfs
import (
"os"
"sync"
"log"
"time"
)
/*
On error, returns an empty map, since we have little options
for outputting any other diagnostics.
*/
func newDirnameMap(dir string) map[string]bool {
result := make(map[string]bool)
f, err := os.Open(dir)
if err != nil {
log.Printf("newDirnameMap(): %v %v", dir, err)
return result
}
names, err := f.Readdirnames(-1)
if err != nil {
log.Printf("newDirnameMap(): readdirnames %v %v", dir, err)
return result
}
for _, n := range names {
result[n] = true
}
return result
}
/*
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
ttlNs int64
// Protects data below.
lock sync.RWMutex
// If nil, you may call refresh() to schedule a new one.
names map[string]bool
updateRunning bool
}
func (me *DirCache) setMap(newMap map[string]bool) {
me.lock.Lock()
defer me.lock.Unlock()
me.names = newMap
me.updateRunning = false
_ = time.AfterFunc(me.ttlNs,
func() {
me.lock.Lock()
me.names = nil
me.lock.Unlock()
})
}
// Try to refresh: if another update is already running, do nothing,
// otherwise, read the directory and set it.
func (me *DirCache) maybeRefresh() {
me.lock.Lock()
defer me.lock.Unlock()
if me.updateRunning {
return
}
me.updateRunning = true
go func() {
me.setMap(newDirnameMap(me.dir))
}()
}
func (me *DirCache) RemoveEntry(name string) {
me.lock.Lock()
defer me.lock.Unlock()
if me.names == nil {
go me.maybeRefresh()
return
}
me.names[name] = false, false
}
func (me *DirCache) AddEntry(name string) {
me.lock.Lock()
defer me.lock.Unlock()
if me.names == nil {
go me.maybeRefresh()
return
}
me.names[name] = true
}
func NewDirCache(dir string, ttlNs int64) *DirCache {
dc := new(DirCache)
dc.dir = dir
dc.ttlNs = ttlNs
return dc
}
func (me *DirCache) HasEntry(name string) (mapPresent bool, found bool) {
me.lock.RLock()
defer me.lock.RUnlock()
if me.names == nil {
go me.maybeRefresh()
return false, false
}
return true, me.names[name]
}
package unionfs
import (
"log"
"sync"
"time"
)
var _ = log.Println
type cacheEntry struct {
data interface{}
// expiryNs is the absolute timestamp of the expiry.
expiryNs int64
}
// TimedIntCache caches the result of fetch() for some time.
//
// Oh, how I wish we had generics.
type TimedCache struct {
fetch func(name string) interface{}
// ttlNs is a duration of the cache.
ttlNs int64
cacheMapMutex sync.RWMutex
cacheMap map[string]*cacheEntry
}
const layerCacheTimeoutNs = 1e9
func NewTimedCache(fetcher func(name string) interface{}, ttlNs int64) *TimedCache {
l := new(TimedCache)
l.ttlNs = ttlNs
l.fetch = fetcher
l.cacheMap = make(map[string]*cacheEntry)
return l
}
func (me *TimedCache) Get(name string) interface{} {
me.cacheMapMutex.RLock()
info, ok := me.cacheMap[name]
me.cacheMapMutex.RUnlock()
now := time.Nanoseconds()
if ok && info.expiryNs > now {
return info.data
}
return me.getDataNoCache(name)
}
func (me *TimedCache) Set(name string, val interface{}) {
me.cacheMapMutex.Lock()
defer me.cacheMapMutex.Unlock()
me.cacheMap[name] = &cacheEntry{
data: val,
expiryNs: time.Nanoseconds() + me.ttlNs,
}
}
func (me *TimedCache) getDataNoCache(name string) interface{} {
data := me.fetch(name)
me.Set(name, data)
return data
}
// Drop all expired entries.
func (me *TimedCache) Purge() {
keys := make([]string, 0, len(me.cacheMap))
now := time.Nanoseconds()
me.cacheMapMutex.Lock()
defer me.cacheMapMutex.Unlock()
for k, v := range me.cacheMap {
if v.expiryNs < now {
keys = append(keys, k)
}
}
for _, k := range keys {
me.cacheMap[k] = nil, false
}
}
package unionfs
import (
"fmt"
"log"
"time"
"testing"
)
var _ = fmt.Print
var _ = log.Print
func TestTimedIntCache(t *testing.T) {
fetchCount := 0
fetch := func(n string) interface{} {
fetchCount++
i := int(n[0])
return &i
}
var ttl int64
ttl = 1e6
cache := NewTimedCache(fetch, ttl)
v := cache.Get("n").(*int)
if *v != int('n') {
t.Error("value mismatch", v)
}
if fetchCount != 1 {
t.Error("fetch count mismatch", fetchCount)
}
// The cache update is async.
time.Sleep(ttl / 10)
w := cache.Get("n")
if v != w {
t.Error("Huh, inconsistent.")
}
if fetchCount > 1 {
t.Error("fetch count fail.", fetchCount)
}
time.Sleep(ttl * 2)
cache.Purge()
w = cache.Get("n")
if fetchCount == 1 {
t.Error("did not fetch again. Purge unsuccessful?")
}
}
package unionfs
import (
"crypto/md5"
"fmt"
"github.com/hanwen/go-fuse/fuse"
"io"
"io/ioutil"
"log"
"os"
"syscall"
"path"
"path/filepath"
"sync"
)
// TODO(hanwen): is md5 sufficiently fast?
func filePathHash(path string) string {
h := md5.New()
h.Write([]byte(path))
// TODO(hanwen): should use a tighter format, so we can pack
// more results in a readdir() roundtrip.
return fmt.Sprintf("%x", h.Sum())
}
/*
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 piggybacks on the existing LoopbackFileSystem in Go-fuse, so we
don't have to translate back and forth between Go's and FUSE's API.
* 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 {
fuse.DefaultPathFilesystem
branches []*fuse.LoopbackFileSystem
// The same, but as interfaces.
fileSystems []fuse.PathFilesystem
// A file-existence cache.
deletionCache *DirCache
// A file -> branch cache.
branchCache *TimedCache
options *UnionFsOptions
}
type UnionFsOptions struct {
BranchCacheTTLSecs float64
DeletionCacheTTLSecs float64
DeletionDirName string
}
func NewUnionfs(roots []string, options UnionFsOptions) *UnionFs {
g := new(UnionFs)
g.options = &options
for _, r := range roots {
pt := fuse.NewLoopbackFileSystem(r)
g.branches = append(g.branches, pt)
// We could use some sort of caching file system here.
g.fileSystems = append(g.fileSystems, fuse.PathFilesystem(pt))
}
deletionDir := g.deletionDir()
err := os.MkdirAll(deletionDir, 0755)
if err != nil {
panic(fmt.Sprintf("could not create deletion path %v: %v",
deletionDir, err))
}
g.deletionCache = NewDirCache(deletionDir, int64(options.DeletionCacheTTLSecs*1e9))
g.branchCache = NewTimedCache(
func(n string) interface{} { return g.getBranchAttrNoCache(n) },
int64(options.BranchCacheTTLSecs*1e9))
return g
}
////////////////
// Deal with all the caches.
func (me *UnionFs) isDeleted(name string) bool {
haveCache, found := me.deletionCache.HasEntry(filePathHash(name))
if haveCache {
return found
}
fileName := me.deletionPath(name)
fi, _ := os.Lstat(fileName)
return fi != nil
}
func (me *UnionFs) getBranch(name string) int {
r := me.branchCache.Get(name)
result := r.(getBranchResult)
return result.branch
}
type getBranchResult struct {
attr *fuse.Attr
code fuse.Status
branch int
}
func (me *UnionFs) getBranchAttrNoCache(name string) getBranchResult {
parent, base := path.Split(name)
parentBranch := 0
if base != "" {
parentBranch = me.getBranch(parent)
}
for i, fs := range me.fileSystems {
if i < parentBranch {
continue
}
a, s := fs.GetAttr(name)
if s == fuse.OK {
return getBranchResult{
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 getBranchResult{nil, fuse.ENOENT, -1}
}
////////////////
// Deletion.
func (me *UnionFs) deletionDir() string {
dir := filepath.Join(me.branches[0].GetPath(""), me.options.DeletionDirName)
return dir
}
func (me *UnionFs) deletionPath(name string) string {
dir := me.deletionDir()
return filepath.Join(dir, filePathHash(name))
}
func (me *UnionFs) removeDeletion(name string) {
fileName := me.deletionPath(name)
me.deletionCache.RemoveEntry(path.Base(fileName))
// 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.
errno := syscall.Unlink(name)
if errno != 0 && errno != syscall.ENOENT {
log.Printf("error unlinking %s: %v", fileName, errno)
}
}
func (me *UnionFs) putDeletion(name string) fuse.Status {
fileName := me.deletionPath(name)
me.deletionCache.AddEntry(path.Base(fileName))
// Is there a WriteStringToFileOrDie ?
err := ioutil.WriteFile(fileName, []byte(name), 0644)
if err != nil {
log.Printf("could not create deletion file %v: %v",
fileName, err)
return fuse.EPERM
}
return fuse.OK
}
////////////////
// Promotion.
// From the golang blog.
func CopyFile(dstName, srcName string) (written int64, err os.Error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}
func (me *UnionFs) Promote(name string,
src *fuse.LoopbackFileSystem) os.Error {
writable := me.branches[0]
_, err := CopyFile(writable.GetPath(name), src.GetPath(name))
me.branchCache.Set(name, getBranchResult{nil, fuse.OK, 0})
return err
}
////////////////////////////////////////////////////////////////
// Below: implement interface for a PathFilesystem.
func (me *UnionFs) Mkdir(path string, mode uint32) (code fuse.Status) {
code = me.fileSystems[0].Mkdir(path, mode)
if code == fuse.OK {
go me.removeDeletion(path)
me.branchCache.Set(path, getBranchResult{nil, fuse.OK, 0})
}
return code
}
func (me *UnionFs) Symlink(pointedTo string, linkName string) (code fuse.Status) {
code = me.fileSystems[0].Symlink(pointedTo, linkName)
if code == fuse.OK {
go me.removeDeletion(linkName)
me.branchCache.Set(linkName, getBranchResult{nil, fuse.OK, 0})
}
return code
}
func (me *UnionFs) Access(name string, mode uint32) (code fuse.Status) {
i := me.getBranch(name)
if i >= 0 {
return me.fileSystems[i].Access(name, mode)
}
return fuse.ENOENT
}
func (me *UnionFs) Unlink(name string) (code fuse.Status) {
branch := me.getBranch(name)
if branch == 0 {
code = me.fileSystems[0].Unlink(name)
if code != fuse.OK {
return code
}
r := me.branchCache.getDataNoCache(name)
branch = r.(getBranchResult).branch
}
if branch > 0 {
// It would be nice to do the putDeletion async.
code = me.putDeletion(name)
}
return code
}
func (me *UnionFs) Readlink(name string) (out string, code fuse.Status) {
i := me.getBranch(name)
if i >= 0 {
return me.fileSystems[i].Readlink(name)
}
return "", fuse.ENOENT
}
func (me *UnionFs) Create(name string, flags uint32, mode uint32) (fuseFile fuse.RawFuseFile, code fuse.Status) {
// TODO(hanwen) - we should check that the name is not a
// directory in another branch.
writable := me.fileSystems[0]
fuseFile, code = writable.Create(name, flags, mode)
if code == fuse.OK {
me.removeDeletion(name)
me.branchCache.Set(name, getBranchResult{nil, fuse.OK, 0})
}
return fuseFile, code
}
func (me *UnionFs) GetAttr(name string) (a *fuse.Attr, s fuse.Status) {
if name == "READONLY" {
return nil, fuse.ENOENT
}
if name == me.options.DeletionDirName {
return nil, fuse.ENOENT
}
if me.isDeleted(name) {
return nil, fuse.ENOENT
}
r := me.branchCache.Get(name).(getBranchResult)
if r.branch < 0 {
return nil, fuse.ENOENT
}
if r.attr == nil {
return me.fileSystems[r.branch].GetAttr(name)
}
return r.attr, r.code
}
func (me *UnionFs) OpenDir(directory string) (stream chan fuse.DirEntry, status fuse.Status) {
dirBranch := me.getBranch(directory)
if dirBranch < 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 deletions map[string]bool
deletionsDone := make(chan bool, 1)
go func() {
deletions = newDirnameMap(me.deletionDir())
deletionsDone <- true
}()
entries := make([]map[string]uint32, len(me.branches))
for i, _ := range me.branches {
entries[i] = make(map[string]uint32)
}
statuses := make([]fuse.Status, len(me.branches))
var wg sync.WaitGroup
for i, l := range me.fileSystems {
if i >= dirBranch {
wg.Add(1)
go func(j int, pfs fuse.PathFilesystem) {
ch, s := pfs.OpenDir(directory)
statuses[j] = s
for s == fuse.OK {
v := <-ch
if v.Name == "" {
break
}
entries[j][v.Name] = v.Mode
}
wg.Done()
}(i, l)
}
}
wg.Wait()
_ = <-deletionsDone
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 == "" {
results[me.options.DeletionDirName] = 0, false
// HACK.
results["READONLY"] = 0, false
}
stream = make(chan fuse.DirEntry)
go func() {
for k, v := range results {
stream <- fuse.DirEntry{
Name: k,
Mode: v,
}
}
close(stream)
}()
return stream, fuse.OK
}
func (me *UnionFs) Open(name string, flags uint32) (fuseFile fuse.RawFuseFile, status fuse.Status) {
branch := me.getBranch(name)
if flags&fuse.O_ANYWRITE != 0 && branch > 0 {
err := me.Promote(name, me.branches[branch])
if err != nil {
panic("copy: " + err.String())
}
branch = 0
}
return me.fileSystems[branch].Open(name, uint32(flags))
}
func (me *UnionFs) Roots() (result []string) {
for _, loopback := range me.branches {
result = append(result, loopback.GetPath(""))
}
return result
}
package unionfs
import (
"os"
"github.com/hanwen/go-fuse/fuse"
"fmt"
"log"
"testing"
)
var _ = fmt.Print
var _ = log.Print
var CheckSuccess = fuse.CheckSuccess
func TestFilePathHash(t *testing.T) {
// Simple test coverage.
t.Log(filePathHash("xyz/abc"))
}
var testOpts = UnionFsOptions{
DeletionCacheTTLSecs: 0.01,
DeletionDirName: "DELETIONS",
BranchCacheTTLSecs: 0.01,
}
func setup(t *testing.T) (workdir string, state *fuse.MountState) {
wd := fuse.MakeTempDir()
err := os.Mkdir(wd+"/mount", 0700)
fuse.CheckSuccess(err)
err = os.Mkdir(wd+"/rw", 0700)
fuse.CheckSuccess(err)
os.Mkdir(wd+"/ro", 0700)
fuse.CheckSuccess(err)
var roots []string
roots = append(roots, wd+"/rw")
roots = append(roots, wd+"/ro")
ufs := NewUnionfs(roots, testOpts)
connector := fuse.NewPathFileSystemConnector(ufs)
state = fuse.NewMountState(connector)
state.Mount(wd + "/mount")
go state.Loop(false)
return wd, state
}
func writeToFile(path string, contents string, create bool) {
var flags int = os.O_WRONLY
if create {
flags |= os.O_CREATE
}
f, err := os.OpenFile(path, flags, 0644)
fuse.CheckSuccess(err)
_, err = f.Write([]byte(contents))
fuse.CheckSuccess(err)
err = f.Close()
fuse.CheckSuccess(err)
}
func readFromFile(path string) string {
f, err := os.Open(path)
fuse.CheckSuccess(err)
fi, err := os.Stat(path)
content := make([]byte, fi.Size)
n, err := f.Read(content)
fuse.CheckSuccess(err)
if n < int(fi.Size) {
panic("short read.")
}
err = f.Close()
fuse.CheckSuccess(err)
return string(content)
}
func dirNames(path string) map[string]bool {
f, err := os.Open(path)
fuse.CheckSuccess(err)
result := make(map[string]bool)
names, err := f.Readdirnames(-1)
fuse.CheckSuccess(err)
err = f.Close()
CheckSuccess(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)
log.Print(msg)
t.Error(msg)
}
}
func mapEq(m1, m2 map[string]bool) bool {
if len(m1) != len(m2) {
return false
}
for k, v := range m1 {
ok, val := 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 remove(path string) {
err := os.Remove(path)
fuse.CheckSuccess(err)
}
func TestSymlink(t *testing.T) {
wd, state := setup(t)
defer state.Unmount()
err := os.Symlink("/foobar", wd+"/mount/link")
CheckSuccess(err)
val, err := os.Readlink(wd + "/mount/link")
CheckSuccess(err)
if val != "/foobar" {
t.Errorf("symlink mismatch: %v", val)
}
}
func TestBasic(t *testing.T) {
wd, state := setup(t)
defer state.Unmount()
writeToFile(wd+"/rw/rw", "a", true)
writeToFile(wd+"/ro/ro1", "a", true)
writeToFile(wd+"/ro/ro2", "b", true)
names := dirNames(wd + "/mount")
expected := map[string]bool{
"rw": true, "ro1": true, "ro2": true,
}
checkMapEq(t, names, expected)
writeToFile(wd+"/mount/new", "new contents", true)
if !fileExists(wd + "/rw/new") {
t.Errorf("missing file in rw layer", names)
}
if readFromFile(wd+"/mount/new") != "new contents" {
t.Errorf("read mismatch.")
}
writeToFile(wd+"/mount/ro1", "promote me", false)
if !fileExists(wd + "/rw/ro1") {
t.Errorf("missing file in rw layer", names)
}
remove(wd + "/mount/new")
names = dirNames(wd + "/mount")
checkMapEq(t, names, map[string]bool{
"rw": true, "ro1": true, "ro2": true,
})
names = dirNames(wd + "/rw")
checkMapEq(t, names, map[string]bool{
testOpts.DeletionDirName: true,
"rw": true, "ro1": true,
})
names = dirNames(wd + "/rw/" + testOpts.DeletionDirName)
if len(names) != 0 {
t.Errorf("Expected 0 entry in %v", names)
}
remove(wd + "/mount/ro1")
names = dirNames(wd + "/mount")
checkMapEq(t, names, map[string]bool{
"rw": true, "ro2": true,
})
names = dirNames(wd + "/rw")
checkMapEq(t, names, map[string]bool{
"rw": true, testOpts.DeletionDirName: true,
})
names = dirNames(wd + "/rw/" + testOpts.DeletionDirName)
if len(names) != 1 {
t.Errorf("Expected 1 entry in %v", names)
}
}
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