Spaces:
Runtime error
Runtime error
const debug = require('debug')('extract-zip') | |
// eslint-disable-next-line node/no-unsupported-features/node-builtins | |
const { createWriteStream, promises: fs } = require('fs') | |
const getStream = require('get-stream') | |
const path = require('path') | |
const { promisify } = require('util') | |
const stream = require('stream') | |
const yauzl = require('yauzl') | |
const openZip = promisify(yauzl.open) | |
const pipeline = promisify(stream.pipeline) | |
class Extractor { | |
constructor (zipPath, opts) { | |
this.zipPath = zipPath | |
this.opts = opts | |
} | |
async extract () { | |
debug('opening', this.zipPath, 'with opts', this.opts) | |
this.zipfile = await openZip(this.zipPath, { lazyEntries: true }) | |
this.canceled = false | |
return new Promise((resolve, reject) => { | |
this.zipfile.on('error', err => { | |
this.canceled = true | |
reject(err) | |
}) | |
this.zipfile.readEntry() | |
this.zipfile.on('close', () => { | |
if (!this.canceled) { | |
debug('zip extraction complete') | |
resolve() | |
} | |
}) | |
this.zipfile.on('entry', async entry => { | |
/* istanbul ignore if */ | |
if (this.canceled) { | |
debug('skipping entry', entry.fileName, { cancelled: this.canceled }) | |
return | |
} | |
debug('zipfile entry', entry.fileName) | |
if (entry.fileName.startsWith('__MACOSX/')) { | |
this.zipfile.readEntry() | |
return | |
} | |
const destDir = path.dirname(path.join(this.opts.dir, entry.fileName)) | |
try { | |
await fs.mkdir(destDir, { recursive: true }) | |
const canonicalDestDir = await fs.realpath(destDir) | |
const relativeDestDir = path.relative(this.opts.dir, canonicalDestDir) | |
if (relativeDestDir.split(path.sep).includes('..')) { | |
throw new Error(`Out of bound path "${canonicalDestDir}" found while processing file ${entry.fileName}`) | |
} | |
await this.extractEntry(entry) | |
debug('finished processing', entry.fileName) | |
this.zipfile.readEntry() | |
} catch (err) { | |
this.canceled = true | |
this.zipfile.close() | |
reject(err) | |
} | |
}) | |
}) | |
} | |
async extractEntry (entry) { | |
/* istanbul ignore if */ | |
if (this.canceled) { | |
debug('skipping entry extraction', entry.fileName, { cancelled: this.canceled }) | |
return | |
} | |
if (this.opts.onEntry) { | |
this.opts.onEntry(entry, this.zipfile) | |
} | |
const dest = path.join(this.opts.dir, entry.fileName) | |
// convert external file attr int into a fs stat mode int | |
const mode = (entry.externalFileAttributes >> 16) & 0xFFFF | |
// check if it's a symlink or dir (using stat mode constants) | |
const IFMT = 61440 | |
const IFDIR = 16384 | |
const IFLNK = 40960 | |
const symlink = (mode & IFMT) === IFLNK | |
let isDir = (mode & IFMT) === IFDIR | |
// Failsafe, borrowed from jsZip | |
if (!isDir && entry.fileName.endsWith('/')) { | |
isDir = true | |
} | |
// check for windows weird way of specifying a directory | |
// https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566 | |
const madeBy = entry.versionMadeBy >> 8 | |
if (!isDir) isDir = (madeBy === 0 && entry.externalFileAttributes === 16) | |
debug('extracting entry', { filename: entry.fileName, isDir: isDir, isSymlink: symlink }) | |
const procMode = this.getExtractedMode(mode, isDir) & 0o777 | |
// always ensure folders are created | |
const destDir = isDir ? dest : path.dirname(dest) | |
const mkdirOptions = { recursive: true } | |
if (isDir) { | |
mkdirOptions.mode = procMode | |
} | |
debug('mkdir', { dir: destDir, ...mkdirOptions }) | |
await fs.mkdir(destDir, mkdirOptions) | |
if (isDir) return | |
debug('opening read stream', dest) | |
const readStream = await promisify(this.zipfile.openReadStream.bind(this.zipfile))(entry) | |
if (symlink) { | |
const link = await getStream(readStream) | |
debug('creating symlink', link, dest) | |
await fs.symlink(link, dest) | |
} else { | |
await pipeline(readStream, createWriteStream(dest, { mode: procMode })) | |
} | |
} | |
getExtractedMode (entryMode, isDir) { | |
let mode = entryMode | |
// Set defaults, if necessary | |
if (mode === 0) { | |
if (isDir) { | |
if (this.opts.defaultDirMode) { | |
mode = parseInt(this.opts.defaultDirMode, 10) | |
} | |
if (!mode) { | |
mode = 0o755 | |
} | |
} else { | |
if (this.opts.defaultFileMode) { | |
mode = parseInt(this.opts.defaultFileMode, 10) | |
} | |
if (!mode) { | |
mode = 0o644 | |
} | |
} | |
} | |
return mode | |
} | |
} | |
module.exports = async function (zipPath, opts) { | |
debug('creating target directory', opts.dir) | |
if (!path.isAbsolute(opts.dir)) { | |
throw new Error('Target directory is expected to be absolute') | |
} | |
await fs.mkdir(opts.dir, { recursive: true }) | |
opts.dir = await fs.realpath(opts.dir) | |
return new Extractor(zipPath, opts).extract() | |
} | |