diff --git a/README.md b/README.md index 67df853..c5f33fc 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ filesystem for full interoperability. * Support for compositional file systems by joining various different file systems (see httpFs) * Filtering of calls to intercept opening / modifying files, several filters may be stacked. +* Unions of filesystems to overlay two filesystems. These may be stacked. * A set of utility functions ported from io, ioutil & hugo to be afero aware @@ -313,6 +314,36 @@ provide a read only view of the source Fs provide a filtered view on file names, any file (not directory) NOT matching the passed regexp will be treated as non-existing +## Unions + +Afero has the possibilty to overlay two filesystems as a union, these are +special types of filters. To create a new union Fs use the `NewUnionFs()`. The +example below creates an memory cache for the OsFs: +```go + ufs := NewUnionFs(&OsFs{}, &MemMapFs{}, NewCacheUnionFs(1 * time.Minute)) +``` + +Available UnionFs are: + +### NewCacheUnionFs(time.Duration) + +Cache files in the layer for the given time.Duration, a cache duration of 0 +means "forever". + +If the base filesystem is writeable, any changes to files will be done first +to the base, then to the overlay layer. Write calls to open file handles +like `Write()` or `Truncate()` to the overlay first. + +A read-only base will make the overlay also read-only but still copy files +from the base to the overlay when they're not present (or outdated) in the +caching layer. + +### NewCoWUnionFs() + +A CopyOnWrite union: any attempt to modify a file in the base will copy +the file to the overlay layer before modification. This overlay layer is +currently limited to MemMapFs. + # About the project ## What's in the name diff --git a/union.go b/union.go new file mode 100644 index 0000000..bd6cff4 --- /dev/null +++ b/union.go @@ -0,0 +1,280 @@ +package afero + +import ( + "io" + "os" + "path/filepath" + "syscall" +) + +type UnionFs func(Fs) FilterFs + +// Create a new UnionFs: +// +// ufs := NewUnionFs(baseFs, layerFs, NewCoWUnionFs()) +// cfs := NewUnionFs(baseFs, layerFs, NewCacheUnionFs(cacheTime)) +func NewUnionFs(base Fs, overlay Fs, impl UnionFs) Fs { + ufs := impl(overlay) + ufs.SetSource(base) + return ufs +} + +func copyToLayer(base Fs, layer Fs, name string) error { + bfh, err := base.Open(name) + if err != nil { + return err + } + defer bfh.Close() + + exists, err := Exists(layer, filepath.Dir(name)) + if err != nil { + return err + } + if !exists { + err = layer.MkdirAll(filepath.Dir(name), 0777) // FIXME? + if err != nil { + return err + } + } + + lfh, err := layer.Create(name) + if err != nil { + return err + } + n, err := io.Copy(lfh, bfh) + if err != nil { + layer.Remove(name) + lfh.Close() + return err + } + + bfi, err := bfh.Stat() + if err != nil || bfi.Size() != n { + layer.Remove(name) + lfh.Close() + return syscall.EIO + } + + err = lfh.Close() + if err != nil { + layer.Remove(name) + lfh.Close() + return err + } + return layer.Chtimes(name, bfi.ModTime(), bfi.ModTime()) +} + +// The UnionFile implements the afero.File interface and will be returned +// when reading a directory present at least in the overlay or opening a file +// for writing. +// +// The calls to +// Readdir() and Readdirnames() merge the file os.FileInfo / names from the +// base and the overlay - for files present in both layers, only those +// from the overlay will be used. +// +// When opening files for writing (Create() / OpenFile() with the right flags) +// the operations will be done in both layers, starting with the overlay. A +// successful read in the overlay will move the cursor position in the base layer +// by the number of bytes read. +type UnionFile struct { + layer File + base File + off int + files []os.FileInfo +} + +func (f *UnionFile) Close() error { + // first close base, so we have a newer timestamp in the overlay. If we'd close + // the overlay first, we'd get a cacheStale the next time we access this file + // -> cache would be useless ;-) + if f.base != nil { + f.base.Close() + } + if f.layer != nil { + return f.layer.Close() + } + return syscall.EBADFD +} + +func (f *UnionFile) Read(s []byte) (int, error) { + if f.layer != nil { + n, err := f.layer.Read(s) + if (err == nil || err == io.EOF) && f.base != nil { + // advance the file position also in the base file, the next + // call may be a write at this position (or a seek with SEEK_CUR) + if _, seekErr := f.base.Seek(int64(n), os.SEEK_CUR); seekErr != nil { + // only overwrite err in case the seek fails: we need to + // report an eventual io.EOF to the caller + err = seekErr + } + } + return n, err + } + if f.base != nil { + return f.base.Read(s) + } + return 0, syscall.EBADFD +} + +func (f *UnionFile) ReadAt(s []byte, o int64) (int, error) { + if f.layer != nil { + n, err := f.layer.ReadAt(s, o) + if (err == nil || err == io.EOF) && f.base != nil { + _, err = f.base.Seek(o+int64(n), os.SEEK_SET) + } + return n, err + } + if f.base != nil { + return f.base.ReadAt(s, o) + } + return 0, syscall.EBADFD +} + +func (f *UnionFile) Seek(o int64, w int) (pos int64, err error) { + if f.layer != nil { + pos, err = f.layer.Seek(o, w) + if (err == nil || err == io.EOF) && f.base != nil { + _, err = f.base.Seek(o, w) + } + return pos, err + } + if f.base != nil { + return f.base.Seek(o, w) + } + return 0, syscall.EBADFD +} + +func (f *UnionFile) Write(s []byte) (n int, err error) { + if f.layer != nil { + n, err = f.layer.Write(s) + if err == nil && f.base != nil { // hmm, do we have fixed size files where a write may hit the EOF mark? + _, err = f.base.Write(s) + } + return n, err + } + if f.base != nil { + return f.base.Write(s) + } + return 0, syscall.EBADFD +} + +func (f *UnionFile) WriteAt(s []byte, o int64) (n int, err error) { + if f.layer != nil { + n, err = f.layer.WriteAt(s, o) + if err == nil && f.base != nil { + _, err = f.base.WriteAt(s, o) + } + return n, err + } + if f.base != nil { + return f.base.WriteAt(s, o) + } + return 0, syscall.EBADFD +} + +func (f *UnionFile) Name() string { + if f.layer != nil { + return f.layer.Name() + } + return f.base.Name() +} + +func (f *UnionFile) Readdir(c int) (ofi []os.FileInfo, err error) { + if f.off == 0 { + var files = make(map[string]os.FileInfo) + var rfi []os.FileInfo + if f.layer != nil { + rfi, err = f.layer.Readdir(-1) + if err != nil { + return nil, err + } + for _, fi := range rfi { + files[fi.Name()] = fi + } + } + if f.base != nil { + rfi, err = f.base.Readdir(-1) + if err != nil { + return nil, err + } + for _, fi := range rfi { + if _, exists := files[fi.Name()]; !exists { + files[fi.Name()] = fi + } + } + } + for _, fi := range files { + f.files = append(f.files, fi) + } + } + if c == -1 { + return f.files[f.off:], nil + } + defer func() { f.off += c }() + return f.files[f.off:c], nil +} + +func (f *UnionFile) Readdirnames(c int) ([]string, error) { + rfi, err := f.Readdir(c) + if err != nil { + return nil, err + } + var names []string + for _, fi := range rfi { + names = append(names, fi.Name()) + } + return names, nil +} + +func (f *UnionFile) Stat() (os.FileInfo, error) { + if f.layer != nil { + return f.layer.Stat() + } + if f.base != nil { + return f.base.Stat() + } + return nil, syscall.EBADFD +} + +func (f *UnionFile) Sync() (err error) { + if f.layer != nil { + err = f.layer.Sync() + if err == nil && f.base != nil { + err = f.base.Sync() + } + return err + } + if f.base != nil { + return f.base.Sync() + } + return syscall.EBADFD +} + +func (f *UnionFile) Truncate(s int64) (err error) { + if f.layer != nil { + err = f.layer.Truncate(s) + if err == nil && f.base != nil { + err = f.base.Truncate(s) + } + return err + } + if f.base != nil { + return f.base.Truncate(s) + } + return syscall.EBADFD +} + +func (f *UnionFile) WriteString(s string) (n int, err error) { + if f.layer != nil { + n, err = f.layer.WriteString(s) + if err == nil && f.base != nil { + _, err = f.base.WriteString(s) + } + return n, err + } + if f.base != nil { + return f.base.WriteString(s) + } + return 0, syscall.EBADFD +} diff --git a/union_cache.go b/union_cache.go new file mode 100644 index 0000000..bae895b --- /dev/null +++ b/union_cache.go @@ -0,0 +1,307 @@ +package afero + +import ( + "os" + "syscall" + "time" +) + +// If the cache duration is 0, cache time will be unlimited, i.e. once +// a file is in the layer, the base will never be read again for this file. +// +// For cache times greater than 0, the modification time of a file is +// checked. Note that a lot of file system implementations only allow a +// resolution of a second for timestamps... or as the godoc for os.Chtimes() +// states: "The underlying filesystem may truncate or round the values to a +// less precise time unit." +// +// This caching union will forward all write calls also to the base file +// system first. To prevent writing to the base Fs, wrap it in a read-only +// filter - Note: this will also make the overlay read-only, for writing files +// in the overlay, use the overlay Fs directly, not via the union Fs. +type CacheUnionFs struct { + base Fs + layer Fs + cacheTime time.Duration +} + +func NewCacheUnionFs(t time.Duration) UnionFs { + return func(layer Fs) FilterFs { + return &CacheUnionFs{cacheTime: t, layer: layer} + } +} + +func (u *CacheUnionFs) AddFilter(fs FilterFs) { + fs.SetSource(u.base) + u.base = fs +} + +func (u *CacheUnionFs) SetSource(fs Fs) { + u.base = fs +} + +type cacheState int + +const ( + cacheUnknown cacheState = iota + // not present in the overlay, unknown if it exists in the base: + cacheMiss + // present in the overlay and in base, base file is newer: + cacheStale + // present in the overlay - with cache time == 0 it may exist in the base, + // with cacheTime > 0 it exists in the base and is same age or newer in the + // overlay + cacheHit + // happens if someone writes directly to the overlay without + // going through this union + cacheLocal +) + +func (u *CacheUnionFs) cacheStatus(name string) (state cacheState, fi os.FileInfo, err error) { + var lfi, bfi os.FileInfo + lfi, err = u.layer.Stat(name) + if err == nil { + if u.cacheTime == 0 { + return cacheHit, lfi, nil + } + if lfi.ModTime().Add(u.cacheTime).Before(time.Now()) { + bfi, err = u.base.Stat(name) + if err != nil { + return cacheLocal, lfi, nil + } + if bfi.ModTime().After(lfi.ModTime()) { + return cacheStale, bfi, nil + } + } + return cacheHit, lfi, nil + } + + if err == syscall.ENOENT { + return cacheMiss, nil, nil + } + var ok bool + if err, ok = err.(*os.PathError); ok { + if err == os.ErrNotExist { + return cacheMiss, nil, nil + } + } + return cacheMiss, nil, err +} + +func (u *CacheUnionFs) copyToLayer(name string) error { + return copyToLayer(u.base, u.layer, name) +} + +func (u *CacheUnionFs) Chtimes(name string, atime, mtime time.Time) error { + st, _, err := u.cacheStatus(name) + if err != nil { + return err + } + switch st { + case cacheLocal: + case cacheHit: + err = u.base.Chtimes(name, atime, mtime) + case cacheStale, cacheMiss: + if err := u.copyToLayer(name); err != nil { + return err + } + err = u.base.Chtimes(name, atime, mtime) + } + if err != nil { + return err + } + return u.layer.Chtimes(name, atime, mtime) +} + +func (u *CacheUnionFs) Chmod(name string, mode os.FileMode) error { + st, _, err := u.cacheStatus(name) + if err != nil { + return err + } + switch st { + case cacheLocal: + case cacheHit: + err = u.base.Chmod(name, mode) + case cacheStale, cacheMiss: + if err := u.copyToLayer(name); err != nil { + return err + } + err = u.base.Chmod(name, mode) + } + if err != nil { + return err + } + return u.layer.Chmod(name, mode) +} + +func (u *CacheUnionFs) Stat(name string) (os.FileInfo, error) { + st, fi, err := u.cacheStatus(name) + if err != nil { + return nil, err + } + switch st { + case cacheMiss: + return u.base.Stat(name) + default: // cacheStale has base, cacheHit and cacheLocal the layer os.FileInfo + return fi, nil + } +} + +func (u *CacheUnionFs) Rename(oldname, newname string) error { + st, _, err := u.cacheStatus(oldname) + if err != nil { + return err + } + switch st { + case cacheLocal: + case cacheHit: + err = u.base.Rename(oldname, newname) + case cacheStale, cacheMiss: + if err := u.copyToLayer(oldname); err != nil { + return err + } + err = u.base.Rename(oldname, newname) + } + if err != nil { + return err + } + return u.layer.Rename(oldname, newname) +} + +func (u *CacheUnionFs) Remove(name string) error { + st, _, err := u.cacheStatus(name) + if err != nil { + return err + } + switch st { + case cacheLocal: + case cacheHit, cacheStale, cacheMiss: + err = u.base.Remove(name) + } + if err != nil { + return err + } + return u.layer.Remove(name) +} + +func (u *CacheUnionFs) RemoveAll(name string) error { + st, _, err := u.cacheStatus(name) + if err != nil { + return err + } + switch st { + case cacheLocal: + case cacheHit, cacheStale, cacheMiss: + err = u.base.RemoveAll(name) + } + if err != nil { + return err + } + return u.layer.RemoveAll(name) +} + +func (u *CacheUnionFs) OpenFile(name string, flag int, perm os.FileMode) (File, error) { + st, _, err := u.cacheStatus(name) + if err != nil { + return nil, err + } + switch st { + case cacheLocal, cacheHit: + default: + if err := u.copyToLayer(name); err != nil { + return nil, err + } + } + if flag&(os.O_WRONLY|syscall.O_RDWR|os.O_APPEND|os.O_CREATE|os.O_TRUNC) != 0 { + bfi, err := u.base.OpenFile(name, flag, perm) + if err != nil { + return nil, err + } + lfi, err := u.layer.OpenFile(name, flag, perm) + if err != nil { + bfi.Close() // oops, what if O_TRUNC was set and file opening in the layer failed...? + return nil, err + } + return &UnionFile{base: bfi, layer: lfi}, nil + } + return u.layer.OpenFile(name, flag, perm) +} + +func (u *CacheUnionFs) Open(name string) (File, error) { + st, fi, err := u.cacheStatus(name) + if err != nil { + return nil, err + } + + switch st { + case cacheLocal: + return u.layer.Open(name) + + case cacheMiss: + bfi, err := u.base.Stat(name) + if err != nil { + return nil, err + } + if bfi.IsDir() { + return u.base.Open(name) + } + if err := u.copyToLayer(name); err != nil { + return nil, err + } + return u.layer.Open(name) + + case cacheStale: + if !fi.IsDir() { + if err := u.copyToLayer(name); err != nil { + return nil, err + } + return u.layer.Open(name) + } + case cacheHit: + if !fi.IsDir() { + return u.layer.Open(name) + } + } + // the dirs from cacheHit, cacheStale fall down here: + bfile, _ := u.base.Open(name) + lfile, err := u.layer.Open(name) + if err != nil && bfile == nil { + return nil, err + } + return &UnionFile{base: bfile, layer: lfile}, nil +} + +func (u *CacheUnionFs) Mkdir(name string, perm os.FileMode) error { + err := u.base.Mkdir(name, perm) + if err != nil { + return err + } + return u.layer.MkdirAll(name, perm) // yes, MkdirAll... we cannot assume it exists in the cache +} + +func (u *CacheUnionFs) Name() string { + return "CacheUnionFs" +} + +func (u *CacheUnionFs) MkdirAll(name string, perm os.FileMode) error { + err := u.base.MkdirAll(name, perm) + if err != nil { + return err + } + return u.layer.MkdirAll(name, perm) +} + +func (u *CacheUnionFs) Create(name string) (File, error) { + bfh, err := u.base.Create(name) + if err != nil { + return nil, err + } + lfh, err := u.layer.Create(name) + if err != nil { + // oops, see comment about OS_TRUNC above, should we remove? then we have to + // remember if the file did not exist before + bfh.Close() + return nil, err + } + return &UnionFile{base: bfh, layer: lfh}, nil +} diff --git a/union_cow.go b/union_cow.go new file mode 100644 index 0000000..0c75e2b --- /dev/null +++ b/union_cow.go @@ -0,0 +1,213 @@ +package afero + +import ( + "os" + "syscall" + "time" +) + +// The CoWUnionFs is a union filesystem: a read only base file system with +// a possibly writeable layer on top. Changes to the file system will only +// be made in the overlay: Changing an existing file in the base layer which +// is not present in the overlay will copy the file to the overlay ("changing" +// includes also calls to e.g. Chtimes() and Chmod()). +// The overlay is currently limited to MemMapFs: +// - missing MkdirAll() calls in the code below, MemMapFs creates them +// implicitly (or better: records the full path and afero.Readdir() +// can handle this). +// +// Reading directories is currently only supported via Open(), not OpenFile(). +type CoWUnionFs struct { + base Fs + layer Fs +} + +func NewCoWUnionFs() UnionFs { + // returns a function to have it the same as other implemtations + return func(layer Fs) FilterFs { + return &CoWUnionFs{layer: layer} + } +} + +func (u *CoWUnionFs) AddFilter(fs FilterFs) { + fs.SetSource(u.base) + u.base = fs +} + +func (u *CoWUnionFs) SetSource(fs Fs) { + u.base = fs +} + +func (u *CoWUnionFs) isBaseFile(name string) (bool, error) { + if _, err := u.layer.Stat(name); err == nil { + return false, nil + } + _, err := u.base.Stat(name) + return true, err +} + +func (u *CoWUnionFs) copyToLayer(name string) error { + return copyToLayer(u.base, u.layer, name) +} + +func (u *CoWUnionFs) Chtimes(name string, atime, mtime time.Time) error { + b, err := u.isBaseFile(name) + if err != nil { + return err + } + if b { + if err := u.copyToLayer(name); err != nil { + return err + } + } + return u.layer.Chtimes(name, atime, mtime) +} + +func (u *CoWUnionFs) Chmod(name string, mode os.FileMode) error { + b, err := u.isBaseFile(name) + if err != nil { + return err + } + if b { + if err := u.copyToLayer(name); err != nil { + return err + } + } + return u.layer.Chmod(name, mode) +} + +func (u *CoWUnionFs) Stat(name string) (os.FileInfo, error) { + fi, err := u.layer.Stat(name) + switch err { + case nil: + return fi, nil + case syscall.ENOENT: + return u.base.Stat(name) + default: + return nil, err + } +} + +// Renaming files present only in the base layer is not permitted +func (u *CoWUnionFs) Rename(oldname, newname string) error { + b, err := u.isBaseFile(oldname) + if err != nil { + return err + } + if b { + return syscall.EPERM + } + return u.layer.Rename(oldname, newname) +} + +// Removing files present only in the base layer is not permitted. If +// a file is present in the base layer and the overlay, only the overlay +// will be removed. +func (u *CoWUnionFs) Remove(name string) error { + err := u.layer.Remove(name) + switch err { + case syscall.ENOENT: + _, err = u.base.Stat(name) + if err == nil { + return syscall.EPERM + } + return syscall.ENOENT + default: + return err + } +} + +func (u *CoWUnionFs) RemoveAll(name string) error { + err := u.layer.RemoveAll(name) + switch err { + case syscall.ENOENT: + _, err = u.base.Stat(name) + if err == nil { + return syscall.EPERM + } + return syscall.ENOENT + default: + return err + } +} + +func (u *CoWUnionFs) OpenFile(name string, flag int, perm os.FileMode) (File, error) { + b, err := u.isBaseFile(name) + if err != nil { + return nil, err + } + + if flag&(os.O_WRONLY|os.O_RDWR|os.O_APPEND|os.O_CREATE|os.O_TRUNC) != 0 { + if b { + if err = u.copyToLayer(name); err != nil { + return nil, err + } + } + return u.layer.OpenFile(name, flag, perm) + } + if b { + return u.base.OpenFile(name, flag, perm) + } + return u.layer.OpenFile(name, flag, perm) +} + +func (u *CoWUnionFs) Open(name string) (File, error) { + b, err := u.isBaseFile(name) + if err != nil { + return nil, err + } + if b { + return u.base.Open(name) + } + + dir, err := IsDir(u.layer, name) + if err != nil { + return nil, err + } + if !dir { + return u.layer.Open(name) + } + + bfile, _ := u.base.Open(name) + lfile, err := u.layer.Open(name) + if err != nil && bfile == nil { + return nil, err + } + return &UnionFile{base: bfile, layer: lfile}, nil +} + +func (u *CoWUnionFs) Mkdir(name string, perm os.FileMode) error { + dir, err := IsDir(u.base, name) + if err != nil { + return u.layer.MkdirAll(name, perm) + } + if dir { + return syscall.EEXIST + } + return u.layer.MkdirAll(name, perm) +} + +func (u *CoWUnionFs) Name() string { + return "CoWUnionFs" +} + +func (u *CoWUnionFs) MkdirAll(name string, perm os.FileMode) error { + dir, err := IsDir(u.base, name) + if err != nil { + return u.layer.MkdirAll(name, perm) + } + if dir { + return syscall.EEXIST + } + return u.layer.MkdirAll(name, perm) +} + +func (u *CoWUnionFs) Create(name string) (File, error) { + b, err := u.isBaseFile(name) + if err == nil && b { + if err = u.copyToLayer(name); err != nil { + return nil, err + } + } + return u.layer.Create(name) +} diff --git a/union_test.go b/union_test.go new file mode 100644 index 0000000..14736ff --- /dev/null +++ b/union_test.go @@ -0,0 +1,143 @@ +package afero + +import ( + "io/ioutil" + "os" + "testing" + "time" +) + +func TestUnionCreateExisting(t *testing.T) { + base := &MemMapFs{} + roBase := NewFilter(base) + roBase.AddFilter(NewReadonlyFilter()) + + ufs := NewUnionFs(roBase, &MemMapFs{}, NewCoWUnionFs()) + + base.MkdirAll("/home/test", 0777) + fh, _ := base.Create("/home/test/file.txt") + fh.WriteString("This is a test") + fh.Close() + + fh, err := ufs.OpenFile("/home/test/file.txt", os.O_RDWR, 0666) + if err != nil { + t.Errorf("Failed to open file r/w: %s", err) + } + + _, err = fh.Write([]byte("####")) + if err != nil { + t.Errorf("Failed to write file: %s", err) + } + fh.Seek(0, 0) + data, err := ioutil.ReadAll(fh) + if err != nil { + t.Errorf("Failed to read file: %s", err) + } + if string(data) != "#### is a test" { + t.Errorf("Got wrong data") + } + fh.Close() + + fh, _ = base.Open("/home/test/file.txt") + data, err = ioutil.ReadAll(fh) + if string(data) != "This is a test" { + t.Errorf("Got wrong data in base file") + } + fh.Close() + + fh, err = ufs.Create("/home/test/file.txt") + switch err { + case nil: + if fi, _ := fh.Stat(); fi.Size() != 0 { + t.Errorf("Create did not truncate file") + } + fh.Close() + default: + t.Errorf("Create failed on existing file") + } + +} + +func TestUnionMergeReaddir(t *testing.T) { + base := &MemMapFs{} + roBase := NewFilter(base) + roBase.AddFilter(NewReadonlyFilter()) + + ufs := NewUnionFs(roBase, &MemMapFs{}, NewCoWUnionFs()) + + base.MkdirAll("/home/test", 0777) + fh, _ := base.Create("/home/test/file.txt") + fh.WriteString("This is a test") + fh.Close() + + fh, _ = ufs.Create("/home/test/file2.txt") + fh.WriteString("This is a test") + fh.Close() + + fh, _ = ufs.Open("/home/test") + files, err := fh.Readdirnames(-1) + if err != nil { + t.Errorf("Readdirnames failed") + } + if len(files) != 2 { + t.Errorf("Got wrong number of files: %v", files) + } +} + +func TestUnionCacheWrite(t *testing.T) { + base := &MemMapFs{} + layer := &MemMapFs{} + ufs := NewUnionFs(base, layer, NewCacheUnionFs(0)) + + base.Mkdir("/data", 0777) + + fh, err := ufs.Create("/data/file.txt") + if err != nil { + t.Errorf("Failed to create file") + } + _, err = fh.Write([]byte("This is a test")) + if err != nil { + t.Errorf("Failed to write file") + } + + fh.Seek(0, os.SEEK_SET) + buf := make([]byte, 4) + _, err = fh.Read(buf) + fh.Write([]byte(" IS A")) + fh.Close() + + baseData, _ := ReadFile(base, "/data/file.txt") + layerData, _ := ReadFile(layer, "/data/file.txt") + if string(baseData) != string(layerData) { + t.Errorf("Different data: %s <=> %s", baseData, layerData) + } +} + +func TestUnionCacheExpire(t *testing.T) { + base := &MemMapFs{} + layer := &MemMapFs{} + ufs := NewUnionFs(base, layer, NewCacheUnionFs(1*time.Second)) + + base.Mkdir("/data", 0777) + + fh, err := ufs.Create("/data/file.txt") + if err != nil { + t.Errorf("Failed to create file") + } + _, err = fh.Write([]byte("This is a test")) + if err != nil { + t.Errorf("Failed to write file") + } + fh.Close() + + fh, _ = base.Create("/data/file.txt") + // sleep some time, so we really get a different time.Now() on write... + time.Sleep(2 * time.Second) + fh.WriteString("Another test") + fh.Close() + + data, _ := ReadFile(ufs, "/data/file.txt") + if string(data) != "Another test" { + t.Errorf("cache time failed: <%s>", data) + } +}