Files
cloudron-box/dashboard/public/views/backups.js

1136 lines
49 KiB
JavaScript
Raw Normal View History

2018-06-07 14:22:48 +02:00
'use strict';
2022-12-24 14:48:46 +01:00
/* global $, angular, TASK_TYPES, SECRET_PLACEHOLDER, STORAGE_PROVIDERS, BACKUP_FORMATS, APP_TYPES */
2024-09-25 12:21:42 +02:00
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR , REGIONS_CONTABO, REGIONS_HETZNER */
2024-12-10 11:53:29 +01:00
/* global async, ERROR */
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.SECRET_PLACEHOLDER = SECRET_PLACEHOLDER;
$scope.MIN_MEMORY_LIMIT = 1024 * 1024 * 1024; // 1 GB
$scope.MAX_MEMORY_LIMIT = $scope.MIN_MEMORY_LIMIT; // set later
2018-06-07 14:22:48 +02:00
$scope.config = Client.getConfig();
$scope.user = Client.getUserInfo();
2020-09-10 00:07:12 -07:00
$scope.memory = null; // { memory, swap }
$scope.mountStatus = null; // { state, message }
$scope.manualBackupApps = [];
2024-09-08 16:35:08 +02:00
$scope.currentTimeZone = '';
2018-06-07 14:22:48 +02:00
$scope.backupConfig = {};
$scope.backups = [];
$scope.backupTasks = [];
$scope.cleanupTasks = [];
2018-06-07 14:22:48 +02:00
2024-12-10 11:53:29 +01:00
$scope.domains = [];
$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;
2023-08-25 07:59:40 +05:30
$scope.contaboRegions = REGIONS_CONTABO;
2024-09-25 12:21:42 +02:00
$scope.hetznerRegions = REGIONS_HETZNER;
2022-12-24 11:10:04 +01:00
$scope.storageProviders = STORAGE_PROVIDERS.concat([
2020-06-05 12:48:27 +02:00
{ name: 'No-op (Only for testing)', value: 'noop' }
]);
2018-06-07 14:22:48 +02:00
$scope.backupRetentions = [
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 }},
2022-05-26 08:38:18 -07:00
{ name: '3 months', value: { keepWithinSecs: 3 * 30 * 24 * 60 * 60 }},
2020-05-21 14:09:06 -07:00
{ name: '2 daily, 4 weekly', value: { keepDaily: 2, keepWeekly: 4 }},
2020-05-14 21:36:22 -07:00
{ 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
];
2020-07-28 21:48:24 -07:00
// values correspond to cron days
2020-07-29 15:09:06 -07:00
$scope.cronDays = [
2020-07-28 21:48:24 -07:00
{ 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;
2018-06-07 14:22:48 +02:00
$scope.prettyProviderName = function (provider) {
switch (provider) {
case 'caas': return 'Managed Cloudron';
default: return provider;
}
};
2020-07-28 21:48:24 -07:00
$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 {
2020-07-29 15:09:06 -07:00
prettyDay = days.map(function (day) { return $scope.cronDays[parseInt(day, 10)].name.substr(0, 3); }).join(',');
2020-07-28 21:48:24 -07:00
}
2020-07-29 15:09:06 -07:00
var prettyHour = hours.map(function (hour) { return $scope.cronHours[parseInt(hour, 10)].name; }).join(',');
2020-07-28 21:48:24 -07:00
return prettyDay + ' at ' + prettyHour;
};
$scope.prettyBackupRetention = function (retention) {
var tmp = $scope.backupRetentions.find(function (p) { return angular.equals(p.value, retention); });
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);
});
}
};
2018-06-07 14:22:48 +02:00
$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
init: function () {
Client.getLatestTaskByType(TASK_TYPES.TASK_BACKUP, function (error, task) {
2018-12-08 20:21:11 -08:00
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
getBackupTasks();
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;
}
getBackupTasks();
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
},
2020-05-14 22:42:41 -07:00
stopTask: 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;
getBackupTasks();
2018-11-17 20:04:41 -08:00
return;
}
});
2018-06-07 14:22:48 +02:00
}
};
$scope.cleanupBackups = {
busy: false,
taskId: 0,
init: function () {
Client.getLatestTaskByType(TASK_TYPES.TASK_CLEAN_BACKUPS, function (error, task) {
if (error) return console.error(error);
if (!task) return;
$scope.cleanupBackups.taskId = task.id;
$scope.cleanupBackups.updateStatus();
getCleanupTasks();
});
},
updateStatus: function () {
Client.getTask($scope.cleanupBackups.taskId, function (error, data) {
if (error) return window.setTimeout($scope.cleanupBackups.updateStatus, 5000);
if (!data.active) {
$scope.cleanupBackups.busy = false;
getCleanupTasks();
fetchBackups();
return;
}
$scope.cleanupBackups.busy = true;
$scope.cleanupBackups.message = data.message;
window.setTimeout($scope.cleanupBackups.updateStatus, 3000);
});
},
ask: function () {
$('#cleanupBackupsModal').modal('show');
},
start: function () {
$scope.cleanupBackups.busy = true;
$('#cleanupBackupsModal').modal('hide');
Client.cleanupBackups(function (error, taskId) {
if (error) console.error(error);
$scope.cleanupBackups.taskId = taskId;
$scope.cleanupBackups.updateStatus();
getCleanupTasks();
});
}
};
$scope.archiveList = {
2024-12-10 11:53:29 +01:00
ready: false,
archives: [],
fetch: function () {
Client.listArchives(function (error, archives) {
if (error) Client.error(error);
$scope.archiveList.archives = archives;
$scope.archiveList.ready = true;
2024-12-10 13:25:44 +01:00
// ensure we use the full api oprigin
$scope.archiveList.archives.forEach(a => {
2024-12-10 13:25:44 +01:00
a.iconUrl = window.cloudronApiOrigin + a.iconUrl;
});
2024-12-10 11:53:29 +01:00
});
},
2024-12-10 14:26:07 +01:00
};
2024-12-10 11:53:29 +01:00
2024-12-10 14:26:07 +01:00
$scope.archiveDelete = {
busy: false,
error: {},
archive: null,
title: '',
fqdn: '',
2024-12-10 14:26:07 +01:00
ask: function (archive) {
$scope.archiveDelete.busy = false;
$scope.archiveDelete.error = {};
$scope.archiveDelete.archive = archive;
$scope.archiveDelete.title = archive.manifest.title;
$scope.archiveDelete.fqdn = archive.appConfig?.fqdn || '-';
2024-12-10 14:26:07 +01:00
$('#archiveDeleteModal').modal('show');
},
submit: function () {
$scope.archiveDelete.busy = true;
$scope.archiveDelete.error = {};
Client.deleteArchive($scope.archiveDelete.archive.id, function (error) {
$scope.archiveDelete.busy = false;
if (error) return console.error('Unable to delete archive.', error.statusCode, error.message);
$scope.archiveList.fetch();
2024-12-10 14:26:07 +01:00
$('#archiveDeleteModal').modal('hide');
2024-12-10 11:53:29 +01:00
});
2024-12-10 14:26:07 +01:00
}
2024-12-10 11:53:29 +01:00
};
// keep in sync with app.js
$scope.archiveRestore = {
2024-12-10 11:53:29 +01:00
busy: false,
error: {},
archive: null,
manifest: null,
appStoreId: '',
fqdn: '',
2024-12-10 11:53:29 +01:00
subdomain: '',
domain: null,
secondaryDomains: {},
needsOverwrite: false,
overwriteDns: false,
ports: {},
portsEnabled: {},
portInfo: {},
accessRestriction: { users: [], groups: [] },
2024-12-10 11:53:29 +01:00
init: function () {
Client.getDomains(function (error, domains) {
if (error) return console.error('Unable to get domain listing.', error);
$scope.domains = domains;
});
},
show: function (archive) {
$scope.archiveRestore.error = {};
$scope.archiveRestore.archive = archive;
$scope.archiveRestore.manifest = archive.manifest;
2024-12-10 11:53:29 +01:00
const app = archive.appConfig || {
subdomain: '',
domain: $scope.domains[0].domain,
secondaryDomains: [],
portBindings: {}
}; // pre-8.2 backups do not have appConfig
$scope.archiveRestore.fqdn = archive.appConfig?.fqdn || '-';
$scope.archiveRestore.subdomain = app.subdomain;
$scope.archiveRestore.domain = $scope.domains.find(function (d) { return app.domain === d.domain; }); // try to pre-select the app's domain
$scope.archiveRestore.needsOverwrite = false;
$scope.archiveRestore.overwriteDns = false;
2024-12-10 11:53:29 +01:00
$scope.archiveRestore.secondaryDomains = {};
2024-12-10 11:53:29 +01:00
var httpPorts = archive.manifest.httpPorts || {};
2024-12-10 11:53:29 +01:00
for (var env2 in httpPorts) {
$scope.archiveRestore.secondaryDomains[env2] = {
2024-12-10 11:53:29 +01:00
subdomain: httpPorts[env2].defaultValue || '',
domain: $scope.archiveRestore.domain
2024-12-10 11:53:29 +01:00
};
}
// now fill secondaryDomains with real values, if it exists
app.secondaryDomains.forEach(function (sd) {
$scope.archiveRestore.secondaryDomains[sd.environmentVariable] = {
subdomain: sd.subdomain,
domain: $scope.domains.find(function (d) { return sd.domain === d.domain; })
};
});
2024-12-10 11:53:29 +01:00
$scope.archiveRestore.portInfo = angular.extend({}, archive.manifest.tcpPorts, archive.manifest.udpPorts); // Portbinding map only for information
2024-12-10 11:53:29 +01:00
// set default ports
for (var env in $scope.archiveRestore.portInfo) {
if (app.portBindings[env]) { // was enabled in the app
$scope.archiveRestore.ports[env] = app.portBindings[env].hostPort;
$scope.archiveRestore.portsEnabled[env] = true;
} else {
$scope.archiveRestore.ports[env] = $scope.archiveRestore.portInfo[env].defaultValue || 0;
$scope.archiveRestore.portsEnabled[env] = false;
}
2024-12-10 11:53:29 +01:00
}
$('#restoreArchiveModal').modal('show');
2024-12-10 11:53:29 +01:00
},
submit: function () {
$scope.archiveRestore.busy = true;
2024-12-10 11:53:29 +01:00
var secondaryDomains = {};
for (var env2 in $scope.archiveRestore.secondaryDomains) {
2024-12-10 11:53:29 +01:00
secondaryDomains[env2] = {
subdomain: $scope.archiveRestore.secondaryDomains[env2].subdomain,
domain: $scope.archiveRestore.secondaryDomains[env2].domain.domain
2024-12-10 11:53:29 +01:00
};
}
// only use enabled ports
var finalPorts = {};
for (var env in $scope.archiveRestore.ports) {
if ($scope.archiveRestore.portsEnabled[env]) {
finalPorts[env] = $scope.archiveRestore.ports[env];
2024-12-10 11:53:29 +01:00
}
}
var data = {
subdomain: $scope.archiveRestore.subdomain,
domain: $scope.archiveRestore.domain.domain,
2024-12-10 11:53:29 +01:00
secondaryDomains: secondaryDomains,
ports: finalPorts,
overwriteDns: $scope.archiveRestore.overwriteDns,
2024-12-10 11:53:29 +01:00
};
var allDomains = [{ domain: data.domain, subdomain: data.subdomain }].concat(Object.keys(secondaryDomains).map(function (k) {
return {
domain: secondaryDomains[k].domain,
subdomain: secondaryDomains[k].subdomain
};
}));
async.eachSeries(allDomains, function (domain, callback) {
if ($scope.archiveRestore.overwriteDns) return callback();
2024-12-10 11:53:29 +01:00
Client.checkDNSRecords(domain.domain, domain.subdomain, function (error, result) {
if (error) return callback(error);
var fqdn = domain.subdomain + '.' + domain.domain;
if (result.error) {
if (result.error.reason === ERROR.ACCESS_DENIED) return callback({ type: 'provider', fqdn: fqdn, message: 'DNS credentials for ' + domain.domain + ' are invalid. Update it in Domains & Certs view' });
return callback({ type: 'provider', fqdn: fqdn, message: result.error.message });
}
if (result.needsOverwrite) {
$scope.archiveRestore.needsOverwrite = true;
$scope.archiveRestore.overwriteDns = true;
2024-12-10 11:53:29 +01:00
return callback({ type: 'externally_exists', fqdn: fqdn, message: 'DNS Record already exists. Confirm that the domain is not in use for services external to Cloudron' });
}
callback();
});
}, function (error) {
if (error) {
if (error.type) {
$scope.archiveRestore.error.location = error;
$scope.archiveRestore.busy = false;
2024-12-10 11:53:29 +01:00
} else {
Client.error(error);
}
$scope.archiveRestore.error.location = error;
$scope.archiveRestore.busy = false;
2024-12-10 11:53:29 +01:00
return;
}
Client.unarchiveApp($scope.archiveRestore.archive.id, data, function (error/*, newApp */) {
$scope.archiveRestore.busy = false;
2024-12-10 11:53:29 +01:00
if (error) {
var errorMessage = error.message.toLowerCase();
if (errorMessage.indexOf('port') !== -1) {
$scope.archiveRestore.error.port = error.message;
2024-12-10 11:53:29 +01:00
} else if (error.message.indexOf('location') !== -1 || error.message.indexOf('subdomain') !== -1) {
// TODO extract fqdn from error message, currently we just set it always to the main location
$scope.archiveRestore.error.location = { type: 'internally_exists', fqdn: data.subdomain + '.' + data.domain, message: error.message };
2024-12-10 11:53:29 +01:00
$('#cloneLocationInput').focus();
} else {
Client.error(error);
}
return;
}
$('#restoreArchiveModal').modal('hide');
2024-12-10 11:53:29 +01:00
$location.path('/apps');
});
});
}
2020-05-14 22:42:41 -07:00
};
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'
2024-09-25 12:21:42 +02:00
|| provider === 'exoscale-sos' || provider === 'digitalocean-spaces' || provider === 'hetzner-objectstorage'
2022-09-27 19:40:58 +02:00
|| provider === 'scaleway-objectstorage' || provider === 'wasabi' || provider === 'backblaze-b2' || provider === 'cloudflare-r2'
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage'
2023-08-25 07:59:40 +05:30
|| provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2'
|| provider === 'contabo-objectstorage';
2018-06-07 14:22:48 +02:00
};
2020-06-22 15:51:18 +02:00
$scope.mountlike = function (provider) {
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs' || provider === 'disk';
2020-06-22 15:51:18 +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);
}
2024-12-11 10:54:51 +01:00
$scope.downloadConfig = function (backup, isArchive) { // can also be a archive object
// 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];
});
2024-12-11 10:54:51 +01:00
let filename;
if (isArchive) {
filename = `${backup.appConfig.fqdn}-archive-config-${(new Date(backup.creationTime)).toISOString().split('T')[0]}.json`;
} else {
filename = `${$scope.config.adminFqdn}-backup-config-${(new Date(backup.creationTime)).toISOString().split('T')[0]}.json`;
}
download(filename, JSON.stringify(tmp, null, 4));
2020-04-20 18:21:35 +02:00
};
$scope.editBackup = {
busy: false,
error: null,
backup: null,
label: '',
2022-04-07 15:31:41 +02:00
persist: false,
show: function (backup) {
$scope.editBackup.backup = backup;
$scope.editBackup.label = backup.label;
2022-04-07 15:31:41 +02:00
$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;
2022-04-07 15:31:41 +02:00
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.backupPolicy = {
busy: false,
error: {},
currentPolicy: null,
retention: null,
2020-07-28 21:48:24 -07:00
days: [],
hours: [],
init: function () {
Client.getBackupPolicy(function (error, policy) {
if (error) Client.error(error);
$scope.backupPolicy.currentPolicy = policy;
});
},
show: function () {
$scope.backupPolicy.error = {};
$scope.backupPolicy.busy = false;
var selectedRetention = $scope.backupRetentions.find(function (x) { return angular.equals(x.value, $scope.backupPolicy.currentPolicy.retention); });
if (!selectedRetention) selectedRetention = $scope.backupRetentions[0];
$scope.backupPolicy.retention = selectedRetention.value;
2020-07-28 21:48:24 -07:00
var tmp = $scope.backupPolicy.currentPolicy.schedule.split(' ');
2020-07-28 21:48:24 -07:00
var hours = tmp[2].split(','), days = tmp[5].split(',');
if (days[0] === '*') {
$scope.backupPolicy.days = angular.copy($scope.cronDays, []);
} else {
$scope.backupPolicy.days = days.map(function (day) { return $scope.cronDays[parseInt(day, 10)]; });
}
$scope.backupPolicy.hours = hours.map(function (hour) { return $scope.cronHours[parseInt(hour, 10)]; });
$('#backupPolicyModal').modal('show');
},
valid: function () {
return $scope.backupPolicy.days.length && $scope.backupPolicy.hours.length;
},
submit: function () {
if (!$scope.backupPolicy.days.length) return;
if (!$scope.backupPolicy.hours.length) return;
$scope.backupPolicy.error = {};
$scope.backupPolicy.busy = true;
2020-07-28 21:48:24 -07:00
var daysPattern;
if ($scope.backupPolicy.days.length === 7) daysPattern = '*';
else daysPattern = $scope.backupPolicy.days.map(function (d) { return d.value; });
2020-07-28 21:48:24 -07:00
var hoursPattern;
if ($scope.backupPolicy.hours.length === 24) hoursPattern = '*';
else hoursPattern = $scope.backupPolicy.hours.map(function (d) { return d.value; });
2020-07-28 21:48:24 -07:00
var policy = {
retention: $scope.backupPolicy.retention,
schedule: '00 00 ' + hoursPattern + ' * * ' + daysPattern
};
Client.setBackupPolicy(policy, function (error) {
$scope.backupPolicy.busy = false;
if (error) {
if (error.statusCode === 424) {
$scope.backupPolicy.error.generic = error.message;
} else if (error.statusCode === 400) {
$scope.backupPolicy.error.generic = error.message;
} else {
console.error('Unable to change schedule or retention.', error);
}
return;
}
$('#backupPolicyModal').modal('hide');
$scope.backupPolicy.init();
});
}
};
$scope.$watch('configureBackup.disk', function (newValue) {
if (!newValue) return;
$scope.configureBackup.mountOptions.diskPath = '/dev/disk/by-uuid/' + newValue.uuid;
});
2018-06-07 14:22:48 +02:00
$scope.configureBackup = {
busy: false,
error: {},
provider: '',
bucket: '',
prefix: '',
accessKeyId: '',
secretAccessKey: '',
gcsKey: { keyFileName: '', content: '' },
region: '',
endpoint: '',
backupFolder: '',
2020-06-05 12:47:33 +02:00
mountPoint: '',
2018-06-07 14:22:48 +02:00
acceptSelfSignedCerts: false,
useHardlinks: true,
chown: true,
2018-06-07 14:22:48 +02:00
format: 'tgz',
2020-05-12 10:54:15 -07:00
password: '',
passwordRepeat: '',
encryptedFilenames: true,
advancedVisible: false,
memoryTicks: [],
memoryLimit: $scope.MIN_MEMORY_LIMIT,
2020-08-19 14:39:41 -07:00
uploadPartSize: 50 * 1024 * 1024,
copyConcurrency: '',
downloadConcurrency: '',
syncConcurrency: '', // sort of similar to upload
2018-06-07 14:22:48 +02:00
noHardlinks: false,
preserveAttributes: true,
blockDevices: [],
disk: null,
mountOptions: {
host: '',
remoteDir: '',
username: '',
password: '',
diskPath: '',
2024-04-15 21:54:40 +02:00
seal: true,
user: '',
port: 22,
privateKey: ''
},
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 = '';
2020-06-05 12:47:33 +02:00
$scope.configureBackup.mountPoint = '';
2018-06-07 14:22:48 +02:00
$scope.configureBackup.acceptSelfSignedCerts = false;
$scope.configureBackup.useHardlinks = true;
$scope.configureBackup.chown = true;
$scope.configureBackup.memoryLimit = $scope.MIN_MEMORY_LIMIT;
2020-08-19 14:39:41 -07:00
// 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;
2020-08-11 16:51:02 -07:00
$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.disk = null;
2024-04-15 21:54:40 +02:00
$scope.configureBackup.mountOptions = { host: '', remoteDir: '', username: '', password: '', diskPath: '', seal: true, user: '', port: 22, privateKey: '' };
2018-06-07 14:22:48 +02:00
},
show: function () {
$scope.configureBackup.error = {};
$scope.configureBackup.busy = false;
2020-08-11 17:03:49 -07:00
$scope.configureBackup.advancedVisible = false;
2018-06-07 14:22:48 +02:00
$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;
2018-06-07 14:22:48 +02:00
$scope.configureBackup.backupFolder = $scope.backupConfig.backupFolder;
2020-06-05 12:47:33 +02:00
$scope.configureBackup.mountPoint = $scope.backupConfig.mountPoint;
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.preserveAttributes = !!$scope.backupConfig.preserveAttributes;
$scope.configureBackup.chown = $scope.backupConfig.chown;
2018-06-07 14:22:48 +02:00
const limits = $scope.backupConfig.limits || {};
$scope.configureBackup.memoryLimit = limits.memoryLimit ? Math.max(limits.memoryLimit, $scope.MIN_MEMORY_LIMIT) : $scope.MIN_MEMORY_LIMIT;
$scope.configureBackup.uploadPartSize = limits.uploadPartSize || ($scope.configureBackup.provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024);
$scope.configureBackup.downloadConcurrency = limits.downloadConcurrency || ($scope.backupConfig.provider === 's3' ? 30 : 10);
$scope.configureBackup.syncConcurrency = limits.syncConcurrency || ($scope.backupConfig.provider === 's3' ? 20 : 10);
$scope.configureBackup.copyConcurrency = limits.copyConcurrency || ($scope.backupConfig.provider === 's3' ? 500 : 10);
var mountOptions = $scope.backupConfig.mountOptions || {};
2021-05-27 13:19:13 -07:00
$scope.configureBackup.mountOptions = {
host: mountOptions.host || '',
remoteDir: mountOptions.remoteDir || '',
username: mountOptions.username || '',
password: mountOptions.password || '',
diskPath: mountOptions.diskPath || '',
2022-01-10 14:28:25 +01:00
seal: mountOptions.seal,
user: mountOptions.user || '',
port: mountOptions.port || 22,
privateKey: mountOptions.privateKey || ''
};
2020-08-19 14:39:41 -07:00
Client.getBlockDevices(function (error, result) {
if (error) return console.error('Failed to list blockdevices:', error);
// only offer non /, /boot or /home disks
result = result.filter(function (d) { return d.mountpoint !== '/' && d.mountpoint !== '/home' && d.mountpoint !== '/boot'; });
// only offer xfs and ext4 disks
result = result.filter(function (d) { return d.type === 'xfs' || d.type === 'ext4'; });
// amend label for UI
result.forEach(function (d) {
d.label = d.path;
// pre-select current if set
if (d.path === $scope.configureBackup.mountOptions.diskPath || ('/dev/disk/by-uuid/' + d.uuid) === $scope.configureBackup.mountOptions.diskPath) {
$scope.configureBackup.disk = d;
}
});
$scope.configureBackup.blockDevices = result;
$('#configureBackupModal').modal('show');
});
2018-06-07 14:22:48 +02:00
},
submit: function () {
$scope.configureBackup.error = {};
$scope.configureBackup.busy = true;
var backupConfig = {
provider: $scope.configureBackup.provider,
format: $scope.configureBackup.format,
// required for api call to provide all fields
2020-07-28 21:48:24 -07:00
schedulePattern: $scope.backupConfig.schedulePattern,
retentionPolicy: $scope.backupConfig.retentionPolicy,
limits: {
memoryLimit: parseInt($scope.configureBackup.memoryLimit),
},
2018-06-07 14:22:48 +02:00
};
if ($scope.configureBackup.password) {
backupConfig.password = $scope.configureBackup.password;
backupConfig.encryptedFilenames = $scope.configureBackup.encryptedFilenames; // ignored with tgz format
}
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 = $scope.configureBackup.region || 'us-east-1';
2018-06-07 14:22:48 +02:00
backupConfig.acceptSelfSignedCerts = $scope.configureBackup.acceptSelfSignedCerts;
backupConfig.s3ForcePathStyle = true; // might want to expose this in the UI
2018-06-07 14:22:48 +02:00
} 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') {
2020-05-19 14:52:40 +02:00
backupConfig.region = $scope.wasabiRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
2019-07-22 16:34:16 -07:00
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';
} else if (backupConfig.provider === 'ionos-objectstorage') {
backupConfig.region = $scope.ionosRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
2021-06-16 22:35:46 -07:00
} else if (backupConfig.provider === 'vultr-objectstorage') {
backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
2023-08-25 07:59:40 +05:30
} else if (backupConfig.provider === 'contabo-objectstorage') {
backupConfig.region = $scope.contaboRegions.find(function (x) { return x.value === $scope.configureBackup.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
backupConfig.s3ForcePathStyle = true; // https://docs.contabo.com/docs/products/Object-Storage/technical-description (no virtual buckets)
} 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';
2018-06-07 14:22:48 +02:00
} else if (backupConfig.provider === 'digitalocean-spaces') {
backupConfig.region = 'us-east-1';
2024-09-25 12:21:42 +02:00
} else if (backupConfig.provider === 'hetzner-objectstorage') {
backupConfig.region = 'us-east-1';
backupConfig.signatureVersion = 'v4';
2018-06-07 14:22:48 +02:00
}
backupConfig.limits.uploadPartSize = parseInt($scope.configureBackup.uploadPartSize);
2018-06-07 14:22:48 +02:00
} 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)) {
2020-06-05 12:47:33 +02:00
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;
2022-01-10 14:28:25 +01:00
backupConfig.mountOptions.seal = $scope.configureBackup.mountOptions.seal;
backupConfig.preserveAttributes = $scope.configureBackup.preserveAttributes;
} 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;
backupConfig.preserveAttributes = true;
}
} else if (backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs' || backupConfig.provider === 'disk') {
2021-06-18 23:03:18 -07:00
backupConfig.mountOptions.diskPath = $scope.configureBackup.mountOptions.diskPath;
backupConfig.preserveAttributes = true;
2021-06-21 22:08:19 -07:00
} else if (backupConfig.provider === 'mountpoint') {
backupConfig.mountPoint = $scope.configureBackup.mountPoint;
backupConfig.chown = $scope.configureBackup.chown;
backupConfig.preserveAttributes = $scope.configureBackup.preserveAttributes;
}
2021-06-21 22:08:19 -07:00
} else if (backupConfig.provider === 'filesystem') {
2018-06-07 14:22:48 +02:00
backupConfig.backupFolder = $scope.configureBackup.backupFolder;
backupConfig.noHardlinks = !$scope.configureBackup.useHardlinks;
backupConfig.preserveAttributes = true;
2018-06-07 14:22:48 +02:00
}
if (backupConfig.format === 'rsync') {
backupConfig.limits.downloadConcurrency = parseInt($scope.configureBackup.downloadConcurrency);
backupConfig.limits.syncConcurrency = parseInt($scope.configureBackup.syncConcurrency);
backupConfig.limits.copyConcurrency = parseInt($scope.configureBackup.copyConcurrency);
}
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 {
$scope.configureBackup.error.generic = error.message;
2018-06-07 14:22:48 +02:00
}
return;
}
// $scope.configureBackup.reset();
$('#configureBackupModal').modal('hide');
getBackupConfig();
});
}
};
function fetchBackups() {
Client.getBackups(function (error, backups) {
if (error) return console.error(error);
$scope.backups = backups;
2020-05-16 09:46:57 -07:00
// add contents property
var appsById = {}, appsByFqdn = {};
Client.getInstalledApps().forEach(function (app) {
appsById[app.id] = app;
appsByFqdn[app.fqdn] = app;
});
2020-05-16 09:46:57 -07:00
$scope.backups.forEach(function (backup) {
backup.contents = []; // { id, label, fqdn }
2020-05-16 09:46:57 -07:00
backup.dependsOn.forEach(function (appBackupId) {
2024-09-08 16:35:08 +02:00
const match = appBackupId.match(/app_(.*?)_.*/); // *? means non-greedy
if (!match) return; // for example, 'mail'
const app = appsById[match[1]];
if (app) {
backup.contents.push({
id: app.id,
label: app.label,
fqdn: app.fqdn
});
} else {
backup.contents.push({
id: match[1],
label: null,
fqdn: null
});
}
2020-05-16 09:46:57 -07:00
});
});
2018-06-07 14:22:48 +02:00
});
}
function getBackupConfig() {
Client.getBackupConfig(function (error, backupConfig) {
if (error) return console.error(error);
$scope.backupConfig = backupConfig;
$scope.mountStatus = null;
if (!$scope.mountlike($scope.backupConfig.provider)) return;
Client.getBackupMountStatus(function (error, mountStatus) {
if (error) return console.error(error);
$scope.mountStatus = mountStatus;
});
2018-06-07 14:22:48 +02:00
});
}
function getBackupTasks() {
Client.getTasksByType(TASK_TYPES.TASK_BACKUP, function (error, tasks) {
if (error) return console.error(error);
if (!tasks.length) return;
$scope.backupTasks = tasks.slice(0, 10);
});
}
function getCleanupTasks() {
Client.getTasksByType(TASK_TYPES.TASK_CLEAN_BACKUPS, function (error, tasks) {
if (error) return console.error(error);
if (!tasks.length) return;
$scope.cleanupTasks = tasks.slice(0, 10);
});
}
2018-06-07 14:22:48 +02:00
Client.onReady(function () {
2020-09-10 00:07:12 -07:00
Client.memory(function (error, memory) {
if (error) console.error(error);
2018-06-07 14:22:48 +02:00
2020-09-10 00:07:12 -07:00
$scope.memory = memory;
2024-04-10 12:48:07 +02:00
var nearestGb = Math.ceil($scope.memory.memory / (1024*1024*1024)) * 1024 * 1024 * 1024;
$scope.MAX_MEMORY_LIMIT = nearestGb;
2020-09-10 00:07:12 -07:00
fetchBackups();
getBackupConfig();
$scope.archiveList.fetch();
2024-12-10 11:53:29 +01:00
2022-09-28 12:13:32 +02:00
$scope.manualBackupApps = Client.getInstalledApps().filter(function (app) { return app.type !== APP_TYPES.LINK && !app.enableBackup; });
2020-09-10 00:07:12 -07:00
// show backup status
$scope.createBackup.init();
$scope.cleanupBackups.init();
$scope.backupPolicy.init();
$scope.archiveRestore.init();
getBackupTasks();
getCleanupTasks();
2020-09-10 00:07:12 -07:00
});
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', 'editBackupModal'].forEach(function (id) {
2018-06-07 14:22:48 +02:00
$('#' + id).on('shown.bs.modal', function () {
$(this).find('[autofocus]:first').focus();
2018-06-07 14:22:48 +02:00
});
});
$('.modal-backdrop').remove();
}]);