diff --git a/src/js/client.js b/src/js/client.js index d46ebc28f..8b22ef8f9 100644 --- a/src/js/client.js +++ b/src/js/client.js @@ -891,6 +891,21 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout }); }; + Client.prototype.importBackup = function (appId, backupId, backupFormat, backupConfig, callback) { + var data = { + backupId: backupId, + backupFormat: backupFormat, + backupConfig: backupConfig, + }; + + post('/api/v1/apps/' + appId + '/import', data, null, function (error, data, status) { + if (error) return callback(error); + if (status !== 202) return callback(new ClientError(status)); + + callback(null); + }); + }; + Client.prototype.getNotifications = function (acknowledged, page, perPage, callback) { var config = { params: { diff --git a/src/views/app.html b/src/views/app.html index aae6215b1..ac7aceb2a 100644 --- a/src/views/app.html +++ b/src/views/app.html @@ -137,6 +137,124 @@ + + +
+
+
+ +

Use this to migrate an app from another Cloudron. The other app must have the same package version and access + control stratey as this one. +

+ +
+
+
diff --git a/src/views/app.js b/src/views/app.js index e2be95452..67d40ca14 100644 --- a/src/views/app.js +++ b/src/views/app.js @@ -11,6 +11,72 @@ angular.module('Application').controller('AppController', ['$scope', '$location', '$timeout', '$interval', '$route', '$routeParams', 'Client', function ($scope, $location, $timeout, $interval, $route, $routeParams, Client) { Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); }); + // 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: '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.wasabiRegions = [ + { name: 'EU Central 1', value: 'https://s3.eu-central-1.wasabisys.com' }, + { name: 'US East 1', value: 'https://s3.wasabisys.com' }, + { name: 'US West 1', value: 'https://s3.us-west-1.wasabisys.com' } + ]; + + $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: '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.storageProvider = [ + { name: 'Amazon S3', value: 's3' }, + { name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' }, + { name: 'Exoscale SOS', value: 'exoscale-sos' }, + { name: 'Filesystem', value: 'filesystem' }, + { name: 'Google Cloud Storage', value: 'gcs' }, + { name: 'Minio', value: 'minio' }, + { name: 'Scaleway Object Storage', value: 'scaleway-objectstorage' }, + // { name: 'No-op (Only for testing)', value: 'noop' }, + { name: 'S3 API Compatible (v4)', value: 's3-v4-compat' }, + { name: 'Wasabi', value: 'wasabi' } + ]; + + $scope.formats = [ + { name: 'Tarball (zipped)', value: 'tgz' }, + { name: 'rsync', value: 'rsync' } + ]; + // Avoid full reload on path change // https://stackoverflow.com/a/22614334 // reloadOnUrl: false in $routeProvider did not work! @@ -651,6 +717,117 @@ angular.module('Application').controller('AppController', ['$scope', '$location' } }; + $scope.s3like = function (provider) { + return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' + || provider === 'exoscale-sos' || provider === 'digitalocean-spaces' + || provider === 'scaleway-objectstorage' || provider === 'wasabi'; + }; + + $scope.importBackup = { + busy: false, + error: {}, + + provider: '', + bucket: '', + prefix: '', + accessKeyId: '', + secretAccessKey: '', + gcsKey: { keyFileName: '', content: '' }, + region: '', + endpoint: '', + backupFolder: '', + acceptSelfSignedCerts: false, + format: 'tgz', + backupId: '', + key: '', + + submit: function () { + $scope.importBackup.busy = true; + + var backupConfig = { + provider: $scope.importBackup.provider, + key: $scope.importBackup.key, + }; + + var backupId = $scope.importBackup.backupId; + + // only set provider specific fields, this will clear them in the db + if ($scope.s3like(backupConfig.provider)) { + backupConfig.bucket = $scope.importBackup.bucket; + backupConfig.prefix = $scope.importBackup.prefix; + backupConfig.accessKeyId = $scope.importBackup.accessKeyId; + backupConfig.secretAccessKey = $scope.importBackup.secretAccessKey; + + if ($scope.importBackup.endpoint) backupConfig.endpoint = $scope.importBackup.endpoint; + + if (backupConfig.provider === 's3') { + if ($scope.importBackup.region) backupConfig.region = $scope.importBackup.region; + delete backupConfig.endpoint; + } else if (backupConfig.provider === 'minio' || backupConfig.provider === 's3-v4-compat') { + backupConfig.region = 'us-east-1'; + backupConfig.acceptSelfSignedCerts = $scope.importBackup.acceptSelfSignedCerts; + } else if (backupConfig.provider === 'exoscale-sos') { + backupConfig.region = 'us-east-1'; + backupConfig.signatureVersion = 'v4'; + } else if (backupConfig.provider === 'wasabi') { + backupConfig.region = 'us-east-1'; + backupConfig.signatureVersion = 'v4'; + } else if (backupConfig.provider === 'scaleway-objectstorage') { + backupConfig.region = $scope.scalewayRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region; + backupConfig.signatureVersion = 'v4'; + } else if (backupConfig.provider === 'digitalocean-spaces') { + backupConfig.region = 'us-east-1'; + } + } else if (backupConfig.provider === 'gcs') { + backupConfig.bucket = $scope.importBackup.bucket; + backupConfig.prefix = $scope.importBackup.prefix; + try { + var serviceAccountKey = JSON.parse($scope.importBackup.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.importBackup.error.generic = 'Cannot parse Google Service Account Key: ' + e.message; + $scope.importBackup.error.gcsKeyInput = true; + $scope.importBackup.busy = false; + return; + } + } else if (backupConfig.provider === 'filesystem') { + var parts = backupId.split('/'); + backupId = parts.pop() || parts.pop(); // removes any trailing slash. this is basename() + backupConfig.backupFolder = parts.join('/'); // this is dirname() + + if ($scope.importBackup.format === 'tgz') { + if (backupId.substring(backupId.length - '.tar.gz'.length, backupId.length) === '.tar.gz') { // endsWith('tgz') + backupId = backupId.replace(/.tar.gz$/, ''); + } + } + } + + Client.importBackup($scope.app.id, backupId, $scope.importBackup.format, backupConfig, function (error) { + if (error) { + Client.error(error); + $scope.restore.busy = false; + return; + } + + $('#importBackupModal').modal('hide'); + + refreshApp($scope.app.id); + }); + }, + + show: function () { + $('#importBackupModal').modal('show'); + } + }; + $scope.uninstall = { busy: false, error: {},