Merge pull request #953 from syncthing/symlink

Symlink support
This commit is contained in:
Jakob Borg 2014-11-20 16:34:12 +01:00
commit 2cd9e7fb55
17 changed files with 814 additions and 83 deletions

View File

@ -29,6 +29,7 @@ import (
"sync"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syndtr/goleveldb/leveldb"
@ -171,7 +172,7 @@ func (f *BlockFinder) Iterate(hash []byte, iterFn func(string, string, uint32) b
for iter.Next() && iter.Error() == nil {
folder, file := fromBlockKey(iter.Key())
index := binary.BigEndian.Uint32(iter.Value())
if iterFn(folder, nativeFilename(file), index) {
if iterFn(folder, osutil.NativeFilename(file), index) {
return true
}
}

View File

@ -25,6 +25,7 @@ import (
"sync"
"github.com/syncthing/syncthing/internal/lamport"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syndtr/goleveldb/leveldb"
)
@ -174,19 +175,19 @@ func (s *Set) WithGlobalTruncated(fn fileIterator) {
}
func (s *Set) Get(device protocol.DeviceID, file string) protocol.FileInfo {
f := ldbGet(s.db, []byte(s.folder), device[:], []byte(normalizedFilename(file)))
f.Name = nativeFilename(f.Name)
f := ldbGet(s.db, []byte(s.folder), device[:], []byte(osutil.NormalizedFilename(file)))
f.Name = osutil.NativeFilename(f.Name)
return f
}
func (s *Set) GetGlobal(file string) protocol.FileInfo {
f := ldbGetGlobal(s.db, []byte(s.folder), []byte(normalizedFilename(file)))
f.Name = nativeFilename(f.Name)
f := ldbGetGlobal(s.db, []byte(s.folder), []byte(osutil.NormalizedFilename(file)))
f.Name = osutil.NativeFilename(f.Name)
return f
}
func (s *Set) Availability(file string) []protocol.DeviceID {
return ldbAvailability(s.db, []byte(s.folder), []byte(normalizedFilename(file)))
return ldbAvailability(s.db, []byte(s.folder), []byte(osutil.NormalizedFilename(file)))
}
func (s *Set) LocalVersion(device protocol.DeviceID) uint64 {
@ -213,7 +214,7 @@ func DropFolder(db *leveldb.DB, folder string) {
func normalizeFilenames(fs []protocol.FileInfo) {
for i := range fs {
fs[i].Name = normalizedFilename(fs[i].Name)
fs[i].Name = osutil.NormalizedFilename(fs[i].Name)
}
}
@ -221,10 +222,10 @@ func nativeFileIterator(fn fileIterator) fileIterator {
return func(fi protocol.FileIntf) bool {
switch f := fi.(type) {
case protocol.FileInfo:
f.Name = nativeFilename(f.Name)
f.Name = osutil.NativeFilename(f.Name)
return fn(f)
case protocol.FileInfoTruncated:
f.Name = nativeFilename(f.Name)
f.Name = osutil.NativeFilename(f.Name)
return fn(f)
default:
panic("unknown interface type")

View File

@ -39,6 +39,7 @@ import (
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syncthing/syncthing/internal/scanner"
"github.com/syncthing/syncthing/internal/stats"
"github.com/syncthing/syncthing/internal/symlinks"
"github.com/syncthing/syncthing/internal/versioner"
"github.com/syndtr/goleveldb/leveldb"
)
@ -114,6 +115,8 @@ type Model struct {
var (
ErrNoSuchFile = errors.New("no such file")
ErrInvalid = errors.New("file is invalid")
SymlinkWarning = sync.Once{}
)
// NewModel creates and starts a new model. The model starts in read-only mode,
@ -440,9 +443,9 @@ func (m *Model) Index(deviceID protocol.DeviceID, folder string, fs []protocol.F
for i := 0; i < len(fs); {
lamport.Default.Tick(fs[i].Version)
if ignores != nil && ignores.Match(fs[i].Name) {
if (ignores != nil && ignores.Match(fs[i].Name)) || symlinkInvalid(fs[i].IsSymlink()) {
if debug {
l.Debugln("dropping update for ignored", fs[i])
l.Debugln("dropping update for ignored/unsupported symlink", fs[i])
}
fs[i] = fs[len(fs)-1]
fs = fs[:len(fs)-1]
@ -484,9 +487,9 @@ func (m *Model) IndexUpdate(deviceID protocol.DeviceID, folder string, fs []prot
for i := 0; i < len(fs); {
lamport.Default.Tick(fs[i].Version)
if ignores != nil && ignores.Match(fs[i].Name) {
if (ignores != nil && ignores.Match(fs[i].Name)) || symlinkInvalid(fs[i].IsSymlink()) {
if debug {
l.Debugln("dropping update for ignored", fs[i])
l.Debugln("dropping update for ignored/unsupported symlink", fs[i])
}
fs[i] = fs[len(fs)-1]
fs = fs[:len(fs)-1]
@ -655,7 +658,7 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
}
lf := r.Get(protocol.LocalDeviceID, name)
if protocol.IsInvalid(lf.Flags) || protocol.IsDeleted(lf.Flags) {
if lf.IsInvalid() || lf.IsDeleted() {
if debug {
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d; invalid: %v", m, deviceID, folder, name, offset, size, lf)
}
@ -675,14 +678,26 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
m.fmut.RLock()
fn := filepath.Join(m.folderCfgs[folder].Path, name)
m.fmut.RUnlock()
fd, err := os.Open(fn) // XXX: Inefficient, should cache fd?
if err != nil {
return nil, err
var reader io.ReaderAt
var err error
if lf.IsSymlink() {
target, _, err := symlinks.Read(fn)
if err != nil {
return nil, err
}
reader = strings.NewReader(target)
} else {
reader, err = os.Open(fn) // XXX: Inefficient, should cache fd?
if err != nil {
return nil, err
}
defer reader.(*os.File).Close()
}
defer fd.Close()
buf := make([]byte, size)
_, err = fd.ReadAt(buf, offset)
_, err = reader.ReadAt(buf, offset)
if err != nil {
return nil, err
}
@ -892,9 +907,9 @@ func sendIndexTo(initial bool, minLocalVer uint64, conn protocol.Connection, fol
maxLocalVer = f.LocalVersion
}
if ignores != nil && ignores.Match(f.Name) {
if (ignores != nil && ignores.Match(f.Name)) || symlinkInvalid(f.IsSymlink()) {
if debug {
l.Debugln("not sending update for ignored", f)
l.Debugln("not sending update for ignored/unsupported symlink", f)
}
return true
}
@ -1085,7 +1100,7 @@ func (m *Model) ScanFolderSub(folder, sub string) error {
}
seenPrefix = true
if !protocol.IsDeleted(f.Flags) {
if !f.IsDeleted() {
if f.IsInvalid() {
return true
}
@ -1095,8 +1110,8 @@ func (m *Model) ScanFolderSub(folder, sub string) error {
batch = batch[:0]
}
if ignores != nil && ignores.Match(f.Name) {
// File has been ignored. Set invalid bit.
if (ignores != nil && ignores.Match(f.Name)) || symlinkInvalid(f.IsSymlink()) {
// File has been ignored or an unsupported symlink. Set invalid bit.
l.Debugln("setting invalid bit on ignored", f)
nf := protocol.FileInfo{
Name: f.Name,
@ -1112,7 +1127,7 @@ func (m *Model) ScanFolderSub(folder, sub string) error {
"size": f.Size(),
})
batch = append(batch, nf)
} else if _, err := os.Stat(filepath.Join(dir, f.Name)); err != nil && os.IsNotExist(err) {
} else if _, err := os.Lstat(filepath.Join(dir, f.Name)); err != nil && os.IsNotExist(err) {
// File has been deleted
nf := protocol.FileInfo{
Name: f.Name,
@ -1326,3 +1341,13 @@ func (m *Model) leveldbPanicWorkaround() {
}
}
}
func symlinkInvalid(isLink bool) bool {
if !symlinks.Supported && isLink {
SymlinkWarning.Do(func() {
l.Warnln("Symlinks are unsupported as they require Administrator priviledges. This might cause your folder to appear out of sync.")
})
return true
}
return false
}

View File

@ -20,6 +20,7 @@ import (
"crypto/sha256"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
@ -32,6 +33,7 @@ import (
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syncthing/syncthing/internal/scanner"
"github.com/syncthing/syncthing/internal/symlinks"
"github.com/syncthing/syncthing/internal/versioner"
)
@ -313,15 +315,16 @@ func (p *Puller) pullerIteration(ncopiers, npullers, nfinishers int, checksum bo
}
switch {
case protocol.IsDeleted(file.Flags):
// A deleted file or directory
case file.IsDeleted():
// A deleted file, directory or symlink
deletions = append(deletions, file)
case protocol.IsDirectory(file.Flags):
case file.IsDirectory() && !file.IsSymlink():
// A new or changed directory
p.handleDir(file)
default:
// A new or changed file. This is the only case where we do stuff
// in the background; the other three are done synchronously.
// A new or changed file or symlink. This is the only case where we
// do stuff in the background; the other three are done
// synchronously.
p.handleFile(file, copyChan, finisherChan)
}
@ -459,24 +462,21 @@ func (p *Puller) deleteFile(file protocol.FileInfo) {
func (p *Puller) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksState, finisherChan chan<- *sharedPullerState) {
curFile := p.model.CurrentFolderFile(p.folder, file.Name)
if len(curFile.Blocks) == len(file.Blocks) {
for i := range file.Blocks {
if !bytes.Equal(curFile.Blocks[i].Hash, file.Blocks[i].Hash) {
goto FilesAreDifferent
}
}
if len(curFile.Blocks) == len(file.Blocks) && scanner.BlocksEqual(curFile.Blocks, file.Blocks) {
// We are supposed to copy the entire file, and then fetch nothing. We
// are only updating metadata, so we don't actually *need* to make the
// copy.
if debug {
l.Debugln(p, "taking shortcut on", file.Name)
}
p.shortcutFile(file)
if file.IsSymlink() {
p.shortcutSymlink(curFile, file)
} else {
p.shortcutFile(file)
}
return
}
FilesAreDifferent:
scanner.PopulateOffsets(file.Blocks)
// Figure out the absolute filenames we need once and for all
@ -571,6 +571,17 @@ func (p *Puller) shortcutFile(file protocol.FileInfo) {
p.model.updateLocal(p.folder, file)
}
// shortcutSymlink changes the symlinks type if necessery.
func (p *Puller) shortcutSymlink(curFile, file protocol.FileInfo) {
err := symlinks.ChangeType(filepath.Join(p.dir, file.Name), file.Flags)
if err != nil {
l.Infof("Puller (folder %q, file %q): symlink shortcut: %v", p.folder, file.Name, err)
return
}
p.model.updateLocal(p.folder, file)
}
// copierRoutine reads copierStates until the in channel closes and performs
// the relevant copies when possible, or passes it to the puller routine.
func (p *Puller) copierRoutine(in <-chan copyBlocksState, pullChan chan<- pullBlockState, out chan<- *sharedPullerState, checksum bool) {
@ -791,6 +802,25 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
continue
}
// If it's a symlink, the target of the symlink is inside the file.
if state.file.IsSymlink() {
content, err := ioutil.ReadFile(state.realName)
if err != nil {
l.Warnln("puller: final: reading symlink:", err)
continue
}
// 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)
}, state.realName)
if err != nil {
l.Warnln("puller: final: creating symlink:", err)
continue
}
}
// Record the updated file in the index
p.model.updateLocal(p.folder, state.file)
}

View File

@ -13,14 +13,14 @@
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package files
package osutil
import "code.google.com/p/go.text/unicode/norm"
func normalizedFilename(s string) string {
func NormalizedFilename(s string) string {
return norm.NFC.String(s)
}
func nativeFilename(s string) string {
func NativeFilename(s string) string {
return norm.NFD.String(s)
}

View File

@ -15,14 +15,14 @@
// +build !windows,!darwin
package files
package osutil
import "code.google.com/p/go.text/unicode/norm"
func normalizedFilename(s string) string {
func NormalizedFilename(s string) string {
return norm.NFC.String(s)
}
func nativeFilename(s string) string {
func NativeFilename(s string) string {
return s
}

View File

@ -13,7 +13,7 @@
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package files
package osutil
import (
"path/filepath"
@ -21,10 +21,10 @@ import (
"code.google.com/p/go.text/unicode/norm"
)
func normalizedFilename(s string) string {
func NormalizedFilename(s string) string {
return norm.NFC.String(filepath.ToSlash(s))
}
func nativeFilename(s string) string {
func NativeFilename(s string) string {
return filepath.FromSlash(s)
}

View File

@ -37,7 +37,7 @@ func (f FileInfo) String() string {
}
func (f FileInfo) Size() (bytes int64) {
if IsDeleted(f.Flags) || IsDirectory(f.Flags) {
if f.IsDeleted() || f.IsDirectory() {
return 128
}
for _, b := range f.Blocks {
@ -47,15 +47,23 @@ func (f FileInfo) Size() (bytes int64) {
}
func (f FileInfo) IsDeleted() bool {
return IsDeleted(f.Flags)
return f.Flags&FlagDeleted != 0
}
func (f FileInfo) IsInvalid() bool {
return IsInvalid(f.Flags)
return f.Flags&FlagInvalid != 0
}
func (f FileInfo) IsDirectory() bool {
return IsDirectory(f.Flags)
return f.Flags&FlagDirectory != 0
}
func (f FileInfo) IsSymlink() bool {
return f.Flags&FlagSymlink != 0
}
func (f FileInfo) HasPermissionBits() bool {
return f.Flags&FlagNoPermBits == 0
}
// Used for unmarshalling a FileInfo structure but skipping the actual block list
@ -75,7 +83,7 @@ func (f FileInfoTruncated) String() string {
// Returns a statistical guess on the size, not the exact figure
func (f FileInfoTruncated) Size() int64 {
if IsDeleted(f.Flags) || IsDirectory(f.Flags) {
if f.IsDeleted() || f.IsDirectory() {
return 128
}
if f.NumBlocks < 2 {
@ -86,17 +94,32 @@ func (f FileInfoTruncated) Size() int64 {
}
func (f FileInfoTruncated) IsDeleted() bool {
return IsDeleted(f.Flags)
return f.Flags&FlagDeleted != 0
}
func (f FileInfoTruncated) IsInvalid() bool {
return IsInvalid(f.Flags)
return f.Flags&FlagInvalid != 0
}
func (f FileInfoTruncated) IsDirectory() bool {
return f.Flags&FlagDirectory != 0
}
func (f FileInfoTruncated) IsSymlink() bool {
return f.Flags&FlagSymlink != 0
}
func (f FileInfoTruncated) HasPermissionBits() bool {
return f.Flags&FlagNoPermBits == 0
}
type FileIntf interface {
Size() int64
IsDeleted() bool
IsInvalid() bool
IsDirectory() bool
IsSymlink() bool
HasPermissionBits() bool
}
type BlockInfo struct {

View File

@ -49,10 +49,14 @@ const (
)
const (
FlagDeleted uint32 = 1 << 12
FlagInvalid = 1 << 13
FlagDirectory = 1 << 14
FlagNoPermBits = 1 << 15
FlagDeleted uint32 = 1 << 12
FlagInvalid = 1 << 13
FlagDirectory = 1 << 14
FlagNoPermBits = 1 << 15
FlagSymlink = 1 << 16
FlagSymlinkMissingTarget = 1 << 17
SymlinkTypeMask = FlagDirectory | FlagSymlinkMissingTarget
)
const (
@ -637,19 +641,3 @@ func (c *rawConnection) Statistics() Statistics {
OutBytesTotal: c.cw.Tot(),
}
}
func IsDeleted(bits uint32) bool {
return bits&FlagDeleted != 0
}
func IsInvalid(bits uint32) bool {
return bits&FlagInvalid != 0
}
func IsDirectory(bits uint32) bool {
return bits&FlagDirectory != 0
}
func HasPermissionBits(bits uint32) bool {
return bits&FlagNoPermBits == 0
}

View File

@ -68,7 +68,7 @@ func HashFile(path string, blockSize int) ([]protocol.BlockInfo, error) {
func hashFiles(dir string, blockSize int, outbox, inbox chan protocol.FileInfo) {
for f := range inbox {
if protocol.IsDirectory(f.Flags) || protocol.IsDeleted(f.Flags) {
if f.IsDirectory() || f.IsDeleted() || f.IsSymlink() {
outbox <- f
continue
}

View File

@ -129,3 +129,18 @@ func Verify(r io.Reader, blocksize int, blocks []protocol.BlockInfo) error {
return nil
}
// BlockEqual returns whether two slices of blocks are exactly the same hash
// and index pair wise.
func BlocksEqual(src, tgt []protocol.BlockInfo) bool {
if len(tgt) != len(src) {
return false
}
for i, sblk := range src {
if !bytes.Equal(sblk.Hash, tgt[i].Hash) {
return false
}
}
return true
}

View File

@ -27,6 +27,7 @@ import (
"github.com/syncthing/syncthing/internal/ignore"
"github.com/syncthing/syncthing/internal/lamport"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syncthing/syncthing/internal/symlinks"
)
type Walker struct {
@ -131,11 +132,75 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun
return nil
}
// We must perform this check, as symlinks on Windows are always
// .IsRegular or .IsDir unlike on Unix.
// Index wise symlinks are always files, regardless of what the target
// is, because symlinks carry their target path as their content.
isSymlink, _ := symlinks.IsSymlink(p)
if isSymlink {
var rval error
// If the target is a directory, do NOT descend down there.
// This will cause files to get tracked, and removing the symlink
// will as a result remove files in their real location.
// But do not SkipDir if the target is not a directory, as it will
// stop scanning the current directory.
if info.IsDir() {
rval = filepath.SkipDir
}
// We always rehash symlinks as they have no modtime or
// permissions.
// We check if they point to the old target by checking that
// their existing blocks match with the blocks in the index.
// If we don't have a filer or don't support symlinks, skip.
if w.CurrentFiler == nil || !symlinks.Supported {
return rval
}
target, flags, err := symlinks.Read(p)
flags = flags & protocol.SymlinkTypeMask
if err != nil {
if debug {
l.Debugln("readlink error:", p, err)
}
return rval
}
blocks, err := Blocks(strings.NewReader(target), w.BlockSize, 0)
if err != nil {
if debug {
l.Debugln("hash link error:", p, err)
}
return rval
}
cf := w.CurrentFiler.CurrentFile(rn)
if !cf.IsDeleted() && cf.IsSymlink() && SymlinkTypeEqual(flags, cf.Flags) && BlocksEqual(cf.Blocks, blocks) {
return rval
}
f := protocol.FileInfo{
Name: rn,
Version: lamport.Default.Tick(0),
Flags: protocol.FlagSymlink | flags | protocol.FlagNoPermBits | 0666,
Modified: 0,
Blocks: blocks,
}
if debug {
l.Debugln("symlink to hash:", p, f)
}
fchan <- f
return rval
}
if info.Mode().IsDir() {
if w.CurrentFiler != nil {
cf := w.CurrentFiler.CurrentFile(rn)
permUnchanged := w.IgnorePerms || !protocol.HasPermissionBits(cf.Flags) || PermsEqual(cf.Flags, uint32(info.Mode()))
if !protocol.IsDeleted(cf.Flags) && protocol.IsDirectory(cf.Flags) && permUnchanged {
permUnchanged := w.IgnorePerms || !cf.HasPermissionBits() || PermsEqual(cf.Flags, uint32(info.Mode()))
if !cf.IsDeleted() && cf.IsDirectory() && permUnchanged {
return nil
}
}
@ -162,8 +227,8 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun
if info.Mode().IsRegular() {
if w.CurrentFiler != nil {
cf := w.CurrentFiler.CurrentFile(rn)
permUnchanged := w.IgnorePerms || !protocol.HasPermissionBits(cf.Flags) || PermsEqual(cf.Flags, uint32(info.Mode()))
if !protocol.IsDeleted(cf.Flags) && cf.Modified == info.ModTime().Unix() && permUnchanged {
permUnchanged := w.IgnorePerms || !cf.HasPermissionBits() || PermsEqual(cf.Flags, uint32(info.Mode()))
if !cf.IsDeleted() && cf.Modified == info.ModTime().Unix() && permUnchanged {
return nil
}
@ -215,3 +280,19 @@ func PermsEqual(a, b uint32) bool {
return a&0777 == b&0777
}
}
// If the target is missing, Unix never knows what type of symlink it is
// and Windows always knows even if there is no target.
// Which means that without this special check a Unix node would be fighting
// with a Windows node about whether or not the target is known.
// Basically, if you don't know and someone else knows, just accept it.
// The fact that you don't know means you are on Unix, and on Unix you don't
// really care what the target type is. The moment you do know, and if something
// doesn't match, that will propogate throught the cluster.
func SymlinkTypeEqual(disk, index uint32) bool {
if disk&protocol.FlagSymlinkMissingTarget != 0 && index&protocol.FlagSymlinkMissingTarget == 0 {
return true
}
return disk&protocol.SymlinkTypeMask == index&protocol.SymlinkTypeMask
}

View File

@ -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 <http://www.gnu.org/licenses/>.
// +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
}

View File

@ -0,0 +1,203 @@
// 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 <http://www.gnu.org/licenses/>.
// +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() {
// 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)
}

View File

@ -439,7 +439,7 @@ The Flags field is made up of the following single bit flags:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reserved |P|I|D| Unix Perm. & Mode |
| Reserved |U|S|P|I|D| Unix Perm. & Mode |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- The lower 12 bits hold the common Unix permission and mode bits. An
@ -461,7 +461,16 @@ The Flags field is made up of the following single bit flags:
disregarded on files with this bit set. The permissions bits MUST be
set to the octal value 0666.
- Bit 0 through 16 are reserved for future use and SHALL be set to
- Bit 16 ("S") is set when the file is a symbolic link. The block list
SHALL be of one or more blocks since the target of the symlink is
stored within the blocks of the file.
- Bit 15 ("U") is set when the symbolic links target does not exist.
On systems where symbolic links have types, this bit being means
that the default file symlink SHALL be used. If this bit is unset
bit 19 will decide the type of symlink to be created.
- Bit 0 through 14 are reserved for future use and SHALL be set to
zero.
The hash algorithm is implied by the Hash length. Currently, the hash

View File

@ -30,6 +30,8 @@ import (
"os/exec"
"path/filepath"
"time"
"github.com/syncthing/syncthing/internal/symlinks"
)
func init() {
@ -355,7 +357,22 @@ func startWalker(dir string, res chan<- fileInfo, abort <-chan struct{}) {
}
var f fileInfo
if info.IsDir() {
if ok, err := symlinks.IsSymlink(path); err == nil && ok {
f = fileInfo{
name: rn,
mode: os.ModeSymlink,
}
tgt, _, err := symlinks.Read(path)
if err != nil {
return err
}
h := md5.New()
h.Write([]byte(tgt))
hash := h.Sum(nil)
copy(f.hash[:], hash)
} else if info.IsDir() {
f = fileInfo{
name: rn,
mode: info.Mode(),

280
test/symlink_test.go Normal file
View File

@ -0,0 +1,280 @@
// 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 <http://www.gnu.org/licenses/>.
// +build integration
package integration_test
import (
"log"
"os"
"strings"
"testing"
"time"
"github.com/syncthing/syncthing/internal/symlinks"
)
func TestSymlinks(t *testing.T) {
log.Println("Cleaning...")
err := removeAll("s1", "s2", "h1/index", "h2/index")
if err != nil {
t.Fatal(err)
}
log.Println("Generating files...")
err = generateFiles("s1", 100, 20, "../bin/syncthing")
if err != nil {
t.Fatal(err)
}
// A file that we will replace with a symlink later
fd, err := os.Create("s1/fileToReplace")
if err != nil {
t.Fatal(err)
}
fd.Close()
// A directory that we will replace with a symlink later
err = os.Mkdir("s1/dirToReplace", 0755)
if err != nil {
t.Fatal(err)
}
// A file and a symlink to that file
fd, err = os.Create("s1/file")
if err != nil {
t.Fatal(err)
}
fd.Close()
err = symlinks.Create("s1/fileLink", "file", 0)
if err != nil {
log.Fatal(err)
}
// A directory and a symlink to that directory
err = os.Mkdir("s1/dir", 0755)
if err != nil {
t.Fatal(err)
}
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("s1/noneLink", "does/not/exist", 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)
if err != nil {
log.Fatal(err)
}
// A link we will replace with a directory later
err = symlinks.Create("s1/repDirLink", "does/not/exist", 0)
if err != nil {
log.Fatal(err)
}
// Verify that the files and symlinks sync to the other side
log.Println("Syncing...")
sender := syncthingProcess{ // id1
log: "1.out",
argv: []string{"-home", "h1"},
port: 8081,
apiKey: apiKey,
}
err = sender.start()
if err != nil {
t.Fatal(err)
}
receiver := syncthingProcess{ // id2
log: "2.out",
argv: []string{"-home", "h2"},
port: 8082,
apiKey: apiKey,
}
err = receiver.start()
if err != nil {
sender.stop()
t.Fatal(err)
}
for {
comp, err := sender.peerCompletion()
if err != nil {
if strings.Contains(err.Error(), "use of closed network connection") {
time.Sleep(time.Second)
continue
}
sender.stop()
receiver.stop()
t.Fatal(err)
}
curComp := comp[id2]
if curComp == 100 {
sender.stop()
receiver.stop()
break
}
time.Sleep(time.Second)
}
sender.stop()
receiver.stop()
log.Println("Comparing directories...")
err = compareDirectories("s1", "s2")
if err != nil {
t.Fatal(err)
}
log.Println("Making some changes...")
// Remove one symlink
err = os.Remove("s1/fileLink")
if err != nil {
log.Fatal(err)
}
// Change the target of another
err = os.Remove("s1/dirLink")
if err != nil {
log.Fatal(err)
}
err = symlinks.Create("s1/dirLink", "file", 0)
if err != nil {
log.Fatal(err)
}
// Replace one with a file
err = os.Remove("s1/repFileLink")
if err != nil {
log.Fatal(err)
}
fd, err = os.Create("s1/repFileLink")
if err != nil {
log.Fatal(err)
}
fd.Close()
/* Currently fails, to be fixed with #80
// Replace one with a directory
err = os.Remove("s1/repDirLink")
if err != nil {
log.Fatal(err)
}
err = os.Mkdir("s1/repDirLink", 0755)
if err != nil {
log.Fatal(err)
}
*/
// Replace a file with a symlink
err = os.Remove("s1/fileToReplace")
if err != nil {
log.Fatal(err)
}
err = symlinks.Create("s1/fileToReplace", "somewhere/non/existent", 0)
if err != nil {
log.Fatal(err)
}
/* Currently fails, to be fixed with #80
// Replace a directory with a symlink
err = os.RemoveAll("s1/dirToReplace")
if err != nil {
log.Fatal(err)
}
err = symlinks.Create("s1/dirToReplace", "somewhere/non/existent", 0)
if err != nil {
log.Fatal(err)
}
*/
// Sync these changes and recheck
log.Println("Syncing...")
err = sender.start()
if err != nil {
t.Fatal(err)
}
err = receiver.start()
if err != nil {
sender.stop()
t.Fatal(err)
}
for {
comp, err := sender.peerCompletion()
if err != nil {
if strings.Contains(err.Error(), "use of closed network connection") {
time.Sleep(time.Second)
continue
}
sender.stop()
receiver.stop()
t.Fatal(err)
}
curComp := comp[id2]
if curComp == 100 {
sender.stop()
receiver.stop()
break
}
time.Sleep(time.Second)
}
sender.stop()
receiver.stop()
log.Println("Comparing directories...")
err = compareDirectories("s1", "s2")
if err != nil {
t.Fatal(err)
}
}