'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) { Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); }); $scope.config = Client.getConfig(); $scope.user = Client.getUserInfo(); $scope.manualBackupApps = []; $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' }, { 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.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' }, ]; $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' }, ]; $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: 'Linode Object Storage', value: 'linode-objectstorage' }, { name: 'Minio', value: 'minio' }, { name: 'OVH Object Storage', value: 'ovh-objectstorage' }, { 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.retentionTimes = [ { name: '2 days', value: 2 * 24 * 60 * 60 }, { name: '1 week', value: 7 * 24 * 60 * 60}, { name: '1 month', value: 30 * 24 * 60 * 60}, { name: 'Forever', value: -1 } ]; $scope.intervalTimes = [ { name: 'Every 6 hours', value: 6 * 60 * 60 }, { name: 'Every 12 hours', value: 12 * 60 * 60 }, { name: 'Every day', value: 24 * 60 * 60 }, { name: 'Every 3 days', value: 3 * 24 * 60 * 60 }, { name: 'Every week', value: 7 * 24 * 60 * 60 }, ]; $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: '', taskId: '', checkStatus: function () { Client.getLatestTaskByType('backup', function (error, task) { if (error) return console.error(error); if (!task) return; $scope.createBackup.taskId = task.id; $scope.createBackup.updateStatus(); }); }, updateStatus: function () { Client.getTask($scope.createBackup.taskId, function (error, data) { if (error) return window.setTimeout($scope.createBackup.updateStatus, 5000); if (!data.active) { $scope.createBackup.busy = false; $scope.createBackup.message = ''; $scope.createBackup.percent = 100; // indicates that 'result' is valid $scope.createBackup.errorMessage = data.success ? '' : data.error.message; return fetchBackups(); } $scope.createBackup.busy = true; $scope.createBackup.percent = data.percent; $scope.createBackup.message = data.message; window.setTimeout($scope.createBackup.updateStatus, 3000); }); }, startBackup: function () { $scope.createBackup.busy = true; $scope.createBackup.percent = 0; $scope.createBackup.message = ''; $scope.createBackup.errorMessage = ''; Client.startBackup(function (error, taskId) { 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; } $scope.createBackup.taskId = taskId; $scope.createBackup.updateStatus(); }); }, stopBackup: function () { Client.stopTask($scope.createBackup.taskId, function (error) { 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; } }); } }; $scope.s3like = function (provider) { return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos' || provider === 'digitalocean-spaces' || provider === 'scaleway-objectstorage' || provider === 'wasabi' || provider === 'linode-objectstorage' || provider === 'ovh-objectstorage'; }; // 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)); }; $scope.configureBackup = { busy: false, error: {}, provider: '', bucket: '', prefix: '', accessKeyId: '', secretAccessKey: '', gcsKey: { keyFileName: '', content: '' }, region: '', endpoint: '', backupFolder: '', retentionSecs: 7 * 24 * 60 * 60, intervalSecs: 24 * 60 * 60, acceptSelfSignedCerts: false, useHardlinks: true, externalDisk: false, format: 'tgz', password: '', clearProviderFields: function () { $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; }, 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; $scope.configureBackup.password = $scope.backupConfig.password; $scope.configureBackup.backupFolder = $scope.backupConfig.backupFolder; $scope.configureBackup.retentionSecs = $scope.backupConfig.retentionSecs; $scope.configureBackup.intervalSecs = $scope.backupConfig.intervalSecs; $scope.configureBackup.format = $scope.backupConfig.format; $scope.configureBackup.acceptSelfSignedCerts = !!$scope.backupConfig.acceptSelfSignedCerts; $scope.configureBackup.useHardlinks = !$scope.backupConfig.noHardlinks; $scope.configureBackup.externalDisk = !!$scope.backupConfig.externalDisk; $('#configureBackupModal').modal('show'); }, submit: function () { $scope.configureBackup.error = {}; $scope.configureBackup.busy = true; var backupConfig = { provider: $scope.configureBackup.provider, retentionSecs: $scope.configureBackup.retentionSecs, intervalSecs: $scope.configureBackup.intervalSecs, format: $scope.configureBackup.format }; if ($scope.configureBackup.password) backupConfig.password = $scope.configureBackup.password; // 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; } 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'; } 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.configureBackup.endpoint; }).region; backupConfig.signatureVersion = 'v4'; } else if (backupConfig.provider === 'linode-objectstorage') { backupConfig.region = $scope.linodeRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region; backupConfig.signatureVersion = 'v4'; } else if (backupConfig.provider === 'ovh-objectstorage') { backupConfig.region = $scope.ovhRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region; backupConfig.signatureVersion = 'v4'; } 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; } Client.setBackupConfig(backupConfig, function (error) { $scope.configureBackup.busy = false; if (error) { if (error.statusCode === 424) { $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; if (error.message.indexOf('password') !== -1) { $scope.configureBackup.error.password = true; $scope.configureBackupForm.password.$setPristine(); } else if ($scope.configureBackup.provider === 'filesystem') { $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(); getBackupConfig(); $scope.manualBackupApps = Client.getInstalledApps().filter(function (app) { return !app.enableBackup; }); // show backup status $scope.createBackup.checkStatus(); }); 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(); }]);