瀏覽代碼

Store files to S3. In Progress.

Thanks to xet7 !

Related #142
Lauri Ojansivu 2 年之前
父節點
當前提交
21e2eabd60

+ 2 - 1
.devcontainer/Dockerfile

@@ -142,7 +142,8 @@ ENV \
     SAML_IDENTIFIER_FORMAT="" \
     SAML_LOCAL_PROFILE_MATCH_ATTRIBUTE="" \
     SAML_ATTRIBUTES="" \
-    DEFAULT_WAIT_SPINNER=""
+    DEFAULT_WAIT_SPINNER="" \
+    S3=""
 # \
 #    NODE_OPTIONS="--max_old_space_size=4096"
 

+ 2 - 1
Dockerfile

@@ -157,7 +157,8 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
     SAML_ATTRIBUTES="" \
     ORACLE_OIM_ENABLED=false \
     WAIT_SPINNER="" \
-    WRITABLE_PATH=/data
+    WRITABLE_PATH=/data \
+    S3=""
 
 #   NODE_OPTIONS="--max_old_space_size=4096" \
 

+ 8 - 0
docker-compose.yml

@@ -155,6 +155,14 @@ services:
       # ==== WRITEABLE PATH FOR FILE UPLOADS ====
       - WRITABLE_PATH=/data
       #-----------------------------------------------------------------
+      # ==== AWS S3 FOR FILES ====
+      # Any region. For example:
+      #   us-standard,us-west-1,us-west-2,
+      #   eu-west-1,eu-central-1,
+      #   ap-southeast-1,ap-northeast-1,sa-east-1
+      #
+      #- S3='{"s3":{"key": "xxx", "secret": "xxx", "bucket": "xxx", "region": "xxx"}}'
+      #-----------------------------------------------------------------
       # ==== MONGO_URL ====
       - MONGO_URL=mongodb://wekandb:27017/wekan
       #---------------------------------------------------------------

+ 30 - 1
models/lib/attachmentStoreStrategy.js

@@ -1,5 +1,5 @@
 import fs from 'fs';
-import FileStoreStrategy, {FileStoreStrategyFilesystem, FileStoreStrategyGridFs} from './fileStoreStrategy'
+import FileStoreStrategy, {FileStoreStrategyFilesystem, FileStoreStrategyGridFs, FileStoreStrategyS3} from './fileStoreStrategy'
 
 const insertActivity = (fileObj, activityType) =>
   Activities.insert({
@@ -70,3 +70,32 @@ export class AttachmentStoreStrategyFilesystem extends FileStoreStrategyFilesyst
     insertActivity(this.fileObj, 'deleteAttachment');
   }
 }
+
+/** Strategy to store attachments at filesystem */
+export class AttachmentStoreStrategyS3 extends FileStoreStrategyS3 {
+
+  /** constructor
+   * @param s3Bucket use this S3 Bucket
+   * @param fileObj the current file object
+   * @param versionName the current version
+   */
+  constructor(s3Bucket, fileObj, versionName) {
+    super(s3Bucket, fileObj, versionName);
+  }
+
+  /** after successfull upload */
+  onAfterUpload() {
+    super.onAfterUpload();
+    // If the attachment doesn't have a source field or its source is different than import
+    if (!this.fileObj.meta.source || this.fileObj.meta.source !== 'import') {
+      // Add activity about adding the attachment
+      insertActivity(this.fileObj, 'addAttachment');
+    }
+  }
+
+  /** after file remove */
+  onAfterRemove() {
+    super.onAfterRemove();
+    insertActivity(this.fileObj, 'deleteAttachment');
+  }
+}

+ 10 - 1
models/lib/fileStoreStrategy.js

@@ -6,6 +6,7 @@ import { ObjectID } from 'bson';
 
 export const STORAGE_NAME_FILESYSTEM = "fs";
 export const STORAGE_NAME_GRIDFS     = "gridfs";
