diff options
Diffstat (limited to 'deps/npm/lib/utils/tar.js')
-rw-r--r-- | deps/npm/lib/utils/tar.js | 628 |
1 files changed, 114 insertions, 514 deletions
diff --git a/deps/npm/lib/utils/tar.js b/deps/npm/lib/utils/tar.js index f315bbf961..415eb7f9e2 100644 --- a/deps/npm/lib/utils/tar.js +++ b/deps/npm/lib/utils/tar.js @@ -1,115 +1,39 @@ -// XXX lib/cache.js and this file need to be rewritten. - // commands for packing and unpacking tarballs // this file is used by lib/cache.js var npm = require("../npm.js") , fs = require("graceful-fs") - , exec = require("./exec.js") - , find = require("./find.js") - , mkdir = require("./mkdir-p.js") - , asyncMap = require("slide").asyncMap , path = require("path") , log = require("./log.js") - , uidNumber = require("./uid-number.js") + , uidNumber = require("uid-number") , rm = require("rimraf") , readJson = require("./read-json.js") , relativize = require("./relativize.js") , cache = require("../cache.js") - , excludes = require("./excludes.js") , myUid = process.getuid && process.getuid() , myGid = process.getgid && process.getgid() , tar = require("tar") , zlib = require("zlib") , fstream = require("fstream") + , Packer = require("fstream-npm") + +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 -exports.makeList = makeList function pack (targetTarball, folder, pkg, dfc, cb) { + log.verbose([targetTarball, folder], "tar.pack") if (typeof cb !== "function") cb = dfc, dfc = true - folder = path.resolve(folder) - - log.verbose(folder, "pack") - - if (typeof pkg === "function") { - cb = pkg, pkg = null - return readJson(path.resolve(folder, "package.json"), function (er, pkg) { - if (er) return log.er(cb, "Couldn't find package.json in "+folder)(er) - pack(targetTarball, folder, pkg, dfc, cb) - }) - } - log.verbose(folder+" "+targetTarball, "pack") - var parent = path.dirname(folder) - , addFolder = path.basename(folder) - - var confEx = npm.config.get("ignore") - log.silly(folder, "makeList") - makeList(folder, pkg, dfc, function (er, files, cleanup) { - if (er) return cb(er) - // log.silly(files, "files") - return packFiles(targetTarball, parent, files, pkg, function (er) { - if (!cleanup || !cleanup.length) return cb(er) - // try to be a good citizen, even/especially in the event of failure. - cleanupResolveLinkDep(cleanup, function (er2) { - if (er || er2) { - if (er) log(er, "packing tarball") - if (er2) log(er2, "while cleaning up resolved deps") - } - return cb(er || er2) - }) - }) - }) -} - -function packFiles (targetTarball, parent, files, pkg, cb_) { - - var p - - files = files.map(function (f) { - p = f.split(/\/|\\/)[0] - return path.resolve(parent, f) - }) - - parent = path.resolve(parent, p) - - var called = false - function cb (er) { - if (called) return - called = true - cb_(er) - } log.verbose(targetTarball, "tarball") - log.verbose(parent, "parent") - fstream.Reader({ type: "Directory" - , path: parent - , filter: function () { - // files should *always* get into tarballs - // in a user-writable state, even if they're - // being installed from some wackey vm-mounted - // read-only filesystem. - this.props.mode = this.props.mode | 0200 - var inc = -1 !== files.indexOf(this.path) + log.verbose(folder, "folder") + new Packer({ path: folder, type: "Directory", isDirectory: true }) + .on("error", log.er(cb, "error reading "+folder)) - // WARNING! Hackety hack! - // XXX Fix this in a better way. - // Rename .gitignore to .npmignore if there is not a - // .npmignore file there already, the better to lock - // down installed packages with git for deployment. - if (this.basename === ".gitignore") { - if (this.parent._entries.indexOf(".npmignore") !== -1) { - return false - } - var d = path.dirname(this.path) - this.basename = ".npmignore" - this.path = path.join(d, ".npmignore") - } - return inc - } - }) - .on("error", log.er(cb, "error reading "+parent)) // 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, @@ -121,11 +45,14 @@ function packFiles (targetTarball, parent, files, pkg, cb_) { .on("error", log.er(cb, "gzip error "+targetTarball)) .pipe(fstream.Writer({ type: "File", path: targetTarball })) .on("error", log.er(cb, "Could not write "+targetTarball)) - .on("close", cb) + .on("close", function () { + cb() + }) } function unpack (tarball, unpackTarget, dMode, fMode, uid, gid, cb) { + log.verbose(tarball, "unpack") 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 @@ -138,466 +65,139 @@ function unpack (tarball, unpackTarget, dMode, fMode, uid, gid, cb) { } function unpack_ ( tarball, unpackTarget, dMode, fMode, uid, gid, cb ) { - // If the desired target is /path/to/foo, - // then unpack into /path/to/.foo.npm/{something} - // rename that to /path/to/foo, and delete /path/to/.foo.npm var parent = path.dirname(unpackTarget) , base = path.basename(unpackTarget) - , tmp = path.resolve(parent, "___" + base + ".npm") - mkdir(tmp, dMode || npm.modes.exec, uid, gid, function (er) { - log.verbose([uid, gid], "unpack_ uid, gid") - log.verbose(unpackTarget, "unpackTarget") - if (er) return log.er(cb, "Could not create "+tmp)(er) - // cp the gzip of the tarball, pipe the stdout into tar's stdin + rm(unpackTarget, function (er) { + if (er) return cb(er) + // gzip {tarball} --decompress --stdout \ // | tar -mvxpf - --strip-components=1 -C {unpackTarget} - gunzTarPerm( tarball, tmp + gunzTarPerm( tarball, unpackTarget , dMode, fMode , uid, gid , function (er, folder) { if (er) return cb(er) - log.verbose(folder, "gunzed") - - rm(unpackTarget, function (er) { - if (er) return cb(er) - log.verbose(unpackTarget, "rm'ed") - - moveIntoPlace(folder, unpackTarget, function (er) { - if (er) return cb(er) - log.verbose([folder, unpackTarget], "renamed") - // curse you, nfs! It will lie and tell you that the - // mv is done, when in fact, it isn't. In theory, - // reading the file should cause it to wait until it's done. - readJson( path.resolve(unpackTarget, "package.json") - , function (er, data) { - // now we read the json, so we know it's there. - rm(tmp, function (er2) { cb(er || er2, data) }) - }) - }) - }) + readJson(path.resolve(folder, "package.json"), cb) }) }) } -// on Windows, A/V software can lock the directory, causing this -// to fail with an EACCES. Try again on failure, for up to 1 second. -// XXX Fix this by not unpacking into a temp directory, instead just -// renaming things on the way out of the tarball. -function moveIntoPlace (folder, unpackTarget, cb) { - var start = Date.now() - fs.rename(folder, unpackTarget, function CB (er) { - if (er - && process.platform === "win32" - && er.code === "EACCES" - && Date.now() - start < 1000) { - return fs.rename(folder, unpackTarget, CB) - } - cb(er) - }) -} - -function gunzTarPerm (tarball, tmp, dMode, fMode, uid, gid, cb) { +function gunzTarPerm (tarball, target, dMode, fMode, uid, gid, cb_) { if (!dMode) dMode = npm.modes.exec if (!fMode) fMode = npm.modes.file log.silly([dMode.toString(8), fMode.toString(8)], "gunzTarPerm modes") - fs.createReadStream(tarball) - .on("error", log.er(cb, "error reading "+tarball)) - .pipe(zlib.Unzip()) - .on("error", log.er(cb, "unzip error "+tarball)) - .pipe(tar.Extract({ type: "Directory", path: tmp })) - .on("error", log.er(cb, "Failed unpacking "+tarball)) - .on("close", afterUntar) - - // - // XXX Do all this in an Extract filter. - // - function afterUntar (er) { - log.silly(er, "afterUntar") - // if we're not doing ownership management, - // then we're done now. - if (er) return log.er(cb, "Failed unpacking "+tarball)(er) - - // HACK skip on windows - if (npm.config.get("unsafe-perm") && process.platform !== "win32") { - uid = process.getuid() - gid = process.getgid() - if (uid === 0) { - if (process.env.SUDO_UID) uid = +process.env.SUDO_UID - if (process.env.SUDO_GID) gid = +process.env.SUDO_GID - } - } - - if (process.platform === "win32") { - return fs.readdir(tmp, function (er, files) { - files = files.filter(function (f) { - return f && f.indexOf("\0") === -1 - }) - cb(er, files && path.resolve(tmp, files[0])) - }) - } - - find(tmp, function (f) { - return f !== tmp - }, function (er, files) { - if (er) return cb(er) - asyncMap(files, function (f, cb) { - f = path.resolve(f) - log.silly(f, "asyncMap in gTP") - fs.lstat(f, function (er, stat) { - - if (er || stat.isSymbolicLink()) return cb(er) - if (typeof uid === "number" && typeof gid === "number") { - fs.chown(f, uid, gid, chown) - } else chown() - - function chown (er) { - if (er) return cb(er) - var mode = stat.isDirectory() ? dMode : fMode - , oldMode = stat.mode & 0777 - , newMode = (oldMode | mode) & (~npm.modes.umask) - if (mode && newMode !== oldMode) { - log.silly(newMode.toString(8), "chmod "+path.basename(f)) - fs.chmod(f, newMode, cb) - } else cb() - } - }) - }, function (er) { - - if (er) return cb(er) - if (typeof myUid === "number" && typeof myGid === "number") { - fs.chown(tmp, myUid, myGid, chown) - } else chown() - - function chown (er) { - if (er) return cb(er) - fs.readdir(tmp, function (er, folder) { - folder = folder && folder.filter(function (f) { - return f && !f.match(/^\._/) - }) - cb(er, folder && path.resolve(tmp, folder[0])) - }) - } - }) - }) + var cbCalled = false + function cb (er) { + if (cbCalled) return + cbCalled = true + cb_(er, target) } -} - -function makeList (dir, pkg, dfc, cb) { - if (typeof cb !== "function") cb = dfc, dfc = true - if (typeof cb !== "function") cb = pkg, pkg = null - dir = path.resolve(dir) - - if (!pkg.path) pkg.path = dir - - var name = path.basename(dir) - - // since this is a top-level traversal, get the user and global - // exclude files, as well as the "ignore" config setting. - var confIgnore = npm.config.get("ignore").trim() - .split(/[\n\r\s\t]+/) - .filter(function (i) { return i.trim() }) - , userIgnore = npm.config.get("userignorefile") - , globalIgnore = npm.config.get("globalignorefile") - , userExclude - , globalExclude - - confIgnore.dir = dir - confIgnore.name = "confIgnore" - - var defIgnore = ["build/"] - defIgnore.dir = dir - - // TODO: only look these up once, and cache outside this function - excludes.parseIgnoreFile( userIgnore, null, dir - , function (er, uex) { - if (er) return cb(er) - userExclude = uex - next() - }) - excludes.parseIgnoreFile( globalIgnore, null, dir - , function (er, gex) { - if (er) return cb(er) - globalExclude = gex - next() - }) + var fst = fs.createReadStream(tarball) - function next () { - if (!globalExclude || !userExclude) return - var exList = [ defIgnore, confIgnore, globalExclude, userExclude ] - - makeList_(dir, pkg, exList, dfc, function (er, files, cleanup) { - if (er) return cb(er) - var dirLen = dir.replace(/(\/|\\)$/, "").length + 1 - log.silly([dir, dirLen], "dir, dirLen") - files = files.map(function (file) { - return path.join(name, file.substr(dirLen)) - }) - return cb(null, files, cleanup) - }) + // 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 } -} - -// Patterns ending in slashes will only match targets -// ending in slashes. To implement this, add a / to -// the filename iff it lstats isDirectory() -function readDir (dir, pkg, dfc, cb) { - fs.readdir(dir, function (er, files) { - if (er) return cb(er) - files = files.filter(function (f) { - return f && f.charAt(0) !== "/" && f.indexOf("\0") === -1 - }) - asyncMap(files, function (file, cb) { - fs.lstat(path.resolve(dir, file), function (er, st) { - if (er) return cb(null, []) - // if it's a directory, then tack "/" onto the name - // so that it can match dir-only patterns in the - // include/exclude logic later. - if (st.isDirectory()) return cb(null, file + "/") - - // if it's a symlink, then we need to do some more - // complex stuff for GH-691 - if (st.isSymbolicLink()) return readSymlink(dir, file, pkg, dfc, cb) - - // otherwise, just let it on through. - return cb(null, file) - }) - }, cb) - }) -} - -// just see where this link is pointing, and resolve relative paths. -function shallowReal (link, cb) { - link = path.resolve(link) - fs.readlink(link, function (er, t) { - if (er) return cb(er) - return cb(null, path.resolve(path.dirname(link), t), t) - }) -} - -function readSymlink (dir, file, pkg, dfc, cb) { - var isNM = dfc - && path.basename(dir) === "node_modules" - && path.dirname(dir) === pkg.path - // see if this thing is pointing outside of the package. - // external symlinks are resolved for deps, ignored for other things. - // internal symlinks are allowed through. - var df = path.resolve(dir, file) - shallowReal(df, function (er, r, target) { - if (er) return cb(null, []) // wtf? exclude file. - if (r.indexOf(dir) === 0) return cb(null, file) // internal - if (!isNM) return cb(null, []) // external non-dep - // now the fun stuff! - fs.realpath(df, function (er, resolved) { - if (er) return cb(null, []) // can't add it. - readJson(path.resolve(resolved, "package.json"), function (er) { - if (er) return cb(null, []) // not a package - resolveLinkDep(dir, file, resolved, target, pkg, function (er, f, c) { - cb(er, f, c) - }) - }) - }) - }) -} - -// put the link back the way it was. -function cleanupResolveLinkDep (cleanup, cb) { - // cut it out of the list, so that cycles will be broken. - if (!cleanup) return cb() - asyncMap(cleanup, function (d, cb) { - rm(d[1], function (er) { - if (er) return cb(er) - fs.symlink(d[0], d[1], cb) - }) - }, cb) -} - -function resolveLinkDep (dir, file, resolved, target, pkg, cb) { - // we've already decided that this is a dep that will be bundled. - // make sure the data reflects this. - var bd = pkg.bundleDependencies || pkg.bundledDependencies || [] - delete pkg.bundledDependencies - pkg.bundleDependencies = bd - var f = path.resolve(dir, file) - , cleanup = [[target, f, resolved]] + function extractEntry (entry) { + log.silly(entry.path, "extracting entry") + // 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([entry.path, originalMode, entry.mode], "modified mode") + } - if (bd.indexOf(file) === -1) { - // then we don't do this one. - // just move the symlink out of the way. - return rm(f, function (er) { - cb(er, file, cleanup) - }) + // 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 + } } - rm(f, function (er) { - if (er) return cb(er) - cache.add(resolved, function (er, data) { - if (er) return cb(er) - cache.unpack(data.name, data.version, f, function (er, data) { - if (er) return cb(er) - // now clear out the cache entry, since it's weird, probably. - // pass the cleanup object along so that the thing getting the - // list of files knows what to clean up afterwards. - cache.clean([data._id], function (er) { cb(er, file, cleanup) }) - }) - }) - }) -} - -// exList is a list of ignore lists. -// Each exList item is an array of patterns of files to ignore -// -function makeList_ (dir, pkg, exList, dfc, cb) { - var files = null - , cleanup = null + var extractOpts = { type: "Directory", path: target, strip: 1 } - readDir(dir, pkg, dfc, function (er, f, c) { - if (er) return cb(er) - cleanup = c - files = f.map(function (f) { - // no nulls in paths! - return f.split(/\0/)[0] - }).filter(function (f) { - // always remove all source control folders and - // waf/vim/OSX garbage. this is a firm requirement. - return !( f === ".git/" - || f === ".lock-wscript" - || f === "CVS/" - || f === ".svn/" - || f === ".hg/" - || f.match(/^\..*\.swp/) - || f === ".DS_Store" - || f.match(/^\._/) - || f === "npm-debug.log" - || f === "" - || f.charAt(0) === "/" - ) - }) - - // if (files.length > 0) files.push(".") - - if (files.indexOf("package.json") !== -1 && dir !== pkg.path) { - // a package.json file starts the whole exclude/include - // logic all over. Otherwise, a parent could break its - // deps with its files list or .npmignore file. - readJson(path.resolve(dir, "package.json"), function (er, data) { - if (!er && typeof data === "object") { - data.path = dir - return makeList(dir, data, dfc, function (er, files) { - // these need to be mounted onto the directory now. - cb(er, files && files.map(function (f) { - return path.resolve(path.dirname(dir), f) - })) - }) - } - next() - }) - //next() - } else next() + if (process.platform !== "win32" && + typeof uid === "number" && + typeof gid === "number") { + extractOpts.uid = uid + extractOpts.gid = gid + } - // add a local ignore file, if found. - if (files.indexOf(".npmignore") === -1 - && files.indexOf(".gitignore") === -1) next() - else { - excludes.addIgnoreFile( path.resolve(dir, ".npmignore") - , ".gitignore" - , exList - , dir - , function (er, list) { - if (!er) exList = list - next(er) - }) + extractOpts.filter = function () { + // symbolic links are not allowed in packages. + if (this.type.match(/^.*Link$/)) { + log.warn( this.path.substr(target.length + 1) + + ' -> ' + this.linkpath + , "excluding symbolic link") + return false } - }) + return true + } - var n = 2 - , errState = null - function next (er) { - if (errState) return - if (er) return cb(errState = er, [], cleanup) - if (-- n > 0) return - if (!pkg) return cb(new Error("No package.json file in "+dir)) - if (pkg.path === dir && pkg.files) { - pkg.files = pkg.files.filter(function (f) { - f = f.trim() - return f && f.charAt(0) !== "#" - }) - if (!pkg.files.length) pkg.files = null - } - if (pkg.path === dir && pkg.files) { - // stuff on the files list MUST be there. - // ignore everything, then include the stuff on the files list. - var pkgFiles = ["*"].concat(pkg.files.map(function (f) { - return "!" + f - })) - pkgFiles.dir = dir - pkgFiles.packageFiles = true - exList.push(pkgFiles) - } - - if (path.basename(dir) === "node_modules" - && pkg.path === path.dirname(dir) - && dfc) { // do fancy crap - files = filterNodeModules(files, pkg) + fst.on("error", log.er(cb, "error reading "+tarball)) + fst.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", log.er(cb, "unzip error "+tarball)) + .pipe(tar.Extract(extractOpts)) + .on("entry", extractEntry) + .on("error", log.er(cb, "untar error "+tarball)) + .on("close", cb) + } else if (c.toString().match(/^package\//)) { + // naked tar + fst + .pipe(tar.Extract(extractOpts)) + .on("entry", extractEntry) + .on("error", log.er(cb, "untar error "+tarball)) + .on("close", cb) } else { - // If a directory is excluded, we still need to be - // able to *include* a file within it, and have that override - // the prior exclusion. - // - // This whole makeList thing probably needs to be rewritten - files = files.filter(function (f) { - return excludes.filter(dir, exList)(f) || f.slice(-1) === "/" - }) - } - - - asyncMap(files, function (file, cb) { - // if this is a dir, then dive into it. - // otherwise, don't. - file = path.resolve(dir, file) - - // in 0.6.0, fs.readdir can produce some really odd results. - // XXX: remove this and make the engines hash exclude 0.6.0 - if (file.indexOf(dir) !== 0) { - return cb(null, []) + // 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 } - fs.lstat(file, function (er, st) { - if (er) return cb(er) - if (st.isDirectory()) { - return makeList_(file, pkg, exList, dfc, cb) - } - return cb(null, file) - }) - }, function (er, files, c) { - if (c) cleanup = (cleanup || []).concat(c) - if (files.length > 0) files.push(dir) - return cb(er, files, cleanup) - }) - } -} - -// only include node_modules folder that are: -// 1. not on the dependencies list or -// 2. on the "bundleDependencies" list. -function filterNodeModules (files, pkg) { - var bd = pkg.bundleDependencies || pkg.bundledDependencies || [] - , deps = Object.keys(pkg.dependencies || {}) - .filter(function (key) { return !pkg.dependencies[key].extraneous }) - .concat(Object.keys(pkg.devDependencies || {})) - - delete pkg.bundledDependencies - pkg.bundleDependencies = bd + fst + .pipe(fstream.Writer(jsOpts)) + .on("error", log.er(cb, "copy error "+tarball)) + .on("close", function () { + var j = path.resolve(target, "package.json") + readJson(j, function (er, d) { + if (er) { + log.error(tarball, "Not a package") + return cb(er) + } + fs.writeFile(j, JSON.stringify(d) + "\n", cb) + }) + }) + } - return files.filter(function (f) { - f = f.replace(/\/$/, "") - return f.charAt(0) !== "." - && f.charAt(0) !== "_" - && bd.indexOf(f) !== -1 + // now un-hook, and re-emit the chunk + fst.removeListener("data", OD) + fst.emit("data", c) }) } |