libzim
Version:
Bindings to zimlib (read/write OpenZIM files)
241 lines (210 loc) • 7.35 kB
JavaScript
var debug = require('debug')('tar-pack')
var uidNumber = require('uid-number')
var rm = require('rimraf')
var tar = require('tar')
var once = require('once')
var fstream = require('fstream')
var packer = require('fstream-ignore')
var PassThrough = require('stream').PassThrough || require('readable-stream').PassThrough
var zlib = require('zlib')
var path = require('path')
var win32 = process.platform === 'win32'
var myUid = process.getuid && process.getuid()
var myGid = process.getgid && process.getgid()
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(folder, options) {
options = options || {}
if (typeof folder === 'string') {
var filter = options.filter || function (entry) { return true; }
folder = packer({
path: folder,
type: 'Directory',
isDirectory: true,
ignoreFiles: options.ignoreFiles || ['.gitignore'],
filter: function (entry) { // {path, basename, dirname, type} (type is "Directory" or "File")
var basename = entry.basename
// some files are *never* allowed under any circumstances
// these files should always be either temporary files or
// version control related files
if (basename === '.git' || basename === '.lock-wscript' || basename.match(/^\.wafpickle-[0-9]+$/) ||
basename === 'CVS' || basename === '.svn' || basename === '.hg' || basename.match(/^\..*\.swp$/) ||
basename === '.DS_Store' || basename.match(/^\._/)) {
return false
}
//custom excludes
return filter(entry)
}
})
}
// 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.
var tarPack = tar.Pack({ noProprietary: options.noProprietary || false })
var gzip = zlib.Gzip()
folder
.on('error', function (er) {
if (er) debug('Error reading folder')
return gzip.emit('error', er)
})
tarPack
.on('error', function (er) {
if (er) debug('tar creation error')
gzip.emit('error', er)
})
return folder.pipe(tarPack).pipe(gzip)
}
function unpack(unpackTarget, options, cb) {
if (typeof options === 'function' && cb === undefined) cb = options, options = undefined
var tarball = new PassThrough()
if (typeof cb === 'function') {
cb = once(cb)
tarball.on('error', cb)
tarball.on('close', function () {
cb()
})
}
var parent = path.dirname(unpackTarget)
var base = path.basename(unpackTarget)
options = options || {}
var gid = options.gid || null
var uid = options.uid || null
var dMode = options.dmode || 0x0777 //npm.modes.exec
var fMode = options.fmode || 0x0666 //npm.modes.file
var defaultName = options.defaultName || (options.defaultName === false ? false : 'index.js')
// figure out who we're supposed to be, if we're not pretending
// to be a specific user.
if (options.unsafe && !win32) {
uid = myUid
gid = myGid
}
var pending = 2
uidNumber(uid, gid, function (er, uid, gid) {
if (er) {
tarball.emit('error', er)
return tarball.end()
}
if (0 === --pending) next()
})
rm(unpackTarget, function (er) {
if (er) {
tarball.emit('error', er)
return tarball.end()
}
if (0 === --pending) next()
})
function next() {
// gzip {tarball} --decompress --stdout \
// | tar -mvxpf - --strip-components=1 -C {unpackTarget}
gunzTarPerm(tarball, unpackTarget, dMode, fMode, uid, gid, defaultName)
}
return tarball
}
function gunzTarPerm(tarball, target, dMode, fMode, uid, gid, defaultName) {
debug('modes %j', [dMode.toString(8), fMode.toString(8)])
function fixEntry(entry) {
debug('fixEntry %j', 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.props.mode = entry.mode
if (originalMode !== entry.mode) {
debug('modified mode %j', [entry.path, originalMode, entry.mode])
}
// if there's a specific owner uid/gid that we want, then set that
if (!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 (!win32 && typeof uid === 'number' && typeof gid === 'number') {
extractOpts.uid = uid
extractOpts.gid = gid
}
extractOpts.filter = function () {
// symbolic links are not allowed in packages.
if (this.type.match(/^.*Link$/)) {
debug('excluding symbolic link: ' + this.path.substr(target.length + 1) + ' -> ' + this.linkpath)
return false
}
return true
}
type(tarball, function (err, type) {
if (err) return tarball.emit('error', err)
var strm = tarball
if (type === 'gzip') {
strm = strm.pipe(zlib.Unzip())
strm.on('error', function (er) {
if (er) debug('unzip error')
tarball.emit('error', er)
})
type = 'tar'
}
if (type === 'tar') {
strm
.pipe(tar.Extract(extractOpts))
.on('entry', fixEntry)
.on('error', function (er) {
if (er) debug('untar error')
tarball.emit('error', er)
})
.on('close', function () {
tarball.emit('close')
})
return
}
if (type === 'naked-file' && defaultName) {
var jsOpts = { path: path.resolve(target, defaultName) }
if (!win32 && typeof uid === 'number' && typeof gid === 'number') {
jsOpts.uid = uid
jsOpts.gid = gid
}
strm
.pipe(fstream.Writer(jsOpts))
.on('error', function (er) {
if (er) debug('copy error')
tarball.emit('error', er)
})
.on('close', function () {
tarball.emit('close')
})
return
}
return cb(new Error('Unrecognised package type'));
})
}
function type(stream, callback) {
stream.on('error', handle)
stream.on('data', parse)
function handle(err) {
stream.removeListener('data', parse)
stream.removeListener('error', handle)
}
function parse(chunk) {
// 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 (chunk[0] === 0x1F && chunk[1] === 0x8B && chunk[2] === 0x08) {
callback(null, 'gzip')
} else if (chunk.toString().match(/^package\/\u0000/)) {
// note, this will only pick up on tarballs with a root directory called package
callback(null, 'tar')
} else {
callback(null, 'naked-file')
}
// now un-hook, and re-emit the chunk
stream.removeListener('data', parse)
stream.removeListener('error', handle)
stream.unshift(chunk)
}
}