forked from mirror/afero
Merge pull request #228 from Maldris/symlink
Add optional interfaces for Symlink and Readlink
This commit is contained in:
commit
a7dc6ae3c5
26
basepath.go
26
basepath.go
|
@ -177,4 +177,30 @@ func (b *BasePathFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
|
||||||
return fi, false, err
|
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
|
// vim: ts=4 sw=4 noexpandtab nolist syn=go
|
||||||
|
|
|
@ -117,6 +117,26 @@ func (u *CopyOnWriteFs) LstatIfPossible(name string) (os.FileInfo, bool, error)
|
||||||
return fi, false, err
|
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 {
|
func (u *CopyOnWriteFs) isNotExist(err error) bool {
|
||||||
if e, ok := err.(*os.PathError); ok {
|
if e, ok := err.(*os.PathError); ok {
|
||||||
err = e.Err
|
err = e.Err
|
||||||
|
|
8
os.go
8
os.go
|
@ -99,3 +99,11 @@ func (OsFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
|
||||||
fi, err := os.Lstat(name)
|
fi, err := os.Lstat(name)
|
||||||
return fi, true, err
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -44,6 +44,18 @@ func (r *ReadOnlyFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
|
||||||
return fi, false, err
|
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 {
|
func (r *ReadOnlyFs) Rename(o, n string) error {
|
||||||
return syscall.EPERM
|
return syscall.EPERM
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright © 2018 Steve Francia <spf@spf13.com>.
|
||||||
|
//
|
||||||
|
// 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 (
|
||||||
|
"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
|
||||||
|
// 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,
|
||||||
|
// or the filesystem otherwise supports Symlink's.
|
||||||
|
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")
|
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue