mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-23 14:57:18 +00:00

`etcher-latest-version` was kept in a separate repository in order to re-use it with the Etcher website, however the Etcher website is not using it at all, and we're moving towards having the website in the main repository. Therefore, this commit brings back the logic from `etcher-latest-version`, but introduces it as `lib/shared/s3-packages.js`, in order to not tie ourselves to the AngularJS framework, and as a step towards the Etcher SDK. As a nice little bonus, this commit adds support for an `ETCHER_FAKE_S3_LATEST_VERSION` environment variable that can be used to trick Etcher that there is an available update, and therefore show the update notifier modal. Also, this commit adds support for snapshot builds update-checks, by checking the `resin-nightly-downloads` S3 bucket if the current version contains a git commit hash build number. If the version is not a production release, then the update notifier modal doesn't present the checkbox to disable update notifications for X days. We also add a property called `updates.semverRange` to `package.json`, which can be used to fine control which versions are considered as candidates for an update notification. This commit adds a setting called `includeUnstableChannel`, which can be used to tweak whether unstable (beta) releases are considered or not when checking for the latest available version. See: https://github.com/resin-io-modules/etcher-latest-version Fixes: https://github.com/resin-io/etcher/issues/953 Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
246 lines
6.5 KiB
JavaScript
246 lines
6.5 KiB
JavaScript
/*
|
|
* Copyright 2017 resin.io
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const _ = require('lodash');
|
|
const semver = require('semver');
|
|
const Bluebird = require('bluebird');
|
|
const request = Bluebird.promisifyAll(require('request'));
|
|
const xml = Bluebird.promisifyAll(require('xml2js'));
|
|
const release = require('./release');
|
|
|
|
/**
|
|
* @summary Etcher S3 bucket URLs
|
|
* @namespace BUCKET_URL
|
|
* @public
|
|
*/
|
|
exports.BUCKET_URL = {
|
|
|
|
/**
|
|
* @property {String} PRODUCTION
|
|
* @memberof BUCKET_URL
|
|
* @description
|
|
* Etcher production S3 bucket URL
|
|
*/
|
|
PRODUCTION: 'https://resin-production-downloads.s3.amazonaws.com',
|
|
|
|
/**
|
|
* @property {String} SNAPSHOT
|
|
* @memberof BUCKET_URL
|
|
* @description
|
|
* Etcher snapshot S3 bucket URL
|
|
*/
|
|
SNAPSHOT: 'https://resin-nightly-downloads.s3.amazonaws.com'
|
|
|
|
};
|
|
|
|
/**
|
|
* @summary Etcher S3 package name
|
|
* @constant
|
|
* @private
|
|
* @type {String}
|
|
*/
|
|
const S3_PACKAGE_NAME = 'etcher';
|
|
|
|
/**
|
|
* @summary Number of packages per Etcher version
|
|
* @constant
|
|
* @private
|
|
* @type {Number}
|
|
*/
|
|
const NUMBER_OF_PACKAGES = 8;
|
|
|
|
/**
|
|
* @summary Get the correct S3 bucket url from a release type
|
|
* @function
|
|
* @private
|
|
*
|
|
* @param {RELEASE_TYPE} releaseType - release type
|
|
* @returns {?String} S3 bucket url
|
|
*
|
|
* @example
|
|
* const bucketUrl = s3Packages.getBucketUrlFromReleaseType(release.RELEASE_TYPE.PRODUCTION);
|
|
*
|
|
* if (bucketUrl) {
|
|
* console.log(bucketUrl);
|
|
* }
|
|
*/
|
|
exports.getBucketUrlFromReleaseType = (releaseType) => {
|
|
if (releaseType === release.RELEASE_TYPE.PRODUCTION) {
|
|
return exports.BUCKET_URL.PRODUCTION;
|
|
}
|
|
|
|
if (releaseType === release.RELEASE_TYPE.SNAPSHOT) {
|
|
return exports.BUCKET_URL.SNAPSHOT;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* @summary Get all remote versions from an S3 bucket
|
|
* @function
|
|
* @private
|
|
*
|
|
* @description
|
|
* We memoize based on the assumption that the received latest version
|
|
* number will not increase while the application is running.
|
|
*
|
|
* @param {String} bucketUrl - s3 bucket url
|
|
* @fulfil {String[]} - remote versions
|
|
* @returns {Promise}
|
|
*
|
|
* @example
|
|
* s3Packages.getRemoteVersions(s3Packages.BUCKET_URL.PRODUCTION).then((versions) => {
|
|
* _.each(versions, (version) => {
|
|
* console.log(version);
|
|
* });
|
|
* });
|
|
*/
|
|
exports.getRemoteVersions = _.memoize((bucketUrl) => {
|
|
if (_.isNil(bucketUrl)) {
|
|
return Bluebird.reject(new Error(`Invalid bucket url: ${bucketUrl}`));
|
|
}
|
|
|
|
/* eslint-disable lodash/prefer-lodash-method */
|
|
|
|
return request.getAsync(bucketUrl)
|
|
|
|
/* eslint-enable lodash/prefer-lodash-method */
|
|
|
|
.get('body')
|
|
.then(xml.parseStringAsync)
|
|
.get('ListBucketResult')
|
|
.then((bucketResult) => {
|
|
return _.get(bucketResult, [ 'Contents' ], []);
|
|
})
|
|
.reduce((accumulator, entry) => {
|
|
const [ name, version ] = _.split(_.first(entry.Key), '/');
|
|
|
|
if (name === S3_PACKAGE_NAME) {
|
|
if (_.isNil(accumulator[version])) {
|
|
accumulator[version] = 1;
|
|
} else {
|
|
accumulator[version] += 1;
|
|
}
|
|
}
|
|
|
|
return accumulator;
|
|
}, [])
|
|
.then((versions) => {
|
|
return _.keys(_.pickBy(versions, (occurrences) => {
|
|
return occurrences >= NUMBER_OF_PACKAGES;
|
|
}));
|
|
})
|
|
.catch({
|
|
code: 'ENOTFOUND'
|
|
}, {
|
|
code: 'ETIMEDOUT'
|
|
}, () => {
|
|
return [];
|
|
});
|
|
});
|
|
|
|
/**
|
|
* @summary Check if a version satisfies a semver range
|
|
* @function
|
|
* @private
|
|
*
|
|
* @description
|
|
* This function is a wrapper around `semver.satisfies`
|
|
* to make it work fine with pre-release versions.
|
|
*
|
|
* @param {String} version - semver version
|
|
* @param {String} range - semver range
|
|
* @returns {Boolean} whether the version satisfies the range
|
|
*
|
|
* @example
|
|
* if (semverSatisfies('1.0.0', '>=1.0.0')) {
|
|
* console.log('The version satisfies the range');
|
|
* }
|
|
*/
|
|
const semverSatisfies = (version, range) => {
|
|
|
|
// The `semver` module refuses to apply ranges to prerelease versions
|
|
// As a workaround, we drop the prerelease tags, if any, apply the range
|
|
// on that, and keep using the prerelease tag from then on.
|
|
// See https://github.com/npm/node-semver#prerelease-tags
|
|
const strippedVersion = `${semver.major(version)}.${semver.minor(version)}.${semver.patch(version)}`;
|
|
|
|
return semver.satisfies(strippedVersion, range);
|
|
};
|
|
|
|
/**
|
|
* @summary Get the latest available version for a given release type
|
|
* @function
|
|
* @public
|
|
*
|
|
* @param {String} releaseType - release type
|
|
* @param {Object} [options] - options
|
|
* @param {String} [options.range] - semver range
|
|
* @param {Boolean} [options.includeUnstableChannel=false] - include unstable channel
|
|
* @fulfil {(String|undefined)} - latest version
|
|
* @returns {Promise}
|
|
*
|
|
* @example
|
|
* s3Packages.getLatestVersion(release.RELEASE_TYPE.PRODUCTION, {
|
|
* range: '>=2.0.0',
|
|
* includeUnstableChannel: true
|
|
* }).then((latestVersion) => {
|
|
* console.log(`The latest version is: ${latestVersion}`);
|
|
* });
|
|
*/
|
|
exports.getLatestVersion = (releaseType, options = {}) => {
|
|
|
|
// For manual testing purposes
|
|
const ETCHER_FAKE_S3_LATEST_VERSION = process.env.ETCHER_FAKE_S3_LATEST_VERSION;
|
|
if (!_.isNil(ETCHER_FAKE_S3_LATEST_VERSION)) {
|
|
if (release.getReleaseType(ETCHER_FAKE_S3_LATEST_VERSION) === releaseType) {
|
|
return Bluebird.resolve(ETCHER_FAKE_S3_LATEST_VERSION);
|
|
}
|
|
|
|
return Bluebird.resolve();
|
|
}
|
|
|
|
const bucketUrl = exports.getBucketUrlFromReleaseType(releaseType);
|
|
if (_.isNil(bucketUrl)) {
|
|
return Bluebird.reject(new Error(`No bucket URL found for release type: ${releaseType}`));
|
|
}
|
|
|
|
/* eslint-disable lodash/prefer-lodash-method */
|
|
|
|
return exports.getRemoteVersions(bucketUrl).filter((version) => {
|
|
|
|
/* eslint-enable lodash/prefer-lodash-method */
|
|
|
|
if (_.some([
|
|
|
|
// This check allows us to ignore snapshot builds in production
|
|
// buckets, and viceversa, which could have been uploaded by mistake.
|
|
release.getReleaseType(version) !== releaseType,
|
|
|
|
!release.isStableRelease(version) && !options.includeUnstableChannel
|
|
])) {
|
|
return false;
|
|
}
|
|
|
|
return semverSatisfies(version, options.range || '*');
|
|
}).then((versions) => {
|
|
return _.last(versions.sort(semver.compare));
|
|
});
|
|
};
|