Browse Source

Merge branch 'feature-ostrio-files' of https://github.com/majus/wekan

Lauri Ojansivu 3 years ago
parent
commit
d00596f88a
42 changed files with 778 additions and 1565 deletions
  1. 1 1
      .devcontainer/Dockerfile
  2. 2 0
      .devcontainer/docker-compose.yml
  3. 7 4
      .meteor/packages
  4. 21 18
      .meteor/versions
  5. 0 3
      Dockerfile
  6. 3 3
      client/components/activities/activities.js
  7. 8 11
      client/components/cards/attachments.jade
  8. 44 96
      client/components/cards/attachments.js
  9. 0 5
      client/components/cards/attachments.styl
  10. 1 1
      client/components/cards/minicard.jade
  11. 6 0
      client/components/cards/minicard.js
  12. 20 41
      client/components/main/editor.js
  13. 2 2
      client/components/users/userAvatar.jade
  14. 24 32
      client/components/users/userAvatar.js
  15. 11 23
      client/lib/utils.js
  16. 4 4
      docker-compose.yml
  17. 0 914
      fix-download-unicode/cfs_access-point.txt
  18. 7 5
      models/activities.js
  19. 76 248
      models/attachments.js
  20. 116 0
      models/attachments_old.js
  21. 49 21
      models/avatars.js
  22. 29 0
      models/avatars_old.js
  23. 1 0
      models/boards.js
  24. 5 5
      models/cards.js
  25. 6 4
      models/exporter.js
  26. 47 0
      models/lib/fsHooks/createInterceptDownload.js
  27. 17 0
      models/lib/fsHooks/createOnAfterRemove.js
  28. 51 0
      models/lib/fsHooks/createOnAfterUpload.js
  29. 9 0
      models/lib/grid/createBucket.js
  30. 4 0
      models/lib/grid/createObjectId.js
  31. 24 36
      models/trelloCreator.js
  32. 23 70
      models/wekanCreator.js
  33. 0 1
      rebuild-wekan.bat
  34. 0 1
      releases/rebuild-release.sh
  35. 155 0
      server/migrations.js
  36. 2 1
      server/publications/avatars.js
  37. 2 2
      server/publications/boards.js
  38. 1 1
      server/publications/notifications.js
  39. 0 0
      snap-src/bin/config
  40. 0 6
      snap-src/bin/wekan-help
  41. 0 2
      stacksmith/user-scripts/build.sh
  42. 0 4
      torodb-postgresql/docker-compose.yml

+ 1 - 1
.devcontainer/Dockerfile

@@ -1,4 +1,4 @@
-FROM quay.io/wekan/ubuntu:groovy-20210115
+FROM ubuntu:rolling
 LABEL maintainer="sgr"
 
 ENV BUILD_DEPS="gnupg gosu libarchive-tools wget curl bzip2 g++ build-essential python git ca-certificates iproute2"

+ 2 - 0
.devcontainer/docker-compose.yml

@@ -33,10 +33,12 @@ services:
       - WITH_API=true
       - RICHER_CARD_COMMENT_EDITOR=true
       - BROWSER_POLICY_ENABLED=true
+      - WRITABLE_PATH=/data
     depends_on:
       - wekandb-dev
     volumes:
       - /etc/localtime:/etc/localtime:ro
+      - ./volumes/data:/data
       - ../client:/home/wekan/app/client
       - ../models:/home/wekan/app/models
       - ../config:/home/wekan/app/config

+ 7 - 4
.meteor/packages

@@ -17,7 +17,7 @@ es5-shim@4.8.0
 
 # Collections
 aldeed:collection2
-wekan-cfs-standard-packages
+cfs:standard-packages
 cottz:publish-relations
 dburles:collection-helpers
 idmontie:migrations
@@ -73,8 +73,8 @@ email@2.0.0
 horka:swipebox
 dynamic-import@0.6.0
 
-accounts-password@1.7.0
-wekan-cfs-gridfs
+accounts-password@1.6.2
+cfs:gridfs
 rzymek:fullcalendar
 momentjs:moment@2.22.2
 browser-policy-framing@1.1.0
@@ -89,7 +89,10 @@ meteorhacks:aggregate@1.3.0
 wekan-markdown
 konecty:mongo-counter
 percolate:synced-cron
-wekan-cfs-filesystem
+cfs:filesystem
+ostrio:cookies
+ostrio:files@2.0.1
+tmeasday:check-npm-versions
 steffo:meteor-accounts-saml
 rajit:bootstrap3-datepicker-fi
 rajit:bootstrap3-datepicker-ar

+ 21 - 18
.meteor/versions

@@ -23,7 +23,24 @@ browser-policy-framing@1.1.0
 caching-compiler@1.2.2
 caching-html-compiler@1.2.0
 callback-hook@1.3.0
+cfs:access-point@0.1.49
+cfs:base-package@0.0.30
+cfs:collection@0.5.5
+cfs:collection-filters@0.2.4
+cfs:data-man@0.0.6
+cfs:file@0.1.17
+cfs:filesystem@0.1.2
+cfs:gridfs@0.0.34
 cfs:http-methods@0.0.32
+cfs:http-publish@0.0.13
+cfs:power-queue@0.9.11
+cfs:reactive-list@0.0.9
+cfs:reactive-property@0.0.4
+cfs:standard-packages@0.5.10
+cfs:storage-adapter@0.2.4
+cfs:tempstore@0.1.6
+cfs:upload-http@0.0.20
+cfs:worker@0.1.5
 check@1.3.1
 chuangbo:cookie@1.1.0
 coagmano:stylus@1.1.0
@@ -117,6 +134,8 @@ oauth2@1.3.0
 observe-sequence@1.0.16
 ongoworks:speakingurl@1.1.0
 ordered-dict@1.1.0
+ostrio:cookies@2.7.0
+ostrio:files@2.0.1
 pascoual:pdfkit@1.0.7
 peerlibrary:assert@0.3.0
 peerlibrary:base-component@0.16.0
@@ -211,8 +230,10 @@ templating@1.4.0
 templating-compiler@1.4.1
 templating-runtime@1.4.0
 templating-tools@1.2.0
+tmeasday:check-npm-versions@1.0.2
 tracker@1.2.0
 twbs:bootstrap@3.3.6
+typescript@4.2.2
 ui@1.0.13
 underscore@1.0.10
 url@1.3.2
@@ -224,24 +245,6 @@ webapp-hashing@1.1.0
 wekan-accounts-cas@0.1.0
 wekan-accounts-lockout@1.0.0
 wekan-accounts-oidc@1.0.10
-wekan-cfs-access-point@0.1.50
-wekan-cfs-base-package@0.0.30
-wekan-cfs-collection@0.5.5
-wekan-cfs-collection-filters@0.2.4
-wekan-cfs-data-man@0.0.6
-wekan-cfs-file@0.1.17
-wekan-cfs-filesystem@0.1.2
-wekan-cfs-gridfs@0.0.34
-wekan-cfs-http-methods@0.0.32
-wekan-cfs-http-publish@0.0.13
-wekan-cfs-power-queue@0.9.11
-wekan-cfs-reactive-list@0.0.9
-wekan-cfs-reactive-property@0.0.4
-wekan-cfs-standard-packages@0.5.10
-wekan-cfs-storage-adapter@0.2.4
-wekan-cfs-tempstore@0.1.6
-wekan-cfs-upload-http@0.0.21
-wekan-cfs-worker@0.1.5
 wekan-ldap@0.0.2
 wekan-markdown@1.0.9
 wekan-oidc@1.0.12

+ 0 - 3
Dockerfile

@@ -31,7 +31,6 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
     ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90 \
     RICHER_CARD_COMMENT_EDITOR=false \
     CARD_OPENED_WEBHOOK_ENABLED=false \
-    ATTACHMENTS_STORE_PATH="" \
     MAX_IMAGE_PIXEL="" \
     IMAGE_COMPRESS_RATIO="" \
     NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE="" \
@@ -290,9 +289,7 @@ RUN \
     chmod u+w *.json && \
     gosu wekan:wekan npm install && \
     gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build && \
-    #cp /home/wekan/app/fix-download-unicode/cfs_access-point.txt /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
     #rm /home/wekan/app_build/bundle/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs && \
-    #chown wekan /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
     #Removed binary version of bcrypt because of security vulnerability that is not fixed yet.
     #https://github.com/wekan/wekan/commit/4b2010213907c61b0e0482ab55abb06f6a668eac
     #https://github.com/wekan/wekan/commit/7eeabf14be3c63fae2226e561ef8a0c1390c8d3c

+ 3 - 3
client/components/activities/activities.js

