Skip to content

Commit

Permalink
Merge pull request golang#781 from ibrasho-forks/fix-symlink-cloning-…
Browse files Browse the repository at this point in the history
…on-windows

internal/fs: don't clone symlinks on windows
  • Loading branch information
sdboyer authored Aug 1, 2017
2 parents 8a46f4d + ced76d2 commit 44a454b
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 97 deletions.
56 changes: 30 additions & 26 deletions internal/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"syscall"
"unicode"

"github.com/pkg/errors"
Expand Down Expand Up @@ -270,10 +272,25 @@ func CopyDir(src, dst string) error {
// the copied data is synced/flushed to stable storage.
func copyFile(src, dst string) (err error) {
if sym, err := IsSymlink(src); err != nil {
return err
return errors.Wrap(err, "symlink check failed")
} else if sym {
err := copySymlink(src, dst)
return err
if err := cloneSymlink(src, dst); err != nil {
if runtime.GOOS == "windows" {
// If cloning the symlink fails on Windows because the user
// does not have the required privileges, ignore the error and
// fall back to copying the file contents.
//
// ERROR_PRIVILEGE_NOT_HELD is 1314 (0x522):
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms681385(v=vs.85).aspx
if lerr, ok := err.(*os.LinkError); ok && lerr.Err != syscall.Errno(1314) {
return err
}
} else {
return err
}
} else {
return nil
}
}

in, err := os.Open(src)
Expand All @@ -286,48 +303,35 @@ func copyFile(src, dst string) (err error) {
if err != nil {
return
}
defer func() {
if e := out.Close(); e != nil {
err = e
}
}()
defer out.Close()

_, err = io.Copy(out, in)
if err != nil {
if _, err = io.Copy(out, in); err != nil {
return
}

err = out.Sync()
if err != nil {
if err = out.Sync(); err != nil {
return
}

si, err := os.Stat(src)
if err != nil {
return
}

err = os.Chmod(dst, si.Mode())
if err != nil {
return
}

return
}

// copySymlink will resolve the src symlink and create a new symlink in dst.
// If src is a relative symlink, dst will also be a relative symlink.
func copySymlink(src, dst string) error {
resolved, err := os.Readlink(src)
// cloneSymlink will create a new symlink that points to the resolved path of sl.
// If sl is a relative symlink, dst will also be a relative symlink.
func cloneSymlink(sl, dst string) error {
resolved, err := os.Readlink(sl)
if err != nil {
return errors.Wrap(err, "failed to resolve symlink")
}

err = os.Symlink(resolved, dst)
if err != nil {
return errors.Wrapf(err, "failed to create symlink %s to %s", src, resolved)
return err
}

return nil
return os.Symlink(resolved, dst)
}

// IsDir determines is the path given is a directory or not.
Expand Down
120 changes: 49 additions & 71 deletions internal/fs/fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,71 +499,51 @@ func TestCopyFile(t *testing.T) {
}

func TestCopyFileSymlink(t *testing.T) {
dir, err := ioutil.TempDir("", "dep")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)

srcPath := filepath.Join(dir, "src")
symlinkPath := filepath.Join(dir, "symlink")
dstPath := filepath.Join(dir, "dst")

srcf, err := os.Create(srcPath)
if err != nil {
t.Fatal(err)
}
srcf.Close()

if err = os.Symlink(srcPath, symlinkPath); err != nil {
t.Fatalf("could not create symlink: %s", err)
}

if err = copyFile(symlinkPath, dstPath); err != nil {
t.Fatalf("failed to copy symlink: %s", err)
}

resolvedPath, err := os.Readlink(dstPath)
if err != nil {
t.Fatalf("could not resolve symlink: %s", err)
}

if resolvedPath != srcPath {
t.Fatalf("resolved path is incorrect. expected %s, got %s", srcPath, resolvedPath)
}
}

func TestCopyFileSymlinkToDirectory(t *testing.T) {
dir, err := ioutil.TempDir("", "dep")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)

srcPath := filepath.Join(dir, "src")
symlinkPath := filepath.Join(dir, "symlink")
dstPath := filepath.Join(dir, "dst")

err = os.MkdirAll(srcPath, 0777)
if err != nil {
t.Fatal(err)
}
h := test.NewHelper(t)
defer h.Cleanup()
h.TempDir(".")

if err = os.Symlink(srcPath, symlinkPath); err != nil {
t.Fatalf("could not create symlink: %v", err)
testcases := map[string]string{
filepath.Join("./testdata/symlinks/file-symlink"): filepath.Join(h.Path("."), "dst-file"),
filepath.Join("./testdata/symlinks/windows-file-symlink"): filepath.Join(h.Path("."), "windows-dst-file"),
filepath.Join("./testdata/symlinks/dir-symlink"): filepath.Join(h.Path("."), "dst-dir"),
filepath.Join("./testdata/symlinks/invalid-symlink"): filepath.Join(h.Path("."), "invalid-symlink"),
}

if err = copyFile(symlinkPath, dstPath); err != nil {
t.Fatalf("failed to copy symlink: %s", err)
}
for symlink, dst := range testcases {
t.Run(symlink, func(t *testing.T) {
var err error
if err = copyFile(symlink, dst); err != nil {
t.Fatalf("failed to copy symlink: %s", err)
}

resolvedPath, err := os.Readlink(dstPath)
if err != nil {
t.Fatalf("could not resolve symlink: %s", err)
}
var want, got string

if runtime.GOOS == "windows" {
// Creating symlinks on Windows require an additional permission
// regular users aren't granted usually. So we copy the file
// content as a fall back instead of creating a real symlink.
srcb, err := ioutil.ReadFile(symlink)
h.Must(err)
dstb, err := ioutil.ReadFile(dst)
h.Must(err)

want = string(srcb)
got = string(dstb)
} else {
want, err = os.Readlink(symlink)
h.Must(err)

got, err = os.Readlink(dst)
if err != nil {
t.Fatalf("could not resolve symlink: %s", err)
}
}

if resolvedPath != srcPath {
t.Fatalf("resolved path is incorrect. expected %s, got %s", srcPath, resolvedPath)
if want != got {
t.Fatalf("resolved path is incorrect. expected %s, got %s", want, got)
}
})
}
}

Expand Down Expand Up @@ -814,13 +794,6 @@ func TestIsNonEmptyDir(t *testing.T) {
}

func TestIsSymlink(t *testing.T) {
if runtime.GOOS == "windows" {
// XXX: creating symlinks is not supported in Go on
// Microsoft Windows. Skipping this this until a solution
// for creating symlinks is is provided.
t.Skip("skipping on windows")
}

dir, err := ioutil.TempDir("", "dep")
if err != nil {
t.Fatal(err)
Expand All @@ -841,6 +814,7 @@ func TestIsSymlink(t *testing.T) {

dirSymlink := filepath.Join(dir, "dirSymlink")
fileSymlink := filepath.Join(dir, "fileSymlink")

if err = os.Symlink(dirPath, dirSymlink); err != nil {
t.Fatal(err)
}
Expand All @@ -866,10 +840,7 @@ func TestIsSymlink(t *testing.T) {
})
defer cleanup()

tests := map[string]struct {
expected bool
err bool
}{
tests := map[string]struct{ expected, err bool }{
dirPath: {false, false},
filePath: {false, false},
dirSymlink: {true, false},
Expand All @@ -878,6 +849,13 @@ func TestIsSymlink(t *testing.T) {
inaccessibleSymlink: {false, true},
}

if runtime.GOOS == "windows" {
// XXX: setting permissions works differently in Windows. Skipping
// these cases until a compatible implementation is provided.
delete(tests, inaccessibleFile)
delete(tests, inaccessibleSymlink)
}

for path, want := range tests {
got, err := IsSymlink(path)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions internal/fs/testdata/symlinks/dir-symlink
1 change: 1 addition & 0 deletions internal/fs/testdata/symlinks/file-symlink
1 change: 1 addition & 0 deletions internal/fs/testdata/symlinks/invalid-symlink
1 change: 1 addition & 0 deletions internal/fs/testdata/symlinks/windows-file-symlink

0 comments on commit 44a454b

Please sign in to comment.