+export const STORAGE_NAME_S3         = "s3";
 
 /** Factory for FileStoreStrategy */
 export default class FileStoreStrategyFactory {
@@ -15,12 +16,16 @@ export default class FileStoreStrategyFactory {
    * @param storagePath file storage path
    * @param classFileStoreStrategyGridFs use this strategy for GridFS storage
    * @param gridFsBucket use this GridFS Bucket as GridFS Storage
+   * @param classFileStoreStartegyS3 use this strategy for S3 storage
+   * @param s3Bucket use this S3 Bucket as S3 Storage
    */
   constructor(classFileStoreStrategyFilesystem, storagePath, classFileStoreStrategyGridFs, gridFsBucket) {
     this.classFileStoreStrategyFilesystem = classFileStoreStrategyFilesystem;
     this.storagePath = storagePath;
     this.classFileStoreStrategyGridFs = classFileStoreStrategyGridFs;
     this.gridFsBucket = gridFsBucket;
+    this.classFileStoreStrategyS3 = classFileStoreStrategyS3;
+    this.s3bucket = s3Bucket;
   }
 
   /** returns the right FileStoreStrategy
@@ -35,6 +40,8 @@ export default class FileStoreStrategyFactory {
         if (fileObj.meta.source == "import" || fileObj.versions[versionName].meta.gridFsFileId) {
           // uploaded by import, so it's in GridFS (MongoDB)
           storage = STORAGE_NAME_GRIDFS;
+        } else if (fileRef && fileRef.versions && fileRef.versions[version] && fileRef.versions[version].meta && fileRef.versions[version].meta.pipePath) {
+          storage = STORAGE_NAME_S3;
         } else {
           // newly uploaded, so it's at the filesystem
           storage = STORAGE_NAME_FILESYSTEM;
@@ -42,9 +49,11 @@ export default class FileStoreStrategyFactory {
       }
     }
     let ret;
-    if ([STORAGE_NAME_FILESYSTEM, STORAGE_NAME_GRIDFS].includes(storage)) {
+    if ([STORAGE_NAME_FILESYSTEM, STORAGE_NAME_GRIDFS, STORAGE_NAME_S3].includes(storage)) {
       if (storage == STORAGE_NAME_FILESYSTEM) {
         ret = new this.classFileStoreStrategyFilesystem(fileObj, versionName);
+      } else if (storage == STORAGE_NAME_S3) {
+        ret = new this.classFileStoreStrategyS3(this.s3Bucket, fileObj, versionName);
       } else if (storage == STORAGE_NAME_GRIDFS) {
         ret = new this.classFileStoreStrategyGridFs(this.gridFsBucket, fileObj, versionName);
       }

+ 187 - 0
models/lib/s3/createOnAfterUpload.js

@@ -0,0 +1,187 @@
+import { Meteor } from 'meteor/meteor';
+import { _ } from 'meteor/underscore';
+import { Random } from 'meteor/random';
+import { FilesCollection } from 'meteor/ostrio:files';
+import stream from 'stream';
+
+import S3 from 'aws-sdk/clients/s3'; /* http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html */
+/* See fs-extra and graceful-fs NPM packages */
+/* For better i/o performance */
+import fs from 'fs';
+
+/* Example: S3='{"s3":{"key": "xxx", "secret": "xxx", "bucket": "xxx", "region": "xxx""}}' meteor */
+if (process.env.S3) {
+  Meteor.settings.s3 = JSON.parse(process.env.S3).s3;
+
+const s3Conf = Meteor.settings.s3 || {};
+const bound  = Meteor.bindEnvironment((callback) => {
+  return callback();
+});
+
+/* Check settings existence in `Meteor.settings` */
+/* This is the best practice for app security */
+if (s3Conf && s3Conf.key && s3Conf.secret && s3Conf.bucket && s3Conf.region) {
+  // Create a new S3 object
+  const s3 = new S3({
+    secretAccessKey: s3Conf.secret,
+    accessKeyId: s3Conf.key,
+    region: s3Conf.region,
+    // sslEnabled: true, // optional
+    httpOptions: {
+      timeout: 6000,
+      agent: false
+    }
+  });
+
+  // Declare the Meteor file collection on the Server
+  const UserFiles = new FilesCollection({
+    debug: false, // Change to `true` for debugging
+    storagePath: 'assets/app/uploads/uploadedFiles',
+    collectionName: 'userFiles',
+    // Disallow Client to execute remove, use the Meteor.method
+    allowClientCode: false,
+
+    // Start moving files to AWS:S3
+    // after fully received by the Meteor server
+    onAfterUpload(fileRef) {
+      // Run through each of the uploaded file
+      _.each(fileRef.versions, (vRef, version) => {
+        // We use Random.id() instead of real file's _id
+        // to secure files from reverse engineering on the AWS client
+        const filePath = 'files/' + (Random.id()) + '-' + version + '.' + fileRef.extension;
+
+        // Create the AWS:S3 object.
+        // Feel free to change the storage class from, see the documentation,
+        // `STANDARD_IA` is the best deal for low access files.
+        // Key is the file name we are creating on AWS:S3, so it will be like files/XXXXXXXXXXXXXXXXX-original.XXXX
+        // Body is the file stream we are sending to AWS
+        s3.putObject({
+          // ServerSideEncryption: 'AES256', // Optional
+          StorageClass: 'STANDARD',
+          Bucket: s3Conf.bucket,
+          Key: filePath,
+          Body: fs.createReadStream(vRef.path),
+          ContentType: vRef.type,
+        }, (error) => {
+          bound(() => {
+            if (error) {
+              console.error(error);
+            } else {
+              // Update FilesCollection with link to the file at AWS
+              const upd = { $set: {} };
+              upd['$set']['versions.' + version + '.meta.pipePath'] = filePath;
+
+              this.collection.update({
+                _id: fileRef._id
+              }, upd, (updError) => {
+                if (updError) {
+                  console.error(updError);
+                } else {
+                  // Unlink original files from FS after successful upload to AWS:S3
+                  this.unlink(this.collection.findOne(fileRef._id), version);
+                }
+              });
+            }
+          });
+        });
+      });
+    },
+
+
+    // Intercept access to the file
+    // And redirect request to AWS:S3
+    interceptDownload(http, fileRef, version) {
+      let path;
+
+      if (fileRef && fileRef.versions && fileRef.versions[version] && fileRef.versions[version].meta && fileRef.versions[version].meta.pipePath) {
+        path = fileRef.versions[version].meta.pipePath;
+      }
+
+      if (path) {
+        // If file is successfully moved to AWS:S3
+        // We will pipe request to AWS:S3
+        // So, original link will stay always secure
+
+        // To force ?play and ?download parameters
+        // and to keep original file name, content-type,
+        // content-disposition, chunked "streaming" and cache-control
+        // we're using low-level .serve() method
+        const opts = {
+          Bucket: s3Conf.bucket,
+          Key: path
+        };
+
+        if (http.request.headers.range) {
+          const vRef  = fileRef.versions[version];
+          let range   = _.clone(http.request.headers.range);
+          const array = range.split(/bytes=([0-9]*)-([0-9]*)/);
+          const start = parseInt(array[1]);
+          let end     = parseInt(array[2]);
+          if (isNaN(end)) {
+            // Request data from AWS:S3 by small chunks
+            end       = (start + this.chunkSize) - 1;
+            if (end >= vRef.size) {
+              end     = vRef.size - 1;
+            }
+          }
+          opts.Range   = `bytes=${start}-${end}`;
+          http.request.headers.range = `bytes=${start}-${end}`;
+        }
+
+        const fileColl = this;
+        s3.getObject(opts, function (error) {
+          if (error) {
+            console.error(error);
+            if (!http.response.finished) {
+              http.response.end();
+            }
+          } else {
+            if (http.request.headers.range && this.httpResponse.headers['content-range']) {
+              // Set proper range header in according to what is returned from AWS:S3
+              http.request.headers.range = this.httpResponse.headers['content-range'].split('/')[0].replace('bytes ', 'bytes=');
+            }
+
+            const dataStream = new stream.PassThrough();
+            fileColl.serve(http, fileRef, fileRef.versions[version], version, dataStream);
+            dataStream.end(this.data.Body);
+          }
+        });
+
+        return true;
+      }
+      // While file is not yet uploaded to AWS:S3
+      // It will be served file from FS
+      return false;
+    }
+  });
+
+  // Intercept FilesCollection's remove method to remove file from AWS:S3
+  const _origRemove = UserFiles.remove;
+  UserFiles.remove = function (selector, callback) {
+    const cursor = this.collection.find(selector);
+    cursor.forEach((fileRef) => {
+      _.each(fileRef.versions, (vRef) => {
+        if (vRef && vRef.meta && vRef.meta.pipePath) {
+          // Remove the object from AWS:S3 first, then we will call the original FilesCollection remove
+          s3.deleteObject({
+            Bucket: s3Conf.bucket,
+            Key: vRef.meta.pipePath,
+          }, (error) => {
+            bound(() => {
+              if (error) {
+                console.error(error);
+              }
+            });
+          });
+        }
+      });
+    });
+
+    // Remove original file from database
+    _origRemove.call(this, selector, callback);
+  };
+} else {
+  throw new Meteor.Error(401, 'Missing Meteor file settings');
+}
+
+}

+ 266 - 65
package-lock.json

@@ -389,9 +389,9 @@
       "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="
     },
     "@types/node": {
-      "version": "14.18.34",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.34.tgz",
-      "integrity": "sha512-hcU9AIQVHmPnmjRK+XUUYlILlr9pQrsqSrwov/JK1pnf3GTQowVBhx54FbvM0AU/VXGH4i3+vgXS5EguR7fysA=="
+      "version": "14.18.35",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.35.tgz",
+      "integrity": "sha512-2ATO8pfhG1kDvw4Lc4C0GXIMSQFFJBCo/R1fSgTwmUlq5oy95LXyjDQinsRVgQY6gp6ghh3H91wk9ES5/5C+Tw=="
     },
     "@wekanteam/cli-table3": {
       "version": "0.8.0",
@@ -590,6 +590,35 @@
       "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
       "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
     },
+    "available-typed-arrays": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+      "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw=="
+    },
+    "aws-sdk": {
+      "version": "2.1280.0",
+      "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1280.0.tgz",
+      "integrity": "sha512-6Dv/RuMQdye7wps4HwcMZwRw9han51PQVcMXq4e5KUNqzQ9wHmaFMFVI/kY4NIkR7D3IczEg4WoUCoKhj464hA==",
+      "requires": {
+        "buffer": "4.9.2",
+        "events": "1.1.1",
+        "ieee754": "1.1.13",
+        "jmespath": "0.16.0",
+        "querystring": "0.2.0",
+        "sax": "1.2.1",
+        "url": "0.10.3",
+        "util": "^0.12.4",
+        "uuid": "8.0.0",
+        "xml2js": "0.4.19"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "8.0.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz",
+          "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw=="
+        }
+      }
+    },
     "babel-plugin-istanbul": {
       "version": "6.1.1",
       "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
@@ -669,6 +698,17 @@
         "buffer": "^5.5.0",
         "inherits": "^2.0.4",
         "readable-stream": "^3.4.0"
+      },
+      "dependencies": {
+        "buffer": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+          "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+          "requires": {
+            "base64-js": "^1.3.1",
+            "ieee754": "^1.1.13"
+          }
+        }
       }
     },
     "bluebird": {
@@ -707,15 +747,27 @@
       "integrity": "sha512-VrlEE4vuiO1WTpfof4VmaVolCVYkYTgB9iWgYNOrVlnifpME/06fhFRmONgBhClD5pFC1t9ZWqFUQEQAzY43bA==",
       "requires": {
         "buffer": "^5.6.0"
+      },
+      "dependencies": {
+        "buffer": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+          "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+          "requires": {
+            "base64-js": "^1.3.1",
+            "ieee754": "^1.1.13"
+          }
+        }
       }
     },
     "buffer": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
-      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+      "version": "4.9.2",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
+      "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
       "requires": {
-        "base64-js": "^1.3.1",
-        "ieee754": "^1.1.13"
+        "base64-js": "^1.0.2",
+        "ieee754": "^1.1.4",
+        "isarray": "^1.0.0"
       }
     },
     "buffer-crc32": {
@@ -765,9 +817,9 @@
       "dev": true
     },
     "caniuse-lite": {
-      "version": "1.0.30001435",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001435.tgz",
-      "integrity": "sha512-kdCkUTjR+v4YAJelyiDTqiu82BDr4W4CP5sgTA0ZBmqn30XfS2ZghPLMowik9TPhS+psWJiUNxsqLyurDbmutA=="
+      "version": "1.0.30001441",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz",
+      "integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg=="
     },
     "chai": {
       "version": "4.3.7",
@@ -951,9 +1003,9 @@
       "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="
     },
     "d3": {
-      "version": "7.7.0",
-      "resolved": "https://registry.npmjs.org/d3/-/d3-7.7.0.tgz",
-      "integrity": "sha512-VEwHCMgMjD2WBsxeRGUE18RmzxT9Bn7ghDpzvTEvkLSBAKgTMydJjouZTjspgQfRHpPt/PB3EHWBa6SSyFQq4g==",
+      "version": "7.8.0",
+      "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.0.tgz",
+      "integrity": "sha512-a5rNemRadWkEfqnY5NsD4RdCP9vn8EIJ4I5Rl14U0uKH1SXqcNmk/h9aGaAF1O98lz6L9M0IeUcuPa9GUYbI5A==",
       "requires": {
         "d3-array": "3",
         "d3-axis": "3",
@@ -1094,9 +1146,9 @@
       "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="
     },
     "d3-geo": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.0.1.tgz",
-      "integrity": "sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz",
+      "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==",
       "requires": {
         "d3-array": "2.5.0 - 3"
       }
@@ -1161,11 +1213,11 @@
       "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="
     },
     "d3-shape": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz",
