mirror of
https://github.com/wekan/wekan.git
synced 2025-09-22 01:50:48 +02:00
Merge branch 'feature-ostrio-files' of https://github.com/majus/wekan
This commit is contained in:
commit
d00596f88a
42 changed files with 786 additions and 1577 deletions
|
@ -1,4 +1,4 @@
|
||||||
FROM quay.io/wekan/ubuntu:groovy-20210115
|
FROM ubuntu:rolling
|
||||||
LABEL maintainer="sgr"
|
LABEL maintainer="sgr"
|
||||||
|
|
||||||
ENV BUILD_DEPS="gnupg gosu libarchive-tools wget curl bzip2 g++ build-essential python git ca-certificates iproute2"
|
ENV BUILD_DEPS="gnupg gosu libarchive-tools wget curl bzip2 g++ build-essential python git ca-certificates iproute2"
|
||||||
|
|
|
@ -33,10 +33,12 @@ services:
|
||||||
- WITH_API=true
|
- WITH_API=true
|
||||||
- RICHER_CARD_COMMENT_EDITOR=true
|
- RICHER_CARD_COMMENT_EDITOR=true
|
||||||
- BROWSER_POLICY_ENABLED=true
|
- BROWSER_POLICY_ENABLED=true
|
||||||
|
- WRITABLE_PATH=/data
|
||||||
depends_on:
|
depends_on:
|
||||||
- wekandb-dev
|
- wekandb-dev
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
- ./volumes/data:/data
|
||||||
- ../client:/home/wekan/app/client
|
- ../client:/home/wekan/app/client
|
||||||
- ../models:/home/wekan/app/models
|
- ../models:/home/wekan/app/models
|
||||||
- ../config:/home/wekan/app/config
|
- ../config:/home/wekan/app/config
|
||||||
|
|
|
@ -17,7 +17,7 @@ es5-shim@4.8.0
|
||||||
|
|
||||||
# Collections
|
# Collections
|
||||||
aldeed:collection2
|
aldeed:collection2
|
||||||
wekan-cfs-standard-packages
|
cfs:standard-packages
|
||||||
cottz:publish-relations
|
cottz:publish-relations
|
||||||
dburles:collection-helpers
|
dburles:collection-helpers
|
||||||
idmontie:migrations
|
idmontie:migrations
|
||||||
|
@ -73,8 +73,8 @@ email@2.0.0
|
||||||
horka:swipebox
|
horka:swipebox
|
||||||
dynamic-import@0.6.0
|
dynamic-import@0.6.0
|
||||||
|
|
||||||
accounts-password@1.7.0
|
accounts-password@1.6.2
|
||||||
wekan-cfs-gridfs
|
cfs:gridfs
|
||||||
rzymek:fullcalendar
|
rzymek:fullcalendar
|
||||||
momentjs:moment@2.22.2
|
momentjs:moment@2.22.2
|
||||||
browser-policy-framing@1.1.0
|
browser-policy-framing@1.1.0
|
||||||
|
@ -89,7 +89,10 @@ meteorhacks:aggregate@1.3.0
|
||||||
wekan-markdown
|
wekan-markdown
|
||||||
konecty:mongo-counter
|
konecty:mongo-counter
|
||||||
percolate:synced-cron
|
percolate:synced-cron
|
||||||
wekan-cfs-filesystem
|
cfs:filesystem
|
||||||
|
ostrio:cookies
|
||||||
|
ostrio:files@2.0.1
|
||||||
|
tmeasday:check-npm-versions
|
||||||
steffo:meteor-accounts-saml
|
steffo:meteor-accounts-saml
|
||||||
rajit:bootstrap3-datepicker-fi
|
rajit:bootstrap3-datepicker-fi
|
||||||
rajit:bootstrap3-datepicker-ar
|
rajit:bootstrap3-datepicker-ar
|
||||||
|
|
|
@ -23,7 +23,24 @@ browser-policy-framing@1.1.0
|
||||||
caching-compiler@1.2.2
|
caching-compiler@1.2.2
|
||||||
caching-html-compiler@1.2.0
|
caching-html-compiler@1.2.0
|
||||||
callback-hook@1.3.0
|
callback-hook@1.3.0
|
||||||
|
cfs:access-point@0.1.49
|
||||||
|
cfs:base-package@0.0.30
|
||||||
|
cfs:collection@0.5.5
|
||||||
|
cfs:collection-filters@0.2.4
|
||||||
|
cfs:data-man@0.0.6
|
||||||
|
cfs:file@0.1.17
|
||||||
|
cfs:filesystem@0.1.2
|
||||||
|
cfs:gridfs@0.0.34
|
||||||
cfs:http-methods@0.0.32
|
cfs:http-methods@0.0.32
|
||||||
|
cfs:http-publish@0.0.13
|
||||||
|
cfs:power-queue@0.9.11
|
||||||
|
cfs:reactive-list@0.0.9
|
||||||
|
cfs:reactive-property@0.0.4
|
||||||
|
cfs:standard-packages@0.5.10
|
||||||
|
cfs:storage-adapter@0.2.4
|
||||||
|
cfs:tempstore@0.1.6
|
||||||
|
cfs:upload-http@0.0.20
|
||||||
|
cfs:worker@0.1.5
|
||||||
check@1.3.1
|
check@1.3.1
|
||||||
chuangbo:cookie@1.1.0
|
chuangbo:cookie@1.1.0
|
||||||
coagmano:stylus@1.1.0
|
coagmano:stylus@1.1.0
|
||||||
|
@ -117,6 +134,8 @@ oauth2@1.3.0
|
||||||
observe-sequence@1.0.16
|
observe-sequence@1.0.16
|
||||||
ongoworks:speakingurl@1.1.0
|
ongoworks:speakingurl@1.1.0
|
||||||
ordered-dict@1.1.0
|
ordered-dict@1.1.0
|
||||||
|
ostrio:cookies@2.7.0
|
||||||
|
ostrio:files@2.0.1
|
||||||
pascoual:pdfkit@1.0.7
|
pascoual:pdfkit@1.0.7
|
||||||
peerlibrary:assert@0.3.0
|
peerlibrary:assert@0.3.0
|
||||||
peerlibrary:base-component@0.16.0
|
peerlibrary:base-component@0.16.0
|
||||||
|
@ -211,8 +230,10 @@ templating@1.4.0
|
||||||
templating-compiler@1.4.1
|
templating-compiler@1.4.1
|
||||||
templating-runtime@1.4.0
|
templating-runtime@1.4.0
|
||||||
templating-tools@1.2.0
|
templating-tools@1.2.0
|
||||||
|
tmeasday:check-npm-versions@1.0.2
|
||||||
tracker@1.2.0
|
tracker@1.2.0
|
||||||
twbs:bootstrap@3.3.6
|
twbs:bootstrap@3.3.6
|
||||||
|
typescript@4.2.2
|
||||||
ui@1.0.13
|
ui@1.0.13
|
||||||
underscore@1.0.10
|
underscore@1.0.10
|
||||||
url@1.3.2
|
url@1.3.2
|
||||||
|
@ -224,24 +245,6 @@ webapp-hashing@1.1.0
|
||||||
wekan-accounts-cas@0.1.0
|
wekan-accounts-cas@0.1.0
|
||||||
wekan-accounts-lockout@1.0.0
|
wekan-accounts-lockout@1.0.0
|
||||||
wekan-accounts-oidc@1.0.10
|
wekan-accounts-oidc@1.0.10
|
||||||
wekan-cfs-access-point@0.1.50
|
|
||||||
wekan-cfs-base-package@0.0.30
|
|
||||||
wekan-cfs-collection@0.5.5
|
|
||||||
wekan-cfs-collection-filters@0.2.4
|
|
||||||
wekan-cfs-data-man@0.0.6
|
|
||||||
wekan-cfs-file@0.1.17
|
|
||||||
wekan-cfs-filesystem@0.1.2
|
|
||||||
wekan-cfs-gridfs@0.0.34
|
|
||||||
wekan-cfs-http-methods@0.0.32
|
|
||||||
wekan-cfs-http-publish@0.0.13
|
|
||||||
wekan-cfs-power-queue@0.9.11
|
|
||||||
wekan-cfs-reactive-list@0.0.9
|
|
||||||
wekan-cfs-reactive-property@0.0.4
|
|
||||||
wekan-cfs-standard-packages@0.5.10
|
|
||||||
wekan-cfs-storage-adapter@0.2.4
|
|
||||||
wekan-cfs-tempstore@0.1.6
|
|
||||||
wekan-cfs-upload-http@0.0.21
|
|
||||||
wekan-cfs-worker@0.1.5
|
|
||||||
wekan-ldap@0.0.2
|
wekan-ldap@0.0.2
|
||||||
wekan-markdown@1.0.9
|
wekan-markdown@1.0.9
|
||||||
wekan-oidc@1.0.12
|
wekan-oidc@1.0.12
|
||||||
|
|
|
@ -31,7 +31,6 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
|
||||||
ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90 \
|
ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90 \
|
||||||
RICHER_CARD_COMMENT_EDITOR=false \
|
RICHER_CARD_COMMENT_EDITOR=false \
|
||||||
CARD_OPENED_WEBHOOK_ENABLED=false \
|
CARD_OPENED_WEBHOOK_ENABLED=false \
|
||||||
ATTACHMENTS_STORE_PATH="" \
|
|
||||||
MAX_IMAGE_PIXEL="" \
|
MAX_IMAGE_PIXEL="" \
|
||||||
IMAGE_COMPRESS_RATIO="" \
|
IMAGE_COMPRESS_RATIO="" \
|
||||||
NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE="" \
|
NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE="" \
|
||||||
|
@ -290,9 +289,7 @@ RUN \
|
||||||
chmod u+w *.json && \
|
chmod u+w *.json && \
|
||||||
gosu wekan:wekan npm install && \
|
gosu wekan:wekan npm install && \
|
||||||
gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build && \
|
gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build && \
|
||||||
#cp /home/wekan/app/fix-download-unicode/cfs_access-point.txt /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
|
|
||||||
#rm /home/wekan/app_build/bundle/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs && \
|
#rm /home/wekan/app_build/bundle/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs && \
|
||||||
#chown wekan /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
|
|
||||||
#Removed binary version of bcrypt because of security vulnerability that is not fixed yet.
|
#Removed binary version of bcrypt because of security vulnerability that is not fixed yet.
|
||||||
#https://github.com/wekan/wekan/commit/4b2010213907c61b0e0482ab55abb06f6a668eac
|
#https://github.com/wekan/wekan/commit/4b2010213907c61b0e0482ab55abb06f6a668eac
|
||||||
#https://github.com/wekan/wekan/commit/7eeabf14be3c63fae2226e561ef8a0c1390c8d3c
|
#https://github.com/wekan/wekan/commit/7eeabf14be3c63fae2226e561ef8a0c1390c8d3c
|
||||||
|
|
|
@ -196,14 +196,14 @@ BlazeComponent.extendComponent({
|
||||||
// trying to display url before file is stored generates js errors
|
// trying to display url before file is stored generates js errors
|
||||||
return (
|
return (
|
||||||
(attachment &&
|
(attachment &&
|
||||||
attachment.url({ download: true }) &&
|
attachment.path &&
|
||||||
Blaze.toHTML(
|
Blaze.toHTML(
|
||||||
HTML.A(
|
HTML.A(
|
||||||
{
|
{
|
||||||
href: attachment.url({ download: true }),
|
href: `${attachment.link()}?download=true`,
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
},
|
},
|
||||||
DOMPurify.sanitize(attachment.name()),
|
DOMPurify.sanitize(attachment.name),
|
||||||
),
|
),
|
||||||
)) ||
|
)) ||
|
||||||
DOMPurify.sanitize(this.currentData().activity.attachmentName)
|
DOMPurify.sanitize(this.currentData().activity.attachmentName)
|
||||||
|
|
|
@ -11,9 +11,6 @@ template(name="previewClipboardImagePopup")
|
||||||
img.preview-clipboard-image()
|
img.preview-clipboard-image()
|
||||||
button.primary.js-upload-pasted-image {{_ 'upload'}}
|
button.primary.js-upload-pasted-image {{_ 'upload'}}
|
||||||
|
|
||||||
template(name="previewAttachedImagePopup")
|
|
||||||
img.preview-large-image.js-large-image-clicked(src="{{url}}")
|
|
||||||
|
|
||||||
template(name="attachmentDeletePopup")
|
template(name="attachmentDeletePopup")
|
||||||
p {{_ "attachment-delete-pop"}}
|
p {{_ "attachment-delete-pop"}}
|
||||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||||
|
@ -22,31 +19,31 @@ template(name="attachmentsGalery")
|
||||||
.attachments-galery
|
.attachments-galery
|
||||||
each attachments
|
each attachments
|
||||||
.attachment-item
|
.attachment-item
|
||||||
a.attachment-thumbnail.swipebox(href="{{url}}" title="{{name}}")
|
a.attachment-thumbnail.swipebox(href="{{link}}" title="{{name}}")
|
||||||
if isUploaded
|
if isUploaded
|
||||||
if isImage
|
if isImage
|
||||||
img.attachment-thumbnail-img(src="{{url}}")
|
img.attachment-thumbnail-img(src="{{link}}")
|
||||||
else if($eq extension 'mp3')
|
else if($eq extension 'mp3')
|
||||||
video(width="100%" height="100%" controls="true")
|
video(width="100%" height="100%" controls="true")
|
||||||
source(src="{{url}}" type="audio/mpeg")
|
source(src="{{link}}" type="audio/mpeg")
|
||||||
else if($eq extension 'ogg')
|
else if($eq extension 'ogg')
|
||||||
video(width="100%" height="100%" controls="true")
|
video(width="100%" height="100%" controls="true")
|
||||||
source(src="{{url}}" type="video/ogg")
|
source(src="{{link}}" type="video/ogg")
|
||||||
else if($eq extension 'webm')
|
else if($eq extension 'webm')
|
||||||
video(width="100%" height="100%" controls="true")
|
video(width="100%" height="100%" controls="true")
|
||||||
source(src="{{url}}" type="video/webm")
|
source(src="{{link}}" type="video/webm")
|
||||||
else if($eq extension 'mp4')
|
else if($eq extension 'mp4')
|
||||||
video(width="100%" height="100%" controls="true")
|
video(width="100%" height="100%" controls="true")
|
||||||
source(src="{{url}}" type="video/mp4")
|
source(src="{{link}}" type="video/mp4")
|
||||||
else
|
else
|
||||||
span.attachment-thumbnail-ext= extension
|
span.attachment-thumbnail-ext= extension
|
||||||
else
|
else
|
||||||
+spinner
|
span.attachment-thumbnail-ext= extension
|
||||||
p.attachment-details
|
p.attachment-details
|
||||||
= name
|
= name
|
||||||
span.file-size ({{fileSize size}} KB)
|
span.file-size ({{fileSize size}} KB)
|
||||||
span.attachment-details-actions
|
span.attachment-details-actions
|
||||||
a.js-download(href="{{url download=true}}")
|
a.js-download(href="{{link}}?download=true", download="{{name}}")
|
||||||
i.fa.fa-download
|
i.fa.fa-download
|
||||||
| {{_ 'download'}}
|
| {{_ 'download'}}
|
||||||
if currentUser.isBoardMember
|
if currentUser.isBoardMember
|
||||||
|
|
|
@ -13,35 +13,10 @@ Template.attachmentsGalery.events({
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
},
|
},
|
||||||
'click .js-add-cover'() {
|
'click .js-add-cover'() {
|
||||||
Cards.findOne(this.cardId).setCover(this._id);
|
Cards.findOne(this.meta.cardId).setCover(this._id);
|
||||||
},
|
},
|
||||||
'click .js-remove-cover'() {
|
'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);
|
|
||||||
// when multiple thumbnails, if click one then another very fast,
|
|
||||||
// we might get a wrong width from previous img.
|
|
||||||
// when popup reused, onRendered() won't be called, so we cannot get there.
|
|
||||||
// here make sure to get correct size when this img fully loaded.
|
|
||||||
const img = $('img.preview-large-image')[0];
|
|
||||||
if (!img) return;
|
|
||||||
const rePosPopup = () => {
|
|
||||||
const w = img.width;
|
|
||||||
const h = img.height;
|
|
||||||
// if the image is too large, we resize & center the popup.
|
|
||||||
if (w > 300) {
|
|
||||||
$('div.pop-over').css({
|
|
||||||
width: w + 20,
|
|
||||||
position: 'absolute',
|
|
||||||
left: (window.innerWidth - w) / 2,
|
|
||||||
top: (window.innerHeight - h) / 2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const url = $(event.currentTarget).attr('src');
|
|
||||||
if (img.src === url && img.complete) rePosPopup();
|
|
||||||
else img.onload = rePosPopup;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -54,60 +29,31 @@ Template.attachmentsGalery.helpers({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Template.previewAttachedImagePopup.events({
|
|
||||||
'click .js-large-image-clicked'() {
|
|
||||||
Popup.back();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Template.cardAttachmentsPopup.events({
|
Template.cardAttachmentsPopup.events({
|
||||||
'change .js-attach-file'(event) {
|
'change .js-attach-file'(event) {
|
||||||
const card = this;
|
const card = this;
|
||||||
const processFile = f => {
|
if (event.currentTarget.files && event.currentTarget.files[0]) {
|
||||||
Utils.processUploadedAttachment(card, f, attachment => {
|
const uploader = Attachments.insert(
|
||||||
if (attachment && attachment._id && attachment.isImage()) {
|
{
|
||||||
card.setCover(attachment._id);
|
file: event.currentTarget.files[0],
|
||||||
|
meta: Utils.getCommonAttachmentMetaFrom(card),
|
||||||
|
chunkSize: 'dynamic',
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
uploader.on('uploaded', (error, fileRef) => {
|
||||||
|
if (!error) {
|
||||||
|
if (fileRef.isImage) {
|
||||||
|
card.setCover(fileRef._id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
uploader.on('end', (error, fileRef) => {
|
||||||
Popup.back();
|
Popup.back();
|
||||||
});
|
});
|
||||||
};
|
uploader.start();
|
||||||
|
|
||||||
FS.Utility.eachFile(event, f => {
|
|
||||||
if (
|
|
||||||
MAX_IMAGE_PIXEL > 0 &&
|
|
||||||
typeof f.type === 'string' &&
|
|
||||||
f.type.match(/^image/)
|
|
||||||
) {
|
|
||||||
// is image
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = function(e) {
|
|
||||||
const dataurl = e && e.target && e.target.result;
|
|
||||||
if (dataurl !== undefined) {
|
|
||||||
Utils.shrinkImage({
|
|
||||||
dataurl,
|
|
||||||
maxSize: MAX_IMAGE_PIXEL,
|
|
||||||
ratio: COMPRESS_RATIO,
|
|
||||||
toBlob: true,
|
|
||||||
callback(blob) {
|
|
||||||
if (blob === false) {
|
|
||||||
processFile(f);
|
|
||||||
} else {
|
|
||||||
blob.name = f.name;
|
|
||||||
processFile(blob);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// couldn't process it let other function handle it?
|
|
||||||
processFile(f);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(f);
|
|
||||||
} else {
|
|
||||||
processFile(f);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
'click .js-computer-upload'(event, templateInstance) {
|
'click .js-computer-upload'(event, templateInstance) {
|
||||||
templateInstance.find('.js-attach-file').click();
|
templateInstance.find('.js-attach-file').click();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -154,30 +100,32 @@ Template.previewClipboardImagePopup.onRendered(() => {
|
||||||
|
|
||||||
Template.previewClipboardImagePopup.events({
|
Template.previewClipboardImagePopup.events({
|
||||||
'click .js-upload-pasted-image'() {
|
'click .js-upload-pasted-image'() {
|
||||||
const results = pastedResults;
|
|
||||||
if (results && results.file) {
|
|
||||||
window.oPasted = pastedResults;
|
|
||||||
const card = this;
|
const card = this;
|
||||||
const file = new FS.File(results.file);
|
if (pastedResults && pastedResults.file) {
|
||||||
if (!results.name) {
|
const file = pastedResults.file;
|
||||||
// if no filename, it's from clipboard. then we give it a name, with ext name from MIME type
|
window.oPasted = pastedResults;
|
||||||
if (typeof results.file.type === 'string') {
|
const uploader = Attachments.insert(
|
||||||
file.name(results.file.type.replace('image/', 'clipboard.'));
|
{
|
||||||
|
file,
|
||||||
|
meta: Utils.getCommonAttachmentMetaFrom(card),
|
||||||
|
fileName: file.name || file.type.replace('image/', 'clipboard.'),
|
||||||
|
chunkSize: 'dynamic',
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
uploader.on('uploaded', (error, fileRef) => {
|
||||||
|
if (!error) {
|
||||||
|
if (fileRef.isImage) {
|
||||||
|
card.setCover(fileRef._id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file.updatedAt(new Date());
|
});
|
||||||
file.boardId = card.boardId;
|
uploader.on('end', (error, fileRef) => {
|
||||||
file.cardId = card._id;
|
|
||||||
file.userId = Meteor.userId();
|
|
||||||
const attachment = Attachments.insert(file);
|
|
||||||
|
|
||||||
if (attachment && attachment._id && attachment.isImage()) {
|
|
||||||
card.setCover(attachment._id);
|
|
||||||
}
|
|
||||||
|
|
||||||
pastedResults = null;
|
pastedResults = null;
|
||||||
$(document.body).pasteImageReader(() => {});
|
$(document.body).pasteImageReader(() => {});
|
||||||
Popup.back();
|
Popup.back();
|
||||||
|
});
|
||||||
|
uploader.start();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -51,11 +51,6 @@
|
||||||
display: block
|
display: block
|
||||||
box-shadow: 0 1px 2px rgba(0,0,0,.2)
|
box-shadow: 0 1px 2px rgba(0,0,0,.2)
|
||||||
|
|
||||||
.preview-large-image
|
|
||||||
max-width: 1000px
|
|
||||||
display: block
|
|
||||||
box-shadow: 0 1px 2px rgba(0,0,0,.2)
|
|
||||||
|
|
||||||
.preview-clipboard-image
|
.preview-clipboard-image
|
||||||
width: 280px
|
width: 280px
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
|
@ -7,7 +7,7 @@ template(name="minicard")
|
||||||
.handle
|
.handle
|
||||||
.fa.fa-arrows
|
.fa.fa-arrows
|
||||||
if cover
|
if cover
|
||||||
.minicard-cover(style="background-image: url('{{cover.url}}');")
|
.minicard-cover(style="background-image: url('{{cover.link 'original' '/'}}?dummyReloadAfterSessionEstablished={{sess}}');")
|
||||||
if labels
|
if labels
|
||||||
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
|
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
|
||||||
each labels
|
each labels
|
||||||
|
|
|
@ -114,6 +114,12 @@ Template.minicard.helpers({
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// XXX resolve this nasty hack for https://github.com/veliovgroup/Meteor-Files/issues/763
|
||||||
|
sess() {
|
||||||
|
return Meteor.connection && Meteor.connection._lastSessionId
|
||||||
|
? Meteor.connection._lastSessionId
|
||||||
|
: null;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
BlazeComponent.extendComponent({
|
BlazeComponent.extendComponent({
|
||||||
|
|
|
@ -153,7 +153,6 @@ BlazeComponent.extendComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onImageUpload(files) {
|
onImageUpload(files) {
|
||||||
const $summernote = getSummernote(this);
|
const $summernote = getSummernote(this);
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
|
@ -161,46 +160,26 @@ BlazeComponent.extendComponent({
|
||||||
const currentCard = Utils.getCurrentCard();
|
const currentCard = Utils.getCurrentCard();
|
||||||
const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
|
const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
|
||||||
const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
|
const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
|
||||||
const insertImage = src => {
|
const processUpload = function(file) {
|
||||||
// process all image upload types to the description/comment window
|
const uploader = Attachments.insert(
|
||||||
|
{
|
||||||
|
file,
|
||||||
|
meta: Utils.getCommonAttachmentMetaFrom(card),
|
||||||
|
chunkSize: 'dynamic',
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
uploader.on('uploaded', (error, fileRef) => {
|
||||||
|
if (!error) {
|
||||||
|
if (fileRef.isImage) {
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.src = src;
|
img.src = fileRef.link();
|
||||||
img.setAttribute('width', '100%');
|
img.setAttribute('width', '100%');
|
||||||
$summernote.summernote('insertNode', img);
|
$summernote.summernote('insertNode', img);
|
||||||
};
|
|
||||||
const processData = function(fileObj) {
|
|
||||||
Utils.processUploadedAttachment(
|
|
||||||
currentCard,
|
|
||||||
fileObj,
|
|
||||||
attachment => {
|
|
||||||
if (
|
|
||||||
attachment &&
|
|
||||||
attachment._id &&
|
|
||||||
attachment.isImage()
|
|
||||||
) {
|
|
||||||
attachment.one('uploaded', function() {
|
|
||||||
const maxTry = 3;
|
|
||||||
const checkItvl = 500;
|
|
||||||
let retry = 0;
|
|
||||||
const checkUrl = function() {
|
|
||||||
// even though uploaded event fired, attachment.url() is still null somehow //TODO
|
|
||||||
const url = attachment.url();
|
|
||||||
if (url) {
|
|
||||||
insertImage(
|
|
||||||
`${location.protocol}//${location.host}${url}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
retry++;
|
|
||||||
if (retry < maxTry) {
|
|
||||||
setTimeout(checkUrl, checkItvl);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
checkUrl();
|
|
||||||
});
|
});
|
||||||
}
|
uploader.start();
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
if (MAX_IMAGE_PIXEL) {
|
if (MAX_IMAGE_PIXEL) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
@ -216,7 +195,7 @@ BlazeComponent.extendComponent({
|
||||||
callback(blob) {
|
callback(blob) {
|
||||||
if (blob !== false) {
|
if (blob !== false) {
|
||||||
blob.name = image.name;
|
blob.name = image.name;
|
||||||
processData(blob);
|
processUpload(blob);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -224,7 +203,7 @@ BlazeComponent.extendComponent({
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(image);
|
reader.readAsDataURL(image);
|
||||||
} else {
|
} else {
|
||||||
processData(image);
|
processUpload(image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -85,7 +85,7 @@ template(name="changeAvatarPopup")
|
||||||
each uploadedAvatars
|
each uploadedAvatars
|
||||||
li: a.js-select-avatar
|
li: a.js-select-avatar
|
||||||
.member
|
.member
|
||||||
img.avatar.avatar-image(src="{{url avatarUrlOptions}}")
|
img.avatar.avatar-image(src="{{link}}?auth=false&brokenIsFine=true")
|
||||||
| {{_ 'uploaded-avatar'}}
|
| {{_ 'uploaded-avatar'}}
|
||||||
if isSelected
|
if isSelected
|
||||||
i.fa.fa-check
|
i.fa.fa-check
|
||||||
|
@ -93,7 +93,7 @@ template(name="changeAvatarPopup")
|
||||||
unless isSelected
|
unless isSelected
|
||||||
a.js-delete-avatar {{_ 'delete'}}
|
a.js-delete-avatar {{_ 'delete'}}
|
||||||
| -
|
| -
|
||||||
= original.name
|
= name
|
||||||
li: a.js-select-initials
|
li: a.js-select-initials
|
||||||
.member
|
.member
|
||||||
+userAvatarInitials(userId=currentUser._id)
|
+userAvatarInitials(userId=currentUser._id)
|
||||||
|
|
|
@ -3,6 +3,7 @@ import Avatars from '/models/avatars';
|
||||||
import Users from '/models/users';
|
import Users from '/models/users';
|
||||||
import Org from '/models/org';
|
import Org from '/models/org';
|
||||||
import Team from '/models/team';
|
import Team from '/models/team';
|
||||||
|
import { formatFleURL } from 'meteor/ostrio:files/lib';
|
||||||
|
|
||||||
Template.userAvatar.helpers({
|
Template.userAvatar.helpers({
|
||||||
userData() {
|
userData() {
|
||||||
|
@ -181,21 +182,14 @@ BlazeComponent.extendComponent({
|
||||||
Meteor.subscribe('my-avatars');
|
Meteor.subscribe('my-avatars');
|
||||||
},
|
},
|
||||||
|
|
||||||
avatarUrlOptions() {
|
|
||||||
return {
|
|
||||||
auth: false,
|
|
||||||
brokenIsFine: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
uploadedAvatars() {
|
uploadedAvatars() {
|
||||||
return Avatars.find({ userId: Meteor.userId() });
|
return Avatars.find({ userId: Meteor.userId() }).each();
|
||||||
},
|
},
|
||||||
|
|
||||||
isSelected() {
|
isSelected() {
|
||||||
const userProfile = Meteor.user().profile;
|
const userProfile = Meteor.user().profile;
|
||||||
const avatarUrl = userProfile && userProfile.avatarUrl;
|
const avatarUrl = userProfile && userProfile.avatarUrl;
|
||||||
const currentAvatarUrl = this.currentData().url(this.avatarUrlOptions());
|
const currentAvatarUrl = `${this.currentData().link()}?auth=false&brokenIsFine=true`;
|
||||||
return avatarUrl === currentAvatarUrl;
|
return avatarUrl === currentAvatarUrl;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -220,32 +214,30 @@ BlazeComponent.extendComponent({
|
||||||
this.$('.js-upload-avatar-input').click();
|
this.$('.js-upload-avatar-input').click();
|
||||||
},
|
},
|
||||||
'change .js-upload-avatar-input'(event) {
|
'change .js-upload-avatar-input'(event) {
|
||||||
let file, fileUrl;
|
const self = this;
|
||||||
|
if (event.currentTarget.files && event.currentTarget.files[0]) {
|
||||||
FS.Utility.eachFile(event, f => {
|
const uploader = Avatars.insert(
|
||||||
try {
|
{
|
||||||
file = Avatars.insert(new FS.File(f));
|
file: event.currentTarget.files[0],
|
||||||
fileUrl = file.url(this.avatarUrlOptions());
|
chunkSize: 'dynamic',
|
||||||
} catch (e) {
|
},
|
||||||
this.setError('avatar-too-big');
|
false,
|
||||||
|
);
|
||||||
|
uploader.on('uploaded', (error, fileRef) => {
|
||||||
|
if (!error) {
|
||||||
|
self.setAvatar(
|
||||||
|
`${formatFleURL(fileRef)}?auth=false&brokenIsFine=true`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
uploader.on('error', (error, fileData) => {
|
||||||
if (fileUrl) {
|
self.setError(error.reason);
|
||||||
this.setError('');
|
|
||||||
const fetchAvatarInterval = window.setInterval(() => {
|
|
||||||
$.ajax({
|
|
||||||
url: fileUrl,
|
|
||||||
success: () => {
|
|
||||||
this.setAvatar(file.url(this.avatarUrlOptions()));
|
|
||||||
window.clearInterval(fetchAvatarInterval);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}, 100);
|
uploader.start();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'click .js-select-avatar'() {
|
'click .js-select-avatar'() {
|
||||||
const avatarUrl = this.currentData().url(this.avatarUrlOptions());
|
const avatarUrl = `${this.currentData().link()}?auth=false&brokenIsFine=true`;
|
||||||
this.setAvatar(avatarUrl);
|
this.setAvatar(avatarUrl);
|
||||||
},
|
},
|
||||||
'click .js-select-initials'() {
|
'click .js-select-initials'() {
|
||||||
|
|
|
@ -162,33 +162,21 @@ Utils = {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
getCommonAttachmentMetaFrom(card) {
|
||||||
|
const meta = {};
|
||||||
|
if (card.isLinkedCard()) {
|
||||||
|
meta.boardId = Cards.findOne(card.linkedId).boardId;
|
||||||
|
meta.cardId = card.linkedId;
|
||||||
|
} else {
|
||||||
|
meta.boardId = card.boardId;
|
||||||
|
meta.swimlaneId = card.swimlaneId;
|
||||||
|
meta.listId = card.listId;
|
||||||
|
meta.cardId = card._id;
|
||||||
|
}
|
||||||
|
return meta;
|
||||||
|
},
|
||||||
MAX_IMAGE_PIXEL: Meteor.settings.public.MAX_IMAGE_PIXEL,
|
MAX_IMAGE_PIXEL: Meteor.settings.public.MAX_IMAGE_PIXEL,
|
||||||
COMPRESS_RATIO: Meteor.settings.public.IMAGE_COMPRESS_RATIO,
|
COMPRESS_RATIO: Meteor.settings.public.IMAGE_COMPRESS_RATIO,
|
||||||
processUploadedAttachment(card, fileObj, callback) {
|
|
||||||
const next = attachment => {
|
|
||||||
if (typeof callback === 'function') {
|
|
||||||
callback(attachment);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (!card) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
const file = new FS.File(fileObj);
|
|
||||||
if (card.isLinkedCard()) {
|
|
||||||
file.boardId = Cards.findOne(card.linkedId).boardId;
|
|
||||||
file.cardId = card.linkedId;
|
|
||||||
} else {
|
|
||||||
file.boardId = card.boardId;
|
|
||||||
file.swimlaneId = card.swimlaneId;
|
|
||||||
file.listId = card.listId;
|
|
||||||
file.cardId = card._id;
|
|
||||||
}
|
|
||||||
file.userId = Meteor.userId();
|
|
||||||
if (file.original) {
|
|
||||||
file.original.name = fileObj.name;
|
|
||||||
}
|
|
||||||
return next(Attachments.insert(file));
|
|
||||||
},
|
|
||||||
shrinkImage(options) {
|
shrinkImage(options) {
|
||||||
// shrink image to certain size
|
// shrink image to certain size
|
||||||
const dataurl = options.dataurl,
|
const dataurl = options.dataurl,
|
||||||
|
|
|
@ -247,10 +247,6 @@ services:
|
||||||
# Defaults below. Uncomment to change. wekan/server/accounts-common.js
|
# Defaults below. Uncomment to change. wekan/server/accounts-common.js
|
||||||
# - ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90
|
# - ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90
|
||||||
#---------------------------------------------------------------
|
#---------------------------------------------------------------
|
||||||
# ==== STORE ATTACHMENT ON SERVER FILESYSTEM INSTEAD OF MONGODB ====
|
|
||||||
# https://github.com/wekan/wekan/pull/2603
|
|
||||||
#- ATTACHMENTS_STORE_PATH = <pathname> # pathname can be relative or fullpath
|
|
||||||
#---------------------------------------------------------------
|
|
||||||
# ==== RICH TEXT EDITOR IN CARD COMMENTS ====
|
# ==== RICH TEXT EDITOR IN CARD COMMENTS ====
|
||||||
# https://github.com/wekan/wekan/pull/2560
|
# https://github.com/wekan/wekan/pull/2560
|
||||||
- RICHER_CARD_COMMENT_EDITOR=false
|
- RICHER_CARD_COMMENT_EDITOR=false
|
||||||
|
@ -329,6 +325,9 @@ services:
|
||||||
# When browser policy is enabled, HTML code at this Trusted URL can have iframe that embeds Wekan inside.
|
# When browser policy is enabled, HTML code at this Trusted URL can have iframe that embeds Wekan inside.
|
||||||
#- TRUSTED_URL=https://intra.example.com
|
#- TRUSTED_URL=https://intra.example.com
|
||||||
#-----------------------------------------------------------------
|
#-----------------------------------------------------------------
|
||||||
|
# ==== WRITEABLE PATH FOR FILE UPLOADS ====
|
||||||
|
- WRITABLE_PATH=/data
|
||||||
|
#-----------------------------------------------------------------
|
||||||
# ==== OUTGOING WEBHOOKS ====
|
# ==== OUTGOING WEBHOOKS ====
|
||||||
# What to send to Outgoing Webhook, or leave out. If commented out the default values will be: cardId,listId,oldListId,boardId,comment,user,card,commentId,swimlaneId,customerField,customFieldValue
|
# What to send to Outgoing Webhook, or leave out. If commented out the default values will be: cardId,listId,oldListId,boardId,comment,user,card,commentId,swimlaneId,customerField,customFieldValue
|
||||||
#- WEBHOOKS_ATTRIBUTES=cardId,listId,oldListId,boardId,comment,user,card,commentId
|
#- WEBHOOKS_ATTRIBUTES=cardId,listId,oldListId,boardId,comment,user,card,commentId
|
||||||
|
@ -674,6 +673,7 @@ services:
|
||||||
- wekandb
|
- wekandb
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
- ./volumes/data:/data
|
||||||
|
|
||||||
#---------------------------------------------------------------------------------
|
#---------------------------------------------------------------------------------
|
||||||
# ==== OPTIONAL: SHARE DATABASE TO OFFICE LAN AND REMOTE VPN ====
|
# ==== OPTIONAL: SHARE DATABASE TO OFFICE LAN AND REMOTE VPN ====
|
||||||
|
|
|
@ -1,914 +0,0 @@
|
||||||
(function () {
|
|
||||||
|
|
||||||
/* Imports */
|
|
||||||
var Meteor = Package.meteor.Meteor;
|
|
||||||
var global = Package.meteor.global;
|
|
||||||
var meteorEnv = Package.meteor.meteorEnv;
|
|
||||||
var FS = Package['wekan-cfs-base-package'].FS;
|
|
||||||
var check = Package.check.check;
|
|
||||||
var Match = Package.check.Match;
|
|
||||||
var EJSON = Package.ejson.EJSON;
|
|
||||||
var HTTP = Package['wekan-cfs-http-methods'].HTTP;
|
|
||||||
|
|
||||||
/* Package-scope variables */
|
|
||||||
var rootUrlPathPrefix, baseUrl, getHeaders, getHeadersByCollection, _existingMountPoints, mountUrls;
|
|
||||||
|
|
||||||
(function(){
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////
|
|
||||||
// //
|
|
||||||
// packages/cfs_access-point/packages/cfs_access-point.js //
|
|
||||||
// //
|
|
||||||
///////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
(function () {
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// //
|
|
||||||
// packages/wekan-cfs-access-point/access-point-common.js //
|
|
||||||
// //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; // 1
|
|
||||||
// Adjust the rootUrlPathPrefix if necessary // 2
|
|
||||||
if (rootUrlPathPrefix.length > 0) { // 3
|
|
||||||
if (rootUrlPathPrefix.slice(0, 1) !== '/') { // 4
|
|
||||||
rootUrlPathPrefix = '/' + rootUrlPathPrefix; // 5
|
|
||||||
} // 6
|
|
||||||
if (rootUrlPathPrefix.slice(-1) === '/') { // 7
|
|
||||||
rootUrlPathPrefix = rootUrlPathPrefix.slice(0, -1); // 8
|
|
||||||
} // 9
|
|
||||||
} // 10
|
|
||||||
// 11
|
|
||||||
// prepend ROOT_URL when isCordova // 12
|
|
||||||
if (Meteor.isCordova) { // 13
|
|
||||||
rootUrlPathPrefix = Meteor.absoluteUrl(rootUrlPathPrefix.replace(/^\/+/, '')).replace(/\/+$/, ''); // 14
|
|
||||||
} // 15
|
|
||||||
// 16
|
|
||||||
baseUrl = '/cfs'; // 17
|
|
||||||
FS.HTTP = FS.HTTP || {}; // 18
|
|
||||||
// 19
|
|
||||||
// Note the upload URL so that client uploader packages know what it is // 20
|
|
||||||
FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 21
|
|
||||||
// 22
|
|
||||||
/** // 23
|
|
||||||
* @method FS.HTTP.setBaseUrl // 24
|
|
||||||
* @public // 25
|
|
||||||
* @param {String} newBaseUrl - Change the base URL for the HTTP GET and DELETE endpoints. // 26
|
|
||||||
* @returns {undefined} // 27
|
|
||||||
*/ // 28
|
|
||||||
FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) { // 29
|
|
||||||
// 30
|
|
||||||
// Adjust the baseUrl if necessary // 31
|
|
||||||
if (newBaseUrl.slice(0, 1) !== '/') { // 32
|
|
||||||
newBaseUrl = '/' + newBaseUrl; // 33
|
|
||||||
} // 34
|
|
||||||
if (newBaseUrl.slice(-1) === '/') { // 35
|
|
||||||
newBaseUrl = newBaseUrl.slice(0, -1); // 36
|
|
||||||
} // 37
|
|
||||||
// 38
|
|
||||||
// Update the base URL // 39
|
|
||||||
baseUrl = newBaseUrl; // 40
|
|
||||||
// 41
|
|
||||||
// Change the upload URL so that client uploader packages know what it is // 42
|
|
||||||
FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 43
|
|
||||||
// 44
|
|
||||||
// Remount URLs with the new baseUrl, unmounting the old, on the server only. // 45
|
|
||||||
// If existingMountPoints is empty, then we haven't run the server startup // 46
|
|
||||||
// code yet, so this new URL will be used at that point for the initial mount. // 47
|
|
||||||
if (Meteor.isServer && !FS.Utility.isEmpty(_existingMountPoints)) { // 48
|
|
||||||
mountUrls(); // 49
|
|
||||||
} // 50
|
|
||||||
}; // 51
|
|
||||||
// 52
|
|
||||||
/* // 53
|
|
||||||
* FS.File extensions // 54
|
|
||||||
*/ // 55
|
|
||||||
// 56
|
|
||||||
/** // 57
|
|
||||||
* @method FS.File.prototype.url Construct the file url // 58
|
|
||||||
* @public // 59
|
|
||||||
* @param {Object} [options] // 60
|
|
||||||
* @param {String} [options.store] Name of the store to get from. If not defined, the first store defined in `options.stores` for the collection on the client is used.
|
|
||||||
* @param {Boolean} [options.auth=null] Add authentication token to the URL query string? By default, a token for the current logged in user is added on the client. Set this to `false` to omit the token. Set this to a string to provide your own token. Set this to a number to specify an expiration time for the token in seconds.
|
|
||||||
* @param {Boolean} [options.download=false] Should headers be set to force a download? Typically this means that clicking the link with this URL will download the file to the user's Downloads folder instead of displaying the file in the browser.
|
|
||||||
* @param {Boolean} [options.brokenIsFine=false] Return the URL even if we know it's currently a broken link because the file hasn't been saved in the requested store yet.
|
|
||||||
* @param {Boolean} [options.metadata=false] Return the URL for the file metadata access point rather than the file itself.
|
|
||||||
* @param {String} [options.uploading=null] A URL to return while the file is being uploaded. // 66
|
|
||||||
* @param {String} [options.storing=null] A URL to return while the file is being stored. // 67
|
|
||||||
* @param {String} [options.filename=null] Override the filename that should appear at the end of the URL. By default it is the name of the file in the requested store.
|
|
||||||
* // 69
|
|
||||||
* Returns the HTTP URL for getting the file or its metadata. // 70
|
|
||||||
*/ // 71
|
|
||||||
FS.File.prototype.url = function(options) { // 72
|
|
||||||
var self = this; // 73
|
|
||||||
options = options || {}; // 74
|
|
||||||
options = FS.Utility.extend({ // 75
|
|
||||||
store: null, // 76
|
|
||||||
auth: null, // 77
|
|
||||||
download: false, // 78
|
|
||||||
metadata: false, // 79
|
|
||||||
brokenIsFine: false, // 80
|
|
||||||
uploading: null, // return this URL while uploading // 81
|
|
||||||
storing: null, // return this URL while storing // 82
|
|
||||||
filename: null // override the filename that is shown to the user // 83
|
|
||||||
}, options.hash || options); // check for "hash" prop if called as helper // 84
|
|
||||||
// 85
|
|
||||||
// Primarily useful for displaying a temporary image while uploading an image // 86
|
|
||||||
if (options.uploading && !self.isUploaded()) { // 87
|
|
||||||
return options.uploading; // 88
|
|
||||||
} // 89
|
|
||||||
// 90
|
|
||||||
if (self.isMounted()) { // 91
|
|
||||||
// See if we've stored in the requested store yet // 92
|
|
||||||
var storeName = options.store || self.collection.primaryStore.name; // 93
|
|
||||||
if (!self.hasStored(storeName)) { // 94
|
|
||||||
if (options.storing) { // 95
|
|
||||||
return options.storing; // 96
|
|
||||||
} else if (!options.brokenIsFine) { // 97
|
|
||||||
// We want to return null if we know the URL will be a broken // 98
|
|
||||||
// link because then we can avoid rendering broken links, broken // 99
|
|
||||||
// images, etc. // 100
|
|
||||||
return null; // 101
|
|
||||||
} // 102
|
|
||||||
} // 103
|
|
||||||
// 104
|
|
||||||
// Add filename to end of URL if we can determine one // 105
|
|
||||||
var filename = options.filename || self.name({store: storeName}); // 106
|
|
||||||
if (typeof filename === "string" && filename.length) { // 107
|
|
||||||
filename = '/' + filename; // 108
|
|
||||||
} else { // 109
|
|
||||||
filename = ''; // 110
|
|
||||||
} // 111
|
|
||||||
// 112
|
|
||||||
// TODO: Could we somehow figure out if the collection requires login? // 113
|
|
||||||
var authToken = ''; // 114
|
|
||||||
if (Meteor.isClient && typeof Accounts !== "undefined" && typeof Accounts._storedLoginToken === "function") { // 115
|
|
||||||
if (options.auth !== false) { // 116
|
|
||||||
// Add reactive deps on the user // 117
|
|
||||||
Meteor.userId(); // 118
|
|
||||||
// 119
|
|
||||||
var authObject = { // 120
|
|
||||||
authToken: Accounts._storedLoginToken() || '' // 121
|
|
||||||
}; // 122
|
|
||||||
// 123
|
|
||||||
// If it's a number, we use that as the expiration time (in seconds) // 124
|
|
||||||
if (options.auth === +options.auth) { // 125
|
|
||||||
authObject.expiration = FS.HTTP.now() + options.auth * 1000; // 126
|
|
||||||
} // 127
|
|
||||||
// 128
|
|
||||||
// Set the authToken // 129
|
|
||||||
var authString = JSON.stringify(authObject); // 130
|
|
||||||
authToken = FS.Utility.btoa(authString); // 131
|
|
||||||
} // 132
|
|
||||||
} else if (typeof options.auth === "string") { // 133
|
|
||||||
// If the user supplies auth token the user will be responsible for // 134
|
|
||||||
// updating // 135
|
|
||||||
authToken = options.auth; // 136
|
|
||||||
} // 137
|
|
||||||
// 138
|
|
||||||
// Construct query string // 139
|
|
||||||
var params = {}; // 140
|
|
||||||
if (authToken !== '') { // 141
|
|
||||||
params.token = authToken; // 142
|
|
||||||
} // 143
|
|
||||||
if (options.download) { // 144
|
|
||||||
params.download = true; // 145
|
|
||||||
} // 146
|
|
||||||
if (options.store) { // 147
|
|
||||||
// We use options.store here instead of storeName because we want to omit the queryString // 148
|
|
||||||
// whenever possible, allowing users to have "clean" URLs if they want. The server will // 149
|
|
||||||
// assume the first store defined on the server, which means that we are assuming that // 150
|
|
||||||
// the first on the client is also the first on the server. If that's not the case, the // 151
|
|
||||||
// store option should be supplied. // 152
|
|
||||||
params.store = options.store; // 153
|
|
||||||
} // 154
|
|
||||||
var queryString = FS.Utility.encodeParams(params); // 155
|
|
||||||
if (queryString.length) { // 156
|
|
||||||
queryString = '?' + queryString; // 157
|
|
||||||
} // 158
|
|
||||||
// 159
|
|
||||||
// Determine which URL to use // 160
|
|
||||||
var area; // 161
|
|
||||||
if (options.metadata) { // 162
|
|
||||||
area = '/record'; // 163
|
|
||||||
} else { // 164
|
|
||||||
area = '/files'; // 165
|
|
||||||
} // 166
|
|
||||||
// 167
|
|
||||||
// Construct and return the http method url // 168
|
|
||||||
return rootUrlPathPrefix + baseUrl + area + '/' + self.collection.name + '/' + self._id + filename + queryString; // 169
|
|
||||||
} // 170
|
|
||||||
// 171
|
|
||||||
}; // 172
|
|
||||||
// 173
|
|
||||||
// 174
|
|
||||||
// 175
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
}).call(this);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// //
|
|
||||||
// packages/wekan-cfs-access-point/access-point-handlers.js //
|
|
||||||
// //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
getHeaders = []; // 1
|
|
||||||
getHeadersByCollection = {}; // 2
|
|
||||||
// 3
|
|
||||||
FS.HTTP.Handlers = {}; // 4
|
|
||||||
// 5
|
|
||||||
/** // 6
|
|
||||||
* @method FS.HTTP.Handlers.Del // 7
|
|
||||||
* @public // 8
|
|
||||||
* @returns {any} response // 9
|
|
||||||
* // 10
|
|
||||||
* HTTP DEL request handler // 11
|
|
||||||
*/ // 12
|
|
||||||
FS.HTTP.Handlers.Del = function httpDelHandler(ref) { // 13
|
|
||||||
var self = this; // 14
|
|
||||||
var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 15
|
|
||||||
// 16
|
|
||||||
// If DELETE request, validate with 'remove' allow/deny, delete the file, and return // 17
|
|
||||||
FS.Utility.validateAction(ref.collection.files._validators['remove'], ref.file, self.userId); // 18
|
|
||||||
// 19
|
|
||||||
/* // 20
|
|
||||||
* From the DELETE spec: // 21
|
|
||||||
* A successful response SHOULD be 200 (OK) if the response includes an // 22
|
|
||||||
* entity describing the status, 202 (Accepted) if the action has not // 23
|
|
||||||
* yet been enacted, or 204 (No Content) if the action has been enacted // 24
|
|
||||||
* but the response does not include an entity. // 25
|
|
||||||
*/ // 26
|
|
||||||
self.setStatusCode(200); // 27
|
|
||||||
// 28
|
|
||||||
return { // 29
|
|
||||||
deleted: !!ref.file.remove() // 30
|
|
||||||
}; // 31
|
|
||||||
}; // 32
|
|
||||||
// 33
|
|
||||||
/** // 34
|
|
||||||
* @method FS.HTTP.Handlers.GetList // 35
|
|
||||||
* @public // 36
|
|
||||||
* @returns {Object} response // 37
|
|
||||||
* // 38
|
|
||||||
* HTTP GET file list request handler // 39
|
|
||||||
*/ // 40
|
|
||||||
FS.HTTP.Handlers.GetList = function httpGetListHandler() { // 41
|
|
||||||
// Not Yet Implemented // 42
|
|
||||||
// Need to check publications and return file list based on // 43
|
|
||||||
// what user is allowed to see // 44
|
|
||||||
}; // 45
|
|
||||||
// 46
|
|
||||||
/* // 47
|
|
||||||
requestRange will parse the range set in request header - if not possible it // 48
|
|
||||||
will throw fitting errors and autofill range for both partial and full ranges // 49
|
|
||||||
// 50
|
|
||||||
throws error or returns the object: // 51
|
|
||||||
{ // 52
|
|
||||||
start // 53
|
|
||||||
end // 54
|
|
||||||
length // 55
|
|
||||||
unit // 56
|
|
||||||
partial // 57
|
|
||||||
} // 58
|
|
||||||
*/ // 59
|
|
||||||
var requestRange = function(req, fileSize) { // 60
|
|
||||||
if (req) { // 61
|
|
||||||
if (req.headers) { // 62
|
|
||||||
var rangeString = req.headers.range; // 63
|
|
||||||
// 64
|
|
||||||
// Make sure range is a string // 65
|
|
||||||
if (rangeString === ''+rangeString) { // 66
|
|
||||||
// 67
|
|
||||||
// range will be in the format "bytes=0-32767" // 68
|
|
||||||
var parts = rangeString.split('='); // 69
|
|
||||||
var unit = parts[0]; // 70
|
|
||||||
// 71
|
|
||||||
// Make sure parts consists of two strings and range is of type "byte" // 72
|
|
||||||
if (parts.length == 2 && unit == 'bytes') { // 73
|
|
||||||
// Parse the range // 74
|
|
||||||
var range = parts[1].split('-'); // 75
|
|
||||||
var start = Number(range[0]); // 76
|
|
||||||
var end = Number(range[1]); // 77
|
|
||||||
// 78
|
|
||||||
// Fix invalid ranges? // 79
|
|
||||||
if (range[0] != start) start = 0; // 80
|
|
||||||
if (range[1] != end || !end) end = fileSize - 1; // 81
|
|
||||||
// 82
|
|
||||||
// Make sure range consists of a start and end point of numbers and start is less than end // 83
|
|
||||||
if (start < end) { // 84
|
|
||||||
// 85
|
|
||||||
var partSize = 0 - start + end + 1; // 86
|
|
||||||
// 87
|
|
||||||
// Return the parsed range // 88
|
|
||||||
return { // 89
|
|
||||||
start: start, // 90
|
|
||||||
end: end, // 91
|
|
||||||
length: partSize, // 92
|
|
||||||
size: fileSize, // 93
|
|
||||||
unit: unit, // 94
|
|
||||||
partial: (partSize < fileSize) // 95
|
|
||||||
}; // 96
|
|
||||||
// 97
|
|
||||||
} else { // 98
|
|
||||||
throw new Meteor.Error(416, "Requested Range Not Satisfiable"); // 99
|
|
||||||
} // 100
|
|
||||||
// 101
|
|
||||||
} else { // 102
|
|
||||||
// The first part should be bytes // 103
|
|
||||||
throw new Meteor.Error(416, "Requested Range Unit Not Satisfiable"); // 104
|
|
||||||
} // 105
|
|
||||||
// 106
|
|
||||||
} else { // 107
|
|
||||||
// No range found // 108
|
|
||||||
} // 109
|
|
||||||
// 110
|
|
||||||
} else { // 111
|
|
||||||
// throw new Error('No request headers set for _parseRange function'); // 112
|
|
||||||
} // 113
|
|
||||||
} else { // 114
|
|
||||||
throw new Error('No request object passed to _parseRange function'); // 115
|
|
||||||
} // 116
|
|
||||||
// 117
|
|
||||||
return { // 118
|
|
||||||
start: 0, // 119
|
|
||||||
end: fileSize - 1, // 120
|
|
||||||
length: fileSize, // 121
|
|
||||||
size: fileSize, // 122
|
|
||||||
unit: 'bytes', // 123
|
|
||||||
partial: false // 124
|
|
||||||
}; // 125
|
|
||||||
}; // 126
|
|
||||||
// 127
|
|
||||||
/** // 128
|
|
||||||
* @method FS.HTTP.Handlers.Get // 129
|
|
||||||
* @public // 130
|
|
||||||
* @returns {any} response // 131
|
|
||||||
* // 132
|
|
||||||
* HTTP GET request handler // 133
|
|
||||||
*/ // 134
|
|
||||||
FS.HTTP.Handlers.Get = function httpGetHandler(ref) { // 135
|
|
||||||
var self = this; // 136
|
|
||||||
// Once we have the file, we can test allow/deny validators // 137
|
|
||||||
// XXX: pass on the "share" query eg. ?share=342hkjh23ggj for shared url access? // 138
|
|
||||||
FS.Utility.validateAction(ref.collection._validators['download'], ref.file, self.userId /*, self.query.shareId*/); // 139
|
|
||||||
// 140
|
|
||||||
var storeName = ref.storeName; // 141
|
|
||||||
// 142
|
|
||||||
// If no storeName was specified, use the first defined storeName // 143
|
|
||||||
if (typeof storeName !== "string") { // 144
|
|
||||||
// No store handed, we default to primary store // 145
|
|
||||||
storeName = ref.collection.primaryStore.name; // 146
|
|
||||||
} // 147
|
|
||||||
// 148
|
|
||||||
// Get the storage reference // 149
|
|
||||||
var storage = ref.collection.storesLookup[storeName]; // 150
|
|
||||||
// 151
|
|
||||||
if (!storage) { // 152
|
|
||||||
throw new Meteor.Error(404, "Not Found", 'There is no store "' + storeName + '"'); // 153
|
|
||||||
} // 154
|
|
||||||
// 155
|
|
||||||
// Get the file // 156
|
|
||||||
var copyInfo = ref.file.copies[storeName]; // 157
|
|
||||||
// 158
|
|
||||||
if (!copyInfo) { // 159
|
|
||||||
throw new Meteor.Error(404, "Not Found", 'This file was not stored in the ' + storeName + ' store'); // 160
|
|
||||||
} // 161
|
|
||||||
// 162
|
|
||||||
// Set the content type for file // 163
|
|
||||||
if (typeof copyInfo.type === "string") { // 164
|
|
||||||
self.setContentType(copyInfo.type); // 165
|
|
||||||
} else { // 166
|
|
||||||
self.setContentType('application/octet-stream'); // 167
|
|
||||||
} // 168
|
|
||||||
// 169
|
|
||||||
// Add 'Content-Disposition' header if requested a download/attachment URL // 170
|
|
||||||
if (typeof ref.download !== "undefined") { // 171
|
|
||||||
var filename = ref.filename || copyInfo.name; // 172
|
|
||||||
self.addHeader('Content-Disposition', 'attachment; filename="' + filename + '"'); // 173
|
|
||||||
} else { // 174
|
|
||||||
self.addHeader('Content-Disposition', 'inline'); // 175
|
|
||||||
} // 176
|
|
||||||
// 177
|
|
||||||
// Get the contents range from request // 178
|
|
||||||
var range = requestRange(self.request, copyInfo.size); // 179
|
|
||||||
// 180
|
|
||||||
// Some browsers cope better if the content-range header is // 181
|
|
||||||
// still included even for the full file being returned. // 182
|
|
||||||
self.addHeader('Content-Range', range.unit + ' ' + range.start + '-' + range.end + '/' + range.size); // 183
|
|
||||||
// 184
|
|
||||||
// If a chunk/range was requested instead of the whole file, serve that' // 185
|
|
||||||
if (range.partial) { // 186
|
|
||||||
self.setStatusCode(206, 'Partial Content'); // 187
|
|
||||||
} else { // 188
|
|
||||||
self.setStatusCode(200, 'OK'); // 189
|
|
||||||
} // 190
|
|
||||||
// 191
|
|
||||||
// Add any other global custom headers and collection-specific custom headers // 192
|
|
||||||
FS.Utility.each(getHeaders.concat(getHeadersByCollection[ref.collection.name] || []), function(header) { // 193
|
|
||||||
self.addHeader(header[0], header[1]); // 194
|
|
||||||
}); // 195
|
|
||||||
// 196
|
|
||||||
// Inform clients about length (or chunk length in case of ranges) // 197
|
|
||||||
self.addHeader('Content-Length', range.length); // 198
|
|
||||||
// 199
|
|
||||||
// Last modified header (updatedAt from file info) // 200
|
|
||||||
self.addHeader('Last-Modified', copyInfo.updatedAt.toUTCString()); // 201
|
|
||||||
// 202
|
|
||||||
// Inform clients that we accept ranges for resumable chunked downloads // 203
|
|
||||||
self.addHeader('Accept-Ranges', range.unit); // 204
|
|
||||||
// 205
|
|
||||||
if (FS.debug) console.log('Read file "' + (ref.filename || copyInfo.name) + '" ' + range.unit + ' ' + range.start + '-' + range.end + '/' + range.size);
|
|
||||||
// 207
|
|
||||||
var readStream = storage.adapter.createReadStream(ref.file, {start: range.start, end: range.end}); // 208
|
|
||||||
// 209
|
|
||||||
readStream.on('error', function(err) { // 210
|
|
||||||
// Send proper error message on get error // 211
|
|
||||||
if (err.message && err.statusCode) { // 212
|
|
||||||
self.Error(new Meteor.Error(err.statusCode, err.message)); // 213
|
|
||||||
} else { // 214
|
|
||||||
self.Error(new Meteor.Error(503, 'Service unavailable')); // 215
|
|
||||||
} // 216
|
|
||||||
}); // 217
|
|
||||||
// 218
|
|
||||||
readStream.pipe(self.createWriteStream()); // 219
|
|
||||||
}; // 220
|
|
||||||
|
|
||||||
const originalHandler = FS.HTTP.Handlers.Get;
|
|
||||||
FS.HTTP.Handlers.Get = function (ref) {
|
|
||||||
//console.log(ref.filename);
|
|
||||||
try {
|
|
||||||
var userAgent = (this.requestHeaders['user-agent']||'').toLowerCase();
|
|
||||||
|
|
||||||
if(userAgent.indexOf('msie') >= 0 || userAgent.indexOf('trident') >= 0 || userAgent.indexOf('chrome') >= 0) {
|
|
||||||
ref.filename = encodeURIComponent(ref.filename);
|
|
||||||
} else if(userAgent.indexOf('firefox') >= 0) {
|
|
||||||
ref.filename = Buffer.from(ref.filename).toString('binary');
|
|
||||||
} else {
|
|
||||||
/* safari*/
|
|
||||||
ref.filename = Buffer.from(ref.filename).toString('binary');
|
|
||||||
}
|
|
||||||
} catch (ex){
|
|
||||||
ref.filename = 'tempfix';
|
|
||||||
}
|
|
||||||
return originalHandler.call(this, ref);
|
|
||||||
};
|
|
||||||
// 221
|
|
||||||
/** // 222
|
|
||||||
* @method FS.HTTP.Handlers.PutInsert // 223
|
|
||||||
* @public // 224
|
|
||||||
* @returns {Object} response object with _id property // 225
|
|
||||||
* // 226
|
|
||||||
* HTTP PUT file insert request handler // 227
|
|
||||||
*/ // 228
|
|
||||||
FS.HTTP.Handlers.PutInsert = function httpPutInsertHandler(ref) { // 229
|
|
||||||
var self = this; // 230
|
|
||||||
var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 231
|
|
||||||
// 232
|
|
||||||
FS.debug && console.log("HTTP PUT (insert) handler"); // 233
|
|
||||||
// 234
|
|
||||||
// Create the nice FS.File // 235
|
|
||||||
var fileObj = new FS.File(); // 236
|
|
||||||
// 237
|
|
||||||
// Set its name // 238
|
|
||||||
fileObj.name(opts.filename || null); // 239
|
|
||||||
// 240
|
|
||||||
// Attach the readstream as the file's data // 241
|
|
||||||
fileObj.attachData(self.createReadStream(), {type: self.requestHeaders['content-type'] || 'application/octet-stream'});
|
|
||||||
// 243
|
|
||||||
// Validate with insert allow/deny // 244
|
|
||||||
FS.Utility.validateAction(ref.collection.files._validators['insert'], fileObj, self.userId); // 245
|
|
||||||
// 246
|
|
||||||
// Insert file into collection, triggering readStream storage // 247
|
|
||||||
ref.collection.insert(fileObj); // 248
|
|
||||||
// 249
|
|
||||||
// Send response // 250
|
|
||||||
self.setStatusCode(200); // 251
|
|
||||||
// 252
|
|
||||||
// Return the new file id // 253
|
|
||||||
return {_id: fileObj._id}; // 254
|
|
||||||
}; // 255
|
|
||||||
// 256
|
|
||||||
/** // 257
|
|
||||||
* @method FS.HTTP.Handlers.PutUpdate // 258
|
|
||||||
* @public // 259
|
|
||||||
* @returns {Object} response object with _id and chunk properties // 260
|
|
||||||
* // 261
|
|
||||||
* HTTP PUT file update chunk request handler // 262
|
|
||||||
*/ // 263
|
|
||||||
FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) { // 264
|
|
||||||
var self = this; // 265
|
|
||||||
var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 266
|
|
||||||
// 267
|
|
||||||
var chunk = parseInt(opts.chunk, 10); // 268
|
|
||||||
if (isNaN(chunk)) chunk = 0; // 269
|
|
||||||
// 270
|
|
||||||
FS.debug && console.log("HTTP PUT (update) handler received chunk: ", chunk); // 271
|
|
||||||
// 272
|
|
||||||
// Validate with insert allow/deny; also mounts and retrieves the file // 273
|
|
||||||
FS.Utility.validateAction(ref.collection.files._validators['insert'], ref.file, self.userId); // 274
|
|
||||||
// 275
|
|
||||||
self.createReadStream().pipe( FS.TempStore.createWriteStream(ref.file, chunk) ); // 276
|
|
||||||
// 277
|
|
||||||
// Send response // 278
|
|
||||||
self.setStatusCode(200); // 279
|
|
||||||
// 280
|
|
||||||
return { _id: ref.file._id, chunk: chunk }; // 281
|
|
||||||
}; // 282
|
|
||||||
// 283
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
}).call(this);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// //
|
|
||||||
// packages/wekan-cfs-access-point/access-point-server.js //
|
|
||||||
// //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
var path = Npm.require("path"); // 1
|
|
||||||
// 2
|
|
||||||
HTTP.publishFormats({ // 3
|
|
||||||
fileRecordFormat: function (input) { // 4
|
|
||||||
// Set the method scope content type to json // 5
|
|
||||||
this.setContentType('application/json'); // 6
|
|
||||||
if (FS.Utility.isArray(input)) { // 7
|
|
||||||
return EJSON.stringify(FS.Utility.map(input, function (obj) { // 8
|
|
||||||
return FS.Utility.cloneFileRecord(obj); // 9
|
|
||||||
})); // 10
|
|
||||||
} else { // 11
|
|
||||||
return EJSON.stringify(FS.Utility.cloneFileRecord(input)); // 12
|
|
||||||
} // 13
|
|
||||||
} // 14
|
|
||||||
}); // 15
|
|
||||||
// 16
|
|
||||||
/** // 17
|
|
||||||
* @method FS.HTTP.setHeadersForGet // 18
|
|
||||||
* @public // 19
|
|
||||||
* @param {Array} headers - List of headers, where each is a two-item array in which item 1 is the header name and item 2 is the header value.
|
|
||||||
* @param {Array|String} [collections] - Which collections the headers should be added for. Omit this argument to add the header for all collections.
|
|
||||||
* @returns {undefined} // 22
|
|
||||||
*/ // 23
|
|
||||||
FS.HTTP.setHeadersForGet = function setHeadersForGet(headers, collections) { // 24
|
|
||||||
if (typeof collections === "string") { // 25
|
|
||||||
collections = [collections]; // 26
|
|
||||||
} // 27
|
|
||||||
if (collections) { // 28
|
|
||||||
FS.Utility.each(collections, function(collectionName) { // 29
|
|
||||||
getHeadersByCollection[collectionName] = headers || []; // 30
|
|
||||||
}); // 31
|
|
||||||
} else { // 32
|
|
||||||
getHeaders = headers || []; // 33
|
|
||||||
} // 34
|
|
||||||
}; // 35
|
|
||||||
// 36
|
|
||||||
/** // 37
|
|
||||||
* @method FS.HTTP.publish // 38
|
|
||||||
* @public // 39
|
|
||||||
* @param {FS.Collection} collection // 40
|
|
||||||
* @param {Function} func - Publish function that returns a cursor. // 41
|
|
||||||
* @returns {undefined} // 42
|
|
||||||
* // 43
|
|
||||||
* Publishes all documents returned by the cursor at a GET URL // 44
|
|
||||||
* with the format baseUrl/record/collectionName. The publish // 45
|
|
||||||
* function `this` is similar to normal `Meteor.publish`. // 46
|
|
||||||
*/ // 47
|
|
||||||
FS.HTTP.publish = function fsHttpPublish(collection, func) { // 48
|
|
||||||
var name = baseUrl + '/record/' + collection.name; // 49
|
|
||||||
// Mount collection listing URL using http-publish package // 50
|
|
||||||
HTTP.publish({ // 51
|
|
||||||
name: name, // 52
|
|
||||||
defaultFormat: 'fileRecordFormat', // 53
|
|
||||||
collection: collection, // 54
|
|
||||||
collectionGet: true, // 55
|
|
||||||
collectionPost: false, // 56
|
|
||||||
documentGet: true, // 57
|
|
||||||
documentPut: false, // 58
|
|
||||||
documentDelete: false // 59
|
|
||||||
}, func); // 60
|
|
||||||
// 61
|
|
||||||
FS.debug && console.log("Registered HTTP method GET URLs:\n\n" + name + '\n' + name + '/:id\n'); // 62
|
|
||||||
}; // 63
|
|
||||||
// 64
|
|
||||||
/** // 65
|
|
||||||
* @method FS.HTTP.unpublish // 66
|
|
||||||
* @public // 67
|
|
||||||
* @param {FS.Collection} collection // 68
|
|
||||||
* @returns {undefined} // 69
|
|
||||||
* // 70
|
|
||||||
* Unpublishes a restpoint created by a call to `FS.HTTP.publish` // 71
|
|
||||||
*/ // 72
|
|
||||||
FS.HTTP.unpublish = function fsHttpUnpublish(collection) { // 73
|
|
||||||
// Mount collection listing URL using http-publish package // 74
|
|
||||||
HTTP.unpublish(baseUrl + '/record/' + collection.name); // 75
|
|
||||||
}; // 76
|
|
||||||
// 77
|
|
||||||
_existingMountPoints = {}; // 78
|
|
||||||
// 79
|
|
||||||
/** // 80
|
|
||||||
* @method defaultSelectorFunction // 81
|
|
||||||
* @private // 82
|
|
||||||
* @returns { collection, file } // 83
|
|
||||||
* // 84
|
|
||||||
* This is the default selector function // 85
|
|
||||||
*/ // 86
|
|
||||||
var defaultSelectorFunction = function() { // 87
|
|
||||||
var self = this; // 88
|
|
||||||
// Selector function // 89
|
|
||||||
// // 90
|
|
||||||
// This function will have to return the collection and the // 91
|
|
||||||
// file. If file not found undefined is returned - if null is returned the // 92
|
|
||||||
// search was not possible // 93
|
|
||||||
var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 94
|
|
||||||
// 95
|
|
||||||
// Get the collection name from the url // 96
|
|
||||||
var collectionName = opts.collectionName; // 97
|
|
||||||
// 98
|
|
||||||
// Get the id from the url // 99
|
|
||||||
var id = opts.id; // 100
|
|
||||||
// 101
|
|
||||||
// Get the collection // 102
|
|
||||||
var collection = FS._collections[collectionName]; // 103
|
|
||||||
// 104
|
|
||||||
// Get the file if possible else return null // 105
|
|
||||||
var file = (id && collection)? collection.findOne({ _id: id }): null; // 106
|
|
||||||
// 107
|
|
||||||
// Return the collection and the file // 108
|
|
||||||
return { // 109
|
|
||||||
collection: collection, // 110
|
|
||||||
file: file, // 111
|
|
||||||
storeName: opts.store, // 112
|
|
||||||
download: opts.download, // 113
|
|
||||||
filename: opts.filename // 114
|
|
||||||
}; // 115
|
|
||||||
}; // 116
|
|
||||||
// 117
|
|
||||||
/* // 118
|
|
||||||
* @method FS.HTTP.mount // 119
|
|
||||||
* @public // 120
|
|
||||||
* @param {array of string} mountPoints mount points to map rest functinality on // 121
|
|
||||||
* @param {function} selector_f [selector] function returns `{ collection, file }` for mount points to work with // 122
|
|
||||||
* // 123
|
|
||||||
*/ // 124
|
|
||||||
FS.HTTP.mount = function(mountPoints, selector_f) { // 125
|
|
||||||
// We take mount points as an array and we get a selector function // 126
|
|
||||||
var selectorFunction = selector_f || defaultSelectorFunction; // 127
|
|
||||||
// 128
|
|
||||||
var accessPoint = { // 129
|
|
||||||
'stream': true, // 130
|
|
||||||
'auth': expirationAuth, // 131
|
|
||||||
'post': function(data) { // 132
|
|
||||||
// Use the selector for finding the collection and file reference // 133
|
|
||||||
var ref = selectorFunction.call(this); // 134
|
|
||||||
// 135
|
|
||||||
// We dont support post - this would be normal insert eg. of filerecord? // 136
|
|
||||||
throw new Meteor.Error(501, "Not implemented", "Post is not supported"); // 137
|
|
||||||
}, // 138
|
|
||||||
'put': function(data) { // 139
|
|
||||||
// Use the selector for finding the collection and file reference // 140
|
|
||||||
var ref = selectorFunction.call(this); // 141
|
|
||||||
// 142
|
|
||||||
// Make sure we have a collection reference // 143
|
|
||||||
if (!ref.collection) // 144
|
|
||||||
throw new Meteor.Error(404, "Not Found", "No collection found"); // 145
|
|
||||||
// 146
|
|
||||||
// Make sure we have a file reference // 147
|
|
||||||
if (ref.file === null) { // 148
|
|
||||||
// No id supplied so we will create a new FS.File instance and // 149
|
|
||||||
// insert the supplied data. // 150
|
|
||||||
return FS.HTTP.Handlers.PutInsert.apply(this, [ref]); // 151
|
|
||||||
} else { // 152
|
|
||||||
if (ref.file) { // 153
|
|
||||||
return FS.HTTP.Handlers.PutUpdate.apply(this, [ref]); // 154
|
|
||||||
} else { // 155
|
|
||||||
throw new Meteor.Error(404, "Not Found", 'No file found'); // 156
|
|
||||||
} // 157
|
|
||||||
} // 158
|
|
||||||
}, // 159
|
|
||||||
'get': function(data) { // 160
|
|
||||||
// Use the selector for finding the collection and file reference // 161
|
|
||||||
var ref = selectorFunction.call(this); // 162
|
|
||||||
// 163
|
|
||||||
// Make sure we have a collection reference // 164
|
|
||||||
if (!ref.collection) // 165
|
|
||||||
throw new Meteor.Error(404, "Not Found", "No collection found"); // 166
|
|
||||||
// 167
|
|
||||||
// Make sure we have a file reference // 168
|
|
||||||
if (ref.file === null) { // 169
|
|
||||||
// No id supplied so we will return the published list of files ala // 170
|
|
||||||
// http.publish in json format // 171
|
|
||||||
return FS.HTTP.Handlers.GetList.apply(this, [ref]); // 172
|
|
||||||
} else { // 173
|
|
||||||
if (ref.file) { // 174
|
|
||||||
return FS.HTTP.Handlers.Get.apply(this, [ref]); // 175
|
|
||||||
} else { // 176
|
|
||||||
throw new Meteor.Error(404, "Not Found", 'No file found'); // 177
|
|
||||||
} // 178
|
|
||||||
} // 179
|
|
||||||
}, // 180
|
|
||||||
'delete': function(data) { // 181
|
|
||||||
// Use the selector for finding the collection and file reference // 182
|
|
||||||
var ref = selectorFunction.call(this); // 183
|
|
||||||
// 184
|
|
||||||
// Make sure we have a collection reference // 185
|
|
||||||
if (!ref.collection) // 186
|
|
||||||
throw new Meteor.Error(404, "Not Found", "No collection found"); // 187
|
|
||||||
// 188
|
|
||||||
// Make sure we have a file reference // 189
|
|
||||||
if (ref.file) { // 190
|
|
||||||
return FS.HTTP.Handlers.Del.apply(this, [ref]); // 191
|
|
||||||
} else { // 192
|
|
||||||
throw new Meteor.Error(404, "Not Found", 'No file found'); // 193
|
|
||||||
} // 194
|
|
||||||
} // 195
|
|
||||||
}; // 196
|
|
||||||
// 197
|
|
||||||
var accessPoints = {}; // 198
|
|
||||||
// 199
|
|
||||||
// Add debug message // 200
|
|
||||||
FS.debug && console.log('Registered HTTP method URLs:'); // 201
|
|
||||||
// 202
|
|
||||||
FS.Utility.each(mountPoints, function(mountPoint) { // 203
|
|
||||||
// Couple mountpoint and accesspoint // 204
|
|
||||||
accessPoints[mountPoint] = accessPoint; // 205
|
|
||||||
// Remember our mountpoints // 206
|
|
||||||
_existingMountPoints[mountPoint] = mountPoint; // 207
|
|
||||||
// Add debug message // 208
|
|
||||||
FS.debug && console.log(mountPoint); // 209
|
|
||||||
}); // 210
|
|
||||||
// 211
|
|
||||||
// XXX: HTTP:methods should unmount existing mounts in case of overwriting? // 212
|
|
||||||
HTTP.methods(accessPoints); // 213
|
|
||||||
// 214
|
|
||||||
}; // 215
|
|
||||||
// 216
|
|
||||||
/** // 217
|
|
||||||
* @method FS.HTTP.unmount // 218
|
|
||||||
* @public // 219
|
|
||||||
* @param {string | array of string} [mountPoints] Optional, if not specified all mountpoints are unmounted // 220
|
|
||||||
* // 221
|
|
||||||
*/ // 222
|
|
||||||
FS.HTTP.unmount = function(mountPoints) { // 223
|
|
||||||
// The mountPoints is optional, can be string or array if undefined then // 224
|
|
||||||
// _existingMountPoints will be used // 225
|
|
||||||
var unmountList; // 226
|
|
||||||
// Container for the mount points to unmount // 227
|
|
||||||
var unmountPoints = {}; // 228
|
|
||||||
// 229
|
|
||||||
if (typeof mountPoints === 'undefined') { // 230
|
|
||||||
// Use existing mount points - unmount all // 231
|
|
||||||
unmountList = _existingMountPoints; // 232
|
|
||||||
} else if (mountPoints === ''+mountPoints) { // 233
|
|
||||||
// Got a string // 234
|
|
||||||
unmountList = [mountPoints]; // 235
|
|
||||||
} else if (mountPoints.length) { // 236
|
|
||||||
// Got an array // 237
|
|
||||||
unmountList = mountPoints; // 238
|
|
||||||
} // 239
|
|
||||||
// 240
|
|
||||||
// If we have a list to unmount // 241
|
|
||||||
if (unmountList) { // 242
|
|
||||||
// Iterate over each item // 243
|
|
||||||
FS.Utility.each(unmountList, function(mountPoint) { // 244
|
|
||||||
// Check _existingMountPoints to make sure the mount point exists in our // 245
|
|
||||||
// context / was created by the FS.HTTP.mount // 246
|
|
||||||
if (_existingMountPoints[mountPoint]) { // 247
|
|
||||||
// Mark as unmount // 248
|
|
||||||
unmountPoints[mountPoint] = false; // 249
|
|
||||||
// Release // 250
|
|
||||||
delete _existingMountPoints[mountPoint]; // 251
|
|
||||||
} // 252
|
|
||||||
}); // 253
|
|
||||||
FS.debug && console.log('FS.HTTP.unmount:'); // 254
|
|
||||||
FS.debug && console.log(unmountPoints); // 255
|
|
||||||
// Complete unmount // 256
|
|
||||||
HTTP.methods(unmountPoints); // 257
|
|
||||||
} // 258
|
|
||||||
}; // 259
|
|
||||||
// 260
|
|
||||||
// ### FS.Collection maps on HTTP pr. default on the following restpoints: // 261
|
|
||||||
// * // 262
|
|
||||||
// baseUrl + '/files/:collectionName/:id/:filename', // 263
|
|
||||||
// baseUrl + '/files/:collectionName/:id', // 264
|
|
||||||
// baseUrl + '/files/:collectionName' // 265
|
|
||||||
// // 266
|
|
||||||
// Change/ replace the existing mount point by: // 267
|
|
||||||
// ```js // 268
|
|
||||||
// // unmount all existing // 269
|
|
||||||
// FS.HTTP.unmount(); // 270
|
|
||||||
// // Create new mount point // 271
|
|
||||||
// FS.HTTP.mount([ // 272
|
|
||||||
// '/cfs/files/:collectionName/:id/:filename', // 273
|
|
||||||
// '/cfs/files/:collectionName/:id', // 274
|
|
||||||
// '/cfs/files/:collectionName' // 275
|
|
||||||
// ]); // 276
|
|
||||||
// ``` // 277
|
|
||||||
// // 278
|
|
||||||
mountUrls = function mountUrls() { // 279
|
|
||||||
// We unmount first in case we are calling this a second time // 280
|
|
||||||
FS.HTTP.unmount(); // 281
|
|
||||||
// 282
|
|
||||||
FS.HTTP.mount([ // 283
|
|
||||||
baseUrl + '/files/:collectionName/:id/:filename', // 284
|
|
||||||
baseUrl + '/files/:collectionName/:id', // 285
|
|
||||||
baseUrl + '/files/:collectionName' // 286
|
|
||||||
]); // 287
|
|
||||||
}; // 288
|
|
||||||
// 289
|
|
||||||
// Returns the userId from URL token // 290
|
|
||||||
var expirationAuth = function expirationAuth() { // 291
|
|
||||||
var self = this; // 292
|
|
||||||
// 293
|
|
||||||
// Read the token from '/hello?token=base64' // 294
|
|
||||||
var encodedToken = self.query.token; // 295
|
|
||||||
// 296
|
|
||||||
FS.debug && console.log("token: "+encodedToken); // 297
|
|
||||||
// 298
|
|
||||||
if (!encodedToken || !Meteor.users) return false; // 299
|
|
||||||
// 300
|
|
||||||
// Check the userToken before adding it to the db query // 301
|
|
||||||
// Set the this.userId // 302
|
|
||||||
var tokenString = FS.Utility.atob(encodedToken); // 303
|
|
||||||
// 304
|
|
||||||
var tokenObject; // 305
|
|
||||||
try { // 306
|
|
||||||
tokenObject = JSON.parse(tokenString); // 307
|
|
||||||
} catch(err) { // 308
|
|
||||||
throw new Meteor.Error(400, 'Bad Request'); // 309
|
|
||||||
} // 310
|
|
||||||
// 311
|
|
||||||
// XXX: Do some check here of the object // 312
|
|
||||||
var userToken = tokenObject.authToken; // 313
|
|
||||||
if (userToken !== ''+userToken) { // 314
|
|
||||||
throw new Meteor.Error(400, 'Bad Request'); // 315
|
|
||||||
} // 316
|
|
||||||
// 317
|
|
||||||
// If we have an expiration token we should check that it's still valid // 318
|
|
||||||
if (tokenObject.expiration != null) { // 319
|
|
||||||
// check if its too old // 320
|
|
||||||
var now = Date.now(); // 321
|
|
||||||
if (tokenObject.expiration < now) { // 322
|
|
||||||
FS.debug && console.log('Expired token: ' + tokenObject.expiration + ' is less than ' + now); // 323
|
|
||||||
throw new Meteor.Error(500, 'Expired token'); // 324
|
|
||||||
} // 325
|
|
||||||
} // 326
|
|
||||||
// 327
|
|
||||||
// We are not on a secure line - so we have to look up the user... // 328
|
|
||||||
var user = Meteor.users.findOne({ // 329
|
|
||||||
$or: [ // 330
|
|
||||||
{'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(userToken)}, // 331
|
|
||||||
{'services.resume.loginTokens.token': userToken} // 332
|
|
||||||
] // 333
|
|
||||||
}); // 334
|
|
||||||
// 335
|
|
||||||
// Set the userId in the scope // 336
|
|
||||||
return user && user._id; // 337
|
|
||||||
}; // 338
|
|
||||||
// 339
|
|
||||||
HTTP.methods( // 340
|
|
||||||
{'/cfs/servertime': { // 341
|
|
||||||
get: function(data) { // 342
|
|
||||||
return Date.now().toString(); // 343
|
|
||||||
} // 344
|
|
||||||
} // 345
|
|
||||||
}); // 346
|
|
||||||
// 347
|
|
||||||
// Unify client / server api // 348
|
|
||||||
FS.HTTP.now = function() { // 349
|
|
||||||
return Date.now(); // 350
|
|
||||||
}; // 351
|
|
||||||
// 352
|
|
||||||
// Start up the basic mount points // 353
|
|
||||||
Meteor.startup(function () { // 354
|
|
||||||
mountUrls(); // 355
|
|
||||||
}); // 356
|
|
||||||
// 357
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
}).call(this);
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
}).call(this);
|
|
||||||
|
|
||||||
|
|
||||||
/* Exports */
|
|
||||||
if (typeof Package === 'undefined') Package = {};
|
|
||||||
Package['wekan-cfs-access-point'] = {};
|
|
||||||
|
|
||||||
})();
|
|
|
@ -153,12 +153,14 @@ if (Meteor.isServer) {
|
||||||
}
|
}
|
||||||
if (activity.listId) {
|
if (activity.listId) {
|
||||||
const list = activity.list();
|
const list = activity.list();
|
||||||
|
if (list) {
|
||||||
if (list.watchers !== undefined) {
|
if (list.watchers !== undefined) {
|
||||||
watchers = _.union(watchers, list.watchers || []);
|
watchers = _.union(watchers, list.watchers || []);
|
||||||
}
|
}
|
||||||
params.list = list.title;
|
params.list = list.title;
|
||||||
params.listId = activity.listId;
|
params.listId = activity.listId;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (activity.oldListId) {
|
if (activity.oldListId) {
|
||||||
const oldList = activity.oldList();
|
const oldList = activity.oldList();
|
||||||
if (oldList) {
|
if (oldList) {
|
||||||
|
@ -242,7 +244,7 @@ if (Meteor.isServer) {
|
||||||
}
|
}
|
||||||
if (activity.attachmentId) {
|
if (activity.attachmentId) {
|
||||||
const attachment = activity.attachment();
|
const attachment = activity.attachment();
|
||||||
params.attachment = attachment.original.name;
|
params.attachment = attachment.name;
|
||||||
params.attachmentId = attachment._id;
|
params.attachmentId = attachment._id;
|
||||||
}
|
}
|
||||||
if (activity.checklistId) {
|
if (activity.checklistId) {
|
||||||
|
|
|
@ -1,267 +1,95 @@
|
||||||
export const AttachmentStorage = new Mongo.Collection(
|
import { Meteor } from 'meteor/meteor';
|
||||||
'cfs_gridfs.attachments.files',
|
import { FilesCollection } from 'meteor/ostrio:files';
|
||||||
);
|
import fs from 'fs';
|
||||||
export const AvatarStorage = new Mongo.Collection('cfs_gridfs.avatars.files');
|
import path from 'path';
|
||||||
|
import { createBucket } from './lib/grid/createBucket';
|
||||||
const localFSStore = process.env.ATTACHMENTS_STORE_PATH;
|
import { createOnAfterUpload } from './lib/fsHooks/createOnAfterUpload';
|
||||||
const storeName = 'attachments';
|
import { createInterceptDownload } from './lib/fsHooks/createInterceptDownload';
|
||||||
const defaultStoreOptions = {
|
import { createOnAfterRemove } from './lib/fsHooks/createOnAfterRemove';
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Attachments = new FS.Collection('attachments', {
|
|
||||||
stores: [store],
|
|
||||||
});
|
|
||||||
|
|
||||||
|
let attachmentBucket;
|
||||||
if (Meteor.isServer) {
|
if (Meteor.isServer) {
|
||||||
Meteor.startup(() => {
|
attachmentBucket = createBucket('attachments');
|
||||||
Attachments.files._ensureIndex({ cardId: 1 });
|
}
|
||||||
|
|
||||||
|
const insertActivity = (fileObj, activityType) =>
|
||||||
|
Activities.insert({
|
||||||
|
userId: fileObj.userId,
|
||||||
|
type: 'card',
|
||||||
|
activityType,
|
||||||
|
attachmentId: fileObj._id,
|
||||||
|
// this preserves the name so that notifications can be meaningful after
|
||||||
|
// this file is removed
|
||||||
|
attachmentName: fileObj.name,
|
||||||
|
boardId: fileObj.meta.boardId,
|
||||||
|
cardId: fileObj.meta.cardId,
|
||||||
|
listId: fileObj.meta.listId,
|
||||||
|
swimlaneId: fileObj.meta.swimlaneId,
|
||||||
});
|
});
|
||||||
|
|
||||||
Attachments.allow({
|
// XXX Enforce a schema for the Attachments FilesCollection
|
||||||
insert(userId, doc) {
|
// see: https://github.com/VeliovGroup/Meteor-Files/wiki/Schema
|
||||||
return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
|
|
||||||
|
Attachments = new FilesCollection({
|
||||||
|
debug: false, // Change to `true` for debugging
|
||||||
|
collectionName: 'attachments',
|
||||||
|
allowClientCode: true,
|
||||||
|
storagePath() {
|
||||||
|
if (process.env.WRITABLE_PATH) {
|
||||||
|
return path.join(process.env.WRITABLE_PATH, 'uploads', 'attachments');
|
||||||
|
}
|
||||||
|
return path.normalize(`assets/app/uploads/${this.collectionName}`);
|
||||||
},
|
},
|
||||||
update(userId, doc) {
|
onAfterUpload: function onAfterUpload(fileRef) {
|
||||||
return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
|
createOnAfterUpload(attachmentBucket).call(this, fileRef);
|
||||||
|
// If the attachment doesn't have a source field
|
||||||
|
// or its source is different than import
|
||||||
|
if (!fileRef.meta.source || fileRef.meta.source !== 'import') {
|
||||||
|
// Add activity about adding the attachment
|
||||||
|
insertActivity(fileRef, 'addAttachment');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
remove(userId, doc) {
|
interceptDownload: createInterceptDownload(attachmentBucket),
|
||||||
return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
|
onAfterRemove: function onAfterRemove(files) {
|
||||||
|
createOnAfterRemove(attachmentBucket).call(this, files);
|
||||||
|
files.forEach(fileObj => {
|
||||||
|
insertActivity(fileObj, 'deleteAttachment');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
// We authorize the attachment download either:
|
// We authorize the attachment download either:
|
||||||
// - if the board is public, everyone (even unconnected) can download it
|
// - if the board is public, everyone (even unconnected) can download it
|
||||||
// - if the board is private, only board members can download it
|
// - if the board is private, only board members can download it
|
||||||
download(userId, doc) {
|
protected(fileObj) {
|
||||||
const board = Boards.findOne(doc.boardId);
|
const board = Boards.findOne(fileObj.meta.boardId);
|
||||||
if (board.isPublic()) {
|
if (board.isPublic()) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
return board.hasMember(userId);
|
|
||||||
}
|
}
|
||||||
|
return board.hasMember(this.userId);
|
||||||
},
|
},
|
||||||
|
});
|
||||||
fetch: ['boardId'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// XXX Enforce a schema for the Attachments CollectionFS
|
|
||||||
|
|
||||||
if (Meteor.isServer) {
|
if (Meteor.isServer) {
|
||||||
Attachments.files.after.insert((userId, doc) => {
|
Attachments.allow({
|
||||||
// If the attachment doesn't have a source field
|
insert(userId, fileObj) {
|
||||||
// or its source is different than import
|
return allowIsBoardMember(userId, Boards.findOne(fileObj.boardId));
|
||||||
if (!doc.source || doc.source !== 'import') {
|
|
||||||
// Add activity about adding the attachment
|
|
||||||
Activities.insert({
|
|
||||||
userId,
|
|
||||||
type: 'card',
|
|
||||||
activityType: 'addAttachment',
|
|
||||||
attachmentId: doc._id,
|
|
||||||
// this preserves the name so that notifications can be meaningful after
|
|
||||||
// this file is removed
|
|
||||||
attachmentName: doc.original.name,
|
|
||||||
boardId: doc.boardId,
|
|
||||||
cardId: doc.cardId,
|
|
||||||
listId: doc.listId,
|
|
||||||
swimlaneId: doc.swimlaneId,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Don't add activity about adding the attachment as the activity
|
|
||||||
// be imported and delete source field
|
|
||||||
Attachments.update(
|
|
||||||
{
|
|
||||||
_id: doc._id,
|
|
||||||
},
|
},
|
||||||
{
|
update(userId, fileObj) {
|
||||||
$unset: {
|
return allowIsBoardMember(userId, Boards.findOne(fileObj.boardId));
|
||||||
source: '',
|
|
||||||
},
|
},
|
||||||
|
remove(userId, fileObj) {
|
||||||
|
return allowIsBoardMember(userId, Boards.findOne(fileObj.boardId));
|
||||||
},
|
},
|
||||||
);
|
fetch: ['meta'],
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Attachments.files.before.remove((userId, doc) => {
|
Meteor.startup(() => {
|
||||||
Activities.insert({
|
Attachments.collection._ensureIndex({ cardId: 1 });
|
||||||
userId,
|
const storagePath = Attachments.storagePath();
|
||||||
type: 'card',
|
console.log("Meteor.startup check storagePath: ", storagePath);
|
||||||
activityType: 'deleteAttachment',
|
if (!fs.existsSync(storagePath)) {
|
||||||
attachmentId: doc._id,
|
console.log("create storagePath because it doesn't exist: " + storagePath);
|
||||||
// this preserves the name so that notifications can be meaningful after
|
fs.mkdirSync(storagePath, { recursive: true });
|
||||||
// this file is removed
|
}
|
||||||
attachmentName: doc.original.name,
|
|
||||||
boardId: doc.boardId,
|
|
||||||
cardId: doc.cardId,
|
|
||||||
listId: doc.listId,
|
|
||||||
swimlaneId: doc.swimlaneId,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
116
models/attachments_old.js
Normal file
116
models/attachments_old.js
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
const storeName = 'attachments';
|
||||||
|
const defaultStoreOptions = {
|
||||||
|
beforeWrite: fileObj => {
|
||||||
|
if (!fileObj.isImage()) {
|
||||||
|
return {
|
||||||
|
type: 'application/octet-stream',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let store;
|
||||||
|
store = new FS.Store.GridFS(storeName, {
|
||||||
|
// XXX Add a new store for cover thumbnails so we don't load big images in
|
||||||
|
// the general board view
|
||||||
|
// If the uploaded document is not an image we need to enforce browser
|
||||||
|
// download instead of execution. This is particularly important for HTML
|
||||||
|
// files that the browser will just execute if we don't serve them with the
|
||||||
|
// appropriate `application/octet-stream` MIME header which can lead to user
|
||||||
|
// data leaks. I imagine other formats (like PDF) can also be attack vectors.
|
||||||
|
// See https://github.com/wekan/wekan/issues/99
|
||||||
|
// XXX Should we use `beforeWrite` option of CollectionFS instead of
|
||||||
|
// collection-hooks?
|
||||||
|
// We should use `beforeWrite`.
|
||||||
|
...defaultStoreOptions,
|
||||||
|
});
|
||||||
|
AttachmentsOld = new FS.Collection('attachments', {
|
||||||
|
stores: [store],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Meteor.isServer) {
|
||||||
|
Meteor.startup(() => {
|
||||||
|
AttachmentsOld.files._ensureIndex({ cardId: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
AttachmentsOld.allow({
|
||||||
|
insert(userId, doc) {
|
||||||
|
return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
|
||||||
|
},
|
||||||
|
update(userId, doc) {
|
||||||
|
return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
|
||||||
|
},
|
||||||
|
remove(userId, doc) {
|
||||||
|
return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
|
||||||
|
},
|
||||||
|
// We authorize the attachment download either:
|
||||||
|
// - if the board is public, everyone (even unconnected) can download it
|
||||||
|
// - if the board is private, only board members can download it
|
||||||
|
download(userId, doc) {
|
||||||
|
const board = Boards.findOne(doc.boardId);
|
||||||
|
if (board.isPublic()) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return board.hasMember(userId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetch: ['boardId'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX Enforce a schema for the AttachmentsOld CollectionFS
|
||||||
|
|
||||||
|
if (Meteor.isServer) {
|
||||||
|
AttachmentsOld.files.after.insert((userId, doc) => {
|
||||||
|
// If the attachment doesn't have a source field
|
||||||
|
// or its source is different than import
|
||||||
|
if (!doc.source || doc.source !== 'import') {
|
||||||
|
// Add activity about adding the attachment
|
||||||
|
Activities.insert({
|
||||||
|
userId,
|
||||||
|
type: 'card',
|
||||||
|
activityType: 'addAttachment',
|
||||||
|
attachmentId: doc._id,
|
||||||
|
// this preserves the name so that notifications can be meaningful after
|
||||||
|
// this file is removed
|
||||||
|
attachmentName: doc.original.name,
|
||||||
|
boardId: doc.boardId,
|
||||||
|
cardId: doc.cardId,
|
||||||
|
listId: doc.listId,
|
||||||
|
swimlaneId: doc.swimlaneId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Don't add activity about adding the attachment as the activity
|
||||||
|
// be imported and delete source field
|
||||||
|
AttachmentsOld.update(
|
||||||
|
{
|
||||||
|
_id: doc._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$unset: {
|
||||||
|
source: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AttachmentsOld.files.before.remove((userId, doc) => {
|
||||||
|
Activities.insert({
|
||||||
|
userId,
|
||||||
|
type: 'card',
|
||||||
|
activityType: 'deleteAttachment',
|
||||||
|
attachmentId: doc._id,
|
||||||
|
// this preserves the name so that notifications can be meaningful after
|
||||||
|
// this file is removed
|
||||||
|
attachmentName: doc.original.name,
|
||||||
|
boardId: doc.boardId,
|
||||||
|
cardId: doc.cardId,
|
||||||
|
listId: doc.listId,
|
||||||
|
swimlaneId: doc.swimlaneId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AttachmentsOld;
|
|
@ -1,29 +1,57 @@
|
||||||
Avatars = new FS.Collection('avatars', {
|
import { Meteor } from 'meteor/meteor';
|
||||||
stores: [new FS.Store.GridFS('avatars')],
|
import { FilesCollection } from 'meteor/ostrio:files';
|
||||||
filter: {
|
import fs from 'fs';
|
||||||
maxSize: 520000,
|
import path from 'path';
|
||||||
allow: {
|
import { createBucket } from './lib/grid/createBucket';
|
||||||
contentTypes: ['image/*'],
|
import { createOnAfterUpload } from './lib/fsHooks/createOnAfterUpload';
|
||||||
},
|
import { createInterceptDownload } from './lib/fsHooks/createInterceptDownload';
|
||||||
},
|
import { createOnAfterRemove } from './lib/fsHooks/createOnAfterRemove';
|
||||||
});
|
|
||||||
|
|
||||||
function isOwner(userId, file) {
|
let avatarsBucket;
|
||||||
return userId && userId === file.userId;
|
if (Meteor.isServer) {
|
||||||
|
avatarsBucket = createBucket('avatars');
|
||||||
}
|
}
|
||||||
|
|
||||||
Avatars.allow({
|
Avatars = new FilesCollection({
|
||||||
|
debug: false, // Change to `true` for debugging
|
||||||
|
collectionName: 'avatars',
|
||||||
|
allowClientCode: true,
|
||||||
|
storagePath() {
|
||||||
|
if (process.env.WRITABLE_PATH) {
|
||||||
|
return path.join(process.env.WRITABLE_PATH, 'uploads', 'avatars');
|
||||||
|
}
|
||||||
|
return path.normalize(`assets/app/uploads/${this.collectionName}`);;
|
||||||
|
},
|
||||||
|
onBeforeUpload(file) {
|
||||||
|
if (file.size <= 72000 && file.type.startsWith('image/')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return 'avatar-too-big';
|
||||||
|
},
|
||||||
|
onAfterUpload: createOnAfterUpload(avatarsBucket),
|
||||||
|
interceptDownload: createInterceptDownload(avatarsBucket),
|
||||||
|
onAfterRemove: createOnAfterRemove(avatarsBucket),
|
||||||
|
});
|
||||||
|
|
||||||
|
function isOwner(userId, doc) {
|
||||||
|
return userId && userId === doc.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Meteor.isServer) {
|
||||||
|
Avatars.allow({
|
||||||
insert: isOwner,
|
insert: isOwner,
|
||||||
update: isOwner,
|
update: isOwner,
|
||||||
remove: isOwner,
|
remove: isOwner,
|
||||||
download() {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
fetch: ['userId'],
|
fetch: ['userId'],
|
||||||
});
|
});
|
||||||
|
|
||||||
Avatars.files.before.insert((userId, doc) => {
|
Meteor.startup(() => {
|
||||||
doc.userId = userId;
|
const storagePath = Avatars.storagePath();
|
||||||
});
|
if (!fs.existsSync(storagePath)) {
|
||||||
|
console.log("create storagePath because it doesn't exist: " + storagePath);
|
||||||
|
fs.mkdirSync(storagePath, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default Avatars;
|
export default Avatars;
|
||||||
|
|
29
models/avatars_old.js
Normal file
29
models/avatars_old.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
AvatarsOld = new FS.Collection('avatars', {
|
||||||
|
stores: [new FS.Store.GridFS('avatars')],
|
||||||
|
filter: {
|
||||||
|
maxSize: 72000,
|
||||||
|
allow: {
|
||||||
|
contentTypes: ['image/*'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function isOwner(userId, file) {
|
||||||
|
return userId && userId === file.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
AvatarsOld.allow({
|
||||||
|
insert: isOwner,
|
||||||
|
update: isOwner,
|
||||||
|
remove: isOwner,
|
||||||
|
download() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
fetch: ['userId'],
|
||||||
|
});
|
||||||
|
|
||||||
|
AvatarsOld.files.before.insert((userId, doc) => {
|
||||||
|
doc.userId = userId;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AvatarsOld;
|
|
@ -8,6 +8,7 @@ import {
|
||||||
import Users from "./users";
|
import Users from "./users";
|
||||||
|
|
||||||
const escapeForRegex = require('escape-string-regexp');
|
const escapeForRegex = require('escape-string-regexp');
|
||||||
|
|
||||||
Boards = new Mongo.Collection('boards');
|
Boards = new Mongo.Collection('boards');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -737,14 +737,14 @@ Cards.helpers({
|
||||||
attachments() {
|
attachments() {
|
||||||
if (this.isLinkedCard()) {
|
if (this.isLinkedCard()) {
|
||||||
return Attachments.find(
|
return Attachments.find(
|
||||||
{ cardId: this.linkedId },
|
{ 'meta.cardId': this.linkedId },
|
||||||
{ sort: { uploadedAt: -1 } },
|
{ sort: { uploadedAt: -1 } },
|
||||||
);
|
).each();
|
||||||
} else {
|
} else {
|
||||||
return Attachments.find(
|
return Attachments.find(
|
||||||
{ cardId: this._id },
|
{ 'meta.cardId': this._id },
|
||||||
{ sort: { uploadedAt: -1 } },
|
{ sort: { uploadedAt: -1 } },
|
||||||
);
|
).each();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -753,7 +753,7 @@ Cards.helpers({
|
||||||
const cover = Attachments.findOne(this.coverId);
|
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
|
// 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?
|
// todo XXX we could return a default "upload pending" image in the meantime?
|
||||||
return cover && cover.url() && cover;
|
return cover && cover.link() && cover;
|
||||||
},
|
},
|
||||||
|
|
||||||
checklists() {
|
checklists() {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
const Papa = require('papaparse');
|
const Papa = require('papaparse');
|
||||||
|
|
||||||
|
//const stringify = require('csv-stringify');
|
||||||
|
|
||||||
// exporter maybe is broken since Gridfs introduced, add fs and path
|
// exporter maybe is broken since Gridfs introduced, add fs and path
|
||||||
export class Exporter {
|
export class Exporter {
|
||||||
constructor(boardId, attachmentId) {
|
constructor(boardId, attachmentId) {
|
||||||
|
@ -78,11 +80,11 @@ export class Exporter {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_id: attachment._id,
|
_id: attachment._id,
|
||||||
cardId: attachment.cardId,
|
cardId: attachment.meta.cardId,
|
||||||
//url: FlowRouter.url(attachment.url()),
|
//url: FlowRouter.url(attachment.url()),
|
||||||
file: filebase64,
|
file: filebase64,
|
||||||
name: attachment.original.name,
|
name: attachment.name,
|
||||||
type: attachment.original.type,
|
type: attachment.type,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
//When has a especific valid attachment return the single element
|
//When has a especific valid attachment return the single element
|
||||||
|
|
47
models/lib/fsHooks/createInterceptDownload.js
Normal file
47
models/lib/fsHooks/createInterceptDownload.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { createObjectId } from '../grid/createObjectId';
|
||||||
|
|
||||||
|
export const createInterceptDownload = bucket =>
|
||||||
|
function interceptDownload(http, file, versionName) {
|
||||||
|
const { gridFsFileId } = file.versions[versionName].meta || {};
|
||||||
|
if (gridFsFileId) {
|
||||||
|
// opens the download stream using a given gfs id
|
||||||
|
// see: http://mongodb.github.io/node-mongodb-native/3.2/api/GridFSBucket.html#openDownloadStream
|
||||||
|
const gfsId = createObjectId({ gridFsFileId });
|
||||||
|
const readStream = bucket.openDownloadStream(gfsId);
|
||||||
|
|
||||||
|
readStream.on('data', data => {
|
||||||
|
http.response.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
readStream.on('end', () => {
|
||||||
|
http.response.end(); // don't pass parameters to end() or it will be attached to the file's binary stream
|
||||||
|
});
|
||||||
|
|
||||||
|
readStream.on('error', () => {
|
||||||
|
// not found probably
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
http.response.statusCode = 404;
|
||||||
|
http.response.end('not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
http.response.setHeader('Cache-Control', this.cacheControl);
|
||||||
|
http.response.setHeader(
|
||||||
|
'Content-Disposition',
|
||||||
|
getContentDisposition(file.name, http?.params?.query?.download),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Boolean(gridFsFileId); // Serve file from either GridFS or FS if it wasn't uploaded yet
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will initiate download, if links are called with ?download="true" queryparam.
|
||||||
|
**/
|
||||||
|
const getContentDisposition = (name, downloadFlag) => {
|
||||||
|
const dispositionType = downloadFlag === 'true' ? 'attachment;' : 'inline;';
|
||||||
|
|
||||||
|
const encodedName = encodeURIComponent(name);
|
||||||
|
const dispositionName = `filename="${encodedName}"; filename=*UTF-8"${encodedName}";`;
|
||||||
|
const dispositionEncoding = 'charset=utf-8';
|
||||||
|
|
||||||
|
return `${dispositionType} ${dispositionName} ${dispositionEncoding}`;
|
||||||
|
};
|
17
models/lib/fsHooks/createOnAfterRemove.js
Normal file
17
models/lib/fsHooks/createOnAfterRemove.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { createObjectId } from '../grid/createObjectId';
|
||||||
|
|
||||||
|
export const createOnAfterRemove = bucket =>
|
||||||
|
function onAfterRemove(files) {
|
||||||
|
files.forEach(file => {
|
||||||
|
Object.keys(file.versions).forEach(versionName => {
|
||||||
|
const gridFsFileId = (file.versions[versionName].meta || {})
|
||||||
|
.gridFsFileId;
|
||||||
|
if (gridFsFileId) {
|
||||||
|
const gfsId = createObjectId({ gridFsFileId });
|
||||||
|
bucket.delete(gfsId, err => {
|
||||||
|
// if (err) console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
51
models/lib/fsHooks/createOnAfterUpload.js
Normal file
51
models/lib/fsHooks/createOnAfterUpload.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
export const createOnAfterUpload = bucket =>
|
||||||
|
function onAfterUpload(file) {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
// here you could manipulate your file
|
||||||
|
// and create a new version, for example a scaled 'thumbnail'
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// then we read all versions we have got so far
|
||||||
|
Object.keys(file.versions).forEach(versionName => {
|
||||||
|
const metadata = { ...file.meta, versionName, fileId: file._id };
|
||||||
|
fs.createReadStream(file.versions[versionName].path)
|
||||||
|
|
||||||
|
// this is where we upload the binary to the bucket using bucket.openUploadStream
|
||||||
|
// see http://mongodb.github.io/node-mongodb-native/3.2/api/GridFSBucket.html#openUploadStream
|
||||||
|
.pipe(
|
||||||
|
bucket.openUploadStream(file.name, {
|
||||||
|
contentType: file.type || 'binary/octet-stream',
|
||||||
|
metadata,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// and we unlink the file from the fs on any error
|
||||||
|
// that occurred during the upload to prevent zombie files
|
||||||
|
.on('error', err => {
|
||||||
|
console.error("[createOnAfterUpload error]", err);
|
||||||
|
self.unlink(this.collection.findOne(file._id), versionName); // Unlink files from FS
|
||||||
|
})
|
||||||
|
|
||||||
|
// once we are finished, we attach the gridFS Object id on the
|
||||||
|
// FilesCollection document's meta section and finally unlink the
|
||||||
|
// upload file from the filesystem
|
||||||
|
.on(
|
||||||
|
'finish',
|
||||||
|
Meteor.bindEnvironment(ver => {
|
||||||
|
const property = `versions.${versionName}.meta.gridFsFileId`;
|
||||||
|
|
||||||
|
self.collection.update(file._id, {
|
||||||
|
$set: {
|
||||||
|
[property]: ver._id.toHexString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
self.unlink(this.collection.findOne(file._id), versionName); // Unlink files from FS
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
9
models/lib/grid/createBucket.js
Normal file
9
models/lib/grid/createBucket.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { MongoInternals } from 'meteor/mongo';
|
||||||
|
|
||||||
|
export const createBucket = bucketName => {
|
||||||
|
const options = bucketName ? { bucketName } : void 0;
|
||||||
|
return new MongoInternals.NpmModule.GridFSBucket(
|
||||||
|
MongoInternals.defaultRemoteCollectionDriver().mongo.db,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
};
|
4
models/lib/grid/createObjectId.js
Normal file
4
models/lib/grid/createObjectId.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { MongoInternals } from 'meteor/mongo';
|
||||||
|
|
||||||
|
export const createObjectId = ({ gridFsFileId }) =>
|
||||||
|
new MongoInternals.NpmModule.ObjectID(gridFsFileId);
|
|
@ -422,47 +422,35 @@ export class TrelloCreator {
|
||||||
}
|
}
|
||||||
const attachments = this.attachments[card.id];
|
const attachments = this.attachments[card.id];
|
||||||
const trelloCoverId = card.idAttachmentCover;
|
const trelloCoverId = card.idAttachmentCover;
|
||||||
if (attachments) {
|
if (attachments && Meteor.isServer) {
|
||||||
const links = [];
|
|
||||||
attachments.forEach(att => {
|
attachments.forEach(att => {
|
||||||
// if the attachment `name` and `url` are the same, then the
|
|
||||||
// attachment is an attached link
|
|
||||||
if (att.name === att.url) {
|
|
||||||
links.push(att.url);
|
|
||||||
} else {
|
|
||||||
const file = new FS.File();
|
|
||||||
// Simulating file.attachData on the client generates multiple errors
|
|
||||||
// - HEAD returns null, which causes exception down the line
|
|
||||||
// - the template then tries to display the url to the attachment which causes other errors
|
|
||||||
// so we make it server only, and let UI catch up once it is done, forget about latency comp.
|
|
||||||
const self = this;
|
const self = this;
|
||||||
if (Meteor.isServer) {
|
const opts = {
|
||||||
file.attachData(att.url, function(error) {
|
type: att.type ? att.type : undefined,
|
||||||
file.boardId = boardId;
|
userId: self._user(att.userId),
|
||||||
file.cardId = cardId;
|
meta: {
|
||||||
file.userId = self._user(att.idMemberCreator);
|
boardId,
|
||||||
// The field source will only be used to prevent adding
|
cardId,
|
||||||
// attachments' related activities automatically
|
source: 'import',
|
||||||
file.source = 'import';
|
},
|
||||||
|
};
|
||||||
|
const cb = (error, fileObj) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error;
|
throw error;
|
||||||
} else {
|
}
|
||||||
const wekanAtt = Attachments.insert(file, () => {
|
self.attachmentIds[att._id] = fileObj._id;
|
||||||
// we do nothing
|
if (trelloCoverId === att._id) {
|
||||||
});
|
|
||||||
self.attachmentIds[att.id] = wekanAtt._id;
|
|
||||||
//
|
|
||||||
if (trelloCoverId === att.id) {
|
|
||||||
Cards.direct.update(cardId, {
|
Cards.direct.update(cardId, {
|
||||||
$set: { coverId: wekanAtt._id },
|
$set: { coverId: fileObj._id },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
if (att.url) {
|
||||||
|
Attachment.load(att.url, opts, cb, true);
|
||||||
|
} else if (att.file) {
|
||||||
|
Attachment.write(att.file, opts, cb, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
// todo XXX set cover - if need be
|
|
||||||
});
|
|
||||||
|
|
||||||
if (links.length) {
|
if (links.length) {
|
||||||
let desc = cardToCreate.description.trim();
|
let desc = cardToCreate.description.trim();
|
||||||
|
|
|
@ -444,81 +444,34 @@ export class WekanCreator {
|
||||||
}
|
}
|
||||||
const attachments = this.attachments[card._id];
|
const attachments = this.attachments[card._id];
|
||||||
const wekanCoverId = card.coverId;
|
const wekanCoverId = card.coverId;
|
||||||
if (attachments) {
|
if (attachments && Meteor.isServer) {
|
||||||
attachments.forEach(att => {
|
attachments.forEach(att => {
|
||||||
const file = new FS.File();
|
|
||||||
// Simulating file.attachData on the client generates multiple errors
|
|
||||||
// - HEAD returns null, which causes exception down the line
|
|
||||||
// - the template then tries to display the url to the attachment which causes other errors
|
|
||||||
// so we make it server only, and let UI catch up once it is done, forget about latency comp.
|
|
||||||
const self = this;
|
const self = this;
|
||||||
if (Meteor.isServer) {
|
const opts = {
|
||||||
|
type: att.type ? att.type : undefined,
|
||||||
|
userId: self._user(att.userId),
|
||||||
|
meta: {
|
||||||
|
boardId,
|
||||||
|
cardId,
|
||||||
|
source: 'import',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const cb = (error, fileObj) => {
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
self.attachmentIds[att._id] = fileObj._id;
|
||||||
|
if (wekanCoverId === att._id) {
|
||||||
|
Cards.direct.update(cardId, {
|
||||||
|
$set: { coverId: fileObj._id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
if (att.url) {
|
if (att.url) {
|
||||||
file.attachData(att.url, function(error) {
|
Attachment.load(att.url, opts, cb, true);
|
||||||
file.boardId = boardId;
|
|
||||||
file.cardId = cardId;
|
|
||||||
file.userId = self._user(att.userId);
|
|
||||||
// The field source will only be used to prevent adding
|
|
||||||
// attachments' related activities automatically
|
|
||||||
file.source = 'import';
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
} else {
|
|
||||||
const wekanAtt = Attachments.insert(file, () => {
|
|
||||||
// we do nothing
|
|
||||||
});
|
|
||||||
self.attachmentIds[att._id] = wekanAtt._id;
|
|
||||||
//
|
|
||||||
if (wekanCoverId === att._id) {
|
|
||||||
Cards.direct.update(cardId, {
|
|
||||||
$set: {
|
|
||||||
coverId: wekanAtt._id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (att.file) {
|
} else if (att.file) {
|
||||||
//If attribute type is null or empty string is set, assume binary stream
|
Attachment.write(att.file, opts, cb, true);
|
||||||
att.type =
|
|
||||||
!att.type || att.type.trim().length === 0
|
|
||||||
? 'application/octet-stream'
|
|
||||||
: att.type;
|
|
||||||
|
|
||||||
file.attachData(
|
|
||||||
Buffer.from(att.file, 'base64'),
|
|
||||||
{
|
|
||||||
type: att.type,
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
file.name(att.name);
|
|
||||||
file.boardId = boardId;
|
|
||||||
file.cardId = cardId;
|
|
||||||
file.userId = self._user(att.userId);
|
|
||||||
// The field source will only be used to prevent adding
|
|
||||||
// attachments' related activities automatically
|
|
||||||
file.source = 'import';
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
} else {
|
|
||||||
const wekanAtt = Attachments.insert(file, () => {
|
|
||||||
// we do nothing
|
|
||||||
});
|
|
||||||
this.attachmentIds[att._id] = wekanAtt._id;
|
|
||||||
//
|
|
||||||
if (wekanCoverId === att._id) {
|
|
||||||
Cards.direct.update(cardId, {
|
|
||||||
$set: {
|
|
||||||
coverId: wekanAtt._id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// todo XXX set cover - if need be
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
result.push(cardId);
|
result.push(cardId);
|
||||||
|
|
|
@ -52,7 +52,6 @@ REM del /S /F /Q node_modules
|
||||||
call meteor npm install
|
call meteor npm install
|
||||||
REM del /S /F /Q .build
|
REM del /S /F /Q .build
|
||||||
call meteor build .build --directory
|
call meteor build .build --directory
|
||||||
copy fix-download-unicode\cfs_access-point.txt .build\bundle\programs\server\packages\cfs_access-point.js
|
|
||||||
REM ## Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
REM ## Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
||||||
del /S /F /Q rm .build/bundle/programs/web.browser.legacy
|
del /S /F /Q rm .build/bundle/programs/web.browser.legacy
|
||||||
REM ## Install some NPM packages
|
REM ## Install some NPM packages
|
||||||
|
|
|
@ -10,7 +10,6 @@ rm -rf node_modules
|
||||||
meteor npm install
|
meteor npm install
|
||||||
rm -rf .build
|
rm -rf .build
|
||||||
METEOR_PROFILE=100 meteor build .build --directory
|
METEOR_PROFILE=100 meteor build .build --directory
|
||||||
cp -f fix-download-unicode/cfs_access-point.txt .build/bundle/programs/server/packages/cfs_access-point.js
|
|
||||||
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
||||||
rm -rf .build/bundle/programs/web.browser.legacy
|
rm -rf .build/bundle/programs/web.browser.legacy
|
||||||
cd .build/bundle/programs/server
|
cd .build/bundle/programs/server
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
import AccountSettings from '../models/accountSettings';
|
import AccountSettings from '../models/accountSettings';
|
||||||
import TableVisibilityModeSettings from '../models/tableVisibilityModeSettings';
|
import TableVisibilityModeSettings from '../models/tableVisibilityModeSettings';
|
||||||
import Actions from '../models/actions';
|
import Actions from '../models/actions';
|
||||||
import Activities from '../models/activities';
|
import Activities from '../models/activities';
|
||||||
import Announcements from '../models/announcements';
|
import Announcements from '../models/announcements';
|
||||||
|
import Attachments from '../models/attachments';
|
||||||
|
import AttachmentsOld from '../models/attachments_old';
|
||||||
|
import Avatars from '../models/avatars';
|
||||||
|
import AvatarsOld from '../models/avatars_old';
|
||||||
import Boards from '../models/boards';
|
import Boards from '../models/boards';
|
||||||
import CardComments from '../models/cardComments';
|
import CardComments from '../models/cardComments';
|
||||||
import Cards from '../models/cards';
|
import Cards from '../models/cards';
|
||||||
|
@ -1119,3 +1125,152 @@ Migrations.add('add-card-details-show-lists', () => {
|
||||||
noValidateMulti,
|
noValidateMulti,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Migrations.add('migrate-attachments-collectionFS-to-ostrioFiles', () => {
|
||||||
|
const storagePath = Attachments.storagePath();
|
||||||
|
if (!fs.existsSync(storagePath)) {
|
||||||
|
console.log("create storagePath because it doesn't exist: " + storagePath);
|
||||||
|
fs.mkdirSync(storagePath, { recursive: true });
|
||||||
|
}
|
||||||
|
AttachmentsOld.find().forEach(function(fileObj) {
|
||||||
|
const newFileName = fileObj.name();
|
||||||
|
const filePath = path.join(storagePath, `${fileObj._id}-${newFileName}`);
|
||||||
|
|
||||||
|
// This is "example" variable, change it to the userId that you might be using.
|
||||||
|
const userId = fileObj.userId;
|
||||||
|
|
||||||
|
const fileType = fileObj.type();
|
||||||
|
const fileSize = fileObj.size();
|
||||||
|
const fileId = fileObj._id;
|
||||||
|
|
||||||
|
const readStream = fileObj.createReadStream('attachments');
|
||||||
|
const writeStream = fs.createWriteStream(filePath);
|
||||||
|
|
||||||
|
writeStream.on('error', error => {
|
||||||
|
console.error('[writeStream error]: ', error, filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
readStream.on('error', error => {
|
||||||
|
console.error('[readStream error]: ', error, filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Once we have a file, then upload it to our new data storage
|
||||||
|
readStream.on('end', () => {
|
||||||
|
console.log('Ended: ', filePath);
|
||||||
|
// UserFiles is the new Meteor-Files/FilesCollection collection instance
|
||||||
|
|
||||||
|
Attachments.addFile(
|
||||||
|
filePath,
|
||||||
|
{
|
||||||
|
fileName: newFileName,
|
||||||
|
type: fileType,
|
||||||
|
meta: {
|
||||||
|
boardId: fileObj.boardId,
|
||||||
|
cardId: fileObj.cardId,
|
||||||
|
listId: fileObj.listId,
|
||||||
|
swimlaneId: fileObj.swimlaneId,
|
||||||
|
source: 'import,'
|
||||||
|
},
|
||||||
|
userId,
|
||||||
|
size: fileSize,
|
||||||
|
fileId,
|
||||||
|
},
|
||||||
|
(error, fileRef) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('[Attachments#addFile error]: ', error);
|
||||||
|
} else {
|
||||||
|
console.log('File Inserted: ', fileRef);
|
||||||
|
// Set the userId again
|
||||||
|
Attachments.update({ _id: fileRef._id }, { $set: { userId } });
|
||||||
|
fileObj.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
); // proceedAfterUpload
|
||||||
|
});
|
||||||
|
|
||||||
|
readStream.pipe(writeStream);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Migrations.add('migrate-avatars-collectionFS-to-ostrioFiles', () => {
|
||||||
|
const storagePath = Avatars.storagePath();
|
||||||
|
if (!fs.existsSync(storagePath)) {
|
||||||
|
console.log("create storagePath because it doesn't exist: " + storagePath);
|
||||||
|
fs.mkdirSync(storagePath, { recursive: true });
|
||||||
|
}
|
||||||
|
AvatarsOld.find().forEach(function(fileObj) {
|
||||||
|
const newFileName = fileObj.name();
|
||||||
|
const filePath = path.join(storagePath, `${fileObj._id}-${newFileName}`);
|
||||||
|
|
||||||
|
// This is "example" variable, change it to the userId that you might be using.
|
||||||
|
const userId = fileObj.userId;
|
||||||
|
|
||||||
|
const fileType = fileObj.type();
|
||||||
|
const fileSize = fileObj.size();
|
||||||
|
const fileId = fileObj._id;
|
||||||
|
|
||||||
|
const readStream = fileObj.createReadStream('avatars');
|
||||||
|
const writeStream = fs.createWriteStream(filePath);
|
||||||
|
|
||||||
|
writeStream.on('error', error => {
|
||||||
|
console.error('[writeStream error]: ', error, filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
readStream.on('error', error => {
|
||||||
|
console.error('[readStream error]: ', error, filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Once we have a file, then upload it to our new data storage
|
||||||
|
readStream.on('end', () => {
|
||||||
|
console.log('Ended: ', filePath);
|
||||||
|
// UserFiles is the new Meteor-Files/FilesCollection collection instance
|
||||||
|
|
||||||
|
Avatars.addFile(
|
||||||
|
filePath,
|
||||||
|
{
|
||||||
|
fileName: newFileName,
|
||||||
|
type: fileType,
|
||||||
|
meta: {
|
||||||
|
boardId: fileObj.boardId,
|
||||||
|
cardId: fileObj.cardId,
|
||||||
|
listId: fileObj.listId,
|
||||||
|
swimlaneId: fileObj.swimlaneId,
|
||||||
|
},
|
||||||
|
userId,
|
||||||
|
size: fileSize,
|
||||||
|
fileId,
|
||||||
|
},
|
||||||
|
(error, fileRef) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('[Avatars#addFile error]: ', error);
|
||||||
|
} else {
|
||||||
|
console.log('File Inserted: ', newFileName, fileRef);
|
||||||
|
// Set the userId again
|
||||||
|
Avatars.update({ _id: fileRef._id }, { $set: { userId } });
|
||||||
|
Users.find().forEach(user => {
|
||||||
|
const old_url = fileObj.url();
|
||||||
|
new_url = Avatars.findOne({ _id: fileRef._id }).link(
|
||||||
|
'original',
|
||||||
|
'/',
|
||||||
|
);
|
||||||
|
if (user.profile.avatarUrl.startsWith(old_url)) {
|
||||||
|
// Set avatar url to new url
|
||||||
|
Users.direct.update(
|
||||||
|
{ _id: user._id },
|
||||||
|
{ $set: { 'profile.avatarUrl': new_url } },
|
||||||
|
noValidate,
|
||||||
|
);
|
||||||
|
console.log('User avatar updated: ', user._id, new_url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fileObj.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true, // proceedAfterUpload
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
readStream.pipe(writeStream);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Avatars from '../../models/avatars';
|
||||||
Meteor.publish('my-avatars', function() {
|
Meteor.publish('my-avatars', function() {
|
||||||
return Avatars.find({ userId: this.userId });
|
return Avatars.find({ userId: this.userId }).cursor;
|
||||||
});
|
});
|
||||||
|
|
|
@ -233,8 +233,8 @@ Meteor.publishRelations('board', function(boardId, isArchived) {
|
||||||
cardCommentsLinkedBoard.selector = _ids => ({ boardId: _ids });
|
cardCommentsLinkedBoard.selector = _ids => ({ boardId: _ids });
|
||||||
const cardCommentReactions = this.join(CardCommentReactions);
|
const cardCommentReactions = this.join(CardCommentReactions);
|
||||||
cardCommentReactions.selector = _ids => ({ cardId: _ids });
|
cardCommentReactions.selector = _ids => ({ cardId: _ids });
|
||||||
const attachments = this.join(Attachments);
|
const attachments = this.join(Attachments.collection);
|
||||||
attachments.selector = _ids => ({ cardId: _ids });
|
attachments.selector = _ids => ({ 'meta.cardId': _ids });
|
||||||
const checklists = this.join(Checklists);
|
const checklists = this.join(Checklists);
|
||||||
checklists.selector = _ids => ({ cardId: _ids });
|
checklists.selector = _ids => ({ cardId: _ids });
|
||||||
const checklistItems = this.join(ChecklistItems);
|
const checklistItems = this.join(ChecklistItems);
|
||||||
|
|
|
@ -12,7 +12,7 @@ Meteor.publish('notificationAttachments', function() {
|
||||||
$in: activities()
|
$in: activities()
|
||||||
.map(v => v.attachmentId)
|
.map(v => v.attachmentId)
|
||||||
.filter(v => !!v),
|
.filter(v => !!v),
|
||||||
},
|
}.cursor,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# All supported keys are defined here together with descriptions and default values
|
# All supported keys are defined here together with descriptions and default values
|
||||||
|
|
||||||
# list of supported keys
|
# list of supported keys
|
||||||
keys="DEBUG MONGO_LOG_DESTINATION MONGO_URL MONGODB_BIND_UNIX_SOCKET MONGO_URL MONGODB_BIND_IP MONGODB_PORT MAIL_URL MAIL_FROM MAIL_SERVICE MAIL_SERVICE_USER MAIL_SERVICE_PASSWORD ROOT_URL PORT DISABLE_MONGODB CADDY_ENABLED CADDY_BIND_PORT WITH_API RICHER_CARD_COMMENT_EDITOR CARD_OPENED_WEBHOOK_ENABLED ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS MAX_IMAGE_PIXEL IMAGE_COMPRESS_RATIO BIGEVENTS_PATTERN NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE NOTIFY_DUE_DAYS_BEFORE_AND_AFTER NOTIFY_DUE_AT_HOUR_OF_DAY EMAIL_NOTIFICATION_TIMEOUT CORS CORS_ALLOW_HEADERS CORS_EXPOSE_HEADERS MATOMO_ADDRESS MATOMO_SITE_ID MATOMO_DO_NOT_TRACK MATOMO_WITH_USERNAME BROWSER_POLICY_ENABLED TRUSTED_URL WEBHOOKS_ATTRIBUTES OAUTH2_ENABLED OAUTH2_CA_CERT OAUTH2_LOGIN_STYLE OAUTH2_CLIENT_ID OAUTH2_SECRET OAUTH2_SERVER_URL OAUTH2_AUTH_ENDPOINT OAUTH2_USERINFO_ENDPOINT OAUTH2_TOKEN_ENDPOINT OAUTH2_ID_MAP OAUTH2_USERNAME_MAP OAUTH2_FULLNAME_MAP OAUTH2_ID_TOKEN_WHITELIST_FIELDS OAUTH2_EMAIL_MAP OAUTH2_REQUEST_PERMISSIONS OAUTH2_ADFS_ENABLED LDAP_ENABLE LDAP_PORT LDAP_HOST LDAP_AD_SIMPLE_AUTH LDAP_BASEDN LDAP_LOGIN_FALLBACK LDAP_RECONNECT LDAP_TIMEOUT LDAP_IDLE_TIMEOUT LDAP_CONNECT_TIMEOUT LDAP_AUTHENTIFICATION LDAP_AUTHENTIFICATION_USERDN LDAP_AUTHENTIFICATION_PASSWORD LDAP_LOG_ENABLED LDAP_BACKGROUND_SYNC LDAP_BACKGROUND_SYNC_INTERVAL LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS LDAP_ENCRYPTION LDAP_CA_CERT LDAP_REJECT_UNAUTHORIZED LDAP_USER_AUTHENTICATION LDAP_USER_AUTHENTICATION_FIELD LDAP_USER_SEARCH_FILTER LDAP_USER_SEARCH_SCOPE LDAP_USER_SEARCH_FIELD LDAP_SEARCH_PAGE_SIZE LDAP_SEARCH_SIZE_LIMIT LDAP_GROUP_FILTER_ENABLE LDAP_GROUP_FILTER_OBJECTCLASS LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT LDAP_GROUP_FILTER_GROUP_NAME LDAP_UNIQUE_IDENTIFIER_FIELD LDAP_UTF8_NAMES_SLUGIFY LDAP_USERNAME_FIELD LDAP_FULLNAME_FIELD LDAP_MERGE_EXISTING_USERS LDAP_SYNC_USER_DATA LDAP_SYNC_USER_DATA_FIELDMAP LDAP_SYNC_GROUP_ROLES LDAP_DEFAULT_DOMAIN LDAP_EMAIL_MATCH_ENABLE LDAP_EMAIL_MATCH_REQUIRE LDAP_EMAIL_MATCH_VERIFIED LDAP_EMAIL_FIELD LDAP_SYNC_ADMIN_STATUS LDAP_SYNC_ADMIN_GROUPS HEADER_LOGIN_ID HEADER_LOGIN_FIRSTNAME HEADER_LOGIN_LASTNAME HEADER_LOGIN_EMAIL LOGOUT_WITH_TIMER LOGOUT_IN LOGOUT_ON_HOURS LOGOUT_ON_MINUTES DEFAULT_AUTHENTICATION_METHOD ATTACHMENTS_STORE_PATH PASSWORD_LOGIN_ENABLED CAS_ENABLED CAS_BASE_URL CAS_LOGIN_URL CAS_VALIDATE_URL SAML_ENABLED SAML_PROVIDER SAML_ENTRYPOINT SAML_ISSUER SAML_CERT SAML_IDPSLO_REDIRECTURL SAML_PRIVATE_KEYFILE SAML_PUBLIC_CERTFILE SAML_IDENTIFIER_FORMAT SAML_LOCAL_PROFILE_MATCH_ATTRIBUTE SAML_ATTRIBUTES ORACLE_OIM_ENABLED RESULTS_PER_PAGE WAIT_SPINNER NODE_OPTIONS"
|
keys="DEBUG MONGO_LOG_DESTINATION MONGO_URL MONGODB_BIND_UNIX_SOCKET MONGO_URL MONGODB_BIND_IP MONGODB_PORT MAIL_URL MAIL_FROM MAIL_SERVICE MAIL_SERVICE_USER MAIL_SERVICE_PASSWORD ROOT_URL PORT DISABLE_MONGODB CADDY_ENABLED CADDY_BIND_PORT WITH_API RICHER_CARD_COMMENT_EDITOR CARD_OPENED_WEBHOOK_ENABLED ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS MAX_IMAGE_PIXEL IMAGE_COMPRESS_RATIO BIGEVENTS_PATTERN NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE NOTIFY_DUE_DAYS_BEFORE_AND_AFTER NOTIFY_DUE_AT_HOUR_OF_DAY EMAIL_NOTIFICATION_TIMEOUT CORS CORS_ALLOW_HEADERS CORS_EXPOSE_HEADERS MATOMO_ADDRESS MATOMO_SITE_ID MATOMO_DO_NOT_TRACK MATOMO_WITH_USERNAME BROWSER_POLICY_ENABLED TRUSTED_URL WEBHOOKS_ATTRIBUTES OAUTH2_ENABLED OAUTH2_CA_CERT OAUTH2_LOGIN_STYLE OAUTH2_CLIENT_ID OAUTH2_SECRET OAUTH2_SERVER_URL OAUTH2_AUTH_ENDPOINT OAUTH2_USERINFO_ENDPOINT OAUTH2_TOKEN_ENDPOINT OAUTH2_ID_MAP OAUTH2_USERNAME_MAP OAUTH2_FULLNAME_MAP OAUTH2_ID_TOKEN_WHITELIST_FIELDS OAUTH2_EMAIL_MAP OAUTH2_REQUEST_PERMISSIONS OAUTH2_ADFS_ENABLED LDAP_ENABLE LDAP_PORT LDAP_HOST LDAP_AD_SIMPLE_AUTH LDAP_BASEDN LDAP_LOGIN_FALLBACK LDAP_RECONNECT LDAP_TIMEOUT LDAP_IDLE_TIMEOUT LDAP_CONNECT_TIMEOUT LDAP_AUTHENTIFICATION LDAP_AUTHENTIFICATION_USERDN LDAP_AUTHENTIFICATION_PASSWORD LDAP_LOG_ENABLED LDAP_BACKGROUND_SYNC LDAP_BACKGROUND_SYNC_INTERVAL LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS LDAP_ENCRYPTION LDAP_CA_CERT LDAP_REJECT_UNAUTHORIZED LDAP_USER_AUTHENTICATION LDAP_USER_AUTHENTICATION_FIELD LDAP_USER_SEARCH_FILTER LDAP_USER_SEARCH_SCOPE LDAP_USER_SEARCH_FIELD LDAP_SEARCH_PAGE_SIZE LDAP_SEARCH_SIZE_LIMIT LDAP_GROUP_FILTER_ENABLE LDAP_GROUP_FILTER_OBJECTCLASS LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT LDAP_GROUP_FILTER_GROUP_NAME LDAP_UNIQUE_IDENTIFIER_FIELD LDAP_UTF8_NAMES_SLUGIFY LDAP_USERNAME_FIELD LDAP_FULLNAME_FIELD LDAP_MERGE_EXISTING_USERS LDAP_SYNC_USER_DATA LDAP_SYNC_USER_DATA_FIELDMAP LDAP_SYNC_GROUP_ROLES LDAP_DEFAULT_DOMAIN LDAP_EMAIL_MATCH_ENABLE LDAP_EMAIL_MATCH_REQUIRE LDAP_EMAIL_MATCH_VERIFIED LDAP_EMAIL_FIELD LDAP_SYNC_ADMIN_STATUS LDAP_SYNC_ADMIN_GROUPS HEADER_LOGIN_ID HEADER_LOGIN_FIRSTNAME HEADER_LOGIN_LASTNAME HEADER_LOGIN_EMAIL LOGOUT_WITH_TIMER LOGOUT_IN LOGOUT_ON_HOURS LOGOUT_ON_MINUTES DEFAULT_AUTHENTICATION_METHOD PASSWORD_LOGIN_ENABLED CAS_ENABLED CAS_BASE_URL CAS_LOGIN_URL CAS_VALIDATE_URL SAML_ENABLED SAML_PROVIDER SAML_ENTRYPOINT SAML_ISSUER SAML_CERT SAML_IDPSLO_REDIRECTURL SAML_PRIVATE_KEYFILE SAML_PUBLIC_CERTFILE SAML_IDENTIFIER_FORMAT SAML_LOCAL_PROFILE_MATCH_ATTRIBUTE SAML_ATTRIBUTES ORACLE_OIM_ENABLED RESULTS_PER_PAGE WAIT_SPINNER NODE_OPTIONS"
|
||||||
|
|
||||||
|
|
||||||
#---------------------------------------------------------------------
|
#---------------------------------------------------------------------
|
||||||
|
@ -126,10 +126,6 @@ DESCRIPTION_ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS="Accounts common login expi
|
||||||
DEFAULT_ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS="90"
|
DEFAULT_ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS="90"
|
||||||
KEY_ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS="accounts-common-login-expiration-in-days"
|
KEY_ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS="accounts-common-login-expiration-in-days"
|
||||||
|
|
||||||
DESCRIPTION_ATTACHMENTS_STORE_PATH="Allow wekan ower to specify where uploaded files to store on the server instead of the mongodb"
|
|
||||||
DEFAULT_ATTACHMENTS_STORE_PATH=""
|
|
||||||
KEY_ATTACHMENTS_STORE_PATH="attachments-store-path"
|
|
||||||
|
|
||||||
# Example, not in use: /var/snap/wekan/common/uploads/
|
# Example, not in use: /var/snap/wekan/common/uploads/
|
||||||
|
|
||||||
DESCRIPTION_MAX_IMAGE_PIXEL="Max image pixel: Allow to shrink attached/pasted image https://github.com/wekan/wekan/pull/2544"
|
DESCRIPTION_MAX_IMAGE_PIXEL="Max image pixel: Allow to shrink attached/pasted image https://github.com/wekan/wekan/pull/2544"
|
||||||
|
|
|
@ -131,12 +131,6 @@ echo -e "\t$ snap set $SNAP_NAME image-compress-ratio='80'"
|
||||||
echo -e "Disable:"
|
echo -e "Disable:"
|
||||||
echo -e "\t$ snap unset $SNAP_NAME image-compress-ratio"
|
echo -e "\t$ snap unset $SNAP_NAME image-compress-ratio"
|
||||||
echo -e "\n"
|
echo -e "\n"
|
||||||
echo -e "Allow to set attachment upload into specified server location. Create that directory first. https://github.com/wekan/wekan/pull/2603"
|
|
||||||
echo -e "Example:"
|
|
||||||
echo -e "\t$ snap set $SNAP_NAME attachments-store-path='/var/snap/wekan/common/attachments'"
|
|
||||||
echo -e "Disable:"
|
|
||||||
echo -e "\t$ snap unset $SNAP_NAME attachments-store-path"
|
|
||||||
echo -e "\n"
|
|
||||||
echo -e "NOTIFICATION TRAY AFTER READ DAYS BEFORE REMOVE https://github.com/wekan/wekan/pull/2998"
|
echo -e "NOTIFICATION TRAY AFTER READ DAYS BEFORE REMOVE https://github.com/wekan/wekan/pull/2998"
|
||||||
echo -e "Number of days after a notification is read before we remove it. Default: 2."
|
echo -e "Number of days after a notification is read before we remove it. Default: 2."
|
||||||
echo -e "Example:"
|
echo -e "Example:"
|
||||||
|
|
|
@ -72,8 +72,6 @@ meteor=/home/wekan/.meteor/meteor
|
||||||
#sudo -u wekan ${meteor} add standard-minifier-js
|
#sudo -u wekan ${meteor} add standard-minifier-js
|
||||||
sudo -u wekan ${meteor} npm install
|
sudo -u wekan ${meteor} npm install
|
||||||
sudo -u wekan ${meteor} build --directory /home/wekan/app_build
|
sudo -u wekan ${meteor} build --directory /home/wekan/app_build
|
||||||
sudo cp /home/wekan/app/fix-download-unicode/cfs_access-point.txt /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js
|
|
||||||
sudo chown wekan:wekan /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js
|
|
||||||
sudo rm /home/wekan/app_build/bundle/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs
|
sudo rm /home/wekan/app_build/bundle/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs
|
||||||
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
||||||
rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
|
rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
|
||||||
|
|
|
@ -252,10 +252,6 @@ services:
|
||||||
# Defaults below. Uncomment to change. wekan/server/accounts-common.js
|
# Defaults below. Uncomment to change. wekan/server/accounts-common.js
|
||||||
# - ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90
|
# - ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90
|
||||||
#---------------------------------------------------------------
|
#---------------------------------------------------------------
|
||||||
# ==== STORE ATTACHMENT ON SERVER FILESYSTEM INSTEAD OF MONGODB ====
|
|
||||||
# https://github.com/wekan/wekan/pull/2603
|
|
||||||
#- ATTACHMENTS_STORE_PATH = <pathname> # pathname can be relative or fullpath
|
|
||||||
#---------------------------------------------------------------
|
|
||||||
# ==== RICH TEXT EDITOR IN CARD COMMENTS ====
|
# ==== RICH TEXT EDITOR IN CARD COMMENTS ====
|
||||||
# https://github.com/wekan/wekan/pull/2560
|
# https://github.com/wekan/wekan/pull/2560
|
||||||
- RICHER_CARD_COMMENT_EDITOR=false
|
- RICHER_CARD_COMMENT_EDITOR=false
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue