Show files excluded via 'Exclude' section in source picker.
Add more metadata to the file list returned by server, so that the source file picker can display filters based on file attributes. Correctly display 'X' in file list if a file is excluded by attributes and not included as a source (#4751). Fix directory separator issue when loading the page and prevent opening folders in source file list.
This commit is contained in:
@@ -43,9 +43,21 @@ namespace Duplicati.Server.Serializable
|
||||
/// </summary>
|
||||
public bool hidden { get; set; }
|
||||
/// <summary>
|
||||
/// True if the element has the system file attribute
|
||||
/// </summary>
|
||||
public bool systemFile { get; set; }
|
||||
/// <summary>
|
||||
/// True if the element is marked as temporary
|
||||
/// </summary>
|
||||
public bool temporary { get; set; }
|
||||
/// <summary>
|
||||
/// True if the element is a symlink
|
||||
/// </summary>
|
||||
public bool symlink { get; set; }
|
||||
/// <summary>
|
||||
/// Size of the file. -1 if directory or inaccessible
|
||||
/// </summary>
|
||||
public long fileSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new TreeNode
|
||||
|
||||
@@ -19,9 +19,9 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using Duplicati.Library.Snapshots;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.Common;
|
||||
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.Common;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Filesystem : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented
|
||||
@@ -130,7 +130,8 @@ namespace Duplicati.Server.WebServer.RESTMethods
|
||||
|
||||
if (specialtoken != null)
|
||||
{
|
||||
res = res.Select(x => {
|
||||
res = res.Select(x =>
|
||||
{
|
||||
x.resolvedpath = x.id;
|
||||
x.id = specialtoken + x.id.Substring(specialpath.Length);
|
||||
return x;
|
||||
@@ -208,6 +209,13 @@ namespace Duplicati.Server.WebServer.RESTMethods
|
||||
return false;
|
||||
};
|
||||
|
||||
Func<string, long> getFileSize = (p) =>
|
||||
{
|
||||
try { return new FileInfo(p).Length; }
|
||||
catch { }
|
||||
return -1;
|
||||
};
|
||||
|
||||
foreach (var s in SystemIO.IO_OS.EnumerateFileSystemEntries(entrypath)
|
||||
// Group directories first
|
||||
.OrderByDescending(f => SystemIO.IO_OS.GetFileAttributes(f) & FileAttributes.Directory)
|
||||
@@ -222,6 +230,9 @@ namespace Duplicati.Server.WebServer.RESTMethods
|
||||
var isFolder = (attr & FileAttributes.Directory) != 0;
|
||||
var isFile = !isFolder;
|
||||
var isHidden = (attr & FileAttributes.Hidden) != 0;
|
||||
bool isSystem = (attr & FileAttributes.System) != 0;
|
||||
bool isTemporary = (attr & FileAttributes.Temporary) != 0;
|
||||
long fileSize = -1;
|
||||
|
||||
var accessible = isFile || canAccess(s);
|
||||
var isLeaf = isFile || !accessible || isEmptyFolder(s);
|
||||
@@ -233,12 +244,20 @@ namespace Duplicati.Server.WebServer.RESTMethods
|
||||
if (!showHidden && isHidden)
|
||||
continue;
|
||||
|
||||
if (isFile)
|
||||
{
|
||||
fileSize = getFileSize(s);
|
||||
}
|
||||
|
||||
tn = new Serializable.TreeNode()
|
||||
{
|
||||
id = rawid,
|
||||
text = SystemIO.IO_OS.PathGetFileName(s),
|
||||
hidden = isHidden,
|
||||
symlink = isSymlink,
|
||||
temporary = isTemporary,
|
||||
systemFile = isSystem,
|
||||
fileSize = fileSize,
|
||||
iconCls = isFolder ? (accessible ? (isSymlink ? "x-tree-icon-symlink" : "x-tree-icon-parent") : "x-tree-icon-locked") : "x-tree-icon-leaf",
|
||||
leaf = isLeaf
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ backupApp.controller('EditBackupController', function ($rootScope, $scope, $rout
|
||||
|
||||
$scope.ExcludeAttributes = [];
|
||||
$scope.ExcludeLargeFiles = false;
|
||||
$scope.ExcludeFileSize = null;
|
||||
|
||||
$scope.fileAttributes = [
|
||||
{'name': gettextCatalog.getString('Hidden files'), 'value': 'hidden'},
|
||||
@@ -676,10 +677,19 @@ backupApp.controller('EditBackupController', function ($rootScope, $scope, $rout
|
||||
$scope.$watch("Options['--compression-module']", reloadOptionsList);
|
||||
$scope.$watch("Backup.TargetURL", reloadOptionsList);
|
||||
$scope.$on('systeminfochanged', reloadOptionsList);
|
||||
$scope.$watch('ExcludeLargeFiles', function() {
|
||||
if ($scope.Options != null && $scope.Options['--skip-files-larger-than'] == null)
|
||||
$scope.Options['--skip-files-larger-than'] = '100MB';
|
||||
});
|
||||
$scope.$watch('ExcludeLargeFiles', function(enabled) {
|
||||
if ($scope.Options != null) {
|
||||
if ($scope.Options['--skip-files-larger-than'] == null) {
|
||||
$scope.Options['--skip-files-larger-than'] = '100MB';
|
||||
}
|
||||
$scope.ExcludeFileSize = enabled ? AppUtils.parseSizeString($scope.Options['--skip-files-larger-than']) : null;
|
||||
}
|
||||
}, true);
|
||||
$scope.$watch("Options['--skip-files-larger-than']", function (value) {
|
||||
if ($scope.ExcludeLargeFiles) {
|
||||
$scope.ExcludeFileSize = AppUtils.parseSizeString(value);
|
||||
}
|
||||
}, true);
|
||||
$scope.$watch("Schedule.AllowedDays", checkAllowedDaysConfig, true);
|
||||
|
||||
if ($routeParams.backupid == null) {
|
||||
|
||||
@@ -5,11 +5,13 @@ backupApp.directive('sourceFolderPicker', function() {
|
||||
scope: {
|
||||
ngSources: '=',
|
||||
ngFilters: '=',
|
||||
ngShowHidden: '='
|
||||
ngShowHidden: '=',
|
||||
ngExcludeAttributes: '=',
|
||||
ngExcludeSize: '='
|
||||
},
|
||||
templateUrl: 'templates/sourcefolderpicker.html',
|
||||
|
||||
controller: function($scope, $timeout, SystemInfo, AppService, AppUtils, gettextCatalog, $anchorScroll) {
|
||||
controller: function($scope, $timeout, SystemInfo, AppService, AppUtils, DialogService, gettextCatalog, $anchorScroll) {
|
||||
|
||||
var scope = $scope;
|
||||
scope.systeminfo = SystemInfo.watch($scope);
|
||||
@@ -25,11 +27,11 @@ backupApp.directive('sourceFolderPicker', function() {
|
||||
var filterList = null;
|
||||
var displayMap = {};
|
||||
|
||||
var dirsep = scope.systeminfo.DirectorySeparator || '/';
|
||||
scope.dirsep = null;
|
||||
|
||||
function compareablePath(path) {
|
||||
if (path.substr(0, 1) == '%' && path.substr(path.length - 1, 1) == '%')
|
||||
path += dirsep;
|
||||
path += scope.dirsep;
|
||||
|
||||
return scope.systeminfo.CaseSensitiveFilesystem ? path : path.toLowerCase();
|
||||
}
|
||||
@@ -66,7 +68,7 @@ backupApp.directive('sourceFolderPicker', function() {
|
||||
n.iconCls = 'x-tree-icon-mssql';
|
||||
else if (defunctmap[cp])
|
||||
n.iconCls = 'x-tree-icon-broken';
|
||||
else if (cp.substr(cp.length - 1, 1) != dirsep)
|
||||
else if (cp.substr(cp.length - 1, 1) != scope.dirsep)
|
||||
n.iconCls = 'x-tree-icon-leaf';
|
||||
|
||||
setEntryType(n);
|
||||
@@ -118,20 +120,18 @@ backupApp.directive('sourceFolderPicker', function() {
|
||||
}
|
||||
|
||||
function buildidlookup(sources, map) {
|
||||
var dirsep = scope.systeminfo.DirectorySeparator || '/';
|
||||
|
||||
map = map || {};
|
||||
|
||||
for(var n in sources) {
|
||||
var parts = compareablePath(n).split(dirsep);
|
||||
var parts = compareablePath(n).split(scope.dirsep);
|
||||
var p = [];
|
||||
|
||||
for(var pi in parts) {
|
||||
p.push(parts[pi]);
|
||||
var r = p.join(dirsep);
|
||||
var r = p.join(scope.dirsep);
|
||||
var l = r.substr(r.length - 1, 1);
|
||||
if (l != dirsep)
|
||||
r += dirsep;
|
||||
if (l != scope.dirsep)
|
||||
r += scope.dirsep;
|
||||
map[r] = true;
|
||||
}
|
||||
}
|
||||
@@ -139,6 +139,64 @@ backupApp.directive('sourceFolderPicker', function() {
|
||||
return map;
|
||||
}
|
||||
|
||||
function nodeExcludedByAttributes(n) {
|
||||
// Check ExcludeAttributes
|
||||
if (scope.ngExcludeAttributes != null && scope.ngExcludeAttributes.length > 0) {
|
||||
if (scope.ngExcludeAttributes.indexOf('hidden') != -1 && n.hidden) {
|
||||
return true;
|
||||
}
|
||||
if (scope.ngExcludeAttributes.indexOf('system') != -1 && n.systemFile) {
|
||||
return true;
|
||||
}
|
||||
if (scope.ngExcludeAttributes.indexOf('temporary') != -1 && n.temporary) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function nodeExcludedBySize(n) {
|
||||
if(scope.ngExcludeSize != null) {
|
||||
return n.fileSize > scope.ngExcludeSize;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldIncludeNode(n, checkSize) {
|
||||
if (checkSize === undefined) {
|
||||
checkSize = true;
|
||||
}
|
||||
|
||||
// ExcludeSize overrides source paths
|
||||
if (checkSize && nodeExcludedBySize(n)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if explicitly included in sources
|
||||
if (sourcemap[compareablePath(n.id)]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Result is true if included, false if excluded, null if none match
|
||||
var result = null;
|
||||
|
||||
// Check filter expression
|
||||
if (filterList == null)
|
||||
result = excludemap[compareablePath(n.id)] ? false : null;
|
||||
else {
|
||||
result = AppUtils.evalFilter(n.id, filterList, null);
|
||||
}
|
||||
|
||||
if (result !== false && nodeExcludedByAttributes(n)) {
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (result === null) {
|
||||
// Include by default
|
||||
result = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function updateIncludeFlags(root, parentFlag) {
|
||||
if (root != null)
|
||||
root = {children: [root], include: parentFlag};
|
||||
@@ -147,13 +205,10 @@ backupApp.directive('sourceFolderPicker', function() {
|
||||
if (n.root)
|
||||
return null;
|
||||
|
||||
if (sourcemap[compareablePath(n.id)])
|
||||
if (sourcemap[compareablePath(n.id)] && !nodeExcludedBySize(n))
|
||||
n.include = '+';
|
||||
else if (p != null && p.include == '+') {
|
||||
if (filterList == null)
|
||||
n.include = excludemap[compareablePath(n.id)] ? '-' : '+';
|
||||
else
|
||||
n.include = AppUtils.evalFilter(n.id, filterList) ? '+' : '-';
|
||||
n.include = shouldIncludeNode(n) ? '+' : '-';
|
||||
}
|
||||
else if (p != null && p.include == '-')
|
||||
n.include = '-';
|
||||
@@ -176,15 +231,15 @@ backupApp.directive('sourceFolderPicker', function() {
|
||||
|
||||
var anySpecials = false;
|
||||
|
||||
for(var i = 0; i < (scope.ngFilters || []).length; i++) {
|
||||
var f = AppUtils.splitFilterIntoTypeAndBody(scope.ngFilters[i], dirsep);
|
||||
for (var i = 0; i < (scope.ngFilters || []).length; i++) {
|
||||
var f = AppUtils.splitFilterIntoTypeAndBody(scope.ngFilters[i], scope.dirsep);
|
||||
if (f != null) {
|
||||
if (f[1].indexOf('?') != -1 || f[1].indexOf('*') != -1)
|
||||
if (f[0].indexOf('+') == 0 || 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;
|
||||
excludemap[compareablePath(f[1] + scope.dirsep)] = true;
|
||||
else
|
||||
anySpecials = true;
|
||||
}
|
||||
@@ -192,6 +247,14 @@ backupApp.directive('sourceFolderPicker', function() {
|
||||
|
||||
if (anySpecials)
|
||||
filterList = AppUtils.filterListToRegexps(scope.ngFilters, scope.systeminfo.CaseSensitiveFilesystem);
|
||||
}
|
||||
|
||||
function syncTreeWithLists() {
|
||||
if (scope.ngSources == null || sourceNodeChildren == null || scope.dirsep == null)
|
||||
return;
|
||||
|
||||
sourcemap = {};
|
||||
updateFilterList();
|
||||
|
||||
sourceNodeChildren.length = 0;
|
||||
|
||||
@@ -236,7 +299,7 @@ backupApp.directive('sourceFolderPicker', function() {
|
||||
|
||||
var p = scope.ngSources[i];
|
||||
if (p.substr(0, 1) == '%' && p.substr(p.length - 1, 1) == '%')
|
||||
p += dirsep;
|
||||
p += scope.dirsep;
|
||||
|
||||
AppService.post('/filesystem/validate', {path: p}).then(function(data) {
|
||||
defunctmap[compareablePath(data.config.data.path)] = false;
|
||||
@@ -259,6 +322,14 @@ backupApp.directive('sourceFolderPicker', function() {
|
||||
|
||||
$scope.$watch('ngSources', syncTreeWithLists, true);
|
||||
$scope.$watch('ngFilters', syncTreeWithLists, true);
|
||||
$scope.$watch('ngExcludeAttributes', syncTreeWithLists, true);
|
||||
$scope.$watch('ngExcludeSize', syncTreeWithLists, true);
|
||||
$scope.$watch('systeminfo.DirectorySeparator', function (val, oldVal) {
|
||||
if (val != null) {
|
||||
scope.dirsep = val;
|
||||
syncTreeWithLists();
|
||||
}
|
||||
}, true);
|
||||
|
||||
function findParent(id) {
|
||||
var r = {};
|
||||
@@ -282,10 +353,8 @@ backupApp.directive('sourceFolderPicker', function() {
|
||||
}
|
||||
|
||||
$scope.toggleCheck = function(node) {
|
||||
dirsep = scope.systeminfo.DirectorySeparator || '/';
|
||||
|
||||
var c = compareablePath(node.id);
|
||||
var c_is_dir = c.substr(c.length - 1, 1) == dirsep;
|
||||
var c_is_dir = c.substr(c.length - 1, 1) == scope.dirsep;
|
||||
|
||||
if (node.include == null || node.include == ' ') {
|
||||
|
||||
@@ -296,7 +365,7 @@ backupApp.directive('sourceFolderPicker', function() {
|
||||
if (s == c)
|
||||
return;
|
||||
|
||||
if (s.substr(s.length - 1, 1) == dirsep && c.indexOf(s) == 0) {
|
||||
if (s.substr(s.length - 1, 1) == scope.dirsep && c.indexOf(s) == 0) {
|
||||
return;
|
||||
} else if (s.indexOf(c) == 0) {
|
||||
scope.ngSources.splice(i, 1);
|
||||
@@ -311,11 +380,11 @@ backupApp.directive('sourceFolderPicker', function() {
|
||||
removePathFromArray(scope.ngSources, node.id);
|
||||
|
||||
for(var i = scope.ngFilters.length - 1; i >= 0; i--) {
|
||||
var n = AppUtils.splitFilterIntoTypeAndBody(scope.ngFilters[i], dirsep);
|
||||
var n = AppUtils.splitFilterIntoTypeAndBody(scope.ngFilters[i], scope.dirsep);
|
||||
if (n != null) {
|
||||
if (c_is_dir) {
|
||||
if (n[0] == '-path' || n[0] == '-folder') {
|
||||
if (compareablePath(n[1] + (n[0] == '-folder' ? dirsep : '')).indexOf(c) == 0)
|
||||
if (compareablePath(n[1] + (n[0] == '-folder' ? scope.dirsep : '')).indexOf(c) == 0)
|
||||
scope.ngFilters.splice(i, 1);
|
||||
}
|
||||
} else {
|
||||
@@ -340,7 +409,7 @@ backupApp.directive('sourceFolderPicker', function() {
|
||||
node.expanded = !node.expanded;
|
||||
self = this;
|
||||
|
||||
if (node.root || node.iconCls == 'x-tree-icon-leaf' || node.iconCls == 'x-tree-icon-locked'
|
||||
if (node.root || node.leaf || node.iconCls == 'x-tree-icon-leaf' || node.iconCls == 'x-tree-icon-locked'
|
||||
|| node.iconCls == 'x-tree-icon-hyperv' || node.iconCls == 'x-tree-icon-hypervmachine'
|
||||
|| node.iconCls == 'x-tree-icon-mssql' || node.iconCls == 'x-tree-icon-mssqldb')
|
||||
return;
|
||||
|
||||
@@ -228,14 +228,27 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer
|
||||
};
|
||||
|
||||
|
||||
this.splitSizeString = function(val) {
|
||||
this.splitSizeString = function (val) {
|
||||
var m = (/(\d*)(\w*)/mg).exec(val);
|
||||
var mul = null;
|
||||
if (!m)
|
||||
return [parseInt(val), null];
|
||||
else
|
||||
return [parseInt(m[1]), m[2]];
|
||||
}
|
||||
};
|
||||
|
||||
this.parseSizeString = function (val) {
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
var split = this.splitSizeString(val);
|
||||
var formatSizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var idx = formatSizes.indexOf((split[1]||'').toUpperCase());
|
||||
if (idx == -1) {
|
||||
idx = 0;
|
||||
}
|
||||
return split[0] * Math.pow(1024, idx);
|
||||
};
|
||||
|
||||
|
||||
this.toDisplayDateAndTime = function(dt) {
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
<a href class="{{EditSourceAdvanced ? 'inactive' : ''}}" ng-click="showhiddenfolders = !showhiddenfolders"><i class="fa {{showhiddenfolders ? 'fa-check' : ''}}"></i> {{'Show hidden folders' | translate}}</a>
|
||||
</div>
|
||||
<div class="resizable filepicker" ng-hide="EditSourceAdvanced">
|
||||
<source-folder-picker ng-sources="Backup.Sources" ng-filters="Backup.Filters" ng-show-hidden="showhiddenfolders"></source-folder-picker>
|
||||
<source-folder-picker ng-sources="Backup.Sources" ng-filters="Backup.Filters" ng-show-hidden="showhiddenfolders" ng-exclude-attributes="ExcludeAttributes" ng-exclude-size="ExcludeFileSize"></source-folder-picker>
|
||||
</div>
|
||||
<div class="input textarea" ng-show="EditSourceAdvanced">
|
||||
<label for="SourcePathList" translate>Source folders</label>
|
||||
@@ -237,7 +237,7 @@
|
||||
<label for="exclude-large-files" translate>Files larger than:</label>
|
||||
|
||||
<input type="number" parse-size-number="uppercase" ng-model="Options['--skip-files-larger-than']" id="exclude-larger-than-number" ng-disabled="!ExcludeLargeFiles" value="1" />
|
||||
<select parse-size-multiplier="uppercase" id="exclude-larger-than-multiplier" ng-model="Options['--skip-files-larger-than']" ng-disabled="!ExcludeLargeFiles" ng-options="item.value as item.name for item in AppUtils.fileSizeMultipliers">
|
||||
<select parse-size-multiplier="uppercase" id="exclude-larger-than-multiplier" ng-model="Options['--skip-files-larger-than']" ng-disabled="!ExcludeLargeFiles" ng-options="item.value.toUpperCase() as item.name for item in AppUtils.fileSizeMultipliers">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user