Files
cloudron-box/dashboard/public/js/restore.js
2024-10-04 17:43:45 +02:00

398 lines
18 KiB
JavaScript

'use strict';
/* global $, angular, SECRET_PLACEHOLDER, STORAGE_PROVIDERS, BACKUP_FORMATS, redirectIfNeeded */
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR, REGIONS_CONTABO, REGIONS_HETZNER */
// create main application module
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']);
app.controller('RestoreController', ['$scope', 'Client', function ($scope, Client) {
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.client = Client;
$scope.busy = false;
$scope.error = {};
$scope.message = ''; // progress
// variables here have to match the import config logic!
$scope.provider = '';
$scope.bucket = '';
$scope.prefix = '';
$scope.mountPoint = '';
$scope.accessKeyId = '';
$scope.secretAccessKey = '';
$scope.gcsKey = { keyFileName: '', content: '' };
$scope.region = '';
$scope.endpoint = '';
$scope.backupFolder = '';
$scope.remotePath = '';
$scope.instanceId = '';
$scope.acceptSelfSignedCerts = false;
$scope.format = 'tgz';
$scope.advancedVisible = false;
$scope.password = '';
$scope.encryptedFilenames = true;
$scope.encrypted = false; // only used if a backup config contains that flag
$scope.setupToken = '';
$scope.skipDnsSetup = false;
$scope.disk = null;
$scope.blockDevices = [];
$scope.mountOptions = {
host: '',
remoteDir: '',
username: '',
password: '',
diskPath: '',
user: '',
seal: true,
port: 22,
privateKey: ''
};
$scope.$watch('disk', function (newValue) {
if (!newValue) return;
$scope.mountOptions.diskPath = '/dev/disk/by-uuid/' + newValue.uuid;
});
$scope.ipv4Config = {
provider: 'generic',
ip: '',
ifname: ''
};
$scope.ipv6Config = {
provider: 'generic',
ip: '',
ifname: ''
};
$scope.ipProviders = [
{ name: 'Disabled', value: 'noop' },
{ name: 'Public IP', value: 'generic' },
{ name: 'Static IP Address', value: 'fixed' },
{ name: 'Network Interface', value: 'network-interface' }
];
$scope.s3Regions = REGIONS_S3;
$scope.wasabiRegions = REGIONS_WASABI;
$scope.doSpacesRegions = REGIONS_DIGITALOCEAN;
$scope.hetznerRegions = REGIONS_HETZNER;
$scope.exoscaleSosRegions = REGIONS_EXOSCALE;
$scope.scalewayRegions = REGIONS_SCALEWAY;
$scope.linodeRegions = REGIONS_LINODE;
$scope.ovhRegions = REGIONS_OVH;
$scope.ionosRegions = REGIONS_IONOS;
$scope.upcloudRegions = REGIONS_UPCLOUD;
$scope.vultrRegions = REGIONS_VULTR;
$scope.contaboRegions = REGIONS_CONTABO;
$scope.storageProviders = STORAGE_PROVIDERS;
$scope.formats = BACKUP_FORMATS;
$scope.s3like = function (provider) {
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos'
|| provider === 'digitalocean-spaces' || provider === 'wasabi' || provider === 'scaleway-objectstorage' || provider === 'hetzner-objectstorage'
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'backblaze-b2' || provider === 'cloudflare-r2'
|| provider === 'ionos-objectstorage' || provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2'
|| provider === 'contabo-objectstorage';
};
$scope.mountlike = function (provider) {
return provider === 'disk' || provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs';
};
$scope.restore = function () {
$scope.error = {};
$scope.busy = true;
var backupConfig = {
provider: $scope.provider,
format: $scope.format,
};
if ($scope.password) {
backupConfig.password = $scope.password;
backupConfig.encryptedFilenames = $scope.encryptedFilenames;
}
// only set provider specific fields, this will clear them in the db
if ($scope.s3like(backupConfig.provider)) {
backupConfig.bucket = $scope.bucket;
backupConfig.prefix = $scope.prefix;
backupConfig.accessKeyId = $scope.accessKeyId;
backupConfig.secretAccessKey = $scope.secretAccessKey;
if ($scope.endpoint) backupConfig.endpoint = $scope.endpoint;
if (backupConfig.provider === 's3') {
if ($scope.region) backupConfig.region = $scope.region;
delete backupConfig.endpoint;
} else if (backupConfig.provider === 'minio' || backupConfig.provider === 's3-v4-compat') {
backupConfig.region = backupConfig.region || 'us-east-1';
backupConfig.acceptSelfSignedCerts = $scope.acceptSelfSignedCerts;
backupConfig.s3ForcePathStyle = true; // might want to expose this in the UI
} else if (backupConfig.provider === 'exoscale-sos') {
backupConfig.region = 'us-east-1';
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'wasabi') {
backupConfig.region = $scope.wasabiRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'scaleway-objectstorage') {
backupConfig.region = $scope.scalewayRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'linode-objectstorage') {
backupConfig.region = $scope.linodeRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'ovh-objectstorage') {
backupConfig.region = $scope.ovhRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'ionos-objectstorage') {
backupConfig.region = $scope.ionosRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'vultr-objectstorage') {
backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'contabo-objectstorage') {
backupConfig.region = $scope.contaboRegions.find(function (x) { return x.value === $scope.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
backupConfig.s3ForcePathStyle = true; // https://docs.contabo.com/docs/products/Object-Storage/technical-description (no virtual buckets)
} else if (backupConfig.provider === 'upcloud-objectstorage') {
var m = /^.*\.(.*)\.upcloudobjects.com$/.exec(backupConfig.endpoint);
backupConfig.region = m ? m[1] : 'us-east-1'; // let it fail in validation phase if m is not valid
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'digitalocean-spaces') {
backupConfig.region = 'us-east-1';
} else if (backupConfig.provider === 'hetzner-objectstorage') {
backupConfig.region = 'us-east-1';
backupConfig.signatureVersion = 'v4';
}
} else if (backupConfig.provider === 'gcs') {
backupConfig.bucket = $scope.bucket;
backupConfig.prefix = $scope.prefix;
try {
var serviceAccountKey = JSON.parse($scope.gcsKey.content);
backupConfig.projectId = serviceAccountKey.project_id;
backupConfig.credentials = {
client_email: serviceAccountKey.client_email,
private_key: serviceAccountKey.private_key
};
if (!backupConfig.projectId || !backupConfig.credentials || !backupConfig.credentials.client_email || !backupConfig.credentials.private_key) {
throw 'fields_missing';
}
} catch (e) {
$scope.error.generic = 'Cannot parse Google Service Account Key: ' + e.message;
$scope.error.gcsKeyInput = true;
$scope.busy = false;
return;
}
} else if ($scope.mountlike(backupConfig.provider)) {
backupConfig.prefix = $scope.prefix;
backupConfig.mountOptions = {};
if (backupConfig.provider === 'cifs' || backupConfig.provider === 'sshfs' || backupConfig.provider === 'nfs') {
backupConfig.mountOptions.host = $scope.mountOptions.host;
backupConfig.mountOptions.remoteDir = $scope.mountOptions.remoteDir;
if (backupConfig.provider === 'cifs') {
backupConfig.mountOptions.username = $scope.mountOptions.username;
backupConfig.mountOptions.password = $scope.mountOptions.password;
backupConfig.mountOptions.seal = $scope.mountOptions.seal;
} else if (backupConfig.provider === 'sshfs') {
backupConfig.mountOptions.user = $scope.mountOptions.user;
backupConfig.mountOptions.port = $scope.mountOptions.port;
backupConfig.mountOptions.privateKey = $scope.mountOptions.privateKey;
}
} else if (backupConfig.provider === 'disk' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs') {
backupConfig.mountOptions.diskPath = $scope.mountOptions.diskPath;
} else if (backupConfig.provider === 'mountpoint') {
backupConfig.mountPoint = $scope.mountPoint;
}
} else if (backupConfig.provider === 'filesystem') {
backupConfig.backupFolder = $scope.backupFolder;
}
if ($scope.remotePath.indexOf('/') === -1) {
$scope.error.generic = 'Backup id must include the directory path';
$scope.error.remotePath = true;
$scope.busy = false;
return;
}
if ($scope.remotePath.indexOf('box') === -1) {
$scope.error.generic = 'Backup id must contain "box"';
$scope.error.remotePath = true;
$scope.busy = false;
return;
}
var version = $scope.remotePath.match(/_v(\d+.\d+.\d+)/);
if (!version) {
$scope.error.generic = 'Backup id is missing version information';
$scope.error.remotePath = true;
$scope.busy = false;
return;
}
var data = {
backupConfig: backupConfig,
remotePath: $scope.remotePath.replace(/\.tar\.gz(\.enc)?$/, ''),
version: version ? version[1] : '',
ipv4Config: $scope.ipv4Config,
ipv6Config: $scope.ipv6Config,
skipDnsSetup: $scope.skipDnsSetup,
setupToken: $scope.setupToken
};
Client.restore(data, function (error) {
$scope.busy = false;
if (error) {
if (error.statusCode === 424) {
$scope.error.generic = error.message;
if (error.message.indexOf('AWS Access Key Id') !== -1) {
$scope.error.accessKeyId = true;
$scope.accessKeyId = '';
$scope.configureBackupForm.accessKeyId.$setPristine();
$('#inputConfigureBackupAccessKeyId').focus();
} else if (error.message.indexOf('not match the signature') !== -1 ) {
$scope.error.secretAccessKey = true;
$scope.secretAccessKey = '';
$scope.configureBackupForm.secretAccessKey.$setPristine();
$('#inputConfigureBackupSecretAccessKey').focus();
} else if (error.message.toLowerCase() === 'access denied') {
$scope.error.bucket = true;
$scope.bucket = '';
$scope.configureBackupForm.bucket.$setPristine();
$('#inputConfigureBackupBucket').focus();
} else if (error.message.indexOf('ECONNREFUSED') !== -1) {
$scope.error.generic = 'Unknown region';
$scope.error.region = true;
$scope.configureBackupForm.region.$setPristine();
$('#inputConfigureBackupDORegion').focus();
} else if (error.message.toLowerCase() === 'wrong region') {
$scope.error.generic = 'Wrong S3 Region';
$scope.error.region = true;
$scope.configureBackupForm.region.$setPristine();
$('#inputConfigureBackupS3Region').focus();
} else {
$('#inputConfigureBackupBucket').focus();
}
} else {
$scope.error.generic = error.message;
}
return;
}
waitForRestore();
});
};
function waitForRestore() {
$scope.busy = true;
Client.getProvisionStatus(function (error, status) {
if (!error && !status.restore.active) { // restore finished
if (status.restore.errorMessage) {
$scope.busy = false;
$scope.error.generic = status.restore.errorMessage;
} else { // restore worked, redirect to admin page
window.location.href = 'https://' + status.adminFqdn + '/';
}
return;
}
if (!error) $scope.message = status.restore.message;
setTimeout(waitForRestore, 5000);
});
}
function readFileLocally(obj, file, fileName) {
return function (event) {
$scope.$apply(function () {
obj[file] = null;
obj[fileName] = event.target.files[0].name;
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read local file');
obj[file] = result.target.result;
};
reader.readAsText(event.target.files[0]);
});
};
}
document.getElementById('gcsKeyFileInput').onchange = readFileLocally($scope.gcsKey, 'content', 'keyFileName');
document.getElementById('backupConfigFileInput').onchange = function (event) {
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read backup config');
var backupConfig;
try {
backupConfig = JSON.parse(result.target.result);
} catch (e) {
console.error('Unable to parse backup config');
return;
}
$scope.$apply(function () {
// we assume property names match here, this does not yet work for gcs keys
Object.keys(backupConfig).forEach(function (k) {
if (k in $scope) $scope[k] = backupConfig[k];
});
// this allows the config to potentially have a raw password (though our UI sets it to placeholder)
if ($scope.mountOptions.password === SECRET_PLACEHOLDER) $scope.mountOptions.password = '';
});
};
reader.readAsText(event.target.files[0]);
};
function init() {
Client.getProvisionStatus(function (error, status) {
if (error) return Client.initError(error, init);
if (redirectIfNeeded(status, 'restore')) return; // redirected to some other view...
if (status.restore.active) return waitForRestore();
if (status.restore.errorMessage) $scope.error.generic = status.restore.errorMessage; // any previous restore error
Client.getProvisionBlockDevices(function (error, result) {
if (error) {
console.error('Failed to list blockdevices:', error);
} else {
// only offer non /, /boot or /home disks
result = result.filter(function (d) { return d.mountpoint !== '/' && d.mountpoint !== '/home' && d.mountpoint !== '/boot'; });
// only offer xfs and ext4 disks
result = result.filter(function (d) { return d.type === 'xfs' || d.type === 'ext4'; });
// amend label for UI
result.forEach(function (d) { d.label = d.path; });
}
$scope.blockDevices = result;
$scope.instanceId = search.instanceId;
$scope.setupToken = search.setupToken;
Client.detectIp(function (error, ip) { // this is never supposed to error
if (!error) $scope.ipv4Config.provider = ip.ipv4 ? 'generic' : 'noop';
if (!error) $scope.ipv6Config.provider = ip.ipv6 ? 'generic' : 'noop';
$scope.initialized = true;
});
});
});
}
init();
}]);