mirror of
https://github.com/bower/bower.git
synced 2026-01-14 08:47:54 -05:00
324 lines
9.5 KiB
JavaScript
324 lines
9.5 KiB
JavaScript
var util = require('util');
|
|
var path = require('path');
|
|
var fs = require('../../util/fs');
|
|
var url = require('url');
|
|
var request = require('request');
|
|
var Q = require('q');
|
|
var mout = require('mout');
|
|
var junk = require('junk');
|
|
var Resolver = require('./Resolver');
|
|
var download = require('../../util/download');
|
|
var extract = require('../../util/extract');
|
|
var createError = require('../../util/createError');
|
|
|
|
function UrlResolver(decEndpoint, config, logger) {
|
|
Resolver.call(this, decEndpoint, config, logger);
|
|
|
|
// If target was specified, error out
|
|
if (this._target !== '*') {
|
|
throw createError("URL sources can't resolve targets", 'ENORESTARGET');
|
|
}
|
|
|
|
// If the name was guessed
|
|
if (this._guessedName) {
|
|
// Remove the ?xxx part
|
|
this._name = this._name.replace(/\?.*$/, '');
|
|
// Remove extension
|
|
this._name = this._name.substr(
|
|
0,
|
|
this._name.length - path.extname(this._name).length
|
|
);
|
|
}
|
|
|
|
this._remote = url.parse(this._source);
|
|
}
|
|
|
|
util.inherits(UrlResolver, Resolver);
|
|
mout.object.mixIn(UrlResolver, Resolver);
|
|
|
|
// -----------------
|
|
|
|
UrlResolver.isTargetable = function() {
|
|
return false;
|
|
};
|
|
|
|
UrlResolver.prototype._hasNew = function(pkgMeta) {
|
|
var oldCacheHeaders = pkgMeta._cacheHeaders || {};
|
|
var reqHeaders = {};
|
|
|
|
// If the previous cache headers contain an ETag,
|
|
// send the "If-None-Match" header with it
|
|
if (oldCacheHeaders.ETag) {
|
|
reqHeaders['If-None-Match'] = oldCacheHeaders.ETag;
|
|
}
|
|
|
|
if (this._config.userAgent) {
|
|
reqHeaders['User-Agent'] = this._config.userAgent;
|
|
}
|
|
|
|
// Make an HEAD request to the source
|
|
return (
|
|
Q.nfcall(request.head, this._source, {
|
|
ca: this._config.ca.default,
|
|
strictSSL: this._config.strictSsl,
|
|
timeout: this._config.timeout,
|
|
headers: reqHeaders
|
|
})
|
|
// Compare new headers with the old ones
|
|
.spread(
|
|
function(response) {
|
|
var cacheHeaders;
|
|
|
|
// If the server responded with 303 then the resource
|
|
// still has the same ETag
|
|
if (response.statusCode === 304) {
|
|
return false;
|
|
}
|
|
|
|
// If status code is not in the 2xx range,
|
|
// then just resolve to true
|
|
if (
|
|
response.statusCode < 200 ||
|
|
response.statusCode >= 300
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
// Fallback to comparing cache headers
|
|
cacheHeaders = this._collectCacheHeaders(response);
|
|
return !mout.object.equals(oldCacheHeaders, cacheHeaders);
|
|
}.bind(this),
|
|
function() {
|
|
// Assume new contents if the request failed
|
|
// Note that we do not retry the request using the "request-replay" module
|
|
// because it would take too long
|
|
return true;
|
|
}
|
|
)
|
|
);
|
|
};
|
|
|
|
// TODO: There's room for improvement by using streams if the URL
|
|
// is an archive file, by piping read stream to the zip extractor
|
|
// This will likely increase the complexity of code but might worth it
|
|
|
|
UrlResolver.prototype._resolve = function() {
|
|
// Download
|
|
return (
|
|
this._download()
|
|
// Parse headers
|
|
.spread(this._parseHeaders.bind(this))
|
|
// Extract file
|
|
.spread(this._extract.bind(this))
|
|
// Rename file to index
|
|
.then(this._rename.bind(this))
|
|
);
|
|
};
|
|
|
|
// -----------------
|
|
|
|
UrlResolver.prototype._parseSourceURL = function(_url) {
|
|
return url.parse(path.basename(_url)).pathname;
|
|
};
|
|
|
|
UrlResolver.prototype._download = function() {
|
|
var fileName = this._parseSourceURL(this._source);
|
|
|
|
if (!fileName) {
|
|
this._source = this._source.replace(/\/(?=\?|#)/, '');
|
|
fileName = this._parseSourceURL(this._source);
|
|
}
|
|
|
|
var file = path.join(this._tempDir, fileName);
|
|
var reqHeaders = {};
|
|
var that = this;
|
|
|
|
if (this._config.userAgent) {
|
|
reqHeaders['User-Agent'] = this._config.userAgent;
|
|
}
|
|
|
|
this._logger.action('download', that._source, {
|
|
url: that._source,
|
|
to: file
|
|
});
|
|
|
|
// Download the file
|
|
return download(this._source, file, {
|
|
ca: this._config.ca.default,
|
|
strictSSL: this._config.strictSsl,
|
|
timeout: this._config.timeout,
|
|
headers: reqHeaders
|
|
})
|
|
.progress(function(state) {
|
|
var msg;
|
|
|
|
// Retry?
|
|
if (state.retry) {
|
|
msg =
|
|
'Download of ' +
|
|
that._source +
|
|
' failed' +
|
|
(state.error.code ? ' with ' + state.error.code : '') +
|
|
', ';
|
|
msg += 'retrying in ' + (state.delay / 1000).toFixed(1) + 's';
|
|
that._logger.debug('error', state.error.message, {
|
|
error: state.error
|
|
});
|
|
return that._logger.warn('retry', msg);
|
|
}
|
|
|
|
// Progress
|
|
msg =
|
|
'received ' + (state.received / 1024 / 1024).toFixed(1) + 'MB';
|
|
if (state.total) {
|
|
msg +=
|
|
' of ' +
|
|
(state.total / 1024 / 1024).toFixed(1) +
|
|
'MB downloaded, ';
|
|
msg += state.percent + '%';
|
|
}
|
|
that._logger.info('progress', msg);
|
|
})
|
|
.then(function(response) {
|
|
that._response = response;
|
|
return [file, response];
|
|
});
|
|
};
|
|
|
|
UrlResolver.prototype._parseHeaders = function(file, response) {
|
|
var disposition;
|
|
var newFile;
|
|
var match;
|
|
|
|
// Check if we got a Content-Disposition header
|
|
disposition = response.headers['content-disposition'];
|
|
if (!disposition) {
|
|
return Q.resolve([file, response]);
|
|
}
|
|
|
|
// Since there's various security issues with parsing this header, we only
|
|
// interpret word chars plus dots, dashes and spaces
|
|
match = disposition.match(/filename=(?:"([\w\-\. ]+)")/i);
|
|
if (!match) {
|
|
// The spec defines that the filename must be in quotes,
|
|
// though a wide range of servers do not follow the rule
|
|
match = disposition.match(/filename=([\w\-\.]+)/i);
|
|
if (!match) {
|
|
return Q.resolve([file, response]);
|
|
}
|
|
}
|
|
|
|
// Trim spaces
|
|
newFile = match[1].trim();
|
|
|
|
// The filename can't end with a dot because this is known
|
|
// to cause issues in Windows
|
|
// See: http://superuser.com/questions/230385/dots-at-end-of-file-name
|
|
if (mout.string.endsWith(newFile, '.')) {
|
|
return Q.resolve([file, response]);
|
|
}
|
|
|
|
newFile = path.join(this._tempDir, newFile);
|
|
|
|
return Q.nfcall(fs.rename, file, newFile).then(function() {
|
|
return [newFile, response];
|
|
});
|
|
};
|
|
|
|
UrlResolver.prototype._extract = function(file, response) {
|
|
var mimeType = response.headers['content-type'];
|
|
|
|
if (mimeType) {
|
|
// Clean everything after ; and trim the end result
|
|
mimeType = mimeType.split(';')[0].trim();
|
|
// Some servers add quotes around the content-type, so we trim that also
|
|
mimeType = mout.string.trim(mimeType, ['"', "'"]);
|
|
}
|
|
|
|
if (!extract.canExtract(file, mimeType)) {
|
|
return Q.resolve();
|
|
}
|
|
|
|
this._logger.action('extract', path.basename(this._source), {
|
|
archive: file,
|
|
to: this._tempDir
|
|
});
|
|
|
|
return extract(file, this._tempDir, {
|
|
mimeType: mimeType
|
|
});
|
|
};
|
|
|
|
UrlResolver.prototype._rename = function() {
|
|
return Q.nfcall(fs.readdir, this._tempDir).then(
|
|
function(files) {
|
|
var file;
|
|
var oldPath;
|
|
var newPath;
|
|
|
|
// Remove any OS specific files from the files array
|
|
// before checking its length
|
|
files = files.filter(junk.isnt);
|
|
|
|
// Only rename if there's only one file and it's not the json
|
|
if (
|
|
files.length === 1 &&
|
|
!/^(component|bower)\.json$/.test(files[0])
|
|
) {
|
|
file = files[0];
|
|
this._singleFile = 'index' + path.extname(file);
|
|
oldPath = path.join(this._tempDir, file);
|
|
newPath = path.join(this._tempDir, this._singleFile);
|
|
|
|
return Q.nfcall(fs.rename, oldPath, newPath);
|
|
}
|
|
}.bind(this)
|
|
);
|
|
};
|
|
|
|
UrlResolver.prototype._savePkgMeta = function(meta) {
|
|
// Store collected headers in the package meta
|
|
meta._cacheHeaders = this._collectCacheHeaders(this._response);
|
|
|
|
// Store ETAG under _release
|
|
if (meta._cacheHeaders.ETag) {
|
|
meta._release =
|
|
'e-tag:' +
|
|
mout.string.trim(meta._cacheHeaders.ETag.substr(0, 10), '"');
|
|
}
|
|
|
|
// Store main if is a single file
|
|
if (this._singleFile) {
|
|
meta.main = this._singleFile;
|
|
}
|
|
|
|
return Resolver.prototype._savePkgMeta.call(this, meta);
|
|
};
|
|
|
|
UrlResolver.prototype._collectCacheHeaders = function(res) {
|
|
var headers = {};
|
|
|
|
// Collect cache headers
|
|
this.constructor._cacheHeaders.forEach(function(name) {
|
|
var value = res.headers[name.toLowerCase()];
|
|
|
|
if (value != null) {
|
|
headers[name] = value;
|
|
}
|
|
});
|
|
|
|
return headers;
|
|
};
|
|
|
|
UrlResolver._cacheHeaders = [
|
|
'Content-MD5',
|
|
'ETag',
|
|
'Last-Modified',
|
|
'Content-Language',
|
|
'Content-Length',
|
|
'Content-Type',
|
|
'Content-Disposition'
|
|
];
|
|
|
|
module.exports = UrlResolver;
|