index.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. var fs = require('fs')
  2. var path = require('path')
  3. var yauzl = require('yauzl')
  4. var mkdirp = require('mkdirp')
  5. var concat = require('concat-stream')
  6. var debug = require('debug')('extract-zip')
  7. module.exports = function (zipPath, opts, cb) {
  8. debug('creating target directory', opts.dir)
  9. if (path.isAbsolute(opts.dir) === false) {
  10. return cb(new Error('Target directory is expected to be absolute'))
  11. }
  12. mkdirp(opts.dir, function (err) {
  13. if (err) return cb(err)
  14. fs.realpath(opts.dir, function (err, canonicalDir) {
  15. if (err) return cb(err)
  16. opts.dir = canonicalDir
  17. openZip(opts)
  18. })
  19. })
  20. function openZip () {
  21. debug('opening', zipPath, 'with opts', opts)
  22. yauzl.open(zipPath, {lazyEntries: true}, function (err, zipfile) {
  23. if (err) return cb(err)
  24. var cancelled = false
  25. zipfile.readEntry()
  26. zipfile.on('close', function () {
  27. if (!cancelled) {
  28. debug('zip extraction complete')
  29. cb()
  30. }
  31. })
  32. zipfile.on('entry', function (entry) {
  33. if (cancelled) {
  34. debug('skipping entry', entry.fileName, {cancelled: cancelled})
  35. return
  36. }
  37. debug('zipfile entry', entry.fileName)
  38. if (/^__MACOSX\//.test(entry.fileName)) {
  39. // dir name starts with __MACOSX/
  40. zipfile.readEntry()
  41. return
  42. }
  43. var destDir = path.dirname(path.join(opts.dir, entry.fileName))
  44. mkdirp(destDir, function (err) {
  45. if (err) {
  46. cancelled = true
  47. zipfile.close()
  48. return cb(err)
  49. }
  50. fs.realpath(destDir, function (err, canonicalDestDir) {
  51. if (err) {
  52. cancelled = true
  53. zipfile.close()
  54. return cb(err)
  55. }
  56. var relativeDestDir = path.relative(opts.dir, canonicalDestDir)
  57. if (relativeDestDir.split(path.sep).indexOf('..') !== -1) {
  58. cancelled = true
  59. zipfile.close()
  60. return cb(new Error('Out of bound path "' + canonicalDestDir + '" found while processing file ' + entry.fileName))
  61. }
  62. extractEntry(entry, function (err) {
  63. // if any extraction fails then abort everything
  64. if (err) {
  65. cancelled = true
  66. zipfile.close()
  67. return cb(err)
  68. }
  69. debug('finished processing', entry.fileName)
  70. zipfile.readEntry()
  71. })
  72. })
  73. })
  74. })
  75. function extractEntry (entry, done) {
  76. if (cancelled) {
  77. debug('skipping entry extraction', entry.fileName, {cancelled: cancelled})
  78. return setImmediate(done)
  79. }
  80. if (opts.onEntry) {
  81. opts.onEntry(entry, zipfile)
  82. }
  83. var dest = path.join(opts.dir, entry.fileName)
  84. // convert external file attr int into a fs stat mode int
  85. var mode = (entry.externalFileAttributes >> 16) & 0xFFFF
  86. // check if it's a symlink or dir (using stat mode constants)
  87. var IFMT = 61440
  88. var IFDIR = 16384
  89. var IFLNK = 40960
  90. var symlink = (mode & IFMT) === IFLNK
  91. var isDir = (mode & IFMT) === IFDIR
  92. // Failsafe, borrowed from jsZip
  93. if (!isDir && entry.fileName.slice(-1) === '/') {
  94. isDir = true
  95. }
  96. // check for windows weird way of specifying a directory
  97. // https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566
  98. var madeBy = entry.versionMadeBy >> 8
  99. if (!isDir) isDir = (madeBy === 0 && entry.externalFileAttributes === 16)
  100. // if no mode then default to default modes
  101. if (mode === 0) {
  102. if (isDir) {
  103. if (opts.defaultDirMode) mode = parseInt(opts.defaultDirMode, 10)
  104. if (!mode) mode = 493 // Default to 0755
  105. } else {
  106. if (opts.defaultFileMode) mode = parseInt(opts.defaultFileMode, 10)
  107. if (!mode) mode = 420 // Default to 0644
  108. }
  109. }
  110. debug('extracting entry', { filename: entry.fileName, isDir: isDir, isSymlink: symlink })
  111. // reverse umask first (~)
  112. var umask = ~process.umask()
  113. // & with processes umask to override invalid perms
  114. var procMode = mode & umask
  115. // always ensure folders are created
  116. var destDir = dest
  117. if (!isDir) destDir = path.dirname(dest)
  118. debug('mkdirp', {dir: destDir})
  119. mkdirp(destDir, function (err) {
  120. if (err) {
  121. debug('mkdirp error', destDir, {error: err})
  122. cancelled = true
  123. return done(err)
  124. }
  125. if (isDir) return done()
  126. debug('opening read stream', dest)
  127. zipfile.openReadStream(entry, function (err, readStream) {
  128. if (err) {
  129. debug('openReadStream error', err)
  130. cancelled = true
  131. return done(err)
  132. }
  133. readStream.on('error', function (err) {
  134. console.log('read err', err)
  135. })
  136. if (symlink) writeSymlink()
  137. else writeStream()
  138. function writeStream () {
  139. var writeStream = fs.createWriteStream(dest, {mode: procMode})
  140. readStream.pipe(writeStream)
  141. writeStream.on('finish', function () {
  142. done()
  143. })
  144. writeStream.on('error', function (err) {
  145. debug('write error', {error: err})
  146. cancelled = true
  147. return done(err)
  148. })
  149. }
  150. // AFAICT the content of the symlink file itself is the symlink target filename string
  151. function writeSymlink () {
  152. readStream.pipe(concat(function (data) {
  153. var link = data.toString()
  154. debug('creating symlink', link, dest)
  155. fs.symlink(link, dest, function (err) {
  156. if (err) cancelled = true
  157. done(err)
  158. })
  159. }))
  160. }
  161. })
  162. })
  163. }
  164. })
  165. }
  166. }