diff --git a/lib/model/rwfolder.go b/lib/model/rwfolder.go index 7b06b2454..8534f18d0 100644 --- a/lib/model/rwfolder.go +++ b/lib/model/rwfolder.go @@ -1002,27 +1002,6 @@ func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- c tempName := ignore.TempName(file.Name) - if hasCurFile && !curFile.IsDirectory() && !curFile.IsSymlink() { - // Check that the file on disk is what we expect it to be according to - // the database. If there's a mismatch here, there might be local - // changes that we don't know about yet and we should scan before - // touching the file. If we can't stat the file we'll just pull it. - if info, err := f.fs.Lstat(file.Name); err == nil { - if !info.ModTime().Equal(curFile.ModTime()) || info.Size() != curFile.Size { - l.Debugln("file modified but not rescanned; not pulling:", file.Name) - // Scan() is synchronous (i.e. blocks until the scan is - // completed and returns an error), but a scan can't happen - // while we're in the puller routine. Request the scan in the - // background and it'll be handled when the current pulling - // sweep is complete. As we do retries, we'll queue the scan - // for this file up to ten times, but the last nine of those - // scans will be cheap... - go f.Scan([]string{file.Name}) - return - } - } - } - scanner.PopulateOffsets(file.Blocks) var blocks []protocol.BlockInfo @@ -1101,7 +1080,8 @@ func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- c available: reused, availableUpdated: time.Now(), ignorePerms: f.ignorePermissions(file), - version: curFile.Version, + hasCurFile: hasCurFile, + curFile: curFile, mut: sync.NewRWMutex(), sparse: !f.DisableSparseFiles, created: time.Now(), @@ -1386,6 +1366,36 @@ func (f *sendReceiveFolder) performFinish(state *sharedPullerState) error { // There is an old file or directory already in place. We need to // handle that. + curMode := uint32(stat.Mode()) + if runtime.GOOS == "windows" && osutil.IsWindowsExecutable(state.file.Name) { + curMode |= 0111 + } + + // Check that the file on disk is what we expect it to be according to + // the database. If there's a mismatch here, there might be local + // changes that we don't know about yet and we should scan before + // touching the file. + // There is also a case where we think the file should be there, but + // it was removed, which is a conflict, yet creations always wins when + // competing with a deletion, so no need to handle that specially. + switch { + // The file reappeared from nowhere, or mtime/size has changed, fallthrough -> rescan. + case !state.hasCurFile || !stat.ModTime().Equal(state.curFile.ModTime()) || stat.Size() != state.curFile.Size: + fallthrough + // Permissions have changed, means the file has changed, rescan. + case !f.ignorePermissions(state.curFile) && state.curFile.HasPermissionBits() && !scanner.PermsEqual(state.curFile.Permissions, curMode): + l.Debugln("file modified but not rescanned; not finishing:", state.curFile.Name) + // Scan() is synchronous (i.e. blocks until the scan is + // completed and returns an error), but a scan can't happen + // while we're in the puller routine. Request the scan in the + // background and it'll be handled when the current pulling + // sweep is complete. As we do retries, we'll queue the scan + // for this file up to ten times, but the last nine of those + // scans will be cheap... + go f.Scan([]string{state.curFile.Name}) + return nil + } + switch { case stat.IsDir() || stat.IsSymlink(): // It's a directory or a symlink. These are not versioned or @@ -1400,13 +1410,13 @@ func (f *sendReceiveFolder) performFinish(state *sharedPullerState) error { return err } - case f.inConflict(state.version, state.file.Version): + case f.inConflict(state.curFile.Version, state.file.Version): // The new file has been changed in conflict with the existing one. We // should file it away as a conflict instead of just removing or // archiving. Also merge with the version vector we had, to indicate // we have resolved the conflict. - state.file.Version = state.file.Version.Merge(state.version) + state.file.Version = state.file.Version.Merge(state.curFile.Version) err = osutil.InWritableDir(func(name string) error { return f.moveForConflict(name, state.file.ModifiedBy.String()) }, f.fs, state.file.Name) diff --git a/lib/model/sharedpullerstate.go b/lib/model/sharedpullerstate.go index c29bedab1..3ecf71ab8 100644 --- a/lib/model/sharedpullerstate.go +++ b/lib/model/sharedpullerstate.go @@ -27,7 +27,8 @@ type sharedPullerState struct { realName string reused int // Number of blocks reused from temporary file ignorePerms bool - version protocol.Vector // The current (old) version + hasCurFile bool // Whether curFile is set + curFile protocol.FileInfo // The file as it exists now in our database sparse bool created time.Time