浏览代码

Export to Excel XLSX. Does work, but does not export all fields yet correctly. In Progress.

Thanks to gameendman, alfredgu and xet7 !

Related #3173
Lauri Ojansivu 4 年之前
父节点
当前提交
855151a8d1

+ 4 - 0
client/components/sidebar/sidebar.jade

@@ -324,6 +324,10 @@ template(name="exportBoard")
       a.download-json-link(href="{{exportUrl}}", download="{{exportJsonFilename}}")
         i.fa.fa-share-alt
         | {{_ 'export-board-json'}}
+    li
+      a(href="{{exportUrlExcel}}", download="{{exportFilenameExcel}}")
+        i.fa.fa-share-alt
+        | {{_ 'export-board-excel'}}
     li
       a(href="{{exportCsvUrl}}", download="{{exportCsvFilename}}")
         i.fa.fa-share-alt

+ 17 - 0
client/components/sidebar/sidebar.js

@@ -435,6 +435,23 @@ BlazeComponent.extendComponent({
     };
     return FlowRouter.path('/api/boards/:boardId/export', params, queryParams);
   },
+  exportUrlExcel() {
+    const params = {
+      boardId: Session.get('currentBoard'),
+    };
+    const queryParams = {
+      authToken: Accounts._storedLoginToken(),
+    };
+    return FlowRouter.path(
+      '/api/boards/:boardId/exportExcel',
+      params,
+      queryParams,
+    );
+  },
+  exportFilenameExcel() {
+    const boardId = Session.get('currentBoard');
+    return `export-board-excel-${boardId}.xlsx`;
+  },
   exportCsvUrl() {
     const params = {
       boardId: Session.get('currentBoard'),

+ 1 - 1
client/components/sidebar/sidebarFilters.js

@@ -166,7 +166,7 @@ Template.disambiguateMultiMemberPopup.events({
 });
 
 Template.moveSelectionPopup.events({
-  'click .js-select-list'(event) {
+  'click .js-select-list'() {
     // Move the minicard to the end of the target list
     mutateSelectedCards('moveToEndOfList', { listId: this._id });
     EscapeActions.executeUpTo('multiselection');

+ 8 - 8
client/components/swimlanes/swimlanes.js

@@ -85,14 +85,14 @@ function initSortable(boardComponent, $listsDom) {
     },
   });
 
-  function userIsMember() {
-    return (
-      Meteor.user() &&
-      Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly() &&
-      !Meteor.user().isWorker()
-    );
-  }
+  //function userIsMember() {
+  //  return (
+  //    Meteor.user() &&
+  //    Meteor.user().isBoardMember() &&
+  //    !Meteor.user().isCommentOnly() &&
+  //    !Meteor.user().isWorker()
+  //  );
+  //}
 
   boardComponent.autorun(() => {
     let showDesktopDragHandles = false;

+ 7 - 1
i18n/en.i18n.json

@@ -327,6 +327,8 @@
   "export-board-json": "Export board to JSON",
   "export-board-csv": "Export board to CSV",
   "export-board-tsv": "Export board to TSV",
+  "export-board-excel": "Export board to Excel",
+  "user-can-not-export-excel": "User can not export Excel",
   "export-board-html": "Export board to HTML",
   "exportBoardPopup-title": "Export board",
   "sort": "Sort",
@@ -607,6 +609,7 @@
   "accounts-allowEmailChange": "Allow Email Change",
   "accounts-allowUserNameChange": "Allow Username Change",
   "createdAt": "Created at",
+  "modifiedAt": "Modified at",
   "verified": "Verified",
   "active": "Active",
   "card-received": "Received",
@@ -669,6 +672,7 @@
   "r-removed-from": "Removed from",
   "r-the-board": "the board",
   "r-list": "list",
+  "list": "list",
   "set-filter": "Set Filter",
   "r-moved-to": "Moved to",
   "r-moved-from": "Moved from",
@@ -909,5 +913,7 @@
   "globalSearch-instructions-notes-3": "Differing operators are *AND*ed together.  Only cards that match all of the differing operators are returned.\n`__operator_list__:Available __operator_label__:red` returns only cards in the list *Available* with a *red* label.",
   "globalSearch-instructions-notes-4": "Text searches are case insensitive.",
   "globalSearch-instructions-notes-5": "Currently archived cards are not searched.",
-  "link-to-search": "Link to this search"
+  "link-to-search": "Link to this search",
+  "excel-font": "Arial",
+  "number": "Number"
 }

+ 476 - 0
models/exportExcel.js

@@ -0,0 +1,476 @@
+if (Meteor.isServer) {
+  // todo XXX once we have a real API in place, move that route there
+  // todo XXX also  share the route definition between the client and the server
+  // so that we could use something like
+  // `ApiRoutes.path('boards/exportExcel', boardId)``
+  // on the client instead of copy/pasting the route path manually between the
+  // client and the server.
+  /**
+   * @operation exportExcel
+   * @tag Boards
+   *
+   * @summary This route is used to export the board Excel.
+   *
+   * @description If user is already logged-in, pass loginToken as param
+   * "authToken": '/api/boards/:boardId/exportExcel?authToken=:token'
+   *
+   * See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/
+   * for detailed explanations
+   *
+   * @param {string} boardId the ID of the board we are exporting
+   * @param {string} authToken the loginToken
+   */
+  const Excel = require('exceljs');
+  Picker.route('/api/boards/:boardId/exportExcel', function(params, req, res) {
+    const boardId = params.boardId;
+    let user = null;
+    //console.log('Excel');
+
+    const loginToken = params.query.authToken;
+    if (loginToken) {
+      const hashToken = Accounts._hashLoginToken(loginToken);
+      user = Meteor.users.findOne({
+        'services.resume.loginTokens.hashedToken': hashToken,
+      });
+    } else if (!Meteor.settings.public.sandstorm) {
+      Authentication.checkUserId(req.userId);
+      user = Users.findOne({
+        _id: req.userId,
+        isAdmin: true,
+      });
+    }
+    const exporterExcel = new ExporterExcel(boardId);
+    if (exporterExcel.canExport(user)) {
+      exporterExcel.build(res);
+    } else {
+      res.end(TAPi18n.__('user-can-not-export-excel'));
+    }
+  });
+}
+
+// exporter maybe is broken since Gridfs introduced, add fs and path
+
+export class ExporterExcel {
+  constructor(boardId) {
+    this._boardId = boardId;
+  }
+
+  build(res) {
+    const fs = Npm.require('fs');
+    const os = Npm.require('os');
+    const path = Npm.require('path');
+
+    const byBoard = {
+      boardId: this._boardId,
+    };
+    const byBoardNoLinked = {
+      boardId: this._boardId,
+      linkedId: {
+        $in: ['', null],
+      },
+    };
+    // we do not want to retrieve boardId in related elements
+    const noBoardId = {
+      fields: {
+        boardId: 0,
+      },
+    };
+    const result = {
+      _format: 'wekan-board-1.0.0',
+    };
+    _.extend(
+      result,
+      Boards.findOne(this._boardId, {
+        fields: {
+          stars: 0,
+        },
+      }),
+    );
+    result.lists = Lists.find(byBoard, noBoardId).fetch();
+    result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
+    result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();
+    result.customFields = CustomFields.find(
+      {
+        boardIds: {
+          $in: [this.boardId],
+        },
+      },
+      {
+        fields: {
+          boardId: 0,
+        },
+      },
+    ).fetch();
+    result.comments = CardComments.find(byBoard, noBoardId).fetch();
+    result.activities = Activities.find(byBoard, noBoardId).fetch();
+    result.rules = Rules.find(byBoard, noBoardId).fetch();
+    result.checklists = [];
+    result.checklistItems = [];
+    result.subtaskItems = [];
+    result.triggers = [];
+    result.actions = [];
+    result.cards.forEach(card => {
+      result.checklists.push(
+        ...Checklists.find({
+          cardId: card._id,
+        }).fetch(),
+      );
+      result.checklistItems.push(
+        ...ChecklistItems.find({
+          cardId: card._id,
+        }).fetch(),
+      );
+      result.subtaskItems.push(
+        ...Cards.find({
+          parentId: card._id,
+        }).fetch(),
+      );
+    });
+    result.rules.forEach(rule => {
+      result.triggers.push(
+        ...Triggers.find(
+          {
+            _id: rule.triggerId,
+          },
+          noBoardId,
+        ).fetch(),
+      );
+      result.actions.push(
+        ...Actions.find(
+          {
+            _id: rule.actionId,
+          },
+          noBoardId,
+        ).fetch(),
+      );
+    });
+
+    // we also have to export some user data - as the other elements only
+    // include id but we have to be careful:
+    // 1- only exports users that are linked somehow to that board
+    // 2- do not export any sensitive information
+    const users = {};
+    result.members.forEach(member => {
+      users[member.userId] = true;
+    });
+    result.lists.forEach(list => {
+      users[list.userId] = true;
+    });
+    result.cards.forEach(card => {
+      users[card.userId] = true;
+      if (card.members) {
+        card.members.forEach(memberId => {
+          users[memberId] = true;
+        });
+      }
+    });
+    result.comments.forEach(comment => {
+      users[comment.userId] = true;
+    });
+    result.activities.forEach(activity => {
+      users[activity.userId] = true;
+    });
+    result.checklists.forEach(checklist => {
+      users[checklist.userId] = true;
+    });
+    const byUserIds = {
+      _id: {
+        $in: Object.getOwnPropertyNames(users),
+      },
+    };
+    // we use whitelist to be sure we do not expose inadvertently
+    // some secret fields that gets added to User later.
+    const userFields = {
+      fields: {
+        _id: 1,
+        username: 1,
+        'profile.fullname': 1,
+        'profile.initials': 1,
+        'profile.avatarUrl': 1,
+      },
+    };
+    result.users = Users.find(byUserIds, userFields)
+      .fetch()
+      .map(user => {
+        // user avatar is stored as a relative url, we export absolute
+        if ((user.profile || {}).avatarUrl) {
+          user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl);
+        }
+        return user;
+      });
+
+    const jdata = result;
+    //init exceljs workbook
+    const workbook = new Excel.Workbook();
+    workbook.creator = TAPi18n.__('export-board');
+    workbook.lastModifiedBy = TAPi18n.__('export-board');
+    workbook.created = new Date();
+    workbook.modified = new Date();
+    workbook.lastPrinted = new Date();
+    const filename = `${jdata.title}.xlsx`;
+    //init worksheet
+    const worksheet = workbook.addWorksheet(jdata.title, {
+      properties: {
+        tabColor: {
+          argb: 'FFC0000',
+        },
+      },
+      pageSetup: {
+        paperSize: 9,
+        orientation: 'landscape',
+      },
+    });
+    //get worksheet
+    const ws = workbook.getWorksheet(jdata.title);
+    ws.properties.defaultRowHeight = 20;
+    //init columns
+    //Excel font. Western: Arial. zh-CN: 宋体
+    ws.columns = [
+      {
+        key: 'a',
+        width: 7,
+      },
+      {
+        key: 'b',
+        width: 16,
+      },
+      {
+        key: 'c',
+        width: 7,
+      },
+      {
+        key: 'd',
+        width: 14,
+        style: {
+          font: {
+            name: TAPi18n.__('excel-font'),
+            size: '10',
+          },
+          numFmt: 'yyyy/mm/dd hh:mm:ss',
+        },
+      },
+      {
+        key: 'e',
+        width: 14,
+        style: {
+          font: {
+            name: TAPi18n.__('excel-font'),
+            size: '10',
+          },
+          numFmt: 'yyyy/mm/dd hh:mm:ss',
+        },
+      },
+      {
+        key: 'f',
+        width: 10,
+      },
+      {
+        key: 'g',
+        width: 10,
+      },
+      {
+        key: 'h',
+        width: 18,
+      },
+    ];
+
+    //add title line
+    ws.mergeCells('A1:H1');
+    ws.getCell('A1').value = jdata.title;
+    ws.getCell('A1').style = {
+      font: {
+        name: TAPi18n.__('excel-font'),
+        size: '20',
+      },
+    };
+    ws.getCell('A1').alignment = {
+      vertical: 'middle',
+      horizontal: 'center',
+    };
+    ws.getRow(1).height = 40;
+    //get member info
+    let jmem = '';
+    const jmeml = {};
+    for (const i in jdata.users) {
+      jmem = `${jmem + jdata.users[i].profile.fullname},`;
+      jmeml[jdata.users[i]._id] = jdata.users[i].profile.fullname;
+    }
+    jmem = jmem.substr(0, jmem.length - 1);
+    //get kanban list info
+    const jlist = {};
+    for (const klist in jdata.lists) {
+      jlist[jdata.lists[k]._id] = jdata.lists[klist].title;
+    }
+    //get kanban label info
+    const jlabel = {};
+    for (const klabel in jdata.labels) {
+      jlabel[jdata.labels[k]._id] = jdata.labels[klabel].name;
+    }
+    //add data +8 hours
+    function add8hours(jdate) {
+      const curdate = new Date(jdate);
+      return new Date(curdate.setHours(curdate.getHours() + 8));
+    }
+    //add blank row
+    ws.addRow().values = ['', '', '', '', '', '', '', ''];
+    //add kanban info
+    ws.addRow().values = [
+      TAPi18n.__('createdAt'),
+      add8hours(jdata.createdAt),
+      TAPi18n.__('modifiedAt'),
+      add8hours(jdata.modifiedAt),
+      TAPi18n.__('r-member'),
+      jmem,
+    ];
+    ws.getRow(3).font = {
+      name: TAPi18n.__('excel-font'),
+      size: 10,
+      bold: true,
+    };
+    ws.getCell('B3').style = {
+      font: {
+        name: TAPi18n.__('excel-font'),
+        size: '10',
+        bold: true,
+      },
+      numFmt: 'yyyy/mm/dd hh:mm:ss',
+    };
+    //cell center
+    function cellCenter(cellno) {
+      ws.getCell(cellno).alignment = {
+        vertical: 'middle',
+        horizontal: 'center',
+        wrapText: true,
+      };
+    }
+    cellCenter('A3');
+    cellCenter('B3');
+    cellCenter('C3');
+    cellCenter('D3');
+    cellCenter('E3');
+    cellCenter('F3');
+    ws.getRow(3).height = 20;
+    //all border
+    function allBorder(cellno) {
+      ws.getCell(cellno).border = {
+        top: {
+          style: 'thin',
+        },
+        left: {
+          style: 'thin',
+        },
+        bottom: {
+          style: 'thin',
+        },
+        right: {
+          style: 'thin',
+        },
+      };
+    }
+    allBorder('A3');
+    allBorder('B3');
+    allBorder('C3');
+    allBorder('D3');
+    allBorder('E3');
+    allBorder('F3');
+    //add blank row
+    ws.addRow().values = ['', '', '', '', '', '', '', '', ''];
+    //add card title
+    //ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];
+    ws.addRow().values = [
+      TAPi18n.__('number'),
+      TAPi18n.__('title'),
+      TAPi18n.__('owner'),
+      TAPi18n.__('createdAt'),
+      TAPi18n.__('last-modified-at'),
+      TAPi18n.__('list'),
+      TAPi18n.__('description'),
+      TAPi18n.__('status'),
+    ];
+    ws.getRow(5).height = 20;
+    allBorder('A5');
+    allBorder('B5');
+    allBorder('C5');
+    allBorder('D5');
+    allBorder('E5');
+    allBorder('F5');
+    allBorder('G5');
+    allBorder('H5');
+    allBorder('I5');
+    cellCenter('A5');
+    cellCenter('B5');
+    cellCenter('C5');
+    cellCenter('D5');
+    cellCenter('E5');
+    cellCenter('F5');
+    cellCenter('G5');
+    cellCenter('H5');
+    cellCenter('I5');
+    ws.getRow(5).font = {
+      name: TAPi18n.__('excel-font'),
+      size: 12,
+      bold: true,
+    };
+    //add blank row
+    //add card info
+    for (const i in jdata.cards) {
+      const jcard = jdata.cards[i];
+      //get member info
+      let jcmem = '';
+      for (const j in jcard.members) {
+        jcmem += jmeml[jcard.members[j]];
+        jcmem += ' ';
+      }
+      //get card label info
+      let jclabel = '';
+      for (const jl in jcard.labelIds) {
+        jclabel += jlabel[jcard.labelIds[jl]];
+        jclabel += ' ';
+      }
+      //      console.log(jclabel);
+
+      //add card detail
+      const t = Number(i) + 1;
+      ws.addRow().values = [
+        t.toString(),
+        jcard.title,
+        jmeml[jcard.userId],
+        add8hours(jcard.createdAt),
+        add8hours(jcard.dateLastActivity),
+        jlist[jcard.listId],
+        jcmem,
+        jcard.description,
+        jclabel,
+      ];
+      const y = Number(i) + 6;
+      //ws.getRow(y).height = 25;
+      allBorder(`A${y}`);
+      allBorder(`B${y}`);
+      allBorder(`C${y}`);
+      allBorder(`D${y}`);
+      allBorder(`E${y}`);
+      allBorder(`F${y}`);
+      allBorder(`G${y}`);
+      allBorder(`H${y}`);
+      allBorder(`I${y}`);
+      cellCenter(`A${y}`);
+      ws.getCell(`B${y}`).alignment = {
+        wrapText: true,
+      };
+      ws.getCell(`H${y}`).alignment = {
+        wrapText: true,
+      };
+      ws.getCell(`I${y}`).alignment = {
+        wrapText: true,
+      };
+    }
+    //    var exporte=new Stream;
+    workbook.xlsx.write(res).then(function() {});
+    //     return exporte;
+  }
+
+  canExport(user) {
+    const board = Boards.findOne(this._boardId);
+    return board && board.isVisibleBy(user);
+  }
+}

+ 495 - 2
package-lock.json

@@ -693,6 +693,33 @@
         "to-fast-properties": "^2.0.0"
       }
     },
+    "@fast-csv/format": {
+      "version": "4.3.5",
+      "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz",
+      "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==",
+      "requires": {
+        "@types/node": "^14.0.1",
+        "lodash.escaperegexp": "^4.1.2",
+        "lodash.isboolean": "^3.0.3",
+        "lodash.isequal": "^4.5.0",
+        "lodash.isfunction": "^3.0.9",
+        "lodash.isnil": "^4.0.0"
+      }
+    },
+    "@fast-csv/parse": {
+      "version": "4.3.6",
+      "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz",
+      "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==",
+      "requires": {
+        "@types/node": "^14.0.1",
+        "lodash.escaperegexp": "^4.1.2",
+        "lodash.groupby": "^4.6.0",
+        "lodash.isfunction": "^3.0.9",
+        "lodash.isnil": "^4.0.0",
+        "lodash.isundefined": "^3.0.1",
+        "lodash.uniq": "^4.5.0"
+      }
+    },
     "@root/request": {
       "version": "1.6.1",
       "resolved": "https://registry.npmjs.org/@root/request/-/request-1.6.1.tgz",
@@ -719,6 +746,11 @@
       "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==",
       "dev": true
     },
+    "@types/node": {
+      "version": "14.14.21",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.21.tgz",
+      "integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A=="
+    },
     "@typescript-eslint/experimental-utils": {
       "version": "1.13.0",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz",
@@ -823,6 +855,49 @@
       "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
       "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
     },
+    "archiver": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.2.0.tgz",
+      "integrity": "sha512-QEAKlgQuAtUxKeZB9w5/ggKXh21bZS+dzzuQ0RPBC20qtDCbTyzqmisoeJP46MP39fg4B4IcyvR+yeyEBdblsQ==",
+      "requires": {
+        "archiver-utils": "^2.1.0",
+        "async": "^3.2.0",
+        "buffer-crc32": "^0.2.1",
+        "readable-stream": "^3.6.0",
+        "readdir-glob": "^1.0.0",
+        "tar-stream": "^2.1.4",
+        "zip-stream": "^4.0.4"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        }
+      }
+    },
+    "archiver-utils": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
+      "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
+      "requires": {
+        "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"
+      }
+    },
     "are-we-there-yet": {
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
@@ -911,6 +986,11 @@
       "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
       "dev": true
     },
