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 @@
+
+
Automatic Backups
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: {},