mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-09 14:50:56 +00:00
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:
parent
c5e0c47989
commit
25b314f5f1
@ -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
|
||||||
|
@ -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.",
|
||||||
|
@ -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')
|
||||||
});
|
});
|
||||||
|
@ -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> <span translate>Save</span>
|
<span class="fa fa-check"></span> <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> <span translate>Ignore Patterns</span>
|
<span class="fa fa-eye-slash"></span> <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">
|
||||||
|
@ -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> <span translate>Save</span>
|
<span class="fa fa-check"></span> <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> <span translate>Close</span>
|
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user