+    "async": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
+      "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
+    },
     "atob": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -1029,6 +1109,20 @@
         "node-pre-gyp": "0.15.0"
       }
     },
+    "big-integer": {
+      "version": "1.6.48",
+      "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
+      "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w=="
+    },
+    "binary": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
+      "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=",
+      "requires": {
+        "buffers": "~0.1.1",
+        "chainsaw": "~0.1.0"
+      }
+    },
     "bl": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
@@ -1038,6 +1132,11 @@
         "safe-buffer": "^5.1.1"
       }
     },
+    "bluebird": {
+      "version": "3.4.7",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
+      "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM="
+    },
     "brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -1094,11 +1193,26 @@
         "ieee754": "^1.1.4"
       }
     },
+    "buffer-crc32": {
+      "version": "0.2.13",
+      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+      "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
+    },
     "buffer-from": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
       "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
     },
+    "buffer-indexof-polyfill": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
+      "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A=="
+    },
+    "buffers": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
+      "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s="
+    },
     "bunyan": {
       "version": "1.8.12",
       "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
@@ -1159,6 +1273,14 @@
       "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
       "dev": true
     },
+    "chainsaw": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
+      "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=",
+      "requires": {
+        "traverse": ">=0.3.0 <0.4"
+      }
+    },
     "chalk": {
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -1281,6 +1403,29 @@
       "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
       "dev": true
     },
