This adds the common "Movies" and "Public" folders to the list of special folders for MacOS (and Linux, if applicable)
639 lines
23 KiB
JavaScript
639 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 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;
|
|
}
|
|
|
|
for(var i = 0; i < scope.ngSources.length; i++) {
|
|
var k = compareablePath(scope.ngSources[i]);
|
|
if (k.length == 0)
|
|
continue;
|
|
|
|
sourcemap[k] = true;
|
|
|
|
var txt = scope.ngSources[i];
|
|
if (k.indexOf('%') == 0) {
|
|
var nx = k.substr(1).indexOf('%') + 2;
|
|
if (nx > 1) {
|
|
var key = compareablePath(k.substr(0, nx));
|
|
txt = displayMap[compareablePath(k)] || ((displayMap[compareablePath(key)] || key) + txt.substr(nx));
|
|
}
|
|
}
|
|
|
|
var n = {
|
|
text: txt,
|
|
id: scope.ngSources[i],
|
|
include: '+',
|
|
other: true,
|
|
leaf: true
|
|
};
|
|
|
|
setIconCls(n);
|
|
|
|
sourceNodeChildren.push(n);
|
|
|
|
if (defunctmap[k] == 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[k] = true;
|
|
|
|
var p = scope.ngSources[i];
|
|
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'
|
|
|| 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;
|
|
|
|
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];
|
|
setEntryType(child);
|
|
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) {
|
|
usernode.children.push(data.data[i]);
|
|
var cp = compareablePath(data.data[i].id);
|
|
displayMap[cp] = data.data[i].text;
|
|
|
|
setIconCls(data.data[i]);
|
|
}
|
|
else
|
|
systemnode.children.push(data.data[i]);
|
|
}
|
|
|
|
syncTreeWithLists();
|
|
|
|
}, AppUtils.connectionError);
|
|
|
|
AppService.get('/hyperv', {path: '/'}).then(function(data) {
|
|
if (data.data != null && data.data.length > 0) {
|
|
var hypervnode = {
|
|
text: gettextCatalog.getString('Hyper-V Machines'),
|
|
id: "%HYPERV%",
|
|
children: []
|
|
};
|
|
setIconCls(hypervnode);
|
|
var cp = compareablePath(hypervnode.id);
|
|
displayMap[cp] = gettextCatalog.getString('All Hyper-V Machines');
|
|
|
|
// add HyperV at the beginning
|
|
if (scope.treedata.children.length < 1)
|
|
scope.treedata.children.push(hypervnode);
|
|
else
|
|
scope.treedata.children = [hypervnode].concat(scope.treedata.children);
|
|
|
|
for (var i = 0; i < data.data.length; i++) {
|
|
var node = {
|
|
leaf: true,
|
|
id: "%HYPERV%\\" + data.data[i].id,
|
|
text: data.data[i].text};
|
|
|
|
cp = compareablePath(node.id);
|
|
displayMap[cp] = gettextCatalog.getString('Hyper-V Machine:') + " " + node.text;
|
|
setIconCls(node);
|
|
hypervnode.children.push(node);
|
|
}
|
|
syncTreeWithLists();
|
|
}
|
|
}, AppUtils.connectionError);
|
|
|
|
AppService.get('/mssql', { path: '/' }).then(function (data) {
|
|
if (data.data != null && data.data.length > 0) {
|
|
var mssqlnode = {
|
|
text: gettextCatalog.getString('Microsoft SQL Databases'),
|
|
id: "%MSSQL%",
|
|
children: []
|
|
};
|
|
setIconCls(mssqlnode);
|
|
var cp = compareablePath(mssqlnode.id);
|
|
displayMap[cp] = gettextCatalog.getString('All Microsoft SQL Databases');
|
|
|
|
// add MS SQL DB at the beginning
|
|
if (scope.treedata.children.length < 1)
|
|
scope.treedata.children.push(mssqlnode);
|
|
else
|
|
scope.treedata.children = [mssqlnode].concat(scope.treedata.children);
|
|
|
|
for (var i = 0; i < data.data.length; i++) {
|
|
var node = {
|
|
leaf: true,
|
|
id: "%MSSQL%\\" + data.data[i].id,
|
|
text: data.data[i].text
|
|
};
|
|
|
|
cp = compareablePath(node.id);
|
|
displayMap[cp] = gettextCatalog.getString('Microsoft SQL Database:') + " " + node.text;
|
|
setIconCls(node);
|
|
mssqlnode.children.push(node);
|
|
}
|
|
syncTreeWithLists();
|
|
}
|
|
}, AppUtils.connectionError);
|
|
}
|
|
}
|
|
});
|