'use strict'; /* global angular */ /* global tld */ /* global $ */ // create main application module var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']); app.filter('zoneName', function () { return function (domain) { return tld.getDomain(domain); }; }); 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.backupId = ''; $scope.instanceId = ''; $scope.acceptSelfSignedCerts = false; $scope.format = 'tgz'; $scope.advancedVisible = false; $scope.password = ''; $scope.encrypted = false; // only used if a backup config contains that flag $scope.setupToken = ''; $scope.skipDnsSetup = false; $scope.mountOptions = { host: '', remoteDir: '', username: '', password: '', diskPath: '', user: '', port: 22, privateKey: '' }; $scope.sysinfo = { provider: 'generic', ip: '', ifname: '' }; $scope.sysinfoProvider = [ { name: 'Public IP', value: 'generic' }, { name: 'Static IP Address', value: 'fixed' }, { name: 'Network Interface', value: 'network-interface' } ]; $scope.prettySysinfoProviderName = function (provider) { switch (provider) { case 'generic': return 'Public IP'; case 'fixed': return 'Static IP Address'; case 'network-interface': return 'Network Interface'; default: return 'Unknown'; } }; // List is from http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region $scope.s3Regions = [ { name: 'Asia Pacific (Mumbai)', value: 'ap-south-1' }, { name: 'Asia Pacific (Osaka-Local)', value: 'ap-northeast-3' }, { name: 'Asia Pacific (Seoul)', value: 'ap-northeast-2' }, { name: 'Asia Pacific (Singapore)', value: 'ap-southeast-1' }, { name: 'Asia Pacific (Sydney)', value: 'ap-southeast-2' }, { name: 'Asia Pacific (Tokyo)', value: 'ap-northeast-1' }, { name: 'Canada (Central)', value: 'ca-central-1' }, { name: 'China (Beijing)', value: 'cn-north-1' }, { name: 'China (Ningxia)', value: 'cn-northwest-1' }, { name: 'EU (Frankfurt)', value: 'eu-central-1' }, { name: 'EU (Ireland)', value: 'eu-west-1' }, { name: 'EU (London)', value: 'eu-west-2' }, { name: 'EU (Paris)', value: 'eu-west-3' }, { name: 'EU (Stockholm)', value: 'eu-north-1' }, { name: 'South America (São Paulo)', value: 'sa-east-1' }, { name: 'US East (N. Virginia)', value: 'us-east-1' }, { name: 'US East (Ohio)', value: 'us-east-2' }, { name: 'US West (N. California)', value: 'us-west-1' }, { name: 'US West (Oregon)', value: 'us-west-2' }, ]; $scope.doSpacesRegions = [ { name: 'AMS3', value: 'https://ams3.digitaloceanspaces.com' }, { name: 'FRA1', value: 'https://fra1.digitaloceanspaces.com' }, { name: 'NYC3', value: 'https://nyc3.digitaloceanspaces.com' }, { name: 'SFO2', value: 'https://sfo2.digitaloceanspaces.com' }, { name: 'SFO3', value: 'https://sfo3.digitaloceanspaces.com' }, { name: 'SGP1', value: 'https://sgp1.digitaloceanspaces.com' } ]; $scope.exoscaleSosRegions = [ { name: 'AT-VIE-1', value: 'https://sos-at-vie-1.exo.io' }, { name: 'CH-DK-2', value: 'https://sos-ch-dk-2.exo.io' }, { name: 'CH-GVA-2', value: 'https://sos-ch-gva-2.exo.io' }, { name: 'DE-FRA-1', value: 'https://sos-de-fra-1.exo.io' }, ]; // https://www.scaleway.com/docs/object-storage-feature/ $scope.scalewayRegions = [ { name: 'FR-PAR', value: 'https://s3.fr-par.scw.cloud', region: 'fr-par' }, // default { name: 'NL-AMS', value: 'https://s3.nl-ams.scw.cloud', region: 'nl-ams' } ]; $scope.linodeRegions = [ { name: 'Newark', value: 'us-east-1.linodeobjects.com', region: 'us-east-1' }, // default { name: 'Frankfurt', value: 'eu-central-1.linodeobjects.com', region: 'us-east-1' }, { name: 'Singapore', value: 'ap-south-1.linodeobjects.com', region: 'us-east-1' }, ]; // note: ovh also has a storage endpoint but that only supports path style access $scope.ovhRegions = [ { name: 'Beauharnois (BHS)', value: 'https://s3.bhs.cloud.ovh.net', region: 'bhs' }, // default { name: 'Frankfurt (DE)', value: 'https://s3.de.cloud.ovh.net', region: 'de' }, { name: 'Gravelines (GRA)', value: 'https://s3.gra.cloud.ovh.net', region: 'gra' }, { name: 'Strasbourg (SBG)', value: 'https://s3.sbg.cloud.ovh.net', region: 'sbg' }, { name: 'London (UK)', value: 'https://s3.uk.cloud.ovh.net', region: 'uk' }, { name: 'Sydney (SYD)', value: 'https://s3.syd.cloud.ovh.net', region: 'syd' }, { name: 'Warsaw (WAW)', value: 'https://s3.waw.cloud.ovh.net', region: 'waw' }, ]; // https://devops.ionos.com/api/s3/ $scope.ionosRegions = [ { name: 'DE', value: 'https://s3-de-central.profitbricks.com', region: 's3-de-central' }, // default ]; $scope.wasabiRegions = [ { name: 'EU Central 1', value: 'https://s3.eu-central-1.wasabisys.com' }, { name: 'US East 1', value: 'https://s3.us-east-1.wasabisys.com' }, { name: 'US East 2', value: 'https://s3.us-east-2.wasabisys.com ' }, { name: 'US West 1', value: 'https://s3.us-west-1.wasabisys.com' } ]; $scope.storageProvider = [ { name: 'Amazon S3', value: 's3' }, { name: 'Backblaze B2 (S3 API)', value: 'backblaze-b2' }, { name: 'CIFS Mount', value: 'cifs' }, { name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' }, { name: 'Exoscale SOS', value: 'exoscale-sos' }, { name: 'Filesystem', value: 'filesystem' }, { name: 'Filesystem (Mountpoint)', value: 'mountpoint' }, // legacy { name: 'Google Cloud Storage', value: 'gcs' }, { name: 'IONOS (Profitbricks)', value: 'ionos-objectstorage' }, { name: 'Linode Object Storage', value: 'linode-objectstorage' }, { name: 'Minio', value: 'minio' }, { name: 'NFS Mount', value: 'nfs' }, { name: 'OVH Object Storage', value: 'ovh-objectstorage' }, { name: 'S3 API Compatible (v4)', value: 's3-v4-compat' }, { name: 'Scaleway Object Storage', value: 'scaleway-objectstorage' }, { name: 'SSHFS Mount', value: 'sshfs' }, { name: 'Wasabi', value: 'wasabi' } ]; $scope.formats = [ { name: 'Tarball (zipped)', value: 'tgz' }, { name: 'rsync', value: 'rsync' } ]; $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 === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'backblaze-b2' || provider === 'ionos-objectstorage'; }; $scope.mountlike = function (provider) { return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4'; }; $scope.restore = function () { $scope.error = {}; $scope.busy = true; var backupConfig = { provider: $scope.provider, format: $scope.format, setupToken: $scope.setupToken }; if ($scope.password) backupConfig.password = $scope.password; // 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 === 'digitalocean-spaces') { backupConfig.region = 'us-east-1'; } } 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.mountPoint = $scope.mountPoint; 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; } 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 === 'ext4') { backupConfig.mountOptions.diskPath = $scope.diskPath; } } else if (backupConfig.provider === 'filesystem' || backupConfig.provider === 'mountpoint') { backupConfig.backupFolder = $scope.configureBackup.backupFolder; } if ($scope.backupId.indexOf('/') === -1) { $scope.error.generic = 'Backup id must include the directory path'; $scope.error.backupId = true; $scope.busy = false; return; } if ($scope.backupId.indexOf('box') === -1) { $scope.error.generic = 'Backup id must contain "box"'; $scope.error.backupId = true; $scope.busy = false; return; } var version = $scope.backupId.match(/_v(\d+.\d+.\d+)/); if (!version) { $scope.error.generic = 'Backup id is missing version information'; $scope.error.backupId = true; $scope.busy = false; return; } var sysinfoConfig = { provider: $scope.sysinfo.provider }; if ($scope.sysinfo.provider === 'fixed') { sysinfoConfig.ip = $scope.sysinfo.ip; } else if ($scope.sysinfo.provider === 'network-interface') { sysinfoConfig.ifname = $scope.sysinfo.ifname; } Client.restore(backupConfig, $scope.backupId.replace(/\.tar\.gz(\.enc)?$/, ''), version ? version[1] : '', sysinfoConfig, $scope.skipDnsSetup, 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.getStatus(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 = '/'; } 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]; }); }); }; reader.readAsText(event.target.files[0]); }; function init() { Client.getStatus(function (error, status) { if (error) return Client.initError(error, init); if (status.restore.active) return waitForRestore(); if (status.restore.errorMessage) $scope.error.generic = status.restore.errorMessage; if (status.activated) { window.location.href = '/'; return; } $scope.status = status; $scope.instanceId = search.instanceId; $scope.setupToken = search.setupToken; $scope.initialized = true; }); } init(); }]);