mirror of https://github.com/spf13/afero.git
add union fs
This commit is contained in:
parent
300870a2d5
commit
e68b257a2b
31
README.md
31
README.md
|
@ -33,6 +33,7 @@ filesystem for full interoperability.
|
||||||
* Support for compositional file systems by joining various different file systems (see httpFs)
|
* Support for compositional file systems by joining various different file systems (see httpFs)
|
||||||
* Filtering of calls to intercept opening / modifying files, several filters
|
* Filtering of calls to intercept opening / modifying files, several filters
|
||||||
may be stacked.
|
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
|
* 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
|
provide a filtered view on file names, any file (not directory) NOT matching
|
||||||
the passed regexp will be treated as non-existing
|
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
|
# About the project
|
||||||
|
|
||||||
## What's in the name
|
## What's in the name
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue