index.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. "use strict"
  2. var debug = require('debug')('tar-pack')
  3. var uidNumber = require('uid-number')
  4. var rm = require('rimraf')
  5. var tar = require('tar')
  6. var once = require('once')
  7. var fstream = require('fstream')
  8. var packer = require('fstream-ignore')
  9. var PassThrough = require('stream').PassThrough || require('readable-stream').PassThrough
  10. var zlib = require('zlib')
  11. var path = require('path')
  12. var win32 = process.platform === 'win32'
  13. var myUid = process.getuid && process.getuid()
  14. var myGid = process.getgid && process.getgid()
  15. if (process.env.SUDO_UID && myUid === 0) {
  16. if (!isNaN(process.env.SUDO_UID)) myUid = +process.env.SUDO_UID
  17. if (!isNaN(process.env.SUDO_GID)) myGid = +process.env.SUDO_GID
  18. }
  19. exports.pack = pack
  20. exports.unpack = unpack
  21. function pack(folder, options) {
  22. options = options || {}
  23. if (typeof folder === 'string') {
  24. var filter = options.filter || function (entry) { return true; }
  25. folder = packer({
  26. path: folder,
  27. type: 'Directory',
  28. isDirectory: true,
  29. ignoreFiles: options.ignoreFiles || ['.gitignore'],
  30. filter: function (entry) { // {path, basename, dirname, type} (type is "Directory" or "File")
  31. var basename = entry.basename
  32. // some files are *never* allowed under any circumstances
  33. // these files should always be either temporary files or
  34. // version control related files
  35. if (basename === '.git' || basename === '.lock-wscript' || basename.match(/^\.wafpickle-[0-9]+$/) ||
  36. basename === 'CVS' || basename === '.svn' || basename === '.hg' || basename.match(/^\..*\.swp$/) ||
  37. basename === '.DS_Store' || basename.match(/^\._/)) {
  38. return false
  39. }
  40. //custom excludes
  41. return filter(entry)
  42. }
  43. })
  44. }
  45. // By default, npm includes some proprietary attributes in the
  46. // package tarball. This is sane, and allowed by the spec.
  47. // However, npm *itself* excludes these from its own package,
  48. // so that it can be more easily bootstrapped using old and
  49. // non-compliant tar implementations.
  50. var tarPack = tar.Pack({ noProprietary: options.noProprietary || false, fromBase: options.fromBase || false })
  51. var gzip = zlib.Gzip()
  52. folder
  53. .on('error', function (er) {
  54. if (er) debug('Error reading folder')
  55. return gzip.emit('error', er)
  56. })
  57. tarPack
  58. .on('error', function (er) {
  59. if (er) debug('tar creation error')
  60. gzip.emit('error', er)
  61. })
  62. return folder.pipe(tarPack).pipe(gzip)
  63. }
  64. function unpack(unpackTarget, options, cb) {
  65. if (typeof options === 'function' && cb === undefined) cb = options, options = undefined
  66. var tarball = new PassThrough()
  67. if (typeof cb === 'function') {
  68. cb = once(cb)
  69. tarball.on('error', cb)
  70. tarball.on('close', function () {
  71. cb()
  72. })
  73. }
  74. var parent = path.dirname(unpackTarget)
  75. var base = path.basename(unpackTarget)
  76. options = options || {}
  77. var gid = options.gid || null
  78. var uid = options.uid || null
  79. var dMode = options.dmode || 0x0777 //npm.modes.exec
  80. var fMode = options.fmode || 0x0666 //npm.modes.file
  81. var defaultName = options.defaultName || (options.defaultName === false ? false : 'index.js')
  82. var strip = (options.strip !== undefined) ? options.strip : 1
  83. // figure out who we're supposed to be, if we're not pretending
  84. // to be a specific user.
  85. if (options.unsafe && !win32) {
  86. uid = myUid
  87. gid = myGid
  88. }
  89. var pending = 2
  90. uidNumber(uid, gid, function (er, uid, gid) {
  91. if (er) {
  92. tarball.emit('error', er)
  93. return tarball.end()
  94. }
  95. if (0 === --pending) next()
  96. })
  97. if (!options.keepFiles) {
  98. rm(unpackTarget, function (er) {
  99. if (er) {
  100. tarball.emit('error', er)
  101. return tarball.end()
  102. }
  103. if (0 === --pending) next()
  104. })
  105. } else {
  106. next()
  107. }
  108. function next() {
  109. // gzip {tarball} --decompress --stdout \
  110. // | tar -mvxpf - --strip-components={strip} -C {unpackTarget}
  111. gunzTarPerm(tarball, unpackTarget, dMode, fMode, uid, gid, defaultName, strip)
  112. }
  113. return tarball
  114. }
  115. function gunzTarPerm(tarball, target, dMode, fMode, uid, gid, defaultName, strip) {
  116. debug('modes %j', [dMode.toString(8), fMode.toString(8)])
  117. function fixEntry(entry) {
  118. debug('fixEntry %j', entry.path)
  119. // never create things that are user-unreadable,
  120. // or dirs that are user-un-listable. Only leads to headaches.
  121. var originalMode = entry.mode = entry.mode || entry.props.mode
  122. entry.mode = entry.mode | (entry.type === 'Directory' ? dMode : fMode)
  123. entry.props.mode = entry.mode
  124. if (originalMode !== entry.mode) {
  125. debug('modified mode %j', [entry.path, originalMode, entry.mode])
  126. }
  127. // if there's a specific owner uid/gid that we want, then set that
  128. if (!win32 && typeof uid === 'number' && typeof gid === 'number') {
  129. entry.props.uid = entry.uid = uid
  130. entry.props.gid = entry.gid = gid
  131. }
  132. }
  133. var extractOpts = { type: 'Directory', path: target, strip: strip }
  134. if (!win32 && typeof uid === 'number' && typeof gid === 'number') {
  135. extractOpts.uid = uid
  136. extractOpts.gid = gid
  137. }
  138. extractOpts.filter = function () {
  139. // symbolic links are not allowed in packages.
  140. if (this.type.match(/^.*Link$/)) {
  141. debug('excluding symbolic link: ' + this.path.substr(target.length + 1) + ' -> ' + this.linkpath)
  142. return false
  143. }
  144. return true
  145. }
  146. type(tarball, function (err, type) {
  147. if (err) return tarball.emit('error', err)
  148. var strm = tarball
  149. if (type === 'gzip') {
  150. strm = strm.pipe(zlib.Unzip())
  151. strm.on('error', function (er) {
  152. if (er) debug('unzip error')
  153. tarball.emit('error', er)
  154. })
  155. type = 'tar'
  156. }
  157. if (type === 'tar') {
  158. strm
  159. .pipe(tar.Extract(extractOpts))
  160. .on('entry', fixEntry)
  161. .on('error', function (er) {
  162. if (er) debug('untar error')
  163. tarball.emit('error', er)
  164. })
  165. .on('close', function () {
  166. tarball.emit('close')
  167. })
  168. return
  169. }
  170. if (type === 'naked-file' && defaultName) {
  171. var jsOpts = { path: path.resolve(target, defaultName) }
  172. if (!win32 && typeof uid === 'number' && typeof gid === 'number') {
  173. jsOpts.uid = uid
  174. jsOpts.gid = gid
  175. }
  176. strm
  177. .pipe(fstream.Writer(jsOpts))
  178. .on('error', function (er) {
  179. if (er) debug('copy error')
  180. tarball.emit('error', er)
  181. })
  182. .on('close', function () {
  183. tarball.emit('close')
  184. })
  185. return
  186. }
  187. return tarball.emit('error', new Error('Unrecognised package type'));
  188. })
  189. }
  190. function type(stream, callback) {
  191. stream.on('error', handle)
  192. stream.on('data', parse)
  193. function handle(err) {
  194. stream.removeListener('data', parse)
  195. stream.removeListener('error', handle)
  196. }
  197. function parse(chunk) {
  198. // detect what it is.
  199. // Then, depending on that, we'll figure out whether it's
  200. // a single-file module, gzipped tarball, or naked tarball.
  201. // gzipped files all start with 1f8b08
  202. if (chunk[0] === 0x1F && chunk[1] === 0x8B && chunk[2] === 0x08) {
  203. callback(null, 'gzip')
  204. } else if (chunk.toString().match(/^package\/\u0000/)) {
  205. // note, this will only pick up on tarballs with a root directory called package
  206. callback(null, 'tar')
  207. } else {
  208. callback(null, 'naked-file')
  209. }
  210. // now un-hook, and re-emit the chunk
  211. stream.removeListener('data', parse)
  212. stream.removeListener('error', handle)
  213. stream.unshift(chunk)
  214. }
  215. }