package afero import ( "fmt" "os" "path/filepath" "syscall" "time" ) var _ Lstater = (*CopyOnWriteFs)(nil) // The CopyOnWriteFs 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()). // // Reading directories is currently only supported via Open(), not OpenFile(). type CopyOnWriteFs struct { base Fs layer Fs } func NewCopyOnWriteFs(base Fs, layer Fs) Fs { return &CopyOnWriteFs{base: base, layer: layer} } // Returns true if the file is not in the overlay func (u *CopyOnWriteFs) isBaseFile(name string) (bool, error) { if _, err := u.layer.Stat(name); err == nil { return false, nil } _, err := u.base.Stat(name) if err != nil { if oerr, ok := err.(*os.PathError); ok { if oerr.Err == os.ErrNotExist || oerr.Err == syscall.ENOENT || oerr.Err == syscall.ENOTDIR { return false, nil } } if err == syscall.ENOENT { return false, nil } } return true, err } func (u *CopyOnWriteFs) copyToLayer(name string) error { return copyToLayer(u.base, u.layer, name) } func (u *CopyOnWriteFs) 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 *CopyOnWriteFs) 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 *CopyOnWriteFs) Stat(name string) (os.FileInfo, error) { fi, err := u.layer.Stat(name) if err != nil { isNotExist := u.isNotExist(err) if isNotExist { return u.base.Stat(name) } return nil, err } return fi, nil } func (u *CopyOnWriteFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { llayer, ok1 := u.layer.(Lstater) lbase, ok2 := u.base.(Lstater) if ok1 { fi, b, err := llayer.LstatIfPossible(name) if err == nil { return fi, b, nil } if !u.isNotExist(err) { return nil, b, err } } if ok2 { fi, b, err := lbase.LstatIfPossible(name) if err == nil { return fi, b, nil } if !u.isNotExist(err) { return nil, b, err } } fi, err := u.Stat(name) return fi, false, err } func (u *CopyOnWriteFs) isNotExist(err error) bool { if e, ok := err.(*os.PathError); ok { err = e.Err } if err == os.ErrNotExist || err == syscall.ENOENT || err == syscall.ENOTDIR { return true } return false } // Renaming files present only in the base layer is not permitted func (u *CopyOnWriteFs) 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 *CopyOnWriteFs) 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 *CopyOnWriteFs) 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 *CopyOnWriteFs) 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) } dir := filepath.Dir(name) isaDir, err := IsDir(u.base, dir) if err != nil && !os.IsNotExist(err) { return nil, err } if isaDir { if err = u.layer.MkdirAll(dir, 0777); err != nil { return nil, err } return u.layer.OpenFile(name, flag, perm) } isaDir, err = IsDir(u.layer, dir) if err != nil { return nil, err } if isaDir { return u.layer.OpenFile(name, flag, perm) } return nil, &os.PathError{Op: "open", Path: name, Err: syscall.ENOTDIR} // ...or os.ErrNotExist? } if b { return u.base.OpenFile(name, flag, perm) } return u.layer.OpenFile(name, flag, perm) } // This function handles the 9 different possibilities caused // by the union which are the intersection of the following... // layer: doesn't exist, exists as a file, and exists as a directory // base: doesn't exist, exists as a file, and exists as a directory func (u *CopyOnWriteFs) Open(name string) (File, error) { // Since the overlay overrides the base we check that first b, err := u.isBaseFile(name) if err != nil { return nil, err } // If overlay doesn't exist, return the base (base state irrelevant) if b { return u.base.Open(name) } // If overlay is a file, return it (base state irrelevant) dir, err := IsDir(u.layer, name) if err != nil { return nil, err } if !dir { return u.layer.Open(name) } // Overlay is a directory, base state now matters. // Base state has 3 states to check but 2 outcomes: // A. It's a file or non-readable in the base (return just the overlay) // B. It's an accessible directory in the base (return a UnionFile) // If base is file or nonreadable, return overlay dir, err = IsDir(u.base, name) if !dir || err != nil { return u.layer.Open(name) } // Both base & layer are directories // Return union file (if opens are without error) bfile, bErr := u.base.Open(name) lfile, lErr := u.layer.Open(name) // If either have errors at this point something is very wrong. Return nil and the errors if bErr != nil || lErr != nil { return nil, fmt.Errorf("BaseErr: %v\nOverlayErr: %v", bErr, lErr) } return &UnionFile{Base: bfile, Layer: lfile}, nil } func (u *CopyOnWriteFs) 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 ErrFileExists } return u.layer.MkdirAll(name, perm) } func (u *CopyOnWriteFs) Name() string { return "CopyOnWriteFs" } func (u *CopyOnWriteFs) 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 ErrFileExists } return u.layer.MkdirAll(name, perm) } func (u *CopyOnWriteFs) Create(name string) (File, error) { return u.OpenFile(name, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0666) }