'use strict'; /* global $, angular, TASK_TYPES, SECRET_PLACEHOLDER, STORAGE_PROVIDERS, BACKUP_FORMATS, APP_TYPES */ /* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR */ 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.SECRET_PLACEHOLDER = SECRET_PLACEHOLDER; $scope.MIN_MEMORY_LIMIT = 800 * 1024 * 1024; $scope.config = Client.getConfig(); $scope.user = Client.getUserInfo(); $scope.memory = null; // { memory, swap } $scope.manualBackupApps = []; $scope.backupConfig = {}; $scope.backups = []; $scope.s3Regions = REGIONS_S3; $scope.wasabiRegions = REGIONS_WASABI; $scope.doSpacesRegions = REGIONS_DIGITALOCEAN; $scope.exoscaleSosRegions = REGIONS_EXOSCALE; $scope.scalewayRegions = REGIONS_SCALEWAY; $scope.linodeRegions = REGIONS_LINODE; $scope.ovhRegions = REGIONS_OVH; $scope.ionosRegions = REGIONS_IONOS; $scope.upcloudRegions = REGIONS_UPCLOUD; $scope.vultrRegions = REGIONS_VULTR; $scope.storageProviders = STORAGE_PROVIDERS.concat([ { name: 'No-op (Only for testing)', value: 'noop' } ]); $scope.retentionPolicies = [ { 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 }}, { name: '3 months', value: { keepWithinSecs: 3 * 30 * 24 * 60 * 60 }}, { name: '2 daily, 4 weekly', value: { keepDaily: 2, keepWeekly: 4 }}, { 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 }}, { name: 'Forever', value: { keepWithinSecs: -1 }} ]; // values correspond to cron days $scope.cronDays = [ { name: 'Sunday', value: 0 }, { name: 'Monday', value: 1 }, { name: 'Tuesday', value: 2 }, { name: 'Wednesday', value: 3 }, { name: 'Thursday', value: 4 }, { name: 'Friday', value: 5 }, { name: 'Saturday', value: 6 }, ]; // generates 24h time sets (instead of american 12h) to avoid having to translate everything to locales eg. 12:00 $scope.cronHours = Array.from({ length: 24 }).map(function (v, i) { return { name: (i < 10 ? '0' : '') + i + ':00', value: i }; }); $scope.formats = BACKUP_FORMATS; $scope.prettyProviderName = function (provider) { switch (provider) { case 'caas': return 'Managed Cloudron'; default: return provider; } }; $scope.prettyBackupSchedule = function (pattern) { if (!pattern) return ''; var tmp = pattern.split(' '); var hours = tmp[2].split(','), days = tmp[5].split(','); var prettyDay; if (days.length === 7 || days[0] === '*') { prettyDay = 'Everyday'; } else { prettyDay = days.map(function (day) { return $scope.cronDays[parseInt(day, 10)].name.substr(0, 3); }).join(','); } var prettyHour = hours.map(function (hour) { return $scope.cronHours[parseInt(hour, 10)].name; }).join(','); return prettyDay + ' at ' + prettyHour; }; $scope.prettyBackupRetentionPolicy = function (retentionPolicy) { var tmp = $scope.retentionPolicies.find(function (p) { return angular.equals(p.value, retentionPolicy); }); return tmp ? tmp.name : ''; }; $scope.remount = { busy: false, error: null, submit: function () { if (!$scope.mountlike($scope.backupConfig.provider)) return; $scope.remount.busy = true; $scope.remount.error = null; Client.remountBackupStorage(function (error) { if (error) { console.error('Failed to remount backup storage.', error); $scope.remount.error = error.message; } // give the backend some time $timeout(function () { $scope.remount.busy = false; getBackupConfig(); }, 2000); }); } }; $scope.createBackup = { busy: false, percent: 0, message: '', errorMessage: '', taskId: '', taskType: TASK_TYPES.TASK_BACKUP, checkStatus: function () { // TODO support both task types TASK_BACKUP and TASK_CLEAN_BACKUPS Client.getLatestTaskByType($scope.createBackup.taskType, 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 = ''; $scope.createBackup.taskType = TASK_TYPES.TASK_BACKUP; 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(); }); }, cleanupBackups: function () { $('#cleanupBackupsModal').modal('show'); }, startCleanup: function () { $scope.createBackup.busy = true; $scope.createBackup.percent = 0; $scope.createBackup.message = ''; $scope.createBackup.errorMessage = ''; $scope.createBackup.taskType = TASK_TYPES.TASK_CLEAN_BACKUPS; $('#cleanupBackupsModal').modal('hide'); Client.cleanupBackups(function (error, taskId) { if (error) console.error(error); $scope.createBackup.taskId = taskId; $scope.createBackup.updateStatus(); }); }, stopTask: 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.listBackups = { }; $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 === 'backblaze-b2' || provider === 'cloudflare-r2' || provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage' || provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2'; }; $scope.mountlike = function (provider) { return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs'; }; // 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 (backup) { // secrets and tokens already come with placeholder characters we remove them var tmp = { remotePath: backup.remotePath, 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]; }); var filename = 'cloudron-backup-config-' + (new Date()).toISOString().replace(/:|T/g,'-').replace(/\..*/,'') + '.json'; download(filename, JSON.stringify(tmp, null, 4)); }; $scope.editBackup = { busy: false, error: null, backup: null, label: '', persist: false, show: function (backup) { $scope.editBackup.backup = backup; $scope.editBackup.label = backup.label; $scope.editBackup.persist = backup.preserveSecs === -1; $scope.editBackup.error = null; $scope.editBackup.busy = false; $('#editBackupModal').modal('show'); }, submit: function () { $scope.editBackup.error = null; $scope.editBackup.busy = true; Client.editBackup($scope.editBackup.backup.id, $scope.editBackup.label, $scope.editBackup.persist ? -1 : 0, function (error) { $scope.editBackup.busy = false; if (error) return $scope.editBackup.error = error.message; fetchBackups(); $('#editBackupModal').modal('hide'); }); } }; $scope.backupDetails = { backup: null, show: function (backup) { $scope.backupDetails.backup = backup; $('#backupDetailsModal').modal('show'); } }; $scope.configureScheduleAndRetention = { busy: false, error: {}, retentionPolicy: $scope.retentionPolicies[0], days: [], hours: [], show: function () { $scope.configureScheduleAndRetention.error = {}; $scope.configureScheduleAndRetention.busy = false; var selectedPolicy = $scope.retentionPolicies.find(function (x) { return angular.equals(x.value, $scope.backupConfig.retentionPolicy); }); if (!selectedPolicy) selectedPolicy = $scope.retentionPolicies[0]; $scope.configureScheduleAndRetention.retentionPolicy = selectedPolicy.value; var tmp = $scope.backupConfig.schedulePattern.split(' '); var hours = tmp[2].split(','), days = tmp[5].split(','); if (days[0] === '*') { $scope.configureScheduleAndRetention.days = angular.copy($scope.cronDays, []); } else { $scope.configureScheduleAndRetention.days = days.map(function (day) { return $scope.cronDays[parseInt(day, 10)]; }); } $scope.configureScheduleAndRetention.hours = hours.map(function (hour) { return $scope.cronHours[parseInt(hour, 10)]; }); $('#configureScheduleAndRetentionModal').modal('show'); }, submit: function () { $scope.configureScheduleAndRetention.error = {}; $scope.configureScheduleAndRetention.busy = true; // start with the full backupConfig since the api requires all fields var backupConfig = $scope.backupConfig; backupConfig.retentionPolicy = $scope.configureScheduleAndRetention.retentionPolicy; var daysPattern; if ($scope.configureScheduleAndRetention.days.length === 7) daysPattern = '*'; else daysPattern = $scope.configureScheduleAndRetention.days.map(function (d) { return d.value; }); var hoursPattern; if ($scope.configureScheduleAndRetention.hours.length === 24) hoursPattern = '*'; else hoursPattern = $scope.configureScheduleAndRetention.hours.map(function (d) { return d.value; }); backupConfig.schedulePattern ='00 00 ' + hoursPattern + ' * * ' + daysPattern; Client.setBackupConfig(backupConfig, function (error) { $scope.configureScheduleAndRetention.busy = false; if (error) { if (error.statusCode === 424) { $scope.configureScheduleAndRetention.error.generic = error.message; } else if (error.statusCode === 400) { $scope.configureScheduleAndRetention.error.generic = error.message; } else { console.error('Unable to change schedule or retention.', error); } return; } $('#configureScheduleAndRetentionModal').modal('hide'); getBackupConfig(); }); } }; $scope.configureBackup = { busy: false, error: {}, provider: '', bucket: '', prefix: '', accessKeyId: '', secretAccessKey: '', gcsKey: { keyFileName: '', content: '' }, region: '', endpoint: '', backupFolder: '', mountPoint: '', acceptSelfSignedCerts: false, useHardlinks: true, chown: true, format: 'tgz', password: '', passwordRepeat: '', encryptedFilenames: true, advancedVisible: false, memoryTicks: [], memoryLimit: $scope.MIN_MEMORY_LIMIT, uploadPartSizeTicks: [], uploadPartSize: 50 * 1024 * 1024, copyConcurrency: '', downloadConcurrency: '', syncConcurrency: '', // sort of similar to upload mountOptions: { host: '', remoteDir: '', username: '', password: '', diskPath: '', seal: false, user: '', port: 22, privateKey: '' }, 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.mountPoint = ''; $scope.configureBackup.acceptSelfSignedCerts = false; $scope.configureBackup.useHardlinks = true; $scope.configureBackup.chown = true; $scope.configureBackup.memoryLimit = $scope.MIN_MEMORY_LIMIT; // scaleway only supports 1000 parts per object (https://www.scaleway.com/en/docs/s3-multipart-upload/) $scope.configureBackup.uploadPartSize = $scope.configureBackup.provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024; $scope.configureBackup.downloadConcurrency = $scope.configureBackup.provider === 's3' ? 30 : 10; $scope.configureBackup.syncConcurrency = $scope.configureBackup.provider === 's3' ? 20 : 10; $scope.configureBackup.copyConcurrency = $scope.configureBackup.provider === 's3' ? 500 : 10; $scope.configureBackup.mountOptions = { host: '', remoteDir: '', username: '', password: '', diskPath: '', seal: false, user: '', port: 22, privateKey: '' }; }, show: function () { $scope.configureBackup.error = {}; $scope.configureBackup.busy = false; $scope.configureBackup.advancedVisible = 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.passwordRepeat = ''; $scope.configureBackup.encryptedFilenames = 'encryptedFilenames' in $scope.backupConfig ? $scope.backupConfig.encryptedFilenames : true; $scope.configureBackup.backupFolder = $scope.backupConfig.backupFolder; $scope.configureBackup.mountPoint = $scope.backupConfig.mountPoint; $scope.configureBackup.format = $scope.backupConfig.format; $scope.configureBackup.acceptSelfSignedCerts = !!$scope.backupConfig.acceptSelfSignedCerts; $scope.configureBackup.useHardlinks = !$scope.backupConfig.noHardlinks; $scope.configureBackup.chown = $scope.backupConfig.chown; $scope.configureBackup.memoryLimit = $scope.backupConfig.memoryLimit; $scope.configureBackup.uploadPartSize = $scope.backupConfig.uploadPartSize || ($scope.configureBackup.provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024); $scope.configureBackup.downloadConcurrency = $scope.backupConfig.downloadConcurrency || ($scope.backupConfig.provider === 's3' ? 30 : 10); $scope.configureBackup.syncConcurrency = $scope.backupConfig.syncConcurrency || ($scope.backupConfig.provider === 's3' ? 20 : 10); $scope.configureBackup.copyConcurrency = $scope.backupConfig.copyConcurrency || ($scope.backupConfig.provider === 's3' ? 500 : 10); var totalMemory = Math.max(($scope.memory.memory + $scope.memory.swap) * 1.5, 2 * 1024 * 1024); $scope.configureBackup.memoryTicks = [ $scope.MIN_MEMORY_LIMIT ]; for (var i = 1024; i <= totalMemory/1024/1024; i *= 2) { $scope.configureBackup.memoryTicks.push(i * 1024 * 1024); } $scope.configureBackup.uploadPartSizeTicks = [ 5 * 1024 * 1024 ]; for (var j = 32; j <= 1 * 1024; j *= 2) { // 5 GB is max for s3. but let's keep things practical for now. we upload 3 parts in parallel $scope.configureBackup.uploadPartSizeTicks.push(j * 1024 * 1024); } var mountOptions = $scope.backupConfig.mountOptions || {}; $scope.configureBackup.mountOptions = { host: mountOptions.host || '', remoteDir: mountOptions.remoteDir || '', username: mountOptions.username || '', password: mountOptions.password || '', diskPath: mountOptions.diskPath || '', seal: mountOptions.seal, user: mountOptions.user || '', port: mountOptions.port || 22, privateKey: mountOptions.privateKey || '' }; $('#configureBackupModal').modal('show'); }, submit: function () { $scope.configureBackup.error = {}; $scope.configureBackup.busy = true; var backupConfig = { provider: $scope.configureBackup.provider, format: $scope.configureBackup.format, memoryLimit: $scope.configureBackup.memoryLimit, // required for api call to provide all fields schedulePattern: $scope.backupConfig.schedulePattern, retentionPolicy: $scope.backupConfig.retentionPolicy }; if ($scope.configureBackup.password) { backupConfig.password = $scope.configureBackup.password; backupConfig.encryptedFilenames = $scope.configureBackup.encryptedFilenames; // ignored with tgz format } // 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 = $scope.configureBackup.region || 'us-east-1'; backupConfig.acceptSelfSignedCerts = $scope.configureBackup.acceptSelfSignedCerts; backupConfig.s3ForcePathStyle = true; // might want to expose this in the UI } else if (backupConfig.provider === 'exoscale-sos') { backupConfig.region = 'us-east-1'; backupConfig.signatureVersion = 'v4'; } else if (backupConfig.provider === 'wasabi') { backupConfig.region = $scope.wasabiRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region; 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 === 'ionos-objectstorage') { backupConfig.region = $scope.ionosRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region; backupConfig.signatureVersion = 'v4'; } else if (backupConfig.provider === 'vultr-objectstorage') { backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region; backupConfig.signatureVersion = 'v4'; } else if (backupConfig.provider === 'upcloud-objectstorage') { // the UI sets region and endpoint var m = /^.*\.(.*)\.upcloudobjects.com$/.exec(backupConfig.endpoint); backupConfig.region = m ? m[1] : 'us-east-1'; // let it fail in validation phase if m is not valid 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 ($scope.mountlike(backupConfig.provider)) { backupConfig.prefix = $scope.configureBackup.prefix; backupConfig.noHardlinks = !$scope.configureBackup.useHardlinks; backupConfig.mountOptions = {}; if (backupConfig.provider === 'cifs' || backupConfig.provider === 'sshfs' || backupConfig.provider === 'nfs') { backupConfig.mountOptions.host = $scope.configureBackup.mountOptions.host; backupConfig.mountOptions.remoteDir = $scope.configureBackup.mountOptions.remoteDir; if (backupConfig.provider === 'cifs') { backupConfig.mountOptions.username = $scope.configureBackup.mountOptions.username; backupConfig.mountOptions.password = $scope.configureBackup.mountOptions.password; backupConfig.mountOptions.seal = $scope.configureBackup.mountOptions.seal; } else if (backupConfig.provider === 'sshfs') { backupConfig.mountOptions.user = $scope.configureBackup.mountOptions.user; backupConfig.mountOptions.port = $scope.configureBackup.mountOptions.port; backupConfig.mountOptions.privateKey = $scope.configureBackup.mountOptions.privateKey; } } else if (backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs') { backupConfig.mountOptions.diskPath = $scope.configureBackup.mountOptions.diskPath; } else if (backupConfig.provider === 'mountpoint') { backupConfig.mountPoint = $scope.configureBackup.mountPoint; backupConfig.chown = $scope.configureBackup.chown; backupConfig.preserveAttributes = true; } } else if (backupConfig.provider === 'filesystem') { backupConfig.backupFolder = $scope.configureBackup.backupFolder; backupConfig.noHardlinks = !$scope.configureBackup.useHardlinks; } backupConfig.uploadPartSize = $scope.configureBackup.uploadPartSize; if (backupConfig.format === 'rsync') { backupConfig.downloadConcurrency = $scope.configureBackup.downloadConcurrency; backupConfig.syncConcurrency = $scope.configureBackup.syncConcurrency; backupConfig.copyConcurrency = $scope.configureBackup.copyConcurrency; } 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; $scope.backups = $scope.backups.slice(0, 20); // only show 20 since we don't have pagination // add contents property var appsById = {}, appsByFqdn = {}; Client.getInstalledApps().forEach(function (app) { appsById[app.id] = app; appsByFqdn[app.fqdn] = app; }); $scope.backups.forEach(function (backup) { backup.contents = []; backup.dependsOn.forEach(function (appBackupId) { let match = appBackupId.match(/app_(.*?)_.*/); // *? means non-greedy if (!match) return; if (match[1].indexOf('.') !== -1) { // newer backups have fqdn in them if (appsByFqdn[match[1]]) backup.contents.push(appsByFqdn[match[1]]); } else { if (appsById[match[1]]) backup.contents.push(appsById[match[1]]); } }); }); }); } function getBackupConfig() { Client.getBackupConfig(function (error, backupConfig) { if (error) return console.error(error); $scope.backupConfig = backupConfig; }); } Client.onReady(function () { Client.memory(function (error, memory) { if (error) console.error(error); $scope.memory = memory; fetchBackups(); getBackupConfig(); $scope.manualBackupApps = Client.getInstalledApps().filter(function (app) { return app.type !== APP_TYPES.LINK && !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', 'editBackupModal'].forEach(function (id) { $('#' + id).on('shown.bs.modal', function () { $(this).find("[autofocus]:first").focus(); }); }); $('.modal-backdrop').remove(); }]);