diff --git a/build.sh b/build.sh index f5ffe76f1..b3f4fddc4 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,7 @@ set -euo pipefail IFS=$'\n\t' -DOCKERIMGV=1.3.3-4 +DOCKERIMGV=1.4-1 case "${1:-default}" in default) diff --git a/docker/Dockerfile b/docker/Dockerfile index 564179273..8f015f1db 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,7 +1,7 @@ FROM debian:squeeze MAINTAINER Jakob Borg -ENV GOLANG_VERSION 1.3.3 +ENV GOLANG_VERSION 1.4rc2 # SCMs for "go get", gcc for cgo RUN apt-get update && apt-get install -y \ @@ -47,7 +47,7 @@ RUN bash -xec '\ # Install packages needed for test coverage RUN go get github.com/tools/godep \ - && go get code.google.com/p/go.tools/cmd/cover \ + && go get golang.org/x/tools/cmd/cover \ && go get github.com/axw/gocov/gocov \ && go get github.com/AlekSi/gocov-xml diff --git a/internal/model/puller.go b/internal/model/puller.go index 3124940d2..1559b1a40 100644 --- a/internal/model/puller.go +++ b/internal/model/puller.go @@ -817,7 +817,7 @@ func (p *Puller) performFinish(state *sharedPullerState) { // Remove the file, and replace it with a symlink. err = osutil.InWritableDir(func(path string) error { os.Remove(path) - return symlinks.Create(path, string(content), state.file.Flags) + return symlinks.Create(string(content), path, state.file.Flags) }, state.realName) if err != nil { l.Warnln("puller: final: creating symlink:", err) diff --git a/internal/symlinks/symlink.go b/internal/symlinks/symlink.go new file mode 100644 index 000000000..30d597f84 --- /dev/null +++ b/internal/symlinks/symlink.go @@ -0,0 +1,138 @@ +// Copyright (C) 2014 The Syncthing Authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see . + +package symlinks + +import ( + "errors" + "os" + "path/filepath" + "runtime" + + "github.com/syncthing/syncthing/internal/osutil" + "github.com/syncthing/syncthing/internal/protocol" +) + +var ( + Supported = false + ErrUnsupported = errors.New("symlinks not supported") +) + +func init() { + Supported = symlinksSupported() +} + +func Read(path string) (string, uint32, error) { + if !Supported { + return "", 0, ErrUnsupported + } + + var mode uint32 + stat, err := os.Stat(path) + if err != nil { + mode = protocol.FlagSymlinkMissingTarget + } else if stat.IsDir() { + mode = protocol.FlagDirectory + } + path, err = os.Readlink(path) + + return osutil.NormalizedFilename(path), mode, err +} + +func IsSymlink(path string) (bool, error) { + if !Supported { + return false, ErrUnsupported + } + + lstat, err := os.Lstat(path) + if err != nil { + return false, err + } + return lstat.Mode()&os.ModeSymlink != 0, nil +} + +func Create(target, source string, flags uint32) error { + if !Supported { + return ErrUnsupported + } + + return os.Symlink(osutil.NativeFilename(target), source) +} + +func ChangeType(path string, flags uint32) error { + if runtime.GOOS != "windows" { + // This is a Windows-only concept. + return nil + } + + if !Supported { + return ErrUnsupported + } + + target, cflags, err := Read(path) + if err != nil { + return err + } + + // If it's the same type, nothing to do. + if cflags&protocol.SymlinkTypeMask == flags&protocol.SymlinkTypeMask { + return nil + } + + // If the actual type is unknown, but the new type is file, nothing to do + if cflags&protocol.FlagSymlinkMissingTarget != 0 && flags&protocol.FlagDirectory == 0 { + return nil + } + + return osutil.InWritableDir(func(path string) error { + // It should be a symlink as well hence no need to change permissions + // on the file. + os.Remove(path) + return Create(target, path, flags) + }, path) +} + +func symlinksSupported() bool { + if runtime.GOOS != "windows" { + // Symlinks are supported. In practice there may be deviations (FAT + // filesystems etc), but these get handled and reported as the errors + // they are when they happen. + return true + } + + // We try to create a symlink and verify that it looks like we expected. + // Needs administrator priviledges and a version higher than XP. + + base := os.TempDir() + path := filepath.Join(base, "syncthing-symlink-test") + defer os.Remove(path) + + err := Create(base, path, protocol.FlagDirectory) + if err != nil { + return false + } + + isLink, err := IsSymlink(path) + if err != nil || !isLink { + return false + } + + target, flags, err := Read(path) + if err != nil || osutil.NativeFilename(target) != base || flags&protocol.FlagDirectory == 0 { + return false + } + + return true +} diff --git a/internal/symlinks/symlink_unix.go b/internal/symlinks/symlink_unix.go deleted file mode 100644 index 814c74118..000000000 --- a/internal/symlinks/symlink_unix.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (C) 2014 The Syncthing Authors. -// -// This program is free software: you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program. If not, see . - -// +build !windows - -package symlinks - -import ( - "os" - - "github.com/syncthing/syncthing/internal/osutil" - "github.com/syncthing/syncthing/internal/protocol" -) - -var ( - Supported = true -) - -func Read(path string) (string, uint32, error) { - var mode uint32 - stat, err := os.Stat(path) - if err != nil { - mode = protocol.FlagSymlinkMissingTarget - } else if stat.IsDir() { - mode = protocol.FlagDirectory - } - path, err = os.Readlink(path) - - return osutil.NormalizedFilename(path), mode, err -} - -func IsSymlink(path string) (bool, error) { - lstat, err := os.Lstat(path) - if err != nil { - return false, err - } - return lstat.Mode()&os.ModeSymlink != 0, nil -} - -func Create(source, target string, flags uint32) error { - return os.Symlink(osutil.NativeFilename(target), source) -} - -func ChangeType(path string, flags uint32) error { - return nil -} diff --git a/internal/symlinks/symlink_windows.go b/internal/symlinks/symlink_windows.go deleted file mode 100644 index 93a14b588..000000000 --- a/internal/symlinks/symlink_windows.go +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright (C) 2014 The Syncthing Authors. -// -// This program is free software: you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program. If not, see . - -// +build windows - -package symlinks - -import ( - "os" - "path/filepath" - - "github.com/syncthing/syncthing/internal/osutil" - "github.com/syncthing/syncthing/internal/protocol" - - "syscall" - "unicode/utf16" - "unsafe" -) - -const ( - FSCTL_GET_REPARSE_POINT = 0x900a8 - FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 - FILE_ATTRIBUTE_REPARSE_POINT = 0x400 - IO_REPARSE_TAG_SYMLINK = 0xA000000C - SYMBOLIC_LINK_FLAG_DIRECTORY = 0x1 -) - -var ( - modkernel32 = syscall.NewLazyDLL("kernel32.dll") - procDeviceIoControl = modkernel32.NewProc("DeviceIoControl") - procCreateSymbolicLink = modkernel32.NewProc("CreateSymbolicLinkW") - - Supported = false -) - -func init() { - defer func() { - if err := recover(); err != nil { - // Ensure that the supported flag is disabled when we hit an - // error, even though it should already be. Also, silently swallow - // the error since it's fine for a system not to support symlinks. - Supported = false - } - }() - - // Needs administrator priviledges. - // Let's check that everything works. - // This could be done more officially: - // http://stackoverflow.com/questions/2094663/determine-if-windows-process-has-privilege-to-create-symbolic-link - // But I don't want to define 10 more structs just to look this up. - base := os.TempDir() - path := filepath.Join(base, "symlinktest") - defer os.Remove(path) - - err := Create(path, base, protocol.FlagDirectory) - if err != nil { - return - } - - isLink, err := IsSymlink(path) - if err != nil || !isLink { - return - } - - target, flags, err := Read(path) - if err != nil || osutil.NativeFilename(target) != base || flags&protocol.FlagDirectory == 0 { - return - } - Supported = true -} - -type reparseData struct { - reparseTag uint32 - reparseDataLength uint16 - reserved uint16 - substitueNameOffset uint16 - substitueNameLength uint16 - printNameOffset uint16 - printNameLength uint16 - flags uint32 - // substituteName - 264 widechars max = 528 bytes - // printName - 260 widechars max = 520 bytes - // = 1048 bytes total - buffer [1048]uint16 -} - -func (r *reparseData) PrintName() string { - // No clue why the offset and length is doubled... - offset := r.printNameOffset / 2 - length := r.printNameLength / 2 - return string(utf16.Decode(r.buffer[offset : offset+length])) -} - -func (r *reparseData) SubstituteName() string { - // No clue why the offset and length is doubled... - offset := r.substitueNameOffset / 2 - length := r.substitueNameLength / 2 - return string(utf16.Decode(r.buffer[offset : offset+length])) -} - -func Read(path string) (string, uint32, error) { - ptr, err := syscall.UTF16PtrFromString(path) - if err != nil { - return "", protocol.FlagSymlinkMissingTarget, err - } - handle, err := syscall.CreateFile(ptr, 0, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OPEN_REPARSE_POINT, 0) - if err != nil || handle == syscall.InvalidHandle { - return "", protocol.FlagSymlinkMissingTarget, err - } - defer syscall.Close(handle) - var ret uint16 - var data reparseData - - r1, _, err := syscall.Syscall9(procDeviceIoControl.Addr(), 8, uintptr(handle), FSCTL_GET_REPARSE_POINT, 0, 0, uintptr(unsafe.Pointer(&data)), unsafe.Sizeof(data), uintptr(unsafe.Pointer(&ret)), 0, 0) - if r1 == 0 { - return "", protocol.FlagSymlinkMissingTarget, err - } - - var flags uint32 = 0 - attr, err := syscall.GetFileAttributes(ptr) - if err != nil { - flags = protocol.FlagSymlinkMissingTarget - } else if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 { - flags = protocol.FlagDirectory - } - - return osutil.NormalizedFilename(data.PrintName()), flags, nil -} - -func IsSymlink(path string) (bool, error) { - ptr, err := syscall.UTF16PtrFromString(path) - if err != nil { - return false, err - } - - attr, err := syscall.GetFileAttributes(ptr) - if err != nil { - return false, err - } - return attr&FILE_ATTRIBUTE_REPARSE_POINT != 0, nil -} - -func Create(source, target string, flags uint32) error { - srcp, err := syscall.UTF16PtrFromString(source) - if err != nil { - return err - } - - trgp, err := syscall.UTF16PtrFromString(osutil.NativeFilename(target)) - if err != nil { - return err - } - - // Sadly for Windows we need to specify the type of the symlink, - // whether it's a directory symlink or a file symlink. - // If the flags doesn't reveal the target type, try to evaluate it - // ourselves, and worst case default to the symlink pointing to a file. - mode := 0 - if flags&protocol.FlagSymlinkMissingTarget != 0 { - path := target - if !filepath.IsAbs(target) { - path = filepath.Join(filepath.Dir(source), target) - } - - stat, err := os.Stat(path) - if err == nil && stat.IsDir() { - mode = SYMBOLIC_LINK_FLAG_DIRECTORY - } - } else if flags&protocol.FlagDirectory != 0 { - mode = SYMBOLIC_LINK_FLAG_DIRECTORY - } - - r0, _, err := syscall.Syscall(procCreateSymbolicLink.Addr(), 3, uintptr(unsafe.Pointer(srcp)), uintptr(unsafe.Pointer(trgp)), uintptr(mode)) - if r0 == 1 { - return nil - } - return err -} - -func ChangeType(path string, flags uint32) error { - target, cflags, err := Read(path) - if err != nil { - return err - } - // If it's the same type, nothing to do. - if cflags&protocol.SymlinkTypeMask == flags&protocol.SymlinkTypeMask { - return nil - } - - // If the actual type is unknown, but the new type is file, nothing to do - if cflags&protocol.FlagSymlinkMissingTarget != 0 && flags&protocol.FlagDirectory == 0 { - return nil - } - return osutil.InWritableDir(func(path string) error { - // It should be a symlink as well hence no need to change permissions on - // the file. - os.Remove(path) - return Create(path, target, flags) - }, path) -} diff --git a/test/symlink_test.go b/test/symlink_test.go index 8cb9e4b43..d11c8ae77 100644 --- a/test/symlink_test.go +++ b/test/symlink_test.go @@ -62,7 +62,7 @@ func TestSymlinks(t *testing.T) { t.Fatal(err) } fd.Close() - err = symlinks.Create("s1/fileLink", "file", 0) + err = symlinks.Create("file", "s1/fileLink", 0) if err != nil { log.Fatal(err) } @@ -73,35 +73,35 @@ func TestSymlinks(t *testing.T) { if err != nil { t.Fatal(err) } - err = symlinks.Create("s1/dirLink", "dir", 0) + err = symlinks.Create("dir", "s1/dirLink", 0) if err != nil { log.Fatal(err) } // A link to something in the repo that does not exist - err = symlinks.Create("s1/noneLink", "does/not/exist", 0) + err = symlinks.Create("does/not/exist", "s1/noneLink", 0) if err != nil { log.Fatal(err) } // A link we will replace with a file later - err = symlinks.Create("s1/repFileLink", "does/not/exist", 0) + err = symlinks.Create("does/not/exist", "s1/repFileLink", 0) if err != nil { log.Fatal(err) } // A link we will replace with a directory later - err = symlinks.Create("s1/repDirLink", "does/not/exist", 0) + err = symlinks.Create("does/not/exist", "s1/repDirLink", 0) if err != nil { log.Fatal(err) } // A link we will remove later - err = symlinks.Create("s1/removeLink", "does/not/exist", 0) + err = symlinks.Create("does/not/exist", "s1/removeLink", 0) if err != nil { log.Fatal(err) } @@ -184,7 +184,7 @@ func TestSymlinks(t *testing.T) { if err != nil { log.Fatal(err) } - err = symlinks.Create("s1/dirLink", "file", 0) + err = symlinks.Create("file", "s1/dirLink", 0) if err != nil { log.Fatal(err) } @@ -220,7 +220,7 @@ func TestSymlinks(t *testing.T) { if err != nil { log.Fatal(err) } - err = symlinks.Create("s1/fileToReplace", "somewhere/non/existent", 0) + err = symlinks.Create("somewhere/non/existent", "s1/fileToReplace", 0) if err != nil { log.Fatal(err) } @@ -231,7 +231,7 @@ func TestSymlinks(t *testing.T) { if err != nil { log.Fatal(err) } - err = symlinks.Create("s1/dirToReplace", "somewhere/non/existent", 0) + err = symlinks.Create("somewhere/non/existent", "s1/dirToReplace", 0) if err != nil { log.Fatal(err) }