-      "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+      "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
       "requires": {
-        "d3-path": "1 - 3"
+        "d3-path": "^3.1.0"
       }
     },
     "d3-time": {
@@ -1223,9 +1275,9 @@
       }
     },
     "dayjs": {
-      "version": "1.11.6",
-      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.6.tgz",
-      "integrity": "sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ=="
+      "version": "1.11.7",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
+      "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
     },
     "debug": {
       "version": "4.3.4",
@@ -1236,9 +1288,9 @@
       }
     },
     "deep-eql": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.2.tgz",
-      "integrity": "sha512-gT18+YW4CcW/DBNTwAmqTtkJh7f9qqScu2qFVlx7kCoeY9tlBu9cUcr7+I+Z/noG8INehS3xQgLpTtd/QUTn4w==",
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
+      "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
       "dev": true,
       "requires": {
         "type-detect": "^4.0.0"
@@ -1416,6 +1468,11 @@
       "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
       "dev": true
     },
+    "events": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
+      "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw=="
+    },
     "exceljs": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.3.0.tgz",
@@ -1502,6 +1559,14 @@
       "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
       "dev": true
     },
+    "for-each": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+      "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+      "requires": {
+        "is-callable": "^1.1.3"
+      }
+    },
     "fs-constants": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -1513,6 +1578,16 @@
       "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
       "requires": {
         "minipass": "^3.0.0"
+      },
+      "dependencies": {
+        "minipass": {
+          "version": "3.3.6",
+          "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+          "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        }
       }
     },
     "fs.realpath": {
@@ -1615,6 +1690,14 @@
       "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
       "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
     },
