index.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. const debug = require('debug')('extract-zip')
  2. // eslint-disable-next-line node/no-unsupported-features/node-builtins
  3. const { createWriteStream, promises: fs } = require('fs')
  4. const getStream = require('get-stream')
  5. const path = require('path')
  6. const { promisify } = require('util')
  7. const stream = require('stream')
  8. const yauzl = require('yauzl')
  9. const openZip = promisify(yauzl.open)
  10. const pipeline = promisify(stream.pipeline)
  11. class Extractor {
  12. constructor (zipPath, opts) {
  13. this.zipPath = zipPath
  14. this.opts = opts
  15. }
  16. async extract () {
  17. debug('opening', this.zipPath, 'with opts', this.opts)
  18. this.zipfile = await openZip(this.zipPath, { lazyEntries: true })
  19. this.canceled = false
  20. return new Promise((resolve, reject) => {
  21. this.zipfile.on('error', err => {
  22. this.canceled = true
  23. reject(err)
  24. })
  25. this.zipfile.readEntry()
  26. this.zipfile.on('close', () => {
  27. if (!this.canceled) {
  28. debug('zip extraction complete')
  29. resolve()
  30. }
  31. })
  32. this.zipfile.on('entry', async entry => {
  33. /* istanbul ignore if */
  34. if (this.canceled) {
  35. debug('skipping entry', entry.fileName, { cancelled: this.canceled })
  36. return
  37. }
  38. debug('zipfile entry', entry.fileName)
  39. if (entry.fileName.startsWith('__MACOSX/')) {
  40. this.zipfile.readEntry()
  41. return
  42. }
  43. const destDir = path.dirname(path.join(this.opts.dir, entry.fileName))
  44. try {
  45. await fs.mkdir(destDir, { recursive: true })
  46. const canonicalDestDir = await fs.realpath(destDir)
  47. const relativeDestDir = path.relative(this.opts.dir, canonicalDestDir)
  48. if (relativeDestDir.split(path.sep).includes('..')) {
  49. throw new Error(`Out of bound path "${canonicalDestDir}" found while processing file ${entry.fileName}`)
  50. }
  51. await this.extractEntry(entry)
  52. debug('finished processing', entry.fileName)
  53. this.zipfile.readEntry()
  54. } catch (err) {
  55. this.canceled = true
  56. this.zipfile.close()
  57. reject(err)
  58. }
  59. })
  60. })
  61. }
  62. async extractEntry (entry) {
  63. /* istanbul ignore if */
  64. if (this.canceled) {
  65. debug('skipping entry extraction', entry.fileName, { cancelled: this.canceled })
  66. return
  67. }
  68. if (this.opts.onEntry) {
  69. this.opts.onEntry(entry, this.zipfile)
  70. }
  71. const dest = path.join(this.opts.dir, entry.fileName)
  72. // convert external file attr int into a fs stat mode int
  73. const mode = (entry.externalFileAttributes >> 16) & 0xFFFF
  74. // check if it's a symlink or dir (using stat mode constants)
  75. const IFMT = 61440
  76. const IFDIR = 16384
  77. const IFLNK = 40960
  78. const symlink = (mode & IFMT) === IFLNK
  79. let isDir = (mode & IFMT) === IFDIR
  80. // Failsafe, borrowed from jsZip
  81. if (!isDir && entry.fileName.endsWith('/')) {
  82. isDir = true
  83. }
  84. // check for windows weird way of specifying a directory
  85. // https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566
  86. const madeBy = entry.versionMadeBy >> 8
  87. if (!isDir) isDir = (madeBy === 0 && entry.externalFileAttributes === 16)
  88. debug('extracting entry', { filename: entry.fileName, isDir: isDir, isSymlink: symlink })
  89. const procMode = this.getExtractedMode(mode, isDir) & 0o777
  90. // always ensure folders are created
  91. const destDir = isDir ? dest : path.dirname(dest)
  92. const mkdirOptions = { recursive: true }
  93. if (isDir) {
  94. mkdirOptions.mode = procMode
  95. }
  96. debug('mkdir', { dir: destDir, ...mkdirOptions })
  97. await fs.mkdir(destDir, mkdirOptions)
  98. if (isDir) return
  99. debug('opening read stream', dest)
  100. const readStream = await promisify(this.zipfile.openReadStream.bind(this.zipfile))(entry)
  101. if (symlink) {
  102. const link = await getStream(readStream)
  103. debug('creating symlink', link, dest)
  104. await fs.symlink(link, dest)
  105. } else {
  106. await pipeline(readStream, createWriteStream(dest, { mode: procMode }))
  107. }
  108. }
  109. getExtractedMode (entryMode, isDir) {
  110. let mode = entryMode
  111. // Set defaults, if necessary
  112. if (mode === 0) {
  113. if (isDir) {
  114. if (this.opts.defaultDirMode) {
  115. mode = parseInt(this.opts.defaultDirMode, 10)
  116. }
  117. if (!mode) {
  118. mode = 0o755
  119. }
  120. } else {
  121. if (this.opts.defaultFileMode) {
  122. mode = parseInt(this.opts.defaultFileMode, 10)
  123. }
  124. if (!mode) {
  125. mode = 0o644
  126. }
  127. }
  128. }
  129. return mode
  130. }
  131. }
  132. module.exports = async function (zipPath, opts) {
  133. debug('creating target directory', opts.dir)
  134. if (!path.isAbsolute(opts.dir)) {
  135. throw new Error('Target directory is expected to be absolute')
  136. }
  137. await fs.mkdir(opts.dir, { recursive: true })
  138. opts.dir = await fs.realpath(opts.dir)
  139. return new Extractor(zipPath, opts).extract()
  140. }