|
|
|
|
|
|
|
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)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|