Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check file names for conflicts on Windows #3810

Closed
wants to merge 13 commits into from
6 changes: 6 additions & 0 deletions cmd/syncthing/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,12 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
ldb.DropDeltaIndexIDs()
}

if cfg.RawCopy().OriginalVersion <= 17 {
// The config version 17->18 migration is about tracking folder versions
// in the database
ldb.AddFolderVersions()
}

m := model.NewModel(cfg, myID, myDeviceName(cfg), "syncthing", Version, ldb, protectedFiles)

if t := os.Getenv("STDEADLOCKTIMEOUT"); len(t) > 0 {
Expand Down
10 changes: 9 additions & 1 deletion lib/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (

const (
OldestHandledVersion = 10
CurrentVersion = 17
CurrentVersion = 18
MaxRescanIntervalS = 365 * 24 * 60 * 60
)

Expand Down Expand Up @@ -257,6 +257,9 @@ func (cfg *Configuration) clean() error {
if cfg.Version == 16 {
convertV16V17(cfg)
}
if cfg.Version == 17 {
convertV17V18(cfg)
}

// Build a list of available devices
existingDevices := make(map[protocol.DeviceID]bool)
Expand Down Expand Up @@ -338,6 +341,11 @@ func convertV16V17(cfg *Configuration) {
cfg.Version = 17
}

func convertV17V18(cfg *Configuration) {
// Triggers a database tweak
cfg.Version = 18
}

func convertV13V14(cfg *Configuration) {
// Not using the ignore cache is the new default. Disable it on existing
// configurations.
Expand Down
15 changes: 15 additions & 0 deletions lib/config/testdata/v18.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<configuration version="18">
<folder id="test" path="testdata" type="readonly" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
<minDiskFreePct>1</minDiskFreePct>
<maxConflicts>-1</maxConflicts>
<fsync>true</fsync>
</folder>
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="metadata">
<address>tcp://a</address>
</device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="metadata">
<address>tcp://b</address>
</device>
</configuration>
1 change: 1 addition & 0 deletions lib/db/leveldb.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
KeyTypeFolderIdx
KeyTypeDeviceIdx
KeyTypeIndexID
KeyTypeFolderVersion
)

func (l VersionList) String() string {
Expand Down
59 changes: 59 additions & 0 deletions lib/db/leveldb_dbinstance.go
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,32 @@ func (db *Instance) setIndexID(device, folder []byte, id protocol.IndexID) {
}
}

func (db *Instance) getFolderVersion(folder []byte) uint64 {
key := db.folderVersionKey(folder)
cur, err := db.Get(key, nil)
if err != nil {
return 0
}
if len(cur) != 8 {
return 0
}
return binary.BigEndian.Uint64(cur)
}

func (db *Instance) setFolderVersion(folder []byte, version uint64) {
key := db.folderVersionKey(folder)
bs := make([]byte, 8)
binary.BigEndian.PutUint64(bs, version)
if err := db.Put(key, bs, nil); err != nil {
panic("storing folder version: " + err.Error())
}
}

func (db *Instance) dropFolderVersion(folder []byte) {
key := db.folderVersionKey(folder)
db.dropPrefix(key)
}

func (db *Instance) indexIDKey(device, folder []byte) []byte {
k := make([]byte, keyPrefixLen+keyDeviceLen+keyFolderLen)
k[0] = KeyTypeIndexID
Expand All @@ -722,12 +748,45 @@ func (db *Instance) mtimesKey(folder []byte) []byte {
return prefix
}

func (db *Instance) folderVersionKey(folder []byte) []byte {
k := make([]byte, keyPrefixLen+keyFolderLen)
k[0] = KeyTypeFolderVersion
binary.BigEndian.PutUint32(k[keyPrefixLen:], db.folderIdx.ID(folder))
return k
}

// DropDeltaIndexIDs removes all index IDs from the database. This will
// cause a full index transmission on the next connection.
func (db *Instance) DropDeltaIndexIDs() {
db.dropPrefix([]byte{KeyTypeIndexID})
}

func (db *Instance) AddFolderVersions() {
t := db.newReadOnlyTransaction()
defer t.close()

// Set folder version of all found folders to 1
done := make(map[string]bool)
k := make([]byte, keyPrefixLen+keyFolderLen)
k[0] = KeyTypeFolderVersion
bs := make([]byte, 8)
binary.BigEndian.PutUint64(bs, 1)
dbi := t.NewIterator(util.BytesPrefix([]byte{KeyTypeDevice}), nil)
for dbi.Next() {
keyFolder := dbi.Key()[keyPrefixLen : keyPrefixLen+keyFolderLen]
if !done[string(keyFolder)] {
copy(k[keyPrefixLen:], keyFolder)
if _, err := db.Get(k, nil); err == leveldb.ErrNotFound {
db.Put(k, bs, nil)
} else if err != nil {
panic(err)
}
done[string(keyFolder)] = true
}
}
dbi.Release()
}

func (db *Instance) dropMtimes(folder []byte) {
db.dropPrefix(db.mtimesKey(folder))
}
Expand Down
82 changes: 79 additions & 3 deletions lib/db/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@ import (
"github.com/syncthing/syncthing/lib/sync"
)

const (
CurrentFolderVersion = 2
)

type FileSet struct {
sequence int64 // Our local sequence number
folder string
version uint64
db *Instance
blockmap *BlockMap
localSize sizeTracker
Expand Down Expand Up @@ -117,6 +122,7 @@ func NewFileSet(folder string, db *Instance) *FileSet {
var s = FileSet{
remoteSequence: make(map[protocol.DeviceID]int64),
folder: folder,
version: db.getFolderVersion([]byte(folder)),
db: db,
blockmap: NewBlockMap(db, db.folderIdx.ID([]byte(folder))),
updateMutex: sync.NewMutex(),
Expand Down Expand Up @@ -171,12 +177,15 @@ func (s *FileSet) Replace(device protocol.DeviceID, fs []protocol.FileInfo) {
}

func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
l.Debugf("%s Update(%v, [%d])", s.folder, device, len(fs))
normalizeFilenames(fs)

s.updateMutex.Lock()
defer s.updateMutex.Unlock()
s.updateLocked(device, fs)
}

func (s *FileSet) updateLocked(device protocol.DeviceID, fs []protocol.FileInfo) {
l.Debugf("%s Update(%v, [%d])", s.folder, device, len(fs))

normalizeFilenames(fs)
if device == protocol.LocalDeviceID {
discards := make([]protocol.FileInfo, 0, len(fs))
updates := make([]protocol.FileInfo, 0, len(fs))
Expand Down Expand Up @@ -318,6 +327,72 @@ func (s *FileSet) ListDevices() []protocol.DeviceID {
return devices
}

func (s *FileSet) UpdateFolderVersion(checkFolderHealth func() error, checkNameConflict func(string) bool) error {
if s.version == CurrentFolderVersion {
return nil
}
s.updateMutex.Lock()
defer s.updateMutex.Unlock()
defer s.db.setFolderVersion([]byte(s.folder), s.version)
if s.version == 0 {
s.version = CurrentFolderVersion
}
if s.version == 1 {
if err := s.convertV1V2(checkFolderHealth, checkNameConflict); err != nil {
return err
}
}
return nil
}

func (s *FileSet) convertV1V2(checkFolderHealth func() error, checkNameConflict func(string) bool) error {
batchSizeFiles := 100
batch := make([]protocol.FileInfo, 0, batchSizeFiles)
var iterError error
s.WithHaveTruncated(protocol.LocalDeviceID, func(fi FileIntf) bool {
f := fi.(FileInfoTruncated)
if len(batch) == batchSizeFiles {
if err := checkFolderHealth(); err != nil {
iterError = err
return false
}
s.updateLocked(protocol.LocalDeviceID, batch)
batch = batch[:0]
}

if !f.IsInvalid() && !f.IsDeleted() && !checkNameConflict(f.Name) {
// Mark conflicting files as invalid to prevent deletion on other devices
l.Debugln("setting invalid bit on conflicting", f)
nf := protocol.FileInfo{
Name: f.Name,
Type: f.Type,
Size: f.Size,
ModifiedS: f.ModifiedS,
ModifiedNs: f.ModifiedNs,
Permissions: f.Permissions,
NoPermissions: f.NoPermissions,
Invalid: true,
Version: f.Version, // The file is still the same, so don't bump version
}
batch = append(batch, nf)
}
return true
})

if iterError != nil {
return iterError
}

if err := checkFolderHealth(); err != nil {
return err
} else if len(batch) > 0 {
s.updateLocked(protocol.LocalDeviceID, batch)
}

s.version = 2
return nil
}

// maxSequence returns the highest of the Sequence numbers found in
// the given slice of FileInfos. This should really be the Sequence of
// the last item, but Syncthing v0.14.0 and other implementations may not
Expand All @@ -337,6 +412,7 @@ func maxSequence(fs []protocol.FileInfo) int64 {
func DropFolder(db *Instance, folder string) {
db.dropFolder([]byte(folder))
db.dropMtimes([]byte(folder))
db.dropFolderVersion([]byte(folder))
bm := &BlockMap{
db: db,
folder: db.folderIdx.ID([]byte(folder)),
Expand Down
27 changes: 26 additions & 1 deletion lib/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ var (
errDevicePaused = errors.New("device is paused")
errDeviceIgnored = errors.New("device is ignored")
errNotRelative = errors.New("not a relative path")
errNameConflict = errors.New("filename collides with existing file")
)

// NewModel creates and starts a new model. The model starts in read-only mode,
Expand Down Expand Up @@ -1163,6 +1164,11 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
return protocol.ErrNoSuchFile
}

if !osutil.CheckNameConflict(folderPath, name) {
l.Debugf("%v REQ(in) for file not in dir: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
return protocol.ErrNoSuchFile
}

// Only check temp files if the flag is set, and if we are set to advertise
// the temp indexes.
if fromTemporary && !folderCfg.DisableTempIndexes {
Expand Down Expand Up @@ -1783,6 +1789,15 @@ func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error
return err
}

if err := fs.UpdateFolderVersion(func() error {
return m.CheckFolderHealth(folder)
}, func(name string) bool {
return osutil.CheckNameConflict(folderCfg.Path(), name)
}); err != nil {
l.Infof("Stopping folder %s mid-update due to folder error: %s", folderCfg.Description(), err)
return err
}

// Clean the list of subitems to ensure that we start at a known
// directory, and don't scan subdirectories of things we've already
// scanned.
Expand Down Expand Up @@ -1899,14 +1914,24 @@ func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error
// The file is valid and not deleted. Lets check if it's
// still here.

if _, err := mtimefs.Lstat(filepath.Join(folderCfg.Path(), f.Name)); err != nil {
var exists bool
if err := osutil.TraversesSymlink(folderCfg.Path(), filepath.Dir(f.Name)); err != nil {
exists = false
} else if !osutil.CheckNameConflict(folderCfg.Path(), f.Name) {
exists = false
} else if _, err := mtimefs.Lstat(filepath.Join(folderCfg.Path(), f.Name)); err != nil {
// We don't specifically verify that the error is
// os.IsNotExist because there is a corner case when a
// directory is suddenly transformed into a file. When that
// happens, files that were in the directory (that is now a
// file) are deleted but will return a confusing error ("not a
// directory") when we try to Lstat() them.

exists = false
} else {
exists = true
}
if !exists {
nf := protocol.FileInfo{
Name: f.Name,
Type: f.Type,
Expand Down