--- /dev/null
+module.exports = CachingRegistryClient
+
+var path = require("path")
+ , fs = require("graceful-fs")
+ , url = require("url")
+ , assert = require("assert")
+ , inherits = require("util").inherits
+
+var RegistryClient = require("npm-registry-client")
+ , npm = require("../npm.js")
+ , log = require("npmlog")
+ , getCacheStat = require("./get-stat.js")
+ , cacheFile = require("npm-cache-filename")
+ , mkdirp = require("mkdirp")
+ , rimraf = require("rimraf")
+ , chownr = require("chownr")
+ , writeFile = require("write-file-atomic")
+
+function CachingRegistryClient (config) {
+ RegistryClient.call(this, adaptConfig(config))
+
+ this._mapToCache = cacheFile(config.get("cache"))
+
+ // swizzle in our custom cache invalidation logic
+ this._request = this.request
+ this.request = this._invalidatingRequest
+ this.get = get
+}
+inherits(CachingRegistryClient, RegistryClient)
+
+CachingRegistryClient.prototype._invalidatingRequest = function (uri, params, cb) {
+ var client = this
+ this._request.call(this, uri, params, function () {
+ var args = arguments
+
+ var method = params.method
+ if (method !== "HEAD" && method !== "GET") {
+ var invalidated = client._mapToCache(uri)
+ // invalidate cache
+ //
+ // This is irrelevant for commands that do etag / last-modified caching,
+ // but ls and view also have a timed cache, so this keeps the user from
+ // thinking that it didn't work when it did.
+ // Note that failure is an acceptable option here, since the only
+ // result will be a stale cache for some helper commands.
+ log.verbose("request", "invalidating", invalidated, "on", method)
+ return rimraf(invalidated, function () {
+ cb.apply(undefined, args)
+ })
+ }
+
+ cb.apply(undefined, args)
+ })
+}
+
+function get (uri, params, cb) {
+ assert(typeof uri === "string", "must pass registry URI to get")
+ assert(params && typeof params === "object", "must pass params to get")
+ assert(typeof cb === "function", "must pass callback to get")
+
+ var parsed = url.parse(uri)
+ assert(
+ parsed.protocol === "http:" || parsed.protocol === "https:",
+ "must have a URL that starts with http: or https:"
+ )
+
+ var cacheBase = cacheFile(npm.config.get("cache"))(uri)
+ var cachePath = path.join(cacheBase, ".cache.json")
+
+ // If the GET is part of a write operation (PUT or DELETE), then
+ // skip past the cache entirely, but still save the results.
+ if (uri.match(/\?write=true$/)) {
+ log.verbose("get", "GET as part of write; not caching result")
+ return get_.call(this, uri, cachePath, params, cb)
+ }
+
+ var client = this
+ fs.stat(cachePath, function (er, stat) {
+ if (!er) {
+ fs.readFile(cachePath, function (er, data) {
+ try {
+ data = JSON.parse(data)
+ }
+ catch (ex) {
+ data = null
+ }
+
+ params.stat = stat
+ params.data = data
+
+ get_.call(client, uri, cachePath, params, cb)
+ })
+ }
+ else {
+ get_.call(client, uri, cachePath, params, cb)
+ }
+ })
+}
+
+function get_ (uri, cachePath, params, cb) {
+ var staleOk = params.staleOk === undefined ? false : params.staleOk
+ , timeout = params.timeout === undefined ? -1 : params.timeout
+ , data = params.data
+ , stat = params.stat
+ , etag
+ , lastModified
+
+ timeout = Math.min(timeout, npm.config.get("cache-max") || 0)
+ timeout = Math.max(timeout, npm.config.get("cache-min") || -Infinity)
+ if (process.env.COMP_CWORD !== undefined &&
+ process.env.COMP_LINE !== undefined &&
+ process.env.COMP_POINT !== undefined) {
+ timeout = Math.max(timeout, 60000)
+ }
+
+ if (data) {
+ if (data._etag) etag = data._etag
+ if (data._lastModified) lastModified = data._lastModified
+
+ if (stat && timeout && timeout > 0) {
+ if ((Date.now() - stat.mtime.getTime())/1000 < timeout) {
+ log.verbose("get", uri, "not expired, no request")
+ delete data._etag
+ delete data._lastModified
+ return cb(null, data, JSON.stringify(data), { statusCode : 304 })
+ }
+
+ if (staleOk) {
+ log.verbose("get", uri, "staleOk, background update")
+ delete data._etag
+ delete data._lastModified
+ process.nextTick(
+ cb.bind(null, null, data, JSON.stringify(data), { statusCode : 304 } )
+ )
+ cb = function () {}
+ }
+ }
+ }
+
+ var options = {
+ etag : etag,
+ lastModified : lastModified,
+ follow : params.follow,
+ auth : params.auth
+ }
+ this.request(uri, options, function (er, remoteData, raw, response) {
+ // if we get an error talking to the registry, but we have it
+ // from the cache, then just pretend we got it.
+ if (er && cachePath && data && !data.error) {
+ er = null
+ response = { statusCode: 304 }
+ }
+
+ if (response) {
+ log.silly("get", "cb", [response.statusCode, response.headers])
+ if (response.statusCode === 304 && (etag || lastModified)) {
+ remoteData = data
+ log.verbose(etag ? "etag" : "lastModified", uri+" from cache")
+ }
+ }
+
+ data = remoteData
+ if (!data) er = er || new Error("failed to fetch from registry: " + uri)
+
+ if (er) return cb(er, data, raw, response)
+
+ saveToCache(cachePath, data, saved)
+
+ // just give the write the old college try. if it fails, whatever.
+ function saved () {
+ delete data._etag
+ delete data._lastModified
+ cb(er, data, raw, response)
+ }
+
+ function saveToCache (cachePath, data, saved) {
+ log.verbose("get", "saving", data.name, "to", cachePath)
+ getCacheStat(function (er, st) {
+ mkdirp(path.dirname(cachePath), function (er, made) {
+ if (er) return saved()
+
+ writeFile(cachePath, JSON.stringify(data), function (er) {
+ if (er) return saved()
+
+ chownr(made || cachePath, st.uid, st.gid, saved)
+ })
+ })
+ })
+ }
+ })
+}
+
+function adaptConfig (config) {
+ return {
+ proxy : {
+ http : config.get("proxy"),
+ https : config.get("https-proxy"),
+ localAddress : config.get("local-address")
+ },
+ ssl : {
+ certificate : config.get("cert"),
+ key : config.get("key"),
+ ca : config.get("ca"),
+ strict : config.get("strict-ssl")
+ },
+ retry : {
+ retries : config.get("fetch-retries"),
+ factor : config.get("fetch-retry-factor"),
+ minTimeout : config.get("fetch-retry-mintimeout"),
+ maxTimeout : config.get("fetch-retry-maxtimeout")
+ },
+ userAgent : config.get("user-agent"),
+ log : log,
+ defaultTag : config.get("tag"),
+ couchToken : config.get("_token"),
+ maxSockets : config.get('maxsockets')
+ }
+}