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

This commit is contained in:
Simon Frei 2022-01-13 23:38:21 +01:00 committed by GitHub
parent 40bb52fdd8
commit 21d04b895a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 718 additions and 218 deletions

View File

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

View File

@ -7,8 +7,12 @@
package cli package cli
import ( import (
"bufio"
"fmt" "fmt"
"path/filepath"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/fs"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -38,6 +42,12 @@ var operationCommand = cli.Command{
ArgsUsage: "[folder id]", ArgsUsage: "[folder id]",
Action: expects(1, foldersOverride), 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") 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
}

View File

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

View File

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

View File

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

View File

@ -3,14 +3,14 @@
<form role="form" name="deviceEditor"> <form role="form" name="deviceEditor">
<ul class="nav nav-tabs" ng-init="loadFormIntoScope(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 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> <li><a data-toggle="tab" href="#device-advanced"><span class="fas fa-cogs"></span> <span translate>Advanced</span></a></li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div id="device-general" class="tab-pane in active"> <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> <label translate for="deviceID">Device ID</label>
<div ng-if="!editingExisting"> <div ng-if="!editingDeviceExisting()">
<div class="input-group"> <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" /> <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"> <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> <span translate ng-if="deviceEditor.deviceID.$error.unique && deviceEditor.deviceID.$dirty">A device with that ID is already added.</span>
</p> </p>
</div> </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="well well-sm text-monospace form-control" style="height: auto;" select-on-click>{{currentDevice.deviceID}}</div>
<div class="input-group-btn"> <div class="input-group-btn">
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#idqr"> <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> <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> </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="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="form-group">
@ -172,7 +172,7 @@
<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="fas fa-times"></span>&nbsp;<span translate>Close</span> <span class="fas fa-times"></span>&nbsp;<span translate>Close</span>
</button> </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"> <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> <span class="fas fa-minus-circle"></span>&nbsp;<span translate>Remove</span>
</button> </button>

View File

@ -2,44 +2,44 @@
<div class="modal-body"> <div class="modal-body">
<form role="form" name="folderEditor"> <form role="form" name="folderEditor">
<ul class="nav nav-tabs" ng-init="loadFormIntoScope(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 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><a data-toggle="tab" href="#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-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-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-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 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><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'}"><a data-toggle="tab" href="{{currentFolder._editing == 'add-ignores' ? '' : '#folder-advanced'}}"><span class="fas fa-cogs"></span> <span translate>Advanced</span></a></li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div id="folder-general" class="tab-pane in active"> <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> <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}}" /> <input name="folderLabel" id="folderLabel" class="form-control" type="text" ng-model="currentFolder.label" value="{{currentFolder.label}}" />
<p class="help-block"> <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> <span translate ng-if="folderEditor.folderLabel.$valid || folderEditor.folderLabel.$pristine">Optional descriptive label for the folder. Can be different on each device.</span>
</p> </p>
</div> </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> <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"> <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.$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.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-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> </p>
</div> </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> <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"> <datalist id="directory-list">
<option ng-repeat="directory in directoryList" value="{{ directory }}" /> <option ng-repeat="directory in directoryList" value="{{ directory }}" />
</datalist> </datalist>
<p class="help-block"> <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 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}}" 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 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}}" 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 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> </span>
@ -148,33 +148,42 @@
</div> </div>
</div> </div>
<div ng-if="!editingDefaults" id="folder-ignores" class="tab-pane"> <div id="folder-ignores" class="tab-pane" ng-switch="currentFolder._editing">
<p translate>Enter ignore patterns, one per line.</p> <div ng-switch-when="add">
<div ng-class="{'has-error': ignores.error != null}"> <label>
<textarea class="form-control" rows="5" ng-model="ignores.text" ng-disabled="ignores.disabled"></textarea> <input type="checkbox" ng-model="currentFolder._addIgnores" >&nbsp;<span translate>Add ignore patterns</span>
<p class="help-block" ng-if="ignores.error"> </label>
{{ignores.error}} <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>
</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> </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>
<div id="folder-advanced" class="tab-pane"> <div id="folder-advanced" class="tab-pane">
@ -205,17 +214,17 @@
<div class="col-md-6 form-group"> <div class="col-md-6 form-group">
<label translate>Folder Type</label> <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> &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="sendreceive" translate>Send &amp; Receive</option>
<option value="sendonly" translate>Send Only</option> <option value="sendonly" translate>Send Only</option>
<option value="receiveonly" translate>Receive 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> </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 == '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 == '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="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="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="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%}" can only be set when adding a new folder.</p>
</div> </div>
<div class="col-md-6 form-group"> <div class="col-md-6 form-group">
<label translate>File Pull Order</label> <label translate>File Pull Order</label>
@ -274,7 +283,7 @@
<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="fas fa-times"></span>&nbsp;<span translate>Close</span> <span class="fas fa-times"></span>&nbsp;<span translate>Close</span>
</button> </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> <span class="fas fa-minus-circle"></span>&nbsp;<span translate>Remove</span>
</button> </button>
</div> </div>

