| 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 CacheOnReadFs struct { |
| base Fs |
| layer Fs |
| cacheTime time.Duration |
| } |
| |
| func NewCacheOnReadFs(base Fs, layer Fs, cacheTime time.Duration) Fs { |
| return &CacheOnReadFs{base: base, layer: layer, cacheTime: cacheTime} |
| } |
| |
| type cacheState int |
| |
| const ( |
| // not present in the overlay, unknown if it exists in the base: |
| cacheMiss cacheState = iota |
| // 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 *CacheOnReadFs) 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 *CacheOnReadFs) copyToLayer(name string) error { |
| return copyToLayer(u.base, u.layer, name) |
| } |
| |
| func (u *CacheOnReadFs) 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 *CacheOnReadFs) 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 *CacheOnReadFs) 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 *CacheOnReadFs) 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 *CacheOnReadFs) 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 *CacheOnReadFs) 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 *CacheOnReadFs) 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 *CacheOnReadFs) 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 *CacheOnReadFs) 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 *CacheOnReadFs) Name() string { |
| return "CacheOnReadFs" |
| } |
| |
| func (u *CacheOnReadFs) 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 *CacheOnReadFs) 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 |
| } |