+    "gopd": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+      "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+      "requires": {
+        "get-intrinsic": "^1.1.3"
+      }
+    },
     "graceful-fs": {
       "version": "4.2.10",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
@@ -1638,6 +1721,14 @@
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
       "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
     },
+    "has-tostringtag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+      "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+      "requires": {
+        "has-symbols": "^1.0.2"
+      }
+    },
     "has-unicode": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -1685,9 +1776,9 @@
       }
     },
     "ieee754": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
-      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+      "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
     },
     "immediate": {
       "version": "3.0.6",
@@ -1713,11 +1804,45 @@
       "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
       "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="
     },
+    "is-arguments": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
+      "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+      "requires": {
+        "call-bind": "^1.0.2",
+        "has-tostringtag": "^1.0.0"
+      }
+    },
+    "is-callable": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+      "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="
+    },
     "is-fullwidth-code-point": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
       "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
     },
+    "is-generator-function": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
+      "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+      "requires": {
+        "has-tostringtag": "^1.0.0"
+      }
+    },
+    "is-typed-array": {
+      "version": "1.1.10",
+      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
+      "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==",
+      "requires": {
+        "available-typed-arrays": "^1.0.5",
+        "call-bind": "^1.0.2",
+        "for-each": "^0.3.3",
+        "gopd": "^1.0.1",
+        "has-tostringtag": "^1.0.0"
+      }
+    },
     "isarray": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@@ -1742,6 +1867,11 @@
         "semver": "^6.3.0"
       }
     },
+    "jmespath": {
+      "version": "0.16.0",
+      "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
+      "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw=="
+    },
     "jquery": {
       "version": "2.2.4",
       "resolved": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz",
@@ -1797,9 +1927,9 @@
       "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
     },
     "json5": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
-      "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz",
+      "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ=="
     },
     "jszip": {
       "version": "3.10.1",
@@ -2975,9 +3105,9 @@
       "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="
     },
     "minipass": {
-      "version": "3.3.6",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
-      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.0.0.tgz",
+      "integrity": "sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==",
       "requires": {
         "yallist": "^4.0.0"
       }
@@ -2989,6 +3119,16 @@
       "requires": {
         "minipass": "^3.0.0",
         "yallist": "^4.0.0"
+      },
+      "dependencies": {
+        "minipass": {
+          "version": "3.3.6",
+          "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+          "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        }
       }
     },
     "mj-context-menu": {
@@ -3143,13 +3283,13 @@
       "optional": true
     },
     "nise": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.2.tgz",
-      "integrity": "sha512-+gQjFi8v+tkfCuSCxfURHLhRhniE/+IaYbIphxAN2JRR9SHKhY8hgXpaXiYfHdw+gcGe4buxgbprBQFab9FkhA==",
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz",
+      "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==",
       "dev": true,
       "requires": {
         "@sinonjs/commons": "^2.0.0",
-        "@sinonjs/fake-timers": "^7.0.4",
+        "@sinonjs/fake-timers": "^10.0.2",
         "@sinonjs/text-encoding": "^0.7.1",
         "just-extend": "^4.0.2",
         "path-to-regexp": "^1.7.0"
@@ -3165,23 +3305,12 @@
           }
         },
         "@sinonjs/fake-timers": {
-          "version": "7.1.2",
-          "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz",
-          "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==",
+          "version": "10.0.2",
+          "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz",
+          "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==",
           "dev": true,
           "requires": {
-            "@sinonjs/commons": "^1.7.0"
-          },
-          "dependencies": {
-            "@sinonjs/commons": {
-              "version": "1.8.6",
-              "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz",
-              "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==",
-              "dev": true,
-              "requires": {
-                "type-detect": "4.0.8"
-              }
-            }
+            "@sinonjs/commons": "^2.0.0"
           }
         },
         "isarray": {
@@ -3210,9 +3339,9 @@
       }
     },
     "node-releases": {
-      "version": "2.0.6",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
-      "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg=="
+      "version": "2.0.8",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
+      "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
     },
     "nodemailer": {
       "version": "6.8.0",
@@ -3420,6 +3549,11 @@
         "side-channel": "^1.0.4"
       }
     },
