Просмотр исходного кода

Merge pull request #1186 from weseek/feat/zip-download

Feat/zip download
Sou Mizobuchi 6 лет назад
Родитель
Сommit
eeaee87ad1

+ 1 - 0
package.json

@@ -66,6 +66,7 @@
       "mongoose: somehow GlobalNotificationSetting CRUD does not work with mongoose v5.6.0",
       "mongoose: somehow GlobalNotificationSetting CRUD does not work with mongoose v5.6.0",
       "openid-client: Node.js 12 or higher is required for openid-client@3 and above."
       "openid-client: Node.js 12 or higher is required for openid-client@3 and above."
     ],
     ],
+    "archiver": "^3.1.1",
     "async": "^3.0.1",
     "async": "^3.0.1",
     "aws-sdk": "^2.88.0",
     "aws-sdk": "^2.88.0",
     "axios": "^0.19.0",
     "axios": "^0.19.0",

+ 1 - 0
resource/locales/en-US/translation.json

@@ -112,6 +112,7 @@
   "UserGroup Management": "UserGroup Management",
   "UserGroup Management": "UserGroup Management",
   "Full Text Search Management": "Full Text Search Management",
   "Full Text Search Management": "Full Text Search Management",
   "Import Data": "Import Data",
   "Import Data": "Import Data",
+  "Export Data": "Export Data",
   "Basic Settings": "Basic Settings",
   "Basic Settings": "Basic Settings",
   "Basic authentication": "Basic authentication",
   "Basic authentication": "Basic authentication",
   "Register limitation": "Register limitation",
   "Register limitation": "Register limitation",

+ 1 - 0
resource/locales/ja/translation.json

@@ -111,6 +111,7 @@
   "UserGroup Management": "グループ管理",
   "UserGroup Management": "グループ管理",
   "Full Text Search Management": "全文検索管理",
   "Full Text Search Management": "全文検索管理",
   "Import Data": "データインポート",
   "Import Data": "データインポート",
+  "Export Data": "データエクスポート",
   "Basic Settings": "基本設定",
   "Basic Settings": "基本設定",
   "Register limitation": "登録の制限",
   "Register limitation": "登録の制限",
   "The contents entered here will be shown in the header etc": "ここに入力した内容は、ヘッダー等に表示されます。",
   "The contents entered here will be shown in the header etc": "ここに入力した内容は、ヘッダー等に表示されます。",

+ 15 - 0
src/client/js/app.jsx

@@ -38,6 +38,7 @@ import CustomCssEditor from './components/Admin/CustomCssEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 import AdminRebuildSearch from './components/Admin/AdminRebuildSearch';
 import AdminRebuildSearch from './components/Admin/AdminRebuildSearch';
+import ExportPage from './components/Admin/Export/ExportPage';
 import GroupDeleteModal from './components/GroupDeleteModal/GroupDeleteModal';
 import GroupDeleteModal from './components/GroupDeleteModal/GroupDeleteModal';
 
 
 import AppContainer from './services/AppContainer';
 import AppContainer from './services/AppContainer';
@@ -187,6 +188,20 @@ if (adminGrantSelectorElem != null) {
   );
   );
 }
 }
 
 
