From 05c53ca01d71a01a608c9ae345475abd67c9939b Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Mon, 18 Nov 2019 01:47:26 +0000 Subject: [PATCH 01/21] Trying to upload an attachment with Meteor-Files --- .meteor/packages | 1 + .meteor/versions | 2 + ...neFBdzt-提議者電子郵件(第一波+第二波).xlsx | Bin 0 -> 29409 bytes client/components/cards/attachments.js | 24 ++-- client/lib/utils.js | 28 +++-- models/attachments.js | 47 ++++++-- rebuild-wekan.bat | 112 +++++++++--------- server/publications/boards.js | 2 +- 8 files changed, 132 insertions(+), 84 deletions(-) create mode 100644 atachments/attachments-gAjLYeSrtAneFBdzt-提議者電子郵件(第一波+第二波).xlsx diff --git a/.meteor/packages b/.meteor/packages index f234baea6..a455c6392 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -96,3 +96,4 @@ konecty:mongo-counter percolate:synced-cron easylogic:summernote cfs:filesystem +ostrio:files diff --git a/.meteor/versions b/.meteor/versions index 3b45f9864..9b5fb93e7 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -133,6 +133,8 @@ oauth2@1.2.1 observe-sequence@1.0.16 ongoworks:speakingurl@1.1.0 ordered-dict@1.1.0 +ostrio:cookies@2.5.0 +ostrio:files@1.13.0 peerlibrary:assert@0.2.5 peerlibrary:base-component@0.16.0 peerlibrary:blaze-components@0.15.1 diff --git a/atachments/attachments-gAjLYeSrtAneFBdzt-提議者電子郵件(第一波+第二波).xlsx b/atachments/attachments-gAjLYeSrtAneFBdzt-提議者電子郵件(第一波+第二波).xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4e89ac5d963442fcbb4394886d48f214e3ea9945 GIT binary patch literal 29409 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 literal 0 HcmV?d00001 diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index e4439155e..604dc0783 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -149,20 +149,28 @@ Template.previewClipboardImagePopup.events({ if (results && results.file) { window.oPasted = pastedResults; const card = this; - const file = new FS.File(results.file); + const settings = { + file: results.file, + streams: 'dynamic', + chunkSize: 'dynamic' + }; if (!results.name) { // if no filename, it's from clipboard. then we give it a name, with ext name from MIME type + // FIXME: Check this behavior if (typeof results.file.type === 'string') { - file.name(results.file.type.replace('image/', 'clipboard.')); + settings.fileName = new Date().getTime() + results.file.type.replace('.+/', ''); } } - file.updatedAt(new Date()); - file.boardId = card.boardId; - file.cardId = card._id; - file.userId = Meteor.userId(); - const attachment = Attachments.insert(file); + settings.meta = {}; + settings.meta.updatedAt = new Date().getTime(); + settings.meta.boardId = card.boardId; + settings.meta.cardId = card._id; + settings.meta.userId = Meteor.userId(); + console.log('settings', settings); + const attachment = Attachments.insert(settings, false); - if (attachment && attachment._id && attachment.isImage()) { + // TODO: Check image cover behavior + if (attachment && attachment._id && attachment.isImage) { card.setCover(attachment._id); } diff --git a/client/lib/utils.js b/client/lib/utils.js index cc3526c03..2694396f0 100644 --- a/client/lib/utils.js +++ b/client/lib/utils.js @@ -34,21 +34,27 @@ Utils = { if (!card) { return next(); } - const file = new FS.File(fileObj); + let settings = { + file: fileObj, + streams: 'dynamic', + chunkSize: 'dynamic' + }; + settings.meta = {}; if (card.isLinkedCard()) { - file.boardId = Cards.findOne(card.linkedId).boardId; - file.cardId = card.linkedId; + settings.meta.boardId = Cards.findOne(card.linkedId).boardId; + settings.meta.cardId = card.linkedId; } else { - file.boardId = card.boardId; - file.swimlaneId = card.swimlaneId; - file.listId = card.listId; - file.cardId = card._id; + settings.meta.boardId = card.boardId; + settings.meta.swimlaneId = card.swimlaneId; + settings.meta.listId = card.listId; + settings.meta.cardId = card._id; } - file.userId = Meteor.userId(); - if (file.original) { + settings.meta.userId = Meteor.userId(); + // FIXME: What is this? +/* if (file.original) { file.original.name = fileObj.name; - } - return next(Attachments.insert(file)); + }*/ + return next(Attachments.insert(settings, false)); }, shrinkImage(options) { // shrink image to certain size diff --git a/models/attachments.js b/models/attachments.js index 9b8ec04f3..fd03e6d24 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -1,3 +1,29 @@ +import { FilesCollection } from 'meteor/ostrio:files'; + +Attachments = new FilesCollection({ + storagePath: storagePath(), + debug: true, // FIXME: Remove debug mode + collectionName: 'attachments2', + allowClientCode: false, // Disallow remove files from Client +}); + +if (Meteor.isServer) { + Meteor.startup(() => { + Attachments.collection._ensureIndex({ cardId: 1 }); + }); + + // TODO: Permission related + // TODO: Add Activity update + // TODO: publish and subscribe +// Meteor.publish('files.attachments.all', function () { +// return Attachments.find().cursor; +// }); +} else { +// Meteor.subscribe('files.attachments.all'); +} + +// ---------- Deprecated fallback ---------- // + const localFSStore = process.env.ATTACHMENTS_STORE_PATH; const storeName = 'attachments'; const defaultStoreOptions = { @@ -171,16 +197,16 @@ if (localFSStore) { ...defaultStoreOptions, }); } -Attachments = new FS.Collection('attachments', { +DeprecatedAttachs = new FS.Collection('attachments', { stores: [store], }); if (Meteor.isServer) { Meteor.startup(() => { - Attachments.files._ensureIndex({ cardId: 1 }); + DeprecatedAttachs.files._ensureIndex({ cardId: 1 }); }); - Attachments.allow({ + DeprecatedAttachs.allow({ insert(userId, doc) { return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); }, @@ -206,10 +232,10 @@ if (Meteor.isServer) { }); } -// XXX Enforce a schema for the Attachments CollectionFS +// XXX Enforce a schema for the DeprecatedAttachs CollectionFS if (Meteor.isServer) { - Attachments.files.after.insert((userId, doc) => { + DeprecatedAttachs.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') { @@ -227,7 +253,7 @@ if (Meteor.isServer) { } else { // Don't add activity about adding the attachment as the activity // be imported and delete source field - Attachments.update( + DeprecatedAttachs.update( { _id: doc._id, }, @@ -240,7 +266,7 @@ if (Meteor.isServer) { } }); - Attachments.files.before.remove((userId, doc) => { + DeprecatedAttachs.files.before.remove((userId, doc) => { Activities.insert({ userId, type: 'card', @@ -253,11 +279,16 @@ if (Meteor.isServer) { }); }); - Attachments.files.after.remove((userId, doc) => { + DeprecatedAttachs.files.after.remove((userId, doc) => { Activities.remove({ attachmentId: doc._id, }); }); } +function storagePath(defaultPath) { + const storePath = process.env.ATTACHMENTS_STORE_PATH; + return storePath ? storePath : defaultPath; +} + export default Attachments; diff --git a/rebuild-wekan.bat b/rebuild-wekan.bat index 056148995..8ea786cbb 100644 --- a/rebuild-wekan.bat +++ b/rebuild-wekan.bat @@ -1,56 +1,56 @@ -@ECHO OFF - -REM NOTE: THIS .BAT DOES NOT WORK !! -REM Use instead this webpage instructions to build on Windows: -REM https://github.com/wekan/wekan/wiki/Install-Wekan-from-source-on-Windows -REM Please add fix PRs, like config of MongoDB etc. - -md C:\repos -cd C:\repos - -REM Install chocolatey -@"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin" - -choco install -y git curl python2 dotnet4.5.2 nano mongodb-3 mongoclient meteor - -curl -O https://nodejs.org/dist/v8.16.1/node-v8.16.1-x64.msi -call node-v8.16.1-x64.msi - -call npm config -g set msvs_version 2015 -call meteor npm config -g set msvs_version 2015 - -call npm -g install npm -call npm -g install node-gyp -call npm -g install fibers -cd C:\repos -git clone https://github.com/wekan/wekan.git -cd wekan -git checkout edge -echo "Building Wekan." -REM del /S /F /Q packages -REM ## REPOS BELOW ARE INCLUDED TO WEKAN -REM md packages -REM cd packages -REM git clone --depth 1 -b master https://github.com/wekan/flow-router.git kadira-flow-router -REM git clone --depth 1 -b master https://github.com/meteor-useraccounts/core.git meteor-useraccounts-core -REM git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-cas.git -REM git clone --depth 1 -b master https://github.com/wekan/wekan-ldap.git -REM git clone --depth 1 -b master https://github.com/wekan/wekan-scrollbar.git -REM git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-oidc.git -REM git clone --depth 1 -b master --recurse-submodules https://github.com/wekan/markdown.git -REM move meteor-accounts-oidc/packages/switch_accounts-oidc wekan_accounts-oidc -REM move meteor-accounts-oidc/packages/switch_oidc wekan_oidc -REM del /S /F /Q meteor-accounts-oidc -REM sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' ~/repos/wekan/packages/meteor-useraccounts-core/package.js -cd .. -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 -cd .build\bundle\programs\server -call meteor npm install -REM cd C:\repos\wekan\.meteor\local\build\programs\server -REM del node_modules -cd C:\repos\wekan -call start-wekan.bat +@ECHO OFF + +REM NOTE: THIS .BAT DOES NOT WORK !! +REM Use instead this webpage instructions to build on Windows: +REM https://github.com/wekan/wekan/wiki/Install-Wekan-from-source-on-Windows +REM Please add fix PRs, like config of MongoDB etc. + +md C:\repos +cd C:\repos + +REM Install chocolatey +@"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin" + +choco install -y git curl python2 dotnet4.5.2 nano mongodb-3 mongoclient meteor + +curl -O https://nodejs.org/dist/v8.16.1/node-v8.16.1-x64.msi +call node-v8.16.1-x64.msi + +call npm config -g set msvs_version 2015 +call meteor npm config -g set msvs_version 2015 + +call npm -g install npm +call npm -g install node-gyp +call npm -g install fibers +cd C:\repos +git clone https://github.com/wekan/wekan.git +cd wekan +git checkout edge +echo "Building Wekan." +REM del /S /F /Q packages +REM ## REPOS BELOW ARE INCLUDED TO WEKAN +REM md packages +REM cd packages +REM git clone --depth 1 -b master https://github.com/wekan/flow-router.git kadira-flow-router +REM git clone --depth 1 -b master https://github.com/meteor-useraccounts/core.git meteor-useraccounts-core +REM git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-cas.git +REM git clone --depth 1 -b master https://github.com/wekan/wekan-ldap.git +REM git clone --depth 1 -b master https://github.com/wekan/wekan-scrollbar.git +REM git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-oidc.git +REM git clone --depth 1 -b master --recurse-submodules https://github.com/wekan/markdown.git +REM move meteor-accounts-oidc/packages/switch_accounts-oidc wekan_accounts-oidc +REM move meteor-accounts-oidc/packages/switch_oidc wekan_oidc +REM del /S /F /Q meteor-accounts-oidc +REM sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' ~/repos/wekan/packages/meteor-useraccounts-core/package.js +cd .. +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 +cd .build\bundle\programs\server +call meteor npm install +REM cd C:\repos\wekan\.meteor\local\build\programs\server +REM del node_modules +cd C:\repos\wekan +call start-wekan.bat diff --git a/server/publications/boards.js b/server/publications/boards.js index e30958332..79e578b85 100644 --- a/server/publications/boards.js +++ b/server/publications/boards.js @@ -128,7 +128,7 @@ Meteor.publishRelations('board', function(boardId, isArchived) { // Gather queries and send in bulk const cardComments = this.join(CardComments); cardComments.selector = _ids => ({ cardId: _ids }); - const attachments = this.join(Attachments); + const attachments = this.join(Attachments.collection); attachments.selector = _ids => ({ cardId: _ids }); const checklists = this.join(Checklists); checklists.selector = _ids => ({ cardId: _ids }); From 4dcdec0084414e7dde9e630add01ecd2865bd941 Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Wed, 20 Nov 2019 10:40:09 +0000 Subject: [PATCH 02/21] Attachment upload from card done, need to fix download link --- client/components/cards/attachments.js | 23 ++++++++++++++++++----- client/components/main/editor.js | 8 ++------ client/lib/utils.js | 14 +++++++++++--- models/attachments.js | 10 +++++----- models/cards.js | 12 +++++++----- models/export.js | 2 +- models/trelloCreator.js | 1 + models/wekanCreator.js | 2 ++ server/migrations.js | 4 ++-- 9 files changed, 49 insertions(+), 27 deletions(-) diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index 604dc0783..f24a7f829 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -45,18 +45,31 @@ Template.attachmentsGalery.events({ }, }); +Template.attachmentsGalery.helpers({ + url() { + return Attachments.link(this); + } +}); + Template.previewAttachedImagePopup.events({ 'click .js-large-image-clicked'() { Popup.close(); }, }); +Template.previewAttachedImagePopup.helpers({ + url() { + return Attachments.link(this); + } +}); + 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()) { + console.log('attachment', attachment); + if (attachment && attachment._id && attachment.isImage) { card.setCover(attachment._id); } Popup.close(); @@ -152,13 +165,14 @@ Template.previewClipboardImagePopup.events({ const settings = { file: results.file, streams: 'dynamic', - chunkSize: 'dynamic' + chunkSize: 'dynamic', }; if (!results.name) { // if no filename, it's from clipboard. then we give it a name, with ext name from MIME type // FIXME: Check this behavior if (typeof results.file.type === 'string') { - settings.fileName = new Date().getTime() + results.file.type.replace('.+/', ''); + settings.fileName = + new Date().getTime() + results.file.type.replace('.+/', ''); } } settings.meta = {}; @@ -166,8 +180,7 @@ Template.previewClipboardImagePopup.events({ settings.meta.boardId = card.boardId; settings.meta.cardId = card._id; settings.meta.userId = Meteor.userId(); - console.log('settings', settings); - const attachment = Attachments.insert(settings, false); + const attachment = Attachments.insert(settings); // TODO: Check image cover behavior if (attachment && attachment._id && attachment.isImage) { diff --git a/client/components/main/editor.js b/client/components/main/editor.js index 39c03aa96..bd110868d 100755 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -229,18 +229,14 @@ Template.editor.onRendered(() => { currentCard, fileObj, attachment => { - if ( - attachment && - attachment._id && - attachment.isImage() - ) { + 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(); + const url = attachment.link(); if (url) { insertImage( `${location.protocol}//${location.host}${url}`, diff --git a/client/lib/utils.js b/client/lib/utils.js index 2694396f0..d667382d0 100644 --- a/client/lib/utils.js +++ b/client/lib/utils.js @@ -37,7 +37,15 @@ Utils = { let settings = { file: fileObj, streams: 'dynamic', - chunkSize: 'dynamic' + chunkSize: 'dynamic', + onUploaded: function(error, fileObj) { + console.log('after insert', Attachments.find({}).fetch()); + if (error) { + console.log('Error while upload', error); + } else { + next(fileObj); + } + }, }; settings.meta = {}; if (card.isLinkedCard()) { @@ -51,10 +59,10 @@ Utils = { } settings.meta.userId = Meteor.userId(); // FIXME: What is this? -/* if (file.original) { + /* if (file.original) { file.original.name = fileObj.name; }*/ - return next(Attachments.insert(settings, false)); + Attachments.insert(settings); }, shrinkImage(options) { // shrink image to certain size diff --git a/models/attachments.js b/models/attachments.js index fd03e6d24..4537e47cb 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -4,7 +4,7 @@ Attachments = new FilesCollection({ storagePath: storagePath(), debug: true, // FIXME: Remove debug mode collectionName: 'attachments2', - allowClientCode: false, // Disallow remove files from Client + allowClientCode: true, // FIXME: Permissions }); if (Meteor.isServer) { @@ -15,11 +15,11 @@ if (Meteor.isServer) { // TODO: Permission related // TODO: Add Activity update // TODO: publish and subscribe -// Meteor.publish('files.attachments.all', function () { -// return Attachments.find().cursor; -// }); + Meteor.publish('attachments', function() { + return Attachments.find().cursor; + }); } else { -// Meteor.subscribe('files.attachments.all'); + Meteor.subscribe('attachments'); } // ---------- Deprecated fallback ---------- // diff --git a/models/cards.js b/models/cards.js index 3944b09f6..4c3e2c994 100644 --- a/models/cards.js +++ b/models/cards.js @@ -366,7 +366,7 @@ Cards.helpers({ // Copy attachments oldCard.attachments().forEach(att => { - att.cardId = _id; + att.meta.cardId = _id; delete att._id; return Attachments.insert(att); }); @@ -456,14 +456,16 @@ Cards.helpers({ attachments() { if (this.isLinkedCard()) { return Attachments.find( - { cardId: this.linkedId }, + { 'meta.cardId': this.linkedId }, { sort: { uploadedAt: -1 } }, ); } else { - return Attachments.find( - { cardId: this._id }, + const ret = Attachments.find( + { 'meta.cardId': this._id }, { sort: { uploadedAt: -1 } }, ); + if (ret.first()) console.log('link', Attachments.link(ret.first())); + return ret; } }, @@ -471,7 +473,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(); }, checklists() { diff --git a/models/export.js b/models/export.js index cc979ce01..c93a8bda0 100644 --- a/models/export.js +++ b/models/export.js @@ -162,7 +162,7 @@ export class Exporter { readStream.pipe(tmpWriteable); }; const getBase64DataSync = Meteor.wrapAsync(getBase64Data); - result.attachments = Attachments.find(byBoard) + result.attachments = Attachments.find({ 'meta.boardId': byBoard.boardId }) .fetch() .map(attachment => { return { diff --git a/models/trelloCreator.js b/models/trelloCreator.js index cb1a6a67b..b38e46528 100644 --- a/models/trelloCreator.js +++ b/models/trelloCreator.js @@ -345,6 +345,7 @@ export class TrelloCreator { // 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) { + // FIXME: Change to new model file.attachData(att.url, function(error) { file.boardId = boardId; file.cardId = cardId; diff --git a/models/wekanCreator.js b/models/wekanCreator.js index ec85d93f0..175c156df 100644 --- a/models/wekanCreator.js +++ b/models/wekanCreator.js @@ -415,6 +415,7 @@ export class WekanCreator { const self = this; if (Meteor.isServer) { if (att.url) { + // FIXME: Change to new file library file.attachData(att.url, function(error) { file.boardId = boardId; file.cardId = cardId; @@ -440,6 +441,7 @@ export class WekanCreator { } }); } else if (att.file) { + // FIXME: Change to new file library file.attachData( new Buffer(att.file, 'base64'), { diff --git a/server/migrations.js b/server/migrations.js index 836220f39..7d5a5cca9 100644 --- a/server/migrations.js +++ b/server/migrations.js @@ -80,7 +80,7 @@ Migrations.add('lowercase-board-permission', () => { Migrations.add('change-attachments-type-for-non-images', () => { const newTypeForNonImage = 'application/octet-stream'; Attachments.find().forEach(file => { - if (!file.isImage()) { + if (!file.isImage) { Attachments.update( file._id, { @@ -97,7 +97,7 @@ Migrations.add('change-attachments-type-for-non-images', () => { Migrations.add('card-covers', () => { Cards.find().forEach(card => { - const cover = Attachments.findOne({ cardId: card._id, cover: true }); + const cover = Attachments.findOne({ 'meta.cardId': card._id, cover: true }); if (cover) { Cards.update(card._id, { $set: { coverId: cover._id } }, noValidate); } From 6cdd464f54fca423876a27ec2a4269ae5841cdb0 Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Wed, 27 Nov 2019 09:40:19 +0000 Subject: [PATCH 03/21] Uploaded done, but uploading not --- client/components/cards/attachments.jade | 2 +- client/components/cards/attachments.js | 26 +++++++++++++----------- client/components/cards/minicard.jade | 2 +- client/components/cards/minicard.js | 3 +++ client/lib/utils.js | 16 ++------------- models/attachments.js | 6 +++++- models/cards.js | 3 +-- 7 files changed, 27 insertions(+), 31 deletions(-) diff --git a/client/components/cards/attachments.jade b/client/components/cards/attachments.jade index 2a96f4f45..3ce639bc8 100644 --- a/client/components/cards/attachments.jade +++ b/client/components/cards/attachments.jade @@ -23,7 +23,7 @@ template(name="attachmentsGalery") each attachments .attachment-item a.attachment-thumbnail.swipebox(href="{{url}}" title="{{name}}") - if isUploaded + if isUploaded if isImage img.attachment-thumbnail-img(src="{{url}}") else diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index f24a7f829..7346e9436 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -13,10 +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(); + Cards.findOne(this.meta.cardId).unsetCover(); }, 'click .js-preview-image'(event) { Popup.open('previewAttachedImage').call(this, event); @@ -47,8 +47,11 @@ Template.attachmentsGalery.events({ Template.attachmentsGalery.helpers({ url() { - return Attachments.link(this); - } + return Attachments.link(this); + }, + isUploaded() { + return !!this.meta.uploaded; + }, }); Template.previewAttachedImagePopup.events({ @@ -67,13 +70,14 @@ Template.cardAttachmentsPopup.events({ 'change .js-attach-file'(event) { const card = this; const processFile = f => { - Utils.processUploadedAttachment(card, f, attachment => { - console.log('attachment', attachment); - if (attachment && attachment._id && attachment.isImage) { - card.setCover(attachment._id); + Utils.processUploadedAttachment(card, f, + (err, attachment) => { + if (attachment && attachment._id && attachment.isImage) { + card.setCover(attachment._id); + } + Popup.close(); } - Popup.close(); - }); + ); }; FS.Utility.eachFile(event, f => { @@ -169,7 +173,6 @@ Template.previewClipboardImagePopup.events({ }; if (!results.name) { // if no filename, it's from clipboard. then we give it a name, with ext name from MIME type - // FIXME: Check this behavior if (typeof results.file.type === 'string') { settings.fileName = new Date().getTime() + results.file.type.replace('.+/', ''); @@ -182,7 +185,6 @@ Template.previewClipboardImagePopup.events({ settings.meta.userId = Meteor.userId(); const attachment = Attachments.insert(settings); - // TODO: Check image cover behavior if (attachment && attachment._id && attachment.isImage) { card.setCover(attachment._id); } diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade index 79672f8cf..0a35bd3a1 100644 --- a/client/components/cards/minicard.jade +++ b/client/components/cards/minicard.jade @@ -11,7 +11,7 @@ template(name="minicard") .handle .fa.fa-arrows if cover - .minicard-cover(style="background-image: url('{{cover.url}}');") + .minicard-cover(style="background-image: url('{{coverUrl}}');") if labels .minicard-labels each labels diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js index 4c76db462..3ddb7e97b 100644 --- a/client/components/cards/minicard.js +++ b/client/components/cards/minicard.js @@ -32,4 +32,7 @@ Template.minicard.helpers({ hiddenMinicardLabelText() { return Meteor.user().hasHiddenMinicardLabelText(); }, + coverUrl() { + return Attachments.findOne(this.coverId).link(); + }, }); diff --git a/client/lib/utils.js b/client/lib/utils.js index d667382d0..8503b0356 100644 --- a/client/lib/utils.js +++ b/client/lib/utils.js @@ -32,20 +32,12 @@ Utils = { } }; if (!card) { - return next(); + return onUploaded(); } let settings = { file: fileObj, streams: 'dynamic', chunkSize: 'dynamic', - onUploaded: function(error, fileObj) { - console.log('after insert', Attachments.find({}).fetch()); - if (error) { - console.log('Error while upload', error); - } else { - next(fileObj); - } - }, }; settings.meta = {}; if (card.isLinkedCard()) { @@ -58,11 +50,7 @@ Utils = { settings.meta.cardId = card._id; } settings.meta.userId = Meteor.userId(); - // FIXME: What is this? - /* if (file.original) { - file.original.name = fileObj.name; - }*/ - Attachments.insert(settings); + Attachments.insert(settings).on('end', next); }, shrinkImage(options) { // shrink image to certain size diff --git a/models/attachments.js b/models/attachments.js index 4537e47cb..5dcc75e6e 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -2,9 +2,12 @@ import { FilesCollection } from 'meteor/ostrio:files'; Attachments = new FilesCollection({ storagePath: storagePath(), - debug: true, // FIXME: Remove debug mode + debug: false, // FIXME: Remove debug mode collectionName: 'attachments2', allowClientCode: true, // FIXME: Permissions + onAfterUpload: (fileRef) => { + Attachments.update({_id:fileRef._id}, {$set: {"meta.uploaded": true}}); + } }); if (Meteor.isServer) { @@ -15,6 +18,7 @@ if (Meteor.isServer) { // TODO: Permission related // TODO: Add Activity update // TODO: publish and subscribe + Meteor.publish('attachments', function() { return Attachments.find().cursor; }); diff --git a/models/cards.js b/models/cards.js index 4c3e2c994..86d22c532 100644 --- a/models/cards.js +++ b/models/cards.js @@ -460,11 +460,10 @@ Cards.helpers({ { sort: { uploadedAt: -1 } }, ); } else { - const ret = Attachments.find( + let ret = Attachments.find( { 'meta.cardId': this._id }, { sort: { uploadedAt: -1 } }, ); - if (ret.first()) console.log('link', Attachments.link(ret.first())); return ret; } }, From 93337c20f83146bb702a5af8cf9cdb37f4c53443 Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Tue, 24 Dec 2019 08:57:34 +0000 Subject: [PATCH 04/21] Change upload routine, add upload popup --- client/components/cards/attachments.jade | 8 +++++ client/components/cards/attachments.js | 40 +++++++++++++++++++++--- client/components/main/editor.js | 1 + client/lib/popup.js | 2 +- client/lib/utils.js | 18 ++++++----- 5 files changed, 56 insertions(+), 13 deletions(-) diff --git a/client/components/cards/attachments.jade b/client/components/cards/attachments.jade index 3ce639bc8..04e0d04c8 100644 --- a/client/components/cards/attachments.jade +++ b/client/components/cards/attachments.jade @@ -18,6 +18,13 @@ template(name="attachmentDeletePopup") p {{_ "attachment-delete-pop"}} button.js-confirm.negate.full(type="submit") {{_ 'delete'}} +template(name="uploadingPopup") + p Uploading... + p + span.upload-percentage 0% + div.upload-progress-bar + span.upload-size {{fileSize}} + template(name="attachmentsGalery") .attachments-galery each attachments @@ -53,3 +60,4 @@ template(name="attachmentsGalery") unless currentUser.isCommentOnly li.attachment-item.add-attachment a.js-add-attachment {{_ 'add-attachment' }} + diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index 7346e9436..026ce1704 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -66,18 +66,38 @@ Template.previewAttachedImagePopup.helpers({ } }); +// For uploading popup + +let uploadFileSize = new ReactiveVar(''); +let uploadProgress = new ReactiveVar(0); + Template.cardAttachmentsPopup.events({ - 'change .js-attach-file'(event) { + 'change .js-attach-file'(event, instance) { const card = this; - const processFile = f => { - Utils.processUploadedAttachment(card, f, - (err, attachment) => { + const callbacks = { + onBeforeUpload: (err, fileData) => { + Popup.open('uploading')(this.clickEvent); + return true; + }, + onUploaded: (err, attachment) => { + console.log('onEnd'); if (attachment && attachment._id && attachment.isImage) { card.setCover(attachment._id); } Popup.close(); + }, + onStart: (error, fileData) => { + console.log('fd', fileData); + uploadFileSize.set(`${fileData.size} bytes`); + }, + onError: (err, fileObj) => { + console.log('Error!', err); + }, + onProgress: (progress, fileData) => { } - ); + }; + const processFile = f => { + Utils.processUploadedAttachment(card, f, callbacks); }; FS.Utility.eachFile(event, f => { @@ -117,12 +137,22 @@ Template.cardAttachmentsPopup.events({ }); }, 'click .js-computer-upload'(event, templateInstance) { + this.clickEvent = event; templateInstance.find('.js-attach-file').click(); event.preventDefault(); }, 'click .js-upload-clipboard-image': Popup.open('previewClipboardImage'), }); +Template.uploadingPopup.onRendered(() => { +}); + +Template.uploadingPopup.helpers({ + fileSize: () => { + return uploadFileSize.get(); + } +}); + const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL; const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO; let pastedResults = null; diff --git a/client/components/main/editor.js b/client/components/main/editor.js index bd110868d..64f24f983 100755 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -225,6 +225,7 @@ Template.editor.onRendered(() => { $summernote.summernote('insertNode', img); }; const processData = function(fileObj) { + // FIXME: Change to new API Utils.processUploadedAttachment( currentCard, fileObj, diff --git a/client/lib/popup.js b/client/lib/popup.js index 8095fbd2f..cae226594 100644 --- a/client/lib/popup.js +++ b/client/lib/popup.js @@ -49,7 +49,7 @@ window.Popup = new (class { // has one. This allows us to position a sub-popup exactly at the same // position than its parent. let openerElement; - if (clickFromPopup(evt)) { + if (clickFromPopup(evt) && self._getTopStack()) { openerElement = self._getTopStack().openerElement; } else { self._stack = []; diff --git a/client/lib/utils.js b/client/lib/utils.js index 8503b0356..213124f1b 100644 --- a/client/lib/utils.js +++ b/client/lib/utils.js @@ -25,12 +25,7 @@ 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); - } - }; + processUploadedAttachment(card, fileObj, callbacks) { if (!card) { return onUploaded(); } @@ -50,7 +45,16 @@ Utils = { settings.meta.cardId = card._id; } settings.meta.userId = Meteor.userId(); - Attachments.insert(settings).on('end', next); + if (typeof callbacks === 'function') { + settings.onEnd = callbacks; + } else { + for (const key in callbacks) { + if (key.substring(0, 2) === 'on') { + settings[key] = callbacks[key]; + } + } + } + Attachments.insert(settings); }, shrinkImage(options) { // shrink image to certain size From 6ebd6defe917a70e84467d64a1c15ec109c73f1b Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Thu, 2 Jan 2020 09:16:28 +0000 Subject: [PATCH 05/21] Uploading dialog done --- client/components/cards/attachments.jade | 8 ++++---- client/components/cards/attachments.js | 25 ++++++++++++++++++------ client/components/cards/attachments.styl | 11 +++++++++++ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/client/components/cards/attachments.jade b/client/components/cards/attachments.jade index 04e0d04c8..a5a5c00bd 100644 --- a/client/components/cards/attachments.jade +++ b/client/components/cards/attachments.jade @@ -19,10 +19,10 @@ template(name="attachmentDeletePopup") button.js-confirm.negate.full(type="submit") {{_ 'delete'}} template(name="uploadingPopup") - p Uploading... - p - span.upload-percentage 0% - div.upload-progress-bar + .uploading-info + span.upload-percentage {{progress}}% + .upload-progress-frame + .upload-progress-bar(style="width: {{progress}}%;") span.upload-size {{fileSize}} template(name="attachmentsGalery") diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index 026ce1704..9e32825e4 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -77,23 +77,24 @@ Template.cardAttachmentsPopup.events({ const callbacks = { onBeforeUpload: (err, fileData) => { Popup.open('uploading')(this.clickEvent); + uploadFileSize.set('...'); + uploadProgress.set(0); return true; }, onUploaded: (err, attachment) => { - console.log('onEnd'); if (attachment && attachment._id && attachment.isImage) { card.setCover(attachment._id); } Popup.close(); }, onStart: (error, fileData) => { - console.log('fd', fileData); - uploadFileSize.set(`${fileData.size} bytes`); + uploadFileSize.set(formatBytes(fileData.size)); }, onError: (err, fileObj) => { console.log('Error!', err); }, onProgress: (progress, fileData) => { + uploadProgress.set(progress); } }; const processFile = f => { @@ -144,12 +145,12 @@ Template.cardAttachmentsPopup.events({ 'click .js-upload-clipboard-image': Popup.open('previewClipboardImage'), }); -Template.uploadingPopup.onRendered(() => { -}); - Template.uploadingPopup.helpers({ fileSize: () => { return uploadFileSize.get(); + }, + progress: () => { + return uploadProgress.get(); } }); @@ -225,3 +226,15 @@ Template.previewClipboardImagePopup.events({ } }, }); + +function formatBytes(bytes, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} diff --git a/client/components/cards/attachments.styl b/client/components/cards/attachments.styl index 4a22fd8a3..61ea82328 100644 --- a/client/components/cards/attachments.styl +++ b/client/components/cards/attachments.styl @@ -64,6 +64,17 @@ border: 1px solid black box-shadow: 0 1px 2px rgba(0,0,0,.2) +.uploading-info + .upload-progress-frame + background-color: grey; + border: 1px solid; + height: 22px; + + .upload-progress-bar + background-color: blue; + height: 20px; + padding: 1px; + @media screen and (max-width: 800px) .attachments-galery flex-direction From d26bf04bfa088b770c85a895700fd704cc08e234 Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Tue, 14 Jan 2020 06:29:34 +0000 Subject: [PATCH 06/21] Change to relative path and /var/attachments to store --- client/components/cards/attachments.js | 4 +-- client/components/cards/minicard.js | 2 +- client/components/main/editor.js | 47 +++++++++++++------------- models/attachments.js | 2 ++ sandstorm-pkgdef.capnp | 3 +- 5 files changed, 31 insertions(+), 27 deletions(-) diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index 9e32825e4..82ecabcf5 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -47,7 +47,7 @@ Template.attachmentsGalery.events({ Template.attachmentsGalery.helpers({ url() { - return Attachments.link(this); + return Attachments.link(this, 'original', '/'); }, isUploaded() { return !!this.meta.uploaded; @@ -62,7 +62,7 @@ Template.previewAttachedImagePopup.events({ Template.previewAttachedImagePopup.helpers({ url() { - return Attachments.link(this); + return Attachments.link(this, 'original', '/'); } }); diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js index 3ddb7e97b..430042f49 100644 --- a/client/components/cards/minicard.js +++ b/client/components/cards/minicard.js @@ -33,6 +33,6 @@ Template.minicard.helpers({ return Meteor.user().hasHiddenMinicardLabelText(); }, coverUrl() { - return Attachments.findOne(this.coverId).link(); + return Attachments.findOne(this.coverId).link('original', '/'); }, }); diff --git a/client/components/main/editor.js b/client/components/main/editor.js index 64f24f983..bc2e0bad8 100755 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -225,32 +225,33 @@ Template.editor.onRendered(() => { $summernote.summernote('insertNode', img); }; const processData = function(fileObj) { - // FIXME: Change to new API 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.link(); - if (url) { - insertImage( - `${location.protocol}//${location.host}${url}`, - ); - } else { - retry++; - if (retry < maxTry) { - setTimeout(checkUrl, checkItvl); + fileObj, + { onUploaded: + 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 = Attachments.link(attachment, 'original', '/'); + if (url) { + insertImage( + `${location.protocol}//${location.host}${url}`, + ); + } else { + retry++; + if (retry < maxTry) { + setTimeout(checkUrl, checkItvl); + } } - } - }; - checkUrl(); - }); + }; + checkUrl(); + }); + } } }, ); diff --git a/models/attachments.js b/models/attachments.js index 5dcc75e6e..25e4b4bb1 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -291,6 +291,8 @@ if (Meteor.isServer) { } function storagePath(defaultPath) { + // FIXME + return '/var/attachments'; const storePath = process.env.ATTACHMENTS_STORE_PATH; return storePath ? storePath : defaultPath; } diff --git a/sandstorm-pkgdef.capnp b/sandstorm-pkgdef.capnp index dbdb76404..fc805f6be 100644 --- a/sandstorm-pkgdef.capnp +++ b/sandstorm-pkgdef.capnp @@ -257,6 +257,7 @@ const myCommand :Spk.Manifest.Command = ( (key = "OAUTH2_TOKEN_ENDPOINT", value=""), (key = "LDAP_ENABLE", value="false"), (key = "SANDSTORM", value="1"), - (key = "METEOR_SETTINGS", value = "{\"public\": {\"sandstorm\": true}}") + (key = "METEOR_SETTINGS", value = "{\"public\": {\"sandstorm\": true}}"), + (key = "ATTACHMENTS_STORE_PATH", value = "/var/attachments/") ] ); From 617fdaeb7418d4e6c2530e7a9d4a3feb62e5a00e Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Tue, 14 Jan 2020 07:06:20 +0000 Subject: [PATCH 07/21] Fix sandstorm storage path --- models/attachments.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/models/attachments.js b/models/attachments.js index 25e4b4bb1..903f64909 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -291,8 +291,12 @@ if (Meteor.isServer) { } function storagePath(defaultPath) { +/* + console.log('path', process.env.ATTACHMENTS_STORE_PATH); + console.log('env', process.env); // FIXME return '/var/attachments'; +*/ const storePath = process.env.ATTACHMENTS_STORE_PATH; return storePath ? storePath : defaultPath; } From b34ed58289a3dae5838d3b621260938a3ecf52d5 Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Thu, 13 Feb 2020 08:47:41 +0000 Subject: [PATCH 08/21] Start writing migration --- models/attachments.js | 100 +------------ server/migrate-attachments.js | 263 ++++++++++++++++++++++++++++++++++ server/migrations.js | 6 + 3 files changed, 272 insertions(+), 97 deletions(-) create mode 100644 server/migrate-attachments.js diff --git a/models/attachments.js b/models/attachments.js index 903f64909..c35d3d4c6 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -19,17 +19,17 @@ if (Meteor.isServer) { // TODO: Add Activity update // TODO: publish and subscribe - Meteor.publish('attachments', function() { + Meteor.publish('attachments2', function() { return Attachments.find().cursor; }); } else { - Meteor.subscribe('attachments'); + Meteor.subscribe('attachments2'); } // ---------- Deprecated fallback ---------- // const localFSStore = process.env.ATTACHMENTS_STORE_PATH; -const storeName = 'attachments'; +const storeName = 'attachments2'; const defaultStoreOptions = { beforeWrite: fileObj => { if (!fileObj.isImage()) { @@ -201,102 +201,8 @@ if (localFSStore) { ...defaultStoreOptions, }); } -DeprecatedAttachs = new FS.Collection('attachments', { - stores: [store], -}); - -if (Meteor.isServer) { - Meteor.startup(() => { - DeprecatedAttachs.files._ensureIndex({ cardId: 1 }); - }); - - DeprecatedAttachs.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 DeprecatedAttachs CollectionFS - -if (Meteor.isServer) { - DeprecatedAttachs.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, - 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 - DeprecatedAttachs.update( - { - _id: doc._id, - }, - { - $unset: { - source: '', - }, - }, - ); - } - }); - - DeprecatedAttachs.files.before.remove((userId, doc) => { - Activities.insert({ - userId, - type: 'card', - activityType: 'deleteAttachment', - attachmentId: doc._id, - boardId: doc.boardId, - cardId: doc.cardId, - listId: doc.listId, - swimlaneId: doc.swimlaneId, - }); - }); - - DeprecatedAttachs.files.after.remove((userId, doc) => { - Activities.remove({ - attachmentId: doc._id, - }); - }); -} function storagePath(defaultPath) { -/* - console.log('path', process.env.ATTACHMENTS_STORE_PATH); - console.log('env', process.env); - // FIXME - return '/var/attachments'; -*/ const storePath = process.env.ATTACHMENTS_STORE_PATH; return storePath ? storePath : defaultPath; } diff --git a/server/migrate-attachments.js b/server/migrate-attachments.js new file mode 100644 index 000000000..7dcc4d396 --- /dev/null +++ b/server/migrate-attachments.js @@ -0,0 +1,263 @@ +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); + + const _FStore = new FS.Store.FileSystem(storeName, { + path: localFSStore, + ...defaultStoreOptions, + }); + 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 || {}; + + // 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; + } + if (stream) return stream; + else { + try { + const stream = GStore[CRS](GStore.fileKey(fileObj), options); + return stream; + } catch (e) { + return undefined; + } + } + }.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, + }); +} +CFSAttachments = new FS.Collection('attachments', { + stores: [store], +}); + +if (Meteor.isServer) { + Meteor.startup(() => { + CFSAttachments.files._ensureIndex({ cardId: 1 }); + }); + + CFSAttachments.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 Attachments CollectionFS + +if (Meteor.isServer) { + CFSAttachments.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, + 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 + CFSAttachments.update( + { + _id: doc._id, + }, + { + $unset: { + source: '', + }, + }, + ); + } + }); + + CFSAttachments.files.before.remove((userId, doc) => { + Activities.insert({ + userId, + type: 'card', + activityType: 'deleteAttachment', + attachmentId: doc._id, + boardId: doc.boardId, + cardId: doc.cardId, + listId: doc.listId, + swimlaneId: doc.swimlaneId, + }); + }); + + CFSAttachments.files.after.remove((userId, doc) => { + Activities.remove({ + attachmentId: doc._id, + }); + }); +} + +export default CFSAttachments; diff --git a/server/migrations.js b/server/migrations.js index 7d5a5cca9..e7c18e093 100644 --- a/server/migrations.js +++ b/server/migrations.js @@ -17,6 +17,7 @@ import Swimlanes from '../models/swimlanes'; import Triggers from '../models/triggers'; import UnsavedEdits from '../models/unsavedEdits'; import Users from '../models/users'; +import CFSAttachments from './migrate-attachments'; // Anytime you change the schema of one of the collection in a non-backward // compatible way you have to write a migration in this file using the following @@ -777,3 +778,8 @@ Migrations.add('fix-incorrect-dates', () => { }), ); }); + +Migrations.add('fix-incorrect-dates', () => { + cas = CFSAttachments.find(); + console.log('cas', cas); +}); From 5899b9366c23f31149e9de8ed006a7e30b4830d5 Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Mon, 2 Mar 2020 02:10:54 +0000 Subject: [PATCH 09/21] Change coagmano:stylus package to 1.1.0 since 2.0.0 building is super slow --- .meteor/packages | 4 ++-- .meteor/release | 2 +- .meteor/versions | 6 +++--- package-lock.json | 8 ++++++++ package.json | 1 + server/migrations.js | 7 +++++++ 6 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.meteor/packages b/.meteor/packages index 83809e48f..896627a61 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -6,7 +6,7 @@ meteor-base@1.4.0 # Build system -ecmascript@0.14.0 +ecmascript@0.14.2 standard-minifier-css@1.6.0 standard-minifier-js@2.6.0 mquandalle:jade @@ -85,7 +85,7 @@ msavin:usercache wekan-scrollbar mquandalle:perfect-scrollbar mdg:meteor-apm-agent@3.2.0-rc.0! -coagmano:stylus +coagmano:stylus@1.1.0 lucasantoniassi:accounts-lockout meteorhacks:subs-manager meteorhacks:picker diff --git a/.meteor/release b/.meteor/release index c6ae8ec13..8558e1492 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.9 +METEOR@1.9.2 diff --git a/.meteor/versions b/.meteor/versions index 8128b3f47..bccba0d65 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -12,7 +12,7 @@ allow-deny@1.1.0 arillo:flow-router-helpers@0.5.2 audit-argument-checks@1.0.7 autoupdate@1.6.0 -babel-compiler@7.5.1 +babel-compiler@7.5.2 babel-runtime@1.5.0 base64@1.0.12 binary-heap@1.0.11 @@ -44,7 +44,7 @@ cfs:upload-http@0.0.20 cfs:worker@0.1.5 check@1.3.1 chuangbo:cookie@1.1.0 -coagmano:stylus@2.0.0 +coagmano:stylus@1.1.0 coffeescript@1.0.17 cottz:publish-relations@2.0.8 dburles:collection-helpers@1.1.0 @@ -57,7 +57,7 @@ deps@1.0.12 diff-sequence@1.1.1 dynamic-import@0.5.1 easylogic:summernote@0.8.8 -ecmascript@0.14.1 +ecmascript@0.14.2 ecmascript-runtime@0.7.0 ecmascript-runtime-client@0.10.0 ecmascript-runtime-server@0.9.0 diff --git a/package-lock.json b/package-lock.json index eb1a8e3f1..4b1e750bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1434,6 +1434,14 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fibers": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fibers/-/fibers-4.0.2.tgz", + "integrity": "sha512-FhICi1K4WZh9D6NC18fh2ODF3EWy1z0gzIdV9P7+s2pRjfRBnCkMDJ6x3bV1DkVymKH8HGrQa/FNOBjYvnJ/tQ==", + "requires": { + "detect-libc": "^1.0.3" + } + }, "figures": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.0.0.tgz", diff --git a/package.json b/package.json index fa9482ceb..0d19b018f 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "bson": "^4.0.3", "bunyan": "^1.8.12", "es6-promise": "^4.2.4", + "fibers": "^4.0.2", "gridfs-stream": "^0.5.3", "ldapjs": "^1.0.2", "meteor-node-stubs": "^0.4.1", diff --git a/server/migrations.js b/server/migrations.js index d7c4c724d..3861f2271 100644 --- a/server/migrations.js +++ b/server/migrations.js @@ -1038,3 +1038,10 @@ Migrations.add('fix-incorrect-dates', () => { cas = CFSAttachments.find(); console.log('cas', cas); }); + +Migrations.add('change-attachment-library', () => { + console.log('migration called here'); + Migrations.rollback('change-attachment-library'); + console.log('migration rollbacked'); +}); + From 0a1bfd37b3b1b2f4108a734c0449c1bd5a1b691f Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Tue, 5 May 2020 14:08:36 +0800 Subject: [PATCH 10/21] Migrating attachments --- server/migrations.js | 38 ++++- server/old-attachments-migration.js | 212 ++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 server/old-attachments-migration.js diff --git a/server/migrations.js b/server/migrations.js index 3861f2271..33f061f52 100644 --- a/server/migrations.js +++ b/server/migrations.js @@ -1039,9 +1039,41 @@ Migrations.add('fix-incorrect-dates', () => { console.log('cas', cas); }); +import { MongoInternals } from 'meteor/mongo'; + Migrations.add('change-attachment-library', () => { - console.log('migration called here'); - Migrations.rollback('change-attachment-library'); - console.log('migration rollbacked'); + const http = require('http'); + const fs = require('fs'); + CFSAttachments.find().forEach(file => { + const bucket = new MongoInternals.NpmModule.GridFSBucket(MongoInternals.defaultRemoteCollectionDriver().mongo.db, {bucketName: 'cfs_gridfs.attachments'}); + const gfsId = new MongoInternals.NpmModule.ObjectID(file.copies.attachments.key); + const reader = bucket.openDownloadStream(gfsId); + const path = `/var/attachments/${file.name()}`; + const fd = fs.createWriteStream(path); + reader.pipe(fd); + let opts = { + fileName: file.name(), + type: file.type(), + fileId: file._id, + meta: { + userId: file.userId, + boardId: file.boardId, + cardId: file.cardId + } + }; + if (file.listId) { + opts.meta.listId = file.listId; + } + if (file.swimlaneId) { + opts.meta.swimlaneId = file.swimlaneId; + } + Attachments.addFile(path, opts, (err, fileRef) => { + if (err) { + console.log('error when migrating ', fileName, err); + } else { + file.remove(); + } + }); + }); }); diff --git a/server/old-attachments-migration.js b/server/old-attachments-migration.js new file mode 100644 index 000000000..3a6aa85de --- /dev/null +++ b/server/old-attachments-migration.js @@ -0,0 +1,212 @@ +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); + + const _FStore = new FS.Store.FileSystem(storeName, { + path: localFSStore, + ...defaultStoreOptions, + }); + 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 || {}; + + // 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; + } + if (stream) return stream; + else { + try { + const stream = GStore[CRS](GStore.fileKey(fileObj), options); + return stream; + } catch (e) { + return undefined; + } + } + }.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, + }); +} +CFSAttachments = new FS.Collection('attachments', { + stores: [store], +}); + +if (Meteor.isServer) { + Meteor.startup(() => { + CFSAttachments.files._ensureIndex({ cardId: 1 }); + }); + + CFSAttachments.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) { + if (Meteor.isServer) { + return true; + } + const board = Boards.findOne(doc.boardId); + if (board.isPublic()) { + return true; + } else { + return board.hasMember(userId); + } + }, + + fetch: ['boardId'], + }); +} + +export default CFSAttachments; From 269698ba780a0af5fe0b24fc3c2c43d1f3f1b49d Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Tue, 5 May 2020 14:18:10 +0800 Subject: [PATCH 11/21] Attachments download --- client/components/cards/attachments.jade | 4 +- server/migrate-attachments.js | 263 ----------------------- 2 files changed, 2 insertions(+), 265 deletions(-) delete mode 100644 server/migrate-attachments.js diff --git a/client/components/cards/attachments.jade b/client/components/cards/attachments.jade index e6e50d7a6..57e46e396 100644 --- a/client/components/cards/attachments.jade +++ b/client/components/cards/attachments.jade @@ -29,7 +29,7 @@ template(name="attachmentsGalery") .attachments-galery each attachments .attachment-item - a.attachment-thumbnail.swipebox(href="{{url}}" title="{{name}}") + a.attachment-thumbnail.swipebox(href="{{url}}" download="{{name}}" title="{{name}}") if isUploaded if isImage img.attachment-thumbnail-img(src="{{url}}") @@ -40,7 +40,7 @@ template(name="attachmentsGalery") p.attachment-details = name span.attachment-details-actions - a.js-download(href="{{url download=true}}") + a.js-download(href="{{url download=true}}" download="{{name}}") i.fa.fa-download | {{_ 'download'}} if currentUser.isBoardMember diff --git a/server/migrate-attachments.js b/server/migrate-attachments.js deleted file mode 100644 index 7dcc4d396..000000000 --- a/server/migrate-attachments.js +++ /dev/null @@ -1,263 +0,0 @@ -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); - - const _FStore = new FS.Store.FileSystem(storeName, { - path: localFSStore, - ...defaultStoreOptions, - }); - 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 || {}; - - // 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; - } - if (stream) return stream; - else { - try { - const stream = GStore[CRS](GStore.fileKey(fileObj), options); - return stream; - } catch (e) { - return undefined; - } - } - }.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, - }); -} -CFSAttachments = new FS.Collection('attachments', { - stores: [store], -}); - -if (Meteor.isServer) { - Meteor.startup(() => { - CFSAttachments.files._ensureIndex({ cardId: 1 }); - }); - - CFSAttachments.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 Attachments CollectionFS - -if (Meteor.isServer) { - CFSAttachments.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, - 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 - CFSAttachments.update( - { - _id: doc._id, - }, - { - $unset: { - source: '', - }, - }, - ); - } - }); - - CFSAttachments.files.before.remove((userId, doc) => { - Activities.insert({ - userId, - type: 'card', - activityType: 'deleteAttachment', - attachmentId: doc._id, - boardId: doc.boardId, - cardId: doc.cardId, - listId: doc.listId, - swimlaneId: doc.swimlaneId, - }); - }); - - CFSAttachments.files.after.remove((userId, doc) => { - Activities.remove({ - attachmentId: doc._id, - }); - }); -} - -export default CFSAttachments; From 7dc0bbd7b26ef50ebd6c0a4d719658c2d288c2d3 Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Wed, 6 May 2020 11:15:01 +0800 Subject: [PATCH 12/21] Set correct storage location --- server/migrations.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/migrations.js b/server/migrations.js index 33f061f52..b02ca2464 100644 --- a/server/migrations.js +++ b/server/migrations.js @@ -1048,7 +1048,11 @@ Migrations.add('change-attachment-library', () => { const bucket = new MongoInternals.NpmModule.GridFSBucket(MongoInternals.defaultRemoteCollectionDriver().mongo.db, {bucketName: 'cfs_gridfs.attachments'}); const gfsId = new MongoInternals.NpmModule.ObjectID(file.copies.attachments.key); const reader = bucket.openDownloadStream(gfsId); - const path = `/var/attachments/${file.name()}`; + let store = Attachments.storagePath(); + if (store.charAt(store.length - 1) === '/') { + store = store.substring(0, store.length - 1); + } + const path = `${store}/${file.name()}`; const fd = fs.createWriteStream(path); reader.pipe(fd); let opts = { From 444848876759173ad80203129250d2f0311f30fc Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Fri, 8 May 2020 09:32:19 +0800 Subject: [PATCH 13/21] Done attachments activities operating --- client/components/activities/activities.js | 7 +- models/activities.js | 6 +- models/attachments.js | 249 ++++++--------------- 3 files changed, 74 insertions(+), 188 deletions(-) diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js index b082273a1..9697d28c1 100644 --- a/client/components/activities/activities.js +++ b/client/components/activities/activities.js @@ -152,17 +152,18 @@ BlazeComponent.extendComponent({ attachmentLink() { const attachment = this.currentData().attachment(); + const link = attachment.link('original', '/'); // trying to display url before file is stored generates js errors return ( attachment && - attachment.url({ download: true }) && + link && Blaze.toHTML( HTML.A( { - href: attachment.url({ download: true }), + href: link, target: '_blank', }, - attachment.name(), + attachment.get('name'), ), ) ); diff --git a/models/activities.js b/models/activities.js index 19e3fb7d6..3f8a0d356 100644 --- a/models/activities.js +++ b/models/activities.js @@ -214,7 +214,11 @@ if (Meteor.isServer) { } if (activity.attachmentId) { const attachment = activity.attachment(); - params.attachment = attachment.original.name; + if (attachment.original) { + params.attachment = attachment.original.name; + } else { + params.attachment = attachment.versions.original.name; + } params.attachmentId = attachment._id; } if (activity.checklistId) { diff --git a/models/attachments.js b/models/attachments.js index c35d3d4c6..798d04bed 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -1,13 +1,15 @@ import { FilesCollection } from 'meteor/ostrio:files'; +const collectionName = 'attachments2'; + Attachments = new FilesCollection({ storagePath: storagePath(), - debug: false, // FIXME: Remove debug mode + debug: false, + allowClientCode: true, collectionName: 'attachments2', - allowClientCode: true, // FIXME: Permissions - onAfterUpload: (fileRef) => { - Attachments.update({_id:fileRef._id}, {$set: {"meta.uploaded": true}}); - } + onAfterUpload: onAttachmentUploaded, + onBeforeRemove: onAttachmentRemoving, + onAfterRemove: onAttachmentRemoved }); if (Meteor.isServer) { @@ -17,189 +19,12 @@ if (Meteor.isServer) { // TODO: Permission related // TODO: Add Activity update - // TODO: publish and subscribe - Meteor.publish('attachments2', function() { + Meteor.publish(collectionName, function() { return Attachments.find().cursor; }); } else { - Meteor.subscribe('attachments2'); -} - -// ---------- Deprecated fallback ---------- // - -const localFSStore = process.env.ATTACHMENTS_STORE_PATH; -const storeName = 'attachments2'; -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); - - const _FStore = new FS.Store.FileSystem(storeName, { - path: localFSStore, - ...defaultStoreOptions, - }); - 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 || {}; - - // 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; - } - if (stream) return stream; - else { - try { - const stream = GStore[CRS](GStore.fileKey(fileObj), options); - return stream; - } catch (e) { - return undefined; - } - } - }.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, - }); + Meteor.subscribe(collectionName); } function storagePath(defaultPath) { @@ -207,4 +32,60 @@ function storagePath(defaultPath) { return storePath ? storePath : defaultPath; } +function onAttachmentUploaded(fileRef) { + Attachments.update({_id:fileRef._id}, {$set: {"meta.uploaded": true}}); + if (!fileRef.meta.source || fileRef.meta.source !== 'import') { + // Add activity about adding the attachment + Activities.insert({ + userId: fileRef.userId, + type: 'card', + activityType: 'addAttachment', + attachmentId: fileRef._id, + boardId: fileRef.meta.boardId, + cardId: fileRef.meta.cardId, + listId: fileRef.meta.listId, + swimlaneId: fileRef.meta.swimlaneId, + }); + } else { + // Don't add activity about adding the attachment as the activity + // be imported and delete source field + CFSAttachments.update( + { + _id: fileRef._id, + }, + { + $unset: { + source: '', + }, + }, + ); + } +} + +function onAttachmentRemoving(cursor) { + const file = cursor.get()[0]; + const meta = file.meta; + Activities.insert({ + userId: this.userId, + type: 'card', + activityType: 'deleteAttachment', + attachmentId: file._id, + boardId: meta.boardId, + cardId: meta.cardId, + listId: meta.listId, + swimlaneId: meta.swimlaneId, + }); + return true; +} + +function onAttachmentRemoved(files) { + // Don't know why we need to remove the activity +/* for (let i in files) { + let doc = files[i]; + Activities.remove({ + attachmentId: doc._id, + }); + }*/ +} + export default Attachments; From 012ca39a8dc29517aef191e85325f3e5889daf37 Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Fri, 8 May 2020 11:50:43 +0800 Subject: [PATCH 14/21] Attachment activities merging done --- .meteor/packages | 1 + .meteor/versions | 1 + client/components/activities/activities.js | 2 +- models/activities.js | 6 +----- models/attachments.js | 21 +++++---------------- 5 files changed, 9 insertions(+), 22 deletions(-) diff --git a/.meteor/packages b/.meteor/packages index ba278f347..b0df2be83 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -98,3 +98,4 @@ percolate:synced-cron easylogic:summernote cfs:filesystem ostrio:cookies +ostrio:files diff --git a/.meteor/versions b/.meteor/versions index 5157f6796..23aa4804f 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -134,6 +134,7 @@ observe-sequence@1.0.16 ongoworks:speakingurl@1.1.0 ordered-dict@1.1.0 ostrio:cookies@2.6.0 +ostrio:files@1.14.2 peerlibrary:assert@0.3.0 peerlibrary:base-component@0.16.0 peerlibrary:blaze-components@0.15.1 diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js index 72af4c350..186200ecc 100644 --- a/client/components/activities/activities.js +++ b/client/components/activities/activities.js @@ -163,7 +163,7 @@ BlazeComponent.extendComponent({ href: link, target: '_blank', }, - attachment.name(), + attachment.name, ), )) || this.currentData().activity.attachmentName diff --git a/models/activities.js b/models/activities.js index df207bcae..2663dd299 100644 --- a/models/activities.js +++ b/models/activities.js @@ -217,11 +217,7 @@ if (Meteor.isServer) { } if (activity.attachmentId) { const attachment = activity.attachment(); - if (attachment.original) { - params.attachment = attachment.original.name; - } else { - params.attachment = attachment.versions.original.name; - } + params.attachment = attachment.name; params.attachmentId = attachment._id; } if (activity.checklistId) { diff --git a/models/attachments.js b/models/attachments.js index cab3d9e3a..d469f702f 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -8,8 +8,7 @@ Attachments = new FilesCollection({ allowClientCode: true, collectionName: 'attachments2', onAfterUpload: onAttachmentUploaded, - onBeforeRemove: onAttachmentRemoving, - onAfterRemove: onAttachmentRemoved + onBeforeRemove: onAttachmentRemoving }); if (Meteor.isServer) { @@ -41,9 +40,9 @@ function onAttachmentUploaded(fileRef) { type: 'card', activityType: 'addAttachment', attachmentId: fileRef._id, - // this preserves the name so that notifications can be meaningful after + // this preserves the name so that notifications can be meaningful after // this file is removed - attachmentName: fileRef.versions.original.name, + attachmentName: fileRef.name, boardId: fileRef.meta.boardId, cardId: fileRef.meta.cardId, listId: fileRef.meta.listId, @@ -73,9 +72,9 @@ function onAttachmentRemoving(cursor) { type: 'card', activityType: 'deleteAttachment', attachmentId: file._id, - // this preserves the name so that notifications can be meaningful after + // this preserves the name so that notifications can be meaningful after // this file is removed - attachmentName: file.versions.original.name, + attachmentName: file.name, boardId: meta.boardId, cardId: meta.cardId, listId: meta.listId, @@ -84,14 +83,4 @@ function onAttachmentRemoving(cursor) { return true; } -function onAttachmentRemoved(files) { - // Don't know why we need to remove the activity -/* for (let i in files) { - let doc = files[i]; - Activities.remove({ - attachmentId: doc._id, - }); - }*/ -} - export default Attachments; From 4c5a2fbd1f8ad2f2447235442bf96b893f18a409 Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Thu, 14 May 2020 14:55:54 +0800 Subject: [PATCH 15/21] Card clone OK --- models/attachments.js | 35 +++++++++++++++++++++++++++++++++-- models/cards.js | 12 ++++++++---- server/migrations.js | 3 +-- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/models/attachments.js b/models/attachments.js index d469f702f..1a55cb858 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -1,4 +1,5 @@ import { FilesCollection } from 'meteor/ostrio:files'; +const fs = require('fs'); const collectionName = 'attachments2'; @@ -19,6 +20,36 @@ if (Meteor.isServer) { // TODO: Permission related // TODO: Add Activity update + Meteor.methods({ + cloneAttachment(file, overrides) { + check(file, Object); + check(overrides, Match.Maybe(Object)); + const path = file.path; + const opts = { + fileName: file.name, + type: file.type, + meta: file.meta, + userId: file.userId + }; + for (let key in overrides) { + if (key === 'meta') { + for (let metaKey in overrides.meta) { + opts.meta[metaKey] = overrides.meta[metaKey]; + } + } else { + opts[key] = overrides[key]; + } + } + const buffer = fs.readFileSync(path); + Attachments.write(buffer, opts, (err, fileRef) => { + if (err) { + console.log('Error when cloning record', err); + } + }); + return true; + } + }); + Meteor.publish(collectionName, function() { return Attachments.find().cursor; }); @@ -51,13 +82,13 @@ function onAttachmentUploaded(fileRef) { } else { // Don't add activity about adding the attachment as the activity // be imported and delete source field - CFSAttachments.update( + Attachments.collection.update( { _id: fileRef._id, }, { $unset: { - source: '', + 'meta.source': '', }, }, ); diff --git a/models/cards.js b/models/cards.js index 1236de1a3..ae52ff048 100644 --- a/models/cards.js +++ b/models/cards.js @@ -402,10 +402,14 @@ Cards.helpers({ const _id = Cards.insert(this); // Copy attachments - oldCard.attachments().forEach(att => { - att.meta.cardId = _id; - delete att._id; - return Attachments.insert(att); + oldCard.attachments().forEach((file) => { + Meteor.call('cloneAttachment', file, + { + meta: { + cardId: _id + } + } + ); }); // copy checklists diff --git a/server/migrations.js b/server/migrations.js index e330f0430..3577c78d9 100644 --- a/server/migrations.js +++ b/server/migrations.js @@ -1061,6 +1061,7 @@ Migrations.add('change-attachment-library', () => { let opts = { fileName: file.name(), type: file.type(), + size: file.size(), fileId: file._id, meta: { userId: file.userId, @@ -1077,8 +1078,6 @@ Migrations.add('change-attachment-library', () => { Attachments.addFile(path, opts, (err, fileRef) => { if (err) { console.log('error when migrating ', fileName, err); - } else { - file.remove(); } }); }); From 09ce3e464fd609b3ecc8bec5263ab06093c3a442 Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Thu, 14 May 2020 16:43:59 +0800 Subject: [PATCH 16/21] Fix typo --- server/migrations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/migrations.js b/server/migrations.js index 3577c78d9..840ab1700 100644 --- a/server/migrations.js +++ b/server/migrations.js @@ -1077,7 +1077,7 @@ Migrations.add('change-attachment-library', () => { } Attachments.addFile(path, opts, (err, fileRef) => { if (err) { - console.log('error when migrating ', fileName, err); + console.log('error when migrating ', fileRef.name, err); } }); }); From b80396f627665119cd38f11f2d466ce53ec573ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E4=BB=B2=E6=98=8E=20=28Romulus=20Urakagi=20Tsai?= =?UTF-8?q?=29?= Date: Thu, 14 May 2020 17:36:57 +0800 Subject: [PATCH 17/21] Purge unneeded require --- server/migrations.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/migrations.js b/server/migrations.js index 840ab1700..bf0193124 100644 --- a/server/migrations.js +++ b/server/migrations.js @@ -1045,7 +1045,6 @@ Migrations.add('add-sort-field-to-boards', () => { import { MongoInternals } from 'meteor/mongo'; Migrations.add('change-attachment-library', () => { - const http = require('http'); const fs = require('fs'); CFSAttachments.find().forEach(file => { const bucket = new MongoInternals.NpmModule.GridFSBucket(MongoInternals.defaultRemoteCollectionDriver().mongo.db, {bucketName: 'cfs_gridfs.attachments'}); From 4064f3f4063136c97aa7bcbcdc18fec923934b74 Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Wed, 20 May 2020 15:11:22 +0800 Subject: [PATCH 18/21] Fix migrated attachment not readable bug Remove reduandant files --- ...neFBdzt-提議者電子郵件(第一波+第二波).xlsx | Bin 29409 -> 0 bytes client/components/cards/attachments.js | 5 +- client/lib/utils.js | 4 +- models/attachments.js | 16 +++++-- server/migrations.js | 44 +++++++++--------- 5 files changed, 43 insertions(+), 26 deletions(-) delete mode 100644 atachments/attachments-gAjLYeSrtAneFBdzt-提議者電子郵件(第一波+第二波).xlsx diff --git a/atachments/attachments-gAjLYeSrtAneFBdzt-提議者電子郵件(第一波+第二波).xlsx b/atachments/attachments-gAjLYeSrtAneFBdzt-提議者電子郵件(第一波+第二波).xlsx deleted file mode 100644 index 4e89ac5d963442fcbb4394886d48f214e3ea9945..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29409 zcmeEtWmH~GvMvO7*Wm8%?(R--cL)x_g1fr~cPF?83GVLhPH?|3d^2a}oS9kc{Jra5 zegJDfU0qdAS6BD$>a8FR0*VR*4g?7V1Vji_=me@13Je5P4gmy&0tERJU4RBu65oEK@zBAu$A_cx+!zKQrG2Ca!Eq35gismkzZ65A0rWuWn9tU(~Rmn9c~)4l)x|16tv1nv=XY|{USNTuaC?l5xBbz0P&MDTAsY!ALrLvJKM8R!N)PAm+NVv z5UgP~^q9AFtH@GA$TlQ0vq0^~j>0ieUtP1-bIVUiH=KVet_=^zZb^71He|t-M}@gO zaVUsoP#iKapNz*UHd#s75cq+@ zJ?ye8Yzk8{zFeFpG%V?lc9k)WH26eh1oDGyNn8H^ieT-+TC}308_}-xFH-d`ZznvxUXvTxMfOXCT3mebN8}mODV(|F?hIW=1Uo7!ZLrJaWzv9jX*v=U=Ps=?kjo=lBFBCI>8uQm^|P z?W!wUs*UNlL{p%~JX3$2AgtR6^L6Q0pcN@1Dt)4w7pH$OkXUd9Do-o_iryugv%8ssK2t__k<8zHY7 zyHFds@Qt{bjLZeQYa{zBVN$G>+dnyUsP7&6z;bJ)9wPkP;L@JUYsm+sUvE$#p!XJl z;QHsVibxojM-fErKPP)dNasAvIdN&#lA5MPDv9mI%!S-Ha2=9cSX~^Eu?S@(#6f%@ z6wO%*NZAmS1O>%LA^!9R+e%CR24>bsX-xtw&wMVsX4Y}u?yihk-WMb|@*%@m20}fyw?dz`RsU>_EVZ`o^Y3@h?Pqsjv)qcd63v7R z2>mk03x~t4nL;{RuhyeZHkfJ;?{jFf$Hu`6%Hn%|Xh*hfhhoQAep)_#?0x}NkrgoV zVi)4Bn9s~C2yd>x*j~Qva;cs&x2xk?Pm1rA@xxSqG2I%|UEqLfRP*00jB)q=hK^Sb zVHKI(@gu+zs@TQq>~zmQ;okHovkysj=A-1ri}b<@!dz0E!esfd-%+Cww|MJ*e}C7O z(7iXYpLZtAm^bKE#l@;FGB$6UZgQ=C&-Sxuvg|=ho=R9!Q)b!^)os39Y@cqo8UnQv zp&DS_Le^cuPNwXF1SK(fi)LWf#AznWqa3#Kr0dtIuND$Mv*~qTzI3u#(Xmk)E9B z$cO*Br||Dg5a9#75s(uB0b%|lpdHOkOq?9)KmM@1C(MK|%eLEWh+gy)J`C3#H58m3 zp!Vp}vEgUJzPZc8KMjX#svCBthD@ic&J~C({wPr7Cojx#W~BBEkBCfd_v7zRpLi&; zn>a6uozO*@{x(rR9UdNLd%TJs$Df-lx40VS<5x{K{M?QqdwqGhpM|$x`l#hd6Xqg@ zPB+TN)o^-az-Q;uzm>u6I$Pg_=umv*xHdKZXtJh#OtkLmKUU<9+&}!lNl&k6x0!=y zl*ffLq(fM`dfePp*B;x#u}k}V9cS3es65lvXe9IMaryYBr;baVD2W z-PIuG+86$9jBh+%N0*1+hTI%xMCG!+J&Qph@Q!&5u6l;G0ZczWvJult-mwj?ll95?>5#!)D#_>Vd8~__y_8k0t`Ml&G3UT)9^qCIM|%G>i8P)YrfXy3h0? zv5|<{Bp61Qmb?8aHw=)A_auxWQbt)Mk;E!OPhM@vqHI$7d0znyus6zsy+|o#8Zk+_ z5+t`F6DLx!ssQ3~%ciGe=&p?DcKT#L@xVz5)M4X(#j->x3_66pOK*5OVi*m6Vw0Qe z$p*C!0iMDx6v5`FFlH4EA^J@CHM=>y2K2>@7gp$35TIw@_&y{mMcI*qo$>)OD2@%f zzA3N~<9AW!c_7M21N#E|04a*)X!TqGEYhVDQJ{fV3($&~k4a#2k#L>iJRjJ^sga6T zm2dQ!5)u4PAgS`JDoZ7wEg0Cm9PCeme#v7P3^7&;l^5ZXmD<)H#(I}m z1mHIS-0@wW9nrGT9uYJr3?LullYfH-sg&s4MxY$bEr`fqhS%~1=m)^UF0L>_HSC@p z$zy87eCQK5!9pBYKYd$HfPx7$a{z9SAxvY*nC;cj@ky|BEDFQ8)PWpwFNqmnM;I}+ zPY}_XjB%hPID)dGN+71RaU1MrRx_CpGKCrccUY>$3WgKHpdNsvJgH+CFw*$PfG8Vk z@)6p*$s26H0Ty^a0`)Kh^dE!(}29-TRMQ+59Ah`O9ABlx? zC$mie$ySMJSVLO29oMwj$B1J^R85?5YyBE zw-zv3@mzs3r>GaE%#9SBhqlkAkI<3~$s%r&E!)q*pW;iskJMgJ~p$0m717(C7oNyZ;3^5=EG^!_|k@jMTbD*B*BzLQ31wR*F9MC}XKa zsPq|IuQ}gI7p~tXA4cQ9J{R5TMencEjX%9OZ;89L^2lUW%}>9quA1Dn`K;=$ z;mSX;NAuv0S1p&loj**k?mDg6;rB>Z(Yvl%H?(=I;;j{KL*DIc^>Og=;<|Wvtay2^ zcIxyCjy)xx&R%u$;LBlPpE|n^f2~@+vdfa^KZ&X$=kM+B8Qy%Ph2_d@J3I4vxOU;O zRF8xDcBob-5ZO(-hA2aqQ)%ra#-W%mGM!! zlXJHGcDL!3lH-Gat4II4lYD;ru@@4nZfa@wx_m=)? zl>sZh;_Z0Y8`c%ZM~D6mE;PvJiNuvs`oV7K-m%HrhhHA=PJVfue%=SC^VP)X{@U}# z%J{){liV;R`l;W}#b@yWW;4F?7`>;dbvqot_l?t?7nXWRe&_Zs_)y^P!p+M&S|n%*V=m`<5Xu7Lf2o_swnhFb}&u zZ5}>%zx6V7U?-W3Yc>c?VHah{-4lKUYhcx;Tp^O9j5-cvswa~?O1ri865boMKyRVS zmq=UxOtGJdDE<@9arlMCJ)+`wxm!dYP0u5}$vvw}YXo76@dhbX&FH78bfjITxs2S3 z-`4L9D0=8`mQ7&0NI>R7=ond*8D>obQlm(zlz$>g7?`cfIHJ@Il*@2gV7+N-bXEhQ_IsRT!U5_cT4UpETKfv^p zgrLEB2YsDw-a$fZTJSSwaSMh1iQyIPm|*=WU?`_h%0R322!?s!`H?w8eLkE5LEMgx z^-3d1GF$Mw)vl_H+%Nzp5j%aN6yQvnO z-|>kO8KD`&H~^gm=M+$<4svm9YLw}1#(}J@&t|2uN@jxM^bcO63&vp|@I|XY6K*9P zU_~->sJ>maL@WpSfD-|51OPUvLKEhu`9ij(4R78sOOV3xf#w0wX8`(H1<<)U#XC9@ zS^+Ld_JP&_P-~6>-&PF(YV}L@L-Q4ER0oJ;ouN5B^>TG`~_!jC1?qLyi}kfB(JQsktKj$ z2A5Q4TAVfx#ExY4?YRfx_2lOZNaDF13#bRAM ze}Ql`DzvEO9w>nQL*Bv{ z6@C;}-xNqe|H-u#JSXE}K|~UO{4nQ8dngB_pQ1G{v{bNkvx36<3_*aoB_fwZF4hqk zuquEM$f~>walyl{l*x!QGep-W;S)V^Qvgx4l>xFX0?4D}3h}O&gAd)32g78*He$>( z{Q2Fa81EYwo2Wn9Jv|1=2)pD$?L$3Rc93b00UakO-%_+k73$NvUTlfr!DZ6&X0`c9H$kz65qyqkYq$P0O)t_HAYd z_^!YWu$?MKx;Y@bP%VdED-aLTDyTHEco}Km?_xW+LiA=q9MCU-Qnbff)n97>>tOse z-&s?Lwc&+6Q%`n)5|phFk_YGIutGTg!mLt(E-)(s*zbxq+)!w&{j=Jav>aW;kcby716>#Btw@BRQfG+aUO>MFipAmv ziW2X>eqfH-xmcxhA?W~;XrON>XrMm^iE?CEF2@bQ)U)S%jwS66Q*j*7&|HSS?-Wf- zAKq{Ux-U}dlV<})xa$-!04jt-YI)T_A1ocA?wJ%Z1e;_7`n?y84&}fIC;)_|h#~=* zkb-g%13h;jCk^_j0zKpz{CjA*=2nP@<_gIC-J*LeEoK!P)9MtNUO~ixcLw3;c7+5t z;Re&Q;sVT%P^%wiU{ya~zN!FlZL{<|p7~2+D?Xk@JSfyl|X9v0W!1ybqegTHS z5-=U!L&`w00s6kS#?+j>C)pd|R15{Q5qt=;==)@nt?2fH{dNxvi7!afTlLlk=(4dvwCKAz*p+Jy;U}vvB8c zBba1TR2}eHmANjx!r<&Zrk-)ktrU6Lg8nx8CX19bn=W%uHfX*d7F!Iv9C&_4Z|N#ZZrL z@1(7`N#b$LJd!3*XHLk_aZ=;fGKR$FDS9E2&+n8EPiZE9jXwxLMnQ=NtUEkALsuNl zWXak|bj<-R#VBcmk(Ut}93M}+IBj2Pnn|*tlF_Jg2nj4!*SSk&(Mw+ zE|&_{$Pp&_Z%yHAsQ57|v&UxuZ3A!@{?hl}H~>F;3eML~{oUUwLPJDi@s90xcX13w z>pdw*shvXp3&?|ZkOlOEYZwS0J0W8mL7xKX=Uu7i9-z4@S1&X!QBG<`HOOawOipao zk-tp&)oAQ2#LRh!_(50fO3NT2z(v9dLgci8-^5@j)Cd6HT{P!q;gNDfalDPn0Bk{V zS2GrG9Dk4)tdR`wyDt51UpKrptl^HiioIq!j-d`$FZO@a7h~lR;(UmBpH>WDl^mA{ zoY8@$lwf`b(W-P;7)f{A?0&oMRMa7zEEsaK1Tqi6er&inG?^M6fSz(lyEGgk5fskn*tox$ zhUTf`jt2-F#LuO>Jtn``Ne}O(F0$%#Dk&Pyq|&yOV&jx`3G)WStI(@5=Y6?BD&vDX<+3(vuz;IGwFU!Fcmb& zAcmtYsu`r8iI95wn)o|<-l-2JYKoa6!ZAOGw4ZGG;_0H=U1WQ}txNF>M4){XOWn@G z4xXi0pm&D&b#w#7ge=xFg{G|r^TyBQ5aA7*f}afYYGLPg+AYP~-_`}cWr|o+F+vi$~Vi<(YgmsJ*i%|bvuvc4A8e-^PGw|w(n+3?q%GmtR7Ss4EFb` z{QE9?k$hx!Inhl8aj5y?+23*=TuxLr7WSAek~6FEE=TdlYZzd*G%i!y^5nZ}IM6s6 z9%Pod%yuX|$u<*~?MhWHvD`<-w_Ux+tiMiwtr&zAi|RHTdT}tv19W${Mo9aE8159I zi|1Wz$MkPv<3_*Db{NMmTlr8LaxGHCnfzn(E>a!LKBx7Ejqm685PV0zd;sKi)N+&`dK#8-o*zZ!%jnruzXp`{T`qy3aW^;efa78DyuJ7cfqe z;qc^MejmZJ$LT$9)_qIYIBv8AXPN0NE$^8g@`mZ#=~uhutD>G|lRsOH{CI5Gx8W~5XQDW-r|e?z;$E~5Zf%dJ0f#>0 z-i?Rt>sqe2W68%w+m}*rq1&%5o!)%1a?8`N&&QhxSu1)k+YcL!%<#*S{4(_knf`yhsoOW@1NF?Xmh9zStRe(mml9p7b3f8r_mYziMs zGt=@ZaK{M4y!ygpiMCU)hT~`j6^fQ8#$cMRBMn4OPAarNMgt9|CQy;&1+&Mycd;f4 zsw9SQZ&5PToni1H{PRS4sin;yJ%~Dbx%zXO9WQl%E?y-4p&M}EU;{lSK zG5UPD5}X5~r+>a13Ld-O{;=|b2Q9Q28NRxO`ghd`lwKN??*9z~ALk<^JFhTqPokuw^HVfqz_LA3jsn=Jw|H`IzE~hH{pRsL@*V$ z0)&AqB55?9nS2dZ*%~}>-jV_6l>-y4qtbVw;mKdNI8D`Ng{%$G2krX4rr6(m{!$yCM#huLMQ%a)thPR?qb=IHx=PWfE^PQf5)oIYo+ z1n+?A>2Lsm0`c1v*GC+vh~tq}M^})4M*&L?RNViH0_J}Xg1;#6{h)w6P|0ssTZmwu z?FC3xcbMp%f`W&qBaL*S=ywWE4?TX&V4z2Z(wsPhKo1u2DOF#1x1Q(<0HR}-r_^$CmZAMAnyMdBfFEhVzjdNf6rnO0Gjx*Qhb-grdiE(zc$z2u&oc08cS&V? zpu_21wr#X_@|$asK?vUXpbsDl`adXe7mocFQ?``&9tEsg`{XZ3P%`srzpu^!m8XBL zNeL$*<^8WH_^3?%_bB)q1quI`Q(%0>LiE3)fb~B{!NmXN6v%v``u4w~fbCx?_$wC_ zK`4jqYD*Ek=K|HgasfNx>(jyCC&K6@qx?}+sxS!*gpRM!pBfkzjr*1#w|3bYIL@|V z0qE$)91X3>dnn}O1-(WR>%o> zi@03T|1@)dZ=oxpc{_hkhu$JS>9IS}Q1K1~U7mum4l&_NyTHHw=Cm3{>t8?8;gS0j z;6A~>PJ}c!qX&1u?M(=(|9DXRkp(`AvmD4?`1)^M7f-#p`S-}Z7O*ddJ^1CSd2iJ3VoOmU}E` z@pX@-XV>c+AEgFLPU&U^-dyO*Ugu_K72#Sk)Ggx5)sst`>H4woS|}asij*CTMLpbF zXXXtu^2^3&1zzu}Ml0BV>cA@(wyLboH!hC{-#X+`{z!LZZYz^a4jmCOm(8YSEN;sFs(Pv$ty5n49_oFB*kD1b?X8v=Jn4s|vhBqCBX_H_FyOc1$g`YO9@*5S^tu&#sLeb45p|JkLchuj4K5?zjVqH>I;GEd+k$Up;wQ#`7sLXK9e;UQpjUQi1vO)N%3>S7wunit(X&h8a*XL%Hv(scBbhjvj*0A|hDO#muEB-L`4oX--Z0JS^fL>gzE@Ev32w*=Gzy%=V~uZ~a(3lOOY zLKPvio4>%amERh1{_(#d{o^Xm^Fv~oz!kNzg^P`u#Y0IxlWtJCGA0=#dcMQB6rnM5 z42l*f*@%m~GJF}Ni;%v&`*eMA&Rb6!5!Lj@FeFH%2Hl^9iv!@F4y8yIDSUqcYP)f? zy}FFu*`QluA`)0VyjYT@H=pYf7P7GzU6e7n+YV?+%iaCyUpawGI9J);6oJzKM($wKM znN8Ji1h<@5lc3Qck^Xp)(nw~44M}rq1QKd9hki0Is(u9JfZ8ifFf2988O#!}bgzac zG9**OC&AikISk{WYGhFEU%TQ09w|RzTknMc^}eC7kQ$e2mjYI_gV-DA7Chz1L_7)Q zF&>s>0UCe@yfHXLeM`P2A0AL~>qM6fN+Ikpnc<~v=AysD!9-)wYy%4b>kYYeCN$EV z;r!q>Mue|6hUJ7PJ2bD1Za+;UL(sCU;# zkC2rbr6sx`?-Y{A5y=7pq9;E?YO4dODk=mf%Gn5=mjh5;zI#x>2+IMC7N#5Pj`<1j z>gQ6xj16dY)h6B$5g~gNxS-&`Zw-S1EO`L*7%+1Rs>G4i6x=6div#E97ZYC79mD(s z)X($=>PT9|kz#|gk2hg;T{!AvsOmy6!#jZP_=6Gve449e#k6Dsd3Sd4CREg{)3cmG zNudmgAQ{zcK&_yrJhsAP)tT&o2(3JVi zcM1*75N!b5PX^TmOb?xHFwXC)Fyn4RU2Kwi9$!>wYkCL&xxeV|{hW~QIvvM}&e zst3g~_hli`ISCf>Q44U(1P8P?HkPAgXNjx)Mwac;5&uG1e_HlD% z;4T9OFY%0Hy;*aX!?ldjJ7O38<*V(E#~4_&lxxQMO8HP)`nN(kFi0QUNrZi|j(zQm zVyFIxvC+~vC;PMQRdIOD{{QM|@Yrp;oW<73wvCis_qe5fDsUi&bE2Y2*#mbclAEhC z=MhKg)0x#fQMLCo4gL?}<4*;6YqsH9&SkCiqk6^}(ALYWPB8L1VJq}@HtbHS%rWi3 zL2V~|`2EyAx6u33Z-6({f8DM;9=dCW0B)J>WBkYM%Ey~)xrVGA;4WD^#(@u_liqN` zX)l{KeG)SIXpX?-X}@NyiB6SLFb|v-m5Rq(|d{a@~b5jZa{cP zwx54+*v%=<>=xw?wLn>gVbeg&sbK~8-(ot;Y8iVp7jubUI(@@X)zl0g>S<~3gW$#) zYLn1>)kb4gms{G(u-{nE94ee%D}BaxRh{GGW!H01W8K)Q;Yohh;ezw0(&NXmnlp_y z3weqh`KkSsgb?*qI-)gK1CsT*HCe z>Y>Do#VQix+O|%YhjKmmtd~EFO0=ET`)zulSueSXkaIdnB@8BOQxRsZvctL{6ucpg zR!oWm#&EjN7Jko=C+xsU(gZ%Z8N0)_cy_rM95HRd3JAt*K_qyPSi(vO# z%kx(l8#WxtBa=xg%b!q|XHF>cC1HLCt7a)Ga9 z`fp@h$1S0xkODFNB%y{1p{5LJ;5iya><@wFmKZd{5~P$}%aK2Y4rqKV?g_Z1Ht6mu zlc6xFdHzypphsO!y$^^R#hplQUbWNS$^uiNFV1XXsq)V2ff#-e@!WWWQuyia$sDojff4pMMy*_~!Bx9F}qU%8+ z1aXLGt0;ANMxxLq9Db{n>VF)`EmzE~NFHdxOfC=Cgbl@-#@I5R?5xmD=)O_RxmL=p zP*b@#^k8r#K`lYJ(rXkbPC}c7K+9*TEQ&4$NP>8w8f^I@&cG=7+n4H?&);Y#PTgoL zhEYt;`C3IVghB`hv6ME_`r8jcL;W0BQ=^lDc*0cB$`;Y8z(gdwG?*xpwPcWk71}7r zx6Zn(bCk_H7DwM7&jwvYP$+jI1H8 zOYWYL%6MI4l%c>)#y!J93cy=KflrTu?dG%z#)`S-<>kb+#a~hI!Rz~H^l@slrbIDW zLmQoEi;NPGmb7eYuW~4!(u;W(Qn04-X723=yrnRWNWETUMbZ_F>ZRPD>&Y(#6c`J@ zw-iKc?*)-40*^~_d-7@;bhD3ya7yWn8TI>Ux0Sy^8jWvKq6r_x@4s3 z+SSY!jaU_7hn zQ{FQK8^oZA4b)F9&g-=xI4k$`M4Vf#>$2`S6kuzAyP zF=NvV=#51o4{(FvIPnER2tap(DP4BiQd}67QDZ3Y9eh!)d^{>UE3Mo)OkzGxVxdhW z5?u=nry+vYRH~7h#9(j6B`gC}BvT{>mFkFTzomISN=}vH63<%+C4fOn1Tkb)Fz(f|6Ly(|+oPw7KLJbOcHl;KbrHU1r z1F=e&r$Pnoh%WSl)Xp~;$diKFd@q|-LvU(7huZuY8MdTPra&o0>Y#i1@orB&>FYl3 zSGWD=ug`}b?aBsni5pZP%%y-Q(?!IrOm62uBVCMR5}p3FfYY4g-hr^=N)UpND|m4LsXclqT+9~H zL#Q=W7+~z&@eL_KPqd z_8IjW#iE|Nc-eVn5NozEd7fjW&E@>%ZkXHtKoROscTC z0fI!h8Sx)kJ#PG}^K*42g9`W1_SO2U7Ja0q#&A#v_?j3`TY746w_nOi;7wK6cO{pQ zCnRnzXZ$92mP#^w5DAHjH)hWKknJ*qcFV6`!VL;Oqr?M~SUY%kB+r=UUOX}TQ_xuZ zyhS(V>6ODIT<~plt#64MJ{-N=ZJEgp^4}R$(T)y^GuA2ho^q`Y`G`b2Umi#C*I#+4 z2!`QIE+$S(i%osrAP#66gNuCK!zfo7Q^kAMpqo_VpzTsx7VQcT7+xb1oO*H`u9dQ; zwMrtwP;vX{udPOyabZ>-F=%;mHmq#0({igS_vE*JtDY^#az~%&3kh+<@56Az((Q+f z;;?w{(#L-PPWa;!7bkNQ8x#7E-ya*e6V37PWiiwa zj7NURRou5YV~>r!F-f-y=JLD=aRMN4 zv;{sRL2wq2;@6`RNoFVZw-6o@(k}FK2Of4E?sj_~%xS~wXAmCNB4QHfvCZPxzy?Hg&6;6Vqt?vv`L#-rQpKZcp71$kiu*;Pb0lF zLvN(Vm{ccDjsRkVtz{Fk6kcnAhNzx+T74vAGX^K{=n4XiJgN zoPKr-(|4z7$Ln;nzdm@F%v+Db(Ou`-9v*tU$Z@(^)enB|*W2NFe*Q@}yWi#%JrE?nLhW(!u^V%Nz$tR` z{&e-+Tcd;EBmMsLu%WfxGVBTF<`q>8Ki_8wdET2%%(XGQ$P2jC7i|_{{;gvV0{Sa` zm^pV?J#bUsQydXbN+62Y{-sRcs?dSJd}Fu}Y1ir^Drmxib_<`H`C2}OvALh{2<$7} zQojasOKSjy{VK684(VhHmTTywn8@ciU@R&W3yV78PXoi+;)qHWy{6|;lTpRCGq|4F zLj?BlNj2p@KIirB5{?Nz9T1ApW&K!-z?4u@q z!8Q-!JN0nFD;`LR^~xOAK^0}HMLBKdgD&K<&I;&=c%fJfrVNWnN1E=sBXSPBFdG1Y zW-ok#?~-bfNB3JM$YLtk1>LkWtszTRx6dD0f6a(OLZ<>bstj=v=te=~Io;f<*XnsT zRM9%u2z|O-ylz25IW1k(R6LYzddp^WouFw8HQ+3tFT9(2)=e9IYiF_hGK@h#APooZ zzSsK8sgu@ay{m8ZE=6I<%rVRrrRGg8`%lETiGkDJ2bBHbni2&52dN-<)D;r3?!FM> zTPOqXb#di|)@0c;<2d6=@=(S|WDe#q(+az&qM!Qo*ac=|4yjr6%9j#|S`Eo3ZMb7F zG@{(~yPQ<6^b2ym?yiF0mwyBX%%n=?mJ%vzvHT$jon{4<6&8v(5z=5`p*dQ4AyT!X z`Tpex+5$FKb&h3P0~nb;XfT<`Wyn6I>LK`ki@lqt z)1eBtXdxQ%l#{Y#$Uq2o(!F`W&&5G4dExEyW?LHN(uwVy0Yk#c;@S1St+r1CyQBqG9;r-p2NEaoMc~~lMqMF%{S4%RAsu&G9QOcZ8bj9ZHHu1 zdrn4F)opgBEM=-E7q-0jkEf`vVLc>cf^>6t(NsHCb7bIL#RUs*b7k0B1+`ABW{>RYWrcUfCg@J9`#*_)>%y$+3nvQN+MrY z3)HV?OC$(z!xzZWtUaI-GqCNqe!9AQk5@i6{_c7DuNfi!G&UuB+yYq zeiAgj{QD)(PeA=sFW}&U67U7wfA|RQqsWw?p&4DqhU#54^Y-1X6PP>II&EZ@_3NyA zvC55$yJ-lsAPNJR>2cNhrPv8b>A*1JvzOUJRC3S#>#?{q)SBCJVlH2RF;Xya6^uR2 z@xJi=KK^>Abx-#3)nkgD$SA@l`FVF00{@%S8{Ja!*@!#-_uv-pfc*};Tg4ePGu_=) zA(l4q9g-!g^UZp%sFuRr2SqH^xo#D1t5)Q+ojbfc z>zTuaKi5hPqgtu#^mNnd1Bj20kOoJkYqJTN2$k+ld*mQ6oh@s>j)3Y5({_MaRa2WS1<0Y?cXnlhT+q0#!;_cir%G=7Hn18n_`PKFE z6^1@*oR=o#W(!_L`toVpY^e29DSQ{kwcI0ZFIdK}^=zu$Kfa~A_`xwjT^{ZyJmMx70=m@*H4J;>u;RlW=8rmV3h1#O)Y319Pew z1Fb{GC0Wccz}4BMv*MyCU8%F;B8e+16Ciisunpk%trB*Gn-q@}wnlmkYj2sQSY$S6 zumfV@)Yt^S(g`oRUO_PFNJs7<@zr6jc&e9!i)nh>zjIvdkCMk+&&R36T7DOxY0CYMVtvNwi4K z`I#|@)6tOA%rPkixd}1}XpL0jN5H$KcEm?cdNkuIkn7Y_!(&gOp=4Pi5(w}QNi2@# zy-h(3NWU(Os}l{XK|%l0jt}+fLqst`8Ni$BXQ_Uhg!=mvTPtILXgISsx z0U3-Us0i)aH8Q7g3f?zK3-mM*rr}}!1G5Xx+j5+C8&W!+Kwp;B2WoTNyU{_wglx>! zN}axvvt=S&9!s#uYj%!p=uz>U)&<3k048_S4oUJ;uf=?CTth6oaMreQ2A z7ke$?oJ5Ru=@r-_dO2v0?yr#WsfC9^RXw86E?&W5%+p?{FtE`De%hb!?M^BH?c@OM zN*T9uS^89E-;K#5&NR)@SVXiUk!V?-jLZ6I|wHY$6X)Vi`YVu_77h z!-k5>bm<^iPEI7KgMGKfP7(PpHMq2gEHI~aALf2O^Av31P3H?R{f-nag!t3y8d9QK zxP*{XIMr(ZWn0wF@-fV2nLsd@{If#3q@h@b%+#gA-qOS1>P%P3au+2R`@(nK_|Gjh zxKeO~gY1r{&pWn9eKV(hsF}dl0ybo!SoTbk{qLQTyDFO%=$?xVXTcq?a*p1`eTFls z%*M$hKFV8Q9CkOX<&Uf3Vv*gTe4%)@N~giFklLSmb#_*=a5>-`Q0y}RooZpb@pI_gFi53wYFWK z)j%BfY!YuS>b>Aw3QZOz;Y)`IpnGH$OhiwqVS-(v;Jo=TIP-XXDl}H9iD{{S;%%MN ziB4l#sq*z}C)U$fjV5QP!=<=U4duQihZ8ff{9PRPfe!-HV`CL_|}-=1z*ZS6Gyi2_BS2dkY^$$&fEeqHe|i*7G; z>SuRz+%=B3Uv7Oa?({S+JrBEXOlyLg5tSV{yp-m4tI;<+ktzaLGm`Z|b=yl3CBYn1 zy>MP2yJ_WKb`N?sD&J72p~wHsSMJa9Llb^$!)~4--qeXEF|JwG4MwR3eEF>Yji>GS za(Ir&1X7YkiN<-Cg365E0rb#SZIha^y(Qjx?p5Z0tIW><-jwTgbpgMAWBYt4dW&|*?C za9n~*mK9ddz zS{sopWfP!)5tEv?9D-T|C(*KcSI3Of3V|*{3JUhJiK!7zrBYx*EjGCelS`2FN<|Qt z8+(K!3$jTmpw>jj&f1=X18~a477{MOFF_#**y0+Y9Lpn>V}8~N1Q380H^mN=fJH*o zS1xWMBfk$x?bEDx$A8J+AghjvbXCUTR(8dc7*nd|XYvFuuxc#)pck!;L}$^g0f}Qh zLksQuyy23HGtKpbIO8G5_Ho9$-VT=1Ih~_Jon}n3ga@}J?A?rAG5KKE3 z5U4O4ZDlW`L9FMtk*{;mSm6G8a z6#`tf?hx}6QO=|vsMDu}3`9nOSlwokWC5ghfvE`wq z>Bo+m2S(7-P_!Io6D$fJi#UGfrhpXkpm}#Qj6c&@RKn$5MayA6V(Kf65{Ib`qp`M(hE_84?=4!y|6zVl$qMJaNt zNg$NdONVVci6_l+7J$8}a7aQebeR6n_#iR@1xGBzs0cqW)E*KpztpMym<_Bp+1meU z?>wWL+?GBL7^-xU7JBc59;7$vf;8y{2wi%UDouI~y{mvAMWh!ILob3#3%v*eA{|6f z1a1!Z+?R85y=QsW`}HIrvhrbmE175K*?T54|Iw~ynYp)%a~OADR+A+Oq>ImBr+e?{8snlQFVgRqI=wu*h}@oBHv&$jC_gw$KAKo0TZjpFfE}vL zT633!Zef-@&lx|o^9Xrx?K@NC#)?ptznd4qVFhx#xYe|ZTXlGsl*uD|GwU?F{+-gC zFRMhSR=q2FkBJurugp4Tpp$NJxSU0FqD$Jf=- z(8zvIlG|ChTSIj{-R+%iFLLE|rbZrP2IN5nW#5RnUYA!^))&DA)LGf!WY;AW#QC3b zua~i!@Osrj{PG!at*~H|s+1k17#f%zfDTfI{w_$_G4J-4{aFW1{anM#_(~5GDKh|j zlE>@XIsbRw{tGiD<)YIPI|jwpq^nko)drRCjALymDZeoc_|zqy4M-JOP-zha%MQ1+ax4EA9_lJ*q@U5?1d`oou~R zH&x=XJ+8e_fwy?03{x^s`$i};u;zO?rRvVuYwY4QH)CH#2byIzE>Tyb(IKMkJ!29y zJ)cW&RRmRk-REp{k<6OB;}P+x&b>O+;3JGH>P6md-@6{1WzWvq6cQ+ zE~RaZd&b%D$-bWFQ|Jw9ab8lT$4pcy&Y5xYF7)aVMH|^qv=Gt2h%73#etD7*-U*XO za-@kj=fPQbMCQ=KqW10Z-63Vco>Ncfn`fpooPC}bIPyzz)v-d-dPW|uu5@Gk(Dan{ zoxgv-a&NAiDOO{M4B41Q&`HdCn>rpl?!LH;u!4Td$}s5YyMcTdXg@UDI;(IxrOGA# zyC;j>!|#g3#qKpPJPOzHj)mCqz8!u5JrQP@?XQcvBl7yZ!Q+G4+Ga6RY{246r9KJn z0#UkFhrn;@$x4KtPVJiAbi7@I8zvT-+5^*eG*q{?>Uh=#Y3j-Zv%|cd8Lwli_9twU zR5pFxcd*@yMI2sL`f_gm+27vuJrG#%V5aq;_3%hOx3$e{Ac=7{McGs5dsn%?2McAq za%-FZ5h;$Rhyrmpi4uRB!*SyaV=96$;$=~TGrQIn{Vt%5&_lQ0s5{VPA2}Z9=|j9N zTkvAGt+SXx5O;^_+IO|(wUxIe?=sDX*P%KBlU6vL)_LPUgKxE z2EWt34@{0F`DVdChN20Mq@Oi=baQPsshvJ&8u&{zF+e`E63Ij{{Bm zjFuzo=G_^l8m_skJ8KM5pQ3SO+;rs4_)1funlMJr+1r4AOzBl8g~7AykvC51d}c$p~No-U_}J&Yo62qugSh7yyrr3S1O1La-TQXGZN1bISar20eFqrqb$lTlmjVW zzfIrd;1Qqf+x~_=`;N1qfgcu_bJ78RpmUSr+k>FkzCz=jJM$Eb3-$iQ=o6P?_n?E&Vm3lUN%Qi zZlK{q$;HvpKy12Og@C@~I9q=`>tvx_B) zvoR(XqodCa!UceDt>nn8P`%#VOj1_Mw>&;h|^g^GiOP2hk{pw_+XLRc)+_gPeYGahXIaK8lBO$ zq&Y=%vfemEG9VGM@|WIB9(k>Y$KJ~$?(`hD1}mj!$ORJI6Gd(LUS?gZI70*kIb9`N z_j0yWZ7EU>8y_|88!})J`p7poVcJ#bMHu9CviLPRvx?gABwkRz!H&&4$E$VkJC}2reZfsoh zsLl~<3rA0STYKGRlcRp4ZB6Jd?$AcLkN8tvpiZ{epIkk4E~spYsNnYP4}-{2m-9f_THtm@&CKz%Qw* zQlVo(mkW__h9R8eL)f^|Vz)7}{8r|Hqv>q^)ny)?LUVcbXl3k~)#l7zRpEDmP5RnO z1BQ$cIM;i5E{rQlfkF|@nOj%2j58YUi21O{UOUf_CXPcxURmLp1>^SN!KeD4-D-hz zY&4pASch?O!ZCy=@6u4ZfM=qrtgG{XB3-^`IvOaXKW4*=tFPbqLE5EXf}A zqEAr!M5eGIs+fP*W87Co@Rq_A=7dOaM-6OEYBk3p2B5#_Q5YJptM<@9K?M$TFn;14 zLuh{@WI;1_GFFtXB-#p4zA`Lo8YAj&H+lRmXrnxK>+0R3uQY@%A%6NQfog8l_#SbG zC4%5ST3p^i0V+9DwATi@!`gW7d6j7}a=4O3AHiLng$Vk#`}Z3aco z`M0?E&fu|DNIqi9W#8(t$m{W}4uSP6@oB8Q71(}v!HJy?^8tXo4n&>=5U>6Aki^sp zNtzk$97FLPNw`HfdR9sHVz}03LQfeA@a$S*1%kW_79b(K@EYc8V9SS zDd;dvp*RfcYL6)j<0Zq*SROZ)oOfuMMSjbU(X%r_pr~%Q$%&rsWc;t9ZhC=EH)&Mw zz9do>Bu3s6zu!;;!;JXBAD@B}3c4l0uqc4Jc+Qk1KiEYY&_9b4+0dJkZmN}ict6u| zhBCChT@GMQ4yF2zK>oIb%+apGPm|J|hl6D?6r^{tvZ{3neHP{-wMjn5$z!a5jVhP%SGndb6~q9W62j%;Eu=mGGDyz{bt`xIE3ilNe2_n zEJQE7Fq9e2Hdll_NAs1U#VsCRB>9YerP)MiH0X$RI3mxDXM6uXH$|ljH4wZ08=S+6 zSb2Z7#GrZbFs;f}B}@sIM&F0jS5zYVJ(HZybo0q0^mi2#GGGRddJ{%TIDG&#qdbPl zY@i_ALAZ&@U|scdcm0j>6WXUC6uJ#L=za!_EKg;7>540Db#_PMR1^JMu?F?64>As> zHjd}9ZJRPP&1D%Vg4HTOU8nPY-cR>-e@qV;u?bjlYrqmKRDn8sqZ$X~@yf45{l7vL zopno1JFcl$SE#!#*YtwI0{L@oa(wvef=*gIcH9z;XpGWd?!rO+nRKC3h4z^uFbgqj zdKxz2AxLY3^3G!1nI8AklLYlYL>PdnY2ud;=jL5wPOhP$#V|dhCohP|=IIMyB z!W>K_I0+BhL3&(x)cnRZL)?_4U6 zc{Nlsa>w^Q?NEDVQA|2c;(P0PM!+Y|VPA#1d-$fR>)*pp0|97k(_RAnuSu&Og7W;Y$`7|OOD(Y!|^e6;k~z^J5) z#5XJoA|XdAtCi5DYnH07Ej1Q|a1zr=B!M_Z2iKOfxmI#KZO^ezv2oTCF-h|pmG_gJ zB)zHftn*DxS;$U^rRJ-OT|L$sE6y(AqV_bld2!@fSbt;W)z>VK-Imn-sbk2=y5)&^ zGC%iZv6NG=62zs`hD=skNm!Wf2UY_sri*738oyYQ=$#MeW32isB0y8(Tx{>GC!K0# zcn=;qFl>CkwrH9IRIFxIC-+q!R^0t`&&4jHJC>A0!>AV1M5IB|kEX56ap~uA;j1Qb zoO5J6NQNw&VE=bq=!y&oJ+0k!tvx+|iwyZu-iz>A+B>^jMM1P7t`>c^DmEO}t~M&x zeO4S692g@l`1!J>a7Y&fE&Qph0h;zawn~lwNVc1blG2C_V+UmT0`J@f3V|*{GrOHS zk};rrLW&>(K@hsC4a2+Sc0g(5bTEk>1HP4z3F8a1WYhEZ!GmQ(hE4eTFnkDS|GGpN znFdCm4RjIBNQ>Cj)fL2Gg>};gi;WHICIcN78aY5igU+sPO2O>BT0U+Ktyer-AJFt( z55t2CeH>(ebK73_DIv4qVz{cjeJQ;JWw3pqLYW-z-RU$VJ2jP6L5x700dY(SLl1;zNnr z1s`K%nW;Gj8X5^wgd6H&rS0zG>cNjZ?_2*?P>K|+|3`Zpc^iy_Ng)N>DIV%PlBy#5^=QB#rxpJ8d`^>()qw9tbqOv(cL?9+kftbzjwPhx+vG z%UKi1y5duPWQgzSVm>Z$lwJA$?uzD!dEVV+WvveH?F=7; zx|C$pu0C_-+AwmHb>jf9;AL{^NfwxyMm9CxCgQi6*4WCW;EWp8{Bii>``+2tZ~J6R z3y&X)Z1a+YTRGWnw5dB|*{<~ixv7*_w{Hq--WE`hMCxEL(xIc`RW$S=Khxq*}$1#yMj}**CAqMLiMp*hU&)(xN7UyyAClARta7Vmd0knqa3+HcT>%YJ2a`W8u3bTd^P$NImHKJv`f=h z2(sT;T;?Vb#j)N83fG@Dj z#t{w0H3}7sU$%9aTJ_w*{m21VU1#kBvB+CsHoTx3Pk4hVw9-fvR-3RehaEA}IaL~G zTqu$+c8xiVi!EMGw(oJcY6lNF;ZmZ^Ub3*a^end|Mt+Qj(dZ5YBEpIVp5vm_-Oj9Ct7V6XuJPL zxoR%uaEtTWkCd?X^=GrwU(ChA!nHQ`m&#{9`BxFLld^elC%#e}eR5S*$!WdGFREX+ zZOLYxiR{y7&s&6YPtmAq#zWdhs<0udv>K}b#&rx9U+7)mGxE@vb=1<7tD6BlLF~_; zt3t8jHcHxmAparx@5180e>M^NXJ19xSO2~bh5zv((ue36yvYCSem)B8KexXBy#8b` z7AoM+lX(7P_}gm-vV-Oib9qn=|2&-JkA|O+k>md!Rf5VvokMX+QbG#rpiHGeHAdC^ zzcePt|3BmZngD=GK~*unq{JZG27dntf23S!_MlQwHIFVSPnc0DKUF|N1)@p_T>^_( z{sMR@D1-_`738@DmazT>@KT-!6^JTta|tYC`wQTu*bOQWRV?NbSc%L<{P{}!!H1X9 zF{nUP>6J_13y!}4UJ9|G0#SubE`hb2sKARQbKyNy3Tl7xCB;(kr<99k6;ujppW7vc z0enHZ?0Q2rMQx3_H06-IFhy;YK?R@|8eal{3P1e@7cNDmpcbuNQm(81lyY&UQ7NdU z43`uot$+IgwXgw|fVvr763oml2q-D9pQgZm9s1b(SD+U;pu@phEu~IA3}c4b90C4eh_eXjJn*hdF;VKZE|k{P&;-)%t&5&=9*{`9>G#wO@yJ e+y4sm;sB$qj)~kK(a?a%X9qU2N!9VUcmDzzJm1y; diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index 82ecabcf5..81f6c6e12 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -50,7 +50,10 @@ Template.attachmentsGalery.helpers({ return Attachments.link(this, 'original', '/'); }, isUploaded() { - return !!this.meta.uploaded; + return !this.meta.uploading; + }, + isImage() { + return !!this.isImage; }, }); diff --git a/client/lib/utils.js b/client/lib/utils.js index d712cc731..e72f177e9 100644 --- a/client/lib/utils.js +++ b/client/lib/utils.js @@ -70,7 +70,9 @@ Utils = { streams: 'dynamic', chunkSize: 'dynamic', }; - settings.meta = {}; + settings.meta = { + uploading: true + }; if (card.isLinkedCard()) { settings.meta.boardId = Cards.findOne(card.linkedId).boardId; settings.meta.cardId = card.linkedId; diff --git a/models/attachments.js b/models/attachments.js index 1a55cb858..03999f55f 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -6,7 +6,7 @@ const collectionName = 'attachments2'; Attachments = new FilesCollection({ storagePath: storagePath(), debug: false, - allowClientCode: true, +// allowClientCode: true, collectionName: 'attachments2', onAfterUpload: onAttachmentUploaded, onBeforeRemove: onAttachmentRemoving @@ -18,7 +18,17 @@ if (Meteor.isServer) { }); // TODO: Permission related - // TODO: Add Activity update + Attachments.allow({ + insert() { + return false; + }, + update() { + return true; + }, + remove() { + return true; + } + }); Meteor.methods({ cloneAttachment(file, overrides) { @@ -63,7 +73,7 @@ function storagePath(defaultPath) { } function onAttachmentUploaded(fileRef) { - Attachments.update({_id:fileRef._id}, {$set: {"meta.uploaded": true}}); + Attachments.update({_id:fileRef._id}, {$set: {"meta.uploading": false}}); if (!fileRef.meta.source || fileRef.meta.source !== 'import') { // Add activity about adding the attachment Activities.insert({ diff --git a/server/migrations.js b/server/migrations.js index 840ab1700..887a60e40 100644 --- a/server/migrations.js +++ b/server/migrations.js @@ -80,7 +80,7 @@ Migrations.add('lowercase-board-permission', () => { Migrations.add('change-attachments-type-for-non-images', () => { const newTypeForNonImage = 'application/octet-stream'; Attachments.find().forEach(file => { - if (!file.isImage()) { + if (!file.isImage) { Attachments.update( file._id, { @@ -1058,27 +1058,29 @@ Migrations.add('change-attachment-library', () => { const path = `${store}/${file.name()}`; const fd = fs.createWriteStream(path); reader.pipe(fd); - let opts = { - fileName: file.name(), - type: file.type(), - size: file.size(), - fileId: file._id, - meta: { - userId: file.userId, - boardId: file.boardId, - cardId: file.cardId - } - }; - if (file.listId) { - opts.meta.listId = file.listId; - } - if (file.swimlaneId) { - opts.meta.swimlaneId = file.swimlaneId; - } - Attachments.addFile(path, opts, (err, fileRef) => { - if (err) { - console.log('error when migrating ', fileRef.name, err); + reader.on('end', () => { + let opts = { + fileName: file.name(), + type: file.type(), + size: file.size(), + fileId: file._id, + meta: { + userId: file.userId, + boardId: file.boardId, + cardId: file.cardId + } + }; + if (file.listId) { + opts.meta.listId = file.listId; } + if (file.swimlaneId) { + opts.meta.swimlaneId = file.swimlaneId; + } + Attachments.addFile(path, opts, (err, fileRef) => { + if (err) { + console.log('error when migrating', file.name(), err); + } + }); }); }); }); From 4156073b1a5d18ec70a00571ffccdd504b99098a Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Fri, 22 May 2020 14:53:10 +0800 Subject: [PATCH 19/21] Fix translation error --- i18n/zh-TW.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/zh-TW.i18n.json b/i18n/zh-TW.i18n.json index ac923fef1..c4b155dc7 100644 --- a/i18n/zh-TW.i18n.json +++ b/i18n/zh-TW.i18n.json @@ -747,7 +747,7 @@ "error-ldap-login": "嘗試登入時出現錯誤", "display-authentication-method": "顯示認證方式", "default-authentication-method": "預設認證方式", - "duplicate-board": "重複的看板", + "duplicate-board": "複製看板", "people-number": "人數是:", "swimlaneDeletePopup-title": "是否刪除泳道?", "swimlane-delete-pop": "所有動作將從活動來源中刪除,您將無法恢復泳道。此操作無法還原。", From 921460db4031134db863e32101c0ad60a17416b5 Mon Sep 17 00:00:00 2001 From: Romulus Urakagi Tsai Date: Fri, 22 May 2020 14:59:56 +0800 Subject: [PATCH 20/21] Fix export attachments (not tested) --- models/export.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/models/export.js b/models/export.js index 35e558048..1eb47e54c 100644 --- a/models/export.js +++ b/models/export.js @@ -146,7 +146,7 @@ export class Exporter { `tmpexport${process.pid}${Math.random()}`, ); const tmpWriteable = fs.createWriteStream(tmpFile); - const readStream = doc.createReadStream(); + const readStream = fs.createReadStream(doc.path); readStream.on('data', function(chunk) { buffer = Buffer.concat([buffer, chunk]); }); @@ -173,11 +173,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, }; }); From 863f0fc5db0bfbd20ea1d5cf388f54e9b866fbcc Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Sun, 24 May 2020 03:46:20 +0300 Subject: [PATCH 21/21] v4.04 --- CHANGELOG.md | 2 +- Stackerfile.yml | 2 +- package-lock.json | 2 +- package.json | 2 +- public/api/wekan.html | 656 +++++++++++++++++++++-------------------- public/api/wekan.yml | 8 +- sandstorm-pkgdef.capnp | 4 +- 7 files changed, 343 insertions(+), 333 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86f9726cb..f1db307a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Upcoming Wekan release +# v4.04 2020-05-24 Wekan release This release adds the following features: diff --git a/Stackerfile.yml b/Stackerfile.yml index 48086cc51..b1cf282c7 100644 --- a/Stackerfile.yml +++ b/Stackerfile.yml @@ -1,5 +1,5 @@ appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928 -appVersion: "v4.03.0" +appVersion: "v4.04.0" files: userUploads: - README.md diff --git a/package-lock.json b/package-lock.json index c9f7cf56a..dc359f423 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "wekan", - "version": "v4.03.0", + "version": "v4.04.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b049a12d2..11155c459 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wekan", - "version": "v4.03.0", + "version": "v4.04.0", "description": "Open-Source kanban", "private": true, "scripts": { diff --git a/public/api/wekan.html b/public/api/wekan.html index 39f2f6fcd..485e6e8b3 100644 --- a/public/api/wekan.html +++ b/public/api/wekan.html @@ -1458,12 +1458,12 @@ Darkula color scheme from the JetBrains family of IDEs opacity: 0.5; } - + - + @@ -1477,552 +1477,552 @@ var n=this.pipeline.run(e.tokenizer(t)),r=new e.Vector,i=[],o=this._fields.reduc
- +
- - + + Shell - - - + + + HTTP - - - + + + JavaScript - - - + + + Node.js - - - + + + Ruby - - - + + + Python - - - + + + Java - - - + + + Go - - + +
- - + +
    - +
    - + - +
    -

    Wekan REST API v4.03

    +

    Wekan REST API v4.04

    Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu.

    @@ -2160,7 +2160,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Content-Type": []string{"application/x-www-form-urlencoded"}, "Accept": []string{"*/*"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -2435,7 +2435,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Content-Type": []string{"application/x-www-form-urlencoded"}, "Accept": []string{"*/*"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -2718,7 +2718,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -2924,7 +2924,7 @@ System.out.println(response.toString()); "Content-Type": []string{"multipart/form-data"}, "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -3219,7 +3219,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -3444,7 +3444,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -3505,8 +3505,8 @@ System.out.println(response.toString()); To perform this operation, you must be authenticated by means of one of the following methods: UserSecurity -

    exportJson

    -

    +

    export

    +

    Code samples

    @@ -3606,7 +3606,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -3620,12 +3620,12 @@ System.out.println(response.toString());

    GET /api/boards/{board}/export

    -

    This route is used to export the board to a json file format.

    +

    This route is used to export the board.

    If user is already logged-in, pass loginToken as param "authToken": '/api/boards/:boardId/export?authToken=:token'

    See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/ for detailed explanations

    -

    Parameters

    +

    Parameters

    @@ -3648,7 +3648,7 @@ for detailed explanations

    Detailed descriptions

    board: the ID of the board we are exporting

    -

    Responses

    +

    Responses

    @@ -3789,7 +3789,7 @@ System.out.println(response.toString()); "Content-Type": []string{"multipart/form-data"}, "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -3994,7 +3994,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Content-Type": []string{"multipart/form-data"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -4216,7 +4216,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -4421,7 +4421,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -4598,7 +4598,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Content-Type": []string{"multipart/form-data"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -4792,7 +4792,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -4965,7 +4965,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -5139,7 +5139,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -5330,7 +5330,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Content-Type": []string{"multipart/form-data"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -5538,7 +5538,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -5727,7 +5727,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -5958,7 +5958,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Content-Type": []string{"multipart/form-data"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -6152,7 +6152,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -6325,7 +6325,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -6507,7 +6507,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -6740,7 +6740,7 @@ System.out.println(response.toString()); "Content-Type": []string{"multipart/form-data"}, "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -7000,7 +7000,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -7166,7 +7166,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -7341,7 +7341,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -7635,7 +7635,7 @@ System.out.println(response.toString()); "Content-Type": []string{"multipart/form-data"}, "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -7857,7 +7857,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -8069,7 +8069,7 @@ System.out.println(response.toString()); "Content-Type": []string{"multipart/form-data"}, "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -8331,7 +8331,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -8533,7 +8533,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -8709,7 +8709,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Content-Type": []string{"multipart/form-data"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -8904,7 +8904,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -9125,7 +9125,7 @@ System.out.println(response.toString()); "Content-Type": []string{"multipart/form-data"}, "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -9347,7 +9347,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -9550,7 +9550,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -9763,7 +9763,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -10005,7 +10005,7 @@ System.out.println(response.toString()); "Content-Type": []string{"multipart/form-data"}, "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -10267,7 +10267,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -10468,7 +10468,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Content-Type": []string{"multipart/form-data"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -10805,7 +10805,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -10978,7 +10978,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -11165,7 +11165,7 @@ System.out.println(response.toString()); "Content-Type": []string{"multipart/form-data"}, "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -11432,7 +11432,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Content-Type": []string{"multipart/form-data"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -11626,7 +11626,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -11827,7 +11827,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -12028,7 +12028,7 @@ System.out.println(response.toString()); "Content-Type": []string{"multipart/form-data"}, "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -12258,7 +12258,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -12492,7 +12492,7 @@ System.out.println(response.toString()); "Content-Type": []string{"multipart/form-data"}, "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -12729,7 +12729,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -12933,7 +12933,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -13154,7 +13154,7 @@ System.out.println(response.toString()); "Content-Type": []string{"multipart/form-data"}, "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -13376,7 +13376,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Accept": []string{"application/json"}, "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -13564,7 +13564,7 @@ System.out.println(response.toString()); headers := map[string][]string{ "Authorization": []string{"API_KEY"}, - + } data := bytes.NewBuffer([]byte{jsonReq}) @@ -14048,6 +14048,14 @@ UserSecurity + + + + + + + + @@ -16141,43 +16149,43 @@ UserSecurity
    - +
    - - + + Shell - - - + + + HTTP - - - + + + JavaScript - - - + + + Node.js - - - + + + Ruby - - - + + + Python - - - + + + Java - - - + + + Go - - + +
    - +
    diff --git a/public/api/wekan.yml b/public/api/wekan.yml index fee1b9fbd..f0b49c945 100644 --- a/public/api/wekan.yml +++ b/public/api/wekan.yml @@ -1,7 +1,7 @@ swagger: '2.0' info: title: Wekan REST API - version: v4.03 + version: v4.04 description: | The REST API allows you to control and extend Wekan with ease. @@ -797,8 +797,8 @@ paths: 200 response /api/boards/{board}/export: get: - operationId: exportJson - summary: This route is used to export the board to a json file format. + operationId: export + summary: This route is used to export the board. description: | If user is already logged-in, pass loginToken as param "authToken": '/api/boards/:boardId/export?authToken=:token' @@ -2079,6 +2079,8 @@ definitions: - relax - corteza - clearblue + - natural + - modern description: description: | The description of the board diff --git a/sandstorm-pkgdef.capnp b/sandstorm-pkgdef.capnp index 9f9ae370e..a902cb474 100644 --- a/sandstorm-pkgdef.capnp +++ b/sandstorm-pkgdef.capnp @@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = ( appTitle = (defaultText = "Wekan"), # The name of the app as it is displayed to the user. - appVersion = 403, + appVersion = 404, # Increment this for every release. - appMarketingVersion = (defaultText = "4.03.0~2020-05-16"), + appMarketingVersion = (defaultText = "4.04.0~2020-05-24"), # Human-readable presentation of the app version. minUpgradableAppVersion = 0,
    clearblue
    colornatural
    colormodern
    presentParentTask prefix-with-full-path