module.exports = CachingRegistryClient var path = require('path') var fs = require('graceful-fs') var url = require('url') var assert = require('assert') var inherits = require('util').inherits var RegistryClient = require('npm-registry-client') var npm = require('../npm.js') var log = require('npmlog') var getCacheStat = require('./get-stat.js') var cacheFile = require('npm-cache-filename') var mkdirp = require('mkdirp') var rimraf = require('rimraf') var chownr = require('chownr') var 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 var timeout = params.timeout === undefined ? -1 : params.timeout var data = params.data var stat = params.stat var etag var 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') } }