mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-01-10 04:07:18 +02:00
7b302d8c29
Afero is a package that lets you mock out a filesystem with an in-memory filesystem. It allows us to easily create the files required for a given test without worrying about a cleanup step or different tests tripping on eachother when run in parallel. Later on I'll standardise on using afero over the vanilla os package
328 lines
7.6 KiB
Go
328 lines
7.6 KiB
Go
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(), Chmod() and Chown()).
|
|
//
|
|
// 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) Chown(name string, uid, gid int) 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.Chown(name, uid, gid)
|
|
}
|
|
|
|
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) SymlinkIfPossible(oldname, newname string) error {
|
|
if slayer, ok := u.layer.(Linker); ok {
|
|
return slayer.SymlinkIfPossible(oldname, newname)
|
|
}
|
|
|
|
return &os.LinkError{Op: "symlink", Old: oldname, New: newname, Err: ErrNoSymlink}
|
|
}
|
|
|
|
func (u *CopyOnWriteFs) ReadlinkIfPossible(name string) (string, error) {
|
|
if rlayer, ok := u.layer.(LinkReader); ok {
|
|
return rlayer.ReadlinkIfPossible(name)
|
|
}
|
|
|
|
if rbase, ok := u.base.(LinkReader); ok {
|
|
return rbase.ReadlinkIfPossible(name)
|
|
}
|
|
|
|
return "", &os.PathError{Op: "readlink", Path: name, Err: ErrNoReadlink}
|
|
}
|
|
|
|
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, 0o777); 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 {
|
|
// This is in line with how os.MkdirAll behaves.
|
|
return nil
|
|
}
|
|
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, 0o666)
|
|
}
|