diff --git a/migrations/20200728232132-settings-set-backups-schedulePattern.js b/migrations/20200728232132-settings-set-backups-schedulePattern.js new file mode 100644 index 000000000..fb1f1944f --- /dev/null +++ b/migrations/20200728232132-settings-set-backups-schedulePattern.js @@ -0,0 +1,29 @@ +'use strict'; + +exports.up = function(db, callback) { + db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) { + if (error || results.length === 0) return callback(error); + + var backupConfig = JSON.parse(results[0].value); + if (backupConfig.intervalSecs === 6 * 60 * 60) { // every 6 hours + backupConfig.schedulePattern = '00 00 5,11,17,23 * * *'; + } else if (backupConfig.intervalSecs === 12 * 60 * 60) { // every 12 hours + backupConfig.schedulePattern = '00 00 5,17 * * *'; + } else if (backupConfig.intervalSecs === 24 * 60 * 60) { // every day + backupConfig.schedulePattern = '00 00 23 * * *'; + } else if (backupConfig.intervalSecs === 3 * 24 * 60 * 60) { // every 3 days (based on day) + backupConfig.schedulePattern = '00 00 23 * * 1,3,5'; + } else if (backupConfig.intervalSecs === 7 * 24 * 60 * 60) { // every week (saturday) + backupConfig.schedulePattern = '00 00 23 * * 6'; + } else { // default to everyday + backupConfig.schedulePattern = '00 00 23 * * *'; + } + + delete backupConfig.intervalSecs; + db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback); + }); +}; + +exports.down = function(db, callback) { + callback(); +}; diff --git a/src/backups.js b/src/backups.js index 9e25734b7..fd9d03191 100644 --- a/src/backups.js +++ b/src/backups.js @@ -9,7 +9,6 @@ exports = module.exports = { get: get, startBackupTask: startBackupTask, - ensureBackup: ensureBackup, restore: restore, @@ -57,6 +56,7 @@ var addons = require('./addons.js'), BoxError = require('./boxerror.js'), collectd = require('./collectd.js'), constants = require('./constants.js'), + CronJob = require('cron').CronJob, crypto = require('crypto'), database = require('./database.js'), DataLayout = require('./datalayout.js'), @@ -143,8 +143,8 @@ function testConfig(backupConfig, callback) { if (backupConfig.format !== 'tgz' && backupConfig.format !== 'rsync') return callback(new BoxError(BoxError.BAD_FIELD, 'unknown format', { field: 'format' })); - // remember to adjust the cron ensureBackup task interval accordingly - if (backupConfig.intervalSecs < 6 * 60 * 60) return callback(new BoxError(BoxError.BAD_FIELD, 'Interval must be atleast 6 hours', { field: 'intervalSecs' })); + const job = safe.safeCall(function () { return new CronJob(backupConfig.schedulePattern); }); + if (!job) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid schedule pattern', { field: 'schedulePattern' })); if ('password' in backupConfig) { if (typeof backupConfig.password !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'password must be a string', { field: 'password' })); @@ -1227,31 +1227,6 @@ function startBackupTask(auditSource, callback) { }); } -function ensureBackup(auditSource, callback) { - assert.strictEqual(typeof auditSource, 'object'); - - debug('ensureBackup: %j', auditSource); - - getByIdentifierAndStatePaged(exports.BACKUP_IDENTIFIER_BOX, exports.BACKUP_STATE_NORMAL, 1, 1, function (error, backups) { - if (error) { - debug('Unable to list backups', error); - return callback(error); - } - - settings.getBackupConfig(function (error, backupConfig) { - if (error) return callback(error); - - if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < (backupConfig.intervalSecs - 3600) * 1000)) { // adjust 1 hour - debug('Previous backup was %j, no need to backup now', backups[0]); - return callback(null); - } - - startBackupTask(auditSource, callback); - }); - }); -} - -// backups must be descending in creationTime function applyBackupRetentionPolicy(backups, policy, referencedBackupIds) { assert(Array.isArray(backups)); assert.strictEqual(typeof policy, 'object'); diff --git a/src/cron.js b/src/cron.js index 87938674f..a3c6d99e5 100644 --- a/src/cron.js +++ b/src/cron.js @@ -4,8 +4,6 @@ // If the patterns overlap all the time, then the task may not ever get a chance to run! // If you change this change dashboard patterns in settings.html const DEFAULT_CLEANUP_BACKUPS_PATTERN = '00 30 1,3,5,23 * * *', - DEFAULT_BOX_ENSURE_BACKUP_PATTERN_LT_6HOURS = '00 45 1,7,13,19 * * *', - DEFAULT_BOX_ENSURE_BACKUP_PATTERN_GT_6HOURS = '00 45 1,3,5,23 * * *', DEFAULT_BOX_AUTOUPDATE_PATTERN = '00 00 1,3,5,23 * * *', DEFAULT_APP_AUTOUPDATE_PATTERN = '00 15 1,3,5,23 * * *'; @@ -179,16 +177,10 @@ function backupConfigChanged(value, tz) { debug(`backupConfigChanged: interval ${value.intervalSecs} (${tz})`); if (gJobs.backup) gJobs.backup.stop(); - let pattern; - if (value.intervalSecs <= 6 * 60 * 60) { - pattern = DEFAULT_BOX_ENSURE_BACKUP_PATTERN_LT_6HOURS; // no option but to backup in the middle of the day - } else { - pattern = DEFAULT_BOX_ENSURE_BACKUP_PATTERN_GT_6HOURS; // avoid middle of the day backups. it's 45 to not overlap auto-updates - } gJobs.backup = new CronJob({ - cronTime: pattern, - onTick: backups.ensureBackup.bind(null, auditSource.CRON, NOOP_CALLBACK), + cronTime: value.schedulePattern, + onTick: backups.startBackupTask.bind(null, auditSource.CRON, NOOP_CALLBACK), start: true, timeZone: tz }); diff --git a/src/routes/settings.js b/src/routes/settings.js index 658bfec36..1c2cbcf5a 100644 --- a/src/routes/settings.js +++ b/src/routes/settings.js @@ -97,7 +97,7 @@ function setBackupConfig(req, res, next) { assert.strictEqual(typeof req.body, 'object'); if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required')); - if (typeof req.body.intervalSecs !== 'number') return next(new HttpError(400, 'intervalSecs is required')); + if (typeof req.body.schedulePattern !== 'string') return next(new HttpError(400, 'schedulePattern is required')); if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string')); if ('syncConcurrency' in req.body) { if (typeof req.body.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer')); diff --git a/src/settings.js b/src/settings.js index b315df61c..2a55e8708 100644 --- a/src/settings.js +++ b/src/settings.js @@ -437,7 +437,7 @@ function setBackupCredentials(credentials, callback) { if (error) return callback(error); // preserve these fields - const extra = _.pick(currentConfig, 'retentionPolicy', 'intervalSecs', 'copyConcurrency', 'syncConcurrency'); + const extra = _.pick(currentConfig, 'retentionPolicy', 'schedulePattern', 'copyConcurrency', 'syncConcurrency'); const backupConfig = _.extend({}, credentials, extra);