const tar = require('tar-stream') const pump = require('pump') const mkdirp = require('mkdirp-classic') const fs = require('fs') const path = require('path') const win32 = process.platform === 'win32' exports.pack = function pack (cwd, opts) { if (!cwd) cwd = '.' if (!opts) opts = {} const xfs = opts.fs || fs const ignore = opts.ignore || opts.filter || noop const mapStream = opts.mapStream || echo const statNext = statAll(xfs, opts.dereference ? xfs.stat : xfs.lstat, cwd, ignore, opts.entries, opts.sort) const strict = opts.strict !== false const umask = typeof opts.umask === 'number' ? ~opts.umask : ~processUmask() const pack = opts.pack || tar.pack() const finish = opts.finish || noop let map = opts.map || noop let dmode = typeof opts.dmode === 'number' ? opts.dmode : 0 let fmode = typeof opts.fmode === 'number' ? opts.fmode : 0 if (opts.strip) map = strip(map, opts.strip) if (opts.readable) { dmode |= parseInt(555, 8) fmode |= parseInt(444, 8) } if (opts.writable) { dmode |= parseInt(333, 8) fmode |= parseInt(222, 8) } onnextentry() function onsymlink (filename, header) { xfs.readlink(path.join(cwd, filename), function (err, linkname) { if (err) return pack.destroy(err) header.linkname = normalize(linkname) pack.entry(header, onnextentry) }) } function onstat (err, filename, stat) { if (err) return pack.destroy(err) if (!filename) { if (opts.finalize !== false) pack.finalize() return finish(pack) } if (stat.isSocket()) return onnextentry() // tar does not support sockets... let header = { name: normalize(filename), mode: (stat.mode | (stat.isDirectory() ? dmode : fmode)) & umask, mtime: stat.mtime, size: stat.size, type: 'file', uid: stat.uid, gid: stat.gid } if (stat.isDirectory()) { header.size = 0 header.type = 'directory' header = map(header) || header return pack.entry(header, onnextentry) } if (stat.isSymbolicLink()) { header.size = 0 header.type = 'symlink' header = map(header) || header return onsymlink(filename, header) } // TODO: add fifo etc... header = map(header) || header if (!stat.isFile()) { if (strict) return pack.destroy(new Error('unsupported type for ' + filename)) return onnextentry() } const entry = pack.entry(header, onnextentry) const rs = mapStream(xfs.createReadStream(path.join(cwd, filename), { start: 0, end: header.size > 0 ? header.size - 1 : header.size }), header) rs.on('error', function (err) { // always forward errors on destroy entry.destroy(err) }) pump(rs, entry) } function onnextentry (err) { if (err) return pack.destroy(err) statNext(onstat) } return pack } function head (list) { return list.length ? list[list.length - 1] : null } function processGetuid () { return process.getuid ? process.getuid() : -1 } function processUmask () { return process.umask ? process.umask() : 0 } exports.extract = function extract (cwd, opts) { if (!cwd) cwd = '.' if (!opts) opts = {} const xfs = opts.fs || fs const ignore = opts.ignore || opts.filter || noop const mapStream = opts.mapStream || echo const own = opts.chown !== false && !win32 && processGetuid() === 0 const extract = opts.extract || tar.extract() const stack = [] const now = new Date() const umask = typeof opts.umask === 'number' ? ~opts.umask : ~processUmask() const strict = opts.strict !== false let map = opts.map || noop let dmode = typeof opts.dmode === 'number' ? opts.dmode : 0 let fmode = typeof opts.fmode === 'number' ? opts.fmode : 0 if (opts.strip) map = strip(map, opts.strip) if (opts.readable) { dmode |= parseInt(555, 8) fmode |= parseInt(444, 8) } if (opts.writable) { dmode |= parseInt(333, 8) fmode |= parseInt(222, 8) } extract.on('entry', onentry) if (opts.finish) extract.on('finish', opts.finish) return extract function onentry (header, stream, next) { header = map(header) || header header.name = normalize(header.name) const name = path.join(cwd, path.join('/', header.name)) if (ignore(name, header)) { stream.resume() return next() } if (header.type === 'directory') { stack.push([name, header.mtime]) return mkdirfix(name, { fs: xfs, own, uid: header.uid, gid: header.gid, mode: header.mode }, stat) } const dir = path.dirname(name) validate(xfs, dir, path.join(cwd, '.'), function (err, valid) { if (err) return next(err) if (!valid) return next(new Error(dir + ' is not a valid path')) mkdirfix(dir, { fs: xfs, own, uid: header.uid, gid: header.gid, // normally, the folders with rights and owner should be part of the TAR file // if this is not the case, create folder for same user as file and with // standard permissions of 0o755 (rwxr-xr-x) mode: 0o755 }, function (err) { if (err) return next(err) switch (header.type) { case 'file': return onfile() case 'link': return onlink() case 'symlink': return onsymlink() } if (strict) return next(new Error('unsupported type for ' + name + ' (' + header.type + ')')) stream.resume() next() }) }) function stat (err) { if (err) return next(err) utimes(name, header, function (err) { if (err) return next(err) if (win32) return next() chperm(name, header, next) }) } function onsymlink () { if (win32) return next() // skip symlinks on win for now before it can be tested xfs.unlink(name, function () { xfs.symlink(header.linkname, name, stat) }) } function onlink () { if (win32) return next() // skip links on win for now before it can be tested xfs.unlink(name, function () { const srcpath = path.join(cwd, path.join('/', header.linkname)) xfs.link(srcpath, name, function (err) { if (err && err.code === 'EPERM' && opts.hardlinkAsFilesFallback) { stream = xfs.createReadStream(srcpath) return onfile() } stat(err) }) }) } function onfile () { const ws = xfs.createWriteStream(name) const rs = mapStream(stream, header) ws.on('error', function (err) { // always forward errors on destroy rs.destroy(err) }) pump(rs, ws, function (err) { if (err) return next(err) ws.on('close', stat) }) } } function utimesParent (name, cb) { // we just set the mtime on the parent dir again everytime we write an entry let top while ((top = head(stack)) && name.slice(0, top[0].length) !== top[0]) stack.pop() if (!top) return cb() xfs.utimes(top[0], now, top[1], cb) } function utimes (name, header, cb) { if (opts.utimes === false) return cb() if (header.type === 'directory') return xfs.utimes(name, now, header.mtime, cb) if (header.type === 'symlink') return utimesParent(name, cb) // TODO: how to set mtime on link? xfs.utimes(name, now, header.mtime, function (err) { if (err) return cb(err) utimesParent(name, cb) }) } function chperm (name, header, cb) { const link = header.type === 'symlink' /* eslint-disable n/no-deprecated-api */ const chmod = link ? xfs.lchmod : xfs.chmod const chown = link ? xfs.lchown : xfs.chown /* eslint-enable n/no-deprecated-api */ if (!chmod) return cb() const mode = (header.mode | (header.type === 'directory' ? dmode : fmode)) & umask if (chown && own) chown.call(xfs, name, header.uid, header.gid, onchown) else onchown(null) function onchown (err) { if (err) return cb(err) if (!chmod) return cb() chmod.call(xfs, name, mode, cb) } } function mkdirfix (name, opts, cb) { // when mkdir is called on an existing directory, the permissions // will be overwritten (?), to avoid this we check for its existance first xfs.stat(name, function (err) { if (!err) return cb(null) if (err.code !== 'ENOENT') return cb(err) mkdirp(name, { fs: opts.fs, mode: opts.mode }, function (err, made) { if (err) return cb(err) chperm(name, opts, cb) }) }) } } function validate (fs, name, root, cb) { if (name === root) return cb(null, true) fs.lstat(name, function (err, st) { if (err && err.code === 'ENOENT') return validate(fs, path.join(name, '..'), root, cb) else if (err) return cb(err) cb(null, st.isDirectory()) }) } function noop () {} function echo (name) { return name } function normalize (name) { return win32 ? name.replace(/\\/g, '/').replace(/[:?<>|]/g, '_') : name } function statAll (fs, stat, cwd, ignore, entries, sort) { if (!entries) entries = ['.'] const queue = entries.slice(0) return function loop (callback) { if (!queue.length) return callback(null) const next = queue.shift() const nextAbs = path.join(cwd, next) stat.call(fs, nextAbs, function (err, stat) { // ignore errors if the files were deleted while buffering if (err) return callback(entries.indexOf(next) === -1 && err.code === 'ENOENT' ? null : err) if (!stat.isDirectory()) return callback(null, next, stat) fs.readdir(nextAbs, function (err, files) { if (err) return callback(err) if (sort) files.sort() for (let i = 0; i < files.length; i++) { if (!ignore(path.join(cwd, next, files[i]))) queue.push(path.join(next, files[i])) } callback(null, next, stat) }) }) } } function strip (map, level) { return function (header) { header.name = header.name.split('/').slice(level).join('/') const linkname = header.linkname if (linkname && (header.type === 'link' || path.isAbsolute(linkname))) { header.linkname = linkname.split('/').slice(level).join('/') } return map(header) } }