From 938e287501c12c9700219f6e31c8bf411916e031 Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Tue, 4 Nov 2014 23:22:15 +0000 Subject: [PATCH 1/8] Code smell --- internal/model/model.go | 4 ++-- internal/model/puller.go | 4 ++-- internal/protocol/message.go | 28 +++++++++++++++++++++------- internal/protocol/protocol.go | 16 ---------------- internal/scanner/blockqueue.go | 2 +- internal/scanner/walk.go | 8 ++++---- 6 files changed, 30 insertions(+), 32 deletions(-) diff --git a/internal/model/model.go b/internal/model/model.go index 2927490e..5cec18cc 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -655,7 +655,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) } @@ -1085,7 +1085,7 @@ func (m *Model) ScanFolderSub(folder, sub string) error { } seenPrefix = true - if !protocol.IsDeleted(f.Flags) { + if !f.IsDeleted() { if f.IsInvalid() { return true } diff --git a/internal/model/puller.go b/internal/model/puller.go index 976995b5..3f34ef2f 100644 --- a/internal/model/puller.go +++ b/internal/model/puller.go @@ -313,10 +313,10 @@ func (p *Puller) pullerIteration(ncopiers, npullers, nfinishers int, checksum bo } switch { - case protocol.IsDeleted(file.Flags): + case file.IsDeleted(): // A deleted file or directory deletions = append(deletions, file) - case protocol.IsDirectory(file.Flags): + case file.IsDirectory(): // A new or changed directory p.handleDir(file) default: diff --git a/internal/protocol/message.go b/internal/protocol/message.go index eff837fe..5d35f17c 100644 --- a/internal/protocol/message.go +++ b/internal/protocol/message.go @@ -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,19 @@ 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) HasPermissionBits() bool { + return f.Flags&FlagNoPermBits == 0 } // Used for unmarshalling a FileInfo structure but skipping the actual block list @@ -75,7 +79,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 +90,27 @@ 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) HasPermissionBits() bool { + return f.Flags&FlagNoPermBits == 0 } type FileIntf interface { Size() int64 IsDeleted() bool IsInvalid() bool + IsDirectory() bool + HasPermissionBits() bool } type BlockInfo struct { diff --git a/internal/protocol/protocol.go b/internal/protocol/protocol.go index ae7f480b..7de53eab 100644 --- a/internal/protocol/protocol.go +++ b/internal/protocol/protocol.go @@ -637,19 +637,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 -} diff --git a/internal/scanner/blockqueue.go b/internal/scanner/blockqueue.go index 9d75dac7..cd0a0a11 100644 --- a/internal/scanner/blockqueue.go +++ b/internal/scanner/blockqueue.go @@ -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() { outbox <- f continue } diff --git a/internal/scanner/walk.go b/internal/scanner/walk.go index 927a41e8..bd3ecc4f 100644 --- a/internal/scanner/walk.go +++ b/internal/scanner/walk.go @@ -134,8 +134,8 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun 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 +162,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 } From c891999e1d718f730f941f0f65bd98f41e4be54c Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Wed, 5 Nov 2014 23:41:51 +0000 Subject: [PATCH 2/8] Move filename conversion into osutil --- internal/files/blockmap.go | 3 ++- internal/files/set.go | 17 +++++++++-------- internal/{files => osutil}/filenames_darwin.go | 6 +++--- internal/{files => osutil}/filenames_unix.go | 6 +++--- internal/{files => osutil}/filenames_windows.go | 6 +++--- 5 files changed, 20 insertions(+), 18 deletions(-) rename internal/{files => osutil}/filenames_darwin.go (89%) rename internal/{files => osutil}/filenames_unix.go (89%) rename internal/{files => osutil}/filenames_windows.go (89%) diff --git a/internal/files/blockmap.go b/internal/files/blockmap.go index d47fe9db..618f44f4 100644 --- a/internal/files/blockmap.go +++ b/internal/files/blockmap.go @@ -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 } } diff --git a/internal/files/set.go b/internal/files/set.go index 01cc517c..730a55d8 100644 --- a/internal/files/set.go +++ b/internal/files/set.go @@ -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") diff --git a/internal/files/filenames_darwin.go b/internal/osutil/filenames_darwin.go similarity index 89% rename from internal/files/filenames_darwin.go rename to internal/osutil/filenames_darwin.go index 4d2559cc..ce8c4085 100644 --- a/internal/files/filenames_darwin.go +++ b/internal/osutil/filenames_darwin.go @@ -13,14 +13,14 @@ // You should have received a copy of the GNU General Public License along // with this program. If not, see . -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) } diff --git a/internal/files/filenames_unix.go b/internal/osutil/filenames_unix.go similarity index 89% rename from internal/files/filenames_unix.go rename to internal/osutil/filenames_unix.go index 573069c1..7741f617 100644 --- a/internal/files/filenames_unix.go +++ b/internal/osutil/filenames_unix.go @@ -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 } diff --git a/internal/files/filenames_windows.go b/internal/osutil/filenames_windows.go similarity index 89% rename from internal/files/filenames_windows.go rename to internal/osutil/filenames_windows.go index 34a37a9e..88ba71ba 100644 --- a/internal/files/filenames_windows.go +++ b/internal/osutil/filenames_windows.go @@ -13,7 +13,7 @@ // You should have received a copy of the GNU General Public License along // with this program. If not, see . -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) } From bf898f10fb20b408577879e13602dee3ff9252cc Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Fri, 7 Nov 2014 23:06:04 +0000 Subject: [PATCH 3/8] Add symlink support at the protocol level --- internal/protocol/message.go | 9 +++++++++ internal/protocol/protocol.go | 12 ++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/internal/protocol/message.go b/internal/protocol/message.go index 5d35f17c..355050ca 100644 --- a/internal/protocol/message.go +++ b/internal/protocol/message.go @@ -58,6 +58,10 @@ func (f FileInfo) IsDirectory() bool { 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 } @@ -101,6 +105,10 @@ 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 } @@ -110,6 +118,7 @@ type FileIntf interface { IsDeleted() bool IsInvalid() bool IsDirectory() bool + IsSymlink() bool HasPermissionBits() bool } diff --git a/internal/protocol/protocol.go b/internal/protocol/protocol.go index 7de53eab..fb51a1c0 100644 --- a/internal/protocol/protocol.go +++ b/internal/protocol/protocol.go @@ -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 ( From 6e88d9688b076807a8060b494cd197b4cd582ffe Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Sat, 8 Nov 2014 13:55:44 +0000 Subject: [PATCH 4/8] Implement symlinks package --- internal/symlinks/symlink_unix.go | 58 ++++++++ internal/symlinks/symlink_windows.go | 203 +++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 internal/symlinks/symlink_unix.go create mode 100644 internal/symlinks/symlink_windows.go diff --git a/internal/symlinks/symlink_unix.go b/internal/symlinks/symlink_unix.go new file mode 100644 index 00000000..c47e822f --- /dev/null +++ b/internal/symlinks/symlink_unix.go @@ -0,0 +1,58 @@ +// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file). +// +// 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 00000000..379c7e9a --- /dev/null +++ b/internal/symlinks/symlink_windows.go @@ -0,0 +1,203 @@ +// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file). +// +// 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() { + // 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) +} From c325ffd0f8c483267cdc723ab7656c6716975548 Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Sun, 9 Nov 2014 04:26:52 +0000 Subject: [PATCH 5/8] Add symlink support (fixes #873) --- internal/model/model.go | 53 ++++++++++++++++------ internal/model/puller.go | 56 +++++++++++++++++------ internal/scanner/blockqueue.go | 2 +- internal/scanner/blocks.go | 15 +++++++ internal/scanner/walk.go | 81 ++++++++++++++++++++++++++++++++++ 5 files changed, 179 insertions(+), 28 deletions(-) diff --git a/internal/model/model.go b/internal/model/model.go index 5cec18cc..73d10b60 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -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] @@ -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 } @@ -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 +} diff --git a/internal/model/puller.go b/internal/model/puller.go index 3f34ef2f..d4fbdc8c 100644 --- a/internal/model/puller.go +++ b/internal/model/puller.go @@ -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" ) @@ -314,14 +316,15 @@ func (p *Puller) pullerIteration(ncopiers, npullers, nfinishers int, checksum bo switch { case file.IsDeleted(): - // A deleted file or directory + // A deleted file, directory or symlink deletions = append(deletions, file) - case file.IsDirectory(): + 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) } diff --git a/internal/scanner/blockqueue.go b/internal/scanner/blockqueue.go index cd0a0a11..2d1a8656 100644 --- a/internal/scanner/blockqueue.go +++ b/internal/scanner/blockqueue.go @@ -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 f.IsDirectory() || f.IsDeleted() { + if f.IsDirectory() || f.IsDeleted() || f.IsSymlink() { outbox <- f continue } diff --git a/internal/scanner/blocks.go b/internal/scanner/blocks.go index 825c1250..2f28b1fe 100644 --- a/internal/scanner/blocks.go +++ b/internal/scanner/blocks.go @@ -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 +} diff --git a/internal/scanner/walk.go b/internal/scanner/walk.go index bd3ecc4f..f2efda59 100644 --- a/internal/scanner/walk.go +++ b/internal/scanner/walk.go @@ -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,6 +132,70 @@ 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) @@ -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 + +} From 20ba0bf4ed77569aea8d84d42a7bccfe7926933e Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Wed, 12 Nov 2014 00:13:57 +0000 Subject: [PATCH 6/8] Update PROTOCOL.md --- protocol/PROTOCOL.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/protocol/PROTOCOL.md b/protocol/PROTOCOL.md index faf2cb58..7d31075f 100644 --- a/protocol/PROTOCOL.md +++ b/protocol/PROTOCOL.md @@ -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 From ce5651f5fa53896487067ba70c6616952e2f7391 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Wed, 19 Nov 2014 08:18:19 +0400 Subject: [PATCH 7/8] Integration tests for symlinks --- test/common_test.go | 19 ++- test/symlink_test.go | 280 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 test/symlink_test.go diff --git a/test/common_test.go b/test/common_test.go index b5fc6a16..6a3275f6 100644 --- a/test/common_test.go +++ b/test/common_test.go @@ -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(), diff --git a/test/symlink_test.go b/test/symlink_test.go new file mode 100644 index 00000000..1d100f04 --- /dev/null +++ b/test/symlink_test.go @@ -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 . + +// +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) + } +} From 1e2d151684b2a7e03c71d22432faf948bdda6d11 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Thu, 20 Nov 2014 16:33:16 +0100 Subject: [PATCH 8/8] Copyright notice update --- internal/symlinks/symlink_unix.go | 2 +- internal/symlinks/symlink_windows.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/symlinks/symlink_unix.go b/internal/symlinks/symlink_unix.go index c47e822f..814c7411 100644 --- a/internal/symlinks/symlink_unix.go +++ b/internal/symlinks/symlink_unix.go @@ -1,4 +1,4 @@ -// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file). +// 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 diff --git a/internal/symlinks/symlink_windows.go b/internal/symlinks/symlink_windows.go index 379c7e9a..1a99e3db 100644 --- a/internal/symlinks/symlink_windows.go +++ b/internal/symlinks/symlink_windows.go @@ -1,4 +1,4 @@ -// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file). +// 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