community: store versionsUrl in the database

This commit is contained in:
Girish Ramakrishnan
2026-02-05 17:29:00 +01:00
parent 91b8f1a457
commit d6eb6d3e3e
13 changed files with 249 additions and 118 deletions

View File

@@ -190,7 +190,7 @@ const appTaskManager = require('./apptaskmanager.js'),
_ = require('./underscore.js');
// NOTE: when adding fields here, update the clone and unarchive logic as well
const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.versionsUrl', 'apps.installationState', 'apps.errorJson', 'apps.runState',
'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',
@@ -592,12 +592,12 @@ function pickFields(app, accessLevel) {
let result;
if (accessLevel === exports.ACCESS_LEVEL_USER) {
result = _.pick(app, [
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'accessRestriction',
'id', 'appStoreId', 'versionsUrl', 'installationState', 'error', 'runState', 'health', 'taskId', 'accessRestriction',
'secondaryDomains', 'redirectDomains', 'aliasDomains', 'sso', 'subdomain', 'domain', 'fqdn', 'certificate',
'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'upstreamUri']);
} else { // admin or operator
result = _.pick(app, [
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId',
'id', 'appStoreId', 'versionsUrl', 'installationState', 'error', 'runState', 'health', 'taskId',
'subdomain', 'domain', 'fqdn', 'certificate', 'crontab', 'upstreamUri',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuQuota', 'operators',
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
@@ -884,9 +884,10 @@ async function checkForPortBindingConflict(portBindings, options) {
}
}
async function add(id, appStoreId, manifest, subdomain, domain, portBindings, data) {
async function add(id, appStoreId, versionsUrl, manifest, subdomain, domain, portBindings, data) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert.strictEqual(typeof versionsUrl, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof manifest.version, 'string');
assert.strictEqual(typeof subdomain, 'string');
@@ -930,11 +931,11 @@ async function add(id, appStoreId, manifest, subdomain, domain, portBindings, da
const queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, operatorsJson, memoryLimit, cpuQuota, '
query: 'INSERT INTO apps (id, appStoreId, versionsUrl, manifestJson, installationState, runState, accessRestrictionJson, operatorsJson, memoryLimit, cpuQuota, '
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, checklistJson, servicesConfigJson, icon, '
+ 'enableMailbox, mailboxDisplayName, upstreamUri, enableTurn, enableRedis, devicesJson, notes, crontab, enableBackup, enableAutomaticUpdate) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, operatorsJson, memoryLimit, cpuQuota,
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, versionsUrl, manifestJson, installationState, runState, accessRestrictionJson, operatorsJson, memoryLimit, cpuQuota,
sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, checklistJson, servicesConfigJson, icon,
enableMailbox, mailboxDisplayName, upstreamUri, enableTurn, enableRedis, devicesJson, notes, crontab,
enableBackup, enableAutomaticUpdate
@@ -1400,7 +1401,8 @@ async function install(data, auditSource) {
overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false,
skipDnsSetup = 'skipDnsSetup' in data ? data.skipDnsSetup : false,
enableTurn = 'enableTurn' in data ? data.enableTurn : true,
appStoreId = data.appStoreId,
appStoreId = data.appStoreId || '',
versionsUrl = data.versionsUrl || '',
upstreamUri = data.upstreamUri || '',
manifest = data.manifest,
notes = data.notes || null,
@@ -1520,7 +1522,7 @@ async function install(data, auditSource) {
installationState: exports.ISTATE_PENDING_INSTALL
};
const [addError] = await safe(add(appId, appStoreId, manifest, subdomain, domain, portBindings, app));
const [addError] = await safe(add(appId, appStoreId, versionsUrl, manifest, subdomain, domain, portBindings, app));
if (addError && addError.reason === BoxError.ALREADY_EXISTS) throw getDuplicateErrorDetails(addError.message, locations, portBindings);
if (addError) throw addError;
@@ -1532,7 +1534,7 @@ async function install(data, auditSource) {
const taskId = await addTask(appId, app.installationState, task, auditSource);
const newApp = Object.assign({}, _.omit(app, ['icon']), { appStoreId, manifest, subdomain, domain, portBindings });
const newApp = Object.assign({}, _.omit(app, ['icon']), { appStoreId, versionsUrl, manifest, subdomain, domain, portBindings });
newApp.fqdn = dns.fqdn(newApp.subdomain, newApp.domain);
newApp.secondaryDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); });
newApp.redirectDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); });
@@ -2129,8 +2131,9 @@ async function updateApp(app, data, auditSource) {
error = await checkManifest(manifest);
if (error) throw error;
const updateConfig = { skipBackup, manifest }; // this will clear appStoreId when updating from a repo and set it if passed in for update route
const updateConfig = { skipBackup, manifest }; // this will clear appStoreId/versionsUrl when updating from a repo and set it if passed in for update route
if ('appStoreId' in data) updateConfig.appStoreId = data.appStoreId;
if ('versionsUrl' in data) updateConfig.versionsUrl = data.versionsUrl;
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
@@ -2141,8 +2144,8 @@ async function updateApp(app, data, auditSource) {
// suffix '0' if prerelease is missing for semver.lte to work as expected
const currentVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
const updateVersion = semver.prerelease(updateConfig.manifest.version) ? updateConfig.manifest.version : `${updateConfig.manifest.version}-0`;
if (app.appStoreId !== '' && semver.lte(updateVersion, currentVersion)) {
if (!data.force) throw new BoxError(BoxError.BAD_FIELD, 'Downgrades are not permitted for apps installed from AppStore. force to override');
if ((app.appStoreId !== '' || app.versionsUrl !== '') && semver.lte(updateVersion, currentVersion)) {
if (!data.force) throw new BoxError(BoxError.BAD_FIELD, 'Downgrades are not permitted for apps installed from AppStore or Community. force to override');
}
if ('icon' in data) {
@@ -2438,7 +2441,7 @@ async function clone(app, data, user, auditSource) {
if (!backup.manifest) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not detect restore manifest');
if (backup.encryptionVersion === 1) throw new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and cannot be cloned');
const manifest = backup.manifest, appStoreId = app.appStoreId;
const manifest = backup.manifest, appStoreId = app.appStoreId, versionsUrl = app.versionsUrl;
let error = validateSecondaryDomains(data.secondaryDomains || {}, manifest);
if (error) throw error;
@@ -2483,7 +2486,7 @@ async function clone(app, data, user, auditSource) {
label: dolly.label ? `${dolly.label}-clone` : '',
});
const [addError] = await safe(add(newAppId, appStoreId, manifest, subdomain, domain, portBindings, obj));
const [addError] = await safe(add(newAppId, appStoreId, versionsUrl, manifest, subdomain, domain, portBindings, obj));
if (addError && addError.reason === BoxError.ALREADY_EXISTS) throw getDuplicateErrorDetails(addError.message, locations, portBindings);
if (addError) throw addError;
@@ -2518,7 +2521,7 @@ async function unarchive(archive, data, auditSource) {
domain = data.domain.toLowerCase(),
overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false;
const manifest = backup.manifest, appStoreId = backup.manifest.id;
const manifest = backup.manifest, appStoreId = backup.manifest.id, versionsUrl = backup.appConfig?.versionsUrl || '';
let error = validateSecondaryDomains(data.secondaryDomains || {}, manifest);
if (error) throw error;
@@ -2558,7 +2561,7 @@ async function unarchive(archive, data, auditSource) {
});
obj.icon = (await archives.getIcons(archive.id))?.icon;
const [addError] = await safe(add(appId, appStoreId, manifest, subdomain, domain, portBindings, obj));
const [addError] = await safe(add(appId, appStoreId, versionsUrl, manifest, subdomain, domain, portBindings, obj));
if (addError && addError.reason === BoxError.ALREADY_EXISTS) throw getDuplicateErrorDetails(addError.message, locations, portBindings);
if (addError) throw addError;