+    "querystring": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+      "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g=="
+    },
     "readable-stream": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@@ -3455,9 +3589,9 @@
           }
         },
         "minimatch": {
-          "version": "5.1.1",
-          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.1.tgz",
-          "integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==",
+          "version": "5.1.2",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz",
+          "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==",
           "requires": {
             "brace-expansion": "^2.0.1"
           }
@@ -3523,6 +3657,11 @@
         "sparse-bitfield": "^3.0.3"
       }
     },
+    "sax": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
+      "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="
+    },
     "saxes": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
@@ -3708,13 +3847,13 @@
       }
     },
     "tar": {
-      "version": "6.1.12",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.12.tgz",
-      "integrity": "sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw==",
+      "version": "6.1.13",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
+      "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==",
       "requires": {
         "chownr": "^2.0.0",
         "fs-minipass": "^2.0.0",
-        "minipass": "^3.0.0",
+        "minipass": "^4.0.0",
         "minizlib": "^2.1.1",
         "mkdirp": "^1.0.3",
         "yallist": "^4.0.0"
@@ -3763,6 +3902,13 @@
       "requires": {
         "@tokenizer/token": "^0.3.0",
         "ieee754": "^1.2.1"
+      },
+      "dependencies": {
+        "ieee754": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+          "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
+        }
       }
     },
     "tr46": {
@@ -3854,6 +4000,34 @@
         "punycode": "^2.1.0"
       }
     },
+    "url": {
+      "version": "0.10.3",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
+      "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==",
+      "requires": {
+        "punycode": "1.3.2",
+        "querystring": "0.2.0"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+          "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="
+        }
+      }
+    },
+    "util": {
+      "version": "0.12.5",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
+      "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
+      "requires": {
+        "inherits": "^2.0.3",
+        "is-arguments": "^1.0.4",
+        "is-generator-function": "^1.0.7",
+        "is-typed-array": "^1.1.3",
+        "which-typed-array": "^1.1.2"
+      }
+    },
     "util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -3959,6 +4133,19 @@
         "webidl-conversions": "^3.0.0"
       }
     },
+    "which-typed-array": {
+      "version": "1.1.9",
+      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz",
+      "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==",
+      "requires": {
+        "available-typed-arrays": "^1.0.5",
+        "call-bind": "^1.0.2",
+        "for-each": "^0.3.3",
+        "gopd": "^1.0.1",
+        "has-tostringtag": "^1.0.0",
+        "is-typed-array": "^1.1.10"
+      }
+    },
     "wicked-good-xpath": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz",
@@ -3977,6 +4164,20 @@
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
     },
+    "xml2js": {
+      "version": "0.4.19",
+      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
+      "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
+      "requires": {
+        "sax": ">=0.6.0",
+        "xmlbuilder": "~9.0.1"
+      }
+    },
+    "xmlbuilder": {
+      "version": "9.0.7",
+      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
+      "integrity": "sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ=="
+    },
     "xmlchars": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",

+ 1 - 0
package.json

@@ -27,6 +27,7 @@
     "@mapbox/node-pre-gyp": "^1.0.8",
     "@wekanteam/markdown-it-mermaid": "^0.6.2",
     "ajv": "^6.12.6",
