mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 23:40:13 +01:00
Multi-File Storage.
Thanks to mfilser ! Related https://github.com/wekan/wekan/pull/4484 Merge branch 'master' into upgrade-meteor
This commit is contained in:
commit
68e8155805
29 changed files with 921 additions and 276 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
*~
|
*~
|
||||||
*.swp
|
*.sw*
|
||||||
.meteor-spk
|
.meteor-spk
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
tmp/
|
tmp/
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ This release adds the following new features:
|
||||||
Thanks to xet7.
|
Thanks to xet7.
|
||||||
- [Added Table View to My Cards](https://github.com/wekan/wekan/pulls/4479).
|
- [Added Table View to My Cards](https://github.com/wekan/wekan/pulls/4479).
|
||||||
Thanks to helioguardabaxo.
|
Thanks to helioguardabaxo.
|
||||||
|
- [Multi file storage for moving between MongoDB GridFS and filesystem](https://github.com/wekan/wekan/pull/4484).
|
||||||
|
Thanks to mfilser.
|
||||||
|
|
||||||
and adds the following updates:
|
and adds the following updates:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ that by providing one-click installation on various platforms.
|
||||||
|
|
||||||
- WeKan ® is used in [most countries of the world](https://snapcraft.io/wekan).
|
- WeKan ® is used in [most countries of the world](https://snapcraft.io/wekan).
|
||||||
- Wekan largest user has 22k users using Wekan in their company.
|
- Wekan largest user has 22k users using Wekan in their company.
|
||||||
- Wekan has been [translated](https://transifex.com/wekan/wekan) to about 70 languages.
|
- Wekan has been [translated](https://transifex.com/wekan/wekan) to about 105 languages.
|
||||||
- [Features][features]: WeKan ® has real-time user interface.
|
- [Features][features]: WeKan ® has real-time user interface.
|
||||||
- [Platforms][platforms]: WeKan ® supports many platforms.
|
- [Platforms][platforms]: WeKan ® supports many platforms.
|
||||||
WeKan ® is critical part of new platforms Wekan is currently being integrated to.
|
WeKan ® is critical part of new platforms Wekan is currently being integrated to.
|
||||||
|
|
|
||||||
|
|
@ -49,17 +49,7 @@ template(name="attachmentsGalery")
|
||||||
if currentUser.isBoardMember
|
if currentUser.isBoardMember
|
||||||
unless currentUser.isCommentOnly
|
unless currentUser.isCommentOnly
|
||||||
unless currentUser.isWorker
|
unless currentUser.isWorker
|
||||||
if isImage
|
a.fa.fa-navicon.attachment-details-menu.js-open-attachment-menu(title="{{_ 'attachmentActionsPopup-title'}}")
|
||||||
a(class="{{#if $eq ../coverId _id}}js-remove-cover{{else}}js-add-cover{{/if}}")
|
|
||||||
i.fa.fa-thumb-tack
|
|
||||||
if($eq ../coverId _id)
|
|
||||||
| {{_ 'remove-cover'}}
|
|
||||||
else
|
|
||||||
| {{_ 'add-cover'}}
|
|
||||||
if currentUser.isBoardAdmin
|
|
||||||
a.js-confirm-delete
|
|
||||||
i.fa.fa-close
|
|
||||||
| {{_ 'delete'}}
|
|
||||||
|
|
||||||
if currentUser.isBoardMember
|
if currentUser.isBoardMember
|
||||||
unless currentUser.isCommentOnly
|
unless currentUser.isCommentOnly
|
||||||
|
|
@ -67,3 +57,31 @@ template(name="attachmentsGalery")
|
||||||
//li.attachment-item.add-attachment
|
//li.attachment-item.add-attachment
|
||||||
a.js-add-attachment(title="{{_ 'add-attachment' }}")
|
a.js-add-attachment(title="{{_ 'add-attachment' }}")
|
||||||
i.fa.fa-plus
|
i.fa.fa-plus
|
||||||
|
|
||||||
|
template(name="attachmentActionsPopup")
|
||||||
|
ul.pop-over-list
|
||||||
|
li
|
||||||
|
if isImage
|
||||||
|
a(class="{{#if isCover}}js-remove-cover{{else}}js-add-cover{{/if}}")
|
||||||
|
i.fa.fa-thumb-tack
|
||||||
|
if isCover
|
||||||
|
| {{_ 'remove-cover'}}
|
||||||
|
else
|
||||||
|
| {{_ 'add-cover'}}
|
||||||
|
if currentUser.isBoardAdmin
|
||||||
|
a.js-confirm-delete
|
||||||
|
i.fa.fa-close
|
||||||
|
| {{_ 'delete'}}
|
||||||
|
p.attachment-storage
|
||||||
|
| {{versions.original.storage}}
|
||||||
|
|
||||||
|
if $neq versions.original.storage "fs"
|
||||||
|
a.js-move-storage-fs
|
||||||
|
i.fa.fa-arrow-right
|
||||||
|
| {{_ 'attachment-move-storage-fs'}}
|
||||||
|
|
||||||
|
if $neq versions.original.storage "gridfs"
|
||||||
|
if versions.original.storage
|
||||||
|
a.js-move-storage-gridfs
|
||||||
|
i.fa.fa-arrow-right
|
||||||
|
| {{_ 'attachment-move-storage-gridfs'}}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,11 @@
|
||||||
Template.attachmentsGalery.events({
|
Template.attachmentsGalery.events({
|
||||||
'click .js-add-attachment': Popup.open('cardAttachments'),
|
'click .js-add-attachment': Popup.open('cardAttachments'),
|
||||||
'click .js-confirm-delete': Popup.afterConfirm(
|
|
||||||
'attachmentDelete',
|
|
||||||
function() {
|
|
||||||
Attachments.remove(this._id);
|
|
||||||
Popup.back();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// If we let this event bubble, FlowRouter will handle it and empty the page
|
// If we let this event bubble, FlowRouter will handle it and empty the page
|
||||||
// content, see #101.
|
// content, see #101.
|
||||||
'click .js-download'(event) {
|
'click .js-download'(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
},
|
},
|
||||||
'click .js-add-cover'() {
|
'click .js-open-attachment-menu': Popup.open('attachmentActions'),
|
||||||
Cards.findOne(this.meta.cardId).setCover(this._id);
|
|
||||||
},
|
|
||||||
'click .js-remove-cover'() {
|
|
||||||
Cards.findOne(this.meta.cardId).unsetCover();
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Template.attachmentsGalery.helpers({
|
Template.attachmentsGalery.helpers({
|
||||||
|
|
@ -33,12 +21,16 @@ Template.cardAttachmentsPopup.events({
|
||||||
'change .js-attach-file'(event) {
|
'change .js-attach-file'(event) {
|
||||||
const card = this;
|
const card = this;
|
||||||
if (event.currentTarget.files && event.currentTarget.files[0]) {
|
if (event.currentTarget.files && event.currentTarget.files[0]) {
|
||||||
|
const fileId = Random.id();
|
||||||
|
const config = {
|
||||||
|
file: event.currentTarget.files[0],
|
||||||
|
fileId: fileId,
|
||||||
|
meta: Utils.getCommonAttachmentMetaFrom(card),
|
||||||
|
chunkSize: 'dynamic',
|
||||||
|
};
|
||||||
|
config.meta.fileId = fileId;
|
||||||
const uploader = Attachments.insert(
|
const uploader = Attachments.insert(
|
||||||
{
|
config,
|
||||||
file: event.currentTarget.files[0],
|
|
||||||
meta: Utils.getCommonAttachmentMetaFrom(card),
|
|
||||||
chunkSize: 'dynamic',
|
|
||||||
},
|
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
uploader.on('uploaded', (error, fileRef) => {
|
uploader.on('uploaded', (error, fileRef) => {
|
||||||
|
|
@ -104,13 +96,17 @@ Template.previewClipboardImagePopup.events({
|
||||||
if (pastedResults && pastedResults.file) {
|
if (pastedResults && pastedResults.file) {
|
||||||
const file = pastedResults.file;
|
const file = pastedResults.file;
|
||||||
window.oPasted = pastedResults;
|
window.oPasted = pastedResults;
|
||||||
|
const fileId = Random.id();
|
||||||
|
const config = {
|
||||||
|
file,
|
||||||
|
fileId: fileId,
|
||||||
|
meta: Utils.getCommonAttachmentMetaFrom(card),
|
||||||
|
fileName: file.name || file.type.replace('image/', 'clipboard.'),
|
||||||
|
chunkSize: 'dynamic',
|
||||||
|
};
|
||||||
|
config.meta.fileId = fileId;
|
||||||
const uploader = Attachments.insert(
|
const uploader = Attachments.insert(
|
||||||
{
|
config,
|
||||||
file,
|
|
||||||
meta: Utils.getCommonAttachmentMetaFrom(card),
|
|
||||||
fileName: file.name || file.type.replace('image/', 'clipboard.'),
|
|
||||||
chunkSize: 'dynamic',
|
|
||||||
},
|
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
uploader.on('uploaded', (error, fileRef) => {
|
uploader.on('uploaded', (error, fileRef) => {
|
||||||
|
|
@ -129,3 +125,36 @@ Template.previewClipboardImagePopup.events({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
BlazeComponent.extendComponent({
|
||||||
|
isCover() {
|
||||||
|
const ret = Cards.findOne(this.data().meta.cardId).coverId == this.data()._id;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
events() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete', function() {
|
||||||
|
Attachments.remove(this._id);
|
||||||
|
Popup.back(2);
|
||||||
|
}),
|
||||||
|
'click .js-add-cover'() {
|
||||||
|
Cards.findOne(this.data().meta.cardId).setCover(this.data()._id);
|
||||||
|
Popup.back();
|
||||||
|
},
|
||||||
|
'click .js-remove-cover'() {
|
||||||
|
Cards.findOne(this.data().meta.cardId).unsetCover();
|
||||||
|
Popup.back();
|
||||||
|
},
|
||||||
|
'click .js-move-storage-fs'() {
|
||||||
|
Meteor.call('moveAttachmentToStorage', this.data()._id, "fs");
|
||||||
|
Popup.back();
|
||||||
|
},
|
||||||
|
'click .js-move-storage-gridfs'() {
|
||||||
|
Meteor.call('moveAttachmentToStorage', this.data()._id, "gridfs");
|
||||||
|
Popup.back();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}).register('attachmentActionsPopup');
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@
|
||||||
.attachment-details-actions a
|
.attachment-details-actions a
|
||||||
display: block
|
display: block
|
||||||
|
|
||||||
|
&.attachment-details-menu
|
||||||
|
padding-top: 10px
|
||||||
|
|
||||||
.attachment-image-preview
|
.attachment-image-preview
|
||||||
max-width: 100px
|
max-width: 100px
|
||||||
display: block
|
display: block
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,6 @@ template(name="adminReports")
|
||||||
i.fa.fa-chain-broken
|
i.fa.fa-chain-broken
|
||||||
| {{_ 'broken-cards'}}
|
| {{_ 'broken-cards'}}
|
||||||
|
|
||||||
li
|
|
||||||
a.js-report-files(data-id="report-orphaned-files")
|
|
||||||
i.fa.fa-paperclip
|
|
||||||
| {{_ 'orphanedFilesReportTitle'}}
|
|
||||||
|
|
||||||
li
|
li
|
||||||
a.js-report-files(data-id="report-files")
|
a.js-report-files(data-id="report-files")
|
||||||
i.fa.fa-paperclip
|
i.fa.fa-paperclip
|
||||||
|
|
@ -43,8 +38,6 @@ template(name="adminReports")
|
||||||
+brokenCardsReport
|
+brokenCardsReport
|
||||||
else if showFilesReport.get
|
else if showFilesReport.get
|
||||||
+filesReport
|
+filesReport
|
||||||
else if showOrphanedFilesReport.get
|
|
||||||
+orphanedFilesReport
|
|
||||||
else if showRulesReport.get
|
else if showRulesReport.get
|
||||||
+rulesReport
|
+rulesReport
|
||||||
else if showBoardsReport.get
|
else if showBoardsReport.get
|
||||||
|
|
@ -64,7 +57,7 @@ template(name="brokenCardsReport")
|
||||||
template(name="rulesReport")
|
template(name="rulesReport")
|
||||||
h1 {{_ 'rulesReportTitle'}}
|
h1 {{_ 'rulesReportTitle'}}
|
||||||
if resultsCount
|
if resultsCount
|
||||||
table.table
|
table
|
||||||
tr
|
tr
|
||||||
th Rule Title
|
th Rule Title
|
||||||
th Board Title
|
th Board Title
|
||||||
|
|
@ -83,44 +76,23 @@ template(name="rulesReport")
|
||||||
template(name="filesReport")
|
template(name="filesReport")
|
||||||
h1 {{_ 'filesReportTitle'}}
|
h1 {{_ 'filesReportTitle'}}
|
||||||
if resultsCount
|
if resultsCount
|
||||||
table.table
|
table
|
||||||
tr
|
tr
|
||||||
th Filename
|
th Filename
|
||||||
th.right Size (kB)
|
th.right Size (kB)
|
||||||
th MIME Type
|
th MIME Type
|
||||||
th.center Usage
|
th Attachment ID
|
||||||
th MD5 Sum
|
th Board ID
|
||||||
th ID
|
th Card ID
|
||||||
|
|
||||||
each att in results
|
each att in results
|
||||||
tr
|
tr
|
||||||
td {{ att.filename }}
|
td {{ att.name }}
|
||||||
td.right {{fileSize att.length }}
|
td.right {{fileSize att.size }}
|
||||||
td {{ att.contentType }}
|
td {{ att.type }}
|
||||||
td.center {{usageCount att._id.toHexString }}
|
td {{ att._id }}
|
||||||
td {{ att.md5 }}
|
td {{ att.meta.boardId }}
|
||||||
td {{ att._id.toHexString }}
|
td {{ att.meta.cardId }}
|
||||||
else
|
|
||||||
div {{_ 'no-results' }}
|
|
||||||
|
|
||||||
template(name="orphanedFilesReport")
|
|
||||||
h1 {{_ 'orphanedFilesReportTitle'}}
|
|
||||||
if resultsCount
|
|
||||||
table.table
|
|
||||||
tr
|
|
||||||
th Filename
|
|
||||||
th.right Size (kB)
|
|
||||||
th MIME Type
|
|
||||||
th MD5 Sum
|
|
||||||
th ID
|
|
||||||
|
|
||||||
each att in results
|
|
||||||
tr
|
|
||||||
td {{ att.filename }}
|
|
||||||
td.right {{fileSize att.length }}
|
|
||||||
td {{ att.contentType }}
|
|
||||||
td {{ att.md5 }}
|
|
||||||
td {{ att._id.toHexString }}
|
|
||||||
else
|
else
|
||||||
div {{_ 'no-results' }}
|
div {{_ 'no-results' }}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ BlazeComponent.extendComponent({
|
||||||
{
|
{
|
||||||
'click a.js-report-broken': this.switchMenu,
|
'click a.js-report-broken': this.switchMenu,
|
||||||
'click a.js-report-files': this.switchMenu,
|
'click a.js-report-files': this.switchMenu,
|
||||||
'click a.js-report-orphaned-files': this.switchMenu,
|
|
||||||
'click a.js-report-rules': this.switchMenu,
|
'click a.js-report-rules': this.switchMenu,
|
||||||
'click a.js-report-cards': this.switchMenu,
|
'click a.js-report-cards': this.switchMenu,
|
||||||
'click a.js-report-boards': this.switchMenu,
|
'click a.js-report-boards': this.switchMenu,
|
||||||
|
|
@ -66,11 +65,6 @@ BlazeComponent.extendComponent({
|
||||||
this.subscription = Meteor.subscribe('attachmentsList', () => {
|
this.subscription = Meteor.subscribe('attachmentsList', () => {
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
});
|
});
|
||||||
} else if ('report-orphaned-files' === targetID) {
|
|
||||||
this.showOrphanedFilesReport.set(true);
|
|
||||||
this.subscription = Meteor.subscribe('orphanedAttachments', () => {
|
|
||||||
this.loading.set(false);
|
|
||||||
});
|
|
||||||
} else if ('report-rules' === targetID) {
|
} else if ('report-rules' === targetID) {
|
||||||
this.subscription = Meteor.subscribe('rulesReport', () => {
|
this.subscription = Meteor.subscribe('rulesReport', () => {
|
||||||
this.showRulesReport.set(true);
|
this.showRulesReport.set(true);
|
||||||
|
|
@ -104,8 +98,6 @@ class AdminReport extends BlazeComponent {
|
||||||
|
|
||||||
results() {
|
results() {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
// console.log('attachments:', AttachmentStorage.find());
|
|
||||||
// console.log('attachments.count:', AttachmentStorage.find().count());
|
|
||||||
return this.collection.find();
|
return this.collection.find();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,10 +117,6 @@ class AdminReport extends BlazeComponent {
|
||||||
return Math.round(size / 1024);
|
return Math.round(size / 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
usageCount(key) {
|
|
||||||
return Attachments.find({ 'copies.attachments.key': key }).count();
|
|
||||||
}
|
|
||||||
|
|
||||||
abbreviate(text) {
|
abbreviate(text) {
|
||||||
if (text.length > 30) {
|
if (text.length > 30) {
|
||||||
return `${text.substr(0, 29)}...`;
|
return `${text.substr(0, 29)}...`;
|
||||||
|
|
@ -138,13 +126,9 @@ class AdminReport extends BlazeComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
(class extends AdminReport {
|
(class extends AdminReport {
|
||||||
collection = AttachmentStorage;
|
collection = Attachments;
|
||||||
}.register('filesReport'));
|
}.register('filesReport'));
|
||||||
|
|
||||||
(class extends AdminReport {
|
|
||||||
collection = AttachmentStorage;
|
|
||||||
}.register('orphanedFilesReport'));
|
|
||||||
|
|
||||||
(class extends AdminReport {
|
(class extends AdminReport {
|
||||||
collection = Rules;
|
collection = Rules;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
.admin-reports-content
|
|
||||||
height: auto !important
|
|
||||||
|
|
||||||
84
client/components/settings/attachments.jade
Normal file
84
client/components/settings/attachments.jade
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
template(name="attachments")
|
||||||
|
.setting-content.attachments-content
|
||||||
|
unless currentUser.isAdmin
|
||||||
|
| {{_ 'error-notAuthorized'}}
|
||||||
|
else
|
||||||
|
.content-body
|
||||||
|
.side-menu
|
||||||
|
ul
|
||||||
|
li
|
||||||
|
a.js-move-attachments(data-id="move-attachments")
|
||||||
|
i.fa.fa-arrow-right
|
||||||
|
| {{_ 'attachment-move'}}
|
||||||
|
|
||||||
|
.main-body
|
||||||
|
if loading.get
|
||||||
|
+spinner
|
||||||
|
else if showMoveAttachments.get
|
||||||
|
+moveAttachments
|
||||||
|
|
||||||
|
template(name="moveAttachments")
|
||||||
|
.move-attachment-buttons
|
||||||
|
.js-move-attachment
|
||||||
|
button.js-move-all-attachments-to-fs {{_ 'move-all-attachments-to-fs'}}
|
||||||
|
.js-move-attachment
|
||||||
|
button.js-move-all-attachments-to-gridfs {{_ 'move-all-attachments-to-gridfs'}}
|
||||||
|
|
||||||
|
each board in getBoardsWithAttachments
|
||||||
|
+moveBoardAttachments board
|
||||||
|
|
||||||
|
template(name="moveBoardAttachments")
|
||||||
|
hr
|
||||||
|
.board-description
|
||||||
|
table
|
||||||
|
tr
|
||||||
|
th {{_ 'board'}} ID
|
||||||
|
th {{_ 'board-title'}}
|
||||||
|
tr
|
||||||
|
td {{ _id }}
|
||||||
|
td {{ title }}
|
||||||
|
|
||||||
|
.move-attachment-buttons
|
||||||
|
.js-move-attachment
|
||||||
|
button.js-move-all-attachments-of-board-to-fs {{_ 'move-all-attachments-of-board-to-fs'}}
|
||||||
|
.js-move-attachment
|
||||||
|
button.js-move-all-attachments-of-board-to-gridfs {{_ 'move-all-attachments-of-board-to-gridfs'}}
|
||||||
|
|
||||||
|
.board-attachments
|
||||||
|
table
|
||||||
|
tr
|
||||||
|
th {{_ 'card'}}-Id
|
||||||
|
th {{_ 'attachment'}}-Id
|
||||||
|
th {{_ 'name'}}
|
||||||
|
th {{_ 'path'}}
|
||||||
|
th {{_ 'version-name'}}
|
||||||
|
th {{_ 'size'}} (B)
|
||||||
|
th GridFsFileId
|
||||||
|
th {{_ 'storage'}}
|
||||||
|
th {{_ 'action'}}
|
||||||
|
|
||||||
|
each attachment in attachments
|
||||||
|
+moveAttachment attachment
|
||||||
|
|
||||||
|
template(name="moveAttachment")
|
||||||
|
each version in flatVersion
|
||||||
|
tr
|
||||||
|
td {{ meta.cardId }}
|
||||||
|
td {{ _id }}
|
||||||
|
td {{ name }}
|
||||||
|
td {{ version.path }}
|
||||||
|
td {{ version.versionName }}
|
||||||
|
td {{ version.size }}
|
||||||
|
td {{ version.meta.gridFsFileId }}
|
||||||
|
td {{ version.storageName }}
|
||||||
|
td
|
||||||
|
if $neq version.storageName "fs"
|
||||||
|
button.js-move-storage-fs
|
||||||
|
i.fa.fa-arrow-right
|
||||||
|
| {{_ 'attachment-move-storage-fs'}}
|
||||||
|
|
||||||
|
if $neq version.storageName "gridfs"
|
||||||
|
if version.storageName
|
||||||
|
button.js-move-storage-gridfs
|
||||||
|
i.fa.fa-arrow-right
|
||||||
|
| {{_ 'attachment-move-storage-gridfs'}}
|
||||||
123
client/components/settings/attachments.js
Normal file
123
client/components/settings/attachments.js
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
import Attachments, { fileStoreStrategyFactory } from '/models/attachments';
|
||||||
|
|
||||||
|
BlazeComponent.extendComponent({
|
||||||
|
subscription: null,
|
||||||
|
showMoveAttachments: new ReactiveVar(false),
|
||||||
|
sessionId: null,
|
||||||
|
|
||||||
|
onCreated() {
|
||||||
|
this.error = new ReactiveVar('');
|
||||||
|
this.loading = new ReactiveVar(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
events() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'click a.js-move-attachments': this.switchMenu,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
switchMenu(event) {
|
||||||
|
const target = $(event.target);
|
||||||
|
if (!target.hasClass('active')) {
|
||||||
|
this.loading.set(true);
|
||||||
|
this.showMoveAttachments.set(false);
|
||||||
|
if (this.subscription) {
|
||||||
|
this.subscription.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.side-menu li.active').removeClass('active');
|
||||||
|
target.parent().addClass('active');
|
||||||
|
const targetID = target.data('id');
|
||||||
|
|
||||||
|
if ('move-attachments' === targetID) {
|
||||||
|
this.showMoveAttachments.set(true);
|
||||||
|
this.subscription = Meteor.subscribe('attachmentsList', () => {
|
||||||
|
this.loading.set(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}).register('attachments');
|
||||||
|
|
||||||
|
BlazeComponent.extendComponent({
|
||||||
|
getBoardsWithAttachments() {
|
||||||
|
this.attachments = Attachments.find().get();
|
||||||
|
this.attachmentsByBoardId = _.chain(this.attachments)
|
||||||
|
.groupBy(fileObj => fileObj.meta.boardId)
|
||||||
|
.value();
|
||||||
|
|
||||||
|
const ret = Object.keys(this.attachmentsByBoardId)
|
||||||
|
.map(boardId => {
|
||||||
|
const boardAttachments = this.attachmentsByBoardId[boardId];
|
||||||
|
|
||||||
|
_.each(boardAttachments, _attachment => {
|
||||||
|
_attachment.flatVersion = Object.keys(_attachment.versions)
|
||||||
|
.map(_versionName => {
|
||||||
|
const _version = Object.assign(_attachment.versions[_versionName], {"versionName": _versionName});
|
||||||
|
_version.storageName = fileStoreStrategyFactory.getFileStrategy(_attachment, _versionName).getStorageName();
|
||||||
|
return _version;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const board = Boards.findOne(boardId);
|
||||||
|
board.attachments = boardAttachments;
|
||||||
|
return board;
|
||||||
|
})
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
getBoardData(boardid) {
|
||||||
|
const ret = Boards.findOne(boardId);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
events() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'click button.js-move-all-attachments-to-fs'(event) {
|
||||||
|
this.attachments.forEach(_attachment => {
|
||||||
|
Meteor.call('moveAttachmentToStorage', _attachment._id, "fs");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'click button.js-move-all-attachments-to-gridfs'(event) {
|
||||||
|
this.attachments.forEach(_attachment => {
|
||||||
|
Meteor.call('moveAttachmentToStorage', _attachment._id, "gridfs");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}).register('moveAttachments');
|
||||||
|
|
||||||
|
BlazeComponent.extendComponent({
|
||||||
|
events() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'click button.js-move-all-attachments-of-board-to-fs'(event) {
|
||||||
|
this.data().attachments.forEach(_attachment => {
|
||||||
|
Meteor.call('moveAttachmentToStorage', _attachment._id, "fs");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'click button.js-move-all-attachments-of-board-to-gridfs'(event) {
|
||||||
|
this.data().attachments.forEach(_attachment => {
|
||||||
|
Meteor.call('moveAttachmentToStorage', _attachment._id, "gridfs");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}).register('moveBoardAttachments');
|
||||||
|
|
||||||
|
BlazeComponent.extendComponent({
|
||||||
|
events() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'click button.js-move-storage-fs'(event) {
|
||||||
|
Meteor.call('moveAttachmentToStorage', this.data()._id, "fs");
|
||||||
|
},
|
||||||
|
'click button.js-move-storage-gridfs'(event) {
|
||||||
|
Meteor.call('moveAttachmentToStorage', this.data()._id, "gridfs");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}).register('moveAttachment');
|
||||||
8
client/components/settings/attachments.styl
Normal file
8
client/components/settings/attachments.styl
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
.move-attachment-buttons
|
||||||
|
display: flex
|
||||||
|
gap: 10px
|
||||||
|
|
||||||
|
.attachments-content
|
||||||
|
hr
|
||||||
|
height: 0px
|
||||||
|
border: 1px solid black
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
|
|
||||||
table
|
table
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
color: #000;
|
color: #000;
|
||||||
|
|
||||||
td, th
|
td, th
|
||||||
|
|
@ -22,14 +20,13 @@ table
|
||||||
.ext-box-left
|
.ext-box-left
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%
|
width: 100%
|
||||||
|
gap: 10px
|
||||||
|
|
||||||
span
|
span
|
||||||
vertical-align: center;
|
vertical-align: center;
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
margin-right: 10px;
|
|
||||||
|
|
||||||
input, button
|
input, button
|
||||||
margin: 0 10px 0 0;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
button
|
button
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,9 @@
|
||||||
display: flex
|
display: flex
|
||||||
|
|
||||||
.setting-content
|
.setting-content
|
||||||
padding 30px
|
|
||||||
color: #727479
|
color: #727479
|
||||||
background: #dedede
|
background: #dedede
|
||||||
width 100%
|
width 100%
|
||||||
height calc(100% - 80px)
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
.content-title
|
.content-title
|
||||||
|
|
@ -21,6 +19,7 @@
|
||||||
display flex
|
display flex
|
||||||
padding-top 15px
|
padding-top 15px
|
||||||
height 100%
|
height 100%
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
.side-menu
|
.side-menu
|
||||||
background-color: #f7f7f7;
|
background-color: #f7f7f7;
|
||||||
|
|
@ -54,7 +53,6 @@
|
||||||
margin-right: 20px
|
margin-right: 20px
|
||||||
|
|
||||||
.main-body
|
.main-body
|
||||||
padding: 0.1em 1em
|
|
||||||
-webkit-user-select: text // Safari 3.1+
|
-webkit-user-select: text // Safari 3.1+
|
||||||
-moz-user-select: text // Firefox 2+
|
-moz-user-select: text // Firefox 2+
|
||||||
-ms-user-select: text // IE 10+
|
-ms-user-select: text // IE 10+
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ template(name="settingHeaderBar")
|
||||||
i.fa(class="fa-list")
|
i.fa(class="fa-list")
|
||||||
span {{_ 'reports'}}
|
span {{_ 'reports'}}
|
||||||
|
|
||||||
|
a.setting-header-btn.informations(href="{{pathFor 'attachments'}}")
|
||||||
|
i.fa(class="fa-paperclip")
|
||||||
|
span {{_ 'attachments'}}
|
||||||
|
|
||||||
a.setting-header-btn.informations(href="{{pathFor 'information'}}")
|
a.setting-header-btn.informations(href="{{pathFor 'information'}}")
|
||||||
i.fa(class="fa-info-circle")
|
i.fa(class="fa-info-circle")
|
||||||
span {{_ 'info'}}
|
span {{_ 'info'}}
|
||||||
|
|
|
||||||
|
|
@ -357,6 +357,30 @@ FlowRouter.route('/admin-reports', {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
FlowRouter.route('/attachments', {
|
||||||
|
name: 'attachments',
|
||||||
|
triggersEnter: [
|
||||||
|
AccountsTemplates.ensureSignedIn,
|
||||||
|
() => {
|
||||||
|
Session.set('currentBoard', null);
|
||||||
|
Session.set('currentList', null);
|
||||||
|
Session.set('currentCard', null);
|
||||||
|
Session.set('popupCardId', null);
|
||||||
|
Session.set('popupCardBoardId', null);
|
||||||
|
|
||||||
|
Filter.reset();
|
||||||
|
Session.set('sortBy', '');
|
||||||
|
EscapeActions.executeAll();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
action() {
|
||||||
|
BlazeLayout.render('defaultLayout', {
|
||||||
|
headerBar: 'settingHeaderBar',
|
||||||
|
content: 'attachments',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
FlowRouter.notFound = {
|
FlowRouter.notFound = {
|
||||||
action() {
|
action() {
|
||||||
BlazeLayout.render('defaultLayout', { content: 'notFound' });
|
BlazeLayout.render('defaultLayout', { content: 'notFound' });
|
||||||
|
|
|
||||||
|
|
@ -1086,7 +1086,6 @@
|
||||||
"custom-field-stringtemplate-item-placeholder": "Press enter to add more items",
|
"custom-field-stringtemplate-item-placeholder": "Press enter to add more items",
|
||||||
"creator": "Creator",
|
"creator": "Creator",
|
||||||
"filesReportTitle": "Files Report",
|
"filesReportTitle": "Files Report",
|
||||||
"orphanedFilesReportTitle": "Orphaned Files Report",
|
|
||||||
"reports": "Reports",
|
"reports": "Reports",
|
||||||
"rulesReportTitle": "Rules Report",
|
"rulesReportTitle": "Rules Report",
|
||||||
"boardsReportTitle": "Boards Report",
|
"boardsReportTitle": "Boards Report",
|
||||||
|
|
@ -1164,5 +1163,19 @@
|
||||||
"copyChecklist": "Copy Checklist",
|
"copyChecklist": "Copy Checklist",
|
||||||
"copyChecklistPopup-title": "Copy Checklist",
|
"copyChecklistPopup-title": "Copy Checklist",
|
||||||
"card-show-lists": "Card Show Lists",
|
"card-show-lists": "Card Show Lists",
|
||||||
"subtaskActionsPopup-title": "Subtask Actions"
|
"subtaskActionsPopup-title": "Subtask Actions",
|
||||||
|
"attachmentActionsPopup-title": "Attachment Actions",
|
||||||
|
"attachment-move-storage-fs": "Move attachment to filesystem",
|
||||||
|
"attachment-move-storage-gridfs": "Move attachment to GridFS",
|
||||||
|
"attachment-move": "Move Attachment",
|
||||||
|
"move-all-attachments-to-fs": "Move all attachments to filesystem",
|
||||||
|
"move-all-attachments-to-gridfs": "Move all attachments to GridFS",
|
||||||
|
"move-all-attachments-of-board-to-fs": "Move all attachments of board to filesystem",
|
||||||
|
"move-all-attachments-of-board-to-gridfs": "Move all attachments of board to GridFS",
|
||||||
|
"path": "Path",
|
||||||
|
"version-name": "Version-Name",
|
||||||
|
"size": "Size",
|
||||||
|
"storage": "Storage",
|
||||||
|
"action": "Action",
|
||||||
|
"board-title": "Board Title"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1085,7 +1085,6 @@
|
||||||
"custom-field-stringtemplate-item-placeholder": "Press enter to add more items",
|
"custom-field-stringtemplate-item-placeholder": "Press enter to add more items",
|
||||||
"creator": "Creator",
|
"creator": "Creator",
|
||||||
"filesReportTitle": "Files Report",
|
"filesReportTitle": "Files Report",
|
||||||
"orphanedFilesReportTitle": "Orphaned Files Report",
|
|
||||||
"reports": "Reports",
|
"reports": "Reports",
|
||||||
"rulesReportTitle": "Rules Report",
|
"rulesReportTitle": "Rules Report",
|
||||||
"boardsReportTitle": "Boards Report",
|
"boardsReportTitle": "Boards Report",
|
||||||
|
|
@ -1163,5 +1162,19 @@
|
||||||
"copyChecklist": "Copy Checklist",
|
"copyChecklist": "Copy Checklist",
|
||||||
"copyChecklistPopup-title": "Copy Checklist",
|
"copyChecklistPopup-title": "Copy Checklist",
|
||||||
"card-show-lists": "Card Show Lists",
|
"card-show-lists": "Card Show Lists",
|
||||||
"subtaskActionsPopup-title": "Subtask Actions"
|
"subtaskActionsPopup-title": "Subtask Actions",
|
||||||
|
"attachmentActionsPopup-title": "Attachment Actions",
|
||||||
|
"attachment-move-storage-fs": "Move attachment to filesystem",
|
||||||
|
"attachment-move-storage-gridfs": "Move attachment to GridFS",
|
||||||
|
"attachment-move": "Move Attachment",
|
||||||
|
"move-all-attachments-to-fs": "Move all attachments to filesystem",
|
||||||
|
"move-all-attachments-to-gridfs": "Move all attachments to GridFS",
|
||||||
|
"move-all-attachments-of-board-to-fs": "Move all attachments of board to filesystem",
|
||||||
|
"move-all-attachments-of-board-to-gridfs": "Move all attachments of board to GridFS",
|
||||||
|
"path": "Path",
|
||||||
|
"version-name": "Version-Name",
|
||||||
|
"size": "Size",
|
||||||
|
"storage": "Storage",
|
||||||
|
"action": "Action",
|
||||||
|
"board-title": "Board Title"
|
||||||
}
|
}
|
||||||
|
|
@ -1085,7 +1085,6 @@
|
||||||
"custom-field-stringtemplate-item-placeholder": "Press enter to add more items",
|
"custom-field-stringtemplate-item-placeholder": "Press enter to add more items",
|
||||||
"creator": "Creator",
|
"creator": "Creator",
|
||||||
"filesReportTitle": "Files Report",
|
"filesReportTitle": "Files Report",
|
||||||
"orphanedFilesReportTitle": "Orphaned Files Report",
|
|
||||||
"reports": "Reports",
|
"reports": "Reports",
|
||||||
"rulesReportTitle": "Rules Report",
|
"rulesReportTitle": "Rules Report",
|
||||||
"boardsReportTitle": "Boards Report",
|
"boardsReportTitle": "Boards Report",
|
||||||
|
|
@ -1163,5 +1162,19 @@
|
||||||
"copyChecklist": "Copy Checklist",
|
"copyChecklist": "Copy Checklist",
|
||||||
"copyChecklistPopup-title": "Copy Checklist",
|
"copyChecklistPopup-title": "Copy Checklist",
|
||||||
"card-show-lists": "Card Show Lists",
|
"card-show-lists": "Card Show Lists",
|
||||||
"subtaskActionsPopup-title": "Subtask Actions"
|
"subtaskActionsPopup-title": "Subtask Actions",
|
||||||
|
"attachmentActionsPopup-title": "Attachment Actions",
|
||||||
|
"attachment-move-storage-fs": "Move attachment to filesystem",
|
||||||
|
"attachment-move-storage-gridfs": "Move attachment to GridFS",
|
||||||
|
"attachment-move": "Move Attachment",
|
||||||
|
"move-all-attachments-to-fs": "Move all attachments to filesystem",
|
||||||
|
"move-all-attachments-to-gridfs": "Move all attachments to GridFS",
|
||||||
|
"move-all-attachments-of-board-to-fs": "Move all attachments of board to filesystem",
|
||||||
|
"move-all-attachments-of-board-to-gridfs": "Move all attachments of board to GridFS",
|
||||||
|
"path": "Path",
|
||||||
|
"version-name": "Version-Name",
|
||||||
|
"size": "Size",
|
||||||
|
"storage": "Storage",
|
||||||
|
"action": "Action",
|
||||||
|
"board-title": "Board Title"
|
||||||
}
|
}
|
||||||
|
|
@ -247,9 +247,8 @@ if (Meteor.isServer) {
|
||||||
params.commentId = comment._id;
|
params.commentId = comment._id;
|
||||||
}
|
}
|
||||||
if (activity.attachmentId) {
|
if (activity.attachmentId) {
|
||||||
const attachment = activity.attachment();
|
params.attachment = activity.attachmentName;
|
||||||
params.attachment = attachment.name;
|
params.attachmentId = activity.attachmentId;
|
||||||
params.attachmentId = attachment._id;
|
|
||||||
}
|
}
|
||||||
if (activity.checklistId) {
|
if (activity.checklistId) {
|
||||||
const checklist = activity.checklist();
|
const checklist = activity.checklist();
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,17 @@
|
||||||
import { Meteor } from 'meteor/meteor';
|
import { Meteor } from 'meteor/meteor';
|
||||||
import { FilesCollection } from 'meteor/ostrio:files';
|
import { FilesCollection } from 'meteor/ostrio:files';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { createBucket } from './lib/grid/createBucket';
|
import { AttachmentStoreStrategyFilesystem, AttachmentStoreStrategyGridFs} from '/models/lib/attachmentStoreStrategy';
|
||||||
import { createOnAfterUpload } from './lib/fsHooks/createOnAfterUpload';
|
import FileStoreStrategyFactory, {moveToStorage, STORAGE_NAME_FILESYSTEM, STORAGE_NAME_GRIDFS} from '/models/lib/fileStoreStrategy';
|
||||||
import { createInterceptDownload } from './lib/fsHooks/createInterceptDownload';
|
|
||||||
import { createOnAfterRemove } from './lib/fsHooks/createOnAfterRemove';
|
|
||||||
|
|
||||||
let attachmentBucket;
|
let attachmentBucket;
|
||||||
|
let storagePath;
|
||||||
if (Meteor.isServer) {
|
if (Meteor.isServer) {
|
||||||
attachmentBucket = createBucket('attachments');
|
attachmentBucket = createBucket('attachments');
|
||||||
|
storagePath = path.join(process.env.WRITABLE_PATH, 'attachments');
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertActivity = (fileObj, activityType) =>
|
export const fileStoreStrategyFactory = new FileStoreStrategyFactory(AttachmentStoreStrategyFilesystem, storagePath, AttachmentStoreStrategyGridFs, attachmentBucket);
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
// XXX Enforce a schema for the Attachments FilesCollection
|
// XXX Enforce a schema for the Attachments FilesCollection
|
||||||
// see: https://github.com/VeliovGroup/Meteor-Files/wiki/Schema
|
// see: https://github.com/VeliovGroup/Meteor-Files/wiki/Schema
|
||||||
|
|
@ -33,26 +20,34 @@ Attachments = new FilesCollection({
|
||||||
debug: false, // Change to `true` for debugging
|
debug: false, // Change to `true` for debugging
|
||||||
collectionName: 'attachments',
|
collectionName: 'attachments',
|
||||||
allowClientCode: true,
|
allowClientCode: true,
|
||||||
|
namingFunction(opts) {
|
||||||
|
const filenameWithoutExtension = opts.name.replace(/(.+)\..+/, "$1");
|
||||||
|
const ret = opts.meta.fileId + "-original-" + filenameWithoutExtension;
|
||||||
|
// remove fileId from meta, it was only stored there to have this information here in the namingFunction function
|
||||||
|
delete opts.meta.fileId;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
storagePath() {
|
storagePath() {
|
||||||
if (process.env.WRITABLE_PATH) {
|
const ret = fileStoreStrategyFactory.storagePath;
|
||||||
return path.join(process.env.WRITABLE_PATH, 'uploads', 'attachments');
|
return ret;
|
||||||
}
|
|
||||||
return path.normalize(`assets/app/uploads/${this.collectionName}`);
|
|
||||||
},
|
},
|
||||||
onAfterUpload: function onAfterUpload(fileRef) {
|
onAfterUpload(fileObj) {
|
||||||
createOnAfterUpload(attachmentBucket).call(this, fileRef);
|
// current storage is the filesystem, update object and database
|
||||||
// If the attachment doesn't have a source field
|
Object.keys(fileObj.versions).forEach(versionName => {
|
||||||
// or its source is different than import
|
fileObj.versions[versionName].storage = STORAGE_NAME_FILESYSTEM;
|
||||||
if (!fileRef.meta.source || fileRef.meta.source !== 'import') {
|
});
|
||||||
// Add activity about adding the attachment
|
Attachments.update({ _id: fileObj._id }, { $set: { "versions" : fileObj.versions } });
|
||||||
insertActivity(fileRef, 'addAttachment');
|
moveToStorage(fileObj, STORAGE_NAME_GRIDFS, fileStoreStrategyFactory);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
interceptDownload: createInterceptDownload(attachmentBucket),
|
interceptDownload(http, fileObj, versionName) {
|
||||||
onAfterRemove: function onAfterRemove(files) {
|
const ret = fileStoreStrategyFactory.getFileStrategy(fileObj, versionName).interceptDownload(http, this.cacheControl);
|
||||||
createOnAfterRemove(attachmentBucket).call(this, files);
|
return ret;
|
||||||
|
},
|
||||||
|
onAfterRemove(files) {
|
||||||
files.forEach(fileObj => {
|
files.forEach(fileObj => {
|
||||||
insertActivity(fileObj, 'deleteAttachment');
|
Object.keys(fileObj.versions).forEach(versionName => {
|
||||||
|
fileStoreStrategyFactory.getFileStrategy(fileObj, versionName).onAfterRemove();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// We authorize the attachment download either:
|
// We authorize the attachment download either:
|
||||||
|
|
@ -81,6 +76,16 @@ if (Meteor.isServer) {
|
||||||
fetch: ['meta'],
|
fetch: ['meta'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Meteor.methods({
|
||||||
|
moveAttachmentToStorage(fileObjId, storageDestination) {
|
||||||
|
check(fileObjId, String);
|
||||||
|
check(storageDestination, String);
|
||||||
|
|
||||||
|
const fileObj = Attachments.findOne({_id: fileObjId});
|
||||||
|
moveToStorage(fileObj, storageDestination, fileStoreStrategyFactory);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
Meteor.startup(() => {
|
Meteor.startup(() => {
|
||||||
Attachments.collection.createIndex({ cardId: 1 });
|
Attachments.collection.createIndex({ cardId: 1 });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,24 @@
|
||||||
import { Meteor } from 'meteor/meteor';
|
import { Meteor } from 'meteor/meteor';
|
||||||
import { FilesCollection } from 'meteor/ostrio:files';
|
import { FilesCollection } from 'meteor/ostrio:files';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { createBucket } from './lib/grid/createBucket';
|
import FileStoreStrategyFactory, { FileStoreStrategyFilesystem, FileStoreStrategyGridFs} from '/models/lib/fileStoreStrategy';
|
||||||
import { createOnAfterUpload } from './lib/fsHooks/createOnAfterUpload';
|
|
||||||
import { createInterceptDownload } from './lib/fsHooks/createInterceptDownload';
|
|
||||||
import { createOnAfterRemove } from './lib/fsHooks/createOnAfterRemove';
|
|
||||||
|
|
||||||
let avatarsBucket;
|
let avatarsBucket;
|
||||||
|
let storagePath;
|
||||||
if (Meteor.isServer) {
|
if (Meteor.isServer) {
|
||||||
avatarsBucket = createBucket('avatars');
|
avatarsBucket = createBucket('avatars');
|
||||||
|
storagePath = path.join(process.env.WRITABLE_PATH, 'avatars');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileStoreStrategyFactory = new FileStoreStrategyFactory(FileStoreStrategyFilesystem, storagePath, FileStoreStrategyGridFs, avatarsBucket);
|
||||||
|
|
||||||
Avatars = new FilesCollection({
|
Avatars = new FilesCollection({
|
||||||
debug: false, // Change to `true` for debugging
|
debug: false, // Change to `true` for debugging
|
||||||
collectionName: 'avatars',
|
collectionName: 'avatars',
|
||||||
allowClientCode: true,
|
allowClientCode: true,
|
||||||
storagePath() {
|
storagePath() {
|
||||||
if (process.env.WRITABLE_PATH) {
|
const ret = fileStoreStrategyFactory.storagePath;
|
||||||
return path.join(process.env.WRITABLE_PATH, 'uploads', 'avatars');
|
return ret;
|
||||||
}
|
|
||||||
return path.normalize(`assets/app/uploads/${this.collectionName}`);;
|
|
||||||
},
|
},
|
||||||
onBeforeUpload(file) {
|
onBeforeUpload(file) {
|
||||||
if (file.size <= 72000 && file.type.startsWith('image/')) {
|
if (file.size <= 72000 && file.type.startsWith('image/')) {
|
||||||
|
|
@ -27,9 +26,24 @@ Avatars = new FilesCollection({
|
||||||
}
|
}
|
||||||
return 'avatar-too-big';
|
return 'avatar-too-big';
|
||||||
},
|
},
|
||||||
onAfterUpload: createOnAfterUpload(avatarsBucket),
|
onAfterUpload(fileObj) {
|
||||||
interceptDownload: createInterceptDownload(avatarsBucket),
|
// current storage is the filesystem, update object and database
|
||||||
onAfterRemove: createOnAfterRemove(avatarsBucket),
|
Object.keys(fileObj.versions).forEach(versionName => {
|
||||||
|
fileObj.versions[versionName].storage = "fs";
|
||||||
|
});
|
||||||
|
Avatars.update({ _id: fileObj._id }, { $set: { "versions" : fileObj.versions } });
|
||||||
|
},
|
||||||
|
interceptDownload(http, fileObj, versionName) {
|
||||||
|
const ret = fileStoreStrategyFactory.getFileStrategy(fileObj, versionName).interceptDownload(http, this.cacheControl);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
onAfterRemove(files) {
|
||||||
|
files.forEach(fileObj => {
|
||||||
|
Object.keys(fileObj.versions).forEach(versionName => {
|
||||||
|
fileStoreStrategyFactory.getFileStrategy(fileObj, versionName).onAfterRemove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function isOwner(userId, doc) {
|
function isOwner(userId, doc) {
|
||||||
|
|
|
||||||
72
models/lib/attachmentStoreStrategy.js
Normal file
72
models/lib/attachmentStoreStrategy.js
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import FileStoreStrategy, {FileStoreStrategyFilesystem, FileStoreStrategyGridFs} from './fileStoreStrategy'
|
||||||
|
|
||||||
|
const insertActivity = (fileObj, activityType) =>
|
||||||
|
Activities.insert({
|
||||||
|
userId: fileObj.userId,
|
||||||
|
type: 'card',
|
||||||
|
activityType,
|
||||||
|
attachmentId: fileObj._id,
|
||||||
|
attachmentName: fileObj.name,
|
||||||
|
boardId: fileObj.meta.boardId,
|
||||||
|
cardId: fileObj.meta.cardId,
|
||||||
|
listId: fileObj.meta.listId,
|
||||||
|
swimlaneId: fileObj.meta.swimlaneId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Strategy to store attachments at GridFS (MongoDB) */
|
||||||
|
export class AttachmentStoreStrategyGridFs extends FileStoreStrategyGridFs {
|
||||||
|
|
||||||
|
/** constructor
|
||||||
|
* @param gridFsBucket use this GridFS Bucket
|
||||||
|
* @param fileObj the current file object
|
||||||
|
* @param versionName the current version
|
||||||
|
*/
|
||||||
|
constructor(gridFsBucket, fileObj, versionName) {
|
||||||
|
super(gridFsBucket, fileObj, versionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** after successfull upload */
|
||||||
|
onAfterUpload() {
|
||||||
|
super.onAfterUpload();
|
||||||
|
// If the attachment doesn't have a source field or its source is different than import
|
||||||
|
if (!this.fileObj.meta.source || this.fileObj.meta.source !== 'import') {
|
||||||
|
// Add activity about adding the attachment
|
||||||
|
insertActivity(this.fileObj, 'addAttachment');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** after file remove */
|
||||||
|
onAfterRemove() {
|
||||||
|
super.onAfterRemove();
|
||||||
|
insertActivity(this.fileObj, 'deleteAttachment');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Strategy to store attachments at filesystem */
|
||||||
|
export class AttachmentStoreStrategyFilesystem extends FileStoreStrategyFilesystem {
|
||||||
|
|
||||||
|
/** constructor
|
||||||
|
* @param fileObj the current file object
|
||||||
|
* @param versionName the current version
|
||||||
|
*/
|
||||||
|
constructor(fileObj, versionName) {
|
||||||
|
super(fileObj, versionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** after successfull upload */
|
||||||
|
onAfterUpload() {
|
||||||
|
super.onAfterUpload();
|
||||||
|
// If the attachment doesn't have a source field or its source is different than import
|
||||||
|
if (!this.fileObj.meta.source || this.fileObj.meta.source !== 'import') {
|
||||||
|
// Add activity about adding the attachment
|
||||||
|
insertActivity(this.fileObj, 'addAttachment');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** after file remove */
|
||||||
|
onAfterRemove() {
|
||||||
|
super.onAfterRemove();
|
||||||
|
insertActivity(this.fileObj, 'deleteAttachment');
|
||||||
|
}
|
||||||
|
}
|
||||||
338
models/lib/fileStoreStrategy.js
Normal file
338
models/lib/fileStoreStrategy.js
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { createObjectId } from './grid/createObjectId';
|
||||||
|
import { httpStreamOutput } from './httpStream.js'
|
||||||
|
|
||||||
|
export const STORAGE_NAME_FILESYSTEM = "fs";
|
||||||
|
export const STORAGE_NAME_GRIDFS = "gridfs";
|
||||||
|
|
||||||
|
/** Factory for FileStoreStrategy */
|
||||||
|
export default class FileStoreStrategyFactory {
|
||||||
|
|
||||||
|
/** constructor
|
||||||
|
* @param classFileStoreStrategyFilesystem use this strategy for filesystem storage
|
||||||
|
* @param storagePath file storage path
|
||||||
|
* @param classFileStoreStrategyGridFs use this strategy for GridFS storage
|
||||||
|
* @param gridFsBucket use this GridFS Bucket as GridFS Storage
|
||||||
|
*/
|
||||||
|
constructor(classFileStoreStrategyFilesystem, storagePath, classFileStoreStrategyGridFs, gridFsBucket) {
|
||||||
|
this.classFileStoreStrategyFilesystem = classFileStoreStrategyFilesystem;
|
||||||
|
this.storagePath = storagePath;
|
||||||
|
this.classFileStoreStrategyGridFs = classFileStoreStrategyGridFs;
|
||||||
|
this.gridFsBucket = gridFsBucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns the right FileStoreStrategy
|
||||||
|
* @param fileObj the current file object
|
||||||
|
* @param versionName the current version
|
||||||
|
* @param use this storage, or if not set, get the storage from fileObj
|
||||||
|
*/
|
||||||
|
getFileStrategy(fileObj, versionName, storage) {
|
||||||
|
if (!storage) {
|
||||||
|
storage = fileObj.versions[versionName].storage;
|
||||||
|
if (!storage) {
|
||||||
|
if (fileObj.meta.source == "import") {
|
||||||
|
// uploaded by import, so it's in GridFS (MongoDB)
|
||||||
|
storage = STORAGE_NAME_GRIDFS;
|
||||||
|
} else {
|
||||||
|
// newly uploaded, so it's at the filesystem
|
||||||
|
storage = STORAGE_NAME_FILESYSTEM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ret;
|
||||||
|
if ([STORAGE_NAME_FILESYSTEM, STORAGE_NAME_GRIDFS].includes(storage)) {
|
||||||
|
if (storage == STORAGE_NAME_FILESYSTEM) {
|
||||||
|
ret = new this.classFileStoreStrategyFilesystem(fileObj, versionName);
|
||||||
|
} else if (storage == STORAGE_NAME_GRIDFS) {
|
||||||
|
ret = new this.classFileStoreStrategyGridFs(this.gridFsBucket, fileObj, versionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Strategy to store files */
|
||||||
|
class FileStoreStrategy {
|
||||||
|
|
||||||
|
/** constructor
|
||||||
|
* @param fileObj the current file object
|
||||||
|
* @param versionName the current version
|
||||||
|
*/
|
||||||
|
constructor(fileObj, versionName) {
|
||||||
|
this.fileObj = fileObj;
|
||||||
|
this.versionName = versionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** after successfull upload */
|
||||||
|
onAfterUpload() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** download the file
|
||||||
|
* @param http the current http request
|
||||||
|
* @param cacheControl cacheControl of FilesCollection
|
||||||
|
*/
|
||||||
|
interceptDownload(http, cacheControl) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** after file remove */
|
||||||
|
onAfterRemove() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns a read stream
|
||||||
|
* @return the read stream
|
||||||
|
*/
|
||||||
|
getReadStream() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns a write stream
|
||||||
|
* @param filePath if set, use this path
|
||||||
|
* @return the write stream
|
||||||
|
*/
|
||||||
|
getWriteStream(filePath) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** writing finished
|
||||||
|
* @param finishedData the data of the write stream finish event
|
||||||
|
*/
|
||||||
|
writeStreamFinished(finishedData) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns the new file path
|
||||||
|
* @param storagePath use this storage path
|
||||||
|
* @return the new file path
|
||||||
|
*/
|
||||||
|
getNewPath(storagePath, name) {
|
||||||
|
if (!_.isString(name)) {
|
||||||
|
name = this.fileObj.name;
|
||||||
|
}
|
||||||
|
const ret = path.join(storagePath, this.fileObj._id + "-" + this.versionName + "-" + name);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** remove the file */
|
||||||
|
unlink() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** return the storage name
|
||||||
|
* @return the storage name
|
||||||
|
*/
|
||||||
|
getStorageName() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Strategy to store attachments at GridFS (MongoDB) */
|
||||||
|
export class FileStoreStrategyGridFs extends FileStoreStrategy {
|
||||||
|
|
||||||
|
/** constructor
|
||||||
|
* @param gridFsBucket use this GridFS Bucket
|
||||||
|
* @param fileObj the current file object
|
||||||
|
* @param versionName the current version
|
||||||
|
*/
|
||||||
|
constructor(gridFsBucket, fileObj, versionName) {
|
||||||
|
super(fileObj, versionName);
|
||||||
|
this.gridFsBucket = gridFsBucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** download the file
|
||||||
|
* @param http the current http request
|
||||||
|
* @param cacheControl cacheControl of FilesCollection
|
||||||
|
*/
|
||||||
|
interceptDownload(http, cacheControl) {
|
||||||
|
const readStream = this.getReadStream();
|
||||||
|
const downloadFlag = http?.params?.query?.download;
|
||||||
|
|
||||||
|
let ret = false;
|
||||||
|
if (readStream) {
|
||||||
|
ret = true;
|
||||||
|
httpStreamOutput(readStream, this.fileObj.name, http, downloadFlag, cacheControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** after file remove */
|
||||||
|
onAfterRemove() {
|
||||||
|
this.unlink();
|
||||||
|
super.onAfterRemove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns a read stream
|
||||||
|
* @return the read stream
|
||||||
|
*/
|
||||||
|
getReadStream() {
|
||||||
|
const gfsId = this.getGridFsObjectId();
|
||||||
|
let ret;
|
||||||
|
if (gfsId) {
|
||||||
|
ret = this.gridFsBucket.openDownloadStream(gfsId);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns a write stream
|
||||||
|
* @param filePath if set, use this path
|
||||||
|
* @return the write stream
|
||||||
|
*/
|
||||||
|
getWriteStream(filePath) {
|
||||||
|
const fileObj = this.fileObj;
|
||||||
|
const versionName = this.versionName;
|
||||||
|
const metadata = { ...fileObj.meta, versionName, fileId: fileObj._id };
|
||||||
|
const ret = this.gridFsBucket.openUploadStream(this.fileObj.name, {
|
||||||
|
contentType: fileObj.type || 'binary/octet-stream',
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** writing finished
|
||||||
|
* @param finishedData the data of the write stream finish event
|
||||||
|
*/
|
||||||
|
writeStreamFinished(finishedData) {
|
||||||
|
const gridFsFileIdName = this.getGridFsFileIdName();
|
||||||
|
Attachments.update({ _id: this.fileObj._id }, { $set: { [gridFsFileIdName]: finishedData._id.toHexString(), } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** remove the file */
|
||||||
|
unlink() {
|
||||||
|
const gfsId = this.getGridFsObjectId();
|
||||||
|
if (gfsId) {
|
||||||
|
this.gridFsBucket.delete(gfsId, err => {
|
||||||
|
if (err) {
|
||||||
|
console.error("error on gfs bucket.delete: ", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const gridFsFileIdName = this.getGridFsFileIdName();
|
||||||
|
Attachments.update({ _id: this.fileObj._id }, { $unset: { [gridFsFileIdName]: 1 } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** return the storage name
|
||||||
|
* @return the storage name
|
||||||
|
*/
|
||||||
|
getStorageName() {
|
||||||
|
return STORAGE_NAME_GRIDFS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns the GridFS Object-Id
|
||||||
|
* @return the GridFS Object-Id
|
||||||
|
*/
|
||||||
|
getGridFsObjectId() {
|
||||||
|
let ret;
|
||||||
|
const gridFsFileId = this.getGridFsFileId();
|
||||||
|
if (gridFsFileId) {
|
||||||
|
ret = createObjectId({ gridFsFileId });
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns the GridFS Object-Id
|
||||||
|
* @return the GridFS Object-Id
|
||||||
|
*/
|
||||||
|
getGridFsFileId() {
|
||||||
|
const ret = (this.fileObj.versions[this.versionName].meta || {})
|
||||||
|
.gridFsFileId;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns the property name of gridFsFileId
|
||||||
|
* @return the property name of gridFsFileId
|
||||||
|
*/
|
||||||
|
getGridFsFileIdName() {
|
||||||
|
const ret = `versions.${this.versionName}.meta.gridFsFileId`;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Strategy to store attachments at filesystem */
|
||||||
|
export class FileStoreStrategyFilesystem extends FileStoreStrategy {
|
||||||
|
|
||||||
|
/** constructor
|
||||||
|
* @param fileObj the current file object
|
||||||
|
* @param versionName the current version
|
||||||
|
*/
|
||||||
|
constructor(fileObj, versionName) {
|
||||||
|
super(fileObj, versionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns a read stream
|
||||||
|
* @return the read stream
|
||||||
|
*/
|
||||||
|
getReadStream() {
|
||||||
|
const ret = fs.createReadStream(this.fileObj.versions[this.versionName].path)
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns a write stream
|
||||||
|
* @param filePath if set, use this path
|
||||||
|
* @return the write stream
|
||||||
|
*/
|
||||||
|
getWriteStream(filePath) {
|
||||||
|
if (!_.isString(filePath)) {
|
||||||
|
filePath = this.fileObj.versions[this.versionName].path;
|
||||||
|
}
|
||||||
|
const ret = fs.createWriteStream(filePath);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** writing finished
|
||||||
|
* @param finishedData the data of the write stream finish event
|
||||||
|
*/
|
||||||
|
writeStreamFinished(finishedData) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** remove the file */
|
||||||
|
unlink() {
|
||||||
|
const filePath = this.fileObj.versions[this.versionName].path;
|
||||||
|
fs.unlink(filePath, () => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** return the storage name
|
||||||
|
* @return the storage name
|
||||||
|
*/
|
||||||
|
getStorageName() {
|
||||||
|
return STORAGE_NAME_FILESYSTEM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** move the fileObj to another storage
|
||||||
|
* @param fileObj move this fileObj to another storage
|
||||||
|
* @param storageDestination the storage destination (fs or gridfs)
|
||||||
|
* @param fileStoreStrategyFactory get FileStoreStrategy from this factory
|
||||||
|
*/
|
||||||
|
export const moveToStorage = function(fileObj, storageDestination, fileStoreStrategyFactory) {
|
||||||
|
Object.keys(fileObj.versions).forEach(versionName => {
|
||||||
|
const strategyRead = fileStoreStrategyFactory.getFileStrategy(fileObj, versionName);
|
||||||
|
const strategyWrite = fileStoreStrategyFactory.getFileStrategy(fileObj, versionName, storageDestination);
|
||||||
|
|
||||||
|
if (strategyRead.constructor.name != strategyWrite.constructor.name) {
|
||||||
|
const readStream = strategyRead.getReadStream();
|
||||||
|
|
||||||
|
const filePath = strategyWrite.getNewPath(fileStoreStrategyFactory.storagePath);
|
||||||
|
const writeStream = strategyWrite.getWriteStream(filePath);
|
||||||
|
|
||||||
|
writeStream.on('error', error => {
|
||||||
|
console.error('[writeStream error]: ', error, fileObjId);
|
||||||
|
});
|
||||||
|
|
||||||
|
readStream.on('error', error => {
|
||||||
|
console.error('[readStream error]: ', error, fileObjId);
|
||||||
|
});
|
||||||
|
|
||||||
|
writeStream.on('finish', Meteor.bindEnvironment((finishedData) => {
|
||||||
|
strategyWrite.writeStreamFinished(finishedData);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// https://forums.meteor.com/t/meteor-code-must-always-run-within-a-fiber-try-wrapping-callbacks-that-you-pass-to-non-meteor-libraries-with-meteor-bindenvironmen/40099/8
|
||||||
|
readStream.on('end', Meteor.bindEnvironment(() => {
|
||||||
|
Attachments.update({ _id: fileObj._id }, { $set: {
|
||||||
|
[`versions.${versionName}.storage`]: strategyWrite.getStorageName(),
|
||||||
|
[`versions.${versionName}.path`]: filePath,
|
||||||
|
} });
|
||||||
|
strategyRead.unlink();
|
||||||
|
}));
|
||||||
|
|
||||||
|
readStream.pipe(writeStream);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
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}`;
|
|
||||||
};
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
31
models/lib/httpStream.js
Normal file
31
models/lib/httpStream.js
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
export const httpStreamOutput = function(readStream, name, http, downloadFlag, cacheControl) {
|
||||||
|
readStream.on('data', data => {
|
||||||
|
http.response.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
readStream.on('end', () => {
|
||||||
|
// don't pass parameters to end() or it will be attached to the file's binary stream
|
||||||
|
http.response.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
readStream.on('error', () => {
|
||||||
|
http.response.statusCode = 404;
|
||||||
|
http.response.end('not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cacheControl) {
|
||||||
|
http.response.setHeader('Cache-Control', cacheControl);
|
||||||
|
}
|
||||||
|
http.response.setHeader('Content-Disposition', getContentDisposition(name, http?.params?.query?.download));
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 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}`;
|
||||||
|
};
|
||||||
|
|
@ -1216,7 +1216,7 @@ Migrations.add('migrate-attachments-collectionFS-to-ostrioFiles', () => {
|
||||||
cardId: fileObj.cardId,
|
cardId: fileObj.cardId,
|
||||||
listId: fileObj.listId,
|
listId: fileObj.listId,
|
||||||
swimlaneId: fileObj.swimlaneId,
|
swimlaneId: fileObj.swimlaneId,
|
||||||
source: 'import,'
|
source: 'import'
|
||||||
},
|
},
|
||||||
userId,
|
userId,
|
||||||
size: fileSize,
|
size: fileSize,
|
||||||
|
|
@ -1328,3 +1328,12 @@ Migrations.add('migrate-attachment-drop-index-cardId', () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Migrations.add('migrate-attachment-migration-fix-source-import', () => {
|
||||||
|
// there was an error at first versions, so source was import, instead of import
|
||||||
|
Attachments.update(
|
||||||
|
{"meta.source":"import,"},
|
||||||
|
{$set:{"meta.source":"import"}},
|
||||||
|
noValidateMulti
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,24 @@
|
||||||
import Attachments, { AttachmentStorage } from '/models/attachments';
|
import Attachments from '/models/attachments';
|
||||||
import { ObjectID } from 'bson';
|
import { ObjectID } from 'bson';
|
||||||
|
|
||||||
Meteor.publish('attachmentsList', function() {
|
Meteor.publish('attachmentsList', function(limit) {
|
||||||
// eslint-disable-next-line no-console
|
const ret = Attachments.find(
|
||||||
// console.log('attachments:', AttachmentStorage.find());
|
|
||||||
const files = AttachmentStorage.find(
|
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
fields: {
|
fields: {
|
||||||
_id: 1,
|
_id: 1,
|
||||||
filename: 1,
|
name: 1,
|
||||||
md5: 1,
|
size: 1,
|
||||||
length: 1,
|
type: 1,
|
||||||
contentType: 1,
|
meta: 1,
|
||||||
metadata: 1,
|
path: 1,
|
||||||
|
versions: 1,
|
||||||
},
|
},
|
||||||
sort: {
|
sort: {
|
||||||
filename: 1,
|
name: 1,
|
||||||
},
|
},
|
||||||
limit: 250,
|
limit: limit,
|
||||||
},
|
},
|
||||||
);
|
).cursor;
|
||||||
const attIds = [];
|
return ret;
|
||||||
files.forEach(file => {
|
|
||||||
attIds.push(file._id._str);
|
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
|
||||||
files,
|
|
||||||
Attachments.find({ 'copies.attachments.key': { $in: attIds } }),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
Meteor.publish('orphanedAttachments', function() {
|
|
||||||
let keys = [];
|
|
||||||
|
|
||||||
if (Attachments.find({}, { fields: { copies: 1 } }) !== undefined) {
|
|
||||||
Attachments.find({}, { fields: { copies: 1 } }).forEach(att => {
|
|
||||||
keys.push(new ObjectID(att.copies.attachments.key));
|
|
||||||
});
|
|
||||||
keys.sort();
|
|
||||||
keys = _.uniq(keys, true);
|
|
||||||
|
|
||||||
return AttachmentStorage.find(
|
|
||||||
{ _id: { $nin: keys } },
|
|
||||||
{
|
|
||||||
fields: {
|
|
||||||
_id: 1,
|
|
||||||
filename: 1,
|
|
||||||
md5: 1,
|
|
||||||
length: 1,
|
|
||||||
contentType: 1,
|
|
||||||
metadata: 1,
|
|
||||||
},
|
|
||||||
sort: {
|
|
||||||
filename: 1,
|
|
||||||
},
|
|
||||||
limit: 250,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue