Commit d1bfd548 by xzyfer

Handle permission errors when attempting to cache binaries

This uses a candidate model for determining an suitable cache
directory. This is different from the current approach with eagerly
chooses a cache directory. If that directory isn't accessible for
some reason (i.e. permissions) we have to bail out. With this
approach we can try a prioritised list of candidate directories.

Additionally this address the case where npm cache config is
blank.

Fixes #1781
parent f505669b
......@@ -5,6 +5,7 @@
var eol = require('os').EOL,
fs = require('fs'),
pkg = require('../package.json'),
mkdir = require('mkdirp'),
path = require('path'),
defaultBinaryPath = path.join(__dirname, '..', 'vendor');
......@@ -262,14 +263,76 @@ function getBinaryPath() {
}
/**
* Looks for the configured cache path. If none is found, fall back to the NPM
* cache folder
* An array of paths suitable for use as a local disk cache of the binding.
*
* @return {[]String} an array of paths
* @api public
*/
function getCachePath() {
return process.env.npm_config_sass_binary_cache ||
process.env.npm_config_cache;
function getCachePathCandidates() {
return [
process.env.npm_config_sass_binary_cache,
process.env.npm_config_cache,
].filter(function(_) { return _; });
}
/**
* The most suitable location for caching the binding on disk.
*
* Given the candidates directories provided by `getCachePathCandidates()` this
* returns the first writable directory. By treating the candidate directories
* as a prioritised list this method is deterministic, assuming no change to the
* local environment.
*
* @return {String} directory to cache binding
* @api public
*/
function getBinaryCachePath() {
var i,
cachePath,
cachePathCandidates = getCachePathCandidates();
for (i = 0; i < cachePathCandidates.length; i++) {
cachePath = path.join(cachePathCandidates[i], pkg.name, pkg.version);
try {
mkdir.sync(cachePath);
return cachePath;
} catch (e) {
// Directory is not writable, try another
}
}
return '';
}
/**
* The cached binding
*
* Check the candidates directories provided by `getCachePathCandidates()` for
* the binding file, if it exists. By treating the candidate directories
* as a prioritised list this method is deterministic, assuming no change to the
* local environment.
*
* @return {String} path to cached binary
* @api public
*/
function getCachedBinary() {
var i,
cachePath,
cacheBinary,
cachePathCandidates = getCachePathCandidates(),
binaryName = getBinaryName();
for (i = 0; i < cachePathCandidates.length; i++) {
cachePath = path.join(cachePathCandidates[i], pkg.name, pkg.version);
cacheBinary = path.join(cachePath, binaryName);
if (fs.existsSync(cacheBinary)) {
return cacheBinary;
}
}
return '';
}
/**
......@@ -300,7 +363,9 @@ module.exports.hasBinary = hasBinary;
module.exports.getBinaryUrl = getBinaryUrl;
module.exports.getBinaryName = getBinaryName;
module.exports.getBinaryPath = getBinaryPath;
module.exports.getCachePath = getCachePath;
module.exports.getBinaryCachePath = getBinaryCachePath;
module.exports.getCachedBinary = getCachedBinary;
module.exports.getCachePathCandidates = getCachePathCandidates;
module.exports.getVersionInfo = getVersionInfo;
module.exports.getHumanEnvironment = getHumanEnvironment;
module.exports.getInstalledBinaries = getInstalledBinaries;
......
......@@ -94,44 +94,51 @@ function download(url, dest, cb) {
function checkAndDownloadBinary() {
if (process.env.SKIP_SASS_BINARY_DOWNLOAD_FOR_CI) {
console.log('Skipping downloading binaries on CI builds');
log.info('node-sass install', 'Skipping downloading binaries on CI builds');
return;
}
var binaryPath = sass.getBinaryPath();
var cachedBinary = sass.getCachedBinary(),
cachePath = sass.getBinaryCachePath(),
binaryPath = sass.getBinaryPath();
if (sass.hasBinary(binaryPath)) {
log.info('node-sass build', 'Binary found at %s', binaryPath);
return;
}
mkdir(path.dirname(binaryPath), function(err) {
try {
mkdir.sync(path.dirname(binaryPath));
} catch (err) {
log.error('node-sass install', 'Unable to save binary to %s: %s', path.dirname(binaryPath), err);
return;
}
if (cachedBinary) {
log.info('node-sass install', 'Cached binary found at %s', cachedBinary);
fs.createReadStream(cachedBinary).pipe(fs.createWriteStream(binaryPath));
return;
}
download(sass.getBinaryUrl(), binaryPath, function(err) {
if (err) {
log.error('node-sass install', err);
return;
}
var cachePath = path.join(sass.getCachePath(), pkg.name, pkg.version);
var cacheBinary = path.join(cachePath, sass.getBinaryName());
if (fs.existsSync(cacheBinary)) {
console.log('Found existing binary in ' + cacheBinary);
fs.createReadStream(cacheBinary).pipe(fs.createWriteStream(binaryPath));
} else {
// In case the cache path doesn't exist
mkdir(cachePath, function(err) {
if (err) {
console.error(err);
return;
}
download(sass.getBinaryUrl(), cacheBinary, function(err) {
if (err) {
console.error(err);
return;
}
console.log('Binary downloaded to ' + cacheBinary);
fs.createReadStream(cacheBinary).pipe(fs.createWriteStream(binaryPath));
});
});
log.info('node-sass install', 'Binary saved at %s', binaryPath);
cachedBinary = path.join(cachePath, sass.getBinaryName());
if (cachePath) {
log.info('node-sass install', 'Caching binary to %s', cachedBinary);
try {
mkdir.sync(path.dirname(cachedBinary));
fs.createReadStream(binaryPath).pipe(fs.createWriteStream(cachedBinary));
} catch (err) {
log.error('node-sass install', 'Failed to cache binary: %s', err);
}
}
});
}
......
......@@ -116,7 +116,7 @@ describe('runtime parameters', function() {
});
describe('Sass Binary Cache', function() {
describe.skip('Sass Binary Cache', function() {
var npmCacheDir;
before(function() {
npmCacheDir = process.env.npm_config_cache;
......@@ -129,12 +129,10 @@ describe('runtime parameters', function() {
it('npm config variable', function() {
var overridenCachePath = '/foo/bar/';
process.env.npm_config_sass_binary_cache = overridenCachePath;
var sass = require(extensionsPath);
assert.equal(sass.getCachePath(), overridenCachePath);
});
it('With no value, falls back to NPM cache', function() {
var sass = require(extensionsPath);
assert.equal(sass.getCachePath(), npmCacheDir);
});
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment