uncompress_stream.js 3.61 KB
Newer Older
Li MingZhe's avatar
Li MingZhe committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
'use strict';

// https://github.com/thejoshwolfe/yauzl#no-streaming-unzip-api

const yauzl = require('yauzl');
const stream = require('stream');
const UncompressBaseStream = require('../base_write_stream');
const utils = require('../utils');

// lazy load iconv-lite
let iconv;

const YAUZL_CALLBACK = Symbol('ZipUncompressStream#yauzlCallback');
const STRIP_NAME = Symbol('ZipUncompressStream#stripName');

// don't decodeStrings on yauzl, we should handle fileName by ourself
// see validateFileName on https://github.com/thejoshwolfe/yauzl/blob/51010ce4e8c7e6345efe195e1b4150518f37b393/index.js#L607
//  - support "absolute path"
const DEFAULTS = { lazyEntries: true, decodeStrings: false };

class ZipUncompressStream extends UncompressBaseStream {
  constructor(opts) {
    opts = opts || {};
    super(opts);

    this._chunks = [];
    this._strip = Number(opts.strip) || 0;
    this._zipFileNameEncoding = opts.zipFileNameEncoding || 'utf8';
    if (this._zipFileNameEncoding === 'utf-8') {
      this._zipFileNameEncoding = 'utf8';
    }

    this[YAUZL_CALLBACK] = this[YAUZL_CALLBACK].bind(this);

    const sourceType = utils.sourceType(opts.source);

    const yauzlOpts = this._yauzlOpts = Object.assign({}, DEFAULTS, opts.yauzl);
    if (sourceType === 'file') {
      yauzl.open(opts.source, yauzlOpts, this[YAUZL_CALLBACK]);
      return;
    }

    if (sourceType === 'buffer') {
      yauzl.fromBuffer(opts.source, yauzlOpts, this[YAUZL_CALLBACK]);
      return;
    }

    if (sourceType === 'stream') {
      utils.streamToBuffer(opts.source)
      .then(buf => yauzl.fromBuffer(buf, yauzlOpts, this[YAUZL_CALLBACK]))
      .catch(e => this.emit('error', e));
      return;
    }

    this.on('pipe', srcStream => {
      srcStream.unpipe(srcStream);

      utils.streamToBuffer(srcStream)
      .then(buf => {
        this._chunks.push(buf);
        buf = Buffer.concat(this._chunks);
        yauzl.fromBuffer(buf, yauzlOpts, this[YAUZL_CALLBACK]);
      })
      .catch(e => this.emit('error', e));
    });
  }

  _write(chunk) {
    // push to _chunks array, this will only happen once, for stream will be unpiped.
    this._chunks.push(chunk);
  }

  [YAUZL_CALLBACK](err, zipFile) {
    if (err) return this.emit('error', err);

    zipFile.readEntry();

    zipFile
    .on('entry', entry => {
      // fileName is buffer by default because decodeStrings = false
      if (Buffer.isBuffer(entry.fileName)) {
        if (this._zipFileNameEncoding === 'utf8') {
          entry.fileName = entry.fileName.toString();
        } else {
          if (!iconv) {
            iconv = require('iconv-lite');
          }
          entry.fileName = iconv.decode(entry.fileName, this._zipFileNameEncoding);
        }
      }
      // directory file names end with '/'
      const type = /\/$/.test(entry.fileName) ? 'directory' : 'file';
      const name = entry.fileName = this[STRIP_NAME](entry.fileName, type);

      const header = { name, type, yauzl: entry };

      if (type === 'file') {
        zipFile.openReadStream(entry, (err, readStream) => {
          if (err) return this.emit('error', err);
          this.emit('entry', header, readStream, next);
        });
      } else { // directory
        const placeholder = new stream.Readable({ read() {} });
        this.emit('entry', header, placeholder, next);
        setImmediate(() => placeholder.emit('end'));
      }
    })
    .on('end', () => this.emit('finish'))
    .on('error', err => this.emit('error', err));

    function next() {
      zipFile.readEntry();
    }
  }

  [STRIP_NAME](fileName, type) {
    return utils.stripFileName(this._strip, fileName, type);
  }
}

module.exports = ZipUncompressStream;