diff --git a/dashboard/src/components/SystemUpdate.vue b/dashboard/src/components/SystemUpdate.vue index 431fbd02c..e6b85593e 100644 --- a/dashboard/src/components/SystemUpdate.vue +++ b/dashboard/src/components/SystemUpdate.vue @@ -77,10 +77,10 @@ async function refreshAutoupdatePattern() { } async function refreshInfo() { - const [error, result] = await updaterModel.info(); + const [error, result] = await updaterModel.getBoxUpdate(); if (error) return console.error(error); - pendingUpdate.value = result.box || null; + pendingUpdate.value = result || null; } function onShowConfigure() { @@ -135,7 +135,7 @@ function onShowUpdate() { async function onCheck() { checkingBusy.value = true; - const [error] = await updaterModel.check(); + const [error] = await updaterModel.checkBoxUpdate(); if (error) return console.error(error); await refreshInfo(); diff --git a/dashboard/src/models/UpdaterModel.js b/dashboard/src/models/UpdaterModel.js index 640aaf27c..b7cf781a9 100644 --- a/dashboard/src/models/UpdaterModel.js +++ b/dashboard/src/models/UpdaterModel.js @@ -6,10 +6,10 @@ function create() { const accessToken = localStorage.token; return { - async info() { + async getBoxUpdate() { let error, result; try { - result = await fetcher.get(`${API_ORIGIN}/api/v1/updater/updates`, { access_token: accessToken }); + result = await fetcher.get(`${API_ORIGIN}/api/v1/updater/box_update`, { access_token: accessToken }); } catch (e) { error = e; } @@ -39,10 +39,10 @@ function create() { if (error || result.status !== 200) return [error || result]; return [null]; }, - async check() { + async checkBoxUpdate() { let error, result; try { - result = await fetcher.post(`${API_ORIGIN}/api/v1/updater/check_for_updates`, {}, { access_token: accessToken }); + result = await fetcher.post(`${API_ORIGIN}/api/v1/updater/check_box_update`, {}, { access_token: accessToken }); } catch (e) { error = e; } diff --git a/migrations/20250626130733-apps-add-updateInfoJson.js b/migrations/20250626130733-apps-add-updateInfoJson.js new file mode 100644 index 000000000..981e8b2b7 --- /dev/null +++ b/migrations/20250626130733-apps-add-updateInfoJson.js @@ -0,0 +1,9 @@ +'use strict'; + +exports.up = async function (db) { + await db.runSql('ALTER TABLE apps ADD COLUMN updateInfoJson TEXT NULL'); +}; + +exports.down = async function (db) { + await db.runSql('ALTER TABLE apps DROP COLUMN updateInfoJson'); +}; diff --git a/setup/start.sh b/setup/start.sh index bea433ac0..29e087643 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -214,6 +214,9 @@ fi # migrate disk usage cache file [[ -f "${PLATFORM_DATA_DIR}/diskusage.json" ]] && mv "${PLATFORM_DATA_DIR}/diskusage.json" "${PLATFORM_DATA_DIR}/diskusage/cache.json" +# this file is obsolete now, moved to apps database +rm -f "${PLATFORM_DATA_DIR}/update/updatechecker.json" + log "Changing ownership" # note, change ownership after db migrate. this allow db migrate to move files around as root and then we can fix it up here # be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change diff --git a/src/apps.js b/src/apps.js index 30e61c36b..7be18f293 100644 --- a/src/apps.js +++ b/src/apps.js @@ -180,7 +180,6 @@ const appTaskManager = require('./apptaskmanager.js'), tasks = require('./tasks.js'), tgz = require('./backupformat/tgz.js'), TransformStream = require('stream').Transform, - updater = require('./updater.js'), users = require('./users.js'), util = require('util'), uuid = require('uuid'), @@ -192,7 +191,7 @@ const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationS 'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuQuota', 'apps.label', 'apps.notes', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.operatorsJson', 'apps.sso', 'apps.devicesJson', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp', 'apps.crontab', - 'apps.creationTime', 'apps.updateTime', 'apps.enableAutomaticUpdate', 'apps.upstreamUri', 'apps.checklistJson', + 'apps.creationTime', 'apps.updateTime', 'apps.enableAutomaticUpdate', 'apps.upstreamUri', 'apps.checklistJson', 'apps.updateInfoJson', 'apps.enableMailbox', 'apps.mailboxDisplayName', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableInbox', 'apps.inboxName', 'apps.inboxDomain', 'apps.enableTurn', 'apps.enableRedis', 'apps.storageVolumeId', 'apps.storageVolumePrefix', 'apps.ts', 'apps.healthTime', '(apps.icon IS NOT NULL) AS hasIcon', '(apps.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ].join(','); @@ -656,6 +655,10 @@ function postProcess(result) { result.checklist = safe.JSON.parse(result.checklistJson) || {}; delete result.checklistJson; + assert(result.updateInfoJson === null || typeof result.updateInfoJson === 'string'); + result.updateInfo = safe.JSON.parse(result.updateInfoJson) || null; + delete result.updateInfoJson; + assert(result.reverseProxyConfigJson === null || typeof result.reverseProxyConfigJson === 'string'); result.reverseProxyConfig = safe.JSON.parse(result.reverseProxyConfigJson) || {}; delete result.reverseProxyConfigJson; @@ -768,7 +771,7 @@ function canAutoupdateAppSync(app, updateInfo) { if (!app.enableAutomaticUpdate) return { code: false, reason: 'Automatic updates for the app is disabled' }; // for invalid subscriptions the appstore does not return a dockerImage - if (!manifest.dockerImage) return { code: false, reason: 'Invalid or Expired subscription '}; + if (!manifest.dockerImage) return { code: false, reason: 'Invalid or Expired subscription' }; if (updateInfo.unstable) return { code: false, reason: 'Update is marked as unstable' }; // only manual update allowed for unstable updates @@ -801,13 +804,11 @@ function attachProperties(app, domainObjectMap) { app.redirectDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); }); app.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); }); - const updateInfo = updater.getAppUpdateInfoSync(app.id); - if (updateInfo) { - const { code, reason } = canAutoupdateAppSync(app, updateInfo); // isAutoUpdatable is not cached since it depends on enableAutomaticUpdate and runState - updateInfo.isAutoUpdatable = code; - updateInfo.manualUpdateReason = reason; + if (app.updateInfo) { + const { code, reason } = canAutoupdateAppSync(app, app.updateInfo); // isAutoUpdatable is not cached since it depends on enableAutomaticUpdate and runState + app.updateInfo.isAutoUpdatable = code; + app.updateInfo.manualUpdateReason = reason; } - app.updateInfo = updateInfo; } function isAdmin(user) { @@ -1069,7 +1070,8 @@ async function updateWithConstraints(id, app, constraints) { const fields = [ ], values = [ ]; for (const p in app) { - if (p === 'manifest' || p === 'tags' || p === 'checklist' || p === 'accessRestriction' || p === 'devices' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'operators') { + if (p === 'manifest' || p === 'tags' || p === 'checklist' || p === 'accessRestriction' || p === 'devices' || p === 'debugMode' || p === 'error' + || p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'operators' || p === 'updateInfo') { fields.push(`${p}Json = ?`); values.push(JSON.stringify(app[p])); } else if (p !== 'portBindings' && p !== 'subdomain' && p !== 'domain' && p !== 'secondaryDomains' && p !== 'redirectDomains' && p !== 'aliasDomains' && p !== 'env' && p !== 'mounts') { diff --git a/src/appstore.js b/src/appstore.js index 2ef0c2982..36a4d1898 100644 --- a/src/appstore.js +++ b/src/appstore.js @@ -149,7 +149,7 @@ async function getBoxUpdate(options) { if (error) throw new BoxError(BoxError.NETWORK_ERROR, error); if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid appstore token'); - if (response.status === 204) return; // no update + if (response.status === 204) return null; // no update if (response.status !== 200 || !response.body) throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response: ${response.status} ${response.text}`); const updateInfo = response.body; @@ -192,7 +192,7 @@ async function getAppUpdate(app, options) { if (error) throw new BoxError(BoxError.NETWORK_ERROR, error); if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid appstore token'); - if (response.status === 204) return; // no update + if (response.status === 204) return null; // no update if (response.status !== 200 || !response.body) throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response: ${response.status} ${response.text}`); const updateInfo = response.body; diff --git a/src/apptask.js b/src/apptask.js index b27b09566..6c2014055 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -663,7 +663,7 @@ async function updateCommand(app, args, progressCallback) { } await progressCallback({ percent: 100, message: 'Done' }); - await updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, updateTime: new Date() }); + await updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, updateInfo: null, updateTime: new Date() }); } async function startCommand(app, args, progressCallback) { diff --git a/src/paths.js b/src/paths.js index aab42dc91..740bcd660 100644 --- a/src/paths.js +++ b/src/paths.js @@ -42,7 +42,7 @@ exports = module.exports = { NGINX_CERT_DIR: path.join(baseDir(), 'platformdata/nginx/cert'), BACKUP_INFO_DIR: path.join(baseDir(), 'platformdata/backup'), UPDATE_DIR: path.join(baseDir(), 'platformdata/update'), - UPDATE_CHECKER_FILE: path.join(baseDir(), 'platformdata/update/updatechecker.json'), + BOX_UPDATE_FILE: path.join(baseDir(), 'platformdata/update/boxupdate.json'), DISK_USAGE_CACHE_FILE: path.join(baseDir(), 'platformdata/diskusage/cache.json'), DISK_USAGE_EXCLUDE_FILE: path.join(baseDir(), 'platformdata/diskusage/exclude'), SNAPSHOT_INFO_FILE: path.join(baseDir(), 'platformdata/backup/snapshot-info.json'), diff --git a/src/routes/apps.js b/src/routes/apps.js index cadbb6c69..c6622f273 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -1046,12 +1046,14 @@ async function listEventlog(req, res, next) { async function checkForUpdates(req, res, next) { assert.strictEqual(typeof req.resources.app, 'object'); + if (!req.resources.app.appStoreId) return next(new HttpError(400, 'Custom apps have no updates')); + // it can take a while sometimes to get all the app updates one by one req.clearTimeout(); - const [error, result] = await safe(updater.checkForUpdates({ stableOnly: false })); // appId argument is ignored for the moment + const [error, result] = await safe(updater.checkAppUpdate(req.resources.app, { stableOnly: false })); if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { update: result[req.resource.app.id] })); + next(new HttpSuccess(200, { update: result })); } async function getTask(req, res, next) { diff --git a/src/routes/updater.js b/src/routes/updater.js index b6c0777e1..870fb8da2 100644 --- a/src/routes/updater.js +++ b/src/routes/updater.js @@ -4,9 +4,10 @@ exports = module.exports = { getAutoupdatePattern, setAutoupdatePattern, - getUpdateInfo, - update, - checkForUpdates + getBoxUpdate, + checkBoxUpdate, + + updateBox, }; const assert = require('assert'), @@ -35,7 +36,7 @@ async function setAutoupdatePattern(req, res, next) { next(new HttpSuccess(200, {})); } -async function update(req, res, next) { +async function updateBox(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 @@ -47,15 +48,17 @@ async function update(req, res, next) { next(new HttpSuccess(202, { taskId })); } -function getUpdateInfo(req, res, next) { - next(new HttpSuccess(200, { update: updater.getUpdateInfoSync() })); +async function getBoxUpdate(req, res, next) { + const [error, boxUpdateInfo] = await safe(updater.getBoxUpdate()); + if (error) return next(new HttpError(500, error)); + next(new HttpSuccess(200, { update: boxUpdateInfo })); } -async function checkForUpdates(req, res, next) { +async function checkBoxUpdate(req, res, next) { // it can take a while sometimes to get all the app updates one by one req.clearTimeout(); - const [error, result ] = await safe(updater.checkForUpdates({ stableOnly: false })); + const [error, result ] = await safe(updater.checkBoxUpdate({ stableOnly: false })); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, { update: result })); diff --git a/src/server.js b/src/server.js index b219a0a73..43eedc83b 100644 --- a/src/server.js +++ b/src/server.js @@ -133,9 +133,9 @@ async function initializeExpressSync() { router.get ('/api/v1/eventlog/:eventId', token, authorizeAdmin, routes.eventlog.get); // updater - router.get ('/api/v1/updater/updates', token, authorizeUser, routes.updater.getUpdateInfo); // allowed for normal users to make it work for app operators - 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/box_update', token, authorizeUser, routes.updater.getBoxUpdate); // allowed for normal users to make it work for app operators + router.post('/api/v1/updater/box_update', json, token, authorizeAdmin, routes.updater.updateBox); + router.post('/api/v1/updater/check_box_update', json, token, authorizeAdmin, routes.updater.checkBoxUpdate); 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); diff --git a/src/test/updater-test.js b/src/test/updater-test.js index 22382df26..c7f7ab5a8 100644 --- a/src/test/updater-test.js +++ b/src/test/updater-test.js @@ -5,7 +5,8 @@ 'use strict'; -const BoxError = require('../boxerror.js'), +const apps = require('../apps.js'), + BoxError = require('../boxerror.js'), common = require('./common.js'), constants = require('../constants.js'), expect = require('expect.js'), @@ -46,7 +47,7 @@ describe('updater', function () { describe('box updates', function () { before(async function () { - safe.fs.unlinkSync(paths.UPDATE_CHECKER_FILE); + safe.fs.unlinkSync(paths.BOX_UPDATE_FILE); await updater.setAutoupdatePattern(constants.AUTOUPDATE_PATTERN_NEVER); }); @@ -59,8 +60,9 @@ describe('updater', function () { .query({ boxVersion: constants.VERSION, accessToken: appstoreToken, stableOnly: false }) .reply(204, { } ); - await updater.checkForUpdates({ stableOnly: false }); - expect(updater.getUpdateInfoSync().box).to.not.be.ok(); + await updater.checkBoxUpdate({ stableOnly: false }); + const boxUpdateInfo = await updater.getBoxUpdate(); + expect(boxUpdateInfo).to.be(null); expect(scope.isDone()).to.be.ok(); }); @@ -72,10 +74,12 @@ describe('updater', function () { .query({ boxVersion: constants.VERSION, accessToken: appstoreToken, stableOnly: false }) .reply(200, { version: UPDATE_VERSION, changelog: [''], sourceTarballUrl: 'box.tar.gz', sourceTarballSigUrl: 'box.tar.gz.sig', boxVersionsUrl: 'box.versions', boxVersionsSigUrl: 'box.versions.sig', unstable: false } ); - await updater.checkForUpdates({ stableOnly: false }); - expect(updater.getUpdateInfoSync().box).to.be.ok(); - expect(updater.getUpdateInfoSync().box.version).to.be(UPDATE_VERSION); - expect(updater.getUpdateInfoSync().box.sourceTarballUrl).to.be('box.tar.gz'); + await updater.checkBoxUpdate({ stableOnly: false }); + const boxUpdateInfo = await updater.getBoxUpdate(); + + expect(boxUpdateInfo).to.be.ok(); + expect(boxUpdateInfo.version).to.be(UPDATE_VERSION); + expect(boxUpdateInfo.sourceTarballUrl).to.be('box.tar.gz'); expect(scope.isDone()).to.be.ok(); }); @@ -87,17 +91,17 @@ describe('updater', function () { .query({ boxVersion: constants.VERSION, accessToken: appstoreToken, stableOnly: false }) .reply(404, { version: '2.0.0-pre.0', changelog: [''], sourceTarballUrl: 'box-pre.tar.gz' } ); - await updater.checkForUpdates({ stableOnly: false }); - expect(updater.getUpdateInfoSync().box.version).to.be(UPDATE_VERSION); - expect(updater.getUpdateInfoSync().box.sourceTarballUrl).to.be('box.tar.gz'); + await safe(updater.checkBoxUpdate({ stableOnly: false })); // ignore error + const boxUpdateInfo = await updater.getBoxUpdate(); + + expect(boxUpdateInfo.version).to.be(UPDATE_VERSION); + expect(boxUpdateInfo.sourceTarballUrl).to.be('box.tar.gz'); expect(scope.isDone()).to.be.ok(); }); }); describe('app updates', function () { before(async function () { - safe.fs.unlinkSync(paths.UPDATE_CHECKER_FILE); - await updater.setAutoupdatePattern(constants.AUTOUPDATE_PATTERN_NEVER); }); @@ -109,9 +113,12 @@ describe('updater', function () { .query({ boxVersion: constants.VERSION, accessToken: appstoreToken, appId: app.appStoreId, appVersion: app.manifest.version, stableOnly: false }) .reply(204, { } ); - await updater._checkAppUpdates({ stableOnly: false }); - expect(updater.getUpdateInfoSync()).to.eql({}); + const appUpdateInfo = await updater.checkAppUpdate(app, { stableOnly: false }); + expect(appUpdateInfo).to.eql(null); expect(scope.isDone()).to.be.ok(); + + const tmp = await apps.get(app.id); + expect(tmp.updateInfo).to.be(null); }); it('bad response', async function () { @@ -122,8 +129,8 @@ describe('updater', function () { .query({ boxVersion: constants.VERSION, accessToken: appstoreToken, appId: app.appStoreId, appVersion: app.manifest.version, stableOnly: false }) .reply(500, { update: { manifest: { version: '1.0.0', changelog: '* some changes' } } } ); - await updater._checkAppUpdates({ stableOnly: false }); - expect(updater.getUpdateInfoSync()).to.eql({}); + const [error] = await safe(updater.checkAppUpdate(app, { stableOnly: false })); + expect(error).to.be.ok(); expect(scope.isDone()).to.be.ok(); }); @@ -135,16 +142,36 @@ describe('updater', function () { .query({ boxVersion: constants.VERSION, accessToken: appstoreToken, appId: app.appStoreId, appVersion: app.manifest.version, stableOnly: false }) .reply(200, { manifest: { version: '2.0.0', changelog: '* some changes' } } ); - await updater._checkAppUpdates({ stableOnly: false }); - expect(updater.getUpdateInfoSync()).to.eql({ 'appid': { manifest: { version: '2.0.0', changelog: '* some changes' }, unstable: false } }); + const expectedUpdateInfo = { + manifest: { version: '2.0.0', changelog: '* some changes' }, + unstable: false, + isAutoUpdatable: false, + manualUpdateReason: 'Invalid or Expired subscription' + }; + + const appUpdateInfo = await updater.checkAppUpdate(app, { stableOnly: false }); + expect(appUpdateInfo.manifest).to.eql(expectedUpdateInfo.manifest); expect(scope.isDone()).to.be.ok(); + + const tmp = await apps.get(app.id); + expect(tmp.updateInfo).to.eql(expectedUpdateInfo); }); it('does not offer old version', async function () { nock.cleanAll(); - await updater._checkAppUpdates({ stableOnly: false }); - expect(updater.getUpdateInfoSync()).to.eql({ }); + const expectedUpdateInfo = { + manifest: { version: '2.0.0', changelog: '* some changes' }, + unstable: false, + isAutoUpdatable: false, + manualUpdateReason: 'Invalid or Expired subscription' + }; + + const [error] = await safe(updater.checkAppUpdate(app, { stableOnly: false })); + expect(error).to.be.ok(); + + const tmp = await apps.get(app.id); + expect(tmp.updateInfo).to.eql(expectedUpdateInfo); }); }); }); diff --git a/src/updater.js b/src/updater.js index 4505a47e8..b14b3bd48 100644 --- a/src/updater.js +++ b/src/updater.js @@ -12,11 +12,10 @@ exports = module.exports = { notifyBoxUpdate, checkForUpdates, + checkAppUpdate, + checkBoxUpdate, - getAppUpdateInfoSync, - getUpdateInfoSync, - - _checkAppUpdates: checkAppUpdates + getBoxUpdate, }; const apps = require('./apps.js'), @@ -44,8 +43,7 @@ const apps = require('./apps.js'), semver = require('semver'), settings = require('./settings.js'), shell = require('./shell.js')('updater'), - tasks = require('./tasks.js'), - _ = require('./underscore.js'); + tasks = require('./tasks.js'); const RELEASES_PUBLIC_KEY = path.join(__dirname, 'releases.gpg'); const UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'); @@ -206,11 +204,16 @@ async function checkBoxUpdateRequirements(boxUpdateInfo) { } } +async function getBoxUpdate() { + const updateInfo = safe.JSON.parse(safe.fs.readFileSync(paths.BOX_UPDATE_FILE, 'utf8')); + return updateInfo || null; +} + async function startBoxUpdateTask(options, auditSource) { assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof auditSource, 'object'); - const boxUpdateInfo = getUpdateInfoSync().box; + const boxUpdateInfo = await getBoxUpdate(); if (!boxUpdateInfo) throw new BoxError(BoxError.NOT_FOUND, 'No update available'); if (!boxUpdateInfo.sourceTarballUrl) throw new BoxError(BoxError.BAD_STATE, 'No automatic update available'); if (semver.gte(constants.VERSION, boxUpdateInfo.version)) throw new BoxError(BoxError.NOT_FOUND, 'No update available'); // can happen after update completed or hotfix @@ -261,35 +264,26 @@ async function notifyBoxUpdate() { async function autoUpdate(auditSource) { assert.strictEqual(typeof auditSource, 'object'); - const updateInfo = getUpdateInfoSync(); - debug('autoUpdate: available updates: %j', Object.keys(updateInfo)); - + const boxUpdateInfo = await getBoxUpdate(); // do box before app updates. for the off chance that the box logic fixes some app update logic issue - if (updateInfo.box && !updateInfo.box.unstable) { - debug('autoUpdate: starting box autoupdate to %j', updateInfo.box.version); + if (boxUpdateInfo && !boxUpdateInfo.unstable) { + debug('autoUpdate: starting box autoupdate to %j', boxUpdateInfo.version); const [error] = await safe(startBoxUpdateTask({ skipBackup: false }, AuditSource.CRON)); if (!error) return; // do not start app updates when a box update got scheduled debug(`autoUpdate: failed to start box autoupdate task: ${error.message}`); // fall through to update apps if box update never started (failed ubuntu or avx check) } - const appUpdateInfoEntries = Object.entries(_.omit(updateInfo, ['box'])); - for (const [appId, appUpdateInfo] of appUpdateInfoEntries) { - const [getError, app] = await safe(apps.get(appId)); - if (getError || !app) { - debug(`autoUpdate: error getting ${appId}: ${getError?.message || 'no such app'}`); - continue; - } - - if (!app.updateInfo) continue; // possible, if an update check happenned in parallel - - if (!app.updateInfo?.isAutoUpdatable) { + const result = await apps.list(); + for (const app of result) { + if (!app.updateInfo) continue; + if (!app.updateInfo.isAutoUpdatable) { debug(`autoUpdate: ${app.fqdn} requires manual update. skipping`); continue; } const data = { - manifest: appUpdateInfo.manifest, + manifest: app.updateInfo.manifest, force: false }; @@ -299,88 +293,39 @@ async function autoUpdate(auditSource) { } } -function setUpdateInfo(state) { - // appid -> update info { creationDate, manifest } - // box -> { version, changelog, upgrade, sourceTarballUrl } - state.version = 2; - if (!safe.fs.writeFileSync(paths.UPDATE_CHECKER_FILE, JSON.stringify(state, null, 4), 'utf8')) debug(`setUpdateInfo: Error writing to update checker file: ${safe.error.message}`); -} - -function getUpdateInfoSync() { - const state = safe.JSON.parse(safe.fs.readFileSync(paths.UPDATE_CHECKER_FILE, 'utf8')); - if (!state || state.version !== 2) return {}; - delete state.version; - return state; -} - -function getAppUpdateInfoSync(appId) { - assert.strictEqual(typeof appId, 'string'); - - return getUpdateInfoSync()[appId] || null; -} - -async function checkAppUpdates(options) { +async function checkAppUpdate(app, options) { + assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); - debug('checkAppUpdates: checking for updates'); + if (app.appStoreId === '') return null; // appStoreId can be '' for dev apps - const state = getUpdateInfoSync(); - const newState = { }; // create new state so that old app ids are removed - - const result = await apps.list(); - - for (const app of result) { - if (app.appStoreId === '') continue; // appStoreId can be '' for dev apps - - const [error, updateInfo] = await safe(appstore.getAppUpdate(app, options)); - if (error) { - debug('checkAppUpdates: Error getting app update info for %s', app.id, error); - continue; // continue to next - } - - if (!updateInfo) continue; // skip if no next version is found - newState[app.id] = updateInfo; - } - - if ('box' in state) newState.box = state.box; // preserve the latest box state information - setUpdateInfo(newState); + const updateInfo = await appstore.getAppUpdate(app, options); + await apps.update(app.id, { updateInfo }); + return updateInfo; } -async function checkBoxUpdates(options) { +async function checkBoxUpdate(options) { assert.strictEqual(typeof options, 'object'); - debug('checkBoxUpdates: checking for updates'); + debug('checkBoxUpdate: checking for updates'); const updateInfo = await appstore.getBoxUpdate(options); - - const state = getUpdateInfoSync(); - - if (!updateInfo) { // no update - if ('box' in state) { - delete state.box; - setUpdateInfo(state); - } - debug('checkBoxUpdates: no updates'); - return; + if (updateInfo) { + safe.fs.writeFileSync(paths.BOX_UPDATE_FILE, JSON.stringify(updateInfo, null, 4)); + } else { + safe.fs.unlinkSync(paths.BOX_UPDATE_FILE); } - - debug(`checkBoxUpdates: ${updateInfo.version} is available`); - - state.box = updateInfo; - setUpdateInfo(state); } async function raiseNotifications() { - const state = getUpdateInfoSync(); - const pattern = await getAutoupdatePattern(); - if (pattern === constants.AUTOUPDATE_PATTERN_NEVER && state.box) { - const updateInfo = state.box; - const changelog = updateInfo.changelog.map((m) => `* ${m}\n`).join(''); + const boxUpdate = await getBoxUpdate(); + if (pattern === constants.AUTOUPDATE_PATTERN_NEVER && boxUpdate) { + const changelog = boxUpdate.changelog.map((m) => `* ${m}\n`).join(''); const message = `Changelog:\n${changelog}\n\nGo to the Settings view to update.\n\n`; - await notifications.pin(notifications.TYPE_BOX_UPDATE, `Cloudron v${updateInfo.version} is available`, message, { context: updateInfo.version }); + await notifications.pin(notifications.TYPE_BOX_UPDATE, `Cloudron v${boxUpdate.version} is available`, message, { context: boxUpdate.version }); } const result = await apps.list(); @@ -399,14 +344,15 @@ async function raiseNotifications() { async function checkForUpdates(options) { assert.strictEqual(typeof options, 'object'); - const [boxError] = await safe(checkBoxUpdates(options)); + const [boxError] = await safe(checkBoxUpdate(options)); if (boxError) debug('checkForUpdates: error checking for box updates: %o', boxError); - const [appError] = await safe(checkAppUpdates(options)); - if (appError) debug('checkForUpdates: error checking for app updates: %o', appError); + // check app updates + const result = await apps.list(); + for (const app of result) { + await safe(checkAppUpdate(app), { debug }); + } // raise notifications here because the updatechecker runs regardless of auto-updater cron job await raiseNotifications(); - - return getUpdateInfoSync(); }