deps/npm/lib/cache/caching-client.js

Summary

Maintainability
C
1 day
Test Coverage
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")
  }
}