blob: 7656b5d9754a3ad5151e0d796e79b350e65935fe [file] [log] [blame]
// commands for packing and unpacking tarballs
// this file is used by lib/cache.js
var npm = require("../npm.js")
, fs = require("graceful-fs")
, writeFileAtomic = require("write-file-atomic")
, writeStreamAtomic = require("fs-write-stream-atomic")
, path = require("path")
, log = require("npmlog")
, uidNumber = require("uid-number")
, rm = require("./gently-rm.js")
, readJson = require("read-package-json")
, myUid = process.getuid && process.getuid()
, myGid = process.getgid && process.getgid()
, tar = require("tar")
, zlib = require("zlib")
, fstream = require("fstream")
, Packer = require("fstream-npm")
, lifecycle = require("./lifecycle.js")
if (process.env.SUDO_UID && myUid === 0) {
if (!isNaN(process.env.SUDO_UID)) myUid = +process.env.SUDO_UID
if (!isNaN(process.env.SUDO_GID)) myGid = +process.env.SUDO_GID
}
exports.pack = pack
exports.unpack = unpack
function pack (tarball, folder, pkg, dfc, cb) {
log.verbose("tar pack", [tarball, folder])
if (typeof cb !== "function") cb = dfc, dfc = false
log.verbose("tarball", tarball)
log.verbose("folder", folder)
if (dfc) {
// do fancy crap
return lifecycle(pkg, "prepublish", folder, function (er) {
if (er) return cb(er)
pack_(tarball, folder, pkg, cb)
})
} else {
pack_(tarball, folder, pkg, cb)
}
}
function pack_ (tarball, folder, pkg, cb) {
new Packer({ path: folder, type: "Directory", isDirectory: true })
.on("error", function (er) {
if (er) log.error("tar pack", "Error reading " + folder)
return cb(er)
})
// By default, npm includes some proprietary attributes in the
// package tarball. This is sane, and allowed by the spec.
// However, npm *itself* excludes these from its own package,
// so that it can be more easily bootstrapped using old and
// non-compliant tar implementations.
.pipe(tar.Pack({ noProprietary: !npm.config.get("proprietary-attribs") }))
.on("error", function (er) {
if (er) log.error("tar.pack", "tar creation error", tarball)
cb(er)
})
.pipe(zlib.Gzip())
.on("error", function (er) {
if (er) log.error("tar.pack", "gzip error "+tarball)
cb(er)
})
.pipe(writeStreamAtomic(tarball))
.on("error", function (er) {
if (er) log.error("tar.pack", "Could not write "+tarball)
cb(er)
})
.on("close", cb)
}
function unpack (tarball, unpackTarget, dMode, fMode, uid, gid, cb) {
log.verbose("tar", "unpack", tarball)
log.verbose("tar", "unpacking to", unpackTarget)
if (typeof cb !== "function") cb = gid, gid = null
if (typeof cb !== "function") cb = uid, uid = null
if (typeof cb !== "function") cb = fMode, fMode = npm.modes.file
if (typeof cb !== "function") cb = dMode, dMode = npm.modes.exec
uidNumber(uid, gid, function (er, uid, gid) {
if (er) return cb(er)
unpack_(tarball, unpackTarget, dMode, fMode, uid, gid, cb)
})
}
function unpack_ ( tarball, unpackTarget, dMode, fMode, uid, gid, cb ) {
rm(unpackTarget, function (er) {
if (er) return cb(er)
// gzip {tarball} --decompress --stdout \
// | tar -mvxpf - --strip-components=1 -C {unpackTarget}
gunzTarPerm( tarball, unpackTarget
, dMode, fMode
, uid, gid
, function (er, folder) {
if (er) return cb(er)
readJson(path.resolve(folder, "package.json"), cb)
})
})
}
function gunzTarPerm (tarball, target, dMode, fMode, uid, gid, cb_) {
if (!dMode) dMode = npm.modes.exec
if (!fMode) fMode = npm.modes.file
log.silly("gunzTarPerm", "modes", [dMode.toString(8), fMode.toString(8)])
var cbCalled = false
function cb (er) {
if (cbCalled) return
cbCalled = true
cb_(er, target)
}
var fst = fs.createReadStream(tarball)
fst.on("open", function (fd) {
fs.fstat(fd, function (er, st) {
if (er) return fst.emit("error", er)
if (st.size === 0) {
er = new Error("0-byte tarball\n" +
"Please run `npm cache clean`")
fst.emit("error", er)
}
})
})
// figure out who we're supposed to be, if we're not pretending
// to be a specific user.
if (npm.config.get("unsafe-perm") && process.platform !== "win32") {
uid = myUid
gid = myGid
}
function extractEntry (entry) {
log.silly("gunzTarPerm", "extractEntry", entry.path)
// never create things that are user-unreadable,
// or dirs that are user-un-listable. Only leads to headaches.
var originalMode = entry.mode = entry.mode || entry.props.mode
entry.mode = entry.mode | (entry.type === "Directory" ? dMode : fMode)
entry.mode = entry.mode & (~npm.modes.umask)
entry.props.mode = entry.mode
if (originalMode !== entry.mode) {
log.silly( "gunzTarPerm", "modified mode"
, [entry.path, originalMode, entry.mode])
}
// if there's a specific owner uid/gid that we want, then set that
if (process.platform !== "win32" &&
typeof uid === "number" &&
typeof gid === "number") {
entry.props.uid = entry.uid = uid
entry.props.gid = entry.gid = gid
}
}
var extractOpts = { type: "Directory", path: target, strip: 1 }
if (process.platform !== "win32" &&
typeof uid === "number" &&
typeof gid === "number") {
extractOpts.uid = uid
extractOpts.gid = gid
}
var sawIgnores = {}
extractOpts.filter = function () {
// symbolic links are not allowed in packages.
if (this.type.match(/^.*Link$/)) {
log.warn( "excluding symbolic link"
, this.path.substr(target.length + 1)
+ " -> " + this.linkpath )
return false
}
// Note: This mirrors logic in the fs read operations that are
// employed during tarball creation, in the fstream-npm module.
// It is duplicated here to handle tarballs that are created
// using other means, such as system tar or git archive.
if (this.type === "File") {
var base = path.basename(this.path)
if (base === ".npmignore") {
sawIgnores[ this.path ] = true
} else if (base === ".gitignore") {
var npmignore = this.path.replace(/\.gitignore$/, ".npmignore")
if (sawIgnores[npmignore]) {
// Skip this one, already seen.
return false
} else {
// Rename, may be clobbered later.
this.path = npmignore
this._path = npmignore
}
}
}
return true
}
fst
.on("error", function (er) {
if (er) log.error("tar.unpack", "error reading "+tarball)
cb(er)
})
.on("data", function OD (c) {
// detect what it is.
// Then, depending on that, we'll figure out whether it's
// a single-file module, gzipped tarball, or naked tarball.
// gzipped files all start with 1f8b08
if (c[0] === 0x1F &&
c[1] === 0x8B &&
c[2] === 0x08) {
fst
.pipe(zlib.Unzip())
.on("error", function (er) {
if (er) log.error("tar.unpack", "unzip error "+tarball)
cb(er)
})
.pipe(tar.Extract(extractOpts))
.on("entry", extractEntry)
.on("error", function (er) {
if (er) log.error("tar.unpack", "untar error "+tarball)
cb(er)
})
.on("close", cb)
} else if (hasTarHeader(c)) {
// naked tar
fst
.pipe(tar.Extract(extractOpts))
.on("entry", extractEntry)
.on("error", function (er) {
if (er) log.error("tar.unpack", "untar error "+tarball)
cb(er)
})
.on("close", cb)
} else {
// naked js file
var jsOpts = { path: path.resolve(target, "index.js") }
if (process.platform !== "win32" &&
typeof uid === "number" &&
typeof gid === "number") {
jsOpts.uid = uid
jsOpts.gid = gid
}
fst
.pipe(fstream.Writer(jsOpts))
.on("error", function (er) {
if (er) log.error("tar.unpack", "copy error "+tarball)
cb(er)
})
.on("close", function () {
var j = path.resolve(target, "package.json")
readJson(j, function (er, d) {
if (er) {
log.error("not a package", tarball)
return cb(er)
}
writeFileAtomic(j, JSON.stringify(d) + "\n", cb)
})
})
}
// now un-hook, and re-emit the chunk
fst.removeListener("data", OD)
fst.emit("data", c)
})
}
function hasTarHeader (c) {
return c[257] === 0x75 && // tar archives have 7573746172 at position
c[258] === 0x73 && // 257 and 003030 or 202000 at position 262
c[259] === 0x74 &&
c[260] === 0x61 &&
c[261] === 0x72 &&
((c[262] === 0x00 &&
c[263] === 0x30 &&
c[264] === 0x30) ||
(c[262] === 0x20 &&
c[263] === 0x20 &&
c[264] === 0x00))
}