lib/model, gui: Allow creating and editing ignores of paused folders (fixes #3608)

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3996
LGTM: calmh, AudriusButkevicius
This commit is contained in:
Simon Frei 2017-04-01 09:58:06 +00:00 committed by Audrius Butkevicius
parent c5e0c47989
commit 25b314f5f1
12 changed files with 202 additions and 146 deletions

View File

@ -969,7 +969,9 @@ func (s *apiService) getRandomString(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getDBIgnores(w http.ResponseWriter, r *http.Request) { func (s *apiService) getDBIgnores(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query() qs := r.URL.Query()
ignores, patterns, err := s.model.GetIgnores(qs.Get("folder")) folder := qs.Get("folder")
ignores, patterns, err := s.model.GetIgnores(folder)
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return

View File

@ -43,6 +43,7 @@
"Copied from original": "Copied from original", "Copied from original": "Copied from original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:", "Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 the following Contributors:", "Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 the following Contributors:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creating ignore patterns, overwriting an existing file at {{path}}.",
"Danger!": "Danger!", "Danger!": "Danger!",
"Deleted": "Deleted", "Deleted": "Deleted",
"Device": "Device", "Device": "Device",
@ -63,6 +64,7 @@
"Edit Device": "Edit Device", "Edit Device": "Edit Device",
"Edit Folder": "Edit Folder", "Edit Folder": "Edit Folder",
"Editing": "Editing", "Editing": "Editing",
"Editing {%path%}.": "Editing {{path}}.",
"Enable NAT traversal": "Enable NAT traversal", "Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying", "Enable Relaying": "Enable Relaying",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.", "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",

View File

@ -58,6 +58,24 @@ angular.module('syncthing.core')
$scope.metricRates = (window.localStorage["metricRates"] == "true"); $scope.metricRates = (window.localStorage["metricRates"] == "true");
} catch (exception) { } } catch (exception) { }
$scope.folderDefaults = {
selectedDevices: {},
type: "readwrite",
rescanIntervalS: 60,
minDiskFreePct: 1,
maxConflicts: 10,
fsync: true,
order: "random",
fileVersioningSelector: "none",
trashcanClean: 0,
simpleKeep: 5,
staggeredMaxAge: 365,
staggeredCleanInterval: 3600,
staggeredVersionsPath: "",
externalCommand: "",
autoNormalize: true
};
$scope.localStateTotal = { $scope.localStateTotal = {
bytes: 0, bytes: 0,
files: 0 files: 0
@ -1393,24 +1411,9 @@ angular.module('syncthing.core')
}; };
$scope.addFolder = function () { $scope.addFolder = function () {
$scope.currentFolder = { $scope.currentFolder = angular.copy($scope.folderDefaults);
selectedDevices: {},
type: "readwrite",
rescanIntervalS: 60,
minDiskFreePct: 1,
maxConflicts: 10,
fsync: true,
order: "random",
fileVersioningSelector: "none",
trashcanClean: 0,
simpleKeep: 5,
staggeredMaxAge: 365,
staggeredCleanInterval: 3600,
staggeredVersionsPath: "",
externalCommand: "",
autoNormalize: true
};
$scope.editingExisting = false; $scope.editingExisting = false;
$('#editIgnores textarea').val("");
$scope.folderEditor.$setPristine(); $scope.folderEditor.$setPristine();
$http.get(urlbase + '/svc/random/string?length=10').success(function (data) { $http.get(urlbase + '/svc/random/string?length=10').success(function (data) {
$scope.currentFolder.id = (data.random.substr(0, 5) + '-' + data.random.substr(5, 5)).toLowerCase(); $scope.currentFolder.id = (data.random.substr(0, 5) + '-' + data.random.substr(5, 5)).toLowerCase();
@ -1420,26 +1423,11 @@ angular.module('syncthing.core')
$scope.addFolderAndShare = function (folder, folderLabel, device) { $scope.addFolderAndShare = function (folder, folderLabel, device) {
$scope.dismissFolderRejection(folder, device); $scope.dismissFolderRejection(folder, device);
$scope.currentFolder = { $scope.currentFolder = angular.copy($scope.folderDefaults);
id: folder, $scope.currentFolder.id = folder;
label: folderLabel, $scope.currentFolder.label = folderLabel;
selectedDevices: {}, $scope.currentFolder.viewFlags = {
rescanIntervalS: 60, importFromOtherDevice: true
minDiskFreePct: 1,
maxConflicts: 10,
fsync: true,
order: "random",
fileVersioningSelector: "none",
trashcanClean: 0,
simpleKeep: 5,
staggeredMaxAge: 365,
staggeredCleanInterval: 3600,
staggeredVersionsPath: "",
externalCommand: "",
autoNormalize: true,
viewFlags: {
importFromOtherDevice: true
}
}; };
$scope.currentFolder.selectedDevices[device] = true; $scope.currentFolder.selectedDevices[device] = true;
@ -1516,10 +1504,20 @@ angular.module('syncthing.core')
delete folderCfg.versioning; delete folderCfg.versioning;
} }
var ignores = $('#editIgnores textarea').val().trim();
if (!$scope.editingExisting && ignores) {
folderCfg.paused = true;
};
$scope.folders[folderCfg.id] = folderCfg; $scope.folders[folderCfg.id] = folderCfg;
$scope.config.folders = folderList($scope.folders); $scope.config.folders = folderList($scope.folders);
$scope.saveConfig(); $scope.saveConfig();
if (!$scope.editingExisting && ignores) {
$scope.saveIgnores();
$scope.setFolderPause(folderCfg.id, false);
};
}; };
$scope.dismissFolderRejection = function (folder, device) { $scope.dismissFolderRejection = function (folder, device) {
@ -1593,11 +1591,21 @@ angular.module('syncthing.core')
}); });
}; };
$scope.saveIgnores = function () { $scope.editIgnoresOnAddingFolder = function () {
if (!$scope.editingExisting) { if ($scope.editingExisting) {
return; return;
} }
if ($scope.currentFolder.path.endsWith($scope.system.pathSeparator)) {
$scope.currentFolder.path = $scope.currentFolder.path.slice(0, -1);
};
$('#editIgnores').modal().one('shown.bs.modal', function () {
textArea.focus();
});
};
$scope.saveIgnores = function () {
$http.post(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id), { $http.post(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id), {
ignore: $('#editIgnores textarea').val().split('\n') ignore: $('#editIgnores textarea').val().split('\n')
}); });

View File

@ -184,7 +184,7 @@
<button type="button" class="btn btn-primary btn-sm" ng-click="saveFolder()" ng-disabled="folderEditor.$invalid"> <button type="button" class="btn btn-primary btn-sm" ng-click="saveFolder()" ng-disabled="folderEditor.$invalid">
<span class="fa fa-check"></span>&nbsp;<span translate>Save</span> <span class="fa fa-check"></span>&nbsp;<span translate>Save</span>
</button> </button>
<button type="button" class="btn btn-default btn-sm" id="editIgnoresButton" ng-click="editIgnores()" ng-if="editingExisting"> <button type="button" class="btn btn-default btn-sm" id="editIgnoresButton" ng-click="editingExisting ? editIgnores() : editIgnoresOnAddingFolder()" ng-disabled="folderEditor.$invalid">
<span class="fa fa-eye-slash"></span>&nbsp;<span translate>Ignore Patterns</span> <span class="fa fa-eye-slash"></span>&nbsp;<span translate>Ignore Patterns</span>
</button> </button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"> <button type="button" class="btn btn-default btn-sm" data-dismiss="modal">

View File

@ -14,12 +14,13 @@
</dl> </dl>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<div class="pull-left"><span translate>Editing</span> <code>{{currentFolder.path}}{{system.pathSeparator}}.stignore</code></div> <div class="pull-left" ng-show="editingExisting"><span translate translate-value-path="{{currentFolder.path}}{{system.pathSeparator}}.stignore">Editing {%path%}.</span></div>
<button type="button" class="btn btn-primary btn-sm" ng-click="saveIgnores()" data-dismiss="modal"> <div class="pull-left" ng-show="!editingExisting"><span translate translate-value-path="{{currentFolder.path}}{{system.pathSeparator}}.stignore">Creating ignore patterns, overwriting an existing file at {%path%}.</span></div>
<button type="button" class="btn btn-primary btn-sm" ng-click="editingExisting ? saveIgnores() : angular.noop()" data-dismiss="modal">
<span class="fa fa-check"></span>&nbsp;<span translate>Save</span> <span class="fa fa-check"></span>&nbsp;<span translate>Save</span>
</button> </button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"> <button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fa fa-times"></span>&nbsp;<span translate>Close</span> <span class="fa fa-times"></span>&nbsp;<span translate>Close</span>
</button> </button>
</div> </div>
</modal> </modal>

View File

@ -19,6 +19,7 @@ import (
"time" "time"
"github.com/gobwas/glob" "github.com/gobwas/glob"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/sync"
) )
@ -64,6 +65,7 @@ func (r Result) IsCaseFolded() bool {
} }
type Matcher struct { type Matcher struct {
lines []string
patterns []Pattern patterns []Pattern
withCache bool withCache bool
matches *cache matches *cache
@ -120,7 +122,7 @@ func (m *Matcher) Parse(r io.Reader, file string) error {
} }
func (m *Matcher) parseLocked(r io.Reader, file string) error { func (m *Matcher) parseLocked(r io.Reader, file string) error {
patterns, err := parseIgnoreFile(r, file, m.modtimes) lines, patterns, err := parseIgnoreFile(r, file, m.modtimes)
// Error is saved and returned at the end. We process the patterns // Error is saved and returned at the end. We process the patterns
// (possibly blank) anyway. // (possibly blank) anyway.
@ -131,6 +133,7 @@ func (m *Matcher) parseLocked(r io.Reader, file string) error {
} }
m.curHash = newHash m.curHash = newHash
m.lines = lines
m.patterns = patterns m.patterns = patterns
if m.withCache { if m.withCache {
m.matches = newCache(patterns) m.matches = newCache(patterns)
@ -206,6 +209,13 @@ func (m *Matcher) Match(file string) (result Result) {
return resultNotMatched return resultNotMatched
} }
// Lines return a list of the unprocessed lines in .stignore at last load
func (m *Matcher) Lines() []string {
m.mut.Lock()
defer m.mut.Unlock()
return m.lines
}
// Patterns return a list of the loaded patterns, as they've been parsed // Patterns return a list of the loaded patterns, as they've been parsed
func (m *Matcher) Patterns() []string { func (m *Matcher) Patterns() []string {
if m == nil { if m == nil {
@ -274,27 +284,28 @@ func hashPatterns(patterns []Pattern) string {
return fmt.Sprintf("%x", h.Sum(nil)) return fmt.Sprintf("%x", h.Sum(nil))
} }
func loadIgnoreFile(file string, modtimes map[string]time.Time) ([]Pattern, error) { func loadIgnoreFile(file string, modtimes map[string]time.Time) ([]string, []Pattern, error) {
if _, ok := modtimes[file]; ok { if _, ok := modtimes[file]; ok {
return nil, fmt.Errorf("Multiple include of ignore file %q", file) return nil, nil, fmt.Errorf("multiple include of ignore file %q", file)
} }
fd, err := os.Open(file) fd, err := os.Open(file)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
defer fd.Close() defer fd.Close()
info, err := fd.Stat() info, err := fd.Stat()
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
modtimes[file] = info.ModTime() modtimes[file] = info.ModTime()
return parseIgnoreFile(fd, file, modtimes) return parseIgnoreFile(fd, file, modtimes)
} }
func parseIgnoreFile(fd io.Reader, currentFile string, modtimes map[string]time.Time) ([]Pattern, error) { func parseIgnoreFile(fd io.Reader, currentFile string, modtimes map[string]time.Time) ([]string, []Pattern, error) {
var lines []string
var patterns []Pattern var patterns []Pattern
defaultResult := resultInclude defaultResult := resultInclude
@ -360,11 +371,12 @@ func parseIgnoreFile(fd io.Reader, currentFile string, modtimes map[string]time.
} else if strings.HasPrefix(line, "#include ") { } else if strings.HasPrefix(line, "#include ") {
includeRel := line[len("#include "):] includeRel := line[len("#include "):]
includeFile := filepath.Join(filepath.Dir(currentFile), includeRel) includeFile := filepath.Join(filepath.Dir(currentFile), includeRel)
includes, err := loadIgnoreFile(includeFile, modtimes) includeLines, includePatterns, err := loadIgnoreFile(includeFile, modtimes)
if err != nil { if err != nil {
return fmt.Errorf("include of %q: %v", includeRel, err) return fmt.Errorf("include of %q: %v", includeRel, err)
} }
patterns = append(patterns, includes...) lines = append(lines, includeLines...)
patterns = append(patterns, includePatterns...)
} else { } else {
// Path name or pattern, add it so it matches files both in // Path name or pattern, add it so it matches files both in
// current directory and subdirs. // current directory and subdirs.
@ -389,6 +401,7 @@ func parseIgnoreFile(fd io.Reader, currentFile string, modtimes map[string]time.
var err error var err error
for scanner.Scan() { for scanner.Scan() {
line := strings.TrimSpace(scanner.Text()) line := strings.TrimSpace(scanner.Text())
lines = append(lines, line)
switch { switch {
case line == "": case line == "":
continue continue
@ -411,11 +424,11 @@ func parseIgnoreFile(fd io.Reader, currentFile string, modtimes map[string]time.
} }
} }
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
} }
return patterns, nil return lines, patterns, nil
} }
// IsInternal returns true if the file, as a path relative to the folder // IsInternal returns true if the file, as a path relative to the folder
@ -434,3 +447,22 @@ func IsInternal(file string) bool {
} }
return false return false
} }
// WriteIgnores is a convenience function to avoid code duplication
func WriteIgnores(path string, content []string) error {
fd, err := osutil.CreateAtomic(path)
if err != nil {
return err
}
for _, line := range content {
fmt.Fprintln(fd, line)
}
if err := fd.Close(); err != nil {
return err
}
osutil.HideFile(path)
return nil
}

View File

@ -10,9 +10,11 @@ import "time"
type folder struct { type folder struct {
stateTracker stateTracker
scan folderScanner
model *Model scan folderScanner
stop chan struct{} model *Model
stop chan struct{}
initialScanCompleted chan struct{}
} }
func (f *folder) IndexUpdated() { func (f *folder) IndexUpdated() {
@ -23,6 +25,7 @@ func (f *folder) DelayScan(next time.Duration) {
} }
func (f *folder) Scan(subdirs []string) error { func (f *folder) Scan(subdirs []string) error {
<-f.initialScanCompleted
return f.scan.Scan(subdirs) return f.scan.Scan(subdirs)
} }
func (f *folder) Stop() { func (f *folder) Stop() {

View File

@ -7,7 +7,6 @@
package model package model
import ( import (
"bufio"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"errors" "errors"
@ -1252,66 +1251,51 @@ func (m *Model) ConnectedTo(deviceID protocol.DeviceID) bool {
} }
func (m *Model) GetIgnores(folder string) ([]string, []string, error) { func (m *Model) GetIgnores(folder string) ([]string, []string, error) {
var lines []string
m.fmut.RLock() m.fmut.RLock()
cfg, ok := m.folderCfgs[folder] cfg, ok := m.folderCfgs[folder]
m.fmut.RUnlock() m.fmut.RUnlock()
if !ok { if ok {
return lines, nil, fmt.Errorf("Folder %s does not exist", folder) if !cfg.HasMarker() {
} return nil, nil, fmt.Errorf("Folder %s stopped", folder)
if !cfg.HasMarker() {
return lines, nil, fmt.Errorf("Folder %s stopped", folder)
}
fd, err := os.Open(filepath.Join(cfg.Path(), ".stignore"))
if err != nil {
if os.IsNotExist(err) {
return lines, nil, nil
} }
l.Warnln("Loading .stignore:", err)
return lines, nil, err
}
defer fd.Close()
scanner := bufio.NewScanner(fd) m.fmut.RLock()
for scanner.Scan() { ignores := m.folderIgnores[folder]
lines = append(lines, strings.TrimSpace(scanner.Text())) m.fmut.RUnlock()
return ignores.Lines(), ignores.Patterns(), nil
} }
m.fmut.RLock() if cfg, ok := m.cfg.Folders()[folder]; ok {
patterns := m.folderIgnores[folder].Patterns() matcher := ignore.New(false)
m.fmut.RUnlock() path := filepath.Join(cfg.Path(), ".stignore")
if err := matcher.Load(path); err != nil {
return nil, nil, err
}
return matcher.Lines(), matcher.Patterns(), nil
}
return lines, patterns, nil return nil, nil, fmt.Errorf("Folder %s does not exist", folder)
} }
func (m *Model) SetIgnores(folder string, content []string) error { func (m *Model) SetIgnores(folder string, content []string) error {
cfg, ok := m.folderCfgs[folder] cfg, ok := m.cfg.Folders()[folder]
if !ok { if !ok {
return fmt.Errorf("Folder %s does not exist", folder) return fmt.Errorf("Folder %s does not exist", folder)
} }
path := filepath.Join(cfg.Path(), ".stignore") if err := ignore.WriteIgnores(filepath.Join(cfg.Path(), ".stignore"), content); err != nil {
fd, err := osutil.CreateAtomic(path)
if err != nil {
l.Warnln("Saving .stignore:", err) l.Warnln("Saving .stignore:", err)
return err return err
} }
for _, line := range content { m.fmut.RLock()
fmt.Fprintln(fd, line) runner, ok := m.folderRunners[folder]
m.fmut.RUnlock()
if ok {
return runner.Scan(nil)
} }
return nil
if err := fd.Close(); err != nil {
l.Warnln("Saving .stignore:", err)
return err
}
osutil.HideFile(path)
return m.ScanFolder(folder)
} }
// OnHello is called when an device connects to us. // OnHello is called when an device connects to us.
@ -2395,9 +2379,13 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
for folderID, cfg := range toFolders { for folderID, cfg := range toFolders {
if _, ok := fromFolders[folderID]; !ok { if _, ok := fromFolders[folderID]; !ok {
// A folder was added. // A folder was added.
l.Debugln(m, "adding folder", folderID) if cfg.Paused {
m.AddFolder(cfg) l.Infoln(m, "Paused folder", cfg.Description())
m.StartFolder(folderID) } else {
l.Infoln(m, "Adding folder", cfg.Description())
m.AddFolder(cfg)
m.StartFolder(folderID)
}
} }
} }

View File

@ -927,7 +927,7 @@ func TestIntroducer(t *testing.T) {
} }
} }
func TestIgnores(t *testing.T) { func changeIgnores(t *testing.T, m *Model, expected []string) {
arrEqual := func(a, b []string) bool { arrEqual := func(a, b []string) bool {
if len(a) != len(b) { if len(a) != len(b) {
return false return false
@ -941,22 +941,6 @@ func TestIgnores(t *testing.T) {
return true return true
} }
// Assure a clean start state
ioutil.WriteFile("testdata/.stfolder", nil, 0644)
ioutil.WriteFile("testdata/.stignore", []byte(".*\nquux\n"), 0644)
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
expected := []string{
".*",
"quux",
}
ignores, _, err := m.GetIgnores("default") ignores, _, err := m.GetIgnores("default")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -999,8 +983,34 @@ func TestIgnores(t *testing.T) {
if !arrEqual(ignores, expected) { if !arrEqual(ignores, expected) {
t.Errorf("Incorrect ignores: %v != %v", ignores, expected) t.Errorf("Incorrect ignores: %v != %v", ignores, expected)
} }
}
_, _, err = m.GetIgnores("doesnotexist") func TestIgnores(t *testing.T) {
// Assure a clean start state
ioutil.WriteFile("testdata/.stfolder", nil, 0644)
ioutil.WriteFile("testdata/.stignore", []byte(".*\nquux\n"), 0644)
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.ServeBackground()
defer m.Stop()
// m.cfg.SetFolder is not usable as it is non-blocking, and there is no
// way to know when the folder is actually added.
m.AddFolder(defaultFolderConfig)
m.StartFolder("default")
// Make sure the initial scan has finished (ScanFolders is blocking)
m.ScanFolders()
expected := []string{
".*",
"quux",
}
changeIgnores(t, m, expected)
_, _, err := m.GetIgnores("doesnotexist")
if err == nil { if err == nil {
t.Error("No error") t.Error("No error")
} }
@ -1016,6 +1026,16 @@ func TestIgnores(t *testing.T) {
if err == nil { if err == nil {
t.Error("No error") t.Error("No error")
} }
// Repeat tests with paused folder
pausedDefaultFolderConfig := defaultFolderConfig
pausedDefaultFolderConfig.Paused = true
m.RestartFolder(pausedDefaultFolderConfig)
// Here folder initialization is not an issue as a paused folder isn't
// added to the model and thus there is no initial scan happening.
changeIgnores(t, m, expected)
} }
func TestROScanRecovery(t *testing.T) { func TestROScanRecovery(t *testing.T) {
@ -1763,13 +1783,8 @@ func TestIssue3028(t *testing.T) {
m.StartFolder("default") m.StartFolder("default")
m.ServeBackground() m.ServeBackground()
// Ugly hack for testing: reach into the model for the SendReceiveFolder and wait // Make sure the initial scan has finished (ScanFolders is blocking)
// for it to complete the initial scan. The risk is that it otherwise m.ScanFolders()
// runs during our modifications and screws up the test.
m.fmut.RLock()
folder := m.folderRunners["default"].(*sendReceiveFolder)
m.fmut.RUnlock()
<-folder.initialScanCompleted
// Get a count of how many files are there now // Get a count of how many files are there now

View File

@ -26,10 +26,11 @@ type sendOnlyFolder struct {
func newSendOnlyFolder(model *Model, cfg config.FolderConfiguration, _ versioner.Versioner, _ *fs.MtimeFS) service { func newSendOnlyFolder(model *Model, cfg config.FolderConfiguration, _ versioner.Versioner, _ *fs.MtimeFS) service {
return &sendOnlyFolder{ return &sendOnlyFolder{
folder: folder{ folder: folder{
stateTracker: newStateTracker(cfg.ID), stateTracker: newStateTracker(cfg.ID),
scan: newFolderScanner(cfg), scan: newFolderScanner(cfg),
stop: make(chan struct{}), stop: make(chan struct{}),
model: model, model: model,
initialScanCompleted: make(chan struct{}),
}, },
FolderConfiguration: cfg, FolderConfiguration: cfg,
} }
@ -43,7 +44,6 @@ func (f *sendOnlyFolder) Serve() {
f.scan.timer.Stop() f.scan.timer.Stop()
}() }()
initialScanCompleted := false
for { for {
select { select {
case <-f.stop: case <-f.stop:
@ -68,9 +68,11 @@ func (f *sendOnlyFolder) Serve() {
continue continue
} }
if !initialScanCompleted { select {
case <-f.initialScanCompleted:
default:
l.Infoln("Completed initial scan (ro) of", f.Description()) l.Infoln("Completed initial scan (ro) of", f.Description())
initialScanCompleted = true close(f.initialScanCompleted)
} }
if f.scan.HasNoInterval() { if f.scan.HasNoInterval() {

View File

@ -96,17 +96,16 @@ type sendReceiveFolder struct {
errors map[string]string // path -> error string errors map[string]string // path -> error string
errorsMut sync.Mutex errorsMut sync.Mutex
initialScanCompleted chan (struct{}) // exposed for testing
} }
func newSendReceiveFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Versioner, mtimeFS *fs.MtimeFS) service { func newSendReceiveFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Versioner, mtimeFS *fs.MtimeFS) service {
f := &sendReceiveFolder{ f := &sendReceiveFolder{
folder: folder{ folder: folder{
stateTracker: newStateTracker(cfg.ID), stateTracker: newStateTracker(cfg.ID),
scan: newFolderScanner(cfg), scan: newFolderScanner(cfg),
stop: make(chan struct{}), stop: make(chan struct{}),
model: model, model: model,
initialScanCompleted: make(chan struct{}),
}, },
FolderConfiguration: cfg, FolderConfiguration: cfg,
@ -119,8 +118,6 @@ func newSendReceiveFolder(model *Model, cfg config.FolderConfiguration, ver vers
remoteIndex: make(chan struct{}, 1), // This needs to be 1-buffered so that we queue a notification if we're busy doing a pull when it comes. remoteIndex: make(chan struct{}, 1), // This needs to be 1-buffered so that we queue a notification if we're busy doing a pull when it comes.
errorsMut: sync.NewMutex(), errorsMut: sync.NewMutex(),
initialScanCompleted: make(chan struct{}),
} }
f.configureCopiersAndPullers() f.configureCopiersAndPullers()
@ -1063,7 +1060,7 @@ func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- c
// sweep is complete. As we do retries, we'll queue the scan // 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 // for this file up to ten times, but the last nine of those
// scans will be cheap... // scans will be cheap...
go f.scan.Scan([]string{file.Name}) go f.Scan([]string{file.Name})
return return
} }
} }

View File

@ -77,11 +77,12 @@ func setUpModel(file protocol.FileInfo) *Model {
return model return model
} }
func setUpSendReceiveFolder(model *Model) sendReceiveFolder { func setUpSendReceiveFolder(model *Model) *sendReceiveFolder {
return sendReceiveFolder{ f := &sendReceiveFolder{
folder: folder{ folder: folder{
stateTracker: newStateTracker("default"), stateTracker: newStateTracker("default"),
model: model, model: model,
initialScanCompleted: make(chan struct{}),
}, },
mtimeFS: fs.NewMtimeFS(fs.DefaultFilesystem, db.NewNamespacedKV(model.db, "mtime")), mtimeFS: fs.NewMtimeFS(fs.DefaultFilesystem, db.NewNamespacedKV(model.db, "mtime")),
@ -90,6 +91,11 @@ func setUpSendReceiveFolder(model *Model) sendReceiveFolder {
errors: make(map[string]string), errors: make(map[string]string),
errorsMut: sync.NewMutex(), errorsMut: sync.NewMutex(),
} }
// Folders are never actually started, so no initial scan will be done
close(f.initialScanCompleted)
return f
} }
// Layout of the files: (indexes from the above array) // Layout of the files: (indexes from the above array)