+const adminExportPageElem = document.getElementById('admin-export-page');
+if (adminExportPageElem != null) {
+  ReactDOM.render(
+    <Provider inject={[]}>
+      <I18nextProvider i18n={i18n}>
+        <ExportPage
+          crowi={appContainer}
+        />
+      </I18nextProvider>
+    </Provider>,
+    adminExportPageElem,
+  );
+}
+
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
   ReactDOM.render(
   ReactDOM.render(

+ 100 - 0
src/client/js/components/Admin/Export/ExportAsZip.jsx

@@ -0,0 +1,100 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+// import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+class ExportPage extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      files: {},
+    };
+
+    this.createZipFile = this.createZipFile.bind(this);
+    this.deleteZipFile = this.deleteZipFile.bind(this);
+  }
+
+  async componentDidMount() {
+    const res = await this.props.appContainer.apiGet('/v3/export', {});
+
+    this.setState({ files: res.files });
+  }
+
+  async createZipFile() {
+    // TODO use appContainer.apiv3.post
+    const res = await this.props.appContainer.apiPost('/v3/export/pages', {});
+    // TODO toastSuccess, toastError
+    this.setState((prevState) => {
+      return {
+        files: {
+          ...prevState.files,
+          [res.collection]: res.file,
+        },
+      };
+    });
+  }
+
+  async deleteZipFile() {
+    // TODO use appContainer.apiv3.delete
+    // TODO toastSuccess, toastError
+  }
+
+  render() {
+    // const { t } = this.props;
+
+    return (
+      <Fragment>
+        <h2>Export Data as Zip</h2>
+        <form className="my-5">
+          {Object.keys(this.state.files).map((file) => {
+            const disabled = file !== 'pages';
+            return (
+              <div className="form-check" key={file}>
+                <input
+                  type="radio"
+                  id={file}
+                  name="collection"
+                  className="form-check-input"
+                  value={file}
+                  disabled={disabled}
+                  checked={!disabled}
+                  onChange={() => {}}
+                />
+                <label className={`form-check-label ml-3 ${disabled ? 'text-muted' : ''}`} htmlFor={file}>
+                  {file} ({this.state.files[file] || 'not found'})
+                </label>
+              </div>
+            );
+          })}
+        </form>
+        <button type="button" className="btn btn-sm btn-default" onClick={this.createZipFile}>Generate</button>
+        <a href="/_api/v3/export/pages">
+          <button type="button" className="btn btn-sm btn-primary ml-2">Download</button>
+        </a>
+        {/* <button type="button" className="btn btn-sm btn-danger ml-2" onClick={this.deleteZipFile}>Clear</button> */}
+      </Fragment>
+    );
+  }
+
+}
+
+ExportPage.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  isAclEnabled: PropTypes.bool,
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const ExportPageWrapper = (props) => {
+  return createSubscribedElement(ExportPage, props, [AppContainer]);
+};
+
+export default withTranslation()(ExportPageWrapper);

+ 17 - 0
src/client/js/components/Admin/Export/ExportPage.jsx

@@ -0,0 +1,17 @@
+import React, { Fragment } from 'react';
+
+import ExportAsZip from './ExportAsZip';
+
+class ExportPage extends React.Component {
+
+  render() {
+    return (
+      <Fragment>
+        <ExportAsZip />
+      </Fragment>
+    );
+  }
+
+}
+
+export default ExportPage;

+ 9 - 0
src/server/crowi/index.js

@@ -46,6 +46,7 @@ function Crowi(rootdir) {
   this.appService = null;
   this.appService = null;
   this.fileUploadService = null;
   this.fileUploadService = null;
   this.restQiitaAPIService = null;
   this.restQiitaAPIService = null;
+  this.exportService = null;
   this.cdnResourcesService = new CdnResourcesService();
   this.cdnResourcesService = new CdnResourcesService();
   this.interceptorManager = new InterceptorManager();
   this.interceptorManager = new InterceptorManager();
   this.xss = new Xss();
   this.xss = new Xss();
@@ -103,6 +104,7 @@ Crowi.prototype.init = async function() {
     this.setUpCustomize(),
     this.setUpCustomize(),
     this.setUpRestQiitaAPI(),
     this.setUpRestQiitaAPI(),
     this.setupUserGroup(),
     this.setupUserGroup(),
+    this.setupExport(),
   ]);
   ]);
 
 
   // globalNotification depends on slack and mailer
   // globalNotification depends on slack and mailer
@@ -532,4 +534,11 @@ Crowi.prototype.setupUserGroup = async function() {
   }
   }
 };
 };
 
 
+Crowi.prototype.setupExport = async function() {
+  const ExportService = require('../service/export');
+  if (this.exportService == null) {
+    this.exportService = new ExportService(this);
+  }
+};
+
 module.exports = Crowi;
 module.exports = Crowi;

+ 6 - 0
src/server/routes/admin.js

@@ -868,6 +868,12 @@ module.exports = function(crowi, app) {
     });
     });
   };
   };
 
 
