module.exports = get var fs = require("graceful-fs") , assert = require("assert") , path = require("path") , mkdir = require("mkdirp") , chownr = require("chownr") , url = require("url") /** * options: * * timeout: request timeouts * follow: follow redirects * staleOk: stale results are OK * stat: results of checking for cached metadata * data: the cached metadata */ function get (uri, options, cb) { assert(uri, "must have URL to fetch") assert(cb, "must have callback") if (!options) options = {} var parsed = url.parse(uri) assert(parsed.protocol, "must have a URL that starts with npm:, http:, or https:") var cache = this.cacheFile(uri) + "/.cache.json" // /-/all is special. // It uses timestamp-based caching and partial updates, // because it is a monster. if (parsed.pathname === "/-/all") { return requestAll.call(this, uri, cache, cb) } // 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$/)) { return get_.call(this, uri, cache, options, cb) } fs.stat(cache, function (er, stat) { if (!er) fs.readFile(cache, function (er, data) { try { data = JSON.parse(data) } catch (ex) { data = null } options.stat = stat options.data = data get_.call(this, uri, cache, options, cb) }.bind(this)) else { get_.call(this, uri, cache, options, cb) } }.bind(this)) } function requestAll (uri, cache, cb) { this.log.info("get", cache) mkdir(path.dirname(cache), function (er) { if (er) return cb(er) fs.readFile(cache, function (er, data) { if (er) return requestAll_.call(this, uri, 0, {}, cache, cb) try { data = JSON.parse(data) } catch (ex) { fs.writeFile(cache, "{}", function (er) { if (er) return cb(new Error("Broken cache.")) return requestAll_.call(this, uri, 0, {}, cache, cb) }.bind(this)) } var t = +data._updated || 0 requestAll_.call(this, uri, t, data, cache, cb) }.bind(this)) }.bind(this)) } function requestAll_ (uri, c, data, cache, cb) { // use the cache and update in the background if it's not too old if (Date.now() - c < 60000) { cb(null, data) cb = function () {} } if (c === 0) { this.log.warn("", "Building the local index for the first time, please be patient") uri = url.resolve(uri, "/-/all") } else { uri = url.resolve(uri, "/-/all/since?stale=update_after&startkey=" + c) } this.request('GET', uri, null, function (er, updates, _, res) { if (er) return cb(er, data) var headers = res.headers , updated = updates._updated || Date.parse(headers.date) Object.keys(updates).forEach(function (p) { data[p] = updates[p] }) data._updated = updated fs.writeFile( cache, JSON.stringify(data) , function (er) { delete data._updated return cb(er, data) }) }) } function get_ (uri, cache, options, cb) { var staleOk = options.staleOk === undefined ? false : options.staleOk , follow = options.follow , data = options.data , stat = options.stat , etag var timeout = options.timeout === undefined ? -1 : options.timeout timeout = Math.min(timeout, this.conf.get('cache-max') || 0) timeout = Math.max(timeout, this.conf.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 && data._etag) etag = data._etag if (timeout && timeout > 0 && options.stat && options.data) { if ((Date.now() - stat.mtime.getTime())/1000 < timeout) { this.log.verbose("registry.get", uri, "not expired, no request") delete data._etag return cb(null, data, JSON.stringify(data), {statusCode:304}) } if (staleOk) { this.log.verbose("registry.get", uri, "staleOk, background update") delete data._etag process.nextTick(cb.bind( null, null, data, JSON.stringify(data) , {statusCode: 304} )) cb = function () {} } } this.request('GET', uri, { etag : etag, follow : follow }, 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 && cache && data && !data.error) { er = null response = {statusCode: 304} } if (response) { this.log.silly("registry.get", "cb", [response.statusCode, response.headers]) if (response.statusCode === 304 && etag) { remoteData = data this.log.verbose("etag", 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) // just give the write the old college try. if it fails, whatever. function saved () { delete data._etag cb(er, data, raw, response) } saveToCache.call(this, cache, data, saved) }.bind(this)) } function saveToCache (cache, data, saved) { if (this._cacheStat) { var cs = this._cacheStat return saveToCache_.call(this, cache, data, cs.uid, cs.gid, saved) } fs.stat(this.conf.get('cache'), function (er, st) { if (er) { return fs.stat(process.env.HOME || "", function (er, st) { // if this fails, oh well. if (er) return saved() this._cacheStat = st return saveToCache.call(this, cache, data, saved) }.bind(this)) } this._cacheStat = st || { uid: null, gid: null } return saveToCache.call(this, cache, data, saved) }.bind(this)) } function saveToCache_ (cache, data, uid, gid, saved) { mkdir(path.dirname(cache), function (er, made) { if (er) return saved() fs.writeFile(cache, JSON.stringify(data), function (er) { if (er || uid === null || gid === null) { return saved() } chownr(made || cache, uid, gid, saved) }) }) }