mirror of
https://github.com/bower/bower.git
synced 2026-01-13 08:17:58 -05:00
441 lines
14 KiB
JavaScript
441 lines
14 KiB
JavaScript
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;
|