Files
cloudron-box/src/community.js
T
Girish Ramakrishnan 36aa641cb9 migrate to "export default"
also, set no-use-before-define in linter
2026-02-14 15:43:24 +01:00

124 lines
6.5 KiB
JavaScript

import assert from 'node:assert';
import BoxError from './boxerror.js';
import debugModule from 'debug';
import manifestFormat from '@cloudron/manifest-format';
import promiseRetry from './promise-retry.js';
import safe from 'safetydance';
import superagent from '@cloudron/superagent';
const debug = debugModule('box:community');
async function getAppVersion(url, version) {
assert.strictEqual(typeof url, 'string');
assert.strictEqual(typeof version, 'string');
if (!url.startsWith('https://')) throw new BoxError(BoxError.BAD_FIELD, 'URL must use HTTPS');
const [error, response] = await safe(superagent.get(url).timeout(60 * 1000).ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND, 'CloudronVersions.json not found');
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Fetch failed: ${response.status}`);
// if the content-type is incorrect, we will get a buffer
const versionsRoot = Buffer.isBuffer(response.body) ? safe.JSON.parse(response.body.toString('utf8')) : response.body;
const versionsError = manifestFormat.parseVersions(versionsRoot);
if (versionsError) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid CloudronVersions.json: ${versionsError.message}`);
const versions = versionsRoot.versions;
const sortedVersions = Object.keys(versions).filter(v => versions[v].publishState !== 'revoked').sort(manifestFormat.packageVersionCompare);
const versionData = version === 'latest' ? versions[sortedVersions.at(-1)] : versions[version];
if (!versionData) throw new BoxError(BoxError.NOT_FOUND, `Version ${version} not found`);
const manifestError = manifestFormat.checkVersionsRequirements(versionData.manifest);
if (manifestError) throw new BoxError(BoxError.BAD_FIELD, `Invalid manifest: ${manifestError.message}`);
return versionData; // { manifest, publishState, creationDate, ts }
}
async function downloadManifest(versionsUrl) {
assert.strictEqual(typeof versionsUrl, 'string');
const atIndex = versionsUrl.lastIndexOf('@');
if (atIndex === -1) throw new BoxError(BoxError.BAD_FIELD, 'version is required in versionsUrl (format: url@version)');
const url = versionsUrl.substring(0, atIndex);
const version = versionsUrl.substring(atIndex + 1);
if (!url.startsWith('https://')) throw new BoxError(BoxError.BAD_FIELD, 'versionsUrl must use https');
if (!version) throw new BoxError(BoxError.BAD_FIELD, 'version is required in versionsUrl (format: url@version)');
debug(`downloading manifest from ${url} version ${version}`);
const [error, response] = await safe(superagent.get(url).timeout(60 * 1000).ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, 'Network error downloading manifest: ' + error.message);
if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND, 'CloudronVersions.json not found');
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Fetch failed: ${response.status}`);
// if the content-type is incorrect, we will get a buffer
const versionsRoot = Buffer.isBuffer(response.body) ? safe.JSON.parse(response.body.toString('utf8')) : response.body;
const versionsError = manifestFormat.parseVersions(versionsRoot);
if (versionsError) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid CloudronVersions.json: ${versionsError.message}`);
const versions = versionsRoot.versions;
const sortedVersions = Object.keys(versions).filter(v => versions[v].publishState !== 'revoked').sort(manifestFormat.packageVersionCompare);
const versionData = version === 'latest' ? versions[sortedVersions.at(-1)] : versions[version];
if (!versionData) throw new BoxError(BoxError.NOT_FOUND, `Version ${version} not found`);
if (!versionData.manifest || typeof versionData.manifest !== 'object') throw new BoxError(BoxError.EXTERNAL_ERROR, 'Missing manifest in version data');
return { versionsUrl: url, manifest: versionData.manifest };
}
async function getAppUpdate(app, options) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
const [error, response] = await safe(superagent.get(app.versionsUrl).timeout(60 * 1000).ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, 'Network error downloading manifest: ' + error.message);
if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND, 'CloudronVersions.json not found');
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Fetch failed: ${response.status}`);
// if the content-type is incorrect, we will get a buffer
const versionsRoot = Buffer.isBuffer(response.body) ? safe.JSON.parse(response.body.toString('utf8')) : response.body;
const versionsError = manifestFormat.parseVersions(versionsRoot);
if (versionsError) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid CloudronVersions.json: ${versionsError.message}`);
const versions = versionsRoot.versions;
const sortedVersions = Object.keys(versions).filter(v => versions[v].publishState !== 'revoked').sort(manifestFormat.packageVersionCompare);
const idx = sortedVersions.findIndex(v => v === app.manifest.version);
if (idx === -1) return null; // current version was revoked or not found, no update info available
if (idx === sortedVersions.length-1) return null; // no update
const nextVersion = versions[sortedVersions[idx+1]];
const unstable = !versionsRoot.stable || nextVersion.publishState !== 'published';
return {
id: app.manifest.id,
creationDate: nextVersion.creationDate,
manifest: nextVersion.manifest,
unstable
};
}
async function downloadIcon(manifest) {
return await promiseRetry({ times: 10, interval: 5000, debug }, async function () {
const [networkError, response] = await safe(superagent.get(manifest.iconUrl)
.timeout(60 * 1000)
.ok(() => true));
if (networkError) throw new BoxError(BoxError.NETWORK_ERROR, `Network error downloading icon: ${networkError.message}`);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Icon download failed. ${response.status} ${JSON.stringify(response.body)}`);
if (!Buffer.isBuffer(response.body)) throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid icon returned for app');
return response.body;
});
}
export default {
getAppVersion,
downloadManifest,
getAppUpdate,
downloadIcon
};