+    "compress-commons": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.0.2.tgz",
+      "integrity": "sha512-qhd32a9xgzmpfoga1VQEiLEwdKZ6Plnpx5UCgIsf89FSolyJ7WnifY4Gtjgv5WR6hWAyRaHxC5MiEhU/38U70A==",
+      "requires": {
+        "buffer-crc32": "^0.2.13",
+        "crc32-stream": "^4.0.1",
+        "normalize-path": "^3.0.0",
+        "readable-stream": "^3.6.0"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        }
+      }
+    },
     "concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1373,6 +1518,36 @@
         }
       }
     },
+    "crc-32": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz",
+      "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==",
+      "requires": {
+        "exit-on-epipe": "~1.0.1",
+        "printj": "~1.1.0"
+      }
+    },
+    "crc32-stream": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.1.tgz",
+      "integrity": "sha512-FN5V+weeO/8JaXsamelVYO1PHyeCsuL3HcG4cqsj0ceARcocxalaShCsohZMSAF+db7UYFwBy1rARK/0oFItUw==",
+      "requires": {
+        "crc-32": "^1.2.0",
+        "readable-stream": "^3.4.0"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        }
+      }
+    },
     "cross-spawn": {
       "version": "6.0.5",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@@ -1397,6 +1572,11 @@
       "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
       "dev": true
     },
