mirror of
https://github.com/bower/bower.git
synced 2026-04-24 03:00:19 -04:00
When we write into directory we use encodeURIComponent, but when we read, we did nothing. Now we use decodeURIComponent to read the correct version and match the correct cache directory.
397 lines
11 KiB
JavaScript
397 lines
11 KiB
JavaScript
var fs = require('graceful-fs');
|
|
var path = require('path');
|
|
var mout = require('mout');
|
|
var Q = require('q');
|
|
var mkdirp = require('mkdirp');
|
|
var rimraf = require('rimraf');
|
|
var LRU = require('lru-cache');
|
|
var semver = require('../util/semver');
|
|
var readJson = require('../util/readJson');
|
|
var copy = require('../util/copy');
|
|
var md5 = require('../util/md5');
|
|
|
|
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;
|
|
|
|
// 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 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);
|
|
|
|
// Check if directory exists
|
|
return Q.nfcall(fs.stat, dir)
|
|
.then(function () {
|
|
// If it does exists, remove it
|
|
return Q.nfcall(rimraf, dir);
|
|
}, function (err) {
|
|
// If directory does not exists, ensure its basename
|
|
// is created
|
|
if (err.code === 'ENOENT') {
|
|
return Q.nfcall(mkdirp, path.dirname(dir));
|
|
}
|
|
|
|
throw err;
|
|
})
|
|
// Move the canonical to sourceId/target
|
|
.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)
|
|
.then(function () {
|
|
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;
|