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