Current File : //lib/node_modules/bower/lib/core/ResolveCache.js |
var fs = require('../util/fs');
var path = require('path');
var mout = require('mout');
var Q = require('q');
var mkdirp = require('mkdirp');
var rimraf = require('../util/rimraf');
var LRU = require('lru-cache');
var lockFile = require('lockfile');
var md5 = require('md5-hex');
var semver = require('../util/semver');
var readJson = require('../util/readJson');
var copy = require('../util/copy');
function ResolveCache(config) {
// TODO: Make some config entries, such as:
// - Max MB
// - Max versions per source
// - Max MB per source
// - etc..
this._config = config;
this._dir = this._config.storage.packages;
this._lockDir = this._config.storage.packages;
mkdirp.sync(this._lockDir);
// Cache is stored/retrieved statically to ensure singularity
// among instances
this._cache = this.constructor._cache.get(this._dir);
if (!this._cache) {
this._cache = new LRU({
max: 100,
maxAge: 60 * 5 * 1000 // 5 minutes
});
this.constructor._cache.set(this._dir, this._cache);
}
// Ensure dir is created
mkdirp.sync(this._dir);
}
// -----------------
ResolveCache.prototype.retrieve = function(source, target) {
var sourceId = md5(source);
var dir = path.join(this._dir, sourceId);
var that = this;
target = target || '*';
return this._getVersions(sourceId)
.spread(function(versions) {
var suitable;
// If target is a semver, find a suitable version
if (semver.validRange(target)) {
suitable = semver.maxSatisfying(versions, target, true);
if (suitable) {
return suitable;
}
}
// If target is '*' check if there's a cached '_wildcard'
if (target === '*') {
return mout.array.find(versions, function(version) {
return version === '_wildcard';
});
}
// Otherwise check if there's an exact match
return mout.array.find(versions, function(version) {
return version === target;
});
})
.then(function(version) {
var canonicalDir;
if (!version) {
return [];
}
// Resolve with canonical dir and package meta
canonicalDir = path.join(dir, encodeURIComponent(version));
return that._readPkgMeta(canonicalDir).then(
function(pkgMeta) {
return [canonicalDir, pkgMeta];
},
function() {
// If there was an error, invalidate the in-memory cache,
// delete the cached package and try again
that._cache.del(sourceId);
return Q.nfcall(rimraf, canonicalDir).then(function() {
return that.retrieve(source, target);
});
}
);
});
};
ResolveCache.prototype.store = function(canonicalDir, pkgMeta) {
var sourceId;
var release;
var dir;
var pkgLock;
var promise;
var that = this;
promise = pkgMeta ? Q.resolve(pkgMeta) : this._readPkgMeta(canonicalDir);
return promise
.then(function(pkgMeta) {
sourceId = md5(pkgMeta._source);
release = that._getPkgRelease(pkgMeta);
dir = path.join(that._dir, sourceId, release);
pkgLock = path.join(
that._lockDir,
sourceId + '-' + release + '.lock'
);
// Check if destination directory exists to prevent issuing lock at all times
return Q.nfcall(fs.stat, dir)
.fail(function(err) {
var lockParams = { wait: 250, retries: 25, stale: 60000 };
return Q.nfcall(lockFile.lock, pkgLock, lockParams)
.then(function() {
// Ensure other process didn't start copying files before lock was created
return Q.nfcall(fs.stat, dir).fail(function(err) {
// If stat fails, it is expected to return ENOENT
if (err.code !== 'ENOENT') {
throw err;
}
// Create missing directory and copy files there
return Q.nfcall(mkdirp, path.dirname(dir)).then(
function() {
return Q.nfcall(
fs.rename,
canonicalDir,
dir
).fail(function(err) {
// If error is EXDEV it means that we are trying to rename
// across different drives, so we copy and remove it instead
if (err.code !== 'EXDEV') {
throw err;
}
return copy.copyDir(
canonicalDir,
dir
);
});
}
);
});
})
.finally(function() {
lockFile.unlockSync(pkgLock);
});
})
.finally(function() {
// Ensure no tmp dir is left on disk.
return Q.nfcall(rimraf, canonicalDir);
});
})
.then(function() {
var versions = that._cache.get(sourceId);
// Add it to the in memory cache
// and sort the versions afterwards
if (versions && versions.indexOf(release) === -1) {
versions.push(release);
that._sortVersions(versions);
}
// Resolve with the final location
return dir;
});
};
ResolveCache.prototype.eliminate = function(pkgMeta) {
var sourceId = md5(pkgMeta._source);
var release = this._getPkgRelease(pkgMeta);
var dir = path.join(this._dir, sourceId, release);
var that = this;
return Q.nfcall(rimraf, dir).then(function() {
var versions = that._cache.get(sourceId) || [];
mout.array.remove(versions, release);
// If this was the last package in the cache,
// delete the parent folder (source)
// For extra security, check against the file system
// if this was really the last package
if (!versions.length) {
that._cache.del(sourceId);
return that._getVersions(sourceId).spread(function(versions) {
if (!versions.length) {
// Do not keep in-memory cache if it's completely
// empty
that._cache.del(sourceId);
return Q.nfcall(rimraf, path.dirname(dir));
}
});
}
});
};
ResolveCache.prototype.clear = function() {
return Q.nfcall(rimraf, this._dir)
.then(
function() {
return Q.nfcall(fs.mkdir, this._dir);
}.bind(this)
)
.then(
function() {
this._cache.reset();
}.bind(this)
);
};
ResolveCache.prototype.reset = function() {
this._cache.reset();
return this;
};
ResolveCache.prototype.versions = function(source) {
var sourceId = md5(source);
return this._getVersions(sourceId).spread(function(versions) {
return versions.filter(function(version) {
return semver.valid(version);
});
});
};
ResolveCache.prototype.list = function() {
var promises;
var dirs = [];
var that = this;
// Get the list of directories
return (
Q.nfcall(fs.readdir, this._dir)
.then(function(sourceIds) {
promises = sourceIds.map(function(sourceId) {
return Q.nfcall(
fs.readdir,
path.join(that._dir, sourceId)
).then(
function(versions) {
versions.forEach(function(version) {
var dir = path.join(
that._dir,
sourceId,
version
);
dirs.push(dir);
});
},
function(err) {
// Ignore lurking files, e.g.: .DS_Store if the user
// has navigated throughout the cache
if (err.code === 'ENOTDIR' && err.path) {
return Q.nfcall(rimraf, err.path);
}
throw err;
}
);
});
return Q.all(promises);
})
// Read every package meta
.then(function() {
promises = dirs.map(function(dir) {
return that._readPkgMeta(dir).then(
function(pkgMeta) {
return {
canonicalDir: dir,
pkgMeta: pkgMeta
};
},
function() {
// If it fails to read, invalidate the in memory
// cache for the source and delete the entry directory
var sourceId = path.basename(path.dirname(dir));
that._cache.del(sourceId);
return Q.nfcall(rimraf, dir);
}
);
});
return Q.all(promises);
})
// Sort by name ASC & release ASC
.then(function(entries) {
// Ignore falsy entries due to errors reading
// package metas
entries = entries.filter(function(entry) {
return !!entry;
});
return entries.sort(function(entry1, entry2) {
var pkgMeta1 = entry1.pkgMeta;
var pkgMeta2 = entry2.pkgMeta;
var comp = pkgMeta1.name.localeCompare(pkgMeta2.name);
// Sort by name
if (comp) {
return comp;
}
// Sort by version
if (pkgMeta1.version && pkgMeta2.version) {
return semver.compare(
pkgMeta1.version,
pkgMeta2.version
);
}
if (pkgMeta1.version) {
return -1;
}
if (pkgMeta2.version) {
return 1;
}
// Sort by target
return pkgMeta1._target.localeCompare(pkgMeta2._target);
});
})
);
};
// ------------------------
ResolveCache.clearRuntimeCache = function() {
// Note that _cache refers to the static _cache variable
// that holds other caches per dir!
// Do not confuse it with the instance cache
// Clear cache of each directory
this._cache.forEach(function(cache) {
cache.reset();
});
// Clear root cache
this._cache.reset();
};
// ------------------------
ResolveCache.prototype._getPkgRelease = function(pkgMeta) {
var release =
pkgMeta.version ||
(pkgMeta._target === '*' ? '_wildcard' : pkgMeta._target);
// Encode some dangerous chars such as / and \
release = encodeURIComponent(release);
return release;
};
ResolveCache.prototype._readPkgMeta = function(dir) {
var filename = path.join(dir, '.bower.json');
return readJson(filename).spread(function(json) {
return json;
});
};
ResolveCache.prototype._getVersions = function(sourceId) {
var dir;
var versions = this._cache.get(sourceId);
var that = this;
if (versions) {
return Q.resolve([versions, true]);
}
dir = path.join(this._dir, sourceId);
return Q.nfcall(fs.readdir, dir).then(
function(versions) {
// Sort and cache in memory
that._sortVersions(versions);
versions = versions.map(decodeURIComponent);
that._cache.set(sourceId, versions);
return [versions, false];
},
function(err) {
// If the directory does not exists, resolve
// as an empty array
if (err.code === 'ENOENT') {
versions = [];
that._cache.set(sourceId, versions);
return [versions, false];
}
throw err;
}
);
};
ResolveCache.prototype._sortVersions = function(versions) {
// Sort DESC
versions.sort(function(version1, version2) {
var validSemver1 = semver.valid(version1);
var validSemver2 = semver.valid(version2);
// If both are semvers, compare them
if (validSemver1 && validSemver2) {
return semver.rcompare(version1, version2);
}
// If one of them are semvers, give higher priority
if (validSemver1) {
return -1;
}
if (validSemver2) {
return 1;
}
// Otherwise they are considered equal
return 0;
});
};
// ------------------------
ResolveCache._cache = new LRU({
max: 5,
maxAge: 60 * 30 * 1000 // 30 minutes
});
module.exports = ResolveCache;