Add UI to import backup
This commit is contained in:
@@ -137,6 +137,124 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- modal import backup -->
|
||||
<div class="modal fade" id="importBackupModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Import Backup</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-danger">Any data generated between now and the last known backup will be irrevocably lost.
|
||||
It is recommended to create a backup of the current data before attempting an import.
|
||||
</p>
|
||||
|
||||
<form name="importBackupForm" role="form" novalidate ng-submit="importBackup.submit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="backups.error">{{ importBackup.error.generic }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageProvider">Storage provider <sup><a ng-href="{{ config.webServerOrigin }}/documentation/backups/#storage-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="storageProvider" ng-model="importBackup.provider" ng-options="a.value as a.name for a in storageProvider" ng-change=importBackup.clearForm()></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.key }">
|
||||
<label ng-show="importBackup.provider !== 'filesystem'" class="control-label" for="inputImportBackupId">Backup ID</label>
|
||||
<label ng-show="importBackup.provider === 'filesystem'" class="control-label" for="inputImportBackupId">Backup Path</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.backupId" id="inputImportBackupId" ng-disabled="importBackup.busy" required>
|
||||
</div>
|
||||
|
||||
<!-- S3/Minio/SOS/GCS -->
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.endpoint }" ng-show="importBackup.provider === 'minio' || importBackup.provider === 's3-v4-compat'">
|
||||
<label class="control-label" for="inputimportBackupEndpoint">Endpoint</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.endpoint" id="inputimportBackupEndpoint" name="endpoint" ng-disabled="importBackup.busy" placeholder="URL of Minio/S3 Compatible" ng-required="importBackup.provider === 'minio' || importBackup.provider === 's3-v4-compat'">
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="importBackup.provider === 'minio' || importBackup.provider === 's3-v4-compat'" >
|
||||
<label>
|
||||
<input type="checkbox" ng-model="importBackup.acceptSelfSignedCerts" id="inputimportBackupSelfSigned">Accept Self-signed certificate</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.bucket }" ng-show="s3like(importBackup.provider) || importBackup.provider === 'gcs'">
|
||||
<label class="control-label" for="inputimportBackupBucket">Bucket name</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.bucket" id="inputimportBackupBucket" name="bucket" ng-disabled="importBackup.busy" ng-required="s3like(importBackup.provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.prefix }" ng-show="importBackup.provider !== 'filesystem' && importBackup.provider !== ''">
|
||||
<label class="control-label" for="inputimportBackupPrefix">Prefix</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.prefix" id="inputimportBackupPrefix" name="prefix" ng-disabled="importBackup.busy" placeholder="Prefix for backup file names">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.region }" ng-show="importBackup.provider === 's3'">
|
||||
<label class="control-label" for="inputimportBackupS3Region">Region</label>
|
||||
<select class="form-control" name="region" id="inputimportBackupS3Region" ng-model="importBackup.region" ng-options="a.value as a.name for a in s3Regions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 's3'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.region }" ng-show="importBackup.provider === 'digitalocean-spaces'">
|
||||
<label class="control-label" for="inputimportBackupDORegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputimportBackupDORegion" ng-model="importBackup.endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'digitalocean-spaces'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.region }" ng-show="importBackup.provider === 'exoscale-sos'">
|
||||
<label class="control-label" for="inputimportBackupExoscaleRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputimportBackupExoscaleRegion" ng-model="importBackup.endpoint" ng-options="a.value as a.name for a in exoscaleSosRegions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'exoscale-sos'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.region }" ng-show="importBackup.provider === 'wasabi'">
|
||||
<label class="control-label" for="inputimportBackupWasabiRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputimportBackupWasabiRegion" ng-model="importBackup.endpoint" ng-options="a.value as a.name for a in wasabiRegions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'wasabi'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.region }" ng-show="importBackup.provider === 'scaleway-objectstorage'">
|
||||
<label class="control-label" for="inputimportBackupScalewayRegion">Region</label>
|
||||
<select class="form-control" name="region" id="inputimportBackupScalewayRegion" ng-model="importBackup.endpoint" ng-options="a.value as a.name for a in scalewayRegions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'scaleway-objectstorage'"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.accessKeyId }" ng-show="s3like(importBackup.provider)">
|
||||
<label class="control-label" for="inputimportBackupAccessKeyId">Access key id</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.accessKeyId" id="inputimportBackupAccessKeyId" name="accessKeyId" ng-disabled="importBackup.busy" ng-required="s3like(importBackup.provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.secretAccessKey }" ng-show="s3like(importBackup.provider)">
|
||||
<label class="control-label" for="inputimportBackupSecretAccessKey">Secret access key</label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.secretAccessKey" id="inputimportBackupSecretAccessKey" name="secretAccessKey" ng-disabled="importBackup.busy" ng-required="s3like(importBackup.provider)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.gcsKeyInput }" ng-show="importBackup.provider === 'gcs'">
|
||||
<label class="control-label" for="gcsKeyInput">Service Account Key</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="file" id="gcsKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="importBackup.gcsKey.keyFileName" id="gcsKeyInput" name="cert" onclick="getElementById('gcsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'gcs'">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('gcsKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="storageFormat">Storage Format <sup><a ng-href="{{ config.webServerOrigin }}/documentation/backups/#backup-formats" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<select class="form-control" id="storageFormat" ng-change="importBackup.key = ''" ng-model="importBackup.format" ng-options="a.value as a.name for a in formats"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': importBackup.error.key }">
|
||||
<label class="control-label" for="inputimportBackupKey">Encryption key (optional) <sup><a ng-href="{{ config.webServerOrigin }}/documentation/backups/#encryption" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="text" class="form-control" ng-model="importBackup.key" id="inputimportBackupKey" ng-disabled="importBackup.busy" placeholder="Passphrase used to encrypt the backups">
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="importBackupForm.$invalid"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="importBackup.submit()" ng-disabled="importBackupForm.$invalid || importBackup.busy"><i class="fa fa-circle-notch fa-spin" ng-show="importBackup.busy"></i><span>Import</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal postinstall -->
|
||||
<div class="modal fade" id="postInstallModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
@@ -756,6 +874,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label class="control-label">Import From External Backup</label>
|
||||
<p>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.
|
||||
</p>
|
||||
<button class="btn btn-primary pull-right" class="btn-primary" ng-click="importBackup.show()" ng-disabled="importBackup.busy"><i class="fa fa-circle-notch fa-spin" ng-show="backups.busy"></i> Import Backup</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label class="control-label">Automatic Backups</label>
|
||||
|
||||
177
src/views/app.js
177
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: {},
|
||||
|
||||
Reference in New Issue
Block a user