duplicati/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreController.js
2024-06-24 16:00:05 +02:00

475 lines
19 KiB
JavaScript

backupApp.controller('RestoreController', function ($rootScope, $scope, $routeParams, $location, AppService, AppUtils, SystemInfo, ServerStatus, DialogService, BackupList, gettextCatalog) {
$scope.SystemInfo = SystemInfo.watch($scope);
$scope.AppUtils = AppUtils;
$scope.restore_step = 0;
$scope.connecting = false;
$scope.HideFolderBrowser = true;
$scope.RestoreLocation = 'direct';
$scope.RestoreMode = 'overwrite';
$scope.passphrase = "";
var filesetsBuilt = {};
var filesetsRepaired = {};
var filesetStamps = {};
var inProgress = {};
var dirsep = $scope.SystemInfo.DirectorySeparator || '/';
$scope.filesetStamps = filesetStamps;
$scope.treedata = {};
$scope.Selected = [];
function createGroupLabel(dt) {
var dateStamp = function(a) { return a.getFullYear() * 10000 + a.getMonth() * 100 + a.getDate(); }
var now = new Date();
var today = dateStamp(now);
var yesterday = dateStamp(new Date(new Date().setDate(now.getDate() - 1)));
var week = dateStamp(new Date(new Date().setDate(now.getDate() - 7)));
var thismonth = dateStamp(new Date(new Date().setMonth(now.getMonth() - 1)));
var lastmonth = dateStamp(new Date(new Date().setMonth(now.getMonth() - 2)));
var dateBuckets = [
{text: gettextCatalog.getString('Today'), stamp: today},
{text: gettextCatalog.getString('Yesterday'), stamp: yesterday},
{text: gettextCatalog.getString('This week'), stamp: week},
{text: gettextCatalog.getString('This month'), stamp: thismonth},
{text: gettextCatalog.getString('Last month'), stamp: lastmonth}
];
var stamp = dateStamp(dt);
for(var t in dateBuckets)
if (stamp >= dateBuckets[t].stamp)
return dateBuckets[t].text;
return dt.getFullYear() + '';
};
$scope.parseBackupTimesData = function() {
for(var n in filesetStamps)
delete filesetStamps[n];
for(var n in $scope.Filesets) {
var item = $scope.Filesets[n];
item.DisplayLabel = item.Version + ': ' + AppUtils.toDisplayDateAndTime(AppUtils.parseDate(item.Time));
if (item.IsFullBackup === 0) {
item.DisplayLabel = item.DisplayLabel + ' (partial)';
}
item.GroupLabel = n == 0 ? gettextCatalog.getString('Latest') : createGroupLabel(AppUtils.parseDate(item.Time));
filesetStamps[item.Version + ''] = item.Time;
}
$scope.RestoreVersion = 0;
};
$scope.fetchBackupTimes = function() {
$scope.connecting = true;
$scope.ConnectionProgress = gettextCatalog.getString('Getting file versions …');
var qp = '';
if ($scope.IsBackupTemporary)
qp += '?from-remote-only=true';
AppService.get('/backup/' + $scope.BackupID + '/filesets' + qp).then(
function(resp) {
$scope.connecting = false;
$scope.ConnectionProgress = '';
$scope.Filesets = resp.data;
$scope.parseBackupTimesData();
$scope.fetchPathInformation();
},
function(resp) {
var message = AppService.responseErrorMessage(resp);
$scope.connecting = false;
$scope.ConnectionProgress = '';
DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Failed to connect: {{message}}', { message: message }));
}
);
};
$scope.fetchPathInformation = function() {
var version = $scope.RestoreVersion + '';
if ($scope.connecting)
return;
if (inProgress[version] || $scope.restore_step != 0)
return;
if (!$scope.IsBackupTemporary && $scope.temporaryDB == null) {
// TODO: Register a temporary db here
}
function handleError(resp) {
delete inProgress[version];
$scope.connecting = false;
$scope.ConnectionProgress = '';
var message = AppService.responseErrorMessage(resp);
DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Failed to fetch path information: {{message}}', { message: message }));
};
if (filesetsBuilt[version] == null) {
if ($scope.IsBackupTemporary && filesetsRepaired[version] == null) {
$scope.connecting = true;
$scope.ConnectionProgress = gettextCatalog.getString('Fetching path information …');
inProgress[version] = true;
AppService.postJson('/backup/' + $scope.BackupID + '/repairupdate', { 'only_paths': true, 'time': filesetStamps[version + '']}).then(
function(resp) {
var taskid = resp.data.ID;
inProgress[version] = taskid;
$scope.taskid = taskid;
ServerStatus.callWhenTaskCompletes(taskid, function() {
AppService.get('/task/' + taskid).then(function(resp) {
delete inProgress[version];
$scope.connecting = false;
$scope.ConnectionProgress = '';
if (resp.data.Status == 'Completed')
{
filesetsRepaired[version] = true;
$scope.fetchPathInformation();
}
else
{
DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Failed to fetch path information: {{message}}', { message: resp.data.ErrorMessage }));
}
}, handleError);
});
}, handleError);
} else {
var stamp = filesetStamps[version];
// In case the times are not loaded yet
if (stamp == null)
return;
$scope.connecting = true;
$scope.ConnectionProgress = gettextCatalog.getString('Fetching path information …');
inProgress[version] = true;
AppService.get('/backup/' + $scope.BackupID + '/files?prefix-only=true&folder-contents=false&time=' + encodeURIComponent(stamp)).then(
function(resp) {
delete inProgress[version];
$scope.connecting = false;
$scope.ConnectionProgress = '';
filesetsBuilt[version] = resp.data.Files;
$scope.Paths = filesetsBuilt[version];
dirsep = resp.data.Files[0].Path[0] == '/' ? '/' : '\\';
}, handleError);
}
} else {
$scope.Paths = filesetsBuilt[version];
}
};
$scope.$watch('RestoreVersion', function() { $scope.fetchPathInformation(); });
$scope.$watch('RestorePath', function() {
if (($scope.RestorePath || '').trim().length == 0)
$scope.RestoreLocation = 'direct';
else
$scope.RestoreLocation = 'custom';
});
$scope.onClickNext = function() {
var results = $scope.Selected;
if (results.length == 0) {
DialogService.dialog(gettextCatalog.getString('No items selected'), gettextCatalog.getString('No items to restore, please select one or more items'));
} else {
$scope.restore_step = 1;
}
};
$scope.onClickBack = function() {
$location.path('/restoredirect')
};
$scope.clearSearch = function() {
$scope.InSearchMode = false;
$scope.fetchPathInformation();
};
$scope.doSearch = function() {
if ($scope.Searching || $scope.restore_step != 0)
return;
if (($scope.SearchFilter || '').trim().length == 0) {
$scope.clearSearch();
return;
}
$scope.Searching = true;
var version = $scope.RestoreVersion + '';
var stamp = filesetStamps[version];
AppService.get('/backup/' + $scope.BackupID + '/files?prefix-only=false&time=' + encodeURIComponent(stamp) + '&filter=*' + encodeURIComponent($scope.SearchFilter) + '*').then(
function(resp) {
$scope.Searching = false;
var searchNodes = [];
function compareablePath(path) {
return $scope.SystemInfo.CaseSensitiveFilesystem ? path : path.toLowerCase();
};
for(var i in filesetsBuilt[version])
searchNodes[i] = { Path: filesetsBuilt[version][i].Path };
var files = resp.data.Files;
for(var i in files) {
var p = files[i].Path;
var cp = compareablePath(p);
var isdir = p[p.length - 1] == dirsep;
for(var j in searchNodes) {
var sn = searchNodes[j];
if (cp.indexOf(compareablePath(sn.Path)) == 0) {
var curpath = sn.Path;
var parts = p.substr(sn.Path.length).split(dirsep);
// Remove empty part if path had dirsep at end
if (parts[parts.length - 1].length == 0) {
parts.pop();
}
var col = sn;
for(var k in parts) {
var found = false;
curpath += parts[k];
if (isdir || k != parts.length - 1)
curpath += dirsep;
if (!col.Children)
col.Children = [];
for(var m in col.Children) {
if (compareablePath(col.Children[m].Path) == compareablePath(curpath)) {
found = true;
col = col.Children[m];
break;
}
}
if (!found) {
var n = { Path: curpath, expanded: true };
if (!isdir && k == parts.length - 1) {
n.iconCls = 'x-tree-icon-leaf';
n.leaf = true;
}
col.Children.push(n);
col = n;
}
}
break;
}
}
}
$scope.Paths = searchNodes;
$scope.InSearchMode = true;
},
function(resp) {
$scope.Searching = false;
var message = AppService.responseErrorMessage(resp);
$scope.connecting = false;
$scope.ConnectionProgress = '';
DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Failed to connect: {{message}}', { message: message }));
}
);
};
$scope.onStartRestore = function() {
if ($scope.RestoreLocation == 'custom' && ($scope.RestorePath || '').trim().length == 0)
{
DialogService.alert(gettextCatalog.getString('You have chosen to restore to a new location, but not entered one'));
return;
}
if ($scope.RestoreLocation != 'custom' && dirsep != $scope.SystemInfo.DirectorySeparator)
{
DialogService.confirm(gettextCatalog.getString('This backup was created on another operating system. Restoring files without specifying a destination folder can cause files to be restored in unexpected places. Are you sure you want to continue without choosing a destination folder?'), function(ix) {
if (ix == 1)
$scope.onStartRestoreProcess();
});
}
else
{
$scope.onStartRestoreProcess();
}
}
$scope.onStartRestoreProcess = function() {
var version = $scope.RestoreVersion + '';
var stamp = filesetStamps[version];
$scope.restore_step = 2;
function handleError(resp) {
var message = AppService.responseErrorMessage(resp);
$scope.restore_step = 1;
$scope.connecting = false;
$scope.ConnectionProgress = '';
DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Failed to connect: {{message}}', { message: message }));
};
var p = {
'time': stamp,
'restore_path': $scope.RestoreLocation == 'custom' ? $scope.RestorePath : null,
'overwrite': $scope.RestoreMode == 'overwrite',
'permissions': $scope.RestorePermissions == null ? false : $scope.RestorePermissions,
'passphrase' : $scope.passphrase
};
var paths = [];
for(var n in $scope.Selected) {
var item = $scope.Selected[n];
if (item.substr(item.length - 1) == dirsep) {
// To support the possibility of encountering paths
// with literal wildcard characters, but also being
// able to add the globbing "*" suffix, use a regular
// expression filter
// Escape regular expression metacharacters
var itemRegex = item.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Add "globbing" suffix
paths.push('[' + itemRegex + '.*]');
} else {
// To support the possibility of encountering paths
// with literal wildcard characters, create a literal
// filter
paths.push('@' + item);
}
}
p.paths = paths;
if ($scope.IsBackupTemporary) {
$scope.connecting = true;
$scope.ConnectionProgress = gettextCatalog.getString('Creating temporary backup …');
AppService.post('/backup/' + $scope.BackupID + '/copytotemp').then(function(resp) {
var backupid = resp.data.ID;
$scope.ConnectionProgress = gettextCatalog.getString('Building partial temporary database …');
AppService.post('/backup/' + backupid + '/repair', p).then(function(resp) {
var taskid = $scope.taskid = resp.data.ID;
ServerStatus.callWhenTaskCompletes(taskid, function() {
AppService.get('/task/' + taskid).then(function(resp) {
$scope.ConnectionProgress = gettextCatalog.getString('Starting the restore process …');
if (resp.data.Status == 'Completed')
{
AppService.post('/backup/' + backupid + '/restore', p).then(function(resp) {
$scope.ConnectionProgress = gettextCatalog.getString('Restoring files …');
var t2 = $scope.taskid = resp.data.ID;
ServerStatus.callWhenTaskCompletes(t2, function() { $scope.onRestoreComplete(t2); });
}, handleError);
}
else
{
DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Failed to build temporary database: {{message}}', { message: resp.data.ErrorMessage }));
$scope.connecting = false;
$scope.ConnectionProgress = '';
$scope.restore_step = 1;
}
}, handleError);
});
}, handleError);
}, handleError);
} else {
$scope.connecting = true;
$scope.ConnectionProgress = gettextCatalog.getString('Starting the restore process …');
AppService.post('/backup/' + $scope.BackupID + '/restore', p).then(function(resp) {
$scope.ConnectionProgress = gettextCatalog.getString('Restoring files …');
var t2 = $scope.taskid = resp.data.ID;
ServerStatus.callWhenTaskCompletes(t2, function() { $scope.onRestoreComplete(t2); });
}, handleError);
}
};
$scope.onRestoreComplete = function(taskid) {
AppService.get('/task/' + taskid).then(function(resp) {
$scope.connecting = false;
$scope.ConnectionProgress = '';
if (resp.data.Status == 'Completed')
{
$scope.restore_step = 3;
}
else
{
DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Failed to restore files: {{message}}', { message: resp.data.ErrorMessage }));
}
}, function(resp) {
var message = AppService.responseErrorMessage(resp);
$scope.restore_step = 1;
$scope.connecting = false;
$scope.ConnectionProgress = '';
DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Failed to connect: {{message}}', { message: message }));
});
};
$scope.onClickComplete = function () {
$location.path('/');
};
$scope.trySetStep = function(pg) {
if ($scope.restore_step < 2)
$scope.restore_step = pg;
};
$scope.showInputPassphrase = function () {
if (!this.Backup) {
return false;
}
if (!('IsUnencryptedOrPassphraseStored' in this.Backup)) {
return false;
}
return !this.Backup['IsUnencryptedOrPassphraseStored'];
}
$scope.BackupID = $routeParams.backupid;
$scope.IsBackupTemporary = parseInt($scope.BackupID) != $scope.BackupID;
if (!$scope.IsBackupTemporary) {
$scope.$on('backuplistchanged', function() { $scope.Backup = BackupList.lookup[$scope.BackupID]; });
$scope.Backup = BackupList.lookup[$scope.BackupID];
}
// We pass in the filelist through a global variable
// ... bit ugly, but we do not want to do two remote queries,
// ... nor do we want to pass the information through the url
if ($scope.IsBackupTemporary && $rootScope.filesets && $rootScope.filesets[$scope.BackupID]) {
$scope.Filesets = $rootScope.filesets[$scope.BackupID];
$scope.parseBackupTimesData();
$scope.fetchPathInformation();
} else {
$scope.fetchBackupTimes();
}
});