+    "dayjs": {
+      "version": "1.10.3",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.3.tgz",
+      "integrity": "sha512-/2fdLN987N8Ki7Id8BUN2nhuiRyxTLumQnSQf9CNncFCyqFsSKb9TNhzRYcC8K8eJSJOKvbvkImo/MKKhNi4iw=="
+    },
     "debug": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@@ -1517,6 +1697,14 @@
         "nan": "^2.14.0"
       }
     },
+    "duplexer2": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
+      "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
+      "requires": {
+        "readable-stream": "^2.0.2"
+      }
+    },
     "elegant-spinner": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz",
@@ -1529,6 +1717,14 @@
       "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
       "dev": true
     },
+    "end-of-stream": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+      "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+      "requires": {
+        "once": "^1.4.0"
+      }
+    },
     "entities": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
@@ -1892,6 +2088,77 @@
       "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
       "dev": true
     },
+    "exceljs": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.2.0.tgz",
+      "integrity": "sha512-r0/ClXRs3cQmMGJOQY6/ymnZSBSzeJL/LjAlKjY75ev1iQgf0LcmeFfTqFTFK0fADLAWieOMXe7abPoGYWI6hA==",
+      "requires": {
+        "archiver": "^5.0.0",
+        "dayjs": "^1.8.34",
+        "fast-csv": "^4.3.1",
+        "jszip": "^3.5.0",
+        "readable-stream": "^3.6.0",
+        "saxes": "^5.0.1",
+        "tmp": "^0.2.0",
+        "unzipper": "^0.10.11",
+        "uuid": "^8.3.0"
+      },
+      "dependencies": {
+        "jszip": {
+          "version": "3.5.0",
+          "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz",
+          "integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==",
+          "requires": {
+            "lie": "~3.3.0",
+            "pako": "~1.0.2",
+            "readable-stream": "~2.3.6",
+            "set-immediate-shim": "~1.0.1"
+          },
+          "dependencies": {
+            "readable-stream": {
+              "version": "2.3.7",
+              "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+              "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+              "requires": {
+                "core-util-is": "~1.0.0",
+                "inherits": "~2.0.3",
+                "isarray": "~1.0.0",
+                "process-nextick-args": "~2.0.0",
+                "safe-buffer": "~5.1.1",
+                "string_decoder": "~1.1.1",
+                "util-deprecate": "~1.0.1"
+              }
+            }
+          }
+        },
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        },
+        "rimraf": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+          "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+          "requires": {
+            "glob": "^7.1.3"
+          }
+        },
+        "tmp": {
+          "version": "0.2.1",
+          "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
+          "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
+          "requires": {
+            "rimraf": "^3.0.0"
+          }
+        }
+      }
+    },
     "execa": {
       "version": "0.9.0",
       "resolved": "https://registry.npmjs.org/execa/-/execa-0.9.0.tgz",
@@ -1920,6 +2187,11 @@
         }
       }
     },