View File

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

View File

@ -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) { func (c *configMuxBuilder) registerOptions(path string) {
c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) { c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, c.cfg.Options()) sendJSON(w, c.cfg.Options())

View File

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

View File

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

View File

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

View File

@ -40,6 +40,16 @@ type Wrapper struct {
defaultFolderReturnsOnCall map[int]struct { defaultFolderReturnsOnCall map[int]struct {
result1 config.FolderConfiguration 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) DeviceStub func(protocol.DeviceID) (config.DeviceConfiguration, bool)
deviceMutex sync.RWMutex deviceMutex sync.RWMutex
deviceArgsForCall []struct { deviceArgsForCall []struct {
@ -449,6 +459,59 @@ func (fake *Wrapper) DefaultFolderReturnsOnCall(i int, result1 config.FolderConf
}{result1} }{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) { func (fake *Wrapper) Device(arg1 protocol.DeviceID) (config.DeviceConfiguration, bool) {
fake.deviceMutex.Lock() fake.deviceMutex.Lock()
ret, specificReturn := fake.deviceReturnsOnCall[len(fake.deviceArgsForCall)] ret, specificReturn := fake.deviceReturnsOnCall[len(fake.deviceArgsForCall)]
@ -1752,6 +1815,8 @@ func (fake *Wrapper) Invocations() map[string][][]interface{} {
defer fake.defaultDeviceMutex.RUnlock() defer fake.defaultDeviceMutex.RUnlock()
fake.defaultFolderMutex.RLock() fake.defaultFolderMutex.RLock()
defer fake.defaultFolderMutex.RUnlock() defer fake.defaultFolderMutex.RUnlock()
fake.defaultIgnoresMutex.RLock()
defer fake.defaultIgnoresMutex.RUnlock()
fake.deviceMutex.RLock() fake.deviceMutex.RLock()
defer fake.deviceMutex.RUnlock() defer fake.deviceMutex.RUnlock()
fake.deviceListMutex.RLock() fake.deviceListMutex.RLock()

View File

@ -100,6 +100,7 @@ type Wrapper interface {
GUI() GUIConfiguration GUI() GUIConfiguration
LDAP() LDAPConfiguration LDAP() LDAPConfiguration
Options() OptionsConfiguration Options() OptionsConfiguration
DefaultIgnores() Ignores
Folder(id string) (FolderConfiguration, bool) Folder(id string) (FolderConfiguration, bool)
Folders() map[string]FolderConfiguration Folders() map[string]FolderConfiguration
@ -437,6 +438,13 @@ func (w *wrapper) GUI() GUIConfiguration {
return w.cfg.GUI.Copy() 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 // IgnoredDevice returns whether or not connection attempts from the given
// device should be silently ignored. // device should be silently ignored.
func (w *wrapper) IgnoredDevice(id protocol.DeviceID) bool { func (w *wrapper) IgnoredDevice(id protocol.DeviceID) bool {

View File

@ -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 { if len(ccDeviceInfos.remote.EncryptionPasswordToken) > 0 || len(ccDeviceInfos.local.EncryptionPasswordToken) > 0 {
fcfg.Type = config.FolderTypeReceiveEncrypted 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) 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 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 { if !ignoresOk {
ignores = ignore.New(cfg.Filesystem()) ignores = ignore.New(cfg.Filesystem())
} }
@ -2081,7 +2081,10 @@ func (m *model) SetIgnores(folder string, content []string) error {
if !ok { if !ok {
return fmt.Errorf("folder %s does not exist", cfg.Description()) 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() err := cfg.CheckPath()
if err == config.ErrPathMissing { if err == config.ErrPathMissing {
if err = cfg.CreateRoot(); err != nil { if err = cfg.CreateRoot(); err != nil {
@ -2099,7 +2102,7 @@ func (m *model) SetIgnores(folder string, content []string) error {
} }
m.fmut.RLock() m.fmut.RLock()
runner, ok := m.folderRunners[folder] runner, ok := m.folderRunners[cfg.ID]
m.fmut.RUnlock() m.fmut.RUnlock()
if ok { if ok {
runner.ScheduleScan() runner.ScheduleScan()

View File

@ -1515,7 +1515,7 @@ func TestIgnores(t *testing.T) {
t.Error("No error") 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"} fcfg := config.FolderConfiguration{ID: "fresh", Path: "XXX"}
ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(m.cfg.Options().CacheIgnoredFiles)) ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(m.cfg.Options().CacheIgnoredFiles))
m.fmut.Lock() m.fmut.Lock()
@ -1524,8 +1524,8 @@ func TestIgnores(t *testing.T) {
m.fmut.Unlock() m.fmut.Unlock()
_, _, err = m.LoadIgnores("fresh") _, _, err = m.LoadIgnores("fresh")
if err == nil { if err != nil {
t.Error("No error") t.Error("Got error for inexistent folder path")
} }
// Repeat tests with paused folder // Repeat tests with paused folder

View File

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