Просмотр исходного кода

lib, gui: Default ignores for new folders (fixes #7428) (#7530)

Simon Frei 4 лет назад
Родитель
Сommit
21d04b895a

+ 24 - 5
cmd/syncthing/cli/client.go

@@ -10,8 +10,10 @@ import (
 	"bytes"
 	"context"
 	"crypto/tls"
+	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
 	"net"
 	"net/http"
 	"strings"
@@ -25,6 +27,7 @@ import (
 type APIClient interface {
 	Get(url string) (*http.Response, error)
 	Post(url, body string) (*http.Response, error)
+	PutJSON(url string, o interface{}) (*http.Response, error)
 }
 
 type apiClient struct {
@@ -118,20 +121,36 @@ func (c *apiClient) Do(req *http.Request) (*http.Response, error) {
 	return resp, checkResponse(resp)
 }
 
-func (c *apiClient) Get(url string) (*http.Response, error) {
-	request, err := http.NewRequest("GET", c.Endpoint()+"rest/"+url, nil)
+func (c *apiClient) Request(url, method string, r io.Reader) (*http.Response, error) {
+	request, err := http.NewRequest(method, c.Endpoint()+"rest/"+url, r)
 	if err != nil {
 		return nil, err
 	}
 	return c.Do(request)
 }
 
-func (c *apiClient) Post(url, body string) (*http.Response, error) {
-	request, err := http.NewRequest("POST", c.Endpoint()+"rest/"+url, bytes.NewBufferString(body))
+func (c *apiClient) RequestString(url, method, data string) (*http.Response, error) {
+	return c.Request(url, method, bytes.NewBufferString(data))
+}
+
+func (c *apiClient) RequestJSON(url, method string, o interface{}) (*http.Response, error) {
+	data, err := json.Marshal(o)
 	if err != nil {
 		return nil, err
 	}
-	return c.Do(request)
+	return c.Request(url, method, bytes.NewBuffer(data))
+}
+
+func (c *apiClient) Get(url string) (*http.Response, error) {
+	return c.RequestString(url, "GET", "")
+}
+
+func (c *apiClient) Post(url, body string) (*http.Response, error) {
+	return c.RequestString(url, "POST", body)
+}
+
+func (c *apiClient) PutJSON(url string, o interface{}) (*http.Response, error) {
+	return c.RequestJSON(url, "PUT", o)
 }
 
 var errNotFound = errors.New("invalid endpoint or API call")

+ 36 - 0
cmd/syncthing/cli/operations.go

@@ -7,8 +7,12 @@
 package cli
 
 import (
+	"bufio"
 	"fmt"
+	"path/filepath"
 
+	"github.com/syncthing/syncthing/lib/config"
+	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/urfave/cli"
 )
 
@@ -38,6 +42,12 @@ var operationCommand = cli.Command{
 			ArgsUsage: "[folder id]",
 			Action:    expects(1, foldersOverride),
 		},
+		{
+			Name:      "default-ignores",
+			Usage:     "Set the default ignores (config) from a file",
+			ArgsUsage: "path",
+			Action:    expects(1, setDefaultIgnores),
+		},
 	},
 }
 
@@ -74,3 +84,29 @@ func foldersOverride(c *cli.Context) error {
 	}
 	return fmt.Errorf("Folder " + rid + " not found")
 }
+
+func setDefaultIgnores(c *cli.Context) error {
+	client, err := getClientFactory(c).getClient()
+	if err != nil {
+		return err
+	}
+	dir, file := filepath.Split(c.Args()[0])
+	filesystem := fs.NewFilesystem(fs.FilesystemTypeBasic, dir)
+
+	fd, err := filesystem.Open(file)
+	if err != nil {
+		return err
+	}
+	scanner := bufio.NewScanner(fd)
+	var lines []string
+	for scanner.Scan() {
+		lines = append(lines, scanner.Text())
+	}
+	fd.Close()
+	if err := scanner.Err(); err != nil {
+		return err
+	}
+
+	_, err = client.PutJSON("config/defaults/ignores", config.Ignores{Lines: lines})
+	return err
+}

+ 199 - 96
gui/default/syncthing/core/syncthingController.js

@@ -58,6 +58,9 @@ angular.module('syncthing.core')
             text: '',
             error: null,
             disabled: false,
+            originalLines: [],
+            defaultLines: [],
+            saved: false,
         };
         resetRemoteNeed();
 
@@ -409,8 +412,14 @@ angular.module('syncthing.core')
             console.log("FolderScanProgress", data);
         });
 
+        // May be called through .error with the presented arguments, or through
+        // .catch with the http response object containing the same arguments.
         $scope.emitHTTPError = function (data, status, headers, config) {
-            $scope.$emit('HTTPError', { data: data, status: status, headers: headers, config: config });
+            var out = data;
+            if (data && !data.data) {
+                out = { data: data, status: status, headers: headers, config: config };
+            }
+            $scope.$emit('HTTPError', out);
         };
 
         var debouncedFuncs = {};
@@ -741,7 +750,7 @@ angular.module('syncthing.core')
         }
 
         function shouldSetDefaultFolderPath() {
-            return $scope.config.defaults.folder.path && !$scope.editingExisting && $scope.folderEditor.folderPath.$pristine && !$scope.editingDefaults;
+            return $scope.config.defaults.folder.path && $scope.folderEditor.folderPath.$pristine && $scope.currentFolder._editing == "add";
         }
 
         function resetRemoteNeed() {
@@ -750,7 +759,6 @@ angular.module('syncthing.core')
             $scope.remoteNeedDevice = undefined;
         }
 
-
         function setDefaultTheme() {
             if (!document.getElementById("fallback-theme-css")) {
 
@@ -767,13 +775,9 @@ angular.module('syncthing.core')
             }
         }
 
-        function saveIgnores(ignores, cb) {
-            $http.post(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id), {
+        function saveIgnores(ignores) {
+            return $http.post(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id), {
                 ignore: ignores
-            }).success(function () {
-                if (cb) {
-                    cb();
-                }
             });
         };
 
@@ -1268,8 +1272,9 @@ angular.module('syncthing.core')
             if (cfg) {
                 cfg.paused = pause;
                 $scope.config.folders = folderList($scope.folders);
-                $scope.saveConfig();
+                return $scope.saveConfig();
             }
+            return $q.when();
         };
 
         $scope.showListenerStatus = function () {
@@ -1421,18 +1426,14 @@ angular.module('syncthing.core')
             });
         };
 
