1 module.exports = CachingRegistryClient
3 var path = require("path")
4 , fs = require("graceful-fs")
6 , assert = require("assert")
7 , inherits = require("util").inherits
9 var RegistryClient = require("npm-registry-client")
10 , npm = require("../npm.js")
11 , log = require("npmlog")
12 , getCacheStat = require("./get-stat.js")
13 , cacheFile = require("npm-cache-filename")
14 , mkdirp = require("mkdirp")
15 , rimraf = require("rimraf")
16 , chownr = require("chownr")
17 , writeFile = require("write-file-atomic")
19 function CachingRegistryClient (config) {
20 RegistryClient.call(this, adaptConfig(config))
22 this._mapToCache = cacheFile(config.get("cache"))
24 // swizzle in our custom cache invalidation logic
25 this._request = this.request
26 this.request = this._invalidatingRequest
29 inherits(CachingRegistryClient, RegistryClient)
31 CachingRegistryClient.prototype._invalidatingRequest = function (uri, params, cb) {
33 this._request.call(this, uri, params, function () {
36 var method = params.method
37 if (method !== "HEAD" && method !== "GET") {
38 var invalidated = client._mapToCache(uri)
41 // This is irrelevant for commands that do etag / last-modified caching,
42 // but ls and view also have a timed cache, so this keeps the user from
43 // thinking that it didn't work when it did.
44 // Note that failure is an acceptable option here, since the only
45 // result will be a stale cache for some helper commands.
46 log.verbose("request", "invalidating", invalidated, "on", method)
47 return rimraf(invalidated, function () {
48 cb.apply(undefined, args)
52 cb.apply(undefined, args)
56 function get (uri, params, cb) {
57 assert(typeof uri === "string", "must pass registry URI to get")
58 assert(params && typeof params === "object", "must pass params to get")
59 assert(typeof cb === "function", "must pass callback to get")
61 var parsed = url.parse(uri)
63 parsed.protocol === "http:" || parsed.protocol === "https:",
64 "must have a URL that starts with http: or https:"
67 var cacheBase = cacheFile(npm.config.get("cache"))(uri)
68 var cachePath = path.join(cacheBase, ".cache.json")
70 // If the GET is part of a write operation (PUT or DELETE), then
71 // skip past the cache entirely, but still save the results.
72 if (uri.match(/\?write=true$/)) {
73 log.verbose("get", "GET as part of write; not caching result")
74 return get_.call(this, uri, cachePath, params, cb)
78 fs.stat(cachePath, function (er, stat) {
80 fs.readFile(cachePath, function (er, data) {
82 data = JSON.parse(data)
91 get_.call(client, uri, cachePath, params, cb)
95 get_.call(client, uri, cachePath, params, cb)
100 function get_ (uri, cachePath, params, cb) {
101 var staleOk = params.staleOk === undefined ? false : params.staleOk
102 , timeout = params.timeout === undefined ? -1 : params.timeout
108 timeout = Math.min(timeout, npm.config.get("cache-max") || 0)
109 timeout = Math.max(timeout, npm.config.get("cache-min") || -Infinity)
110 if (process.env.COMP_CWORD !== undefined &&
111 process.env.COMP_LINE !== undefined &&
112 process.env.COMP_POINT !== undefined) {
113 timeout = Math.max(timeout, 60000)
117 if (data._etag) etag = data._etag
118 if (data._lastModified) lastModified = data._lastModified
120 if (stat && timeout && timeout > 0) {
121 if ((Date.now() - stat.mtime.getTime())/1000 < timeout) {
122 log.verbose("get", uri, "not expired, no request")
124 delete data._lastModified
125 return cb(null, data, JSON.stringify(data), { statusCode : 304 })
129 log.verbose("get", uri, "staleOk, background update")
131 delete data._lastModified
133 cb.bind(null, null, data, JSON.stringify(data), { statusCode : 304 } )
142 lastModified : lastModified,
143 follow : params.follow,
146 this.request(uri, options, function (er, remoteData, raw, response) {
147 // if we get an error talking to the registry, but we have it
148 // from the cache, then just pretend we got it.
149 if (er && cachePath && data && !data.error) {
151 response = { statusCode: 304 }
155 log.silly("get", "cb", [response.statusCode, response.headers])
156 if (response.statusCode === 304 && (etag || lastModified)) {
158 log.verbose(etag ? "etag" : "lastModified", uri+" from cache")
163 if (!data) er = er || new Error("failed to fetch from registry: " + uri)
165 if (er) return cb(er, data, raw, response)
167 saveToCache(cachePath, data, saved)
169 // just give the write the old college try. if it fails, whatever.
172 delete data._lastModified
173 cb(er, data, raw, response)
176 function saveToCache (cachePath, data, saved) {
177 log.verbose("get", "saving", data.name, "to", cachePath)
178 getCacheStat(function (er, st) {
179 mkdirp(path.dirname(cachePath), function (er, made) {
180 if (er) return saved()
182 writeFile(cachePath, JSON.stringify(data), function (er) {
183 if (er) return saved()
185 chownr(made || cachePath, st.uid, st.gid, saved)
193 function adaptConfig (config) {
196 http : config.get("proxy"),
197 https : config.get("https-proxy"),
198 localAddress : config.get("local-address")
201 certificate : config.get("cert"),
202 key : config.get("key"),
203 ca : config.get("ca"),
204 strict : config.get("strict-ssl")
207 retries : config.get("fetch-retries"),
208 factor : config.get("fetch-retry-factor"),
209 minTimeout : config.get("fetch-retry-mintimeout"),
210 maxTimeout : config.get("fetch-retry-maxtimeout")
212 userAgent : config.get("user-agent"),
214 defaultTag : config.get("tag"),
215 couchToken : config.get("_token"),
216 maxSockets : config.get('maxsockets')