+    "exit-on-epipe": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz",
+      "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw=="
+    },
     "expand-brackets": {
       "version": "2.1.4",
       "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
@@ -2072,6 +2344,15 @@
       "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz",
       "integrity": "sha1-4mifjzVvrWLMplo6kcXfX5VRaS8="
     },
+    "fast-csv": {
+      "version": "4.3.6",
+      "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz",
+      "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==",
+      "requires": {
+        "@fast-csv/format": "4.3.5",
+        "@fast-csv/parse": "4.3.6"
+      }
+    },
     "fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2211,6 +2492,11 @@
         "map-cache": "^0.2.2"
       }
     },
+    "fs-constants": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
+    },
     "fs-minipass": {
       "version": "1.2.7",
       "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
@@ -2224,6 +2510,17 @@
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
       "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
     },
+    "fstream": {
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
+      "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "inherits": "~2.0.0",
+        "mkdirp": ">=0.5 0",
+        "rimraf": "2"
+      }
+    },
     "function-bind": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -2301,8 +2598,7 @@
     "graceful-fs": {
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
-      "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
-      "dev": true
+      "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ=="
     },
     "gridfs-stream": {
       "version": "https://github.com/wekan/gridfs-stream/tarball/master",
@@ -2853,6 +3149,14 @@
       "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
       "dev": true
     },