-        $scope.saveConfig = function (callback) {
+        $scope.saveConfig = function () {
             var cfg = JSON.stringify($scope.config);
             var opts = {
                 headers: {
                     'Content-Type': 'application/json'
                 }
             };
-            $http.put(urlbase + '/config', cfg, opts).finally(refreshConfig).then(function() {
-                if (callback) {
-                    callback();
-                }
-            }, $scope.emitHTTPError);
+            return $http.put(urlbase + '/config', cfg, opts).finally(refreshConfig).catch($scope.emitHTTPError);
         };
 
         $scope.urVersions = function () {
@@ -1512,7 +1513,7 @@ angular.module('syncthing.core')
                 // here as well...
                 $scope.devices = deviceMap($scope.config.devices);
 
-                $scope.saveConfig(function () {
+                $scope.saveConfig.then(function () {
                     if (themeChanged) {
                         document.location.reload(true);
                     }
@@ -1578,11 +1579,11 @@ angular.module('syncthing.core')
         }
 
         $scope.editDeviceModalTitle = function() {
-            if ($scope.editingDefaults) {
+            if ($scope.editingDeviceDefaults()) {
                 return $translate.instant("Edit Device Defaults");
             }
             var title = '';
-            if ($scope.editingExisting) {
+            if ($scope.editingDeviceExisting()) {
                 title += $translate.instant("Edit Device");
             } else {
                 title += $translate.instant("Add Device");
@@ -1595,16 +1596,23 @@ angular.module('syncthing.core')
         };
 
         $scope.editDeviceModalIcon = function() {
-            if ($scope.editingDefaults || $scope.editingExisting) {
+            if ($scope.has(["existing", "defaults"], $scope.currentDevice._editing)) {
                 return 'fas fa-pencil-alt';
             }
             return 'fas fa-desktop';
         };
 
+        $scope.editingDeviceDefaults = function() {
+            return $scope.currentDevice._editing == 'defaults';
+        }
+
+        $scope.editingDeviceExisting = function() {
+            return $scope.currentDevice._editing == 'existing';
+        }
+
         $scope.editDeviceExisting = function (deviceCfg) {
             $scope.currentDevice = $.extend({}, deviceCfg);
-            $scope.editingExisting = true;
-            $scope.editingDefaults = false;
+            $scope.currentDevice._editing = "existing";
             $scope.willBeReintroducedBy = undefined;
             if (deviceCfg.introducedBy) {
                 var introducerDevice = $scope.devices[deviceCfg.introducedBy];
@@ -1633,7 +1641,7 @@ angular.module('syncthing.core')
         $scope.editDeviceDefaults = function () {
             $http.get(urlbase + '/config/defaults/device').then(function (p) {
                 $scope.currentDevice = p.data;
-                $scope.editingDefaults = true;
+                $scope.currentDevice._editing = "defaults";
                 editDeviceModal();
             }, $scope.emitHTTPError);
         };
@@ -1671,8 +1679,7 @@ angular.module('syncthing.core')
                         $scope.currentDevice = p.data;
                         $scope.currentDevice.name = name;
                         $scope.currentDevice.deviceID = deviceID;
-                        $scope.editingExisting = false;
-                        $scope.editingDefaults = false;
+                        $scope.currentDevice._editing = "add";
                         initShareEditing('device');
                         $scope.currentSharing.unrelated = $scope.folderList();
                         editDeviceModal();
@@ -1682,7 +1689,7 @@ angular.module('syncthing.core')
 
         $scope.deleteDevice = function () {
             $('#editDevice').modal('hide');
-            if (!$scope.editingExisting) {
+            if ($scope.currentDevice._editing != "existing") {
                 return;
             }
 
@@ -1705,13 +1712,13 @@ angular.module('syncthing.core')
                 return x.trim();
             });
             delete $scope.currentDevice._addressesStr;
-            if ($scope.editingDefaults) {
+            if ($scope.currentDevice._editing == "defaults") {
                 $scope.config.defaults.device = $scope.currentDevice;
             } else {
                 setDeviceConfig();
             }
             delete $scope.currentSharing;
-            delete $scope.currentDevice;
+            $scope.currentDevice = {};
             $scope.saveConfig();
         };
 
@@ -1955,20 +1962,34 @@ angular.module('syncthing.core')
                     $('#folder-ignores textarea').focus();
                 }
             }).one('hidden.bs.modal', function () {
-                window.location.hash = "";
-                $scope.currentFolder = {};
+                var p = $q.when();
+                // If the modal was closed default patterns should still apply
+                if ($scope.currentFolder._editing == "add-ignores" && !$scope.ignores.saved && $scope.ignores.defaultLines) {
+                    p = saveFolderAddIgnores($scope.currentFolder.id, true);
+                }
+                p.then(function () {
+                    window.location.hash = "";
+                    $scope.currentFolder = {};
+                    $scope.ignores = {};
+                });
             });
         };
 
         $scope.editFolderModalTitle = function() {
-            if ($scope.editingDefaults) {
+            if ($scope.editingFolderDefaults()) {
                 return $translate.instant("Edit Folder Defaults");
             }
             var title = '';
-            if ($scope.editingExisting) {
-                title += $translate.instant("Edit Folder");
-            } else {
-                title += $translate.instant("Add Folder");
+            switch ($scope.currentFolder._editing) {
+            case "existing":
+                title = $translate.instant("Edit Folder");
+                break;
+            case "add":
+                title = $translate.instant("Add Folder");
+                break;
+            case "add-ignores":
+                title = $translate.instant("Set Ignores on Added Folder");
+                break;
             }
             if ($scope.currentFolder.id !== '') {
                 title += ' (' + $scope.folderLabel($scope.currentFolder.id) + ')';
@@ -1977,12 +1998,20 @@ angular.module('syncthing.core')
         };
 
         $scope.editFolderModalIcon = function() {
-            if ($scope.editingDefaults || $scope.editingExisting) {
+            if ($scope.has(["existing", "defaults"], $scope.currentFolder._editing)) {
                 return 'fas fa-pencil-alt';
             }
             return 'fas fa-folder';
         };
 
+        $scope.editingFolderDefaults = function() {
+            return $scope.currentFolder._editing == 'defaults';
+        }
+
+        $scope.editingFolderExisting = function() {
+            return $scope.currentFolder._editing == 'existing';
+        }
+
         function editFolder(initialTab) {
             if ($scope.currentFolder.path.length > 1 && $scope.currentFolder.path.slice(-1) === $scope.system.pathSeparator) {
                 $scope.currentFolder.path = $scope.currentFolder.path.slice(0, -1);
@@ -2033,39 +2062,60 @@ angular.module('syncthing.core')
         };
 
         $scope.editFolderExisting = function(folderCfg, initialTab) {
-            $scope.editingExisting = true;
-            $scope.editingDefaults = false;
             $scope.currentFolder = angular.copy(folderCfg);
+            $scope.currentFolder._editing = "existing";
+            editFolderLoadIgnores();
+            editFolder(initialTab);
+        };
 
+        function editFolderLoadingIgnores() {
             $scope.ignores.text = 'Loading...';
             $scope.ignores.error = null;
             $scope.ignores.disabled = true;
-            $http.get(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id))
-                .success(function (data) {
-                    $scope.currentFolder.ignores = data.ignore || [];
-                    $scope.ignores.text = $scope.currentFolder.ignores.join('\n');
-                    $scope.ignores.error = data.error;
-                    $scope.ignores.disabled = false;
-                })
-                .error(function (err) {
-                    $scope.ignores.text = $translate.instant("Failed to load ignore patterns.");
-                    $scope.emitHTTPError(err);
-                });
+        }
 
-            editFolder(initialTab);
+        function editFolderGetIgnores() {
+            return $http.get(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id))
+                .then(function(r) {
+                    return r.data;
+                }, function (response) {
+                    $scope.ignores.text = $translate.instant("Failed to load ignore patterns.");
+                    return $q.reject(response);
+            });
         };
 
+        function editFolderLoadIgnores() {
+            editFolderLoadingIgnores();
+            return editFolderGetIgnores().then(editFolderInitIgnores, $scope.emitHTTPError);
+        }
+
         $scope.editFolderDefaults = function() {
-            $http.get(urlbase + '/config/defaults/folder')
-                 .success(function (data) {
-                     $scope.currentFolder = data;
-                     $scope.editingExisting = false;
-                     $scope.editingDefaults = true;
-                     editFolder();
-                 })
-                 .error($scope.emitHTTPError);
+            $q.all([
+                $http.get(urlbase + '/config/defaults/folder').then(function (response) {
+                    $scope.currentFolder = response.data;
+                    $scope.currentFolder._editing = "defaults";
+                }),
+                getDefaultIgnores().then(editFolderInitIgnores),
+            ]).then(editFolder, $scope.emitHTTPError);
         };
 
+        function getDefaultIgnores() {
+            return $http.get(urlbase + '/config/defaults/ignores').then(function (r) {
+                return r.data.lines;
+            });
+        }
+
+        function editFolderInitIgnores(data) {
+            $scope.ignores.originalLines = data.ignore || [];
+            setIgnoresText(data.ignore);
+            $scope.ignores.error = data.error;
+            $scope.ignores.disabled = false;
+        }
+
+        function setIgnoresText(lines) {
+            $scope.ignores.text = lines ? lines.join('\n') : "";
+        }
+
         $scope.selectAllSharedDevices = function (state) {
             var devices = $scope.currentSharing.shared;
             for (var i = 0; i < devices.length; i++) {
@@ -2093,9 +2143,6 @@ angular.module('syncthing.core')
 
         $scope.addFolderAndShare = function (folderID, pendingFolder, device) {
             addFolderInit(folderID).then(function() {
-                $scope.currentFolder.viewFlags = {
-                    importFromOtherDevice: true
-                };
                 $scope.currentSharing.selected[device] = true;
                 $scope.currentFolder.label = pendingFolder.offeredBy[device].label;
                 for (var k in pendingFolder.offeredBy) {
@@ -2110,19 +2157,16 @@ angular.module('syncthing.core')
         };
 
         function addFolderInit(folderID) {
-            $scope.editingExisting = false;
-            $scope.editingDefaults = false;
-            return $http.get(urlbase + '/config/defaults/folder').then(function(p) {
-                $scope.currentFolder = p.data;
+            return $http.get(urlbase + '/config/defaults/folder').then(function (response) {
+                $scope.currentFolder = response.data;
+                $scope.currentFolder._editing = "add";
                 $scope.currentFolder.id = folderID;
-
                 initShareEditing('folder');
                 $scope.currentSharing.unrelated = $scope.currentSharing.unrelated.concat($scope.currentSharing.shared);
                 $scope.currentSharing.shared = [];
-
-                $scope.ignores.text = '';
-                $scope.ignores.error = null;
-                $scope.ignores.disabled = false;
+                // Ignores don't need to be initialized here, as that happens in
+                // a second step if the user indicates in the creation modal
+                // that they want to set ignores
             }, $scope.emitHTTPError);
         }
 
@@ -2142,7 +2186,14 @@ angular.module('syncthing.core')
         };
 
         $scope.saveFolder = function () {
-            $('#editFolder').modal('hide');
+            if ($scope.currentFolder._editing == "add-ignores") {
+                // On modal being hidden without clicking save, the defaults will be saved.
+                $scope.ignores.saved = true;
+                saveFolderAddIgnores($scope.currentFolder.id);
+                hideFolderModal();
+                return;
+            }
+
             var folderCfg = angular.copy($scope.currentFolder);
             $scope.currentSharing.selected[$scope.myID] = true;
             var newDevices = [];
@@ -2191,45 +2242,89 @@ angular.module('syncthing.core')
             }
             delete folderCfg._guiVersioning;
 
-            if ($scope.editingDefaults) {
+            if ($scope.currentFolder._editing == "defaults") {
+                hideFolderModal();
+                $scope.config.defaults.ignores.lines = ignoresArray();
                 $scope.config.defaults.folder = folderCfg;
                 $scope.saveConfig();
-            } else {
-                saveFolderExisting(folderCfg);
+                return;
             }
-        };
 
-        function saveFolderExisting(folderCfg) {
-            var ignoresLoaded = !$scope.ignores.disabled;
-            var ignores = $scope.ignores.text.split('\n');
-            // Split always returns a minimum 1-length array even for no patterns
-            if (ignores.length === 1 && ignores[0] === "") {
-                ignores = [];
-            }
-            if (!$scope.editingExisting && ignores.length) {
+            // This is a new folder where ignores should apply before it first starts.
+            if ($scope.currentFolder._addIgnores) {
                 folderCfg.paused = true;
-            };
-
+            }
             $scope.folders[folderCfg.id] = folderCfg;
             $scope.config.folders = folderList($scope.folders);
 
-            function arrayEquals(a, b) {
-              return a.length === b.length && a.every(function(v, i) { return v === b[i] });
+            if ($scope.currentFolder._editing == "existing") {
+                hideFolderModal();
+                saveFolderIgnoresExisting();
+                $scope.saveConfig();
+                return;
             }
 
-            if (ignoresLoaded && $scope.editingExisting && !arrayEquals(ignores, folderCfg.ignores)) {
-                saveIgnores(ignores);
-            };
+            // No ignores to be set on the new folder, save directly.
+            if (!$scope.currentFolder._addIgnores) {
+                hideFolderModal();
+                $scope.saveConfig();
+                return;
+            }
 
-            $scope.saveConfig(function () {
-                if (!$scope.editingExisting && ignores.length) {
-                    saveIgnores(ignores, function () {
-                        $scope.setFolderPause(folderCfg.id, false);
+            // Add folder (paused), load existing ignores and if there are none,
+            // load default ignores, then let the user edit them.
+            $scope.saveConfig().then(function() {
+                editFolderLoadingIgnores();
+                $scope.currentFolder._editing = "add-ignores";
+                $('.nav-tabs a[href="#folder-ignores"]').tab('show');
+                return editFolderGetIgnores();
+            }).then(function(data) {
+                // Error getting ignores -> leave error message.
+                if (!data) {
+                    return;
+                }
+                if ((data.ignore && data.ignore.length > 0) || data.error) {
+                    editFolderInitIgnores(data);
+                } else {
+                    getDefaultIgnores().then(function(lines) {
+                        setIgnoresText(lines);
+                        $scope.ignores.defaultLines = lines;
+                        $scope.ignores.disabled = false;
                     });
                 }
+            }, $scope.emitHTTPError);
+        };
+
+        function saveFolderIgnoresExisting() {
+            if ($scope.ignores.disabled) {
+                return;
+            }
+            var ignores = ignoresArray();
+
+            function arrayDiffers(a, b) {
+                return !a !== !b || a.length !== b.length || a.some(function(v, i) { return v !== b[i]; });
+            }
+            if (arrayDiffers(ignores, $scope.ignores.originalLines)) {
+                return saveIgnores(ignores);
+            };
+        }
+
+        function saveFolderAddIgnores(folderID, useDefault) {
+            var ignores = useDefault ? $scope.ignores.defaultLines : ignoresArray();
+            return saveIgnores(ignores).then(function () {
+                return $scope.setFolderPause(folderID, $scope.currentFolder.paused);
             });
         };
 
+        function ignoresArray() {
+            var ignores = $scope.ignores.text.split('\n');
+            // Split always returns a minimum 1-length array even for no patterns
+            if (ignores.length === 1 && ignores[0] === "") {
+                ignores = [];
+            }
+            return ignores;
+        }
+
         $scope.ignoreFolder = function (device, folderID, offeringDevice) {
             var ignoredFolder = {
                 id: folderID,
@@ -2282,8 +2377,8 @@ angular.module('syncthing.core')
         };
 
         $scope.deleteFolder = function (id) {
-            $('#editFolder').modal('hide');
-            if (!$scope.editingExisting) {
+            hideFolderModal();
+            if ($scope.currentFolder._editing != "existing") {
                 return;
             }
 
@@ -2295,6 +2390,10 @@ angular.module('syncthing.core')
             $scope.saveConfig();
         };
 
+        function hideFolderModal() {
+            $('#editFolder').modal('hide');
+        }
+
         function resetRestoreVersions() {
             $scope.restoreVersions = {
                 folder: null,
@@ -2839,6 +2938,10 @@ angular.module('syncthing.core')
             return Object.keys(dict).length;
         };
 
+        $scope.has = function (array, element) {
+            return array.indexOf(element) >= 0;
+        };
+
         $scope.dismissNotification = function (id) {
             var idx = $scope.config.options.unackedNotificationIDs.indexOf(id);
             if (idx > -1) {

+ 1 - 1
gui/default/syncthing/core/uniqueFolderDirective.js

@@ -4,7 +4,7 @@ angular.module('syncthing.core')
             require: 'ngModel',
             link: function (scope, elm, attrs, ctrl) {
                 ctrl.$parsers.unshift(function (viewValue) {
-                    if (scope.editingExisting) {
+                    if (scope.currentFolder._editing != "add") {
                         // we shouldn't validate
                         ctrl.$setValidity('uniqueFolder', true);
                     } else if (scope.folders.hasOwnProperty(viewValue)) {

+ 1 - 1
gui/default/syncthing/core/validDeviceidDirective.js

@@ -4,7 +4,7 @@ angular.module('syncthing.core')
             require: 'ngModel',
             link: function (scope, elm, attrs, ctrl) {
                 ctrl.$parsers.unshift(function (viewValue) {
-                    if (scope.editingExisting) {
+                    if (scope.currentDevice._editing != "add") {
                         // we shouldn't validate
                         ctrl.$setValidity('validDeviceid', true);
                     } else {

+ 6 - 6
gui/default/syncthing/device/editDeviceModalView.html

@@ -3,14 +3,14 @@
     <form role="form" name="deviceEditor">
       <ul class="nav nav-tabs" ng-init="loadFormIntoScope(deviceEditor)">
         <li class="active"><a data-toggle="tab" href="#device-general"><span class="fas fa-cog"></span> <span translate>General</span></a></li>
-        <li ng-if="!editingDefaults"><a data-toggle="tab" href="#device-sharing"><span class="fas fa-share-alt"></span> <span translate>Sharing</span></a></li>
+        <li ng-if="!editingDeviceDefaults()"><a data-toggle="tab" href="#device-sharing"><span class="fas fa-share-alt"></span> <span translate>Sharing</span></a></li>
         <li><a data-toggle="tab" href="#device-advanced"><span class="fas fa-cogs"></span> <span translate>Advanced</span></a></li>
       </ul>
       <div class="tab-content">
         <div id="device-general" class="tab-pane in active">
-          <div ng-if="!editingDefaults" class="form-group" ng-class="{'has-error': deviceEditor.deviceID.$invalid && deviceEditor.deviceID.$dirty}" ng-init="loadFormIntoScope(deviceEditor)">
+          <div ng-if="!editingDeviceDefaults()" class="form-group" ng-class="{'has-error': deviceEditor.deviceID.$invalid && deviceEditor.deviceID.$dirty}" ng-init="loadFormIntoScope(deviceEditor)">
             <label translate for="deviceID">Device ID</label>
-            <div ng-if="!editingExisting">
+            <div ng-if="!editingDeviceExisting()">
               <div class="input-group">
                 <input name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.deviceID" required="" valid-deviceid list="discovery-list" aria-required="true" />
                 <div class="input-group-btn">
@@ -40,7 +40,7 @@
                 <span translate ng-if="deviceEditor.deviceID.$error.unique && deviceEditor.deviceID.$dirty">A device with that ID is already added.</span>
               </p>
             </div>
-            <div ng-if="editingExisting" class="input-group">
+            <div ng-if="editingDeviceExisting()" class="input-group">
               <div class="well well-sm text-monospace form-control" style="height: auto;" select-on-click>{{currentDevice.deviceID}}</div>
               <div class="input-group-btn">
                 <button type="button" class="btn btn-default" data-toggle="modal" data-target="#idqr">
@@ -56,7 +56,7 @@
             <p translate ng-if="currentDevice.deviceID != myID" class="help-block">Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.</p>
           </div>
         </div>
-        <div ng-if="!editingDefaults" id="device-sharing" class="tab-pane">
+        <div ng-if="!editingDeviceDefaults()" id="device-sharing" class="tab-pane">
           <div class="row">
             <div class="col-md-6">
               <div class="form-group">
@@ -172,7 +172,7 @@
     <button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
       <span class="fas fa-times"></span>&nbsp;<span translate>Close</span>
     </button>
-    <div ng-if="editingExisting && !editingDefaults" class="pull-left">
+    <div ng-if="has(['existing', 'defaults'], currentDevice._editing)" class="pull-left">
       <button type="button" class="btn btn-warning btn-sm" data-toggle="modal" data-target="#remove-device-confirmation">
         <span class="fas fa-minus-circle"></span>&nbsp;<span translate>Remove</span>
       </button>

+ 53 - 44
gui/default/syncthing/folder/editFolderModalView.html

@@ -2,44 +2,44 @@
   <div class="modal-body">
     <form role="form" name="folderEditor">
       <ul class="nav nav-tabs" ng-init="loadFormIntoScope(folderEditor)">
-        <li class="active"><a data-toggle="tab" href="#folder-general"><span class="fas fa-cog"></span> <span translate>General</span></a></li>
-        <li><a data-toggle="tab" href="#folder-sharing"><span class="fas fa-share-alt"></span> <span translate>Sharing</span></a></li>
-        <li><a data-toggle="tab" href="#folder-versioning"><span class="fas fa-copy"></span> <span translate>File Versioning</span></a></li>
-        <li ng-if="!editingDefaults" ng-class="{'disabled': currentFolder._recvEnc}"><a ng-attr-data-toggle="{{ currentFolder._recvEnc ? undefined : 'tab'}}" href="{{currentFolder._recvEnc ? '#' : '#folder-ignores'}}"><span class="fas fa-filter"></span> <span translate>Ignore Patterns</span></a></li>
-        <li><a data-toggle="tab" href="#folder-advanced"><span class="fas fa-cogs"></span> <span translate>Advanced</span></a></li>
+        <li ng-class="{'disabled': currentFolder._editing == 'add-ignores'}" class="active"><a data-toggle="tab" href="{{currentFolder._editing == 'add-ignores' ? '' : '#folder-general'}}"><span class="fas fa-cog"></span> <span translate>General</span></a></li>
+        <li ng-class="{'disabled': currentFolder._editing == 'add-ignores'}"><a data-toggle="tab" href="{{currentFolder._editing == 'add-ignores' ? '' : '#folder-sharing'}}"><span class="fas fa-share-alt"></span> <span translate>Sharing</span></a></li>
+        <li ng-class="{'disabled': currentFolder._editing == 'add-ignores'}"><a data-toggle="tab" href="{{currentFolder._editing == 'add-ignores' ? '' : '#folder-versioning'}}"><span class="fas fa-copy"></span> <span translate>File Versioning</span></a></li>
+        <li ng-class="{'disabled': currentFolder._recvEnc}"><a data-toggle="tab" href="{{currentFolder._recvEnc ? '' : '#folder-ignores'}}"><span class="fas fa-filter"></span> <span translate>Ignore Patterns</span></a></li>
+        <li ng-class="{'disabled': currentFolder._editing == 'add-ignores'}"><a data-toggle="tab" href="{{currentFolder._editing == 'add-ignores' ? '' : '#folder-advanced'}}"><span class="fas fa-cogs"></span> <span translate>Advanced</span></a></li>
       </ul>
       <div class="tab-content">
 
         <div id="folder-general" class="tab-pane in active">
-          <div class="form-group" ng-class="{'has-error': folderEditor.folderLabel.$invalid && folderEditor.folderLabel.$dirty && !editingDefaults}">
+          <div class="form-group" ng-class="{'has-error': folderEditor.folderLabel.$invalid && folderEditor.folderLabel.$dirty && !editingFolderDefaults()}">
             <label for="folderLabel"><span translate>Folder Label</span></label>
             <input name="folderLabel" id="folderLabel" class="form-control" type="text" ng-model="currentFolder.label" value="{{currentFolder.label}}" />
             <p class="help-block">
               <span translate ng-if="folderEditor.folderLabel.$valid || folderEditor.folderLabel.$pristine">Optional descriptive label for the folder. Can be different on each device.</span>
             </p>
           </div>
-          <div ng-if="!editingDefaults" class="form-group" ng-class="{'has-error': folderEditor.folderID.$invalid && folderEditor.folderID.$dirty}">
+          <div ng-if="!editingFolderDefaults()" class="form-group" ng-class="{'has-error': folderEditor.folderID.$invalid && folderEditor.folderID.$dirty}">
             <label for="folderID"><span translate>Folder ID</span></label>
-            <input name="folderID" ng-readonly="editingExisting || (!editingExisting && currentFolder.viewFlags.importFromOtherDevice)" id="folderID" class="form-control" type="text" ng-model="currentFolder.id" required="" aria-required="true" unique-folder value="{{currentFolder.id}}" />
+            <input name="folderID" ng-readonly="has(['existing', 'add'], currentFolder._editing)" id="folderID" class="form-control" type="text" ng-model="currentFolder.id" required="" aria-required="true" unique-folder value="{{currentFolder.id}}" />
             <p class="help-block">
               <span translate ng-if="folderEditor.folderID.$valid || folderEditor.folderID.$pristine">Required identifier for the folder. Must be the same on all cluster devices.</span>
               <span translate ng-if="folderEditor.folderID.$error.uniqueFolder">The folder ID must be unique.</span>
               <span translate ng-if="folderEditor.folderID.$error.required && folderEditor.folderID.$dirty">The folder ID cannot be blank.</span>
-              <span translate ng-show="!editingExisting">When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.</span>
+              <span translate ng-show="!editingFolderExisting()">When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.</span>
             </p>
           </div>
-          <div class="form-group" ng-class="{'has-error': folderEditor.folderPath.$invalid && folderEditor.folderPath.$dirty && !editingDefaults}">
+          <div class="form-group" ng-class="{'has-error': folderEditor.folderPath.$invalid && folderEditor.folderPath.$dirty && !editingFolderDefaults()}">
             <label translate for="folderPath">Folder Path</label>
-            <input name="folderPath" ng-readonly="editingExisting && !editingDefaults" id="folderPath" class="form-control" type="text" ng-model="currentFolder.path" list="directory-list" ng-required="!editingDefaults" ng-aria-required="!editingDefaults" path-is-sub-dir />
+            <input name="folderPath" ng-readonly="editingFolderExisting()" id="folderPath" class="form-control" type="text" ng-model="currentFolder.path" list="directory-list" ng-required="!editingFolderDefaults()" ng-aria-required="!editingFolderDefaults()" path-is-sub-dir />
             <datalist id="directory-list">
               <option ng-repeat="directory in directoryList" value="{{ directory }}" />
             </datalist>
             <p class="help-block">
               <span ng-if="folderEditor.folderPath.$valid || folderEditor.folderPath.$pristine"><span translate>Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for</span> <code>{{system.tilde}}</code>.</br></span>
-              <span translate ng-if="folderEditor.folderPath.$error.required && folderEditor.folderPath.$dirty && !editingDefaults">The folder path cannot be blank.</span>
+              <span translate ng-if="folderEditor.folderPath.$error.required && folderEditor.folderPath.$dirty && !editingFolderDefaults()">The folder path cannot be blank.</span>
               <span class="text-danger" translate translate-value-other-folder="{{folderPathErrors.otherID}}" ng-if="folderPathErrors.isSub && folderPathErrors.otherLabel.length == 0">Warning, this path is a subdirectory of an existing folder "{%otherFolder%}".</span>
               <span class="text-danger" translate translate-value-other-folder="{{folderPathErrors.otherID}}" translate-value-other-folder-label="{{folderPathErrors.otherLabel}}" ng-if="folderPathErrors.isSub && folderPathErrors.otherLabel.length != 0">Warning, this path is a subdirectory of an existing folder "{%otherFolderLabel%}" ({%otherFolder%}).</span>
-              <span ng-if="folderPathErrors.isParent && !editingDefaults">
+              <span ng-if="folderPathErrors.isParent && !editingFolderDefaults()">
                 <span class="text-danger" translate translate-value-other-folder="{{folderPathErrors.otherID}}" ng-if="folderPathErrors.otherLabel.length == 0">Warning, this path is a parent directory of an existing folder "{%otherFolder%}".</span>
                 <span class="text-danger" translate translate-value-other-folder="{{folderPathErrors.otherID}}" translate-value-other-folder-label="{{folderPathErrors.otherLabel}}" ng-if="folderPathErrors.otherLabel.length != 0">Warning, this path is a parent directory of an existing folder "{%otherFolderLabel%}" ({%otherFolder%}).</span>
               </span>
@@ -148,33 +148,42 @@
           </div>
         </div>
 
-        <div ng-if="!editingDefaults" id="folder-ignores" class="tab-pane">
-          <p translate>Enter ignore patterns, one per line.</p>
-          <div ng-class="{'has-error': ignores.error != null}">
-            <textarea class="form-control" rows="5" ng-model="ignores.text" ng-disabled="ignores.disabled"></textarea>
-            <p class="help-block" ng-if="ignores.error">
-              {{ignores.error}}
-            </p>
+        <div id="folder-ignores" class="tab-pane" ng-switch="currentFolder._editing">
+          <div ng-switch-when="add">
+            <label>
+              <input type="checkbox" ng-model="currentFolder._addIgnores" >&nbsp;<span translate>Add ignore patterns</span>
+            </label>
+            <p translate class="help-block">Ignore patterns can only be added after the folder is created. If checked, an input field to enter ignore patterns will be presented after saving.</p>
+          </div>
+          <div ng-switch-default>
+            <p translate>Enter ignore patterns, one per line.</p>
+            <div ng-class="{'has-error': ignores.error != null}">
+              <textarea class="form-control" name="ignoresText" rows="5" ng-model="ignores.text" ng-disabled="ignores.disabled"></textarea>
+              <p class="help-block" ng-if="ignores.error">
+                {{ignores.error}}
+              </p>
+            </div>
+            <hr />
+            <p class="small"><span translate>Quick guide to supported patterns</span> (<a href="https://docs.syncthing.net/users/ignoring.html" target="_blank" translate>full documentation</a>):</p>
+            <dl class="dl-horizontal dl-narrow small">
+              <dt><code>(?d)</code></dt>
+              <dd><b><span translate>Prefix indicating that the file can be deleted if preventing directory removal</span></b></dd>
+              <dt><code>(?i)</code></dt>
+              <dd><span translate>Prefix indicating that the pattern should be matched without case sensitivity</span></dd>
+              <dt><code>!</code></dt>
+              <dd><span translate>Inversion of the given condition (i.e. do not exclude)</span></dd>
+              <dt><code>*</code></dt>
+              <dd><span translate>Single level wildcard (matches within a directory only)</span></dd>
+              <dt><code>**</code></dt>
+              <dd><span translate>Multi level wildcard (matches multiple directory levels)</span></dd>
+              <dt><code>//</code></dt>
+              <dd><span translate>Comment, when used at the start of a line</span></dd>
+            </dl>
+            <div ng-if="!editingFolderDefaults()">
+              <hr />
+              <span translate translate-value-path="{{currentFolder.path}}{{system.pathSeparator}}.stignore">Editing {%path%}.</span>
+            </div>
           </div>
-          <hr />
-          <p class="small"><span translate>Quick guide to supported patterns</span> (<a href="https://docs.syncthing.net/users/ignoring.html" target="_blank" translate>full documentation</a>):</p>
-          <dl class="dl-horizontal dl-narrow small">
-            <dt><code>(?d)</code></dt>
-            <dd><b><span translate>Prefix indicating that the file can be deleted if preventing directory removal</span></b></dd>
-            <dt><code>(?i)</code></dt>
-            <dd><span translate>Prefix indicating that the pattern should be matched without case sensitivity</span></dd>
-            <dt><code>!</code></dt>
-            <dd><span translate>Inversion of the given condition (i.e. do not exclude)</span></dd>
-            <dt><code>*</code></dt>
-            <dd><span translate>Single level wildcard (matches within a directory only)</span></dd>
-            <dt><code>**</code></dt>
-            <dd><span translate>Multi level wildcard (matches multiple directory levels)</span></dd>
-            <dt><code>//</code></dt>
-            <dd><span translate>Comment, when used at the start of a line</span></dd>
-          </dl>
-          <hr />
-          <span translate ng-show="editingExisting" translate-value-path="{{currentFolder.path}}{{system.pathSeparator}}.stignore">Editing {%path%}.</span>
-          <span translate ng-show="!editingExisting" translate-value-path="{{currentFolder.path}}{{system.pathSeparator}}.stignore">Creating ignore patterns, overwriting an existing file at {%path%}.</span>
         </div>
 
         <div id="folder-advanced" class="tab-pane">
@@ -205,17 +214,17 @@
             <div class="col-md-6 form-group">
               <label translate>Folder Type</label>
               &nbsp;<a href="https://docs.syncthing.net/users/foldertypes.html" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span></a>
-              <select class="form-control" ng-change="setDefaultsForFolderType()" ng-model="currentFolder.type" ng-disabled="editingExisting && currentFolder.type == 'receiveencrypted'">
+              <select class="form-control" ng-change="setDefaultsForFolderType()" ng-model="currentFolder.type" ng-disabled="editingFolderExisting() && currentFolder.type == 'receiveencrypted'">
                 <option value="sendreceive" translate>Send &amp; Receive</option>
                 <option value="sendonly" translate>Send Only</option>
                 <option value="receiveonly" translate>Receive Only</option>
-                <option value="receiveencrypted" ng-disabled="editingExisting" translate>Receive Encrypted</option>
+                <option value="receiveencrypted" ng-disabled="editingFolderExisting()" translate>Receive Encrypted</option>
               </select>
               <p ng-if="currentFolder.type == 'sendonly'" translate class="help-block">Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.</p>
               <p ng-if="currentFolder.type == 'receiveonly'" translate class="help-block">Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.</p>
               <p ng-if="currentFolder.type == 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type "{%receiveEncrypted%}" too.</p>
-              <p ng-if="editingExisting && currentFolder.type == 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Folder type "{%receiveEncrypted%}" cannot be changed after adding the folder. You need to remove the folder, delete or decrypt the data on disk, and add the folder again.</p>
-              <p ng-if="editingExisting && currentFolder.type != 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Folder type "{%receiveEncrypted%}" can only be set when adding a new folder.</p>
+              <p ng-if="editingFolderExisting() && currentFolder.type == 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Folder type "{%receiveEncrypted%}" cannot be changed after adding the folder. You need to remove the folder, delete or decrypt the data on disk, and add the folder again.</p>
+              <p ng-if="editingFolderExisting() && currentFolder.type != 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Folder type "{%receiveEncrypted%}" can only be set when adding a new folder.</p>
             </div>
             <div class="col-md-6 form-group">
               <label translate>File Pull Order</label>
@@ -274,7 +283,7 @@
     <button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
       <span class="fas fa-times"></span>&nbsp;<span translate>Close</span>
     </button>
-    <button type="button" class="btn btn-warning pull-left btn-sm" data-toggle="modal" data-target="#remove-folder-confirmation" ng-if="editingExisting && !editingDefaults">
+    <button type="button" class="btn btn-warning pull-left btn-sm" data-toggle="modal" data-target="#remove-folder-confirmation" ng-if="editingFolderExisting()">
       <span class="fas fa-minus-circle"></span>&nbsp;<span translate>Remove</span>
     </button>
   </div>

+ 1 - 0
lib/api/api.go

@@ -313,6 +313,7 @@ func (s *service) Serve(ctx context.Context) error {
 	configBuilder.registerDevice("/rest/config/devices/:id")
 	configBuilder.registerDefaultFolder("/rest/config/defaults/folder")
 	configBuilder.registerDefaultDevice("/rest/config/defaults/device")
+	configBuilder.registerDefaultIgnores("/rest/config/defaults/ignores")
 	configBuilder.registerOptions("/rest/config/options")
 	configBuilder.registerLDAP("/rest/config/ldap")
 	configBuilder.registerGUI("/rest/config/gui")

+ 22 - 0
lib/api/confighandler.go

@@ -229,6 +229,28 @@ func (c *configMuxBuilder) registerDefaultDevice(path string) {
 	})
 }
 
+func (c *configMuxBuilder) registerDefaultIgnores(path string) {
+	c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
+		sendJSON(w, c.cfg.DefaultIgnores())
+	})
+
+	c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
+		var ignores config.Ignores
+		if err := unmarshalTo(r.Body, &ignores); err != nil {
+			http.Error(w, err.Error(), http.StatusBadRequest)
+			return
+		}
+		waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
+			cfg.Defaults.Ignores = ignores
+		})
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		c.finish(w, waiter)
+	})
+}
+
 func (c *configMuxBuilder) registerOptions(path string) {
 	c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
 		sendJSON(w, c.cfg.Options())

+ 6 - 0
lib/config/config.go

@@ -621,3 +621,9 @@ func ensureZeroForNodefault(empty interface{}, target interface{}) {
 		return len(v) > 0
 	})
 }
+
+func (i Ignores) Copy() Ignores {
+	out := Ignores{Lines: make([]string, len(i.Lines))}
+	copy(out.Lines, i.Lines)
+	return out
+}

+ 264 - 44
lib/config/config.pb.go

@@ -69,8 +69,9 @@ func (m *Configuration) XXX_DiscardUnknown() {
 var xxx_messageInfo_Configuration proto.InternalMessageInfo
 
 type Defaults struct {
-	Folder FolderConfiguration `protobuf:"bytes,1,opt,name=folder,proto3" json:"folder" xml:"folder"`
-	Device DeviceConfiguration `protobuf:"bytes,2,opt,name=device,proto3" json:"device" xml:"device"`
+	Folder  FolderConfiguration `protobuf:"bytes,1,opt,name=folder,proto3" json:"folder" xml:"folder"`
+	Device  DeviceConfiguration `protobuf:"bytes,2,opt,name=device,proto3" json:"device" xml:"device"`
+	Ignores Ignores             `protobuf:"bytes,3,opt,name=ignores,proto3" json:"ignores" xml:"ignores"`
 }
 
 func (m *Defaults) Reset()         { *m = Defaults{} }
@@ -106,56 +107,98 @@ func (m *Defaults) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_Defaults proto.InternalMessageInfo
 
+type Ignores struct {
+	Lines []string `protobuf:"bytes,1,rep,name=lines,proto3" json:"lines" xml:"line"`
+}
+
+func (m *Ignores) Reset()         { *m = Ignores{} }
+func (m *Ignores) String() string { return proto.CompactTextString(m) }
+func (*Ignores) ProtoMessage()    {}
+func (*Ignores) Descriptor() ([]byte, []int) {
+	return fileDescriptor_baadf209193dc627, []int{2}
+}
+func (m *Ignores) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *Ignores) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_Ignores.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *Ignores) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Ignores.Merge(m, src)
+}
+func (m *Ignores) XXX_Size() int {
+	return m.ProtoSize()
+}
+func (m *Ignores) XXX_DiscardUnknown() {
+	xxx_messageInfo_Ignores.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Ignores proto.InternalMessageInfo
+
 func init() {
 	proto.RegisterType((*Configuration)(nil), "config.Configuration")
 	proto.RegisterType((*Defaults)(nil), "config.Defaults")
+	proto.RegisterType((*Ignores)(nil), "config.Ignores")
 }
 
 func init() { proto.RegisterFile("lib/config/config.proto", fileDescriptor_baadf209193dc627) }
 
 var fileDescriptor_baadf209193dc627 = []byte{
-	// 654 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x94, 0xcd, 0x6e, 0xd3, 0x40,
-	0x10, 0xc7, 0xed, 0xa6, 0x4d, 0xda, 0xed, 0x17, 0x32, 0x08, 0x5c, 0x3e, 0xbc, 0x61, 0x15, 0x50,
-	0x41, 0xa5, 0x95, 0xca, 0x05, 0x71, 0x23, 0x44, 0x94, 0x0a, 0x24, 0x2a, 0xa3, 0x22, 0xe0, 0x82,
-	0x92, 0x78, 0xeb, 0xae, 0x94, 0xd8, 0x96, 0xbd, 0xae, 0xda, 0x47, 0xe0, 0x86, 0x78, 0x02, 0x4e,
-	0x48, 0xdc, 0x79, 0x88, 0xdc, 0x92, 0x23, 0xa7, 0x95, 0x9a, 0xdc, 0x7c, 0xf4, 0x91, 0x13, 0xda,
-	0x0f, 0xbb, 0xb6, 0x6a, 0xe0, 0x64, 0xcf, 0xfc, 0xff, 0xf3, 0x9b, 0xd5, 0x78, 0xc7, 0xe0, 0xc6,
-	0x80, 0xf4, 0x76, 0xfa, 0xbe, 0x77, 0x44, 0x5c, 0xf5, 0xd8, 0x0e, 0x42, 0x9f, 0xfa, 0x46, 0x5d,
-	0x46, 0x37, 0x5b, 0x05, 0xc3, 0x91, 0x3f, 0x70, 0x70, 0x28, 0x83, 0x38, 0xec, 0x52, 0xe2, 0x7b,
-	0xd2, 0x5d, 0x72, 0x39, 0xf8, 0x84, 0xf4, 0x71, 0x95, 0xeb, 0x6e, 0xc1, 0xe5, 0xc6, 0xa4, 0xca,
-	0x82, 0x0a, 0x96, 0x81, 0xd3, 0x0d, 0xaa, 0x3c, 0xf7, 0x0a, 0x1e, 0x3f, 0xe0, 0x42, 0x54, 0x65,
-	0xdb, 0x28, 0xda, 0x7a, 0x11, 0x0e, 0x4f, 0xb0, 0xa3, 0xa4, 0x25, 0x7c, 0x4a, 0xe5, 0x2b, 0xfa,
-	0xde, 0x00, 0xab, 0xcf, 0x8b, 0xd5, 0x86, 0x0d, 0x1a, 0x27, 0x38, 0x8c, 0x88, 0xef, 0x99, 0x7a,
-	0x53, 0xdf, 0x5c, 0x68, 0x3f, 0x49, 0x18, 0xcc, 0x52, 0x29, 0x83, 0xc6, 0xe9, 0x70, 0xf0, 0x14,
-	0xa9, 0x78, 0xab, 0x4b, 0x69, 0x88, 0x7e, 0x33, 0x58, 0x23, 0x1e, 0x4d, 0xc6, 0xad, 0x95, 0x62,
-	0xde, 0xce, 0xaa, 0x8c, 0x77, 0xa0, 0x21, 0x87, 0x17, 0x99, 0x73, 0xcd, 0xda, 0xe6, 0xf2, 0xee,
-	0xad, 0x6d, 0x35, 0xed, 0x17, 0x22, 0x5d, 0x3a, 0x41, 0x1b, 0x8e, 0x18, 0xd4, 0x78, 0x53, 0x55,
-	0x93, 0x32, 0xb8, 0x22, 0x9a, 0xca, 0x18, 0xd9, 0x99, 0xc0, 0xb9, 0x72, 0xdc, 0x91, 0x59, 0x2b,
-	0x73, 0x3b, 0x22, 0xfd, 0x17, 0xae, 0xaa, 0xc9, 0xb9, 0x32, 0x46, 0x76, 0x26, 0x18, 0x36, 0xa8,
-	0xb9, 0x31, 0x31, 0xe7, 0x9b, 0xfa, 0xe6, 0xf2, 0xae, 0x99, 0x31, 0xf7, 0x0e, 0xf7, 0xcb, 0xc0,
-	0xfb, 0x1c, 0x38, 0x65, 0xb0, 0xb6, 0x77, 0xb8, 0x9f, 0x30, 0xc8, 0x6b, 0x52, 0x06, 0x97, 0x04,
-	0xd3, 0x8d, 0x09, 0xfa, 0x3a, 0x69, 0x71, 0xc9, 0xe6, 0x82, 0xf1, 0x01, 0xcc, 0xf3, 0x2f, 0x6a,
-	0x2e, 0x08, 0xe8, 0x46, 0x06, 0x7d, 0xdd, 0x79, 0x76, 0x50, 0xa6, 0x3e, 0x54, 0xd4, 0x79, 0x2e,
-	0x25, 0x0c, 0x8a, 0xb2, 0x94, 0x41, 0x20, 0xb8, 0x3c, 0xe0, 0x60, 0xa1, 0xda, 0x42, 0x33, 0xde,
-	0x83, 0x86, 0xba, 0x08, 0x66, 0x5d, 0xd0, 0x6f, 0x67, 0xf4, 0x37, 0x32, 0x5d, 0x6e, 0xd0, 0xcc,
-	0xe6, 0xa0, 0x8a, 0x52, 0x06, 0x57, 0x05, 0x5b, 0xc5, 0xc8, 0xce, 0x14, 0xe3, 0x87, 0x0e, 0xd6,
-	0x89, 0xeb, 0xf9, 0x21, 0x76, 0x3e, 0x65, 0x93, 0x6e, 0x88, 0x49, 0x5f, 0xcf, 0x5b, 0xa8, 0xbb,
-	0x25, 0x27, 0xde, 0x3e, 0x56, 0xf0, 0x6b, 0x21, 0x1e, 0xfa, 0x14, 0xef, 0xcb, 0xe2, 0x4e, 0x3e,
-	0xf1, 0x0d, 0xd1, 0xa9, 0x42, 0x44, 0xc9, 0xb8, 0x75, 0xb5, 0x22, 0x9f, 0x8e, 0x5b, 0x95, 0x2c,
-	0x7b, 0x8d, 0x94, 0x62, 0xe3, 0xb3, 0x0e, 0xd6, 0x03, 0xec, 0x39, 0xc4, 0x73, 0xf3, 0xb3, 0x2e,
-	0xfe, 0xf3, 0xac, 0x2f, 0xd5, 0xa4, 0xcd, 0x0e, 0x0e, 0x42, 0xdc, 0xef, 0x52, 0xec, 0x1c, 0x48,
-	0x80, 0x62, 0x26, 0x0c, 0xea, 0x8f, 0x52, 0x06, 0xef, 0x88, 0x43, 0x07, 0x45, 0x6d, 0xcb, 0x1f,
-	0x12, 0x8a, 0x87, 0x01, 0x3d, 0x43, 0xa6, 0x6e, 0xaf, 0x95, 0xb4, 0xc8, 0x38, 0x00, 0x8b, 0x0e,
-	0x3e, 0xea, 0xc6, 0x03, 0x1a, 0x99, 0x4b, 0xe2, 0x93, 0x5c, 0xb9, 0xb8, 0x99, 0x32, 0xdf, 0x46,
-	0x6a, 0x52, 0xb9, 0x33, 0x65, 0x70, 0x4d, 0xdd, 0x47, 0x99, 0x40, 0x76, 0xae, 0xa1, 0x9f, 0x3a,
-	0x58, 0xcc, 0x4a, 0x8d, 0xb7, 0xa0, 0x2e, 0x57, 0x40, 0xac, 0xe8, 0x7f, 0xd6, 0xc9, 0x52, 0x7d,
-	0x54, 0xc9, 0xa5, 0x6d, 0x52, 0x79, 0x0e, 0x95, 0x63, 0x33, 0xe7, 0xca, 0xd0, 0xaa, 0x5d, 0xca,
-	0xa1, 0xb2, 0xe4, 0xd2, 0x2a, 0xa9, 0x7c, 0xfb, 0xd5, 0xe8, 0xdc, 0xd2, 0x26, 0xe7, 0x96, 0x36,
-	0x9a, 0x5a, 0xfa, 0x64, 0x6a, 0xe9, 0x5f, 0x66, 0x96, 0xf6, 0x6d, 0x66, 0xe9, 0x93, 0x99, 0xa5,
-	0xfd, 0x9a, 0x59, 0xda, 0xc7, 0x07, 0x2e, 0xa1, 0xc7, 0x71, 0x6f, 0xbb, 0xef, 0x0f, 0x77, 0xa2,
-	0x33, 0xaf, 0x4f, 0x8f, 0x89, 0xe7, 0x16, 0xde, 0x2e, 0x7e, 0x63, 0xbd, 0xba, 0xf8, 0x67, 0x3d,
-	0xfe, 0x13, 0x00, 0x00, 0xff, 0xff, 0x50, 0xe9, 0xd5, 0x50, 0xb6, 0x05, 0x00, 0x00,
+	// 709 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x94, 0x4b, 0x6f, 0xd3, 0x40,
+	0x10, 0xc7, 0xe3, 0xa6, 0x8d, 0x9b, 0xed, 0x0b, 0x19, 0x44, 0x5d, 0x1e, 0xde, 0xb0, 0x0a, 0x28,
+	0xa0, 0x3e, 0xa4, 0x72, 0xa9, 0xb8, 0x11, 0x22, 0x4a, 0x55, 0x24, 0x2a, 0xa3, 0x22, 0xe0, 0x82,
+	0x92, 0x78, 0xeb, 0xae, 0x94, 0xd8, 0x96, 0xed, 0x54, 0xed, 0x91, 0x23, 0x37, 0xc4, 0x27, 0xe0,
+	0x84, 0xc4, 0x37, 0xe9, 0xad, 0x39, 0x72, 0x5a, 0xa9, 0xcd, 0xcd, 0x47, 0x1f, 0x39, 0xa1, 0x7d,
+	0x39, 0xb6, 0x6a, 0xe0, 0x94, 0xcc, 0xfc, 0xff, 0xf3, 0xdb, 0xd5, 0xec, 0x8c, 0xc1, 0xea, 0x80,
+	0xf4, 0xb6, 0xfa, 0xbe, 0x77, 0x44, 0x5c, 0xf9, 0xb3, 0x19, 0x84, 0x7e, 0xec, 0x1b, 0x35, 0x11,
+	0xdd, 0x69, 0xe6, 0x0c, 0x47, 0xfe, 0xc0, 0xc1, 0xa1, 0x08, 0x46, 0x61, 0x37, 0x26, 0xbe, 0x27,
+	0xdc, 0x05, 0x97, 0x83, 0x4f, 0x48, 0x1f, 0x97, 0xb9, 0x1e, 0xe4, 0x5c, 0xee, 0x88, 0x94, 0x59,
+	0x50, 0xce, 0x32, 0x70, 0xba, 0x41, 0x99, 0xe7, 0x61, 0xce, 0xe3, 0x07, 0x4c, 0x88, 0xca, 0x6c,
+	0x6b, 0x79, 0x5b, 0x2f, 0xc2, 0xe1, 0x09, 0x76, 0xa4, 0x54, 0xc7, 0xa7, 0xb1, 0xf8, 0x8b, 0x7e,
+	0xe8, 0x60, 0xe9, 0x45, 0xbe, 0xda, 0xb0, 0x81, 0x7e, 0x82, 0xc3, 0x88, 0xf8, 0x9e, 0xa9, 0x35,
+	0xb4, 0xd6, 0x5c, 0x7b, 0x27, 0xa1, 0x50, 0xa5, 0x52, 0x0a, 0x8d, 0xd3, 0xe1, 0xe0, 0x19, 0x92,
+	0xf1, 0x7a, 0x37, 0x8e, 0x43, 0xf4, 0x9b, 0xc2, 0x2a, 0xf1, 0xe2, 0xe4, 0xa2, 0xb9, 0x98, 0xcf,
+	0xdb, 0xaa, 0xca, 0x78, 0x07, 0x74, 0xd1, 0xbc, 0xc8, 0x9c, 0x69, 0x54, 0x5b, 0x0b, 0xdb, 0x77,
+	0x37, 0x65, 0xb7, 0x5f, 0xf2, 0x74, 0xe1, 0x06, 0x6d, 0x78, 0x4e, 0x61, 0x85, 0x1d, 0x2a, 0x6b,
+	0x52, 0x0a, 0x17, 0xf9, 0xa1, 0x22, 0x46, 0xb6, 0x12, 0x18, 0x57, 0xb4, 0x3b, 0x32, 0xab, 0x45,
+	0x6e, 0x87, 0xa7, 0xff, 0xc2, 0x95, 0x35, 0x19, 0x57, 0xc4, 0xc8, 0x56, 0x82, 0x61, 0x83, 0xaa,
+	0x3b, 0x22, 0xe6, 0x6c, 0x43, 0x6b, 0x2d, 0x6c, 0x9b, 0x8a, 0xb9, 0x7b, 0xb8, 0x57, 0x04, 0x3e,
+	0x62, 0xc0, 0x2b, 0x0a, 0xab, 0xbb, 0x87, 0x7b, 0x09, 0x85, 0xac, 0x26, 0xa5, 0xb0, 0xce, 0x99,
+	0xee, 0x88, 0xa0, 0x6f, 0xe3, 0x26, 0x93, 0x6c, 0x26, 0x18, 0x1f, 0xc0, 0x2c, 0x7b, 0x51, 0x73,
+	0x8e, 0x43, 0xd7, 0x14, 0xf4, 0x75, 0xe7, 0xf9, 0x41, 0x91, 0xfa, 0x44, 0x52, 0x67, 0x99, 0x94,
+	0x50, 0xc8, 0xcb, 0x52, 0x0a, 0x01, 0xe7, 0xb2, 0x80, 0x81, 0xb9, 0x6a, 0x73, 0xcd, 0x78, 0x0f,
+	0x74, 0x39, 0x08, 0x66, 0x8d, 0xd3, 0xef, 0x29, 0xfa, 0x1b, 0x91, 0x2e, 0x1e, 0xd0, 0x50, 0x7d,
+	0x90, 0x45, 0x29, 0x85, 0x4b, 0x9c, 0x2d, 0x63, 0x64, 0x2b, 0xc5, 0xf8, 0xa9, 0x81, 0x15, 0xe2,
+	0x7a, 0x7e, 0x88, 0x9d, 0x4f, 0xaa, 0xd3, 0x3a, 0xef, 0xf4, 0xed, 0xec, 0x08, 0x39, 0x5b, 0xa2,
+	0xe3, 0xed, 0x63, 0x09, 0xbf, 0x15, 0xe2, 0xa1, 0x1f, 0xe3, 0x3d, 0x51, 0xdc, 0xc9, 0x3a, 0xbe,
+	0xc6, 0x4f, 0x2a, 0x11, 0x51, 0x72, 0xd1, 0xbc, 0x59, 0x92, 0x4f, 0x2f, 0x9a, 0xa5, 0x2c, 0x7b,
+	0x99, 0x14, 0x62, 0xe3, 0x8b, 0x06, 0x56, 0x02, 0xec, 0x39, 0xc4, 0x73, 0xb3, 0xbb, 0xce, 0xff,
+	0xf3, 0xae, 0xaf, 0x64, 0xa7, 0xcd, 0x0e, 0x0e, 0x42, 0xdc, 0xef, 0xc6, 0xd8, 0x39, 0x10, 0x00,
+	0xc9, 0x4c, 0x28, 0xd4, 0x36, 0x52, 0x0a, 0xef, 0xf3, 0x4b, 0x07, 0x79, 0x6d, 0xdd, 0x1f, 0x92,
+	0x18, 0x0f, 0x83, 0xf8, 0x0c, 0x99, 0x9a, 0xbd, 0x5c, 0xd0, 0x22, 0xe3, 0x00, 0xcc, 0x3b, 0xf8,
+	0xa8, 0x3b, 0x1a, 0xc4, 0x91, 0x59, 0xe7, 0x4f, 0x72, 0x63, 0x3a, 0x99, 0x22, 0xdf, 0x46, 0xb2,
+	0x53, 0x99, 0x33, 0xa5, 0x70, 0x59, 0xce, 0xa3, 0x48, 0x20, 0x3b, 0xd3, 0xd0, 0xe7, 0x19, 0x30,
+	0xaf, 0x4a, 0x8d, 0xb7, 0xa0, 0x26, 0x56, 0x80, 0xaf, 0xe8, 0x7f, 0xd6, 0xc9, 0x92, 0xe7, 0xc8,
+	0x92, 0x6b, 0xdb, 0x24, 0xf3, 0x0c, 0x2a, 0xda, 0x66, 0xce, 0x14, 0xa1, 0x65, 0xbb, 0x94, 0x41,
+	0x45, 0xc9, 0xb5, 0x55, 0x92, 0x79, 0x63, 0x1f, 0xe8, 0xe2, 0x99, 0xd8, 0x86, 0x32, 0xea, 0x8a,
+	0xa2, 0x8a, 0xd7, 0x8c, 0xa6, 0xd3, 0x28, 0x7d, 0xd9, 0x34, 0xca, 0x18, 0xd9, 0x4a, 0x41, 0x3b,
+	0x40, 0x97, 0x55, 0xc6, 0x06, 0x98, 0x1b, 0x10, 0x0f, 0x47, 0xa6, 0xd6, 0xa8, 0xb6, 0xea, 0xed,
+	0xd5, 0x84, 0x42, 0x91, 0x98, 0x2e, 0x0a, 0xf1, 0x30, 0xb2, 0x45, 0xb2, 0xbd, 0x7f, 0x7e, 0x69,
+	0x55, 0xc6, 0x97, 0x56, 0xe5, 0xfc, 0xca, 0xd2, 0xc6, 0x57, 0x96, 0xf6, 0x75, 0x62, 0x55, 0xbe,
+	0x4f, 0x2c, 0x6d, 0x3c, 0xb1, 0x2a, 0xbf, 0x26, 0x56, 0xe5, 0xe3, 0x63, 0x97, 0xc4, 0xc7, 0xa3,
+	0xde, 0x66, 0xdf, 0x1f, 0x6e, 0x45, 0x67, 0x5e, 0x3f, 0x3e, 0x26, 0x9e, 0x9b, 0xfb, 0x37, 0xfd,
+	0x9a, 0xf6, 0x6a, 0xfc, 0xd3, 0xf9, 0xf4, 0x4f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x77, 0x7d, 0xcb,
+	0x2a, 0x3d, 0x06, 0x00, 0x00,
 }
 
 func (m *Configuration) Marshal() (dAtA []byte, err error) {
@@ -302,6 +345,16 @@ func (m *Defaults) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	{
+		size, err := m.Ignores.MarshalToSizedBuffer(dAtA[:i])
+		if err != nil {
+			return 0, err
+		}
+		i -= size
+		i = encodeVarintConfig(dAtA, i, uint64(size))
+	}
+	i--
+	dAtA[i] = 0x1a
 	{
 		size, err := m.Device.MarshalToSizedBuffer(dAtA[:i])
 		if err != nil {
@@ -325,6 +378,38 @@ func (m *Defaults) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
+func (m *Ignores) Marshal() (dAtA []byte, err error) {
+	size := m.ProtoSize()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *Ignores) MarshalTo(dAtA []byte) (int, error) {
+	size := m.ProtoSize()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *Ignores) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if len(m.Lines) > 0 {
+		for iNdEx := len(m.Lines) - 1; iNdEx >= 0; iNdEx-- {
+			i -= len(m.Lines[iNdEx])
+			copy(dAtA[i:], m.Lines[iNdEx])
+			i = encodeVarintConfig(dAtA, i, uint64(len(m.Lines[iNdEx])))
+			i--
+			dAtA[i] = 0xa
+		}
+	}
+	return len(dAtA) - i, nil
+}
+
 func encodeVarintConfig(dAtA []byte, offset int, v uint64) int {
 	offset -= sovConfig(v)
 	base := offset
@@ -390,6 +475,23 @@ func (m *Defaults) ProtoSize() (n int) {
 	n += 1 + l + sovConfig(uint64(l))
 	l = m.Device.ProtoSize()
 	n += 1 + l + sovConfig(uint64(l))
+	l = m.Ignores.ProtoSize()
+	n += 1 + l + sovConfig(uint64(l))
+	return n
+}
+
+func (m *Ignores) ProtoSize() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if len(m.Lines) > 0 {
+		for _, s := range m.Lines {
+			l = len(s)
+			n += 1 + l + sovConfig(uint64(l))
+		}
+	}
 	return n
 }
 
@@ -831,6 +933,124 @@ func (m *Defaults) Unmarshal(dAtA []byte) error {
 				return err
 			}
 			iNdEx = postIndex
+		case 3:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Ignores", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowConfig
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthConfig
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthConfig
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if err := m.Ignores.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipConfig(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if skippy < 0 {
+				return ErrInvalidLengthConfig
+			}
+			if (iNdEx + skippy) < 0 {
+				return ErrInvalidLengthConfig
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *Ignores) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowConfig
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: Ignores: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: Ignores: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Lines", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowConfig
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthConfig
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthConfig
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Lines = append(m.Lines, string(dAtA[iNdEx:postIndex]))
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipConfig(dAtA[iNdEx:])

+ 3 - 0
lib/config/config_test.go

@@ -113,6 +113,9 @@ func TestDefaultValues(t *testing.T) {
 				Compression:     protocol.CompressionMetadata,
 				IgnoredFolders:  []ObservedFolder{},
 			},
+			Ignores: Ignores{
+				Lines: []string{},
+			},
 		},
 		IgnoredDevices: []ObservedDevice{},
 	}

+ 65 - 0
lib/config/mocks/mocked_wrapper.go

@@ -40,6 +40,16 @@ type Wrapper struct {
 	defaultFolderReturnsOnCall map[int]struct {
 		result1 config.FolderConfiguration
 	}
+	DefaultIgnoresStub        func() config.Ignores
+	defaultIgnoresMutex       sync.RWMutex
+	defaultIgnoresArgsForCall []struct {
+	}
+	defaultIgnoresReturns struct {
+		result1 config.Ignores
+	}
+	defaultIgnoresReturnsOnCall map[int]struct {
+		result1 config.Ignores
+	}
 	DeviceStub        func(protocol.DeviceID) (config.DeviceConfiguration, bool)
 	deviceMutex       sync.RWMutex
 	deviceArgsForCall []struct {
@@ -449,6 +459,59 @@ func (fake *Wrapper) DefaultFolderReturnsOnCall(i int, result1 config.FolderConf
 	}{result1}
 }
 
+func (fake *Wrapper) DefaultIgnores() config.Ignores {
+	fake.defaultIgnoresMutex.Lock()
+	ret, specificReturn := fake.defaultIgnoresReturnsOnCall[len(fake.defaultIgnoresArgsForCall)]
+	fake.defaultIgnoresArgsForCall = append(fake.defaultIgnoresArgsForCall, struct {
+	}{})
+	stub := fake.DefaultIgnoresStub
+	fakeReturns := fake.defaultIgnoresReturns
+	fake.recordInvocation("DefaultIgnores", []interface{}{})
+	fake.defaultIgnoresMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *Wrapper) DefaultIgnoresCallCount() int {
+	fake.defaultIgnoresMutex.RLock()
+	defer fake.defaultIgnoresMutex.RUnlock()
+	return len(fake.defaultIgnoresArgsForCall)
+}
+
+func (fake *Wrapper) DefaultIgnoresCalls(stub func() config.Ignores) {
+	fake.defaultIgnoresMutex.Lock()
+	defer fake.defaultIgnoresMutex.Unlock()
+	fake.DefaultIgnoresStub = stub
+}
+
+func (fake *Wrapper) DefaultIgnoresReturns(result1 config.Ignores) {
+	fake.defaultIgnoresMutex.Lock()
+	defer fake.defaultIgnoresMutex.Unlock()
+	fake.DefaultIgnoresStub = nil
+	fake.defaultIgnoresReturns = struct {
+		result1 config.Ignores
+	}{result1}
+}
+
+func (fake *Wrapper) DefaultIgnoresReturnsOnCall(i int, result1 config.Ignores) {
+	fake.defaultIgnoresMutex.Lock()
+	defer fake.defaultIgnoresMutex.Unlock()
+	fake.DefaultIgnoresStub = nil
+	if fake.defaultIgnoresReturnsOnCall == nil {
+		fake.defaultIgnoresReturnsOnCall = make(map[int]struct {
+			result1 config.Ignores
+		})
+	}
+	fake.defaultIgnoresReturnsOnCall[i] = struct {
+		result1 config.Ignores
+	}{result1}
+}
+
 func (fake *Wrapper) Device(arg1 protocol.DeviceID) (config.DeviceConfiguration, bool) {
 	fake.deviceMutex.Lock()
 	ret, specificReturn := fake.deviceReturnsOnCall[len(fake.deviceArgsForCall)]
@@ -1752,6 +1815,8 @@ func (fake *Wrapper) Invocations() map[string][][]interface{} {
 	defer fake.defaultDeviceMutex.RUnlock()
 	fake.defaultFolderMutex.RLock()
 	defer fake.defaultFolderMutex.RUnlock()
+	fake.defaultIgnoresMutex.RLock()
+	defer fake.defaultIgnoresMutex.RUnlock()
 	fake.deviceMutex.RLock()
 	defer fake.deviceMutex.RUnlock()
 	fake.deviceListMutex.RLock()

+ 8 - 0
lib/config/wrapper.go

@@ -100,6 +100,7 @@ type Wrapper interface {
 	GUI() GUIConfiguration
 	LDAP() LDAPConfiguration
 	Options() OptionsConfiguration
+	DefaultIgnores() Ignores
 
 	Folder(id string) (FolderConfiguration, bool)
 	Folders() map[string]FolderConfiguration
@@ -437,6 +438,13 @@ func (w *wrapper) GUI() GUIConfiguration {
 	return w.cfg.GUI.Copy()
 }
 
+// DefaultIgnores returns the list of ignore patterns to be used by default on folders.
+func (w *wrapper) DefaultIgnores() Ignores {
+	w.mut.Lock()
+	defer w.mut.Unlock()
+	return w.cfg.Defaults.Ignores.Copy()
+}
+
 // IgnoredDevice returns whether or not connection attempts from the given
 // device should be silently ignored.
 func (w *wrapper) IgnoredDevice(id protocol.DeviceID) bool {

+ 9 - 6
lib/model/model.go

@@ -1651,6 +1651,11 @@ func (m *model) handleAutoAccepts(deviceID protocol.DeviceID, folder protocol.Fo
 
 			if len(ccDeviceInfos.remote.EncryptionPasswordToken) > 0 || len(ccDeviceInfos.local.EncryptionPasswordToken) > 0 {
 				fcfg.Type = config.FolderTypeReceiveEncrypted
+			} else {
+				ignores := m.cfg.DefaultIgnores()
+				if err := m.setIgnores(fcfg, ignores.Lines); err != nil {
+					l.Warnf("Failed to apply default ignores to auto-accepted folder %s at path %s: %v", folder.Description(), fcfg.Path, err)
+				}
 			}
 
 			l.Infof("Auto-accepted %s folder %s at path %s", deviceID, folder.Description(), fcfg.Path)
@@ -2035,11 +2040,6 @@ func (m *model) LoadIgnores(folder string) ([]string, []string, error) {
 		return nil, nil, nil
 	}
 
-	// On creation a new folder with ignore patterns validly has no marker yet.
-	if err := cfg.CheckPath(); err != nil && err != config.ErrMarkerMissing {
-		return nil, nil, err
-	}
-
 	if !ignoresOk {
 		ignores = ignore.New(cfg.Filesystem())
 	}
@@ -2081,7 +2081,10 @@ func (m *model) SetIgnores(folder string, content []string) error {
 	if !ok {
 		return fmt.Errorf("folder %s does not exist", cfg.Description())
 	}
+	return m.setIgnores(cfg, content)
+}
 
+func (m *model) setIgnores(cfg config.FolderConfiguration, content []string) error {
 	err := cfg.CheckPath()
 	if err == config.ErrPathMissing {
 		if err = cfg.CreateRoot(); err != nil {
@@ -2099,7 +2102,7 @@ func (m *model) SetIgnores(folder string, content []string) error {
 	}
 
 	m.fmut.RLock()
-	runner, ok := m.folderRunners[folder]
+	runner, ok := m.folderRunners[cfg.ID]
 	m.fmut.RUnlock()
 	if ok {
 		runner.ScheduleScan()

+ 3 - 3
lib/model/model_test.go

@@ -1515,7 +1515,7 @@ func TestIgnores(t *testing.T) {
 		t.Error("No error")
 	}
 
-	// Invalid path, marker should be missing, hence returns an error.
+	// Invalid path, treated like no patterns at all.
 	fcfg := config.FolderConfiguration{ID: "fresh", Path: "XXX"}
 	ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(m.cfg.Options().CacheIgnoredFiles))
 	m.fmut.Lock()
@@ -1524,8 +1524,8 @@ func TestIgnores(t *testing.T) {
 	m.fmut.Unlock()
 
 	_, _, err = m.LoadIgnores("fresh")
-	if err == nil {
-		t.Error("No error")
+	if err != nil {
+		t.Error("Got error for inexistent folder path")
 	}
 
 	// Repeat tests with paused folder

+ 7 - 2
proto/lib/config/config.proto

@@ -24,6 +24,11 @@ message Configuration {
 }
 
 message Defaults {
-    FolderConfiguration folder = 1;
-    DeviceConfiguration device = 2;
+    FolderConfiguration folder  = 1;
+    DeviceConfiguration device  = 2;
+    Ignores             ignores = 3;
+}
+
+message Ignores {
+    repeated string lines = 1;
 }