all: Remove symlink support on Windows, SymlinksEnabled config

After this change,

- Symlinks on Windows are always unsupported. Sorry.

- Symlinks are always enabled on other platforms. They are just a small
  file like anything else. There is no need to special case them. If you
  don't want to sync some symlinks, ignore them.

- The protocol doesn't differentiate between different "types" of
  symlinks. If that distinction ever does become relevant the individual
  devices can figure it out by looking at the destination when they
  create the link.

It's backwards compatible in that all the old symlink types are still
understood to be symlinks, and the new SYMLINK type is equivalent to the
old SYMLINK_UNKNOWN which was always a valid way to do it.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3962
LGTM: AudriusButkevicius
This commit is contained in:
Jakob Borg
2017-02-07 08:34:24 +00:00
parent 9fda9642d3
commit c4ba580cbb
28 changed files with 227 additions and 707 deletions

View File

@@ -20,24 +20,10 @@ func (BasicFilesystem) SymlinksSupported() bool {
return symlinksSupported
}
func (BasicFilesystem) CreateSymlink(name, target string, _ LinkTargetType) error {
func (BasicFilesystem) CreateSymlink(name, target string) error {
return os.Symlink(target, name)
}
func (BasicFilesystem) ChangeSymlinkType(_ string, _ LinkTargetType) error {
return nil
}
func (BasicFilesystem) ReadSymlink(path string) (string, LinkTargetType, error) {
tt := LinkTargetUnknown
if stat, err := os.Stat(path); err == nil {
if stat.IsDir() {
tt = LinkTargetDirectory
} else {
tt = LinkTargetFile
}
}
path, err := os.Readlink(path)
return path, tt, err
func (BasicFilesystem) ReadSymlink(path string) (string, error) {
return os.Readlink(path)
}

View File

@@ -8,188 +8,20 @@
package fs
import (
"os"
"path/filepath"
import "errors"
"github.com/syncthing/syncthing/lib/osutil"
var errNotSupported = errors.New("symlinks not supported")
"syscall"
"unicode/utf16"
"unsafe"
)
const (
win32FsctlGetReparsePoint = 0x900a8
win32FileFlagOpenReparsePoint = 0x00200000
win32SymbolicLinkFlagDirectory = 0x1
)
var (
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
procDeviceIoControl = modkernel32.NewProc("DeviceIoControl")
procCreateSymbolicLink = modkernel32.NewProc("CreateSymbolicLinkW")
symlinksSupported = 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.
symlinksSupported = false
}
}()
// Needs administrator privileges.
// 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 := DefaultFilesystem.CreateSymlink(path, base, LinkTargetDirectory)
if err != nil {
return
}
stat, err := osutil.Lstat(path)
if err != nil || stat.Mode()&os.ModeSymlink == 0 {
return
}
target, tt, err := DefaultFilesystem.ReadSymlink(path)
if err != nil || osutil.NativeFilename(target) != base || tt != LinkTargetDirectory {
return
}
symlinksSupported = true
}
func DisableSymlinks() {
symlinksSupported = false
}
func DisableSymlinks() {}
func (BasicFilesystem) SymlinksSupported() bool {
return symlinksSupported
return false
}
func (BasicFilesystem) ReadSymlink(path string) (string, LinkTargetType, error) {
ptr, err := syscall.UTF16PtrFromString(path)
if err != nil {
return "", LinkTargetUnknown, 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|win32FileFlagOpenReparsePoint, 0)
if err != nil || handle == syscall.InvalidHandle {
return "", LinkTargetUnknown, err
}
defer syscall.Close(handle)
var ret uint16
var data reparseData
r1, _, err := syscall.Syscall9(procDeviceIoControl.Addr(), 8, uintptr(handle), win32FsctlGetReparsePoint, 0, 0, uintptr(unsafe.Pointer(&data)), unsafe.Sizeof(data), uintptr(unsafe.Pointer(&ret)), 0, 0)
if r1 == 0 {
return "", LinkTargetUnknown, err
}
tt := LinkTargetUnknown
if attr, err := syscall.GetFileAttributes(ptr); err == nil {
if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
tt = LinkTargetDirectory
} else {
tt = LinkTargetFile
}
}
return osutil.NormalizedFilename(data.printName()), tt, nil
func (BasicFilesystem) ReadSymlink(path string) (string, error) {
return "", errNotSupported
}
func (BasicFilesystem) CreateSymlink(path, target string, tt LinkTargetType) error {
srcp, err := syscall.UTF16PtrFromString(path)
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 tt == LinkTargetUnknown {
path := target
if !filepath.IsAbs(target) {
path = filepath.Join(filepath.Dir(path), target)
}
stat, err := os.Stat(path)
if err == nil && stat.IsDir() {
mode = win32SymbolicLinkFlagDirectory
}
} else if tt == LinkTargetDirectory {
mode = win32SymbolicLinkFlagDirectory
}
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 (fs BasicFilesystem) ChangeSymlinkType(path string, tt LinkTargetType) error {
target, existingTargetType, err := fs.ReadSymlink(path)
if err != nil {
return err
}
// If it's the same type, nothing to do.
if tt == existingTargetType {
return nil
}
// If the actual type is unknown, but the new type is file, nothing to do
if existingTargetType == LinkTargetUnknown && tt != LinkTargetDirectory {
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 fs.CreateSymlink(path, target, tt)
}, path)
}
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 / 2]uint16
}
func (r *reparseData) printName() string {
// offset and length are in bytes but we're indexing a []uint16
offset := r.printNameOffset / 2
length := r.printNameLength / 2
return string(utf16.Decode(r.buffer[offset : offset+length]))
}
func (r *reparseData) substituteName() string {
// offset and length are in bytes but we're indexing a []uint16
offset := r.substitueNameOffset / 2
length := r.substitueNameLength / 2
return string(utf16.Decode(r.buffer[offset : offset+length]))
func (BasicFilesystem) CreateSymlink(path, target string) error {
return errNotSupported
}

View File

@@ -12,26 +12,17 @@ import (
"time"
)
type LinkTargetType int
const (
LinkTargetFile LinkTargetType = iota
LinkTargetDirectory
LinkTargetUnknown
)
// The Filesystem interface abstracts access to the file system.
type Filesystem interface {
ChangeSymlinkType(name string, tt LinkTargetType) error
Chmod(name string, mode FileMode) error
Chtimes(name string, atime time.Time, mtime time.Time) error
Create(name string) (File, error)
CreateSymlink(name, target string, tt LinkTargetType) error
CreateSymlink(name, target string) error
DirNames(name string) ([]string, error)
Lstat(name string) (FileInfo, error)
Mkdir(name string, perm FileMode) error
Open(name string) (File, error)
ReadSymlink(name string) (string, LinkTargetType, error)
ReadSymlink(name string) (string, error)
Remove(name string) error
Rename(oldname, newname string) error
Stat(name string) (FileInfo, error)