+    "lazystream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz",
+      "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=",
+      "requires": {
+        "readable-stream": "^2.0.5"
+      }
+    },
     "ldap-filter": {
       "version": "0.3.3",
       "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz",
@@ -2955,6 +3259,11 @@
         }
       }
     },
+    "listenercount": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
+      "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc="
+    },
     "listr": {
       "version": "0.14.3",
       "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz",
@@ -3087,6 +3396,61 @@
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
       "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
     },
+    "lodash.defaults": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+      "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
+    },
+    "lodash.difference": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
+      "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw="
+    },
+    "lodash.escaperegexp": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
+      "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c="
+    },
+    "lodash.flatten": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
+      "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
+    },
+    "lodash.groupby": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz",
+      "integrity": "sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E="
+    },
+    "lodash.isboolean": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+      "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
+    },
+    "lodash.isequal": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+      "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
+    },
+    "lodash.isfunction": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
+      "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="
+    },
+    "lodash.isnil": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz",
+      "integrity": "sha1-SeKM1VkBNFjIFMVHnTxmOiG/qmw="
+    },
+    "lodash.isplainobject": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+      "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
+    },
+    "lodash.isundefined": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz",
+      "integrity": "sha1-I+89lTVWUgOmbO/VuDD4SJEa+0g="
+    },
     "lodash.merge": {
       "version": "4.6.2",
       "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -3099,6 +3463,16 @@
       "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=",
       "dev": true
     },
