diff --git a/basepath.go b/basepath.go index 004aeaa..7fbf223 100644 --- a/basepath.go +++ b/basepath.go @@ -9,6 +9,8 @@ import ( "time" ) +var _ Lstater = (*BasePathFs)(nil) + // The BasePathFs restricts all operations to a given path within an Fs. // The given file name to the operations on this Fs will be prepended with // the base path before calling the base Fs. @@ -164,4 +166,16 @@ func (b *BasePathFs) Create(name string) (f File, err error) { return &BasePathFile{File: sourcef, path: b.path}, nil } +func (b *BasePathFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { + name, err := b.RealPath(name) + if err != nil { + return nil, false, &os.PathError{Op: "lstat", Path: name, Err: err} + } + if lstater, ok := b.source.(Lstater); ok { + return lstater.LstatIfPossible(name) + } + fi, err := b.source.Stat(name) + return fi, false, err +} + // vim: ts=4 sw=4 noexpandtab nolist syn=go diff --git a/copyOnWriteFs.go b/copyOnWriteFs.go index f2ebcd2..76ebf98 100644 --- a/copyOnWriteFs.go +++ b/copyOnWriteFs.go @@ -8,6 +8,8 @@ import ( "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 @@ -76,18 +78,55 @@ func (u *CopyOnWriteFs) Chmod(name string, mode os.FileMode) error { func (u *CopyOnWriteFs) Stat(name string) (os.FileInfo, error) { fi, err := u.layer.Stat(name) if err != nil { - origErr := err - if e, ok := err.(*os.PathError); ok { - err = e.Err - } - if err == os.ErrNotExist || err == syscall.ENOENT || err == syscall.ENOTDIR { + isNotExist := u.isNotExist(err) + if isNotExist { return u.base.Stat(name) } - return nil, origErr + 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) diff --git a/lstater.go b/lstater.go new file mode 100644 index 0000000..89c1bfc --- /dev/null +++ b/lstater.go @@ -0,0 +1,27 @@ +// Copyright © 2018 Steve Francia . +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package afero + +import ( + "os" +) + +// Lstater is an optional interface in Afero. It is only implemented by the +// filesystems saying so. +// It will call Lstat if the filesystem iself is, or it delegates to, the os filesystem. +// Else it will call Stat. +// In addtion to the FileInfo, it will return a boolean telling whether Lstat was called or not. +type Lstater interface { + LstatIfPossible(name string) (os.FileInfo, bool, error) +} diff --git a/lstater_test.go b/lstater_test.go new file mode 100644 index 0000000..477924f --- /dev/null +++ b/lstater_test.go @@ -0,0 +1,102 @@ +// Copyright ©2018 Steve Francia +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package afero + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLstatIfPossible(t *testing.T) { + wd, _ := os.Getwd() + defer func() { + os.Chdir(wd) + }() + + osFs := &OsFs{} + + workDir, err := TempDir(osFs, "", "afero-lstate") + if err != nil { + t.Fatal(err) + } + + defer func() { + osFs.RemoveAll(workDir) + }() + + memWorkDir := "/lstate" + + memFs := NewMemMapFs() + overlayFs1 := &CopyOnWriteFs{base: osFs, layer: memFs} + overlayFs2 := &CopyOnWriteFs{base: memFs, layer: osFs} + overlayFsMemOnly := &CopyOnWriteFs{base: memFs, layer: NewMemMapFs()} + basePathFs := &BasePathFs{source: osFs, path: workDir} + basePathFsMem := &BasePathFs{source: memFs, path: memWorkDir} + roFs := &ReadOnlyFs{source: osFs} + roFsMem := &ReadOnlyFs{source: memFs} + + pathFileMem := filepath.Join(memWorkDir, "aferom.txt") + + WriteFile(osFs, filepath.Join(workDir, "afero.txt"), []byte("Hi, Afero!"), 0777) + WriteFile(memFs, filepath.Join(pathFileMem), []byte("Hi, Afero!"), 0777) + + os.Chdir(workDir) + if err := os.Symlink("afero.txt", "symafero.txt"); err != nil { + t.Fatal(err) + } + + pathFile := filepath.Join(workDir, "afero.txt") + pathSymlink := filepath.Join(workDir, "symafero.txt") + + checkLstat := func(l Lstater, name string, shouldLstat bool) os.FileInfo { + statFile, isLstat, err := l.LstatIfPossible(name) + if err != nil { + t.Fatalf("Lstat check failed: %s", err) + } + if isLstat != shouldLstat { + t.Fatalf("Lstat status was %t for %s", isLstat, name) + } + return statFile + } + + testLstat := func(l Lstater, pathFile, pathSymlink string) { + shouldLstat := pathSymlink != "" + statRegular := checkLstat(l, pathFile, shouldLstat) + statSymlink := checkLstat(l, pathSymlink, shouldLstat) + if statRegular == nil || statSymlink == nil { + t.Fatal("got nil FileInfo") + } + + symSym := statSymlink.Mode()&os.ModeSymlink == os.ModeSymlink + if symSym == (pathSymlink == "") { + t.Fatal("expected the FileInfo to describe the symlink") + } + + _, _, err := l.LstatIfPossible("this-should-not-exist.txt") + if err == nil || !os.IsNotExist(err) { + t.Fatalf("expected file to not exist, got %s", err) + } + } + + testLstat(osFs, pathFile, pathSymlink) + testLstat(overlayFs1, pathFile, pathSymlink) + testLstat(overlayFs2, pathFile, pathSymlink) + testLstat(basePathFs, "afero.txt", "symafero.txt") + testLstat(overlayFsMemOnly, pathFileMem, "") + testLstat(basePathFsMem, "aferom.txt", "") + testLstat(roFs, pathFile, pathSymlink) + testLstat(roFsMem, pathFileMem, "") +} diff --git a/match.go b/match.go index 08b3b7e..c18a87f 100644 --- a/match.go +++ b/match.go @@ -33,8 +33,8 @@ import ( // built-ins from that package. func Glob(fs Fs, pattern string) (matches []string, err error) { if !hasMeta(pattern) { - // afero does not support Lstat directly. - if _, err = lstatIfOs(fs, pattern); err != nil { + // Lstat not supported by a ll filesystems. + if _, err = lstatIfPossible(fs, pattern); err != nil { return nil, nil } return []string{pattern}, nil diff --git a/os.go b/os.go index 6b8bce1..13cc1b8 100644 --- a/os.go +++ b/os.go @@ -19,6 +19,8 @@ import ( "time" ) +var _ Lstater = (*OsFs)(nil) + // OsFs is a Fs implementation that uses functions provided by the os package. // // For details in any method, check the documentation of the os package @@ -92,3 +94,8 @@ func (OsFs) Chmod(name string, mode os.FileMode) error { func (OsFs) Chtimes(name string, atime time.Time, mtime time.Time) error { return os.Chtimes(name, atime, mtime) } + +func (OsFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { + fi, err := os.Lstat(name) + return fi, true, err +} diff --git a/path.go b/path.go index 1d90e46..18f60a0 100644 --- a/path.go +++ b/path.go @@ -60,7 +60,7 @@ func walk(fs Fs, path string, info os.FileInfo, walkFn filepath.WalkFunc) error for _, name := range names { filename := filepath.Join(path, name) - fileInfo, err := lstatIfOs(fs, filename) + fileInfo, err := lstatIfPossible(fs, filename) if err != nil { if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir { return err @@ -77,15 +77,13 @@ func walk(fs Fs, path string, info os.FileInfo, walkFn filepath.WalkFunc) error return nil } -// if the filesystem is OsFs use Lstat, else use fs.Stat -func lstatIfOs(fs Fs, path string) (info os.FileInfo, err error) { - _, ok := fs.(*OsFs) - if ok { - info, err = os.Lstat(path) - } else { - info, err = fs.Stat(path) +// if the filesystem supports it, use Lstat, else use fs.Stat +func lstatIfPossible(fs Fs, path string) (os.FileInfo, error) { + if lfs, ok := fs.(Lstater); ok { + fi, _, err := lfs.LstatIfPossible(path) + return fi, err } - return + return fs.Stat(path) } // Walk walks the file tree rooted at root, calling walkFn for each file or @@ -100,7 +98,7 @@ func (a Afero) Walk(root string, walkFn filepath.WalkFunc) error { } func Walk(fs Fs, root string, walkFn filepath.WalkFunc) error { - info, err := lstatIfOs(fs, root) + info, err := lstatIfPossible(fs, root) if err != nil { return walkFn(root, nil, err) } diff --git a/readonlyfs.go b/readonlyfs.go index f1fa55b..c6376ec 100644 --- a/readonlyfs.go +++ b/readonlyfs.go @@ -6,6 +6,8 @@ import ( "time" ) +var _ Lstater = (*ReadOnlyFs)(nil) + type ReadOnlyFs struct { source Fs } @@ -34,6 +36,14 @@ func (r *ReadOnlyFs) Stat(name string) (os.FileInfo, error) { return r.source.Stat(name) } +func (r *ReadOnlyFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { + if lsf, ok := r.source.(Lstater); ok { + return lsf.LstatIfPossible(name) + } + fi, err := r.Stat(name) + return fi, false, err +} + func (r *ReadOnlyFs) Rename(o, n string) error { return syscall.EPERM } diff --git a/unionFile.go b/unionFile.go index 99f9e5d..27b067e 100644 --- a/unionFile.go +++ b/unionFile.go @@ -123,6 +123,24 @@ func (f *UnionFile) Name() string { return f.base.Name() } +// UnionKey can be implemented to use an alternate key than FileInfo.Name when +// weaving the two directories together. The use case(s) are uncommon and special, +// but this will allow a union file system with multiple files with the same filename. +// This needs to be implemented by your own implementation of os.FileInfo as +// returned in Readdir. +type UnionKey interface { + AferoUnionKey() string +} + +func (f *UnionFile) key(ofi os.FileInfo) string { + switch tp := ofi.(type) { + case UnionKey: + return tp.AferoUnionKey() + default: + return tp.Name() + } +} + // Readdir will weave the two directories together and // return a single view of the overlayed directories func (f *UnionFile) Readdir(c int) (ofi []os.FileInfo, err error) { @@ -135,7 +153,7 @@ func (f *UnionFile) Readdir(c int) (ofi []os.FileInfo, err error) { return nil, err } for _, fi := range rfi { - files[fi.Name()] = fi + files[f.key(fi)] = fi } } @@ -146,7 +164,7 @@ func (f *UnionFile) Readdir(c int) (ofi []os.FileInfo, err error) { } for _, fi := range rfi { if _, exists := files[fi.Name()]; !exists { - files[fi.Name()] = fi + files[f.key(fi)] = fi } } }