'use strict'; /* global $, angular, TASK_TYPES, 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.SECRET_PLACEHOLDER = SECRET_PLACEHOLDER; $scope.config = Client.getConfig(); $scope.user = Client.getUserInfo(); $scope.memory = null; // { memory, swap } $scope.manualBackupApps = []; $scope.backupCheck = { ok: true, message: '' }; $scope.backupConfig = {}; $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: 'China (Beijing)', value: 'cn-north-1' }, { name: 'China (Ningxia)', value: 'cn-northwest-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.us-east-1.wasabisys.com' }, { name: 'US East 2', value: 'https://s3.us-east-2.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: 'SFO3', value: 'https://sfo3.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' }, { name: 'Singapore', value: 'ap-south-1.linodeobjects.com', region: 'us-east-1' } ]; // note: ovh also has a storage endpoint but that only supports path style access $scope.ovhRegions = [ { name: 'Beauharnois (BHS)', value: 'https://s3.bhs.cloud.ovh.net', region: 'bhs' }, // default { name: 'Frankfurt (DE)', value: 'https://s3.de.cloud.ovh.net', region: 'de' }, { name: 'Gravelines (GRA)', value: 'https://s3.gra.cloud.ovh.net', region: 'gra' }, { name: 'Strasbourg (SBG)', value: 'https://s3.sbg.cloud.ovh.net', region: 'sbg' }, { name: 'London (UK)', value: 'https://s3.uk.cloud.ovh.net', region: 'uk' }, { name: 'Sydney (SYD)', value: 'https://s3.syd.cloud.ovh.net', region: 'syd' }, { name: 'Warsaw (WAW)', value: 'https://s3.waw.cloud.ovh.net', region: 'waw' }, ]; // https://devops.ionos.com/api/s3/ $scope.ionosRegions = [ { name: 'DE', value: 'https://s3-de-central.profitbricks.com', region: 's3-de-central' }, // default ]; $scope.storageProvider = [ { name: 'Amazon S3', value: 's3' }, { name: 'Backblaze B2 (S3 API)', value: 'backblaze-b2' }, { name: 'CIFS Mount', value: 'cifs' }, { name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' }, { name: 'EXT4 Disk', value: 'ext4' }, { name: 'Exoscale SOS', value: 'exoscale-sos' }, { name: 'Filesystem', value: 'filesystem' }, { name: 'Filesystem (Mountpoint)', value: 'mountpoint' }, // legacy { name: 'Google Cloud Storage', value: 'gcs' }, { name: 'IONOS (Profitbricks)', value: 'ionos-objectstorage' }, { name: 'Linode Object Storage', value: 'linode-objectstorage' }, { name: 'Minio', value: 'minio' }, { name: 'NFS Mount', value: 'nfs' }, { name: 'OVH Object Storage', value: 'ovh-objectstorage' }, { name: 'S3 API Compatible (v4)', value: 's3-v4-compat' }, { name: 'Scaleway Object Storage', value: 'scaleway-objectstorage' }, { name: 'SSHFS Mount', value: 'sshfs' }, { name: 'Wasabi', value: 'wasabi' }, { 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: '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 = [ { name: 'Tarball (zipped)', value: 'tgz' }, { name: 'rsync', value: 'rsync' } ]; $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.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 === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage'; }; $scope.mountlike = function (provider) { return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4'; }; // 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 = { backupId: backup.id, 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)); }; $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, format: 'tgz', password: '', passwordRepeat: '', advancedVisible: false, memoryTicks: [], memoryLimit: 400 * 1024 * 1024, uploadPartSizeTicks: [], uploadPartSize: 50 * 1024 * 1024, copyConcurrency: '', downloadConcurrency: '', syncConcurrency: '', // sort of similar to upload mountOptions: { host: '', remoteDir: '', username: '', password: '', diskPath: '', 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.memoryLimit = 400 * 1024 * 1024; // 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.mount = { host: '', remoteDir: '', username: '', password: '', diskPath: '', 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.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.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 = [ 400 * 1024 * 1024 ]; for (var i = 512; 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 || '', 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; // 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 === '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.mountPoint = $scope.configureBackup.mountPoint; 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; } 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.mountOptions.diskPath = $scope.configureBackup.diskPath; } } else if (backupConfig.provider === 'filesystem' || backupConfig.provider === 'mountpoint') { 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(); checkBackupConfig(); }); } }; 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; }); } function checkBackupConfig() { Client.checkBackupConfig(function (error, check) { if (error) return console.error(error); // { ok: bool, message: checkMessageTranslationKey } $scope.backupCheck = check; }); } Client.onReady(function () { Client.memory(function (error, memory) { if (error) console.error(error); $scope.memory = memory; fetchBackups(); getBackupConfig(); checkBackupConfig(); $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(); }]);