Files
cloudron-box/src/views/backups.js

500 lines
23 KiB
JavaScript
Raw Normal View History

2018-06-07 14:22:48 +02:00
'use strict';
/* global angular:false */
/* global $:false */
/* global SECRET_PLACEHOLDER */
angular.module('Application').controller('BackupsController', ['$scope', '$location', '$rootScope', '$timeout', 'Client', function ($scope, $location, $rootScope, $timeout, Client) {
2020-02-24 12:56:13 +01:00
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
2018-06-07 14:22:48 +02:00
$scope.config = Client.getConfig();
$scope.user = Client.getUserInfo();
$scope.manualBackupApps = [];
2018-06-07 14:22:48 +02:00
$scope.backupConfig = {};
$scope.lastBackup = null;
$scope.backups = [];
// 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' },
2019-01-14 09:57:50 -08:00
{ name: 'Asia Pacific (Osaka-Local)', value: 'ap-northeast-3' },
2018-06-07 14:22:48 +02:00
{ 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' },
2019-01-14 09:57:50 -08:00
{ name: 'EU (Paris)', value: 'eu-west-3' },
{ name: 'EU (Stockholm)', value: 'eu-north-1' },
2018-06-07 14:22:48 +02:00
{ 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' },
];
2019-07-22 16:34:16 -07:00
$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' }
];
2018-06-07 14:22:48 +02:00
$scope.doSpacesRegions = [
{ name: 'AMS3', value: 'https://ams3.digitaloceanspaces.com' },
{ name: 'FRA1', value: 'https://fra1.digitaloceanspaces.com' },
2018-06-07 14:22:48 +02:00
{ name: 'NYC3', value: 'https://nyc3.digitaloceanspaces.com' },
2018-09-10 09:27:08 -07:00
{ name: 'SFO2', value: 'https://sfo2.digitaloceanspaces.com' },
2018-06-07 14:22:48 +02:00
{ name: 'SGP1', value: 'https://sgp1.digitaloceanspaces.com' }
];
2018-10-27 14:44:13 -07:00
$scope.exoscaleSosRegions = [
2019-06-21 10:44:24 -07:00
{ 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' },
2018-10-27 14:44:13 -07:00
{ name: 'DE-FRA-1', value: 'https://sos-de-fra-1.exo.io' },
];
2019-04-12 10:04:26 -07:00
// 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' }
];
2020-02-26 09:08:34 -08:00
$scope.linodeRegions = [
{ name: 'Newark', value: 'us-east-1.linodeobjects.com', region: 'us-east-1' }, // default
2020-03-02 20:03:02 -08:00
{ name: 'Frankfurt', value: 'eu-central-1.linodeobjects.com', region: 'us-east-1' },
2020-02-26 09:08:34 -08:00
];
2020-04-29 12:54:19 -07:00
$scope.ovhRegions = [
{ name: 'Beauharnois (BHS)', value: 'https://s3.bhs.cloud.ovh.net', region: 'us-east-1' }, // default
{ name: 'Frankfurt (DE)', value: 'https://s3.de.cloud.ovh.net', region: 'us-east-1' },
{ name: 'Gravelines (GRA)', value: 'https://s3.gra.cloud.ovh.net', region: 'us-east-1' },
{ name: 'Strasbourg (SBG)', value: 'https://s3.sbg.cloud.ovh.net', region: 'us-east-1' },
{ name: 'London (UK)', value: 'https://s3.uk.cloud.ovh.net', region: 'us-east-1' },
{ name: 'Warsaw (WAW)', value: 'https://s3.waw.cloud.ovh.net', region: 'us-east-1' },
];
2018-06-07 14:22:48 +02:00
$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' },
2020-02-26 09:08:34 -08:00
{ name: 'Linode Object Storage', value: 'linode-objectstorage' },
2018-06-07 14:22:48 +02:00
{ name: 'Minio', value: 'minio' },
2020-04-29 12:54:19 -07:00
{ name: 'OVH Object Storage', value: 'ovh-objectstorage' },
2019-04-12 10:04:26 -07:00
{ name: 'Scaleway Object Storage', value: 'scaleway-objectstorage' },
2018-06-07 14:22:48 +02:00
{ name: 'No-op (Only for testing)', value: 'noop' },
2019-07-22 16:34:16 -07:00
{ name: 'S3 API Compatible (v4)', value: 's3-v4-compat' },
{ name: 'Wasabi', value: 'wasabi' }
2018-06-07 14:22:48 +02:00
];
$scope.retentionPolicies = [
2020-05-14 16:45:52 -07:00
{ name: '2 days', value: { keepWithinSecs: 2 * 24 * 60 * 60 }},
{ name: '1 week', value: { keepWithinSecs: 7 * 24 * 60 * 60 }}, // default
{ name: '1 month', value: { keepWithinSecs: 30 * 24 * 60 * 60 }},
2020-05-14 21:36:22 -07:00
{ name: '2 daily, 1 monthly', value: { keepDaily: 2, keepMonthly: 1 }},
{ name: '3 daily, 4 weekly, 6 monthly', value: { keepDaily: 3, keepWeekly: 4, keepMonthly: 6 }},
{ name: '7 daily, 4 weekly, 12 monthly', value: { keepDaily: 7, keepWeekly: 4, keepMonthly: 12 }},
2020-05-14 16:45:52 -07:00
{ name: 'Forever', value: { keepWithinSecs: -1 }}
2018-06-07 14:22:48 +02:00
];
$scope.intervalTimes = [
{ name: 'Every 6 hours', value: 6 * 60 * 60 },
{ name: 'Every 12 hours', value: 12 * 60 * 60 },
2018-08-28 22:10:17 -07:00
{ name: 'Every day', value: 24 * 60 * 60 },
{ name: 'Every 3 days', value: 3 * 24 * 60 * 60 },
{ name: 'Every week', value: 7 * 24 * 60 * 60 },
];
2018-06-07 14:22:48 +02:00
$scope.formats = [
{ name: 'Tarball (zipped)', value: 'tgz' },
{ name: 'rsync', value: 'rsync' }
];
$scope.prettyProviderName = function (provider) {
switch (provider) {
case 'caas': return 'Managed Cloudron';
default: return provider;
}
};
$scope.createBackup = {
busy: false,
percent: 0,
message: '',
errorMessage: '',
2018-12-08 21:45:49 -08:00
taskId: '',
2018-06-07 14:22:48 +02:00
2018-12-08 20:21:11 -08:00
checkStatus: function () {
Client.getLatestTaskByType('backup', function (error, task) {
if (error) return console.error(error);
if (!task) return;
2018-12-08 21:45:49 -08:00
$scope.createBackup.taskId = task.id;
$scope.createBackup.updateStatus();
2018-12-08 20:21:11 -08:00
});
},
2018-12-08 21:45:49 -08:00
updateStatus: function () {
Client.getTask($scope.createBackup.taskId, function (error, data) {
if (error) return window.setTimeout($scope.createBackup.updateStatus, 5000);
2018-06-07 14:22:48 +02:00
2018-11-29 23:13:58 -08:00
if (!data.active) {
2018-06-07 14:22:48 +02:00
$scope.createBackup.busy = false;
$scope.createBackup.message = '';
$scope.createBackup.percent = 100; // indicates that 'result' is valid
$scope.createBackup.errorMessage = data.success ? '' : data.error.message;
2018-06-07 14:22:48 +02:00
return fetchBackups();
}
$scope.createBackup.busy = true;
2018-11-19 17:34:14 -08:00
$scope.createBackup.percent = data.percent;
$scope.createBackup.message = data.message;
2019-04-03 11:45:56 -07:00
window.setTimeout($scope.createBackup.updateStatus, 3000);
2018-06-07 14:22:48 +02:00
});
},
2018-11-17 20:04:41 -08:00
startBackup: function () {
2018-06-07 14:22:48 +02:00
$scope.createBackup.busy = true;
$scope.createBackup.percent = 0;
$scope.createBackup.message = '';
$scope.createBackup.errorMessage = '';
2018-12-08 20:21:11 -08:00
Client.startBackup(function (error, taskId) {
2018-06-07 14:22:48 +02:00
if (error) {
if (error.statusCode === 409 && error.message.indexOf('full_backup') !== -1) {
$scope.createBackup.errorMessage = 'Backup already in progress. Please retry later.';
} else if (error.statusCode === 409) {
$scope.createBackup.errorMessage = 'App task is currently in progress. Please retry later.';
} else {
console.error(error);
$scope.createBackup.errorMessage = error.message;
}
$scope.createBackup.busy = false;
$('#createBackupFailedModal').modal('show');
return;
}
2018-12-08 21:45:49 -08:00
$scope.createBackup.taskId = taskId;
$scope.createBackup.updateStatus();
2018-06-07 14:22:48 +02:00
});
2018-11-17 20:04:41 -08:00
},
stopBackup: function () {
2019-01-24 15:55:21 -08:00
Client.stopTask($scope.createBackup.taskId, function (error) {
2018-11-17 20:04:41 -08:00
if (error) {
if (error.statusCode === 409) {
$scope.createBackup.errorMessage = 'No backup is currently in progress';
} else {
console.error(error);
$scope.createBackup.errorMessage = error.message;
}
$scope.createBackup.busy = false;
return;
}
});
2018-06-07 14:22:48 +02:00
}
};
$scope.s3like = function (provider) {
2019-04-12 10:04:26 -07:00
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat'
|| provider === 'exoscale-sos' || provider === 'digitalocean-spaces'
2020-02-26 09:08:34 -08:00
|| provider === 'scaleway-objectstorage' || provider === 'wasabi'
2020-04-29 12:54:19 -07:00
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage';
2018-06-07 14:22:48 +02:00
};
2020-04-20 18:21:35 +02:00
// https://stackoverflow.com/questions/3665115/how-to-create-a-file-in-memory-for-user-to-download-but-not-through-server#18197341
function download(filename, text) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
$scope.downloadConfig = function () {
// secrets and tokens already come with placeholder characters we remove them
var tmp = {
encrypted: !!$scope.backupConfig.password // we add this just to help the import UI
};
Object.keys($scope.backupConfig).forEach(function (k) {
if ($scope.backupConfig[k] !== SECRET_PLACEHOLDER) tmp[k] = $scope.backupConfig[k];
});
download('cloudron_backup.json', JSON.stringify(tmp));
2020-04-20 18:21:35 +02:00
};
2018-06-07 14:22:48 +02:00
$scope.configureBackup = {
busy: false,
error: {},
provider: '',
bucket: '',
prefix: '',
accessKeyId: '',
secretAccessKey: '',
gcsKey: { keyFileName: '', content: '' },
region: '',
endpoint: '',
backupFolder: '',
retentionPolicy: $scope.retentionPolicies[0],
intervalSecs: 24 * 60 * 60,
2018-06-07 14:22:48 +02:00
acceptSelfSignedCerts: false,
useHardlinks: true,
externalDisk: false,
2018-06-07 14:22:48 +02:00
format: 'tgz',
2020-05-12 10:54:15 -07:00
password: '',
2018-06-07 14:22:48 +02:00
clearProviderFields: function () {
2018-06-07 14:22:48 +02:00
$scope.configureBackup.bucket = '';
$scope.configureBackup.prefix = '';
$scope.configureBackup.accessKeyId = '';
$scope.configureBackup.secretAccessKey = '';
$scope.configureBackup.gcsKey.keyFileName = '';
$scope.configureBackup.gcsKey.content = '';
$scope.configureBackup.endpoint = '';
$scope.configureBackup.region = '';
$scope.configureBackup.backupFolder = '';
$scope.configureBackup.acceptSelfSignedCerts = false;
$scope.configureBackup.useHardlinks = true;
$scope.configureBackup.externalDisk = false;
2018-06-07 14:22:48 +02:00
},
show: function () {
$scope.configureBackup.error = {};
$scope.configureBackup.busy = false;
$scope.configureBackup.provider = $scope.backupConfig.provider;
$scope.configureBackup.bucket = $scope.backupConfig.bucket;
$scope.configureBackup.prefix = $scope.backupConfig.prefix;
$scope.configureBackup.region = $scope.backupConfig.region;
$scope.configureBackup.accessKeyId = $scope.backupConfig.accessKeyId;
$scope.configureBackup.secretAccessKey = $scope.backupConfig.secretAccessKey;
if ($scope.backupConfig.provider === 'gcs') {
$scope.configureBackup.gcsKey.keyFileName = $scope.backupConfig.credentials.client_email;
$scope.configureBackup.gcsKey.content = JSON.stringify({
project_id: $scope.backupConfig.projectId,
client_email: $scope.backupConfig.credentials.client_email,
private_key: $scope.backupConfig.credentials.private_key,
});
}
$scope.configureBackup.endpoint = $scope.backupConfig.endpoint;
2020-05-12 10:54:15 -07:00
$scope.configureBackup.password = $scope.backupConfig.password;
2018-06-07 14:22:48 +02:00
$scope.configureBackup.backupFolder = $scope.backupConfig.backupFolder;
2020-05-14 16:41:55 -07:00
$scope.configureBackup.retentionPolicy = $scope.retentionPolicies.find(function (x) { return angular.equals(x.value, $scope.backupConfig.retentionPolicy); }).value;
$scope.configureBackup.intervalSecs = $scope.backupConfig.intervalSecs;
2018-06-07 14:22:48 +02:00
$scope.configureBackup.format = $scope.backupConfig.format;
$scope.configureBackup.acceptSelfSignedCerts = !!$scope.backupConfig.acceptSelfSignedCerts;
$scope.configureBackup.useHardlinks = !$scope.backupConfig.noHardlinks;
$scope.configureBackup.externalDisk = !!$scope.backupConfig.externalDisk;
2018-06-07 14:22:48 +02:00
$('#configureBackupModal').modal('show');
},
submit: function () {
$scope.configureBackup.error = {};
$scope.configureBackup.busy = true;
var backupConfig = {
provider: $scope.configureBackup.provider,
retentionPolicy: $scope.configureBackup.retentionPolicy,
intervalSecs: $scope.configureBackup.intervalSecs,
2018-06-07 14:22:48 +02:00
format: $scope.configureBackup.format
};
2020-05-12 10:54:15 -07:00
if ($scope.configureBackup.password) backupConfig.password = $scope.configureBackup.password;
2018-06-07 14:22:48 +02:00
// only set provider specific fields, this will clear them in the db
if ($scope.s3like(backupConfig.provider)) {
backupConfig.bucket = $scope.configureBackup.bucket;
backupConfig.prefix = $scope.configureBackup.prefix;
backupConfig.accessKeyId = $scope.configureBackup.accessKeyId;
backupConfig.secretAccessKey = $scope.configureBackup.secretAccessKey;
if ($scope.configureBackup.endpoint) backupConfig.endpoint = $scope.configureBackup.endpoint;
if (backupConfig.provider === 's3') {
if ($scope.configureBackup.region) backupConfig.region = $scope.configureBackup.region;
delete backupConfig.endpoint;
2018-06-07 14:22:48 +02:00
} else if (backupConfig.provider === 'minio' || backupConfig.provider === 's3-v4-compat') {
backupConfig.region = 'us-east-1';
backupConfig.acceptSelfSignedCerts = $scope.configureBackup.acceptSelfSignedCerts;
} else if (backupConfig.provider === 'exoscale-sos') {
backupConfig.region = 'us-east-1';
backupConfig.signatureVersion = 'v4';
2019-07-22 16:34:16 -07:00
} else if (backupConfig.provider === 'wasabi') {
backupConfig.region = 'us-east-1';
backupConfig.signatureVersion = 'v4';
2019-04-12 10:04:26 -07:00
} else if (backupConfig.provider === 'scaleway-objectstorage') {
backupConfig.region = $scope.scalewayRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
2020-02-26 09:08:34 -08:00
} else if (backupConfig.provider === 'linode-objectstorage') {
backupConfig.region = $scope.linodeRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
2020-04-29 12:54:19 -07:00
} else if (backupConfig.provider === 'ovh-objectstorage') {
backupConfig.region = $scope.ovhRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
2018-06-07 14:22:48 +02:00
} else if (backupConfig.provider === 'digitalocean-spaces') {
backupConfig.region = 'us-east-1';
}
} else if (backupConfig.provider === 'gcs') {
backupConfig.bucket = $scope.configureBackup.bucket;
backupConfig.prefix = $scope.configureBackup.prefix;
try {
var serviceAccountKey = JSON.parse($scope.configureBackup.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.configureBackup.error.generic = 'Cannot parse Google Service Account Key: ' + e.message;
$scope.configureBackup.error.gcsKeyInput = true;
$scope.configureBackup.busy = false;
return;
}
} else if (backupConfig.provider === 'filesystem') {
backupConfig.backupFolder = $scope.configureBackup.backupFolder;
backupConfig.noHardlinks = !$scope.configureBackup.useHardlinks;
backupConfig.externalDisk = $scope.configureBackup.externalDisk;
2018-06-07 14:22:48 +02:00
}
Client.setBackupConfig(backupConfig, function (error) {
$scope.configureBackup.busy = false;
if (error) {
2018-09-10 10:36:32 -07:00
if (error.statusCode === 424) {
2018-06-07 14:22:48 +02:00
$scope.configureBackup.error.generic = error.message;
if (error.message.indexOf('AWS Access Key Id') !== -1) {
$scope.configureBackup.error.accessKeyId = true;
$scope.configureBackup.accessKeyId = '';
$scope.configureBackupForm.accessKeyId.$setPristine();
$('#inputConfigureBackupAccessKeyId').focus();
} else if (error.message.indexOf('not match the signature') !== -1 ) {
$scope.configureBackup.error.secretAccessKey = true;
$scope.configureBackup.secretAccessKey = '';
$scope.configureBackupForm.secretAccessKey.$setPristine();
$('#inputConfigureBackupSecretAccessKey').focus();
} else if (error.message.toLowerCase() === 'access denied') {
$scope.configureBackup.error.bucket = true;
$scope.configureBackup.bucket = '';
$scope.configureBackupForm.bucket.$setPristine();
$('#inputConfigureBackupBucket').focus();
} else if (error.message.indexOf('ECONNREFUSED') !== -1) {
$scope.configureBackup.error.generic = 'Unknown region';
$scope.configureBackup.error.region = true;
$scope.configureBackupForm.region.$setPristine();
$('#inputConfigureBackupDORegion').focus();
} else if (error.message.toLowerCase() === 'wrong region') {
$scope.configureBackup.error.generic = 'Wrong S3 Region';
$scope.configureBackup.error.region = true;
$scope.configureBackupForm.region.$setPristine();
$('#inputConfigureBackupS3Region').focus();
} else {
$('#inputConfigureBackupBucket').focus();
}
} else if (error.statusCode === 400) {
$scope.configureBackup.error.generic = error.message;
2020-05-12 10:54:15 -07:00
if (error.message.indexOf('password') !== -1) {
$scope.configureBackup.error.password = true;
$scope.configureBackupForm.password.$setPristine();
} else if ($scope.configureBackup.provider === 'filesystem') {
2018-06-07 14:22:48 +02:00
$scope.configureBackup.error.backupFolder = true;
}
} else {
console.error('Unable to change provider.', error);
}
return;
}
// $scope.configureBackup.reset();
$('#configureBackupModal').modal('hide');
getBackupConfig();
});
}
};
function fetchBackups() {
Client.getBackups(function (error, backups) {
if (error) return console.error(error);
$scope.backups = backups;
if ($scope.backups.length > 0) {
$scope.lastBackup = backups[0];
} else {
$scope.lastBackup = null;
}
});
}
function getBackupConfig() {
Client.getBackupConfig(function (error, backupConfig) {
if (error) return console.error(error);
$scope.backupConfig = backupConfig;
});
}
Client.onReady(function () {
fetchBackups();
2019-05-07 10:11:54 -07:00
getBackupConfig();
2018-06-07 14:22:48 +02:00
$scope.manualBackupApps = Client.getInstalledApps().filter(function (app) { return !app.enableBackup; });
2018-06-07 14:22:48 +02:00
// show backup status
2018-12-08 20:21:11 -08:00
$scope.createBackup.checkStatus();
2018-06-07 14:22:48 +02:00
});
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.configureBackup.gcsKey, 'content', 'keyFileName');
// setup all the dialog focus handling
['configureBackupModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find("[autofocus]:first").focus();
});
});
$('.modal-backdrop').remove();
}]);