From 7a3dece44a44481953ec26a091bce0f584d72e98 Mon Sep 17 00:00:00 2001 From: Jojo-1000 <33495614+Jojo-1000@users.noreply.github.com> Date: Sat, 3 Jun 2023 23:22:36 +0200 Subject: [PATCH 1/5] Fix UI display of filtered files with wildcards. The previous JS code incorrectly converted glob expressions to regexp which prevented them from showing the correct file states in the list. This was only a UI bug and has no effect on the backend. --- .../ngax/scripts/directives/sourceFolderPicker.js | 4 +++- .../webroot/ngax/scripts/services/AppUtils.js | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Duplicati/Server/webroot/ngax/scripts/directives/sourceFolderPicker.js b/Duplicati/Server/webroot/ngax/scripts/directives/sourceFolderPicker.js index 27b77a6bd..afa88466c 100644 --- a/Duplicati/Server/webroot/ngax/scripts/directives/sourceFolderPicker.js +++ b/Duplicati/Server/webroot/ngax/scripts/directives/sourceFolderPicker.js @@ -178,7 +178,9 @@ backupApp.directive('sourceFolderPicker', function() { for(var i = 0; i < (scope.ngFilters || []).length; i++) { var f = AppUtils.splitFilterIntoTypeAndBody(scope.ngFilters[i], dirsep); if (f != null) { - if (f[0] == '-path') + if (f[1].indexOf('?') != -1 || f[1].indexOf('*') != -1) + anySpecials = true; + else if (f[0] == '-path') excludemap[compareablePath(f[1])] = true; else if (f[0] == '-folder') excludemap[compareablePath(f[1] + dirsep)] = true; diff --git a/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js b/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js index 0817690d2..95f2ce91e 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js @@ -422,6 +422,13 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer return str.replace( new RegExp( "(" + this.preg_quote(pattern) + ")" , 'g' ), replacement ); }; + this.globToRegexp = function (str) { + // Escape special chars, except ? and * + str = (str + '').replace(/([\\\.\+\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1"); + // Replace ? and * with .? and .* + return str.replace(/(\?|\*)/g, ".$1"); + }; + this.format = function() { if (arguments == null || arguments.length < 1) return null; @@ -609,7 +616,7 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer if (rx) filter = filter.substr(1, filter.length - 2); else - filter = this.replace_all(this.replace_all(this.preg_quote(filter), '*', '.*'), '?', '.'); + filter = this.globToRegexp(filter); try { res.push([flag == '+', new RegExp(filter, caseSensitive ? 'g' : 'gi')]); @@ -621,9 +628,11 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer }; this.evalFilter = function(path, filters, include) { - for(var i = 0; i < filters.length; i++) { + for (var i = 0; i < filters.length; i++) { var m = path.match(filters[i][1]); - if (m && m.length == 1 && m[0].length == path.length) + // Regex such as .* might match empty string at the end which is unwanted + // Check that the first match covers the full string + if (m && m.length >= 1 && m[0].length == path.length) return filters[i][0]; } From f4c04da31a4fd3e0df5eaa355b3b74b156a85e5c Mon Sep 17 00:00:00 2001 From: Jojo-1000 <33495614+Jojo-1000@users.noreply.github.com> Date: Sun, 4 Jun 2023 20:07:15 +0200 Subject: [PATCH 2/5] Fix bugs with simple edit filter UI. No longer adds invisible dir separators to filter list. --- .../scripts/directives/parseFilterType.js | 21 ++++++++++--------- .../webroot/ngax/scripts/services/AppUtils.js | 7 ++++++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Duplicati/Server/webroot/ngax/scripts/directives/parseFilterType.js b/Duplicati/Server/webroot/ngax/scripts/directives/parseFilterType.js index 0fc22b2fc..90aee7227 100644 --- a/Duplicati/Server/webroot/ngax/scripts/directives/parseFilterType.js +++ b/Duplicati/Server/webroot/ngax/scripts/directives/parseFilterType.js @@ -3,11 +3,13 @@ backupApp.directive('parseFilterType', function(AppUtils, SystemInfo) { restrict: 'A', require: ['ngModel'], link: function(scope, element, attr, ctrl) { - var body = null; - ctrl[0].$parsers.push(function(txt) { var dirsep = SystemInfo.state.DirectorySeparator || '/'; - return AppUtils.buildFilter(txt, body, dirsep); + // Store type in scope for parseFilterContent directive + // This works because ng-repeat creates a child scope for each iteration + // It is still glitchy in some edge cases, but should be more consistent + scope.filterType = txt; + return AppUtils.buildFilter(txt, scope.filterBody, dirsep); }); ctrl[0].$formatters.push(function(src) { @@ -18,8 +20,7 @@ backupApp.directive('parseFilterType', function(AppUtils, SystemInfo) { return null; } - body = parts[1]; - + scope.filterType = parts[0]; return parts[0]; }); } @@ -31,23 +32,23 @@ backupApp.directive('parseFilterContent', function(AppUtils, SystemInfo) { restrict: 'A', require: ['ngModel'], link: function(scope, element, attr, ctrl) { - - var type = null; - ctrl[0].$parsers.push(function(txt) { var dirsep = SystemInfo.state.DirectorySeparator || '/'; - return AppUtils.buildFilter(type, txt, dirsep); + // Store type in scope for parseFilterType directive + scope.filterBody = txt; + return AppUtils.buildFilter(scope.filterType, txt, dirsep); }); ctrl[0].$formatters.push(function(src) { var dirsep = SystemInfo.state.DirectorySeparator || '/'; + // Remove double trailing slashes var parts = AppUtils.splitFilterIntoTypeAndBody(src, dirsep); if (parts == null) { type = null; return null; } - type = parts[0]; + scope.filterBody = parts[1]; return parts[1]; }); } diff --git a/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js b/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js index 95f2ce91e..242bb2290 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js @@ -138,11 +138,13 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer key: '-folder', prefix: '-', suffix: '!', + stripSep: true, rx: '\\-(.*)\\!' }, { name: gettextCatalog.getString('Exclude file'), key: '-path', prefix: '-', + stripSep: true, exclude: ['*', '?', '{'], rx: '\\-([^\\[\\{\\*\\?]+)' }, { @@ -597,7 +599,10 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer if (pre.length >= 2 && pre[1] == '[') { //Regexp encode body .... } - + if (f.stripSep == true && body[body.length - 1] == dirsep) { + // Remove trailing dirsep + body = body.slice(0, -1); + } return pre + body + suf; }; From b94734498e0a55dabe3a5312ac309b1b71810693 Mon Sep 17 00:00:00 2001 From: Jojo-1000 <33495614+Jojo-1000@users.noreply.github.com> Date: Sun, 4 Jun 2023 21:37:53 +0200 Subject: [PATCH 3/5] Fix filter options for web UI command line. Only the last exclude or include option was sent to the backend. Now all include and exclude options are used. --- .../controllers/CommandlineController.js | 15 ++++++++---- .../webroot/ngax/scripts/services/AppUtils.js | 24 +++++++++++++++---- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/CommandlineController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/CommandlineController.js index 2bc18fd0c..38a38886e 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/CommandlineController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/CommandlineController.js @@ -54,11 +54,18 @@ backupApp.controller('CommandlineController', function($scope, $routeParams, $lo combined.unshift($scope.Command); - for(n in opts) { - if (opts[n] == null) + for (n in opts) { + var value = opts[n]; + if (n == 'include' || n == 'exclude') { + // Handle filters that appear multiple times + for (var i = 0; i < value.length; ++i) { + combined.push('--' + n + '=' + value[i]); + } + } else if (value == null) { combined.push(n); - else - combined.push(n + '=' + opts[n]); + } else { + combined.push(n + '=' + value); + } } options = { diff --git a/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js b/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js index 242bb2290..50a3ea3bc 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js @@ -259,12 +259,14 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer return new Date(dt); }; - this.parseOptionStrings = function(val, dict, validateCallback) { + this.parseOptionStrings = function (val, dict, validateCallback) { + // Parse options and return a dict with dict['--key']=value options + // Include and exclude filters are returned as arrays in dict['include'], dict['exclude'] if present dict = dict || {}; var lines = null; - if (val != null && typeof(val) == typeof([])) + if (val != null && Array.isArray(val)) lines = val; else lines = this.replace_all(val || '', '\r', '\n').split('\n'); @@ -289,8 +291,18 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer if (validateCallback) if (!validateCallback(dict, key, value)) return null; - - dict['--' + key] = value; + // Special handing for --include and --exclude, which are the only options that can appear multiple times (compare FilterCollector.DoExtractOptions) + // In all other cases, the last value overrides the earlier values + var lower = key.toLowerCase(); + if (lower == 'include' || lower == 'exclude') { + if (!Array.isArray(dict[lower])) { + dict[lower] = [value]; + } else { + dict[lower].push(value); + } + } else { + dict['--' + key] = value; + } } } @@ -302,6 +314,10 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer if (d['--' + k] !== undefined) { DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Duplicate option {{opt}}', { opt: k })); return false; + } else if (k == 'include' || k == 'exclude') { + // Cannot specify filters in extra options + DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Cannot specify filter include or excludes in extra options')); + return false; } return true; From e2569cca68e08c16c94804f3c1b962b3d5dd59b2 Mon Sep 17 00:00:00 2001 From: Jojo-1000 <33495614+Jojo-1000@users.noreply.github.com> Date: Sun, 4 Jun 2023 22:39:32 +0200 Subject: [PATCH 4/5] Improve filter UI type matching. Instead of selecting the first matching in list, which does not recognize regexp without ? or *, choose the type with longest prefix and suffix. --- .../webroot/ngax/scripts/services/AppUtils.js | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js b/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js index 50a3ea3bc..353ed5525 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/AppUtils.js @@ -593,13 +593,25 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer return [type, body]; } - for(var i in this.filterClasses) { + var m = []; + for (var i in this.filterClasses) { var n = matches(src, this.filterClasses[i]); if (n != null) - return n; + m.push(n); } - - return null; + if (m.length == 0) { + return null; + } + // Select match with shortest body, meaning longest prefix and suffix + var shortestIdx = 0; + var shortestLen = src.length; + for (i = 0; i < m.length; ++i) { + if (m[i][1].length < shortestLen) { + shortestIdx = i; + shortestLen = m[i][1].length; + } + } + return m[shortestIdx]; }; this.buildFilter = function(type, body, dirsep) { From 9e9850113e74a0e48276b0003620c68a19aba37b Mon Sep 17 00:00:00 2001 From: Jojo-1000 <33495614+Jojo-1000@users.noreply.github.com> Date: Wed, 14 Jun 2023 20:34:00 +0200 Subject: [PATCH 5/5] Fix highlighting with wildcards in restore file picker. Inputs were interpreted as regexp in the UI instead of globbing expressions which are used in the backend. Closes #2682. --- .../Server/webroot/ngax/scripts/directives/restoreFilePicker.js | 1 + Duplicati/Server/webroot/ngax/templates/restorefilepicker.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js b/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js index 3e71e8648..c5a6b026a 100644 --- a/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js +++ b/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js @@ -21,6 +21,7 @@ backupApp.directive('restoreFilePicker', function() { $scope.systeminfo = SystemInfo.watch($scope); $scope.treedata = []; + $scope.AppUtils = AppUtils; function compareablePath(path) { return scope.systeminfo.CaseSensitiveFilesystem ? path : path.toLowerCase(); diff --git a/Duplicati/Server/webroot/ngax/templates/restorefilepicker.html b/Duplicati/Server/webroot/ngax/templates/restorefilepicker.html index d23d0466e..2141bfe06 100644 --- a/Duplicati/Server/webroot/ngax/templates/restorefilepicker.html +++ b/Duplicati/Server/webroot/ngax/templates/restorefilepicker.html @@ -5,7 +5,7 @@ - +