diff --git a/.gitignore b/.gitignore index c9f8187e..b95f8319 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ node_modules .env docs/_book/ +.idea/ diff --git a/docs/assets.md b/docs/assets.md index dd8eca74..c57c95a0 100644 --- a/docs/assets.md +++ b/docs/assets.md @@ -20,7 +20,7 @@ Filetype and usage will be detected from the extension: | Platform | Extensions (sorted by priority) | | -------- | ---------- | -| Windows | `.exe`, `.nupkg`, `.zip` | +| Windows | `.exe`, `.nupkg`, `.zip`, `.msi` | | OS X | `.dmg`, `.zip` | | Linux | `.deb`, `.rpm`, `.zip` | diff --git a/docs/faq.md b/docs/faq.md index 3d6a3a64..fec794d0 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -20,7 +20,7 @@ Since version 3.0.0, Nuts can works with [other backends](https://github.com/Git Nuts can detect the type of file from its filename, there is no strict policy on file naming. Nuts tries to respect the filename/extension conventions for the different platforms. request:) -- Windows: `.exe`, `.nupkg` etc +- Windows: `.exe`, `.nupkg`, `.msi` etc - Linux: `.deb`, `.tar.gz`, etc - OS X: `.dmg`, etc diff --git a/docs/using-it.md b/docs/using-it.md index 59c6f412..5878e8f7 100644 --- a/docs/using-it.md +++ b/docs/using-it.md @@ -4,4 +4,5 @@ Using Nuts for your application? Post a Pull-Request! - [GitBook Editor](https://www.gitbook.com/editor) - [DeckHub](https://getdeckhub.com) +- [c2softwareDesk] (http://desktop.c2software.de) diff --git a/lib/api.js b/lib/api.js index 70e789f4..d889e2e6 100644 --- a/lib/api.js +++ b/lib/api.js @@ -9,9 +9,9 @@ module.exports = { }, 'versions': function (req) { - return this.versions.filter({ + return this.versions.select({ platform: req.query.platform, - channel: req.query.channel || '*' + channel: req.query.channel }); }, diff --git a/lib/nuts.js b/lib/nuts.js index a8d8e11a..c51e094e 100644 --- a/lib/nuts.js +++ b/lib/nuts.js @@ -91,7 +91,7 @@ Nuts.prototype._init = function() { if (!that.opts.preFetch) return return that.versions.list(); }); -} +}; // Perform a hook using promised functions @@ -107,7 +107,7 @@ Nuts.prototype.performQ = function(name, arg, fn) { .then(function() { next(); }, next); - }) + }); }; // Serve an asset to the response @@ -211,6 +211,7 @@ Nuts.prototype.onUpdate = function(req, res, next) { var fullUrl = getFullUrl(req); var platform = req.params.platform; var tag = req.params.version; + var channel = req.query.channel; var filetype = req.query.filetype ? req.query.filetype : "zip"; Q() @@ -220,10 +221,10 @@ Nuts.prototype.onUpdate = function(req, res, next) { platform = platforms.detect(platform); - return that.versions.filter({ - tag: '>='+tag, + return that.versions.newer({ + tag: tag, platform: platform, - channel: '*' + channel: channel }); }) .then(function(versions) { @@ -255,15 +256,16 @@ Nuts.prototype.onUpdateWin = function(req, res, next) { var fullUrl = getFullUrl(req); var platform = 'win_32'; var tag = req.params.version; + var channel = req.query.channel; that.init() .then(function() { platform = platforms.detect(platform); - return that.versions.filter({ - tag: '>='+tag, + return that.versions.newer({ + tag: tag, platform: platform, - channel: '*' + channel: channel }); }) .then(function(versions) { @@ -285,7 +287,7 @@ Nuts.prototype.onUpdateWin = function(req, res, next) { // Change filename to use download proxy .map(function(entry) { - entry.filename = urljoin(fullUrl, '/../../../../', '/download/'+entry.semver+'/'+entry.filename); + entry.filename = urljoin(fullUrl, '/../../../../', '/download/'+entry.version+'/'+entry.filename); return entry; }) @@ -309,9 +311,8 @@ Nuts.prototype.onServeNotes = function(req, res, next) { Q() .then(function() { - return that.versions.filter({ - tag: tag? '>='+tag : '*', - channel: '*' + return that.versions.select({ + tag: tag }); }) .then(function(versions) { diff --git a/lib/utils/platforms.js b/lib/utils/platforms.js index 82706333..63ed484a 100644 --- a/lib/utils/platforms.js +++ b/lib/utils/platforms.js @@ -30,7 +30,8 @@ function detectPlatform(platform) { // Detect prefix: osx, widnows or linux if (_.contains(name, 'win') - || hasSuffix(name, '.exe')) prefix = platforms.WINDOWS; + || hasSuffix(name, '.exe') + || hasSuffix(name, '.msi')) prefix = platforms.WINDOWS; if (_.contains(name, 'linux') || _.contains(name, 'ubuntu') @@ -72,7 +73,7 @@ function satisfiesPlatform(platform, list) { function resolveForVersion(version, platformID, opts) { opts = _.defaults(opts || {}, { // Order for filetype - filePreference: ['.exe', '.dmg', '.deb', '.rpm', '.tgz', '.tar.gz', '.zip', '.nupkg'], + filePreference: ['.exe', '.msi', '.dmg', '.deb', '.rpm', '.tgz', '.tar.gz', '.zip', '.nupkg'], wanted: null }); diff --git a/lib/utils/win-releases.js b/lib/utils/win-releases.js index e468f97b..b49fa578 100644 --- a/lib/utils/win-releases.js +++ b/lib/utils/win-releases.js @@ -10,6 +10,9 @@ var CHANNELS = [ // RELEASES parsing var releaseRe = /^([0-9a-fA-F]{40})\s+(\S+)\s+(\d+)[\r]*$/; +var suffixRe = /(-full|-delta)?\.nupkg/; +var versionRe = /\d+(\.\d+){0,3}(-[a-z][0-9a-z-\.]*$)?$/; +var prereleaseRe = /-[a-z][0-9a-z-\.]*$/; // Hash a prerelease @@ -73,13 +76,22 @@ function parseRELEASES(content) { .split(/\.|-/) .reverse(); - var version = _.chain(filenameParts) - .filter(function(x) { - return /^\d+$/.exec(x); + var version = _.chain( + filename + .replace(suffixRe, '') + .match(versionRe) + ) + .thru(function(matchResult) { + return matchResult ? matchResult[0] : ''; }) - .reverse() - .value() - .join('.'); + .replace(prereleaseRe, function(prerelease) { + // NuGet doesn't support dots in prereleases + // https://docs.nuget.org/create/versioning#user-content-prerelease-versions + return prerelease.replace(/\./g, ''); + }) + .value(); + + if (!version) throw new Error('Release missing valid version: ' + filename); return { sha: parts[1], diff --git a/lib/versions.js b/lib/versions.js index 999cdc1f..a7d5ca50 100644 --- a/lib/versions.js +++ b/lib/versions.js @@ -4,6 +4,8 @@ var semver = require('semver'); var platforms = require('./utils/platforms'); +var channelRe = /^[a-z]*/; + // Normalize tag name function normalizeTag(tag) { if (tag[0] == 'v') tag = tag.slice(1); @@ -15,7 +17,7 @@ function extractChannel(tag) { var suffix = tag.split('-')[1]; if (!suffix) return 'stable'; - return suffix.split('.')[0]; + return suffix.match(channelRe)[0] || suffix.split('.')[0]; } // Normalize a release to a version @@ -36,6 +38,8 @@ function normalizeVersion(release) { filename: asset.name, size: asset.size, content_type: asset.content_type, + download_url: asset.url, + download_count: asset.download_count, raw: asset }; }) @@ -47,7 +51,8 @@ function normalizeVersion(release) { channel: extractChannel(release.tag_name), notes: release.body || "", published_at: new Date(release.published_at), - platforms: releasePlatforms + platforms: releasePlatforms, + download_count: downloadCount }; } @@ -62,6 +67,45 @@ function compareVersions(v1, v2) { return 0; } +function supportsPlatform(requestedPlatform, versionPlatforms) { + return (!requestedPlatform || + platforms.satisfies(requestedPlatform, _.pluck(versionPlatforms, 'type'))); +} + +function matchingChannel(requestedChannel, versionChannel) { + return (!requestedChannel || + requestedChannel === '*' || + versionChannel === requestedChannel); +} + +// Creates filter function that checks for updates +function newVersionFilter(opts) { + return function(version) { + // Not available for requested paltform + if (!supportsPlatform(opts.platform, version.platforms)) { + return false; + } + + // If the caller specifies a channel then stay on that channel. Otherwise + // look for anything closer to the main release. + if (opts.channel) { + return (opts.channel === version.channel && + semver.rcompare(version.tag, opts.tag) <= 0); + } else { + return semver.satisfies(version.tag, '>=' + opts.tag); + } + } +} + +// Creates generic filter function for all versions +function allVersionFilter(opts) { + return function(version) { + return ((!opts.tag || opts.tag === version.tag) && + matchingChannel(opts.channel, version.channel) && + supportsPlatform(opts.platform, version.platforms)); + } +} + function Versions(backend) { this.backend = backend; @@ -87,34 +131,33 @@ Versions.prototype.get = function(tag) { }; // Filter versions with criterias -Versions.prototype.filter = function(opts) { - opts = _.defaults(opts || {}, { - tag: 'latest', - platform: null, - channel: 'stable' - }); +Versions.prototype.filter = function(opts, filterBuilder) { + opts = opts || {}; if (opts.platform) opts.platform = platforms.detect(opts.platform); - + return this.list() .then(function(versions) { return _.chain(versions) - .filter(function(version) { - // Check channel - if (opts.channel != '*' && version.channel != opts.channel) return false; - - // Not available for requested paltform - if (opts.platform && !platforms.satisfies(opts.platform, _.pluck(version.platforms, 'type'))) return false; - - // Check tag satisfies request version - return opts.tag == 'latest' || semver.satisfies(version.tag, opts.tag); - }) + .filter(filterBuilder(opts)) .value(); }); }; +Versions.prototype.newer = function (opts) { + return this.filter(opts, newVersionFilter); +}; + +Versions.prototype.select = function (opts) { + return this.filter(opts, allVersionFilter); +}; + // Resolve a platform, by filtering then taking the first result Versions.prototype.resolve = function(opts) { - return this.filter(opts) + opts = opts || {}; + opts.channel = opts.channel || 'stable'; + if (opts.tag === 'latest') opts.tag = null; + + return this.filter(opts, allVersionFilter) .then(function(versions) { var version = _.first(versions); if (!version) throw new Error('Version not found: '+opts.tag); diff --git a/package.json b/package.json index 3d1af7f9..fc4659e1 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ }, "devDependencies": { "mocha": "1.18.2", + "mockery": "^1.4.0", + "rewire": "^2.5.1", "should": "7.0.4" }, "bugs": { diff --git a/test/platforms.js b/test/platforms.js index 7048084e..ae285b92 100644 --- a/test/platforms.js +++ b/test/platforms.js @@ -13,6 +13,7 @@ describe('Platforms', function() { it('should detect windows_32', function() { platforms.detect('myapp-v0.25.1-win32-ia32.zip').should.be.exactly(platforms.WINDOWS_32); platforms.detect('atom-1.0.9-delta.nupkg').should.be.exactly(platforms.WINDOWS_32); + platforms.detect('TestSetup.msi').should.be.exactly(platforms.WINDOWS_32); platforms.detect('RELEASES').should.be.exactly(platforms.WINDOWS_32); }); @@ -78,6 +79,14 @@ describe('Platforms', function() { "content_type": "application/zip", "download_url": "https://api.github.com/repos/atom/atom/releases/assets/825728", "download_count": 5612 + }, + { + "type": "windows_32", + "filename": "TestSetup.msi", + "size": 78675700, + "content_type": "application/x-msi", + "download_url": "https://api.github.com/repos/test/test2/releases/assets/7938398", + "download_count": 1 } ] }; @@ -91,7 +100,11 @@ describe('Platforms', function() { it('should resolve to best platform with a preferred filetype', function() { platforms.resolve(version, 'osx', { filePreference: ['.zip'] - }).filename.should.be.exactly("test-3.3.1-darwin-x64.zip") + }).filename.should.be.exactly("test-3.3.1-darwin-x64.zip"), + + platforms.resolve(version, 'win32', { + filePreference: ['.msi'] + }).filename.should.be.exactly("TestSetup.msi") }); it('should resolve to best platform with a wanted filetype', function() { diff --git a/test/versions.js b/test/versions.js new file mode 100644 index 00000000..ce3dfbb2 --- /dev/null +++ b/test/versions.js @@ -0,0 +1,289 @@ +var + _ = require('lodash'), + Q = require('q'), + should = require('should'), + rewire = require('rewire'); + +describe('Versions', function() { + + var Versions = rewire('../lib/versions'); + + describe('when extracting a channel from a tag', function() { + + var mockGithubReleases = [ + { + expectations: { + channel: 'alpha' + }, + data: { + tag_name: '0.0.8-alpha.20151214.1', + name: '0.0.8-alpha.20151214.1', + assets: [ { + url: 'https://api.github.com/repos/skewart/hooktest/releases/assets/1120656', + name: 'AlphaTest-osx-0.0.6-alpha.20151211.1.zip', + content_type: 'application/x-msdownload', + size: 168493, + download_count: 0, + }, { + url: 'https://api.github.com/repos/skewart/hooktest/releases/assets/1120656', + name: 'AlphaTest-osx-0.0.6-alpha.20151211.1.dmg', + content_type: 'application/x-msdownload', + size: 168493, + download_count: 0, + }] + } + }, + { + expectations: { + channel: '3000' + }, + data: { + tag_name: '0.0.8-3000.20151214.1', + name: '0.0.8-3000.20151214.1', + assets: [ { + url: 'https://api.github.com/repos/skewart/hooktest/releases/assets/1120656', + name: 'AlphaTest-osx-0.0.6-3000.20151211.1.zip', + content_type: 'application/x-msdownload', + size: 168493, + download_count: 0, + }, { + url: 'https://api.github.com/repos/skewart/hooktest/releases/assets/1120656', + name: 'AlphaTest-osx-0.0.6-3000.20151211.1.dmg', + content_type: 'application/x-msdownload', + size: 168493, + download_count: 0, + }] + } + }, + { + expectations: { + channel: 'alpha' + }, + data: { + tag_name: '0.0.8-alpha201512101', + name: '0.0.8-alpha201512101', + assets: [ { + url: 'https://api.github.com/repos/skewart/hooktest/releases/assets/1120656', + name: 'AlphaTest-osx-0.0.6-alpha201512101.zip', + content_type: 'application/x-msdownload', + size: 168493, + download_count: 0, + }, { + url: 'https://api.github.com/repos/skewart/hooktest/releases/assets/1120656', + name: 'AlphaTest-osx-0.0.6-alpha201512101.dmg', + content_type: 'application/x-msdownload', + size: 168493, + download_count: 0, + }] + } + } + ] + + _.map(mockGithubReleases, function(release) { + + var versions = new Versions({ + releases: function() { + return Q.fcall(function() { + return [release.data]; + }); + } + }); + + var expectedChannel = release.expectations.channel, + tag = release.data.tag_name; + + it('should get channel ' + expectedChannel + ' from tag ' + tag, function(done) { + versions.list().then(function(versions) { + versions[0].channel.should.be.exactly(expectedChannel); + done(); + }); + }); + + }); + + }); + + describe('filtering', function() { + + function testVersionFilter(filterName, scenarios) { + describe('with ' + filterName, function() { + var filterFunction = Versions.__get__(filterName); + + scenarios.forEach(function(scenario) { + var opts = scenario.opts, + description = 'with opts = { tag: ' + opts.tag + ', channel: ' + opts.channel + + ', platform: ' + opts.platform + ' }, return -> [' + scenario.expectation.join(', ') + ']'; + + it(description, function() { + var result = _.chain(testVersions) + .filter(filterFunction(scenario.opts)) + .pluck('tag') + .value() + .join(', ') + .should.equal(scenario.expectation.join(', ')); + }); + }); + }); + } + + var testVersions = [ + { tag: '0.0.8-alpha.1', + channel: 'alpha', + notes: 'More placeholder text.', + published_at: 'Mon Dec 14 2015 12:13:58 GMT-0800 (PST)', + platforms: [{ + type: 'osx_64' + }, { + type: 'windows_32' + }], + download_count: 3 }, + { tag: '0.0.7', + channel: 'stable', + notes: 'Kinda drifted away from the story thing and I\'m just writing random stuff now.', + published_at: 'Mon Dec 14 2015 11:41:30 GMT-0800 (PST)', + platforms: [{ + type: 'osx_64' + }, { + type: 'windows_32' + }], + download_count: 0 }, + { tag: '0.0.7-beta.1', + channel: 'beta', + notes: 'A beautiful brand new release', + published_at: 'Fri Dec 11 2015 17:01:43 GMT-0800 (PST)', + platforms: [{ + type: 'osx_64' + }, { + type: 'windows_32' + }], + download_count: 0 }, + { tag: '0.0.7-alpha.2', + channel: 'alpha', + notes: 'More excitement!!!', + published_at: 'Fri Dec 11 2015 17:21:08 GMT-0800 (PST)', + platforms: [{ + type: 'osx_64' + }, { + type: 'windows_32' + }], + download_count: 0 }, + { tag: '0.0.7-alpha.1', + channel: 'alpha', + notes: 'A beautiful brand new release', + published_at: 'Fri Dec 11 2015 17:01:43 GMT-0800 (PST)', + platforms: [{ + type: 'windows_32' + }], + download_count: 0 }, + { tag: '0.0.6', + channel: 'stable', + notes: 'A wonderful new release!', + published_at: 'Fri Dec 11 2015 15:55:45 GMT-0800 (PST)', + platforms: [{ + type: 'osx_64' + }], + download_count: 2 } + ]; + + testVersionFilter('newVersionFilter', [ + { + opts: { + tag: '0.0.7', + platform: 'osx', + channel: undefined + }, + expectation: ['0.0.7'] + }, + { + opts: { + tag: '0.0.6', + platform: 'osx', + channel: undefined + }, + expectation: ['0.0.7', '0.0.6'] + }, + { + opts: { + tag: '0.0.7-beta.1', + platform: 'osx', + channel: undefined + }, + expectation: ['0.0.7', '0.0.7-beta.1'] + }, + { + opts: { + tag: '0.0.7-alpha.2', + platform: 'osx', + channel: 'alpha' + }, + expectation: ['0.0.8-alpha.1', '0.0.7-alpha.2'] + }, + { + opts: { + tag: '0.0.7-beta.1', + platform: 'osx', + channel: 'beta' + }, + expectation: ['0.0.7-beta.1'] + }, + { + opts: { + tag: '0.0.7-alpha.1', + platform: 'windows', + channel: 'alpha' + }, + expectation: ['0.0.8-alpha.1', '0.0.7-alpha.2', '0.0.7-alpha.1'] + } + ]); + + testVersionFilter('allVersionFilter', [ + { + opts: {}, + expectation: ['0.0.8-alpha.1', '0.0.7', '0.0.7-beta.1', '0.0.7-alpha.2', + '0.0.7-alpha.1', '0.0.6'] + }, + { + opts: { + platform: 'osx' + }, + expectation: ['0.0.8-alpha.1', '0.0.7', '0.0.7-beta.1', '0.0.7-alpha.2', + '0.0.6'] + }, + { + opts: { + platform: 'windows' + }, + expectation: ['0.0.8-alpha.1', '0.0.7', '0.0.7-beta.1', '0.0.7-alpha.2', + '0.0.7-alpha.1'] + }, + { + opts: { + channel: 'alpha' + }, + expectation: ['0.0.8-alpha.1', '0.0.7-alpha.2', '0.0.7-alpha.1'] + }, + { + opts: { + channel: '*' + }, + expectation: ['0.0.8-alpha.1', '0.0.7', '0.0.7-beta.1', '0.0.7-alpha.2', + '0.0.7-alpha.1', '0.0.6'] + }, + { + opts: { + channel: 'alpha', + platform: 'osx' + }, + expectation: ['0.0.8-alpha.1', '0.0.7-alpha.2'] + }, + { + opts: { + tag: '0.0.7-alpha.2' + }, + expectation: ['0.0.7-alpha.2'] + } + ]); + + }); + +});