+  // Export management
+  actions.export = {};
+  actions.export.index = (req, res) => {
+    return res.render('admin/export');
+  };
+
   actions.api = {};
   actions.api = {};
   actions.api.appSetting = async function(req, res) {
   actions.api.appSetting = async function(req, res) {
     const form = req.form.settingForm;
     const form = req.form.settingForm;

+ 138 - 0
src/server/routes/apiv3/export.js

@@ -0,0 +1,138 @@
+const loggerFactory = require('@alias/logger');
+
+const logger = loggerFactory('growi:routes:apiv3:export'); // eslint-disable-line no-unused-vars
+const path = require('path');
+
+const express = require('express');
+
+const router = express.Router();
+
+/**
+ * @swagger
+ *  tags:
+ *    name: Export
+ */
+
+module.exports = (crowi) => {
+  const { exportService } = crowi;
+  const { Page } = crowi.models;
+
+  /**
+   * @swagger
+   *
+   *  /export:
+   *    get:
+   *      tags: [Export]
+   *      description: get mongodb collections names and zip files for them
+   *      produces:
+   *        - application/json
+   *      responses:
+   *        200:
+   *          description: export cache info
+   *          content:
+   *            application/json:
+   */
+  router.get('/', async(req, res) => {
+    const files = exportService.getStatus();
+
+    // TODO: use res.apiv3
+    return res.json({ ok: true, files });
+  });
+
+  /**
+   * @swagger
+   *
+   *  /export/pages:
+   *    get:
+   *      tags: [Export]
+   *      description: download a zipped json for page collection
+   *      produces:
+   *        - application/json
+   *      responses:
+   *        200:
+   *          description: a zip file
+   *          content:
+   *            application/zip:
+   */
+  router.get('/pages', async(req, res) => {
+    // TODO: rename path to "/:collection" and add express validator
+    try {
+      const file = exportService.getZipFile(Page);
+
+      if (file == null) {
+        throw new Error('the target file does not exist');
+      }
+
+      return res.download(file);
+    }
+    catch (err) {
+      // TODO: use ApiV3Error
+      logger.error(err);
+      return res.status(500).send({ status: 'ERROR' });
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *  /export/pages:
+   *    post:
+   *      tags: [Export]
+   *      description: generate a zipped json for page collection
+   *      produces:
+   *        - application/json
+   *      responses:
+   *        200:
+   *          description: a zip file is generated
+   *          content:
+   *            application/json:
+   */
+  router.post('/pages', async(req, res) => {
+    // TODO: rename path to "/:collection" and add express validator
+    try {
+      const file = await exportService.exportCollection(Page);
+      // TODO: use res.apiv3
+      return res.status(200).json({
+        ok: true,
+        collection: [Page.collection.collectionName],
+        file: path.basename(file),
+      });
+    }
+    catch (err) {
+      // TODO: use ApiV3Error
+      logger.error(err);
+      return res.status(500).send({ status: 'ERROR' });
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *  /export/pages:
+   *    delete:
+   *      tags: [Export]
+   *      description: unlink a json and zip file for page collection
+   *      produces:
+   *        - application/json
+   *      responses:
+   *        200:
+   *          description: the json and zip file are removed
+   *          content:
+   *            application/json:
+   */
+  // router.delete('/pages', async(req, res) => {
+  //   // TODO: rename path to "/:collection" and add express validator
+  //   try {
+  //     // remove .json and .zip for collection
+  //     // TODO: use res.apiv3
+  //     return res.status(200).send({ status: 'DONE' });
+  //   }
+  //   catch (err) {
+  //     // TODO: use ApiV3Error
+  //     logger.error(err);
+  //     return res.status(500).send({ status: 'ERROR' });
+  //   }
+  // });
+
+  return router;
+};

+ 3 - 0
src/server/routes/apiv3/index.js

@@ -8,5 +8,8 @@ const router = express.Router();
 
 
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   router.use('/healthcheck', require('./healthcheck')(crowi));
   router.use('/healthcheck', require('./healthcheck')(crowi));
+
+  router.use('/export', require('./export')(crowi));
+
   return router;
   return router;
 };
 };

+ 3 - 0
src/server/routes/index.js

@@ -163,6 +163,9 @@ module.exports = function(crowi, app) {
   app.post('/_api/admin/import/qiita'        , loginRequired() , adminRequired , admin.api.importDataFromQiita);
   app.post('/_api/admin/import/qiita'        , loginRequired() , adminRequired , admin.api.importDataFromQiita);
   app.post('/_api/admin/import/testQiitaAPI' , loginRequired() , adminRequired , csrf , form.admin.importerQiita , admin.api.testQiitaAPI);
   app.post('/_api/admin/import/testQiitaAPI' , loginRequired() , adminRequired , csrf , form.admin.importerQiita , admin.api.testQiitaAPI);
 
 
+  // export management for admin
+  app.get('/admin/export' , loginRequired() , adminRequired ,admin.export.index);
+
   app.get('/me'                       , loginRequired() , me.index);
   app.get('/me'                       , loginRequired() , me.index);
   app.get('/me/password'              , loginRequired() , me.password);
   app.get('/me/password'              , loginRequired() , me.password);
   app.get('/me/apiToken'              , loginRequired() , me.apiToken);
   app.get('/me/apiToken'              , loginRequired() , me.apiToken);

+ 205 - 0
src/server/service/export.js

@@ -0,0 +1,205 @@
+const logger = require('@alias/logger')('growi:services:ExportService'); // eslint-disable-line no-unused-vars
+const fs = require('fs');
+const path = require('path');
+const streamToPromise = require('stream-to-promise');
+const archiver = require('archiver');
+
+class ExportService {
+
+  constructor(crowi) {
+    this.baseDir = path.join(crowi.tmpDir, 'downloads');
+    this.extension = 'json';
+    this.encoding = 'utf-8';
+    this.per = 100;
+    this.zlibLevel = 9; // 0(min) - 9(max)
+
+    this.files = {};
+    // populate this.files
+    // this.files = {
+    //   configs: path.join(this.baseDir, 'configs.json'),
+    //   pages: path.join(this.baseDir, 'pages.json'),
+    //   pagetagrelations: path.join(this.baseDir, 'pagetagrelations.json'),
+    //   ...
+    // };
+    // TODO: handle 3 globalnotificationsettings collection properly
+    // see Object.values(crowi.models).forEach((m) => { return console.log(m.collection.collectionName) });
+    Object.values(crowi.models).forEach((m) => {
+      const name = m.collection.collectionName;
+      this.files[name] = path.join(this.baseDir, `${name}.${this.extension}`);
+    });
+  }
+
+  /**
+   * dump a collection into json
+   *
+   * @memberOf ExportService
+   * @return {object} cache info for exported zip files
+   */
+  getStatus() {
+    const status = {};
+    const collections = Object.keys(this.files);
+    collections.forEach((file) => {
+      status[path.basename(file, '.zip')] = null;
+    });
+
+    // extract ${collectionName}.zip
+    const files = fs.readdirSync(this.baseDir).filter((file) => { return path.extname(file) === '.zip' && collections.includes(path.basename(file, '.zip')) });
+
+    files.forEach((file) => {
+      status[path.basename(file, '.zip')] = file;
+    });
+
+    return status;
+  }
+
+  /**
+   * dump a collection into json
+   *
+   * @memberOf ExportService
+   * @param {string} file path to json file to be written
+   * @param {readStream} readStream  read stream
+   * @param {number} [total] number of target items (optional)
+   */
+  async export(file, readStream, total) {
+    let n = 0;
+    const ws = fs.createWriteStream(file, { encoding: this.encoding });
+
+    // open an array
+    ws.write('[');
+
+    readStream.on('data', (chunk) => {
+      if (n !== 0) ws.write(',');
+      ws.write(JSON.stringify(chunk));
+      n++;
+      this.logProgress(n, total);
+    });
+
+    readStream.on('end', () => {
+      // close the array
+      ws.write(']');
+      ws.close();
+    });
+
+    await streamToPromise(readStream);
+  }
+
+  /**
+   * dump a mongodb collection into json
+   *
+   * @memberOf ExportService
+   * @param {object} Model instance of mongoose model
+   * @return {string} path to zip file
+   */
+  async exportCollection(Model) {
+    const modelName = Model.collection.collectionName;
+    const file = this.files[modelName];
+    const total = await Model.countDocuments();
+    const readStream = Model.find().cursor();
+
+    await this.export(file, readStream, total);
+
+    const { file: zipFile, size } = await this.zipSingleFile(file);
+
+    logger.info(`exported ${modelName} collection into ${zipFile} (${size} bytes)`);
+
+    return zipFile;
+  }
+
+  /**
+   * log export progress
+   *
+   * @memberOf ExportService
+   * @param {number} n number of items exported
+   * @param {number} [total] number of target items (optional)
+   */
+  logProgress(n, total) {
+    let output;
+    if (total) {
+      output = `${n}/${total} written`;
+    }
+    else {
+      output = `${n} items written`;
+    }
+
+    // output every this.per items
+    if (n % this.per === 0) logger.debug(output);
+    // output last item
+    else if (n === total) logger.info(output);
+  }
+
+  /**
+   * zip a file
+   *
+   * @memberOf ExportService
+   * @param {string} from path to input file
+   * @param {string} [to=`${path.join(path.dirname(from), `${path.basename(from, path.extname(from))}.zip`)}`] path to output file
+   * @param {string} [as=path.basename(from)] file name after unzipped
+   * @return {object} file path and file size
+   * @see https://www.archiverjs.com/#quick-start
+   */
+  async zipSingleFile(from, to = this.replaceExtension(from, 'zip'), as = path.basename(from)) {
+    const archive = archiver('zip', {
+      zlib: { level: this.zlibLevel },
+    });
+    const input = fs.createReadStream(from);
+    const output = fs.createWriteStream(to);
+
+    // good practice to catch warnings (ie stat failures and other non-blocking errors)
+    archive.on('warning', (err) => {
+      if (err.code === 'ENOENT') logger.error(err);
+      else throw err;
+    });
+
+    // good practice to catch this error explicitly
+    archive.on('error', (err) => { throw err });
+
+    // append a file from stream
+    archive.append(input, { name: as });
+
+    // pipe archive data to the file
+    archive.pipe(output);
+
+    // finalize the archive (ie we are done appending files but streams have to finish yet)
+    // 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand
+    archive.finalize();
+
+    await streamToPromise(archive);
+
+    return {
+      file: to,
+      size: archive.pointer(),
+    };
+  }
+
+  /**
+   * replace a file extension
+   *
+   * @memberOf ExportService
+   * @param {string} file file path
+   * @param {string} extension new extension
+   * @return {string} path to file with new extension
+   */
+  replaceExtension(file, extension) {
+    return `${path.join(path.dirname(file), `${path.basename(file, path.extname(file))}.${extension}`)}`;
+  }
+
+  /**
+   * get the path to the zipped file for a collection
+   *
+   * @memberOf ExportService
+   * @param {object} Model instance of mongoose model
+   * @return {string} path to zip file
+   */
+  getZipFile(Model) {
+    const json = this.files[Model.collection.collectionName];
+    const zip = this.replaceExtension(json, 'zip');
+    if (!fs.existsSync(zip)) {
+      return null;
+    }
+
+    return zip;
+  }
+
+}
+
+module.exports = ExportService;

+ 31 - 0
src/server/views/admin/export.html

@@ -0,0 +1,31 @@
+{% extends '../layout/admin.html' %}
+
+{% block html_title %}{{ customizeService.generateCustomTitle(t('Export Data')) }}{% endblock %}
+
+{% block content_header %}
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 id="admin-title" class="title">{{ t('Export Data') }}</h1>
+  </header>
+</div>
+{% endblock %}
+
+{% block content_main %}
+<div class="content-main admin-export">
+
+  <div class="row">
+    <div class="col-md-3">
+      {% include './widget/menu.html' with {current: 'export'} %}
+    </div>
+    <div
+      id="admin-export-page"
+      class="col-md-9"
+    >
+    </div>
+  </div>
+</div>
+
+{% endblock content_main %}
+
+{% block content_footer %}
+{% endblock content_footer %}

+ 2 - 1
src/server/views/admin/widget/menu.html

@@ -7,7 +7,8 @@
   <li class="{% if current == 'security'%}active{% endif %}"><a href="/admin/security"><i class="icon-fw icon-shield"></i> {{ t('security_settings') }}</a></li>
   <li class="{% if current == 'security'%}active{% endif %}"><a href="/admin/security"><i class="icon-fw icon-shield"></i> {{ t('security_settings') }}</a></li>
   <li class="{% if current == 'markdown'%}active{% endif %}"><a href="/admin/markdown"><i class="icon-fw icon-note"></i> {{ t('Markdown Settings') }}</a></li>
   <li class="{% if current == 'markdown'%}active{% endif %}"><a href="/admin/markdown"><i class="icon-fw icon-note"></i> {{ t('Markdown Settings') }}</a></li>
   <li class="{% if current == 'customize'%}active{% endif %}"><a href="/admin/customize"><i class="icon-fw icon-wrench"></i> {{ t('Customize') }}</a></li>
   <li class="{% if current == 'customize'%}active{% endif %}"><a href="/admin/customize"><i class="icon-fw icon-wrench"></i> {{ t('Customize') }}</a></li>
-  <li class="{% if current == 'importer'%}active{% endif %}"><a href="/admin/importer"><i class="icon-fw icon-cloud-download"></i> {{ t('Import Data') }}</a></li>
+  <li class="{% if current == 'importer'%}active{% endif %}"><a href="/admin/importer"><i class="icon-fw icon-cloud-upload"></i> {{ t('Import Data') }}</a></li>
+  <li class="{% if current == 'export'%}active{% endif %}"><a href="/admin/export"><i class="icon-fw icon-cloud-download"></i> {{ t('Export Data') }}</a></li>
   <li class="{% if current == 'notification'%}active{% endif %}"><a href="/admin/notification"><i class="icon-fw icon-bell"></i> {{ t('Notification Settings') }}</a></li>
   <li class="{% if current == 'notification'%}active{% endif %}"><a href="/admin/notification"><i class="icon-fw icon-bell"></i> {{ t('Notification Settings') }}</a></li>
   <li class="{% if current == 'user' || current == 'external-account' %}active{% endif %}"><a href="/admin/users"><i class="icon-fw icon-user"></i> {{ t('User_Management') }}</a></li>
   <li class="{% if current == 'user' || current == 'external-account' %}active{% endif %}"><a href="/admin/users"><i class="icon-fw icon-user"></i> {{ t('User_Management') }}</a></li>
   <li class="{% if current == 'user-group'%}active{% endif %}"><a href="/admin/user-groups"><i class="icon-fw icon-people"></i> {{ t('UserGroup Management') }}</a></li>
   <li class="{% if current == 'user-group'%}active{% endif %}"><a href="/admin/user-groups"><i class="icon-fw icon-people"></i> {{ t('UserGroup Management') }}</a></li>

+ 155 - 3
yarn.lock

@@ -1482,6 +1482,35 @@ aproba@^1.0.3, aproba@^1.1.1:
   version "1.2.0"
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
   resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
 
 
+archiver-utils@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2"
+  integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==
+  dependencies:
+    glob "^7.1.4"
+    graceful-fs "^4.2.0"
+    lazystream "^1.0.0"
+    lodash.defaults "^4.2.0"
+    lodash.difference "^4.5.0"
+    lodash.flatten "^4.4.0"
+    lodash.isplainobject "^4.0.6"
+    lodash.union "^4.6.0"
+    normalize-path "^3.0.0"
+    readable-stream "^2.0.0"
+
+archiver@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/archiver/-/archiver-3.1.1.tgz#9db7819d4daf60aec10fe86b16cb9258ced66ea0"
+  integrity sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==
+  dependencies:
+    archiver-utils "^2.1.0"
+    async "^2.6.3"
+    buffer-crc32 "^0.2.1"
+    glob "^7.1.4"
+    readable-stream "^3.4.0"
+    tar-stream "^2.1.0"
+    zip-stream "^2.1.2"
+
 are-we-there-yet@~1.1.2:
 are-we-there-yet@~1.1.2:
   version "1.1.4"
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
   resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
@@ -1645,6 +1674,13 @@ async@^0.9.0:
   version "0.9.2"
   version "0.9.2"
   resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
   resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
 
 
+async@^2.6.3:
+  version "2.6.3"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
+  integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
+  dependencies:
+    lodash "^4.17.14"
+
 async@~0.2.6:
 async@~0.2.6:
   version "0.2.10"
   version "0.2.10"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
@@ -1914,6 +1950,13 @@ binary-extensions@^1.0.0:
   version "1.11.0"
   version "1.11.0"
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
 
 
+bl@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88"
+  integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A==
+  dependencies:
+    readable-stream "^3.0.1"
+
 blob@0.0.4:
 blob@0.0.4:
   version "0.0.4"
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
   resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
@@ -2203,6 +2246,11 @@ bson@^1.1.0, bson@^1.1.1, bson@~1.1.0:
   resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.1.tgz#4330f5e99104c4e751e7351859e2d408279f2f13"
   resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.1.tgz#4330f5e99104c4e751e7351859e2d408279f2f13"
   integrity sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==
   integrity sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==
 
 
+buffer-crc32@^0.2.1, buffer-crc32@^0.2.13:
+  version "0.2.13"
+  resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
+  integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
+
 buffer-equal-constant-time@1.0.1:
 buffer-equal-constant-time@1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
   resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
@@ -2223,6 +2271,14 @@ buffer@4.9.1, buffer@^4.3.0:
     ieee754 "^1.1.4"
     ieee754 "^1.1.4"
     isarray "^1.0.0"
     isarray "^1.0.0"
 
 
+buffer@^5.1.0:
+  version "5.4.2"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.2.tgz#2012872776206182480eccb2c0fba5f672a2efef"
+  integrity sha512-iy9koArjAFCzGnx3ZvNA6Z0clIbbFgbdWQ0mKD3hO0krOrZh8UgA6qMKcZvwLJxS+D6iVR76+5/pV56yMNYTag==
+  dependencies:
+    base64-js "^1.0.2"
+    ieee754 "^1.1.4"
+
 builtin-modules@^1.0.0:
 builtin-modules@^1.0.0:
   version "1.1.1"
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
   resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
@@ -2835,6 +2891,16 @@ component-inherit@0.0.3:
   version "0.0.3"
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
   resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
 
 
+compress-commons@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-2.1.1.tgz#9410d9a534cf8435e3fbbb7c6ce48de2dc2f0610"
+  integrity sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==
+  dependencies:
+    buffer-crc32 "^0.2.13"
+    crc32-stream "^3.0.1"
+    normalize-path "^3.0.0"
+    readable-stream "^2.3.6"
+
 concat-map@0.0.1:
 concat-map@0.0.1:
   version "0.0.1"
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -3064,6 +3130,21 @@ cosmiconfig@^5.0.0:
     js-yaml "^3.9.0"
     js-yaml "^3.9.0"
     parse-json "^4.0.0"
     parse-json "^4.0.0"
 
 
+crc32-stream@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-3.0.1.tgz#cae6eeed003b0e44d739d279de5ae63b171b4e85"
+  integrity sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==
+  dependencies:
+    crc "^3.4.4"
+    readable-stream "^3.4.0"
+
+crc@^3.4.4:
+  version "3.8.0"
+  resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6"
+  integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==
+  dependencies:
+    buffer "^5.1.0"
+
 create-ecdh@^4.0.0:
 create-ecdh@^4.0.0:
   version "4.0.0"
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
   resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
@@ -3845,7 +3926,7 @@ encoding@^0.1.11:
   dependencies:
   dependencies:
     iconv-lite "~0.4.13"
     iconv-lite "~0.4.13"
 
 
-end-of-stream@^1.0.0, end-of-stream@^1.1.0:
+end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:
   version "1.4.1"
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
   resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
   dependencies:
   dependencies:
@@ -4885,6 +4966,11 @@ from@~0:
   version "0.1.7"
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
   resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
 
 
+fs-constants@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
+  integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
+
 fs-extra@3.0.1:
 fs-extra@3.0.1:
   version "3.0.1"
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291"
@@ -5248,6 +5334,11 @@ graceful-fs@^4.1.15:
   version "4.1.15"
   version "4.1.15"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
 
 
+graceful-fs@^4.2.0:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02"
+  integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==
+
 growi-commons@^4.0.7:
 growi-commons@^4.0.7:
   version "4.0.7"
   version "4.0.7"
   resolved "https://registry.yarnpkg.com/growi-commons/-/growi-commons-4.0.7.tgz#f9ff9c2f6afe3a9982b689d368e8e7a000d137e8"
   resolved "https://registry.yarnpkg.com/growi-commons/-/growi-commons-4.0.7.tgz#f9ff9c2f6afe3a9982b689d368e8e7a000d137e8"
@@ -6984,6 +7075,13 @@ lazy-cache@^1.0.3:
   version "1.0.4"
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
   resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
 
 
+lazystream@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4"
+  integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=
+  dependencies:
+    readable-stream "^2.0.5"
+
 lcid@^1.0.0:
 lcid@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
   resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
@@ -7149,6 +7247,21 @@ lodash.debounce@^4.0.8:
   version "4.0.8"
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
   resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
 
 
+lodash.defaults@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
+  integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
+
+lodash.difference@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c"
+  integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=
+
+lodash.flatten@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
+  integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
+
 lodash.foreach@^4.1.0:
 lodash.foreach@^4.1.0:
   version "4.5.0"
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
   resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
@@ -7170,6 +7283,11 @@ lodash.isfinite@^3.3.2:
   version "3.3.2"
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
   resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
 
 
+lodash.isplainobject@^4.0.6:
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+  integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
+
 lodash.memoize@^4.1.2:
 lodash.memoize@^4.1.2:
   version "4.1.2"
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -7187,6 +7305,11 @@ lodash.sortby@^4.7.0:
   resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
   resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
   integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
   integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
 
 
+lodash.union@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
+  integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=
+
 lodash.uniq@^4.5.0:
 lodash.uniq@^4.5.0:
   version "4.5.0"
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
@@ -7207,7 +7330,7 @@ lodash@^4.17.10, lodash@^4.17.5:
   version "4.17.10"
   version "4.17.10"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
 
 
-lodash@^4.17.15:
+lodash@^4.17.14, lodash@^4.17.15:
   version "4.17.15"
   version "4.17.15"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
   integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
   integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -10016,7 +10139,7 @@ read-pkg@^3.0.0:
     normalize-package-data "^2.3.2"
     normalize-package-data "^2.3.2"
     path-type "^3.0.0"
     path-type "^3.0.0"
 
 
-"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.4, readable-stream@^2.1.5:
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.3.6:
   version "2.3.6"
   version "2.3.6"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
   dependencies:
   dependencies:
@@ -10058,6 +10181,15 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable
     string_decoder "~1.0.3"
     string_decoder "~1.0.3"
     util-deprecate "~1.0.1"
     util-deprecate "~1.0.1"
 
 
+readable-stream@^3.0.1, readable-stream@^3.4.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc"
+  integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
 readable-stream@^3.1.1:
 readable-stream@^3.1.1:
   version "3.2.0"
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d"
@@ -11764,6 +11896,17 @@ tapable@^1.1.3:
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
   integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
   integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
 
 
+tar-stream@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3"
+  integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw==
+  dependencies:
+    bl "^3.0.0"
+    end-of-stream "^1.4.1"
+    fs-constants "^1.0.0"
+    inherits "^2.0.3"
+    readable-stream "^3.1.1"
+
 tar@^2.0.0:
 tar@^2.0.0:
   version "2.2.1"
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
   resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
@@ -13093,3 +13236,12 @@ z-schema@^3.23.0:
     validator "^10.0.0"
     validator "^10.0.0"
   optionalDependencies:
   optionalDependencies:
     commander "^2.7.1"
     commander "^2.7.1"
+
+zip-stream@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-2.1.2.tgz#841efd23214b602ff49c497cba1a85d8b5fbc39c"
+  integrity sha512-ykebHGa2+uzth/R4HZLkZh3XFJzivhVsjJt8bN3GvBzLaqqrUdRacu+c4QtnUgjkkQfsOuNE1JgLKMCPNmkKgg==
+  dependencies:
+    archiver-utils "^2.1.0"
+    compress-commons "^2.1.1"
+    readable-stream "^3.4.0"