+    "lodash.union": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
+      "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg="
+    },
+    "lodash.uniq": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+      "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M="
+    },
     "log-symbols": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
@@ -4069,6 +4443,11 @@
         "validate-npm-package-license": "^3.0.1"
       }
     },
+    "normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
+    },
     "npm-bundled": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
@@ -4560,6 +4939,11 @@
         }
       }
     },
+    "printj": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
+      "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ=="
+    },
     "process-nextick-args": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -4633,6 +5017,14 @@
         "util-deprecate": "~1.0.1"
       }
     },
+    "readdir-glob": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz",
+      "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==",
+      "requires": {
+        "minimatch": "^3.0.4"
+      }
+    },
     "regenerator-runtime": {
       "version": "0.13.5",
       "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
@@ -4781,6 +5173,14 @@
       "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
       "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
     },
+    "saxes": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
+      "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
+      "requires": {
+        "xmlchars": "^2.2.0"
+      }
+    },
     "semver": {
       "version": "5.7.1",
       "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -4825,6 +5225,11 @@
         }
       }
     },
+    "setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
+    },
     "shebang-command": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@@ -5319,6 +5724,40 @@
         "yallist": "^3.0.3"
       }
     },
+    "tar-stream": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+      "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+      "requires": {
+        "bl": "^4.0.3",
+        "end-of-stream": "^1.4.1",
+        "fs-constants": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.1.1"
+      },
+      "dependencies": {
+        "bl": {
+          "version": "4.0.3",
+          "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz",
+          "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==",
+          "requires": {
+            "buffer": "^5.5.0",
+            "inherits": "^2.0.4",
+            "readable-stream": "^3.4.0"
+          }
+        },
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        }
+      }
+    },
     "text-table": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -5387,6 +5826,11 @@
         "repeat-string": "^1.6.1"
       }
     },