+    "aws-sdk": "^2.1279.0",
     "babel-runtime": "^6.26.0",
     "bcryptjs": "^2.4.3",
     "bson": "^4.5.2",

文件差異過大導致無法顯示
+ 0 - 0
snap-src/bin/config


+ 5 - 0
snap-src/bin/wekan-help

@@ -15,6 +15,11 @@ echo -e "\t$ snap set $SNAP_NAME debug='true'"
 echo -e "\t-Disable the Debug of Wekan:"
 echo -e "\t$ snap unset $SNAP_NAME debug"
 echo -e "\n"
+echo -e "AWS S3 for files:"
+echo -e "\t$ snap set $NAP_NAME s3='{\"s3\":{\"key\": \"xxx\", \"secret\": \"xxx\", \"bucket\": \"xxx\", \"region\": \"eu-west-1\"}}'"
+echo -e "Disable S3:"
+echo -e "\t$ snap unset $SNAP_NAME s3"
+echo -e "\n"
 #echo -e "Writable path. Snap can not write outside of /var/snap/wekan/common sandbox directory."
 #echo -e "Default:"
 #echo -e "\t$ snap set $SNAP_NAME writable-path='\$SNAP_COMMON\files'"

+ 8 - 0
start-wekan.bat

@@ -15,6 +15,14 @@ REM Writable path for temporary saving attachments during migration to Meteor-Fi
 REM Create directory wekan-uploads
 SET WRITABLE_PATH=..
 
+REM # ==== AWS S3 FOR FILES ====
+REM # Any region. For example:
+REM #   us-standard,us-west-1,us-west-2,
+REM #   eu-west-1,eu-central-1,
+REM #   ap-southeast-1,ap-northeast-1,sa-east-1
+REM #
+REM SET S3='{"s3":{"key": "xxx", "secret": "xxx", "bucket": "xxx", "region": "eu-west-1"}}'
+
 REM # https://github.com/wekan/wekan/wiki/Troubleshooting-Mail
 REM SET MAIL_URL=smtps://username:password@email-smtp.eu-west-1.amazonaws.com:587/
 REM SET MAIL_FROM="Wekan Boards <info@example.com>"

+ 9 - 1
start-wekan.sh

@@ -11,7 +11,15 @@
       #---------------------------------------------
       # WRITEABLE PATH
       export WRITABLE_PATH=..
-      #---------------------------------------------
+      #-----------------------------------------------------------------
+      # ==== AWS S3 FOR FILES ====
+      # Any region. For example:
+      #   us-standard,us-west-1,us-west-2,
+      #   eu-west-1,eu-central-1,
+      #   ap-southeast-1,ap-northeast-1,sa-east-1
+      #
+      #export S3='{"s3":{"key": "xxx", "secret": "xxx", "bucket": "xxx", "region": "xxx"}}'
+      #-----------------------------------------------------------------
       # Production: https://example.com/wekan
       # Local: http://localhost:2000
       #export ipaddress=$(ifdata -pa eth0)

+ 8 - 0
torodb-postgresql/docker-compose.yml

@@ -169,6 +169,14 @@ services:
       # ==== WRITEABLE PATH FOR FILE UPLOADS ====
       - WRITABLE_PATH=/data
       #-----------------------------------------------------------------
+      # ==== AWS S3 FOR FILES ====
+      # Any region. For example:
+      #   us-standard,us-west-1,us-west-2,
+      #   eu-west-1,eu-central-1,
+      #   ap-southeast-1,ap-northeast-1,sa-east-1
+      #
+      #- S3='{"s3":{"key": "xxx", "secret": "xxx", "bucket": "xxx", "region": "xxx"}}'
+      #-----------------------------------------------------------------
       # ==== MONGO_URL ====
       - MONGO_URL=mongodb://mongodb:27017/wekan
       #---------------------------------------------------------------

部分文件因文件數量過多而無法顯示