add union fs
diff --git a/README.md b/README.md
index 67df853..c5f33fc 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,7 @@
* 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 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)
+ }
+}