@@ -196,14 +196,14 @@ BlazeComponent.extendComponent({
     // trying to display url before file is stored generates js errors
     return (
       (attachment &&
-        attachment.url({ download: true }) &&
+        attachment.path &&
         Blaze.toHTML(
           HTML.A(
             {
-              href: attachment.url({ download: true }),
+              href: `${attachment.link()}?download=true`,
               target: '_blank',
             },
-            DOMPurify.sanitize(attachment.name()),
+            DOMPurify.sanitize(attachment.name),
           ),
         )) ||
       DOMPurify.sanitize(this.currentData().activity.attachmentName)

+ 8 - 11
client/components/cards/attachments.jade

@@ -11,9 +11,6 @@ template(name="previewClipboardImagePopup")
   img.preview-clipboard-image()
   button.primary.js-upload-pasted-image {{_ 'upload'}}
 
-template(name="previewAttachedImagePopup")
-  img.preview-large-image.js-large-image-clicked(src="{{url}}")
-
 template(name="attachmentDeletePopup")
   p {{_ "attachment-delete-pop"}}
   button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
@@ -22,31 +19,31 @@ template(name="attachmentsGalery")
   .attachments-galery
     each attachments
       .attachment-item
-        a.attachment-thumbnail.swipebox(href="{{url}}" title="{{name}}")
+        a.attachment-thumbnail.swipebox(href="{{link}}" title="{{name}}")
           if isUploaded
             if isImage
-              img.attachment-thumbnail-img(src="{{url}}")
+              img.attachment-thumbnail-img(src="{{link}}")
             else if($eq extension 'mp3')
                 video(width="100%" height="100%" controls="true")
-                  source(src="{{url}}" type="audio/mpeg")
+                  source(src="{{link}}" type="audio/mpeg")
             else if($eq extension 'ogg')
                 video(width="100%" height="100%" controls="true")
-                  source(src="{{url}}" type="video/ogg")
+                  source(src="{{link}}" type="video/ogg")
             else if($eq extension 'webm')
                 video(width="100%" height="100%" controls="true")
-                  source(src="{{url}}" type="video/webm")
+                  source(src="{{link}}" type="video/webm")
             else if($eq extension 'mp4')
                 video(width="100%" height="100%" controls="true")
-                  source(src="{{url}}" type="video/mp4")
+                  source(src="{{link}}" type="video/mp4")
             else
               span.attachment-thumbnail-ext= extension
           else
-            +spinner
+            span.attachment-thumbnail-ext= extension
         p.attachment-details
           = name
           span.file-size ({{fileSize size}} KB)
           span.attachment-details-actions
-            a.js-download(href="{{url download=true}}")
+            a.js-download(href="{{link}}?download=true", download="{{name}}")
               i.fa.fa-download
               | {{_ 'download'}}
             if currentUser.isBoardMember

+ 44 - 96
client/components/cards/attachments.js

@@ -13,35 +13,10 @@ Template.attachmentsGalery.events({
     event.stopPropagation();
   },
   'click .js-add-cover'() {
-    Cards.findOne(this.cardId).setCover(this._id);
+    Cards.findOne(this.meta.cardId).setCover(this._id);
   },
   'click .js-remove-cover'() {
-    Cards.findOne(this.cardId).unsetCover();
-  },
-  'click .js-preview-image'(event) {
-    Popup.open('previewAttachedImage').call(this, event);
-    // when multiple thumbnails, if click one then another very fast,
-    // we might get a wrong width from previous img.
-    // when popup reused, onRendered() won't be called, so we cannot get there.
-    // here make sure to get correct size when this img fully loaded.
-    const img = $('img.preview-large-image')[0];
-    if (!img) return;
-    const rePosPopup = () => {
-      const w = img.width;
-      const h = img.height;
-      // if the image is too large, we resize & center the popup.
-      if (w > 300) {
-        $('div.pop-over').css({
-          width: w + 20,
-          position: 'absolute',
-          left: (window.innerWidth - w) / 2,
-          top: (window.innerHeight - h) / 2,
-        });
-      }
-    };
-    const url = $(event.currentTarget).attr('src');
-    if (img.src === url && img.complete) rePosPopup();
-    else img.onload = rePosPopup;
+    Cards.findOne(this.meta.cardId).unsetCover();
   },
 });
 
@@ -54,59 +29,30 @@ Template.attachmentsGalery.helpers({
   },
 });
 
-Template.previewAttachedImagePopup.events({
-  'click .js-large-image-clicked'() {
-    Popup.back();
-  },
-});
-
 Template.cardAttachmentsPopup.events({
   'change .js-attach-file'(event) {
     const card = this;
-    const processFile = f => {
-      Utils.processUploadedAttachment(card, f, attachment => {
-        if (attachment && attachment._id && attachment.isImage()) {
-          card.setCover(attachment._id);
+    if (event.currentTarget.files && event.currentTarget.files[0]) {
+      const uploader = Attachments.insert(
+        {
+          file: event.currentTarget.files[0],
+          meta: Utils.getCommonAttachmentMetaFrom(card),
+          chunkSize: 'dynamic',
+        },
+        false,
+      );
+      uploader.on('uploaded', (error, fileRef) => {
+        if (!error) {
+          if (fileRef.isImage) {
+            card.setCover(fileRef._id);
+          }
         }
+      });
+      uploader.on('end', (error, fileRef) => {
         Popup.back();
       });
-    };
-
-    FS.Utility.eachFile(event, f => {
-      if (
-        MAX_IMAGE_PIXEL > 0 &&
-        typeof f.type === 'string' &&
-        f.type.match(/^image/)
-      ) {
-        // is image
-        const reader = new FileReader();
-        reader.onload = function(e) {
-          const dataurl = e && e.target && e.target.result;
-          if (dataurl !== undefined) {
-            Utils.shrinkImage({
-              dataurl,
-              maxSize: MAX_IMAGE_PIXEL,
-              ratio: COMPRESS_RATIO,
-              toBlob: true,
-              callback(blob) {
-                if (blob === false) {
-                  processFile(f);
-                } else {
-                  blob.name = f.name;
-                  processFile(blob);
-                }
-              },
-            });
-          } else {
-            // couldn't process it let other function handle it?
-            processFile(f);
-          }
-        };
-        reader.readAsDataURL(f);
-      } else {
-        processFile(f);
-      }
-    });
+      uploader.start();
+    }
   },
   'click .js-computer-upload'(event, templateInstance) {
     templateInstance.find('.js-attach-file').click();
@@ -154,30 +100,32 @@ Template.previewClipboardImagePopup.onRendered(() => {
 
 Template.previewClipboardImagePopup.events({
   'click .js-upload-pasted-image'() {
-    const results = pastedResults;
-    if (results && results.file) {
+    const card = this;
+    if (pastedResults && pastedResults.file) {
+      const file = pastedResults.file;
       window.oPasted = pastedResults;
-      const card = this;
-      const file = new FS.File(results.file);
-      if (!results.name) {
-        // if no filename, it's from clipboard. then we give it a name, with ext name from MIME type
-        if (typeof results.file.type === 'string') {
-          file.name(results.file.type.replace('image/', 'clipboard.'));
+      const uploader = Attachments.insert(
+        {
+          file,
+          meta: Utils.getCommonAttachmentMetaFrom(card),
+          fileName: file.name || file.type.replace('image/', 'clipboard.'),
+          chunkSize: 'dynamic',
+        },
+        false,
+      );
+      uploader.on('uploaded', (error, fileRef) => {
+        if (!error) {
+          if (fileRef.isImage) {
+            card.setCover(fileRef._id);
+          }
         }
-      }
-      file.updatedAt(new Date());
-      file.boardId = card.boardId;
-      file.cardId = card._id;
-      file.userId = Meteor.userId();
-      const attachment = Attachments.insert(file);
-
-      if (attachment && attachment._id && attachment.isImage()) {
-        card.setCover(attachment._id);
-      }
-
-      pastedResults = null;
-      $(document.body).pasteImageReader(() => {});
-      Popup.back();
+      });
+      uploader.on('end', (error, fileRef) => {
+        pastedResults = null;
+        $(document.body).pasteImageReader(() => {});
+        Popup.back();
+      });
+      uploader.start();
     }
   },
 });

+ 0 - 5
client/components/cards/attachments.styl

@@ -51,11 +51,6 @@
   display: block
   box-shadow: 0 1px 2px rgba(0,0,0,.2)
 
-.preview-large-image
-  max-width: 1000px
-  display: block
-  box-shadow: 0 1px 2px rgba(0,0,0,.2)
-
 .preview-clipboard-image
   width: 280px
   max-width: 100%;

+ 1 - 1
client/components/cards/minicard.jade

@@ -7,7 +7,7 @@ template(name="minicard")
       .handle
         .fa.fa-arrows
     if cover
-      .minicard-cover(style="background-image: url('{{cover.url}}');")
+      .minicard-cover(style="background-image: url('{{cover.link 'original' '/'}}?dummyReloadAfterSessionEstablished={{sess}}');")
     if labels
       .minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
         each labels

+ 6 - 0
client/components/cards/minicard.js

@@ -114,6 +114,12 @@ Template.minicard.helpers({
       return false;
     }
   },
+  // XXX resolve this nasty hack for https://github.com/veliovgroup/Meteor-Files/issues/763
+  sess() {
+    return Meteor.connection && Meteor.connection._lastSessionId
+      ? Meteor.connection._lastSessionId
+      : null;
+  },
 });
 
 BlazeComponent.extendComponent({

+ 20 - 41
client/components/main/editor.js

@@ -153,7 +153,6 @@ BlazeComponent.extendComponent({
                   });
                 }
               },
-
               onImageUpload(files) {
                 const $summernote = getSummernote(this);
                 if (files && files.length > 0) {
@@ -161,46 +160,26 @@ BlazeComponent.extendComponent({
                   const currentCard = Utils.getCurrentCard();
                   const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
                   const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
-                  const insertImage = src => {
-                    // process all image upload types to the description/comment window
-                    const img = document.createElement('img');
-                    img.src = src;
-                    img.setAttribute('width', '100%');
-                    $summernote.summernote('insertNode', img);
-                  };
-                  const processData = function(fileObj) {
-                    Utils.processUploadedAttachment(
-                      currentCard,
-                      fileObj,
-                      attachment => {
-                        if (
-                          attachment &&
-                          attachment._id &&
-                          attachment.isImage()
-                        ) {
-                          attachment.one('uploaded', function() {
-                            const maxTry = 3;
-                            const checkItvl = 500;
-                            let retry = 0;
-                            const checkUrl = function() {
-                              // even though uploaded event fired, attachment.url() is still null somehow //TODO
-                              const url = attachment.url();
-                              if (url) {
-                                insertImage(
-                                  `${location.protocol}//${location.host}${url}`,
-                                );
-                              } else {
-                                retry++;
-                                if (retry < maxTry) {
-                                  setTimeout(checkUrl, checkItvl);
-                                }
-                              }
-                            };
-                            checkUrl();
-                          });
-                        }
+                  const processUpload = function(file) {
+                    const uploader = Attachments.insert(
+                      {
+                        file,
+                        meta: Utils.getCommonAttachmentMetaFrom(card),
+                        chunkSize: 'dynamic',
                       },
+                      false,
                     );
+                    uploader.on('uploaded', (error, fileRef) => {
+                      if (!error) {
+                        if (fileRef.isImage) {
+                          const img = document.createElement('img');
+                          img.src = fileRef.link();
+                          img.setAttribute('width', '100%');
+                          $summernote.summernote('insertNode', img);
+                        }
+                      }
+                    });
+                    uploader.start();
                   };
                   if (MAX_IMAGE_PIXEL) {
                     const reader = new FileReader();
@@ -216,7 +195,7 @@ BlazeComponent.extendComponent({
                           callback(blob) {
                             if (blob !== false) {
                               blob.name = image.name;
-                              processData(blob);
+                              processUpload(blob);
                             }
                           },
                         });
@@ -224,7 +203,7 @@ BlazeComponent.extendComponent({
                     };
                     reader.readAsDataURL(image);
                   } else {
-                    processData(image);
+                    processUpload(image);
                   }
                 }
               },

+ 2 - 2
client/components/users/userAvatar.jade

@@ -85,7 +85,7 @@ template(name="changeAvatarPopup")
     each uploadedAvatars
       li: a.js-select-avatar
         .member
-          img.avatar.avatar-image(src="{{url avatarUrlOptions}}")
+          img.avatar.avatar-image(src="{{link}}?auth=false&brokenIsFine=true")
         | {{_ 'uploaded-avatar'}}
         if isSelected
           i.fa.fa-check
@@ -93,7 +93,7 @@ template(name="changeAvatarPopup")
           unless isSelected
             a.js-delete-avatar {{_ 'delete'}}
             |  -
-          = original.name
+          = name
     li: a.js-select-initials
       .member
         +userAvatarInitials(userId=currentUser._id)

+ 24 - 32
client/components/users/userAvatar.js

@@ -3,6 +3,7 @@ import Avatars from '/models/avatars';
 import Users from '/models/users';
 import Org from '/models/org';
 import Team from '/models/team';
+import { formatFleURL } from 'meteor/ostrio:files/lib';
 
 Template.userAvatar.helpers({
   userData() {
@@ -181,21 +182,14 @@ BlazeComponent.extendComponent({
     Meteor.subscribe('my-avatars');
   },
 
-  avatarUrlOptions() {
-    return {
-      auth: false,
-      brokenIsFine: true,
-    };
-  },
-
   uploadedAvatars() {
-    return Avatars.find({ userId: Meteor.userId() });
+    return Avatars.find({ userId: Meteor.userId() }).each();
   },
 
   isSelected() {
     const userProfile = Meteor.user().profile;
     const avatarUrl = userProfile && userProfile.avatarUrl;
-    const currentAvatarUrl = this.currentData().url(this.avatarUrlOptions());
+    const currentAvatarUrl = `${this.currentData().link()}?auth=false&brokenIsFine=true`;
     return avatarUrl === currentAvatarUrl;
   },
 
@@ -220,32 +214,30 @@ BlazeComponent.extendComponent({
           this.$('.js-upload-avatar-input').click();
         },
         'change .js-upload-avatar-input'(event) {
-          let file, fileUrl;
-
-          FS.Utility.eachFile(event, f => {
-            try {
-              file = Avatars.insert(new FS.File(f));
-              fileUrl = file.url(this.avatarUrlOptions());
-            } catch (e) {
-              this.setError('avatar-too-big');
-            }
-          });
-
-          if (fileUrl) {
-            this.setError('');
-            const fetchAvatarInterval = window.setInterval(() => {
-              $.ajax({
-                url: fileUrl,
-                success: () => {
-                  this.setAvatar(file.url(this.avatarUrlOptions()));
-                  window.clearInterval(fetchAvatarInterval);
-                },
-              });
-            }, 100);
+          const self = this;
+          if (event.currentTarget.files && event.currentTarget.files[0]) {
+            const uploader = Avatars.insert(
+              {
+                file: event.currentTarget.files[0],
+                chunkSize: 'dynamic',
+              },
+              false,
+            );
+            uploader.on('uploaded', (error, fileRef) => {
+              if (!error) {
+                self.setAvatar(
+                  `${formatFleURL(fileRef)}?auth=false&brokenIsFine=true`,
+                );
+              }
+            });
+            uploader.on('error', (error, fileData) => {
+              self.setError(error.reason);
+            });
+            uploader.start();
           }
         },
         'click .js-select-avatar'() {
-          const avatarUrl = this.currentData().url(this.avatarUrlOptions());
+          const avatarUrl = `${this.currentData().link()}?auth=false&brokenIsFine=true`;
           this.setAvatar(avatarUrl);
         },
         'click .js-select-initials'() {

+ 11 - 23
client/lib/utils.js

@@ -162,33 +162,21 @@ Utils = {
       })
     );
   },
-  MAX_IMAGE_PIXEL: Meteor.settings.public.MAX_IMAGE_PIXEL,
-  COMPRESS_RATIO: Meteor.settings.public.IMAGE_COMPRESS_RATIO,
-  processUploadedAttachment(card, fileObj, callback) {
-    const next = attachment => {
-      if (typeof callback === 'function') {
-        callback(attachment);
-      }
-    };
-    if (!card) {
-      return next();
-    }
-    const file = new FS.File(fileObj);
+  getCommonAttachmentMetaFrom(card) {
+    const meta = {};
     if (card.isLinkedCard()) {
-      file.boardId = Cards.findOne(card.linkedId).boardId;
-      file.cardId = card.linkedId;
+      meta.boardId = Cards.findOne(card.linkedId).boardId;
+      meta.cardId = card.linkedId;
     } else {
-      file.boardId = card.boardId;
-      file.swimlaneId = card.swimlaneId;
-      file.listId = card.listId;
-      file.cardId = card._id;
-    }
-    file.userId = Meteor.userId();
-    if (file.original) {
-      file.original.name = fileObj.name;
+      meta.boardId = card.boardId;
+      meta.swimlaneId = card.swimlaneId;
+      meta.listId = card.listId;
+      meta.cardId = card._id;
     }
-    return next(Attachments.insert(file));
+    return meta;
   },
+  MAX_IMAGE_PIXEL: Meteor.settings.public.MAX_IMAGE_PIXEL,
+  COMPRESS_RATIO: Meteor.settings.public.IMAGE_COMPRESS_RATIO,
   shrinkImage(options) {
     // shrink image to certain size
     const dataurl = options.dataurl,

+ 4 - 4
docker-compose.yml

@@ -247,10 +247,6 @@ services:
       # Defaults below. Uncomment to change. wekan/server/accounts-common.js
       # - ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90
       #---------------------------------------------------------------
-      # ==== STORE ATTACHMENT ON SERVER FILESYSTEM INSTEAD OF MONGODB ====
-      # https://github.com/wekan/wekan/pull/2603
-      #- ATTACHMENTS_STORE_PATH = <pathname> # pathname can be relative or fullpath
-      #---------------------------------------------------------------
       # ==== RICH TEXT EDITOR IN CARD COMMENTS ====
       # https://github.com/wekan/wekan/pull/2560
       - RICHER_CARD_COMMENT_EDITOR=false
@@ -329,6 +325,9 @@ services:
       # When browser policy is enabled, HTML code at this Trusted URL can have iframe that embeds Wekan inside.
       #- TRUSTED_URL=https://intra.example.com
       #-----------------------------------------------------------------
+      # ==== WRITEABLE PATH FOR FILE UPLOADS ====
+      - WRITABLE_PATH=/data
+      #-----------------------------------------------------------------
       # ==== OUTGOING WEBHOOKS ====
       # What to send to Outgoing Webhook, or leave out. If commented out the default values will be: cardId,listId,oldListId,boardId,comment,user,card,commentId,swimlaneId,customerField,customFieldValue
       #- WEBHOOKS_ATTRIBUTES=cardId,listId,oldListId,boardId,comment,user,card,commentId
@@ -674,6 +673,7 @@ services:
       - wekandb
     volumes:
       - /etc/localtime:/etc/localtime:ro
+      - ./volumes/data:/data
 
 #---------------------------------------------------------------------------------
 # ==== OPTIONAL: SHARE DATABASE TO OFFICE LAN AND REMOTE VPN ====

+ 0 - 914
fix-download-unicode/cfs_access-point.txt

@@ -1,914 +0,0 @@
-(function () {
-
-/* Imports */
-var Meteor = Package.meteor.Meteor;
-var global = Package.meteor.global;
-var meteorEnv = Package.meteor.meteorEnv;
-var FS = Package['wekan-cfs-base-package'].FS;
-var check = Package.check.check;
-var Match = Package.check.Match;
-var EJSON = Package.ejson.EJSON;
-var HTTP = Package['wekan-cfs-http-methods'].HTTP;
-
-/* Package-scope variables */
-var rootUrlPathPrefix, baseUrl, getHeaders, getHeadersByCollection, _existingMountPoints, mountUrls;
-
-(function(){
-
-///////////////////////////////////////////////////////////////////////
-//                                                                   //
-// packages/cfs_access-point/packages/cfs_access-point.js            //
-//                                                                   //
-///////////////////////////////////////////////////////////////////////
-                                                                     //
-(function () {
-
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-//                                                                                                                    //
-// packages/wekan-cfs-access-point/access-point-common.js                                                                   //
-//                                                                                                                    //
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-                                                                                                                      //
-rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || "";                                             // 1
-// Adjust the rootUrlPathPrefix if necessary                                                                          // 2
-if (rootUrlPathPrefix.length > 0) {                                                                                   // 3
-  if (rootUrlPathPrefix.slice(0, 1) !== '/') {                                                                        // 4
-    rootUrlPathPrefix = '/' + rootUrlPathPrefix;                                                                      // 5
-  }                                                                                                                   // 6
-  if (rootUrlPathPrefix.slice(-1) === '/') {                                                                          // 7
-    rootUrlPathPrefix = rootUrlPathPrefix.slice(0, -1);                                                               // 8
-  }                                                                                                                   // 9
-}                                                                                                                     // 10
-                                                                                                                      // 11
-// prepend ROOT_URL when isCordova                                                                                    // 12
-if (Meteor.isCordova) {                                                                                               // 13
-  rootUrlPathPrefix = Meteor.absoluteUrl(rootUrlPathPrefix.replace(/^\/+/, '')).replace(/\/+$/, '');                  // 14
-}                                                                                                                     // 15
-                                                                                                                      // 16
-baseUrl = '/cfs';                                                                                                     // 17
-FS.HTTP = FS.HTTP || {};                                                                                              // 18
-                                                                                                                      // 19
-// Note the upload URL so that client uploader packages know what it is                                               // 20
-FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files';                                                           // 21
-                                                                                                                      // 22
-/**                                                                                                                   // 23
- * @method FS.HTTP.setBaseUrl                                                                                         // 24
- * @public                                                                                                            // 25
- * @param {String} newBaseUrl - Change the base URL for the HTTP GET and DELETE endpoints.                            // 26
- * @returns {undefined}                                                                                               // 27
- */                                                                                                                   // 28
-FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) {                                                                // 29
-                                                                                                                      // 30
-  // Adjust the baseUrl if necessary                                                                                  // 31
-  if (newBaseUrl.slice(0, 1) !== '/') {                                                                               // 32
-    newBaseUrl = '/' + newBaseUrl;                                                                                    // 33
-  }                                                                                                                   // 34
-  if (newBaseUrl.slice(-1) === '/') {                                                                                 // 35
-    newBaseUrl = newBaseUrl.slice(0, -1);                                                                             // 36
-  }                                                                                                                   // 37
-                                                                                                                      // 38
-  // Update the base URL                                                                                              // 39
-  baseUrl = newBaseUrl;                                                                                               // 40
-                                                                                                                      // 41
-  // Change the upload URL so that client uploader packages know what it is                                           // 42
-  FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files';                                                         // 43
-                                                                                                                      // 44
-  // Remount URLs with the new baseUrl, unmounting the old, on the server only.                                       // 45
-  // If existingMountPoints is empty, then we haven't run the server startup                                          // 46
-  // code yet, so this new URL will be used at that point for the initial mount.                                      // 47
-  if (Meteor.isServer && !FS.Utility.isEmpty(_existingMountPoints)) {                                                 // 48
-    mountUrls();                                                                                                      // 49
-  }                                                                                                                   // 50
-};                                                                                                                    // 51
-                                                                                                                      // 52
-/*                                                                                                                    // 53
- * FS.File extensions                                                                                                 // 54
- */                                                                                                                   // 55
-                                                                                                                      // 56
-/**                                                                                                                   // 57
- * @method FS.File.prototype.url Construct the file url                                                               // 58
- * @public                                                                                                            // 59
- * @param {Object} [options]                                                                                          // 60
- * @param {String} [options.store] Name of the store to get from. If not defined, the first store defined in `options.stores` for the collection on the client is used.
- * @param {Boolean} [options.auth=null] Add authentication token to the URL query string? By default, a token for the current logged in user is added on the client. Set this to `false` to omit the token. Set this to a string to provide your own token. Set this to a number to specify an expiration time for the token in seconds.
- * @param {Boolean} [options.download=false] Should headers be set to force a download? Typically this means that clicking the link with this URL will download the file to the user's Downloads folder instead of displaying the file in the browser.
- * @param {Boolean} [options.brokenIsFine=false] Return the URL even if we know it's currently a broken link because the file hasn't been saved in the requested store yet.
- * @param {Boolean} [options.metadata=false] Return the URL for the file metadata access point rather than the file itself.
- * @param {String} [options.uploading=null] A URL to return while the file is being uploaded.                         // 66
- * @param {String} [options.storing=null] A URL to return while the file is being stored.                             // 67
- * @param {String} [options.filename=null] Override the filename that should appear at the end of the URL. By default it is the name of the file in the requested store.
- *                                                                                                                    // 69
- * Returns the HTTP URL for getting the file or its metadata.                                                         // 70
- */                                                                                                                   // 71
-FS.File.prototype.url = function(options) {                                                                           // 72
-  var self = this;                                                                                                    // 73
-  options = options || {};                                                                                            // 74
-  options = FS.Utility.extend({                                                                                       // 75
-    store: null,                                                                                                      // 76
-    auth: null,                                                                                                       // 77
-    download: false,                                                                                                  // 78
-    metadata: false,                                                                                                  // 79
-    brokenIsFine: false,                                                                                              // 80
-    uploading: null, // return this URL while uploading                                                               // 81
-    storing: null, // return this URL while storing                                                                   // 82
-    filename: null // override the filename that is shown to the user                                                 // 83
-  }, options.hash || options); // check for "hash" prop if called as helper                                           // 84
-                                                                                                                      // 85
-  // Primarily useful for displaying a temporary image while uploading an image                                       // 86
-  if (options.uploading && !self.isUploaded()) {                                                                      // 87
-    return options.uploading;                                                                                         // 88
-  }                                                                                                                   // 89
-                                                                                                                      // 90
-  if (self.isMounted()) {                                                                                             // 91
-    // See if we've stored in the requested store yet                                                                 // 92
-    var storeName = options.store || self.collection.primaryStore.name;                                               // 93
-    if (!self.hasStored(storeName)) {                                                                                 // 94
-      if (options.storing) {                                                                                          // 95
-        return options.storing;                                                                                       // 96
-      } else if (!options.brokenIsFine) {                                                                             // 97
-        // We want to return null if we know the URL will be a broken                                                 // 98
-        // link because then we can avoid rendering broken links, broken                                              // 99
-        // images, etc.                                                                                               // 100
-        return null;                                                                                                  // 101
-      }                                                                                                               // 102
-    }                                                                                                                 // 103
-                                                                                                                      // 104
-    // Add filename to end of URL if we can determine one                                                             // 105
-    var filename = options.filename || self.name({store: storeName});                                                 // 106
-    if (typeof filename === "string" && filename.length) {                                                            // 107
-      filename = '/' + filename;                                                                                      // 108
-    } else {                                                                                                          // 109
-      filename = '';                                                                                                  // 110
-    }                                                                                                                 // 111
-                                                                                                                      // 112
-    // TODO: Could we somehow figure out if the collection requires login?                                            // 113
-    var authToken = '';                                                                                               // 114
-    if (Meteor.isClient && typeof Accounts !== "undefined" && typeof Accounts._storedLoginToken === "function") {     // 115
-      if (options.auth !== false) {                                                                                   // 116
-        // Add reactive deps on the user                                                                              // 117
-        Meteor.userId();                                                                                              // 118
-                                                                                                                      // 119
-        var authObject = {                                                                                            // 120
-          authToken: Accounts._storedLoginToken() || ''                                                               // 121
-        };                                                                                                            // 122
-                                                                                                                      // 123
-        // If it's a number, we use that as the expiration time (in seconds)                                          // 124
-        if (options.auth === +options.auth) {                                                                         // 125
-          authObject.expiration = FS.HTTP.now() + options.auth * 1000;                                                // 126
-        }                                                                                                             // 127
-                                                                                                                      // 128
-        // Set the authToken                                                                                          // 129
-        var authString = JSON.stringify(authObject);                                                                  // 130
-        authToken = FS.Utility.btoa(authString);                                                                      // 131
-      }                                                                                                               // 132
-    } else if (typeof options.auth === "string") {                                                                    // 133
-      // If the user supplies auth token the user will be responsible for                                             // 134
-      // updating                                                                                                     // 135
-      authToken = options.auth;                                                                                       // 136
-    }                                                                                                                 // 137
-                                                                                                                      // 138
-    // Construct query string                                                                                         // 139
-    var params = {};                                                                                                  // 140
-    if (authToken !== '') {                                                                                           // 141
-      params.token = authToken;                                                                                       // 142
-    }                                                                                                                 // 143
-    if (options.download) {                                                                                           // 144
-      params.download = true;                                                                                         // 145
-    }                                                                                                                 // 146
-    if (options.store) {                                                                                              // 147
-      // We use options.store here instead of storeName because we want to omit the queryString                       // 148
-      // whenever possible, allowing users to have "clean" URLs if they want. The server will                         // 149
-      // assume the first store defined on the server, which means that we are assuming that                          // 150
-      // the first on the client is also the first on the server. If that's not the case, the                         // 151
-      // store option should be supplied.                                                                             // 152
-      params.store = options.store;                                                                                   // 153
-    }                                                                                                                 // 154
-    var queryString = FS.Utility.encodeParams(params);                                                                // 155
-    if (queryString.length) {                                                                                         // 156
-      queryString = '?' + queryString;                                                                                // 157
-    }                                                                                                                 // 158
-                                                                                                                      // 159
-    // Determine which URL to use                                                                                     // 160
-    var area;                                                                                                         // 161
-    if (options.metadata) {                                                                                           // 162
-      area = '/record';                                                                                               // 163
-    } else {                                                                                                          // 164
-      area = '/files';                                                                                                // 165
-    }                                                                                                                 // 166
-                                                                                                                      // 167
-    // Construct and return the http method url                                                                       // 168
-    return rootUrlPathPrefix + baseUrl + area + '/' + self.collection.name + '/' + self._id + filename + queryString; // 169
-  }                                                                                                                   // 170
-                                                                                                                      // 171
-};                                                                                                                    // 172
-                                                                                                                      // 173
-                                                                                                                      // 174
-                                                                                                                      // 175
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-}).call(this);
-
-
-
-
-
-
-(function () {
-
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-//                                                                                                                    //
-// packages/wekan-cfs-access-point/access-point-handlers.js                                                                 //
-//                                                                                                                    //
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-                                                                                                                      //
-getHeaders = [];                                                                                                      // 1
-getHeadersByCollection = {};                                                                                          // 2
-                                                                                                                      // 3
-FS.HTTP.Handlers = {};                                                                                                // 4
-                                                                                                                      // 5
-/**                                                                                                                   // 6
- * @method FS.HTTP.Handlers.Del                                                                                       // 7
- * @public                                                                                                            // 8
- * @returns {any} response                                                                                            // 9
- *                                                                                                                    // 10
- * HTTP DEL request handler                                                                                           // 11
- */                                                                                                                   // 12
-FS.HTTP.Handlers.Del = function httpDelHandler(ref) {                                                                 // 13
-  var self = this;                                                                                                    // 14
-  var opts = FS.Utility.extend({}, self.query || {}, self.params || {});                                              // 15
-                                                                                                                      // 16
-  // If DELETE request, validate with 'remove' allow/deny, delete the file, and return                                // 17
-  FS.Utility.validateAction(ref.collection.files._validators['remove'], ref.file, self.userId);                       // 18
-                                                                                                                      // 19
-  /*                                                                                                                  // 20
-   * From the DELETE spec:                                                                                            // 21
-   * A successful response SHOULD be 200 (OK) if the response includes an                                             // 22
-   * entity describing the status, 202 (Accepted) if the action has not                                               // 23
-   * yet been enacted, or 204 (No Content) if the action has been enacted                                             // 24
-   * but the response does not include an entity.                                                                     // 25
-   */                                                                                                                 // 26
-  self.setStatusCode(200);                                                                                            // 27
-                                                                                                                      // 28
-  return {                                                                                                            // 29
-    deleted: !!ref.file.remove()                                                                                      // 30
-  };                                                                                                                  // 31
-};                                                                                                                    // 32
-                                                                                                                      // 33
-/**                                                                                                                   // 34
- * @method FS.HTTP.Handlers.GetList                                                                                   // 35
- * @public                                                                                                            // 36
- * @returns {Object} response                                                                                         // 37
- *                                                                                                                    // 38
- * HTTP GET file list request handler                                                                                 // 39
- */                                                                                                                   // 40
-FS.HTTP.Handlers.GetList = function httpGetListHandler() {                                                            // 41
-  // Not Yet Implemented                                                                                              // 42
-  // Need to check publications and return file list based on                                                         // 43
-  // what user is allowed to see                                                                                      // 44
-};                                                                                                                    // 45
-                                                                                                                      // 46
-/*                                                                                                                    // 47
-  requestRange will parse the range set in request header - if not possible it                                        // 48
-  will throw fitting errors and autofill range for both partial and full ranges                                       // 49
-                                                                                                                      // 50
-  throws error or returns the object:                                                                                 // 51
-  {                                                                                                                   // 52
-    start                                                                                                             // 53
-    end                                                                                                               // 54
-    length                                                                                                            // 55
-    unit                                                                                                              // 56
-    partial                                                                                                           // 57
-  }                                                                                                                   // 58
-*/                                                                                                                    // 59
-var requestRange = function(req, fileSize) {                                                                          // 60
-  if (req) {                                                                                                          // 61
-    if (req.headers) {                                                                                                // 62
-      var rangeString = req.headers.range;                                                                            // 63
-                                                                                                                      // 64
-      // Make sure range is a string                                                                                  // 65
-      if (rangeString === ''+rangeString) {                                                                           // 66
-                                                                                                                      // 67
-        // range will be in the format "bytes=0-32767"                                                                // 68
-        var parts = rangeString.split('=');                                                                           // 69
-        var unit = parts[0];                                                                                          // 70
-                                                                                                                      // 71
-        // Make sure parts consists of two strings and range is of type "byte"                                        // 72
-        if (parts.length == 2 && unit == 'bytes') {                                                                   // 73
-          // Parse the range                                                                                          // 74
-          var range = parts[1].split('-');                                                                            // 75
-          var start = Number(range[0]);                                                                               // 76
-          var end = Number(range[1]);                                                                                 // 77
-                                                                                                                      // 78
-          // Fix invalid ranges?                                                                                      // 79
-          if (range[0] != start) start = 0;                                                                           // 80
-          if (range[1] != end || !end) end = fileSize - 1;                                                            // 81
-                                                                                                                      // 82
-          // Make sure range consists of a start and end point of numbers and start is less than end                  // 83
-          if (start < end) {                                                                                          // 84
-                                                                                                                      // 85
-            var partSize = 0 - start + end + 1;                                                                       // 86
-                                                                                                                      // 87
-            // Return the parsed range                                                                                // 88
-            return {                                                                                                  // 89
-              start: start,                                                                                           // 90
-              end: end,                                                                                               // 91
-              length: partSize,                                                                                       // 92
-              size: fileSize,                                                                                         // 93
-              unit: unit,                                                                                             // 94
-              partial: (partSize < fileSize)                                                                          // 95
-            };                                                                                                        // 96
-                                                                                                                      // 97
-          } else {                                                                                                    // 98
-            throw new Meteor.Error(416, "Requested Range Not Satisfiable");                                           // 99
-          }                                                                                                           // 100
-                                                                                                                      // 101
-        } else {                                                                                                      // 102
-          // The first part should be bytes                                                                           // 103
-          throw new Meteor.Error(416, "Requested Range Unit Not Satisfiable");                                        // 104
-        }                                                                                                             // 105
-                                                                                                                      // 106
-      } else {                                                                                                        // 107
-        // No range found                                                                                             // 108
-      }                                                                                                               // 109
-                                                                                                                      // 110
-    } else {                                                                                                          // 111
-      // throw new Error('No request headers set for _parseRange function');                                          // 112
-    }                                                                                                                 // 113
-  } else {                                                                                                            // 114
-    throw new Error('No request object passed to _parseRange function');                                              // 115
-  }                                                                                                                   // 116
-                                                                                                                      // 117
-  return {                                                                                                            // 118
-    start: 0,                                                                                                         // 119
-    end: fileSize - 1,                                                                                                // 120
-    length: fileSize,                                                                                                 // 121
-    size: fileSize,                                                                                                   // 122
-    unit: 'bytes',                                                                                                    // 123
-    partial: false                                                                                                    // 124
-  };                                                                                                                  // 125
-};                                                                                                                    // 126
-                                                                                                                      // 127
-/**                                                                                                                   // 128
- * @method FS.HTTP.Handlers.Get                                                                                       // 129
- * @public                                                                                                            // 130
- * @returns {any} response                                                                                            // 131
- *                                                                                                                    // 132
- * HTTP GET request handler                                                                                           // 133
- */                                                                                                                   // 134
-FS.HTTP.Handlers.Get = function httpGetHandler(ref) {                                                                 // 135
-  var self = this;                                                                                                    // 136
-  // Once we have the file, we can test allow/deny validators                                                         // 137
-  // XXX: pass on the "share" query eg. ?share=342hkjh23ggj for shared url access?                                    // 138
-  FS.Utility.validateAction(ref.collection._validators['download'], ref.file, self.userId /*, self.query.shareId*/);  // 139
-                                                                                                                      // 140
-  var storeName = ref.storeName;                                                                                      // 141
-                                                                                                                      // 142
-  // If no storeName was specified, use the first defined storeName                                                   // 143
-  if (typeof storeName !== "string") {                                                                                // 144
-    // No store handed, we default to primary store                                                                   // 145
-    storeName = ref.collection.primaryStore.name;                                                                     // 146
-  }                                                                                                                   // 147
-                                                                                                                      // 148
-  // Get the storage reference                                                                                        // 149
-  var storage = ref.collection.storesLookup[storeName];                                                               // 150
-                                                                                                                      // 151
-  if (!storage) {                                                                                                     // 152
-    throw new Meteor.Error(404, "Not Found", 'There is no store "' + storeName + '"');                                // 153
-  }                                                                                                                   // 154
-                                                                                                                      // 155
-  // Get the file                                                                                                     // 156
-  var copyInfo = ref.file.copies[storeName];                                                                          // 157
-                                                                                                                      // 158
-  if (!copyInfo) {                                                                                                    // 159
-    throw new Meteor.Error(404, "Not Found", 'This file was not stored in the ' + storeName + ' store');              // 160
-  }                                                                                                                   // 161
-                                                                                                                      // 162
-  // Set the content type for file                                                                                    // 163
-  if (typeof copyInfo.type === "string") {                                                                            // 164
-    self.setContentType(copyInfo.type);                                                                               // 165
-  } else {                                                                                                            // 166
-    self.setContentType('application/octet-stream');                                                                  // 167
-  }                                                                                                                   // 168
-                                                                                                                      // 169
-  // Add 'Content-Disposition' header if requested a download/attachment URL                                          // 170
-  if (typeof ref.download !== "undefined") {                                                                          // 171
-    var filename = ref.filename || copyInfo.name;                                                                     // 172
-    self.addHeader('Content-Disposition', 'attachment; filename="' + filename + '"');                                 // 173
-  } else {                                                                                                            // 174
-    self.addHeader('Content-Disposition', 'inline');                                                                  // 175
-  }                                                                                                                   // 176
-                                                                                                                      // 177
-  // Get the contents range from request                                                                              // 178
-  var range = requestRange(self.request, copyInfo.size);                                                              // 179
-                                                                                                                      // 180
-  // Some browsers cope better if the content-range header is                                                         // 181
-  // still included even for the full file being returned.                                                            // 182
-  self.addHeader('Content-Range', range.unit + ' ' + range.start + '-' + range.end + '/' + range.size);               // 183
-                                                                                                                      // 184
-  // If a chunk/range was requested instead of the whole file, serve that'                                            // 185
-  if (range.partial) {                                                                                                // 186
-    self.setStatusCode(206, 'Partial Content');                                                                       // 187
-  } else {                                                                                                            // 188
-    self.setStatusCode(200, 'OK');                                                                                    // 189
-  }                                                                                                                   // 190
-                                                                                                                      // 191
-  // Add any other global custom headers and collection-specific custom headers                                       // 192
-  FS.Utility.each(getHeaders.concat(getHeadersByCollection[ref.collection.name] || []), function(header) {            // 193
-    self.addHeader(header[0], header[1]);                                                                             // 194
-  });                                                                                                                 // 195
-                                                                                                                      // 196
-  // Inform clients about length (or chunk length in case of ranges)                                                  // 197
-  self.addHeader('Content-Length', range.length);                                                                     // 198
-                                                                                                                      // 199
-  // Last modified header (updatedAt from file info)                                                                  // 200
-  self.addHeader('Last-Modified', copyInfo.updatedAt.toUTCString());                                                  // 201
-                                                                                                                      // 202
-  // Inform clients that we accept ranges for resumable chunked downloads                                             // 203
-  self.addHeader('Accept-Ranges', range.unit);                                                                        // 204
-                                                                                                                      // 205
-  if (FS.debug) console.log('Read file "' + (ref.filename || copyInfo.name) + '" ' + range.unit + ' ' + range.start + '-' + range.end + '/' + range.size);
-                                                                                                                      // 207
-  var readStream = storage.adapter.createReadStream(ref.file, {start: range.start, end: range.end});                  // 208
-                                                                                                                      // 209
-  readStream.on('error', function(err) {                                                                              // 210
-    // Send proper error message on get error                                                                         // 211
-    if (err.message && err.statusCode) {                                                                              // 212
-      self.Error(new Meteor.Error(err.statusCode, err.message));                                                      // 213
-    } else {                                                                                                          // 214
-      self.Error(new Meteor.Error(503, 'Service unavailable'));                                                       // 215
-    }                                                                                                                 // 216
-  });                                                                                                                 // 217
-                                                                                                                      // 218
-  readStream.pipe(self.createWriteStream());                                                                          // 219
-};                                                                                                                    // 220
-
-const originalHandler = FS.HTTP.Handlers.Get;
-FS.HTTP.Handlers.Get = function (ref) {
-//console.log(ref.filename);
-  try {
-     var userAgent = (this.requestHeaders['user-agent']||'').toLowerCase();
-
-        if(userAgent.indexOf('msie') >= 0 || userAgent.indexOf('trident') >= 0 || userAgent.indexOf('chrome') >= 0) {
-            ref.filename =  encodeURIComponent(ref.filename);
-        } else if(userAgent.indexOf('firefox') >= 0) {
-            ref.filename = Buffer.from(ref.filename).toString('binary');
-        } else {
-            /* safari*/
-            ref.filename = Buffer.from(ref.filename).toString('binary');
-        }
-   } catch (ex){
-        ref.filename = 'tempfix';
-   }
-   return originalHandler.call(this, ref);
-};
-                                                                                                                      // 221
-/**                                                                                                                   // 222
- * @method FS.HTTP.Handlers.PutInsert                                                                                 // 223
- * @public                                                                                                            // 224
- * @returns {Object} response object with _id property                                                                // 225
- *                                                                                                                    // 226
- * HTTP PUT file insert request handler                                                                               // 227
- */                                                                                                                   // 228
-FS.HTTP.Handlers.PutInsert = function httpPutInsertHandler(ref) {                                                     // 229
-  var self = this;                                                                                                    // 230
-  var opts = FS.Utility.extend({}, self.query || {}, self.params || {});                                              // 231
-                                                                                                                      // 232
-  FS.debug && console.log("HTTP PUT (insert) handler");                                                               // 233
-                                                                                                                      // 234
-  // Create the nice FS.File                                                                                          // 235
-  var fileObj = new FS.File();                                                                                        // 236
-                                                                                                                      // 237
-  // Set its name                                                                                                     // 238
-  fileObj.name(opts.filename || null);                                                                                // 239
-                                                                                                                      // 240
-  // Attach the readstream as the file's data                                                                         // 241
-  fileObj.attachData(self.createReadStream(), {type: self.requestHeaders['content-type'] || 'application/octet-stream'});
-                                                                                                                      // 243
-  // Validate with insert allow/deny                                                                                  // 244
-  FS.Utility.validateAction(ref.collection.files._validators['insert'], fileObj, self.userId);                        // 245
-                                                                                                                      // 246
-  // Insert file into collection, triggering readStream storage                                                       // 247
-  ref.collection.insert(fileObj);                                                                                     // 248
-                                                                                                                      // 249
-  // Send response                                                                                                    // 250
-  self.setStatusCode(200);                                                                                            // 251
-                                                                                                                      // 252
-  // Return the new file id                                                                                           // 253
-  return {_id: fileObj._id};                                                                                          // 254
-};                                                                                                                    // 255
-                                                                                                                      // 256
-/**                                                                                                                   // 257
- * @method FS.HTTP.Handlers.PutUpdate                                                                                 // 258
- * @public                                                                                                            // 259
- * @returns {Object} response object with _id and chunk properties                                                    // 260
- *                                                                                                                    // 261
- * HTTP PUT file update chunk request handler                                                                         // 262
- */                                                                                                                   // 263
-FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) {                                                     // 264
-  var self = this;                                                                                                    // 265
-  var opts = FS.Utility.extend({}, self.query || {}, self.params || {});                                              // 266
-                                                                                                                      // 267
-  var chunk = parseInt(opts.chunk, 10);                                                                               // 268
-  if (isNaN(chunk)) chunk = 0;                                                                                        // 269
-                                                                                                                      // 270
-  FS.debug && console.log("HTTP PUT (update) handler received chunk: ", chunk);                                       // 271
-                                                                                                                      // 272
-  // Validate with insert allow/deny; also mounts and retrieves the file                                              // 273
-  FS.Utility.validateAction(ref.collection.files._validators['insert'], ref.file, self.userId);                       // 274
-                                                                                                                      // 275
-  self.createReadStream().pipe( FS.TempStore.createWriteStream(ref.file, chunk) );                                    // 276
-                                                                                                                      // 277
-  // Send response                                                                                                    // 278
-  self.setStatusCode(200);                                                                                            // 279
-                                                                                                                      // 280
-  return { _id: ref.file._id, chunk: chunk };                                                                         // 281
-};                                                                                                                    // 282
-                                                                                                                      // 283
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-}).call(this);
-
-
-
-
-
-
-(function () {
-
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-//                                                                                                                    //
-// packages/wekan-cfs-access-point/access-point-server.js                                                                   //
-//                                                                                                                    //
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-                                                                                                                      //
-var path = Npm.require("path");                                                                                       // 1
-                                                                                                                      // 2
-HTTP.publishFormats({                                                                                                 // 3
-  fileRecordFormat: function (input) {                                                                                // 4
-    // Set the method scope content type to json                                                                      // 5
-    this.setContentType('application/json');                                                                          // 6
-    if (FS.Utility.isArray(input)) {                                                                                  // 7
-      return EJSON.stringify(FS.Utility.map(input, function (obj) {                                                   // 8
-        return FS.Utility.cloneFileRecord(obj);                                                                       // 9
-      }));                                                                                                            // 10
-    } else {                                                                                                          // 11
-      return EJSON.stringify(FS.Utility.cloneFileRecord(input));                                                      // 12
-    }                                                                                                                 // 13
-  }                                                                                                                   // 14
-});                                                                                                                   // 15
-                                                                                                                      // 16
-/**                                                                                                                   // 17
- * @method FS.HTTP.setHeadersForGet                                                                                   // 18
- * @public                                                                                                            // 19
- * @param {Array} headers - List of headers, where each is a two-item array in which item 1 is the header name and item 2 is the header value.
- * @param {Array|String} [collections] - Which collections the headers should be added for. Omit this argument to add the header for all collections.
- * @returns {undefined}                                                                                               // 22
- */                                                                                                                   // 23
-FS.HTTP.setHeadersForGet = function setHeadersForGet(headers, collections) {                                          // 24
-  if (typeof collections === "string") {                                                                              // 25
-    collections = [collections];                                                                                      // 26
-  }                                                                                                                   // 27
-  if (collections) {                                                                                                  // 28
-    FS.Utility.each(collections, function(collectionName) {                                                           // 29
-      getHeadersByCollection[collectionName] = headers || [];                                                         // 30
-    });                                                                                                               // 31
-  } else {                                                                                                            // 32
-    getHeaders = headers || [];                                                                                       // 33
-  }                                                                                                                   // 34
-};                                                                                                                    // 35
-                                                                                                                      // 36
-/**                                                                                                                   // 37
- * @method FS.HTTP.publish                                                                                            // 38
- * @public                                                                                                            // 39
- * @param {FS.Collection} collection                                                                                  // 40
- * @param {Function} func - Publish function that returns a cursor.                                                   // 41
- * @returns {undefined}                                                                                               // 42
- *                                                                                                                    // 43
- * Publishes all documents returned by the cursor at a GET URL                                                        // 44
- * with the format baseUrl/record/collectionName. The publish                                                         // 45
- * function `this` is similar to normal `Meteor.publish`.                                                             // 46
- */                                                                                                                   // 47
-FS.HTTP.publish = function fsHttpPublish(collection, func) {                                                          // 48
-  var name = baseUrl + '/record/' + collection.name;                                                                  // 49
-  // Mount collection listing URL using http-publish package                                                          // 50
-  HTTP.publish({                                                                                                      // 51
-    name: name,                                                                                                       // 52
-    defaultFormat: 'fileRecordFormat',                                                                                // 53
-    collection: collection,                                                                                           // 54
-    collectionGet: true,                                                                                              // 55
-    collectionPost: false,                                                                                            // 56
-    documentGet: true,                                                                                                // 57
-    documentPut: false,                                                                                               // 58
-    documentDelete: false                                                                                             // 59
-  }, func);                                                                                                           // 60
-                                                                                                                      // 61
-  FS.debug && console.log("Registered HTTP method GET URLs:\n\n" + name + '\n' + name + '/:id\n');                    // 62
-};                                                                                                                    // 63
-                                                                                                                      // 64
-/**                                                                                                                   // 65
- * @method FS.HTTP.unpublish                                                                                          // 66
- * @public                                                                                                            // 67
- * @param {FS.Collection} collection                                                                                  // 68
- * @returns {undefined}                                                                                               // 69
- *                                                                                                                    // 70
- * Unpublishes a restpoint created by a call to `FS.HTTP.publish`                                                     // 71
- */                                                                                                                   // 72
-FS.HTTP.unpublish = function fsHttpUnpublish(collection) {                                                            // 73
-  // Mount collection listing URL using http-publish package                                                          // 74
-  HTTP.unpublish(baseUrl + '/record/' + collection.name);                                                             // 75
-};                                                                                                                    // 76
-                                                                                                                      // 77
-_existingMountPoints = {};                                                                                            // 78
-                                                                                                                      // 79
-/**                                                                                                                   // 80
- * @method defaultSelectorFunction                                                                                    // 81
- * @private                                                                                                           // 82
- * @returns { collection, file }                                                                                      // 83
- *                                                                                                                    // 84
- * This is the default selector function                                                                              // 85
- */                                                                                                                   // 86
-var defaultSelectorFunction = function() {                                                                            // 87
-  var self = this;                                                                                                    // 88
-  // Selector function                                                                                                // 89
-  //                                                                                                                  // 90
-  // This function will have to return the collection and the                                                         // 91
-  // file. If file not found undefined is returned - if null is returned the                                          // 92
-  // search was not possible                                                                                          // 93
-  var opts = FS.Utility.extend({}, self.query || {}, self.params || {});                                              // 94
-                                                                                                                      // 95
-  // Get the collection name from the url                                                                             // 96
-  var collectionName = opts.collectionName;                                                                           // 97
-                                                                                                                      // 98
-  // Get the id from the url                                                                                          // 99
-  var id = opts.id;                                                                                                   // 100
-                                                                                                                      // 101
-  // Get the collection                                                                                               // 102
-  var collection = FS._collections[collectionName];                                                                   // 103
-                                                                                                                      // 104
-  // Get the file if possible else return null                                                                        // 105
-  var file = (id && collection)? collection.findOne({ _id: id }): null;                                               // 106
-                                                                                                                      // 107
-  // Return the collection and the file                                                                               // 108
-  return {                                                                                                            // 109
-    collection: collection,                                                                                           // 110
-    file: file,                                                                                                       // 111
-    storeName: opts.store,                                                                                            // 112
-    download: opts.download,                                                                                          // 113
-    filename: opts.filename                                                                                           // 114
-  };                                                                                                                  // 115
-};                                                                                                                    // 116
-                                                                                                                      // 117
-/*                                                                                                                    // 118
- * @method FS.HTTP.mount                                                                                              // 119
- * @public                                                                                                            // 120
- * @param {array of string} mountPoints mount points to map rest functinality on                                      // 121
- * @param {function} selector_f [selector] function returns `{ collection, file }` for mount points to work with      // 122
- *                                                                                                                    // 123
-*/                                                                                                                    // 124
-FS.HTTP.mount = function(mountPoints, selector_f) {                                                                   // 125
-  // We take mount points as an array and we get a selector function                                                  // 126
-  var selectorFunction = selector_f || defaultSelectorFunction;                                                       // 127
-                                                                                                                      // 128
-  var accessPoint = {                                                                                                 // 129
-    'stream': true,                                                                                                   // 130
-    'auth': expirationAuth,                                                                                           // 131
-    'post': function(data) {                                                                                          // 132
-      // Use the selector for finding the collection and file reference                                               // 133
-      var ref = selectorFunction.call(this);                                                                          // 134
-                                                                                                                      // 135
-      // We dont support post - this would be normal insert eg. of filerecord?                                        // 136
-      throw new Meteor.Error(501, "Not implemented", "Post is not supported");                                        // 137
-    },                                                                                                                // 138
-    'put': function(data) {                                                                                           // 139
-      // Use the selector for finding the collection and file reference                                               // 140
-      var ref = selectorFunction.call(this);                                                                          // 141
-                                                                                                                      // 142
-      // Make sure we have a collection reference                                                                     // 143
-      if (!ref.collection)                                                                                            // 144
-        throw new Meteor.Error(404, "Not Found", "No collection found");                                              // 145
-                                                                                                                      // 146
-      // Make sure we have a file reference                                                                           // 147
-      if (ref.file === null) {                                                                                        // 148
-        // No id supplied so we will create a new FS.File instance and                                                // 149
-        // insert the supplied data.                                                                                  // 150
-        return FS.HTTP.Handlers.PutInsert.apply(this, [ref]);                                                         // 151
-      } else {                                                                                                        // 152
-        if (ref.file) {                                                                                               // 153
-          return FS.HTTP.Handlers.PutUpdate.apply(this, [ref]);                                                       // 154
-        } else {                                                                                                      // 155
-          throw new Meteor.Error(404, "Not Found", 'No file found');                                                  // 156
-        }                                                                                                             // 157
-      }                                                                                                               // 158
-    },                                                                                                                // 159
-    'get': function(data) {                                                                                           // 160
-      // Use the selector for finding the collection and file reference                                               // 161
-      var ref = selectorFunction.call(this);                                                                          // 162
-                                                                                                                      // 163
-      // Make sure we have a collection reference                                                                     // 164
-      if (!ref.collection)                                                                                            // 165
-        throw new Meteor.Error(404, "Not Found", "No collection found");                                              // 166
-                                                                                                                      // 167
-      // Make sure we have a file reference                                                                           // 168
-      if (ref.file === null) {                                                                                        // 169
-        // No id supplied so we will return the published list of files ala                                           // 170
-        // http.publish in json format                                                                                // 171
-        return FS.HTTP.Handlers.GetList.apply(this, [ref]);                                                           // 172
-      } else {                                                                                                        // 173
-        if (ref.file) {                                                                                               // 174
-          return FS.HTTP.Handlers.Get.apply(this, [ref]);                                                             // 175
-        } else {                                                                                                      // 176
-          throw new Meteor.Error(404, "Not Found", 'No file found');                                                  // 177
-        }                                                                                                             // 178
-      }                                                                                                               // 179
-    },                                                                                                                // 180
-    'delete': function(data) {                                                                                        // 181
-      // Use the selector for finding the collection and file reference                                               // 182
-      var ref = selectorFunction.call(this);                                                                          // 183
-                                                                                                                      // 184
-      // Make sure we have a collection reference                                                                     // 185
-      if (!ref.collection)                                                                                            // 186
-        throw new Meteor.Error(404, "Not Found", "No collection found");                                              // 187
-                                                                                                                      // 188
-      // Make sure we have a file reference                                                                           // 189
-      if (ref.file) {                                                                                                 // 190
-        return FS.HTTP.Handlers.Del.apply(this, [ref]);                                                               // 191
-      } else {                                                                                                        // 192
-        throw new Meteor.Error(404, "Not Found", 'No file found');                                                    // 193
-      }                                                                                                               // 194
-    }                                                                                                                 // 195
-  };                                                                                                                  // 196
-                                                                                                                      // 197
-  var accessPoints = {};                                                                                              // 198
-                                                                                                                      // 199
-  // Add debug message                                                                                                // 200
-  FS.debug && console.log('Registered HTTP method URLs:');                                                            // 201
-                                                                                                                      // 202
-  FS.Utility.each(mountPoints, function(mountPoint) {                                                                 // 203
-    // Couple mountpoint and accesspoint                                                                              // 204
-    accessPoints[mountPoint] = accessPoint;                                                                           // 205
-    // Remember our mountpoints                                                                                       // 206
-    _existingMountPoints[mountPoint] = mountPoint;                                                                    // 207
-    // Add debug message                                                                                              // 208
-    FS.debug && console.log(mountPoint);                                                                              // 209
-  });                                                                                                                 // 210
-                                                                                                                      // 211
-  // XXX: HTTP:methods should unmount existing mounts in case of overwriting?                                         // 212
-  HTTP.methods(accessPoints);                                                                                         // 213
-                                                                                                                      // 214
-};                                                                                                                    // 215
-                                                                                                                      // 216
-/**                                                                                                                   // 217
- * @method FS.HTTP.unmount                                                                                            // 218
- * @public                                                                                                            // 219
- * @param {string | array of string} [mountPoints] Optional, if not specified all mountpoints are unmounted           // 220
- *                                                                                                                    // 221
- */                                                                                                                   // 222
-FS.HTTP.unmount = function(mountPoints) {                                                                             // 223
-  // The mountPoints is optional, can be string or array if undefined then                                            // 224
-  // _existingMountPoints will be used                                                                                // 225
-  var unmountList;                                                                                                    // 226
-  // Container for the mount points to unmount                                                                        // 227
-  var unmountPoints = {};                                                                                             // 228
-                                                                                                                      // 229
-  if (typeof mountPoints === 'undefined') {                                                                           // 230
-    // Use existing mount points - unmount all                                                                        // 231
-    unmountList = _existingMountPoints;                                                                               // 232
-  } else if (mountPoints === ''+mountPoints) {                                                                        // 233
-    // Got a string                                                                                                   // 234
-    unmountList = [mountPoints];                                                                                      // 235
-  } else if (mountPoints.length) {                                                                                    // 236
-    // Got an array                                                                                                   // 237
-    unmountList = mountPoints;                                                                                        // 238
-  }                                                                                                                   // 239
-                                                                                                                      // 240
-  // If we have a list to unmount                                                                                     // 241
-  if (unmountList) {                                                                                                  // 242
-    // Iterate over each item                                                                                         // 243
-    FS.Utility.each(unmountList, function(mountPoint) {                                                               // 244
-      // Check _existingMountPoints to make sure the mount point exists in our                                        // 245
-      // context / was created by the FS.HTTP.mount                                                                   // 246
-      if (_existingMountPoints[mountPoint]) {                                                                         // 247
-        // Mark as unmount                                                                                            // 248
-        unmountPoints[mountPoint] = false;                                                                            // 249
-        // Release                                                                                                    // 250
-        delete _existingMountPoints[mountPoint];                                                                      // 251
-      }                                                                                                               // 252
-    });                                                                                                               // 253
-    FS.debug && console.log('FS.HTTP.unmount:');                                                                      // 254
-    FS.debug && console.log(unmountPoints);                                                                           // 255
-    // Complete unmount                                                                                               // 256
-    HTTP.methods(unmountPoints);                                                                                      // 257
-  }                                                                                                                   // 258
-};                                                                                                                    // 259
-                                                                                                                      // 260
-// ### FS.Collection maps on HTTP pr. default on the following restpoints:                                            // 261
-// *                                                                                                                  // 262
-//    baseUrl + '/files/:collectionName/:id/:filename',                                                               // 263
-//    baseUrl + '/files/:collectionName/:id',                                                                         // 264
-//    baseUrl + '/files/:collectionName'                                                                              // 265
-//                                                                                                                    // 266
-// Change/ replace the existing mount point by:                                                                       // 267
-// ```js                                                                                                              // 268
-//   // unmount all existing                                                                                          // 269
-//   FS.HTTP.unmount();                                                                                               // 270
-//   // Create new mount point                                                                                        // 271
-//   FS.HTTP.mount([                                                                                                  // 272
-//    '/cfs/files/:collectionName/:id/:filename',                                                                     // 273
-//    '/cfs/files/:collectionName/:id',                                                                               // 274
-//    '/cfs/files/:collectionName'                                                                                    // 275
-//  ]);                                                                                                               // 276
-//  ```                                                                                                               // 277
-//                                                                                                                    // 278
-mountUrls = function mountUrls() {                                                                                    // 279
-  // We unmount first in case we are calling this a second time                                                       // 280
-  FS.HTTP.unmount();                                                                                                  // 281
-                                                                                                                      // 282
-  FS.HTTP.mount([                                                                                                     // 283
-    baseUrl + '/files/:collectionName/:id/:filename',                                                                 // 284
-    baseUrl + '/files/:collectionName/:id',                                                                           // 285
-    baseUrl + '/files/:collectionName'                                                                                // 286
-  ]);                                                                                                                 // 287
-};                                                                                                                    // 288
-                                                                                                                      // 289
-// Returns the userId from URL token                                                                                  // 290
-var expirationAuth = function expirationAuth() {                                                                      // 291
-  var self = this;                                                                                                    // 292
-                                                                                                                      // 293
-  // Read the token from '/hello?token=base64'                                                                        // 294
-  var encodedToken = self.query.token;                                                                                // 295
-                                                                                                                      // 296
-  FS.debug && console.log("token: "+encodedToken);                                                                    // 297
-                                                                                                                      // 298
-  if (!encodedToken || !Meteor.users) return false;                                                                   // 299
-                                                                                                                      // 300
-  // Check the userToken before adding it to the db query                                                             // 301
-  // Set the this.userId                                                                                              // 302
-  var tokenString = FS.Utility.atob(encodedToken);                                                                    // 303
-                                                                                                                      // 304
-  var tokenObject;                                                                                                    // 305
-  try {                                                                                                               // 306
-    tokenObject = JSON.parse(tokenString);                                                                            // 307
-  } catch(err) {                                                                                                      // 308
-    throw new Meteor.Error(400, 'Bad Request');                                                                       // 309
-  }                                                                                                                   // 310
-                                                                                                                      // 311
-  // XXX: Do some check here of the object                                                                            // 312
-  var userToken = tokenObject.authToken;                                                                              // 313
-  if (userToken !== ''+userToken) {                                                                                   // 314
-    throw new Meteor.Error(400, 'Bad Request');                                                                       // 315
-  }                                                                                                                   // 316
-                                                                                                                      // 317
-  // If we have an expiration token we should check that it's still valid                                             // 318
-  if (tokenObject.expiration != null) {                                                                               // 319
-    // check if its too old                                                                                           // 320
-    var now = Date.now();                                                                                             // 321
-    if (tokenObject.expiration < now) {                                                                               // 322
-      FS.debug && console.log('Expired token: ' + tokenObject.expiration + ' is less than ' + now);                   // 323
-      throw new Meteor.Error(500, 'Expired token');                                                                   // 324
-    }                                                                                                                 // 325
-  }                                                                                                                   // 326
-                                                                                                                      // 327
-  // We are not on a secure line - so we have to look up the user...                                                  // 328
-  var user = Meteor.users.findOne({                                                                                   // 329
-    $or: [                                                                                                            // 330
-      {'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(userToken)},                               // 331
-      {'services.resume.loginTokens.token': userToken}                                                                // 332
-    ]                                                                                                                 // 333
-  });                                                                                                                 // 334
-                                                                                                                      // 335
-  // Set the userId in the scope                                                                                      // 336
-  return user && user._id;                                                                                            // 337
-};                                                                                                                    // 338
-                                                                                                                      // 339
-HTTP.methods(                                                                                                         // 340
-  {'/cfs/servertime': {                                                                                               // 341
-    get: function(data) {                                                                                             // 342
-      return Date.now().toString();                                                                                   // 343
-    }                                                                                                                 // 344
-  }                                                                                                                   // 345
-});                                                                                                                   // 346
-                                                                                                                      // 347
-// Unify client / server api                                                                                          // 348
-FS.HTTP.now = function() {                                                                                            // 349
-  return Date.now();                                                                                                  // 350
-};                                                                                                                    // 351
-                                                                                                                      // 352
-// Start up the basic mount points                                                                                    // 353
-Meteor.startup(function () {                                                                                          // 354
-  mountUrls();                                                                                                        // 355
-});                                                                                                                   // 356
-                                                                                                                      // 357
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-}).call(this);
-
-///////////////////////////////////////////////////////////////////////
-
-}).call(this);
-
-
-/* Exports */
-if (typeof Package === 'undefined') Package = {};
-Package['wekan-cfs-access-point'] = {};
-
-})();

+ 7 - 5
models/activities.js

@@ -153,11 +153,13 @@ if (Meteor.isServer) {
     }
     if (activity.listId) {
       const list = activity.list();
-      if (list.watchers !== undefined) {
-        watchers = _.union(watchers, list.watchers || []);
+      if (list) {
+        if (list.watchers !== undefined) {
+          watchers = _.union(watchers, list.watchers || []);
+        }
+        params.list = list.title;
+        params.listId = activity.listId;
       }
-      params.list = list.title;
-      params.listId = activity.listId;
     }
     if (activity.oldListId) {
       const oldList = activity.oldList();
@@ -242,7 +244,7 @@ if (Meteor.isServer) {
     }
     if (activity.attachmentId) {
       const attachment = activity.attachment();
-      params.attachment = attachment.original.name;
+      params.attachment = attachment.name;
       params.attachmentId = attachment._id;
     }
     if (activity.checklistId) {

+ 76 - 248
models/attachments.js

@@ -1,268 +1,96 @@
-export const AttachmentStorage = new Mongo.Collection(
-  'cfs_gridfs.attachments.files',
-);
-export const AvatarStorage = new Mongo.Collection('cfs_gridfs.avatars.files');
-
-const localFSStore = process.env.ATTACHMENTS_STORE_PATH;
-const storeName = 'attachments';
-const defaultStoreOptions = {
-  beforeWrite: fileObj => {
-    if (!fileObj.isImage()) {
-      return {
-        type: 'application/octet-stream',
-      };
-    }
-    return {};
-  },
-};
-let store;
-if (localFSStore) {
-  // have to reinvent methods from FS.Store.GridFS and FS.Store.FileSystem
-  const fs = Npm.require('fs');
-  const path = Npm.require('path');
-  const mongodb = Npm.require('mongodb');
-  const Grid = Npm.require('gridfs-stream');
-  // calulate the absolute path here, because FS.Store.FileSystem didn't expose the aboslutepath or FS.Store didn't expose api calls :(
-  let pathname = localFSStore;
-  /*eslint camelcase: ["error", {allow: ["__meteor_bootstrap__"]}] */
-
-  if (!pathname && __meteor_bootstrap__ && __meteor_bootstrap__.serverDir) {
-    pathname = path.join(
-      __meteor_bootstrap__.serverDir,
-      `../../../cfs/files/${storeName}`,
-    );
-  }
-
-  if (!pathname)
-    throw new Error('FS.Store.FileSystem unable to determine path');
-
-  // Check if we have '~/foo/bar'
-  if (pathname.split(path.sep)[0] === '~') {
-    const homepath =
-      process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
-    if (homepath) {
-      pathname = pathname.replace('~', homepath);
-    } else {
-      throw new Error('FS.Store.FileSystem unable to resolve "~" in path');
-    }
-  }
-
-  // Set absolute path
-  const absolutePath = path.resolve(pathname);
+import { Meteor } from 'meteor/meteor';
+import { FilesCollection } from 'meteor/ostrio:files';
+import fs from 'fs';
+import path from 'path';
+import { createBucket } from './lib/grid/createBucket';
+import { createOnAfterUpload } from './lib/fsHooks/createOnAfterUpload';
+import { createInterceptDownload } from './lib/fsHooks/createInterceptDownload';
+import { createOnAfterRemove } from './lib/fsHooks/createOnAfterRemove';
+
+let attachmentBucket;
+if (Meteor.isServer) {
+  attachmentBucket = createBucket('attachments');
+}
 
-  const _FStore = new FS.Store.FileSystem(storeName, {
-    path: localFSStore,
-    ...defaultStoreOptions,
+const insertActivity = (fileObj, activityType) =>
+  Activities.insert({
+    userId: fileObj.userId,
+    type: 'card',
+    activityType,
+    attachmentId: fileObj._id,
+    // this preserves the name so that notifications can be meaningful after
+    // this file is removed
+    attachmentName: fileObj.name,
+    boardId: fileObj.meta.boardId,
+    cardId: fileObj.meta.cardId,
+    listId: fileObj.meta.listId,
+    swimlaneId: fileObj.meta.swimlaneId,
   });
-  const GStore = {
-    fileKey(fileObj) {
-      const key = {
-        _id: null,
-        filename: null,
-      };
-
-      // If we're passed a fileObj, we retrieve the _id and filename from it.
-      if (fileObj) {
-        const info = fileObj._getInfo(storeName, {
-          updateFileRecordFirst: false,
-        });
-        key._id = info.key || null;
-        key.filename =
-          info.name ||
-          fileObj.name({ updateFileRecordFirst: false }) ||
-          `${fileObj.collectionName}-${fileObj._id}`;
-      }
 
-      // If key._id is null at this point, createWriteStream will let GridFS generate a new ID
-      return key;
-    },
-    db: undefined,
-    mongoOptions: { useNewUrlParser: true },
-    mongoUrl: process.env.MONGO_URL,
-    init() {
-      this._init(err => {
-        this.inited = !err;
-      });
-    },
-    _init(callback) {
-      const self = this;
-      mongodb.MongoClient.connect(self.mongoUrl, self.mongoOptions, function(
-        err,
-        db,
-      ) {
-        if (err) {
-          return callback(err);
-        }
-        self.db = db;
-        return callback(null);
-      });
-      return;
-    },
-    createReadStream(fileKey, options) {
-      const self = this;
-      if (!self.inited) {
-        self.init();
-        return undefined;
-      }
-      options = options || {};
+// XXX Enforce a schema for the Attachments FilesCollection
+// see: https://github.com/VeliovGroup/Meteor-Files/wiki/Schema
 
-      // Init GridFS
-      const gfs = new Grid(self.db, mongodb);
-
-      // Set the default streamning settings
-      const settings = {
-        _id: new mongodb.ObjectID(fileKey._id),
-        root: `cfs_gridfs.${storeName}`,
-      };
-
-      // Check if this should be a partial read
-      if (
-        typeof options.start !== 'undefined' &&
-        typeof options.end !== 'undefined'
-      ) {
-        // Add partial info
-        settings.range = {
-          startPos: options.start,
-          endPos: options.end,
-        };
-      }
-      return gfs.createReadStream(settings);
-    },
-  };
-  GStore.init();
-  const CRS = 'createReadStream';
-  const _CRS = `_${CRS}`;
-  const FStore = _FStore._transform;
-  FStore[_CRS] = FStore[CRS].bind(FStore);
-  FStore[CRS] = function(fileObj, options) {
-    let stream;
-    try {
-      const localFile = path.join(
-        absolutePath,
-        FStore.storage.fileKey(fileObj),
-      );
-      const state = fs.statSync(localFile);
-      if (state) {
-        stream = FStore[_CRS](fileObj, options);
-      }
-    } catch (e) {
-      // file is not there, try GridFS ?
-      stream = undefined;
+Attachments = new FilesCollection({
+  debug: false, // Change to `true` for debugging
+  collectionName: 'attachments',
+  allowClientCode: true,
+  storagePath() {
+    if (process.env.WRITABLE_PATH) {
+      return path.join(process.env.WRITABLE_PATH, 'uploads', 'attachments');
     }
-    if (stream) return stream;
-    else {
-      try {
-        const stream = GStore[CRS](GStore.fileKey(fileObj), options);
-        return stream;
-      } catch (e) {
-        return undefined;
-      }
+    return path.normalize(`assets/app/uploads/${this.collectionName}`);
+  },
+  onAfterUpload: function onAfterUpload(fileRef) {
+    createOnAfterUpload(attachmentBucket).call(this, fileRef);
+    // If the attachment doesn't have a source field
+    // or its source is different than import
+    if (!fileRef.meta.source || fileRef.meta.source !== 'import') {
+      // Add activity about adding the attachment
+      insertActivity(fileRef, 'addAttachment');
     }
-  }.bind(FStore);
-  store = _FStore;
-} else {
-  store = new FS.Store.GridFS(localFSStore ? `G${storeName}` : storeName, {
-    // XXX Add a new store for cover thumbnails so we don't load big images in
-    // the general board view
-    // If the uploaded document is not an image we need to enforce browser
-    // download instead of execution. This is particularly important for HTML
-    // files that the browser will just execute if we don't serve them with the
-    // appropriate `application/octet-stream` MIME header which can lead to user
-    // data leaks. I imagine other formats (like PDF) can also be attack vectors.
-    // See https://github.com/wekan/wekan/issues/99
-    // XXX Should we use `beforeWrite` option of CollectionFS instead of
-    // collection-hooks?
-    // We should use `beforeWrite`.
-    ...defaultStoreOptions,
-  });
-}
-Attachments = new FS.Collection('attachments', {
-  stores: [store],
+  },
+  interceptDownload: createInterceptDownload(attachmentBucket),
+  onAfterRemove: function onAfterRemove(files) {
+    createOnAfterRemove(attachmentBucket).call(this, files);
+    files.forEach(fileObj => {
+      insertActivity(fileObj, 'deleteAttachment');
+    });
+  },
+  // We authorize the attachment download either:
+  // - if the board is public, everyone (even unconnected) can download it
+  // - if the board is private, only board members can download it
+  protected(fileObj) {
+    const board = Boards.findOne(fileObj.meta.boardId);
+    if (board.isPublic()) {
+      return true;
+    }
+    return board.hasMember(this.userId);
+  },
 });
 
 if (Meteor.isServer) {
-  Meteor.startup(() => {
-    Attachments.files._ensureIndex({ cardId: 1 });
-  });
-
   Attachments.allow({
-    insert(userId, doc) {
-      return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+    insert(userId, fileObj) {
+      return allowIsBoardMember(userId, Boards.findOne(fileObj.boardId));
     },
-    update(userId, doc) {
-      return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+    update(userId, fileObj) {
+      return allowIsBoardMember(userId, Boards.findOne(fileObj.boardId));
     },
-    remove(userId, doc) {
-      return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+    remove(userId, fileObj) {
+      return allowIsBoardMember(userId, Boards.findOne(fileObj.boardId));
     },
-    // We authorize the attachment download either:
-    // - if the board is public, everyone (even unconnected) can download it
-    // - if the board is private, only board members can download it
-    download(userId, doc) {
-      const board = Boards.findOne(doc.boardId);
-      if (board.isPublic()) {
-        return true;
-      } else {
-        return board.hasMember(userId);
-      }
-    },
-
-    fetch: ['boardId'],
+    fetch: ['meta'],
   });
-}
-
-// XXX Enforce a schema for the Attachments CollectionFS
 
-if (Meteor.isServer) {
-  Attachments.files.after.insert((userId, doc) => {
-    // If the attachment doesn't have a source field
-    // or its source is different than import
-    if (!doc.source || doc.source !== 'import') {
-      // Add activity about adding the attachment
-      Activities.insert({
-        userId,
-        type: 'card',
-        activityType: 'addAttachment',
-        attachmentId: doc._id,
-        // this preserves the name so that notifications can be meaningful after
-        // this file is removed
-        attachmentName: doc.original.name,
-        boardId: doc.boardId,
-        cardId: doc.cardId,
-        listId: doc.listId,
-        swimlaneId: doc.swimlaneId,
-      });
-    } else {
-      // Don't add activity about adding the attachment as the activity
-      // be imported and delete source field
-      Attachments.update(
-        {
-          _id: doc._id,
-        },
-        {
-          $unset: {
-            source: '',
-          },
-        },
-      );
+  Meteor.startup(() => {
+    Attachments.collection._ensureIndex({ cardId: 1 });
+    const storagePath = Attachments.storagePath();
+    console.log("Meteor.startup check storagePath: ", storagePath);
+    if (!fs.existsSync(storagePath)) {
+      console.log("create storagePath because it doesn't exist: " + storagePath);
+      fs.mkdirSync(storagePath, { recursive: true });
     }
   });
-
-  Attachments.files.before.remove((userId, doc) => {
-    Activities.insert({
-      userId,
-      type: 'card',
-      activityType: 'deleteAttachment',
-      attachmentId: doc._id,
-      // this preserves the name so that notifications can be meaningful after
-      // this file is removed
-      attachmentName: doc.original.name,
-      boardId: doc.boardId,
-      cardId: doc.cardId,
-      listId: doc.listId,
-      swimlaneId: doc.swimlaneId,
-    });
-  });
 }
 
 export default Attachments;

+ 116 - 0
models/attachments_old.js

@@ -0,0 +1,116 @@
+const storeName = 'attachments';
+const defaultStoreOptions = {
+  beforeWrite: fileObj => {
+    if (!fileObj.isImage()) {
+      return {
+        type: 'application/octet-stream',
+      };
+    }
+    return {};
+  },
+};
+let store;
+store = new FS.Store.GridFS(storeName, {
+  // XXX Add a new store for cover thumbnails so we don't load big images in
+  // the general board view
+  // If the uploaded document is not an image we need to enforce browser
+  // download instead of execution. This is particularly important for HTML
+  // files that the browser will just execute if we don't serve them with the
+  // appropriate `application/octet-stream` MIME header which can lead to user
+  // data leaks. I imagine other formats (like PDF) can also be attack vectors.
+  // See https://github.com/wekan/wekan/issues/99
+  // XXX Should we use `beforeWrite` option of CollectionFS instead of
+  // collection-hooks?
+  // We should use `beforeWrite`.
+  ...defaultStoreOptions,
+});
+AttachmentsOld = new FS.Collection('attachments', {
+  stores: [store],
+});
+
+if (Meteor.isServer) {
+  Meteor.startup(() => {
+    AttachmentsOld.files._ensureIndex({ cardId: 1 });
+  });
+
+  AttachmentsOld.allow({
+    insert(userId, doc) {
+      return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+    },
+    update(userId, doc) {
+      return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+    },
+    remove(userId, doc) {
+      return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+    },
+    // We authorize the attachment download either:
+    // - if the board is public, everyone (even unconnected) can download it
+    // - if the board is private, only board members can download it
+    download(userId, doc) {
+      const board = Boards.findOne(doc.boardId);
+      if (board.isPublic()) {
+        return true;
+      } else {
+        return board.hasMember(userId);
+      }
+    },
+
+    fetch: ['boardId'],
+  });
+}
+
+// XXX Enforce a schema for the AttachmentsOld CollectionFS
+
+if (Meteor.isServer) {
+  AttachmentsOld.files.after.insert((userId, doc) => {
+    // If the attachment doesn't have a source field
+    // or its source is different than import
+    if (!doc.source || doc.source !== 'import') {
+      // Add activity about adding the attachment
+      Activities.insert({
+        userId,
+        type: 'card',
+        activityType: 'addAttachment',
+        attachmentId: doc._id,
+        // this preserves the name so that notifications can be meaningful after
+        // this file is removed
+        attachmentName: doc.original.name,
+        boardId: doc.boardId,
+        cardId: doc.cardId,
+        listId: doc.listId,
+        swimlaneId: doc.swimlaneId,
+      });
+    } else {
+      // Don't add activity about adding the attachment as the activity
+      // be imported and delete source field
+      AttachmentsOld.update(
+        {
+          _id: doc._id,
+        },
+        {
+          $unset: {
+            source: '',
+          },
+        },
+      );
+    }
+  });
+
+  AttachmentsOld.files.before.remove((userId, doc) => {
+    Activities.insert({
+      userId,
+      type: 'card',
+      activityType: 'deleteAttachment',
+      attachmentId: doc._id,
+      // this preserves the name so that notifications can be meaningful after
+      // this file is removed
+      attachmentName: doc.original.name,
+      boardId: doc.boardId,
+      cardId: doc.cardId,
+      listId: doc.listId,
+      swimlaneId: doc.swimlaneId,
+    });
+  });
+}
+
+export default AttachmentsOld;

+ 49 - 21
models/avatars.js

@@ -1,29 +1,57 @@
-Avatars = new FS.Collection('avatars', {
-  stores: [new FS.Store.GridFS('avatars')],
-  filter: {
-    maxSize: 520000,
-    allow: {
-      contentTypes: ['image/*'],
-    },
-  },
-});
+import { Meteor } from 'meteor/meteor';
+import { FilesCollection } from 'meteor/ostrio:files';
+import fs from 'fs';
+import path from 'path';
+import { createBucket } from './lib/grid/createBucket';
+import { createOnAfterUpload } from './lib/fsHooks/createOnAfterUpload';
+import { createInterceptDownload } from './lib/fsHooks/createInterceptDownload';
+import { createOnAfterRemove } from './lib/fsHooks/createOnAfterRemove';
 
-function isOwner(userId, file) {
-  return userId && userId === file.userId;
+let avatarsBucket;
+if (Meteor.isServer) {
+  avatarsBucket = createBucket('avatars');
 }
 
-Avatars.allow({
-  insert: isOwner,
-  update: isOwner,
-  remove: isOwner,
-  download() {
-    return true;
+Avatars = new FilesCollection({
+  debug: false, // Change to `true` for debugging
+  collectionName: 'avatars',
+  allowClientCode: true,
+  storagePath() {
+    if (process.env.WRITABLE_PATH) {
+      return path.join(process.env.WRITABLE_PATH, 'uploads', 'avatars');
+    }
+    return path.normalize(`assets/app/uploads/${this.collectionName}`);;
   },
-  fetch: ['userId'],
+  onBeforeUpload(file) {
+    if (file.size <= 72000 && file.type.startsWith('image/')) {
+      return true;
+    }
+    return 'avatar-too-big';
+  },
+  onAfterUpload: createOnAfterUpload(avatarsBucket),
+  interceptDownload: createInterceptDownload(avatarsBucket),
+  onAfterRemove: createOnAfterRemove(avatarsBucket),
 });
 
-Avatars.files.before.insert((userId, doc) => {
-  doc.userId = userId;
-});
+function isOwner(userId, doc) {
+  return userId && userId === doc.userId;
+}
+
+if (Meteor.isServer) {
+  Avatars.allow({
+    insert: isOwner,
+    update: isOwner,
+    remove: isOwner,
+    fetch: ['userId'],
+  });
+
+  Meteor.startup(() => {
+    const storagePath = Avatars.storagePath();
+    if (!fs.existsSync(storagePath)) {
+      console.log("create storagePath because it doesn't exist: " + storagePath);
+      fs.mkdirSync(storagePath, { recursive: true });
+    }
+  });
+}
 
 export default Avatars;

+ 29 - 0
models/avatars_old.js

@@ -0,0 +1,29 @@
+AvatarsOld = new FS.Collection('avatars', {
+  stores: [new FS.Store.GridFS('avatars')],
+  filter: {
+    maxSize: 72000,
+    allow: {
+      contentTypes: ['image/*'],
+    },
+  },
+});
+
+function isOwner(userId, file) {
+  return userId && userId === file.userId;
+}
+
+AvatarsOld.allow({
+  insert: isOwner,
+  update: isOwner,
+  remove: isOwner,
+  download() {
+    return true;
+  },
+  fetch: ['userId'],
+});
+
+AvatarsOld.files.before.insert((userId, doc) => {
+  doc.userId = userId;
+});
+
+export default AvatarsOld;

+ 1 - 0
models/boards.js

@@ -8,6 +8,7 @@ import {
 import Users from "./users";
 
 const escapeForRegex = require('escape-string-regexp');
+
 Boards = new Mongo.Collection('boards');
 
 /**

+ 5 - 5
models/cards.js

@@ -737,14 +737,14 @@ Cards.helpers({
   attachments() {
     if (this.isLinkedCard()) {
       return Attachments.find(
-        { cardId: this.linkedId },
+        { 'meta.cardId': this.linkedId },
         { sort: { uploadedAt: -1 } },
-      );
+      ).each();
     } else {
       return Attachments.find(
-        { cardId: this._id },
+        { 'meta.cardId': this._id },
         { sort: { uploadedAt: -1 } },
-      );
+      ).each();
     }
   },
 
@@ -753,7 +753,7 @@ Cards.helpers({
     const cover = Attachments.findOne(this.coverId);
     // if we return a cover before it is fully stored, we will get errors when we try to display it
     // todo XXX we could return a default "upload pending" image in the meantime?
-    return cover && cover.url() && cover;
+    return cover && cover.link() && cover;
   },
 
   checklists() {

+ 6 - 4
models/exporter.js

@@ -1,5 +1,7 @@
 const Papa = require('papaparse');
 
+//const stringify = require('csv-stringify');
+
 // exporter maybe is broken since Gridfs introduced, add fs and path
 export class Exporter {
   constructor(boardId, attachmentId) {
@@ -78,11 +80,11 @@ export class Exporter {
 
         return {
           _id: attachment._id,
-          cardId: attachment.cardId,
+          cardId: attachment.meta.cardId,
           //url: FlowRouter.url(attachment.url()),
           file: filebase64,
-          name: attachment.original.name,
-          type: attachment.original.type,
+          name: attachment.name,
+          type: attachment.type,
         };
       });
     //When has a especific valid attachment return the single element
@@ -209,7 +211,7 @@ export class Exporter {
       delimiter: userDelimiter,
       header: true,
       newline: "\r\n",
-      skipEmptyLines: false, 
+      skipEmptyLines: false,
       escapeFormulae: true,
     };
 

+ 47 - 0
models/lib/fsHooks/createInterceptDownload.js

@@ -0,0 +1,47 @@
+import { createObjectId } from '../grid/createObjectId';
+
+export const createInterceptDownload = bucket =>
+  function interceptDownload(http, file, versionName) {
+    const { gridFsFileId } = file.versions[versionName].meta || {};
+    if (gridFsFileId) {
+      // opens the download stream using a given gfs id
+      // see: http://mongodb.github.io/node-mongodb-native/3.2/api/GridFSBucket.html#openDownloadStream
+      const gfsId = createObjectId({ gridFsFileId });
+      const readStream = bucket.openDownloadStream(gfsId);
+
+      readStream.on('data', data => {
+        http.response.write(data);
+      });
+
+      readStream.on('end', () => {
+        http.response.end(); // don't pass parameters to end() or it will be attached to the file's binary stream
+      });
+
+      readStream.on('error', () => {
+        // not found probably
+        // eslint-disable-next-line no-param-reassign
+        http.response.statusCode = 404;
+        http.response.end('not found');
+      });
+
+      http.response.setHeader('Cache-Control', this.cacheControl);
+      http.response.setHeader(
+        'Content-Disposition',
+        getContentDisposition(file.name, http?.params?.query?.download),
+      );
+    }
+    return Boolean(gridFsFileId); // Serve file from either GridFS or FS if it wasn't uploaded yet
+  };
+
+/**
+ * Will initiate download, if links are called with ?download="true" queryparam.
+ **/
+const getContentDisposition = (name, downloadFlag) => {
+  const dispositionType = downloadFlag === 'true' ? 'attachment;' : 'inline;';
+
+  const encodedName = encodeURIComponent(name);
+  const dispositionName = `filename="${encodedName}"; filename=*UTF-8"${encodedName}";`;
+  const dispositionEncoding = 'charset=utf-8';
+
+  return `${dispositionType} ${dispositionName} ${dispositionEncoding}`;
+};

+ 17 - 0
models/lib/fsHooks/createOnAfterRemove.js

@@ -0,0 +1,17 @@
+import { createObjectId } from '../grid/createObjectId';
+
+export const createOnAfterRemove = bucket =>
+  function onAfterRemove(files) {
+    files.forEach(file => {
+      Object.keys(file.versions).forEach(versionName => {
+        const gridFsFileId = (file.versions[versionName].meta || {})
+          .gridFsFileId;
+        if (gridFsFileId) {
+          const gfsId = createObjectId({ gridFsFileId });
+          bucket.delete(gfsId, err => {
+            // if (err) console.error(err);
+          });
+        }
+      });
+    });
+  };

+ 51 - 0
models/lib/fsHooks/createOnAfterUpload.js

@@ -0,0 +1,51 @@
+import { Meteor } from 'meteor/meteor';
+import fs from 'fs';
+
+export const createOnAfterUpload = bucket =>
+  function onAfterUpload(file) {
+    const self = this;
+
+    // here you could manipulate your file
+    // and create a new version, for example a scaled 'thumbnail'
+    // ...
+
+    // then we read all versions we have got so far
+    Object.keys(file.versions).forEach(versionName => {
+      const metadata = { ...file.meta, versionName, fileId: file._id };
+      fs.createReadStream(file.versions[versionName].path)
+
+        // this is where we upload the binary to the bucket using bucket.openUploadStream
+        // see http://mongodb.github.io/node-mongodb-native/3.2/api/GridFSBucket.html#openUploadStream
+        .pipe(
+          bucket.openUploadStream(file.name, {
+            contentType: file.type || 'binary/octet-stream',
+            metadata,
+          }),
+        )
+
+        // and we unlink the file from the fs on any error
+        // that occurred during the upload to prevent zombie files
+        .on('error', err => {
+          console.error("[createOnAfterUpload error]", err);
+          self.unlink(this.collection.findOne(file._id), versionName); // Unlink files from FS
+        })
+
+        // once we are finished, we attach the gridFS Object id on the
+        // FilesCollection document's meta section and finally unlink the
+        // upload file from the filesystem
+        .on(
+          'finish',
+          Meteor.bindEnvironment(ver => {
+            const property = `versions.${versionName}.meta.gridFsFileId`;
+
+            self.collection.update(file._id, {
+              $set: {
+                [property]: ver._id.toHexString(),
+              },
+            });
+
+            self.unlink(this.collection.findOne(file._id), versionName); // Unlink files from FS
+          }),
+        );
+    });
+  };

+ 9 - 0
models/lib/grid/createBucket.js

@@ -0,0 +1,9 @@
+import { MongoInternals } from 'meteor/mongo';
+
+export const createBucket = bucketName => {
+  const options = bucketName ? { bucketName } : void 0;
+  return new MongoInternals.NpmModule.GridFSBucket(
+    MongoInternals.defaultRemoteCollectionDriver().mongo.db,
+    options,
+  );
+};

+ 4 - 0
models/lib/grid/createObjectId.js

@@ -0,0 +1,4 @@
+import { MongoInternals } from 'meteor/mongo';
+
+export const createObjectId = ({ gridFsFileId }) =>
+  new MongoInternals.NpmModule.ObjectID(gridFsFileId);

+ 24 - 36
models/trelloCreator.js

@@ -422,46 +422,34 @@ export class TrelloCreator {
       }
       const attachments = this.attachments[card.id];
       const trelloCoverId = card.idAttachmentCover;
-      if (attachments) {
-        const links = [];
+      if (attachments && Meteor.isServer) {
         attachments.forEach(att => {
-          // if the attachment `name` and `url` are the same, then the
-          // attachment is an attached link
-          if (att.name === att.url) {
-            links.push(att.url);
-          } else {
-            const file = new FS.File();
-            // Simulating file.attachData on the client generates multiple errors
-            // - HEAD returns null, which causes exception down the line
-            // - the template then tries to display the url to the attachment which causes other errors
-            // so we make it server only, and let UI catch up once it is done, forget about latency comp.
-            const self = this;
-            if (Meteor.isServer) {
-              file.attachData(att.url, function(error) {
-                file.boardId = boardId;
-                file.cardId = cardId;
-                file.userId = self._user(att.idMemberCreator);
-                // The field source will only be used to prevent adding
-                // attachments' related activities automatically
-                file.source = 'import';
-                if (error) {
-                  throw error;
-                } else {
-                  const wekanAtt = Attachments.insert(file, () => {
-                    // we do nothing
-                  });
-                  self.attachmentIds[att.id] = wekanAtt._id;
-                  //
-                  if (trelloCoverId === att.id) {
-                    Cards.direct.update(cardId, {
-                      $set: { coverId: wekanAtt._id },
-                    });
-                  }
-                }
+          const self = this;
+          const opts = {
+            type: att.type ? att.type : undefined,
+            userId: self._user(att.userId),
+            meta: {
+              boardId,
+              cardId,
+              source: 'import',
+            },
+          };
+          const cb = (error, fileObj) => {
+            if (error) {
+              throw error;
+            }
+            self.attachmentIds[att._id] = fileObj._id;
+            if (trelloCoverId === att._id) {
+              Cards.direct.update(cardId, {
+                $set: { coverId: fileObj._id },
               });
             }
+          };
+          if (att.url) {
+            Attachment.load(att.url, opts, cb, true);
+          } else if (att.file) {
+            Attachment.write(att.file, opts, cb, true);
           }
-          // todo XXX set cover - if need be
         });
 
         if (links.length) {

+ 23 - 70
models/wekanCreator.js

@@ -444,81 +444,34 @@ export class WekanCreator {
       }
       const attachments = this.attachments[card._id];
       const wekanCoverId = card.coverId;
-      if (attachments) {
+      if (attachments && Meteor.isServer) {
         attachments.forEach(att => {
-          const file = new FS.File();
-          // Simulating file.attachData on the client generates multiple errors
-          // - HEAD returns null, which causes exception down the line
-          // - the template then tries to display the url to the attachment which causes other errors
-          // so we make it server only, and let UI catch up once it is done, forget about latency comp.
           const self = this;
-          if (Meteor.isServer) {
-            if (att.url) {
-              file.attachData(att.url, function(error) {
-                file.boardId = boardId;
-                file.cardId = cardId;
-                file.userId = self._user(att.userId);
-                // The field source will only be used to prevent adding
-                // attachments' related activities automatically
-                file.source = 'import';
-                if (error) {
-                  throw error;
-                } else {
-                  const wekanAtt = Attachments.insert(file, () => {
-                    // we do nothing
-                  });
-                  self.attachmentIds[att._id] = wekanAtt._id;
-                  //
-                  if (wekanCoverId === att._id) {
-                    Cards.direct.update(cardId, {
-                      $set: {
-                        coverId: wekanAtt._id,
-                      },
-                    });
-                  }
-                }
+          const opts = {
+            type: att.type ? att.type : undefined,
+            userId: self._user(att.userId),
+            meta: {
+              boardId,
+              cardId,
+              source: 'import',
+            },
+          };
+          const cb = (error, fileObj) => {
+            if (error) {
+              throw error;
+            }
+            self.attachmentIds[att._id] = fileObj._id;
+            if (wekanCoverId === att._id) {
+              Cards.direct.update(cardId, {
+                $set: { coverId: fileObj._id },
               });
-            } else if (att.file) {
-              //If attribute type is null or empty string is set, assume binary stream
-              att.type =
-                !att.type || att.type.trim().length === 0
-                  ? 'application/octet-stream'
-                  : att.type;
-
-              file.attachData(
-                Buffer.from(att.file, 'base64'),
-                {
-                  type: att.type,
-                },
-                error => {
-                  file.name(att.name);
-                  file.boardId = boardId;
-                  file.cardId = cardId;
-                  file.userId = self._user(att.userId);
-                  // The field source will only be used to prevent adding
-                  // attachments' related activities automatically
-                  file.source = 'import';
-                  if (error) {
-                    throw error;
-                  } else {
-                    const wekanAtt = Attachments.insert(file, () => {
-                      // we do nothing
-                    });
-                    this.attachmentIds[att._id] = wekanAtt._id;
-                    //
-                    if (wekanCoverId === att._id) {
-                      Cards.direct.update(cardId, {
-                        $set: {
-                          coverId: wekanAtt._id,
-                        },
-                      });
-                    }
-                  }
-                },
-              );
             }
+          };
+          if (att.url) {
+            Attachment.load(att.url, opts, cb, true);
+          } else if (att.file) {
+            Attachment.write(att.file, opts, cb, true);
           }
-          // todo XXX set cover - if need be
         });
       }
       result.push(cardId);

+ 0 - 1
rebuild-wekan.bat

@@ -52,7 +52,6 @@ REM del /S /F /Q node_modules
 call meteor npm install
 REM del /S /F /Q .build
 call meteor build .build --directory
-copy fix-download-unicode\cfs_access-point.txt .build\bundle\programs\server\packages\cfs_access-point.js
 REM ## Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
 del /S /F /Q rm .build/bundle/programs/web.browser.legacy
 REM ## Install some NPM packages

+ 0 - 1
releases/rebuild-release.sh

@@ -10,7 +10,6 @@ rm -rf node_modules
 meteor npm install
 rm -rf .build
 METEOR_PROFILE=100 meteor build .build --directory
-cp -f fix-download-unicode/cfs_access-point.txt .build/bundle/programs/server/packages/cfs_access-point.js
 # Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
 rm -rf .build/bundle/programs/web.browser.legacy
 cd .build/bundle/programs/server

+ 155 - 0
server/migrations.js

@@ -1,8 +1,14 @@
+import fs from 'fs';
+import path from 'path';
 import AccountSettings from '../models/accountSettings';
 import TableVisibilityModeSettings from '../models/tableVisibilityModeSettings';
 import Actions from '../models/actions';
 import Activities from '../models/activities';
 import Announcements from '../models/announcements';
+import Attachments from '../models/attachments';
+import AttachmentsOld from '../models/attachments_old';
+import Avatars from '../models/avatars';
+import AvatarsOld from '../models/avatars_old';
 import Boards from '../models/boards';
 import CardComments from '../models/cardComments';
 import Cards from '../models/cards';
@@ -1119,3 +1125,152 @@ Migrations.add('add-card-details-show-lists', () => {
     noValidateMulti,
   );
 });
+
+Migrations.add('migrate-attachments-collectionFS-to-ostrioFiles', () => {
+  const storagePath = Attachments.storagePath();
+  if (!fs.existsSync(storagePath)) {
+    console.log("create storagePath because it doesn't exist: " + storagePath);
+    fs.mkdirSync(storagePath, { recursive: true });
+  }
+  AttachmentsOld.find().forEach(function(fileObj) {
+    const newFileName = fileObj.name();
+    const filePath = path.join(storagePath, `${fileObj._id}-${newFileName}`);
+
+    // This is "example" variable, change it to the userId that you might be using.
+    const userId = fileObj.userId;
+
+    const fileType = fileObj.type();
+    const fileSize = fileObj.size();
+    const fileId = fileObj._id;
+
+    const readStream = fileObj.createReadStream('attachments');
+    const writeStream = fs.createWriteStream(filePath);
+
+    writeStream.on('error', error => {
+      console.error('[writeStream error]: ', error, filePath);
+    });
+
+    readStream.on('error', error => {
+      console.error('[readStream error]: ', error, filePath);
+    });
+
+    // Once we have a file, then upload it to our new data storage
+    readStream.on('end', () => {
+      console.log('Ended: ', filePath);
+      // UserFiles is the new Meteor-Files/FilesCollection collection instance
+
+      Attachments.addFile(
+        filePath,
+        {
+          fileName: newFileName,
+          type: fileType,
+          meta: {
+            boardId: fileObj.boardId,
+            cardId: fileObj.cardId,
+            listId: fileObj.listId,
+            swimlaneId: fileObj.swimlaneId,
+            source: 'import,'
+          },
+          userId,
+          size: fileSize,
+          fileId,
+        },
+        (error, fileRef) => {
+          if (error) {
+            console.error('[Attachments#addFile error]: ', error);
+          } else {
+            console.log('File Inserted: ', fileRef);
+            // Set the userId again
+            Attachments.update({ _id: fileRef._id }, { $set: { userId } });
+            fileObj.remove();
+          }
+        },
+        true,
+      ); // proceedAfterUpload
+    });
+
+    readStream.pipe(writeStream);
+  });
+});
+
+Migrations.add('migrate-avatars-collectionFS-to-ostrioFiles', () => {
+  const storagePath = Avatars.storagePath();
+  if (!fs.existsSync(storagePath)) {
+    console.log("create storagePath because it doesn't exist: " + storagePath);
+    fs.mkdirSync(storagePath, { recursive: true });
+  }
+  AvatarsOld.find().forEach(function(fileObj) {
+    const newFileName = fileObj.name();
+    const filePath = path.join(storagePath, `${fileObj._id}-${newFileName}`);
+
+    // This is "example" variable, change it to the userId that you might be using.
+    const userId = fileObj.userId;
+
+    const fileType = fileObj.type();
+    const fileSize = fileObj.size();
+    const fileId = fileObj._id;
+
+    const readStream = fileObj.createReadStream('avatars');
+    const writeStream = fs.createWriteStream(filePath);
+
+    writeStream.on('error', error => {
+      console.error('[writeStream error]: ', error, filePath);
+    });
+
+    readStream.on('error', error => {
+      console.error('[readStream error]: ', error, filePath);
+    });
+
+    // Once we have a file, then upload it to our new data storage
+    readStream.on('end', () => {
+      console.log('Ended: ', filePath);
+      // UserFiles is the new Meteor-Files/FilesCollection collection instance
+
+      Avatars.addFile(
+        filePath,
+        {
+          fileName: newFileName,
+          type: fileType,
+          meta: {
+            boardId: fileObj.boardId,
+            cardId: fileObj.cardId,
+            listId: fileObj.listId,
+            swimlaneId: fileObj.swimlaneId,
+          },
+          userId,
+          size: fileSize,
+          fileId,
+        },
+        (error, fileRef) => {
+          if (error) {
+            console.error('[Avatars#addFile error]: ', error);
+          } else {
+            console.log('File Inserted: ', newFileName, fileRef);
+            // Set the userId again
+            Avatars.update({ _id: fileRef._id }, { $set: { userId } });
+            Users.find().forEach(user => {
+              const old_url = fileObj.url();
+              new_url = Avatars.findOne({ _id: fileRef._id }).link(
+                'original',
+                '/',
+              );
+              if (user.profile.avatarUrl.startsWith(old_url)) {
+                // Set avatar url to new url
+                Users.direct.update(
+                  { _id: user._id },
+                  { $set: { 'profile.avatarUrl': new_url } },
+                  noValidate,
+                );
+                console.log('User avatar updated: ', user._id, new_url);
+              }
+            });
+            fileObj.remove();
+          }
+        },
+        true, // proceedAfterUpload
+      );
+    });
+
+    readStream.pipe(writeStream);
+  });
+});

+ 2 - 1
server/publications/avatars.js

@@ -1,3 +1,4 @@
+import Avatars from '../../models/avatars';
 Meteor.publish('my-avatars', function() {
-  return Avatars.find({ userId: this.userId });
+  return Avatars.find({ userId: this.userId }).cursor;
 });

+ 2 - 2
server/publications/boards.js

@@ -233,8 +233,8 @@ Meteor.publishRelations('board', function(boardId, isArchived) {
       cardCommentsLinkedBoard.selector = _ids => ({ boardId: _ids });
       const cardCommentReactions = this.join(CardCommentReactions);
       cardCommentReactions.selector = _ids => ({ cardId: _ids });
-      const attachments = this.join(Attachments);
-      attachments.selector = _ids => ({ cardId: _ids });
+      const attachments = this.join(Attachments.collection);
+      attachments.selector = _ids => ({ 'meta.cardId': _ids });
       const checklists = this.join(Checklists);
       checklists.selector = _ids => ({ cardId: _ids });
       const checklistItems = this.join(ChecklistItems);

+ 1 - 1
server/publications/notifications.js

@@ -12,7 +12,7 @@ Meteor.publish('notificationAttachments', function() {
       $in: activities()
         .map(v => v.attachmentId)
         .filter(v => !!v),
-    },
+    }.cursor,
   });
 });
 

File diff suppressed because it is too large
+ 0 - 0
snap-src/bin/config


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

@@ -131,12 +131,6 @@ echo -e "\t$ snap set $SNAP_NAME image-compress-ratio='80'"
 echo -e "Disable:"
 echo -e "\t$ snap unset $SNAP_NAME image-compress-ratio"
 echo -e "\n"
-echo -e "Allow to set attachment upload into specified server location. Create that directory first. https://github.com/wekan/wekan/pull/2603"
-echo -e "Example:"
-echo -e "\t$ snap set $SNAP_NAME attachments-store-path='/var/snap/wekan/common/attachments'"
-echo -e "Disable:"
-echo -e "\t$ snap unset $SNAP_NAME attachments-store-path"
-echo -e "\n"
 echo -e "NOTIFICATION TRAY AFTER READ DAYS BEFORE REMOVE https://github.com/wekan/wekan/pull/2998"
 echo -e "Number of days after a notification is read before we remove it. Default: 2."
 echo -e "Example:"

+ 0 - 2
stacksmith/user-scripts/build.sh

@@ -72,8 +72,6 @@ meteor=/home/wekan/.meteor/meteor
 #sudo -u wekan ${meteor} add standard-minifier-js
 sudo -u wekan ${meteor} npm install
 sudo -u wekan ${meteor} build --directory /home/wekan/app_build
-sudo cp /home/wekan/app/fix-download-unicode/cfs_access-point.txt /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js
-sudo chown wekan:wekan /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js
 sudo rm /home/wekan/app_build/bundle/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs
 # Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
 rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy

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

@@ -252,10 +252,6 @@ services:
       # Defaults below. Uncomment to change. wekan/server/accounts-common.js
       # - ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90
       #---------------------------------------------------------------
-      # ==== STORE ATTACHMENT ON SERVER FILESYSTEM INSTEAD OF MONGODB ====
-      # https://github.com/wekan/wekan/pull/2603
-      #- ATTACHMENTS_STORE_PATH = <pathname> # pathname can be relative or fullpath
-      #---------------------------------------------------------------
       # ==== RICH TEXT EDITOR IN CARD COMMENTS ====
       # https://github.com/wekan/wekan/pull/2560
       - RICHER_CARD_COMMENT_EDITOR=false

Some files were not shown because too many files changed in this diff