add union fs

This commit is contained in:
Hanno Hecker 2016-01-02 07:52:40 +01:00 committed by Steve Francia
parent 300870a2d5
commit e68b257a2b
5 changed files with 974 additions and 0 deletions

View File

@ -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

280
union.go Normal file
View File

@ -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
}

307
union_cache.go Normal file
View File

@ -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
}

213
union_cow.go Normal file
View File

@ -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)
}

143
union_test.go Normal file
View File

@ -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)
}
}