duplicati/Duplicati/Server/webroot/ngax/scripts/directives/sourceFolderPicker.js

619 lines
23 KiB
JavaScript

backupApp.directive('sourceFolderPicker', function() {
return {
restrict: 'E',
require: ['ngSources', 'ngFilters', '$anchorScroll'],
scope: {
ngSources: '=',
ngFilters: '=',
ngShowHidden: '=',
ngExcludeAttributes: '=',
ngExcludeSize: '='
},
templateUrl: 'templates/sourcefolderpicker.html',
controller: function($scope, $timeout, SystemInfo, AppService, AppUtils, DialogService, gettextCatalog, $anchorScroll) {
var scope = $scope;
scope.systeminfo = SystemInfo.watch($scope);
var sourceNodeChildren = null;
$scope.treedata = {};
$scope.expandedPath = null;
var sourcemap = {};
var excludemap = {};
var defunctmap = {};
var partialincludemap = {};
var filterList = null;
var displayMap = {};
scope.dirsep = null;
function compareablePath(path) {
if (path.substr(0, 1) == '%' && path.substr(path.length - 1, 1) == '%')
path += scope.dirsep;
return scope.systeminfo.CaseSensitiveFilesystem ? path : path.toLowerCase();
}
function setEntryType(n)
{
n.entrytype = AppUtils.getEntryTypeFromIconCls(n.iconCls);
}
function setIconCls(n) {
var cp = compareablePath(n.id);
if (cp == compareablePath('%MY_DOCUMENTS%'))
n.iconCls = 'x-tree-icon-mydocuments';
else if (cp == compareablePath('%MY_MUSIC%'))
n.iconCls = 'x-tree-icon-mymusic';
else if (cp == compareablePath('%MY_PICTURES%'))
n.iconCls = 'x-tree-icon-mypictures';
else if (cp == compareablePath('%DESKTOP%'))
n.iconCls = 'x-tree-icon-desktop';
else if (cp == compareablePath('%HOME%'))
n.iconCls = 'x-tree-icon-home';
else if (cp == compareablePath('%MY_MOVIES%'))
n.iconCls = 'x-tree-icon-mymovies';
else if (cp == compareablePath('%MY_DOWNLOADS%'))
n.iconCls = 'x-tree-icon-mydownloads';
else if (cp == compareablePath('%MY_PUBLIC%'))
n.iconCls = 'x-tree-icon-mypublic';
else if (n.id.substr(0, 9) == "%HYPERV%\\" && n.id.length >= 10) {
n.iconCls = 'x-tree-icon-hypervmachine';
n.tooltip = gettextCatalog.getString("ID:") + " " + n.id.substring(9, n.id.length);
}
else if (n.id.substr(0, 8) == "%HYPERV%")
n.iconCls = 'x-tree-icon-hyperv';
else if (n.id.substr(0, 8) == "%MSSQL%\\" && n.id.length >= 9) {
n.iconCls = 'x-tree-icon-mssqldb';
n.tooltip = gettextCatalog.getString("ID:") + " " + n.id.substring(8, n.id.length);
}
else if (n.id.substr(0, 7) == "%MSSQL%")
n.iconCls = 'x-tree-icon-mssql';
else if (defunctmap[cp])
n.iconCls = 'x-tree-icon-broken';
else if (cp.substr(cp.length - 1, 1) != scope.dirsep)
n.iconCls = 'x-tree-icon-leaf';
setEntryType(n);
}
function indexOfPathInArray(array, item) {
item = compareablePath(item);
for(var ix in array)
if (compareablePath(array[ix]) == item)
return ix;
return -1;
}
function removePathFromArray(array, item) {
var ix = indexOfPathInArray(array, item);
if (ix >= 0) {
array.splice(ix, 1);
return true;
}
return false;
}
function traversenodes(m, start) {
var root = (start || scope.treedata);
if (!root.children)
return;
var work = [];
for(var v in root.children)
work.push([root, root.children[v]]);
while(work.length > 0) {
var x = work.pop();
var r = m(x[1], x[0]);
// false == stop
if (r === false)
return;
// true == no recurse
if (r !== true && x[1].children) {
for(var v in x[1].children)
work.push([x[1], x[1].children[v]]);
}
}
}
function buildidlookup(sources, map) {
map = map || {};
for(var n in sources) {
var parts = compareablePath(n).split(scope.dirsep);
var p = [];
for(var pi in parts) {
p.push(parts[pi]);
var r = p.join(scope.dirsep);
var l = r.substr(r.length - 1, 1);
if (l != scope.dirsep)
r += scope.dirsep;
map[r] = true;
}
}
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};
traversenodes(function(n, p) {
if (n.root)
return null;
if (sourcemap[compareablePath(n.id)] && !nodeExcludedBySize(n))
n.include = '+';
else if (p != null && p.include == '+') {
n.include = shouldIncludeNode(n) ? '+' : '-';
}
else if (p != null && p.include == '-')
n.include = '-';
else if (partialincludemap[compareablePath(n.id)])
n.include = ' ';
else
n.include = null;
}, root);
}
function updateFilterList() {
excludemap = {};
filterList = null;
var anySpecials = false;
for (var i = 0; i < (scope.ngFilters || []).length; i++) {
var f = AppUtils.splitFilterIntoTypeAndBody(scope.ngFilters[i], scope.dirsep);
if (f != null) {
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] + scope.dirsep)] = true;
else
anySpecials = true;
}
}
if (anySpecials)
filterList = AppUtils.filterListToRegexps(scope.ngFilters, scope.systeminfo.CaseSensitiveFilesystem);
}
function buildDisplayName(child, parent) {
var p = parent ? compareablePath(parent.id) : null;
if (p === compareablePath('%HYPERV%') || p === compareablePath('%MSSQL%')) {
return child.text;
}
var parentName = displayMap[compareablePath(parent.id)] || parent.text || '';
return parentName ? (parentName + '\\' + child.text) : child.text;
}
function syncTreeWithLists() {
if (scope.ngSources === null || sourceNodeChildren === null || scope.dirsep === null)
return;
sourcemap = {};
updateFilterList();
sourceNodeChildren.length = 0;
function findInList(lst, path) {
for(var x in lst)
if (compareablePath(lst[x].id) === path)
return x;
return false;
}
function getHyperVText(path, key, name) {
return path === key ? gettextCatalog.getString('All Hyper-V Machines') : gettextCatalog.getString('Hyper-V Machine: {{name}}', { name });
}
for(let i = 0; i < scope.ngSources.length; i++) {
const pathId = scope.ngSources[i];
const path = compareablePath(pathId);
if (path.length === 0)
continue;
sourcemap[path] = true;
let txt = pathId;
const rootMatch = /^%[^%]*%/.exec(path);
if (rootMatch) {
const root = rootMatch[0];
const key = compareablePath(root);
const subPath = pathId.slice(root.length);
txt = (displayMap[key] || root) + subPath;
if (key === compareablePath('%HYPERV%')) {
if (displayMap[compareablePath(path)] === undefined) {
AppService.postJson('/filesystem?onlyfolders=false&showhidden=true', { path: pathId })
.then((res) => {
if (res.data && res.data.length > 0) {
const name = res.data[0].text;
displayMap[compareablePath(path)] = name;
const node = sourceNodeChildren.find(x => compareablePath(x.id) === path);
if (node) {
node.text = getHyperVText(path, key, name);
}
}
});
} else {
txt = getHyperVText(path, key, displayMap[compareablePath(path)]);
}
}
if (key === compareablePath('%MSSQL%')) {
if (path === key) {
txt = gettextCatalog.getString('All Microsoft SQL Servers');
} else {
const normSubPath = subPath && subPath[0] === scope.dirsep ? subPath.slice(1) : subPath;
const parts = (normSubPath || '').split(scope.dirsep).filter(Boolean);
txt = path.endsWith(scope.dirsep)? gettextCatalog.getString('Microsoft SQL Server: {{name}}', {name: parts.join('\\')}) : gettextCatalog.getString('Microsoft SQL Database: {{name}}', {name: parts.join('\\')});
}
displayMap[compareablePath(path)] = txt;
}
}
var n = {
text: txt,
id: pathId,
include: '+',
other: true,
leaf: true
};
setIconCls(n);
sourceNodeChildren.push(n);
if (defunctmap[path] == null && n.iconCls != "x-tree-icon-hyperv" && n.iconCls != "x-tree-icon-hypervmachine" && n.iconCls != "x-tree-icon-mssql" && n.iconCls != "x-tree-icon-mssqldb") {
defunctmap[path] = true;
var p = pathId;
if (p.substr(0, 1) == '%' && p.substr(p.length - 1, 1) == '%')
p += scope.dirsep;
AppService.postJson('/filesystem/validate', {path: p}).then(function(data) {
defunctmap[compareablePath(data.config.data.path)] = false;
}, function(data) {
var p = data.config.data.path;
var ix = findInList(sourceNodeChildren, compareablePath(p));
if (ix != null && sourceNodeChildren[ix].id == p) {
sourceNodeChildren[ix].iconCls = 'x-tree-icon-broken';
setEntryType(sourceNodeChildren[ix]);
}
});
}
}
partialincludemap = buildidlookup(sourcemap);
updateIncludeFlags();
}
$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 = {};
id = compareablePath(id);
r[id] = true;
var map = buildidlookup(r);
var fit = null;
traversenodes(function(n) {
if (compareablePath(n.id) == id) {
fit = n;
return false;
}
if (!map[compareablePath(n.id)])
return true;
});
return fit;
}
$scope.toggleCheck = function(node) {
var c = compareablePath(node.id);
var c_is_dir = c.substr(c.length - 1, 1) == scope.dirsep;
if (node.include == null || node.include == ' ') {
if (c_is_dir) {
for(var i = scope.ngSources.length - 1; i >= 0; i--) {
var s = compareablePath(scope.ngSources[i]);
if (s == c)
return;
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);
}
}
}
scope.ngSources.push(node.id);
} else if (node.include == '+') {
if (sourcemap[c]) {
removePathFromArray(scope.ngSources, node.id);
for(var i = scope.ngFilters.length - 1; i >= 0; i--) {
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' ? scope.dirsep : '')).indexOf(c) == 0)
scope.ngFilters.splice(i, 1);
}
} else {
if (n[0] == '-path' && compareablePath(n[1]) == c)
scope.ngFilters.splice(i, 1);
}
}
}
} else {
removePathFromArray(scope.ngFilters, '+' + node.id);
updateFilterList();
if (shouldIncludeNode(node, false)) {
// No explicit include filter, add exclude filter to start of list
scope.ngFilters.unshift("-" + node.id);
}
}
} else if (node.include == '-') {
removePathFromArray(scope.ngFilters, '-' + node.id);
updateFilterList();
if (nodeExcludedByAttributes(node)
&& indexOfPathInArray(scope.ngSources, node.id) == -1) {
// Node is excluded by attributes, have to add as source to override
scope.ngSources.push(node.id);
} else if (!shouldIncludeNode(node, false)) {
// No explicit exclude filter, add include filter to start of list
scope.ngFilters.unshift('+' + node.id);
}
if (nodeExcludedBySize(node)) {
DialogService.dialog(gettextCatalog.getString('Cannot include "{{text}}"', node),
gettextCatalog.getString('The file size is {{size}}, larger than the maximum specified size. If the file size decreases, it will be included in future backups.', { size: AppUtils.formatSizeString(node.fileSize) }));
}
}
};
function shouldExpand(path, expandedPath) {
return expandedPath.indexOf(path) == 0 && path.length < expandedPath.length;
}
$scope.toggleExpanded = function(node) {
node.expanded = !node.expanded;
self = this;
if (node.root || node.leaf || node.iconCls == 'x-tree-icon-leaf' || node.iconCls == 'x-tree-icon-locked')
return;
if (!node.children && !node.loading) {
node.loading = true;
AppService.postJson('/filesystem?onlyfolders=false&showhidden=true', {path: node.id}).then(function(data) {
node.children = data.data;
node.loading = false;
if (node.children != null)
for (var i in node.children) {
var child = node.children[i];
displayMap[compareablePath(child.id)] = buildDisplayName(child, node);
if (self.expandedPath != null) {
var childPath = compareablePath(child.id);
if (shouldExpand(childPath, self.expandedPath)) {
self.toggleExpanded(child);
} else if (childPath == self.expandedPath) {
self.expandedPath = null;
self.scrollId = child.id;
}
}
}
updateIncludeFlags(node, node.include);
}, function() {
node.loading = false;
node.expanded = false;
AppUtils.connectionError.apply(AppUtils, arguments);
});
}
};
$scope.expandPath = function(path) {
cPath = compareablePath(path);
this.expandedPath = cPath;
traversenodes(function (n, p) {
if (n.root) {
return null;
}
var nodePath = compareablePath(n.id);
if (nodePath == cPath && !n.other) {
// Scroll to node
scope.scrollId = n.id;
// Cancel traverse
return false;
}
if (shouldExpand(nodePath, cPath)) {
if (!p.expanded) {
// Handle root nodes
scope.toggleExpanded(p);
}
if (!n.expanded) {
scope.toggleExpanded(n);
}
// Continue traverse
return null;
} else {
// Do not continue this subtree
return true;
}
}, this.treedata);
}
$scope.$watch('scrollId', function (scrollId, oldVal, scope) {
// Scroll to node
if (scrollId != null) {
scope.scrollId = null;
// Need to wait until all nodes are processed
$timeout(function () {
$anchorScroll('node-' + scrollId);
}, 100);
}
});
$scope.toggleSelected = function(node) {
if (scope.selectednode != null)
scope.selectednode.selected = false;
scope.selectednode = node;
scope.selectednode.selected = true;
};
$scope.doubleClick = function (node) {
if (sourceNodeChildren.indexOf(node) != -1) {
// Open folder in file picker
scope.expandPath(node.id);
}
};
scope.treedata.children = [];
// Load filter groups
AppUtils.loadFilterGroups();
AppService.postJson('/filesystem?onlyfolders=false&showhidden=true', {path: '/'}).then(function(data) {
var usernode = {
text: gettextCatalog.getString('User data'),
root: true,
iconCls: 'x-tree-icon-userdata',
expanded: true,
children: []
};
var systemnode = {
text: gettextCatalog.getString('Computer'),
root: true,
iconCls: 'x-tree-icon-computer',
children: []
};
var sourcenode = {
text: gettextCatalog.getString('Source data'),
root: true,
iconCls: 'x-tree-icon-others',
expanded: true,
children: [],
isSourcenode: true
};
sourceNodeChildren = sourcenode.children;
scope.treedata.children.push(usernode, systemnode, sourcenode);
for(var i = 0; i < data.data.length; i++) {
if (data.data[i].id.indexOf('%') === 0) {
const path = compareablePath(data.data[i].id)
if (path === compareablePath('%HYPERV%') || path === compareablePath('%MSSQL%')) {
scope.treedata.children.unshift(data.data[i]);
} else {
usernode.children.push(data.data[i]);
}
displayMap[path] = data.data[i].text;
setIconCls(data.data[i]);
}
else
systemnode.children.push(data.data[i]);
}
syncTreeWithLists();
}, AppUtils.connectionError);
}
}
});