diff --git a/dashboard/src/js/client.js b/dashboard/src/js/client.js index 803d0d1c0..e82648611 100644 --- a/dashboard/src/js/client.js +++ b/dashboard/src/js/client.js @@ -1236,7 +1236,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout Client.prototype.getUpdateInfo = function (callback) { if (!this._userInfo.isAtLeastAdmin) return callback(new Error('Not allowed')); - get('/api/v1/cloudron/update', null, function (error, data, status) { + get('/api/v1/updater/updates', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); @@ -1245,7 +1245,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout }; Client.prototype.checkForUpdates = function (callback) { - post('/api/v1/cloudron/check_for_updates', {}, null, function (error, data, status) { + post('/api/v1/updater/check_for_updates', {}, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); @@ -1263,7 +1263,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout }; Client.prototype.setAutoupdatePattern = function (pattern, callback) { - post('/api/v1/settings/autoupdate_pattern', { pattern: pattern }, null, function (error, data, status) { + post('/api/v1/updater/autoupdate_pattern', { pattern: pattern }, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); @@ -1272,7 +1272,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout }; Client.prototype.getAutoupdatePattern = function (callback) { - get('/api/v1/settings/autoupdate_pattern', null, function (error, data, status) { + get('/api/v1/updater/autoupdate_pattern', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); @@ -1992,7 +1992,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout skipBackup: !!options.skipBackup }; - post('/api/v1/cloudron/update', data, null, function (error, data, status) { + post('/api/v1/updater/update', data, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); diff --git a/src/cron.js b/src/cron.js index d741394c2..f591673a0 100644 --- a/src/cron.js +++ b/src/cron.js @@ -12,6 +12,7 @@ exports = module.exports = { stopJobs, handleSettingsChanged, + autoupdatePatternChanged, dynamicDnsChanged, DEFAULT_AUTOUPDATE_PATTERN, @@ -164,7 +165,7 @@ async function startJobs() { const tz = allSettings[settings.TIME_ZONE_KEY]; backupPolicyChanged(allSettings[settings.BACKUP_POLICY_KEY], tz); - autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY], tz); + await autoupdatePatternChanged(await updater.getAutoupdatePattern()); dynamicDnsChanged(await network.getDynamicDns()); } @@ -202,9 +203,11 @@ function backupPolicyChanged(value, tz) { }); } -function autoupdatePatternChanged(pattern, tz) { +async function autoupdatePatternChanged(pattern) { assert.strictEqual(typeof pattern, 'string'); - assert.strictEqual(typeof tz, 'string'); + + const allSettings = await settings.list(); + const tz = allSettings[settings.TIME_ZONE_KEY]; debug(`autoupdatePatternChanged: pattern - ${pattern} (${tz})`); diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index 0dd28019d..2799675bb 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -13,9 +13,6 @@ exports = module.exports = { getDiskUsage, updateDiskUsage, getMemory, - getUpdateInfo, - update, - checkForUpdates, getLogs, getLogStream, updateDashboardDomain, @@ -44,9 +41,7 @@ const assert = require('assert'), system = require('../system.js'), tokens = require('../tokens.js'), translation = require('../translation.js'), - updater = require('../updater.js'), - users = require('../users.js'), - updateChecker = require('../updatechecker.js'); + users = require('../users.js'); async function login(req, res, next) { assert.strictEqual(typeof req.user, 'object'); @@ -195,30 +190,6 @@ async function getMemory(req, res, next) { next(new HttpSuccess(200, result)); } -async function update(req, res, next) { - if ('skipBackup' in req.body && typeof req.body.skipBackup !== 'boolean') return next(new HttpError(400, 'skipBackup must be a boolean')); - - // this only initiates the update, progress can be checked via the progress route - const [error, taskId] = await safe(updater.updateToLatest(req.body, AuditSource.fromRequest(req))); - if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(422, error.message)); - if (error && error.reason === BoxError.BAD_STATE) return next(new HttpError(409, error.message)); - if (error) return next(new HttpError(500, error)); - - next(new HttpSuccess(202, { taskId })); -} - -function getUpdateInfo(req, res, next) { - next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() })); -} - -async function checkForUpdates(req, res, next) { - // it can take a while sometimes to get all the app updates one by one - req.clearTimeout(); - - await updateChecker.checkForUpdates({ automatic: false }); - next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() })); -} - async function getLogs(req, res, next) { assert.strictEqual(typeof req.params.unit, 'string'); diff --git a/src/routes/index.js b/src/routes/index.js index 7fd894735..d1c243af1 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -27,6 +27,7 @@ exports = module.exports = { support: require('./support.js'), tasks: require('./tasks.js'), tokens: require('./tokens.js'), + updater: require('./updater.js'), users: require('./users.js'), volumes: require('./volumes.js'), wellknown: require('./wellknown.js') diff --git a/src/routes/settings.js b/src/routes/settings.js index d122e9ed8..e0106357b 100644 --- a/src/routes/settings.js +++ b/src/routes/settings.js @@ -17,24 +17,6 @@ const assert = require('assert'), safe = require('safetydance'), settings = require('../settings.js'); -async function getAutoupdatePattern(req, res, next) { - const [error, pattern] = await safe(settings.getAutoupdatePattern()); - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(200, { pattern })); -} - -async function setAutoupdatePattern(req, res, next) { - assert.strictEqual(typeof req.body, 'object'); - - if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required')); - - const [error] = await safe(settings.setAutoupdatePattern(req.body.pattern)); - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(200, {})); -} - async function getTimeZone(req, res, next) { const [error, timeZone] = await safe(settings.getTimeZone()); if (error) return next(BoxError.toHttpError(error)); @@ -179,7 +161,6 @@ function get(req, res, next) { case settings.REGISTRY_CONFIG_KEY: return getRegistryConfig(req, res, next); case settings.LANGUAGE_KEY: return getLanguage(req, res, next); - case settings.AUTOUPDATE_PATTERN_KEY: return getAutoupdatePattern(req, res, next); case settings.TIME_ZONE_KEY: return getTimeZone(req, res, next); default: return next(new HttpError(404, 'No such setting')); @@ -194,7 +175,6 @@ function set(req, res, next) { case settings.REGISTRY_CONFIG_KEY: return setRegistryConfig(req, res, next); case settings.LANGUAGE_KEY: return setLanguage(req, res, next); - case settings.AUTOUPDATE_PATTERN_KEY: return setAutoupdatePattern(req, res, next); case settings.TIME_ZONE_KEY: return setTimeZone(req, res, next); default: return next(new HttpError(404, 'No such setting')); diff --git a/src/routes/test/settings-test.js b/src/routes/test/settings-test.js index 4161bde21..0c08ae789 100644 --- a/src/routes/test/settings-test.js +++ b/src/routes/test/settings-test.js @@ -18,58 +18,6 @@ describe('Settings API', function () { before(setup); after(cleanup); - describe('autoupdate_pattern', function () { - it('can get app auto update pattern (default)', async function () { - const response = await superagent.get(`${serverUrl}/api/v1/settings/autoupdate_pattern`) - .query({ access_token: owner.token }); - expect(response.statusCode).to.equal(200); - expect(response.body.pattern).to.be.ok(); - }); - - it('cannot set autoupdate_pattern without pattern', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/settings/autoupdate_pattern`) - .query({ access_token: owner.token }) - .ok(() => true); - expect(response.statusCode).to.equal(400); - }); - - it('can set autoupdate_pattern', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/settings/autoupdate_pattern`) - .query({ access_token: owner.token }) - .send({ pattern: '00 30 11 * * 1-5' }); - expect(response.statusCode).to.equal(200); - }); - - it('can get auto update pattern', async function () { - const response = await superagent.get(`${serverUrl}/api/v1/settings/autoupdate_pattern`) - .query({ access_token: owner.token }); - expect(response.statusCode).to.equal(200); - expect(response.body.pattern).to.be('00 30 11 * * 1-5'); - }); - - it('can set autoupdate_pattern to never', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/settings/autoupdate_pattern`) - .query({ access_token: owner.token }) - .send({ pattern: constants.AUTOUPDATE_PATTERN_NEVER }); - expect(response.statusCode).to.equal(200); - }); - - it('can get auto update pattern', async function () { - const response = await superagent.get(`${serverUrl}/api/v1/settings/autoupdate_pattern`) - .query({ access_token: owner.token }); - expect(response.statusCode).to.equal(200); - expect(response.body.pattern).to.be(constants.AUTOUPDATE_PATTERN_NEVER); - }); - - it('cannot set invalid autoupdate_pattern', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/settings/autoupdate_pattern`) - .query({ access_token: owner.token }) - .send({ pattern: '1 3 x 5 6' }) - .ok(() => true); - expect(response.statusCode).to.equal(400); - }); - }); - describe('time_zone', function () { it('succeeds', async function () { const response = await superagent.get(`${serverUrl}/api/v1/settings/time_zone`) diff --git a/src/routes/test/updater-test.js b/src/routes/test/updater-test.js new file mode 100644 index 000000000..26080a990 --- /dev/null +++ b/src/routes/test/updater-test.js @@ -0,0 +1,70 @@ +'use strict'; + +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +const common = require('./common.js'), + constants = require('../../constants.js'), + expect = require('expect.js'), + superagent = require('superagent'); + +describe('Updater API', function () { + const { setup, cleanup, serverUrl, owner } = common; + + before(setup); + after(cleanup); + + describe('autoupdate_pattern', function () { + it('can get app auto update pattern (default)', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/updater/autoupdate_pattern`) + .query({ access_token: owner.token }); + expect(response.statusCode).to.equal(200); + expect(response.body.pattern).to.be.ok(); + }); + + it('cannot set autoupdate_pattern without pattern', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/updater/autoupdate_pattern`) + .query({ access_token: owner.token }) + .ok(() => true); + expect(response.statusCode).to.equal(400); + }); + + it('can set autoupdate_pattern', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/updater/autoupdate_pattern`) + .query({ access_token: owner.token }) + .send({ pattern: '00 30 11 * * 1-5' }); + expect(response.statusCode).to.equal(200); + }); + + it('can get auto update pattern', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/updater/autoupdate_pattern`) + .query({ access_token: owner.token }); + expect(response.statusCode).to.equal(200); + expect(response.body.pattern).to.be('00 30 11 * * 1-5'); + }); + + it('can set autoupdate_pattern to never', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/updater/autoupdate_pattern`) + .query({ access_token: owner.token }) + .send({ pattern: constants.AUTOUPDATE_PATTERN_NEVER }); + expect(response.statusCode).to.equal(200); + }); + + it('can get auto update pattern', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/updater/autoupdate_pattern`) + .query({ access_token: owner.token }); + expect(response.statusCode).to.equal(200); + expect(response.body.pattern).to.be(constants.AUTOUPDATE_PATTERN_NEVER); + }); + + it('cannot set invalid autoupdate_pattern', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/updater/autoupdate_pattern`) + .query({ access_token: owner.token }) + .send({ pattern: '1 3 x 5 6' }) + .ok(() => true); + expect(response.statusCode).to.equal(400); + }); + }); +}); diff --git a/src/routes/updater.js b/src/routes/updater.js new file mode 100644 index 000000000..85500797d --- /dev/null +++ b/src/routes/updater.js @@ -0,0 +1,61 @@ +'use strict'; + +exports = module.exports = { + getAutoupdatePattern, + setAutoupdatePattern, + + getUpdateInfo, + update, + checkForUpdates +}; + +const assert = require('assert'), + AuditSource = require('../auditsource.js'), + BoxError = require('../boxerror.js'), + HttpError = require('connect-lastmile').HttpError, + HttpSuccess = require('connect-lastmile').HttpSuccess, + safe = require('safetydance'), + updater = require('../updater.js'), + updateChecker = require('../updatechecker.js'); + +async function getAutoupdatePattern(req, res, next) { + const [error, pattern] = await safe(updater.getAutoupdatePattern()); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(200, { pattern })); +} + +async function setAutoupdatePattern(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); + + if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required')); + + const [error] = await safe(updater.setAutoupdatePattern(req.body.pattern)); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(200, {})); +} + +async function update(req, res, next) { + if ('skipBackup' in req.body && typeof req.body.skipBackup !== 'boolean') return next(new HttpError(400, 'skipBackup must be a boolean')); + + // this only initiates the update, progress can be checked via the progress route + const [error, taskId] = await safe(updater.updateToLatest(req.body, AuditSource.fromRequest(req))); + if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(422, error.message)); + if (error && error.reason === BoxError.BAD_STATE) return next(new HttpError(409, error.message)); + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(202, { taskId })); +} + +function getUpdateInfo(req, res, next) { + next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() })); +} + +async function checkForUpdates(req, res, next) { + // it can take a while sometimes to get all the app updates one by one + req.clearTimeout(); + + await updateChecker.checkForUpdates({ automatic: false }); + next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() })); +} diff --git a/src/server.js b/src/server.js index e6e4d1985..7e325bf10 100644 --- a/src/server.js +++ b/src/server.js @@ -108,13 +108,10 @@ async function initializeExpressSync() { router.post('/api/v1/cloudron/setup_account', json, routes.cloudron.setupAccount); // cloudron routes - router.get ('/api/v1/cloudron/update', token, authorizeAdmin, routes.cloudron.getUpdateInfo); - router.post('/api/v1/cloudron/update', json, token, authorizeAdmin, routes.cloudron.update); router.post('/api/v1/cloudron/prepare_dashboard_domain', json, token, authorizeAdmin, routes.cloudron.prepareDashboardDomain); router.post('/api/v1/cloudron/set_dashboard_domain', json, token, authorizeAdmin, routes.cloudron.updateDashboardDomain); router.post('/api/v1/cloudron/renew_certs', json, token, authorizeAdmin, routes.cloudron.renewCerts); router.post('/api/v1/cloudron/sync_dns', json, token, authorizeAdmin, routes.cloudron.syncDnsRecords); - router.post('/api/v1/cloudron/check_for_updates', json, token, authorizeAdmin, routes.cloudron.checkForUpdates); router.get ('/api/v1/cloudron/reboot', token, authorizeAdmin, routes.cloudron.isRebootRequired); router.post('/api/v1/cloudron/reboot', json, token, authorizeAdmin, routes.cloudron.reboot); router.get ('/api/v1/cloudron/graphs', token, authorizeAdmin, routes.cloudron.getSystemGraphs); @@ -128,6 +125,13 @@ async function initializeExpressSync() { router.get ('/api/v1/cloudron/eventlog', token, authorizeAdmin, routes.eventlog.list); router.get ('/api/v1/cloudron/eventlog/:eventId', token, authorizeAdmin, routes.eventlog.get); + // updater + router.get ('/api/v1/updater/updates', token, authorizeAdmin, routes.updater.getUpdateInfo); + router.post('/api/v1/updater/update', json, token, authorizeAdmin, routes.updater.update); + router.post('/api/v1/updater/check_for_updates', json, token, authorizeAdmin, routes.updater.checkForUpdates); + router.get ('/api/v1/updater/autoupdate_pattern', token, authorizeAdmin, routes.updater.getAutoupdatePattern); + router.post('/api/v1/updater/autoupdate_pattern', json, token, authorizeAdmin, routes.updater.setAutoupdatePattern); + // task routes router.get ('/api/v1/tasks', token, authorizeAdmin, routes.tasks.list); router.get ('/api/v1/tasks/:taskId', token, authorizeAdmin, routes.tasks.load, routes.tasks.get); diff --git a/src/settings.js b/src/settings.js index 304cd899b..02a215c07 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1,9 +1,6 @@ 'use strict'; exports = module.exports = { - getAutoupdatePattern, - setAutoupdatePattern, - getTimeZone, setTimeZone, @@ -101,9 +98,7 @@ exports = module.exports = { const assert = require('assert'), backups = require('./backups.js'), BoxError = require('./boxerror.js'), - constants = require('./constants.js'), cron = require('./cron.js'), - CronJob = require('cron').CronJob, database = require('./database.js'), debug = require('debug')('box:settings'), docker = require('./docker.js'), @@ -119,7 +114,6 @@ const SETTINGS_BLOB_FIELDS = [ 'name', 'valueBlob' ].join(','); const gDefaults = (function () { const result = { }; - result[exports.AUTOUPDATE_PATTERN_KEY] = cron.DEFAULT_AUTOUPDATE_PATTERN; result[exports.TIME_ZONE_KEY] = 'UTC'; result[exports.LANGUAGE_KEY] = 'en'; result[exports.BACKUP_CONFIG_KEY] = { @@ -206,24 +200,6 @@ async function clear() { await database.query('DELETE FROM settings'); } -async function setAutoupdatePattern(pattern) { - assert.strictEqual(typeof pattern, 'string'); - - if (pattern !== constants.AUTOUPDATE_PATTERN_NEVER) { // check if pattern is valid - const job = safe.safeCall(function () { return new CronJob(pattern); }); - if (!job) throw new BoxError(BoxError.BAD_FIELD, 'Invalid pattern'); - } - - await set(exports.AUTOUPDATE_PATTERN_KEY, pattern); - notifyChange(exports.AUTOUPDATE_PATTERN_KEY, pattern); -} - -async function getAutoupdatePattern() { - const pattern = await get(exports.AUTOUPDATE_PATTERN_KEY); - if (pattern === null) return gDefaults[exports.AUTOUPDATE_PATTERN_KEY]; - return pattern; -} - async function setTimeZone(tz) { assert.strictEqual(typeof tz, 'string'); diff --git a/src/test/settings-test.js b/src/test/settings-test.js index 8ade7ee53..d668833e6 100644 --- a/src/test/settings-test.js +++ b/src/test/settings-test.js @@ -22,11 +22,6 @@ describe('Settings', function () { expect(tz.length).to.not.be(0); }); - it('can get default autoupdate_pattern', async function () { - const pattern = await settings.getAutoupdatePattern(); - expect(pattern).to.be('00 00 1,3,5,23 * * *'); - }); - it('can get backup config', async function () { const backupConfig = await settings.getBackupConfig(); expect(backupConfig.provider).to.be('filesystem'); @@ -64,6 +59,5 @@ describe('Settings', function () { it('can get all values', async function () { const allSettings = await settings.list(); expect(allSettings[settings.TIME_ZONE_KEY]).to.be.a('string'); - expect(allSettings[settings.AUTOUPDATE_PATTERN_KEY]).to.be.a('string'); }); }); diff --git a/src/test/updatechecker-test.js b/src/test/updatechecker-test.js index 7f607c22e..5c9d9c6e3 100644 --- a/src/test/updatechecker-test.js +++ b/src/test/updatechecker-test.js @@ -12,8 +12,8 @@ const common = require('./common.js'), paths = require('../paths.js'), safe = require('safetydance'), semver = require('semver'), - settings = require('../settings.js'), - updatechecker = require('../updatechecker.js'); + updatechecker = require('../updatechecker.js'), + updater = require('../updater.js'); const UPDATE_VERSION = semver.inc(constants.VERSION, 'major'); @@ -27,7 +27,7 @@ describe('updatechecker', function () { before(async function () { safe.fs.unlinkSync(paths.UPDATE_CHECKER_FILE); - await settings.setAutoupdatePattern(constants.AUTOUPDATE_PATTERN_NEVER); + await updater.setAutoupdatePattern(constants.AUTOUPDATE_PATTERN_NEVER); }); it('no updates', async function () { @@ -76,7 +76,7 @@ describe('updatechecker', function () { before(async function () { safe.fs.unlinkSync(paths.UPDATE_CHECKER_FILE); - await settings.setAutoupdatePattern(constants.AUTOUPDATE_PATTERN_NEVER); + await updater.setAutoupdatePattern(constants.AUTOUPDATE_PATTERN_NEVER); }); it('no updates', async function () { diff --git a/src/test/updater-test.js b/src/test/updater-test.js new file mode 100644 index 000000000..e299447a5 --- /dev/null +++ b/src/test/updater-test.js @@ -0,0 +1,33 @@ +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +'use strict'; + +const BoxError = require('../boxerror.js'), + common = require('./common.js'), + expect = require('expect.js'), + safe = require('safetydance'), + updater = require('../updater.js'); + +describe('updater', function () { + const { setup, cleanup } = common; + + before(setup); + after(cleanup); + + it('can get default autoupdate_pattern', async function () { + const pattern = await updater.getAutoupdatePattern(); + expect(pattern).to.be('00 00 1,3,5,23 * * *'); + }); + + it('cannot set invalid autoupdate_pattern', async function () { + const [error] = await safe(updater.setAutoupdatePattern('02 * 1 *')); + expect(error.reason).to.be(BoxError.BAD_FIELD); + }); + + it('can set default autoupdate_pattern', async function () { + await updater.setAutoupdatePattern('02 * 1-5 * * *'); + }); +}); diff --git a/src/updater.js b/src/updater.js index c80c14729..65b78fe14 100644 --- a/src/updater.js +++ b/src/updater.js @@ -1,6 +1,9 @@ 'use strict'; exports = module.exports = { + setAutoupdatePattern, + getAutoupdatePattern, + updateToLatest, update }; @@ -10,6 +13,8 @@ const apps = require('./apps.js'), BoxError = require('./boxerror.js'), backuptask = require('./backuptask.js'), constants = require('./constants.js'), + cron = require('./cron.js'), + CronJob = require('cron').CronJob, crypto = require('crypto'), debug = require('debug')('box:updater'), df = require('./df.js'), @@ -30,6 +35,23 @@ const apps = require('./apps.js'), const RELEASES_PUBLIC_KEY = path.join(__dirname, 'releases.gpg'); const UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'); +async function setAutoupdatePattern(pattern) { + assert.strictEqual(typeof pattern, 'string'); + + if (pattern !== constants.AUTOUPDATE_PATTERN_NEVER) { // check if pattern is valid + const job = safe.safeCall(function () { return new CronJob(pattern); }); + if (!job) throw new BoxError(BoxError.BAD_FIELD, 'Invalid pattern'); + } + + await settings.set(settings.AUTOUPDATE_PATTERN_KEY, pattern); + await cron.autoupdatePatternChanged(pattern); +} + +async function getAutoupdatePattern() { + const pattern = await settings.get(settings.AUTOUPDATE_PATTERN_KEY); + return pattern || cron.DEFAULT_AUTOUPDATE_PATTERN; +} + async function downloadUrl(url, file) { assert.strictEqual(typeof file, 'string');