diff --git a/build.sh b/build.sh index b3f4fddc4..f5ffe76f1 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,7 @@ set -euo pipefail IFS=$'\n\t' -DOCKERIMGV=1.4-1 +DOCKERIMGV=1.3.3-4 case "${1:-default}" in default) diff --git a/docker/Dockerfile b/docker/Dockerfile index 8f015f1db..564179273 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,7 +1,7 @@ FROM debian:squeeze MAINTAINER Jakob Borg -ENV GOLANG_VERSION 1.4rc2 +ENV GOLANG_VERSION 1.3.3 # 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 golang.org/x/tools/cmd/cover \ + && go get code.google.com/p/go.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 1559b1a40..3124940d2 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(string(content), path, state.file.Flags) + return symlinks.Create(path, string(content), 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 deleted file mode 100644 index 30d597f84..000000000 --- a/internal/symlinks/symlink.go +++ /dev/null @@ -1,138 +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 . - -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 new file mode 100644 index 000000000..814c74118 --- /dev/null +++ b/internal/symlinks/symlink_unix.go @@ -0,0 +1,58 @@ +// 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 new file mode 100644 index 000000000..93a14b588 --- /dev/null +++ b/internal/symlinks/symlink_windows.go @@ -0,0 +1,212 @@ +// 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 d11c8ae77..8cb9e4b43 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("file", "s1/fileLink", 0) + err = symlinks.Create("s1/fileLink", "file", 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("dir", "s1/dirLink", 0) + err = symlinks.Create("s1/dirLink", "dir", 0) if err != nil { log.Fatal(err) } // A link to something in the repo that does not exist - err = symlinks.Create("does/not/exist", "s1/noneLink", 0) + err = symlinks.Create("s1/noneLink", "does/not/exist", 0) if err != nil { log.Fatal(err) } // A link we will replace with a file later - err = symlinks.Create("does/not/exist", "s1/repFileLink", 0) + err = symlinks.Create("s1/repFileLink", "does/not/exist", 0) if err != nil { log.Fatal(err) } // A link we will replace with a directory later - err = symlinks.Create("does/not/exist", "s1/repDirLink", 0) + err = symlinks.Create("s1/repDirLink", "does/not/exist", 0) if err != nil { log.Fatal(err) } // A link we will remove later - err = symlinks.Create("does/not/exist", "s1/removeLink", 0) + err = symlinks.Create("s1/removeLink", "does/not/exist", 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("file", "s1/dirLink", 0) + err = symlinks.Create("s1/dirLink", "file", 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("somewhere/non/existent", "s1/fileToReplace", 0) + err = symlinks.Create("s1/fileToReplace", "somewhere/non/existent", 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("somewhere/non/existent", "s1/dirToReplace", 0) + err = symlinks.Create("s1/dirToReplace", "somewhere/non/existent", 0) if err != nil { log.Fatal(err) }