+    "traverse": {
+      "version": "0.3.9",
+      "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
+      "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk="
+    },
     "tslib": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
@@ -5471,6 +5915,23 @@
         }
       }
     },
+    "unzipper": {
+      "version": "0.10.11",
+      "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
+      "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
+      "requires": {
+        "big-integer": "^1.6.17",
+        "binary": "~0.3.0",
+        "bluebird": "~3.4.1",
+        "buffer-indexof-polyfill": "~1.0.0",
+        "duplexer2": "~0.1.4",
+        "fstream": "^1.0.12",
+        "graceful-fs": "^4.2.2",
+        "listenercount": "~1.0.1",
+        "readable-stream": "~2.3.6",
+        "setimmediate": "~1.0.4"
+      }
+    },
     "uri-js": {
       "version": "4.2.2",
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
@@ -5496,6 +5957,11 @@
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
       "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
     },
+    "uuid": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
+    },
     "validate-npm-package-license": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -5672,6 +6138,11 @@
         "mkdirp": "^0.5.1"
       }
     },
+    "xmlchars": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+      "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
+    },
     "xss": {
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.8.tgz",
@@ -5685,6 +6156,28 @@
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
       "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
+    },
+    "zip-stream": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.0.4.tgz",
+      "integrity": "sha512-a65wQ3h5gcQ/nQGWV1mSZCEzCML6EK/vyVPcrPNynySP1j3VBbQKh3nhC8CbORb+jfl2vXvh56Ul5odP1bAHqw==",
+      "requires": {
+        "archiver-utils": "^2.1.0",
+        "compress-commons": "^4.0.2",
+        "readable-stream": "^3.6.0"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        }
+      }
     }
   }
 }

+ 1 - 0
package.json

@@ -63,6 +63,7 @@
     "bson": "^4.0.3",
     "bunyan": "^1.8.12",
     "es6-promise": "^4.2.4",
+    "exceljs": "^4.2.0",
     "fibers": "^5.0.0",
     "flatted": "^3.0.4",
     "gridfs-stream": "https://github.com/wekan/gridfs-stream/tarball/master",