From 529d861398fa6d0cf9de4c6a3021ebbd5c212464 Mon Sep 17 00:00:00 2001 From: Scott Owens Date: Tue, 10 Dec 2019 16:15:25 +1100 Subject: [PATCH 1/4] Add an optional interface for Sylink and Readlink --- symlink.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 symlink.go diff --git a/symlink.go b/symlink.go new file mode 100644 index 0000000..a730413 --- /dev/null +++ b/symlink.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 +// Linker is an optional interface in Afero. It is only implemented by the +// filesystems saying so. +// It will call Symlink if the filesystem itself is, or it delegates to, the os filesystem, +// or the filesystem otherwise supports Symlink's. +type Linker interface { + SymlinkIfPossible(oldname, newname string) error +} + +// LinkReader is an optional interface in Afero. It is only implemented by the +// filesystems saying so. +type LinkReader interface { + ReadlinkIfPossible(name string) (string, error) +} From 819f7ad35d6a3ab7e72ab6851a1fcd52332f0d90 Mon Sep 17 00:00:00 2001 From: Scott Owens Date: Tue, 10 Dec 2019 16:16:41 +1100 Subject: [PATCH 2/4] Adding and optional Symlinker interface, union interface supportint lstat, symlink and readlink --- symlink.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/symlink.go b/symlink.go index a730413..ba9d179 100644 --- a/symlink.go +++ b/symlink.go @@ -12,6 +12,20 @@ // limitations under the License. package afero + +// Symlinker is an optional interface in Afero. It is only implemented by the +// filesystems saying so. +// It indicates support for 3 symlink related interfaces that implement the +// behaviors of the os methods: +// - Lstat +// - Symlink, and +// - Readlink +type Symlinker interface { + Lstater + Linker + LinkReader +} + // Linker is an optional interface in Afero. It is only implemented by the // filesystems saying so. // It will call Symlink if the filesystem itself is, or it delegates to, the os filesystem, From b4b149f834bfe497475c8f8ffc5857a670da891e Mon Sep 17 00:00:00 2001 From: Scott Owens Date: Tue, 10 Dec 2019 16:24:03 +1100 Subject: [PATCH 3/4] adding support for Linker and LinkReader --- basepath.go | 26 ++++++++++++++++++++++++++ copyOnWriteFs.go | 20 ++++++++++++++++++++ os.go | 8 ++++++++ readonlyfs.go | 12 ++++++++++++ symlink.go | 14 ++++++++++++++ 5 files changed, 80 insertions(+) diff --git a/basepath.go b/basepath.go index 616ff8f..3a14b83 100644 --- a/basepath.go +++ b/basepath.go @@ -177,4 +177,30 @@ func (b *BasePathFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { return fi, false, err } +func (b *BasePathFs) SymlinkIfPossible(oldname, newname string) error { + oldname, err := b.RealPath(oldname) + if err != nil { + return &os.LinkError{Op: "symlink", Old: oldname, New: newname, Err: err} + } + newname, err = b.RealPath(newname) + if err != nil { + return &os.LinkError{Op: "symlink", Old: oldname, New: newname, Err: err} + } + if linker, ok := b.source.(Linker); ok { + return linker.SymlinkIfPossible(oldname, newname) + } + return &os.LinkError{Op: "symlink", Old: oldname, New: newname, Err: ErrNoSymlink} +} + +func (b *BasePathFs) ReadlinkIfPossible(name string) (string, error) { + name, err := b.RealPath(name) + if err != nil { + return "", &os.PathError{Op: "readlink", Path: name, Err: err} + } + if reader, ok := b.source.(LinkReader); ok { + return reader.ReadlinkIfPossible(name) + } + return "", &os.PathError{Op: "readlink", Path: name, Err: ErrNoReadlink} +} + // vim: ts=4 sw=4 noexpandtab nolist syn=go diff --git a/copyOnWriteFs.go b/copyOnWriteFs.go index e8108a8..96b7701 100644 --- a/copyOnWriteFs.go +++ b/copyOnWriteFs.go @@ -117,6 +117,26 @@ func (u *CopyOnWriteFs) LstatIfPossible(name string) (os.FileInfo, bool, error) 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 diff --git a/os.go b/os.go index 13cc1b8..4761db5 100644 --- a/os.go +++ b/os.go @@ -99,3 +99,11 @@ func (OsFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { fi, err := os.Lstat(name) return fi, true, err } + +func (OsFs) SymlinkIfPossible(oldname, newname string) error { + return os.Symlink(oldname, newname) +} + +func (OsFs) ReadlinkIfPossible(name string) (string, error) { + return os.Readlink(name) +} diff --git a/readonlyfs.go b/readonlyfs.go index c6376ec..f94b181 100644 --- a/readonlyfs.go +++ b/readonlyfs.go @@ -44,6 +44,18 @@ func (r *ReadOnlyFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { return fi, false, err } +func (r *ReadOnlyFs) SymlinkIfPossible(oldname, newname string) error { + return &os.LinkError{Op: "symlink", Old: oldname, New: newname, Err: ErrNoSymlink} +} + +func (r *ReadOnlyFs) ReadlinkIfPossible(name string) (string, error) { + if srdr, ok := r.source.(LinkReader); ok { + return srdr.ReadlinkIfPossible(name) + } + + return "", &os.PathError{Op: "readlink", Path: name, Err: ErrNoReadlink} +} + func (r *ReadOnlyFs) Rename(o, n string) error { return syscall.EPERM } diff --git a/symlink.go b/symlink.go index ba9d179..d1c6ea5 100644 --- a/symlink.go +++ b/symlink.go @@ -13,6 +13,10 @@ package afero +import ( + "errors" +) + // Symlinker is an optional interface in Afero. It is only implemented by the // filesystems saying so. // It indicates support for 3 symlink related interfaces that implement the @@ -34,8 +38,18 @@ type Linker interface { SymlinkIfPossible(oldname, newname string) error } +// ErrNoSymlink is the error that will be wrapped in an os.LinkError if a file system +// does not support Symlink's either directly or through its delegated filesystem. +// As expressed by support for the Linker interface. +var ErrNoSymlink = errors.New("symlink not supported") + // LinkReader is an optional interface in Afero. It is only implemented by the // filesystems saying so. type LinkReader interface { ReadlinkIfPossible(name string) (string, error) } + +// ErrNoReadlink is the error that will be wrapped in an os.Path if a file system +// does not support the readlink operation either directly or through its delegated filesystem. +// As expressed by support for the LinkReader interface. +var ErrNoReadlink = errors.New("readlink not supported") From 02af70d733b291edbaa99e7997e64ccb0359b295 Mon Sep 17 00:00:00 2001 From: Scott Owens Date: Wed, 11 Dec 2019 19:49:09 +1100 Subject: [PATCH 4/4] adding tests for Linker and ReadLinker interfaces --- symlink_test.go | 160 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 symlink_test.go diff --git a/symlink_test.go b/symlink_test.go new file mode 100644 index 0000000..5a9e5db --- /dev/null +++ b/symlink_test.go @@ -0,0 +1,160 @@ +package afero + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestSymlinkIfPossible(t *testing.T) { + wd, _ := os.Getwd() + defer func() { + os.Chdir(wd) + }() + + osFs := &OsFs{} + + workDir, err := TempDir(osFs, "", "afero-symlink") + if err != nil { + t.Fatal(err) + } + + defer func() { + osFs.RemoveAll(workDir) + }() + + memWorkDir := "/sym" + + 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") + osPath := filepath.Join(workDir, "afero.txt") + + WriteFile(osFs, osPath, []byte("Hi, Afero!"), 0777) + WriteFile(memFs, filepath.Join(pathFileMem), []byte("Hi, Afero!"), 0777) + + testLink := func(l Linker, source, destination string, output *string) { + if fs, ok := l.(Fs); ok { + dir := filepath.Dir(destination) + if dir != "" { + fs.MkdirAll(dir, 0777) + } + } + + err := l.SymlinkIfPossible(source, destination) + if (err == nil) && (output != nil) { + t.Fatalf("Error creating symlink, succeeded when expecting error %v", *output) + } else if (err != nil) && (output == nil) { + t.Fatalf("Error creating symlink, expected success, got %v", err) + } else if err != nil && err.Error() != *output && !strings.HasSuffix(err.Error(), *output) { + t.Fatalf("Error creating symlink, expected error '%v', instead got output '%v'", *output, err) + } else { + // test passed, if expecting a successful link, check the link with lstat if able + if output == nil { + if lst, ok := l.(Lstater); ok { + _, ok, err := lst.LstatIfPossible(destination) + if !ok { + if err != nil { + t.Fatalf("Error calling lstat on file after successful link, got: %v", err) + } else { + t.Fatalf("Error calling lstat on file after successful link, result didn't use lstat (not link)") + } + return + } + } + } + } + } + + notSupported := ErrNoSymlink.Error() + + testLink(osFs, osPath, filepath.Join(workDir, "os/link.txt"), nil) + testLink(overlayFs1, osPath, filepath.Join(workDir, "overlay/link1.txt"), ¬Supported) + testLink(overlayFs2, pathFileMem, filepath.Join(workDir, "overlay2/link2.txt"), nil) + testLink(overlayFsMemOnly, pathFileMem, filepath.Join(memWorkDir, "overlay3/link.txt"), ¬Supported) + testLink(basePathFs, "afero.txt", "basepath/link.txt", nil) + testLink(basePathFsMem, pathFileMem, "link/file.txt", ¬Supported) + testLink(roFs, osPath, filepath.Join(workDir, "ro/link.txt"), ¬Supported) + testLink(roFsMem, pathFileMem, filepath.Join(memWorkDir, "ro/link.txt"), ¬Supported) +} + +func TestReadlinkIfPossible(t *testing.T) { + wd, _ := os.Getwd() + defer func() { + os.Chdir(wd) + }() + + osFs := &OsFs{} + + workDir, err := TempDir(osFs, "", "afero-readlink") + if err != nil { + t.Fatal(err) + } + + defer func() { + osFs.RemoveAll(workDir) + }() + + memWorkDir := "/read" + + 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") + osPath := filepath.Join(workDir, "afero.txt") + + WriteFile(osFs, osPath, []byte("Hi, Afero!"), 0777) + WriteFile(memFs, filepath.Join(pathFileMem), []byte("Hi, Afero!"), 0777) + + createLink := func(l Linker, source, destination string) error { + if fs, ok := l.(Fs); ok { + dir := filepath.Dir(destination) + if dir != "" { + fs.MkdirAll(dir, 0777) + } + } + + return l.SymlinkIfPossible(source, destination) + } + + testRead := func(r LinkReader, name string, output *string) { + _, err := r.ReadlinkIfPossible(name) + if (err != nil) && (output == nil) { + t.Fatalf("Error reading link, expected success, got error: %v", err) + } else if (err == nil) && (output != nil) { + t.Fatalf("Error reading link, succeeded when expecting error: %v", *output) + } else if err != nil && err.Error() != *output && !strings.HasSuffix(err.Error(), *output) { + t.Fatalf("Error reading link, expected error '%v', instead received '%v'", *output, err) + } + } + + notSupported := ErrNoReadlink.Error() + + err = createLink(osFs, osPath, filepath.Join(workDir, "os/link.txt")) + if err != nil { + t.Fatal("Error creating test link: ", err) + } + + testRead(osFs, filepath.Join(workDir, "os/link.txt"), nil) + testRead(overlayFs1, filepath.Join(workDir, "os/link.txt"), nil) + testRead(overlayFs2, filepath.Join(workDir, "os/link.txt"), nil) + testRead(overlayFsMemOnly, pathFileMem, ¬Supported) + testRead(basePathFs, "os/link.txt", nil) + testRead(basePathFsMem, pathFileMem, ¬Supported) + testRead(roFs, filepath.Join(workDir, "os/link.txt"), nil) + testRead(roFsMem, pathFileMem, ¬Supported) +}