diff --git a/.gitignore b/.gitignore index e30d6282f..e159c4fe2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ *~ -*.swp +*.sw* .meteor-spk *.sublime-workspace tmp/ diff --git a/client/components/cards/attachments.jade b/client/components/cards/attachments.jade index 74a22f38d..975d21e84 100644 --- a/client/components/cards/attachments.jade +++ b/client/components/cards/attachments.jade @@ -49,17 +49,7 @@ template(name="attachmentsGalery") if currentUser.isBoardMember unless currentUser.isCommentOnly unless currentUser.isWorker - if isImage - 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'}} + a.fa.fa-navicon.attachment-details-menu.js-open-attachment-menu(title="{{_ 'attachmentActionsPopup-title'}}") if currentUser.isBoardMember unless currentUser.isCommentOnly @@ -67,3 +57,31 @@ template(name="attachmentsGalery") //li.attachment-item.add-attachment a.js-add-attachment(title="{{_ 'add-attachment' }}") 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'}} diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index bfa345186..8c78b363d 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -1,23 +1,11 @@ Template.attachmentsGalery.events({ '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 // content, see #101. 'click .js-download'(event) { event.stopPropagation(); }, - 'click .js-add-cover'() { - Cards.findOne(this.meta.cardId).setCover(this._id); - }, - 'click .js-remove-cover'() { - Cards.findOne(this.meta.cardId).unsetCover(); - }, + 'click .js-open-attachment-menu': Popup.open('attachmentActions'), }); Template.attachmentsGalery.helpers({ @@ -33,12 +21,16 @@ Template.cardAttachmentsPopup.events({ 'change .js-attach-file'(event) { const card = this; 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( - { - file: event.currentTarget.files[0], - meta: Utils.getCommonAttachmentMetaFrom(card), - chunkSize: 'dynamic', - }, + config, false, ); uploader.on('uploaded', (error, fileRef) => { @@ -104,13 +96,17 @@ Template.previewClipboardImagePopup.events({ if (pastedResults && pastedResults.file) { const file = pastedResults.file; 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( - { - file, - meta: Utils.getCommonAttachmentMetaFrom(card), - fileName: file.name || file.type.replace('image/', 'clipboard.'), - chunkSize: 'dynamic', - }, + config, false, ); 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'); diff --git a/client/components/cards/attachments.styl b/client/components/cards/attachments.styl index a8320adcd..02566b850 100644 --- a/client/components/cards/attachments.styl +++ b/client/components/cards/attachments.styl @@ -46,6 +46,9 @@ .attachment-details-actions a display: block + &.attachment-details-menu + padding-top: 10px + .attachment-image-preview max-width: 100px display: block diff --git a/client/components/settings/adminReports.jade b/client/components/settings/adminReports.jade index 7a1b8cb96..961980b46 100644 --- a/client/components/settings/adminReports.jade +++ b/client/components/settings/adminReports.jade @@ -11,11 +11,6 @@ template(name="adminReports") i.fa.fa-chain-broken | {{_ 'broken-cards'}} - li - a.js-report-files(data-id="report-orphaned-files") - i.fa.fa-paperclip - | {{_ 'orphanedFilesReportTitle'}} - li a.js-report-files(data-id="report-files") i.fa.fa-paperclip @@ -43,8 +38,6 @@ template(name="adminReports") +brokenCardsReport else if showFilesReport.get +filesReport - else if showOrphanedFilesReport.get - +orphanedFilesReport else if showRulesReport.get +rulesReport else if showBoardsReport.get @@ -64,7 +57,7 @@ template(name="brokenCardsReport") template(name="rulesReport") h1 {{_ 'rulesReportTitle'}} if resultsCount - table.table + table tr th Rule Title th Board Title @@ -83,44 +76,23 @@ template(name="rulesReport") template(name="filesReport") h1 {{_ 'filesReportTitle'}} if resultsCount - table.table + table tr th Filename th.right Size (kB) th MIME Type - th.center Usage - th MD5 Sum - th ID + th Attachment ID + th Board ID + th Card ID each att in results tr - td {{ att.filename }} - td.right {{fileSize att.length }} - td {{ att.contentType }} - td.center {{usageCount att._id.toHexString }} - td {{ att.md5 }} - td {{ att._id.toHexString }} - 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 }} + td {{ att.name }} + td.right {{fileSize att.size }} + td {{ att.type }} + td {{ att._id }} + td {{ att.meta.boardId }} + td {{ att.meta.cardId }} else div {{_ 'no-results' }} diff --git a/client/components/settings/adminReports.js b/client/components/settings/adminReports.js index 6dcbb0fc4..c1c58692a 100644 --- a/client/components/settings/adminReports.js +++ b/client/components/settings/adminReports.js @@ -1,4 +1,4 @@ -import { AttachmentStorage } from '/models/attachments'; +import Attachments from '/models/attachments'; import { CardSearchPagedComponent } from '/client/lib/cardSearch'; import SessionData from '/models/usersessiondata'; import { QueryParams } from '/config/query-classes'; @@ -25,7 +25,6 @@ BlazeComponent.extendComponent({ { 'click a.js-report-broken': 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-cards': this.switchMenu, 'click a.js-report-boards': this.switchMenu, @@ -65,11 +64,6 @@ BlazeComponent.extendComponent({ this.subscription = Meteor.subscribe('attachmentsList', () => { 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) { this.subscription = Meteor.subscribe('rulesReport', () => { this.showRulesReport.set(true); @@ -103,8 +97,6 @@ class AdminReport extends BlazeComponent { results() { // eslint-disable-next-line no-console - // console.log('attachments:', AttachmentStorage.find()); - // console.log('attachments.count:', AttachmentStorage.find().count()); return this.collection.find(); } @@ -124,10 +116,6 @@ class AdminReport extends BlazeComponent { return Math.round(size / 1024); } - usageCount(key) { - return Attachments.find({ 'copies.attachments.key': key }).count(); - } - abbreviate(text) { if (text.length > 30) { return `${text.substr(0, 29)}...`; @@ -137,13 +125,9 @@ class AdminReport extends BlazeComponent { } (class extends AdminReport { - collection = AttachmentStorage; + collection = Attachments; }.register('filesReport')); -(class extends AdminReport { - collection = AttachmentStorage; -}.register('orphanedFilesReport')); - (class extends AdminReport { collection = Rules; diff --git a/client/components/settings/adminReports.styl b/client/components/settings/adminReports.styl deleted file mode 100644 index 3a5234842..000000000 --- a/client/components/settings/adminReports.styl +++ /dev/null @@ -1,3 +0,0 @@ -.admin-reports-content - height: auto !important - diff --git a/client/components/settings/attachments.jade b/client/components/settings/attachments.jade new file mode 100644 index 000000000..0c35fb0e4 --- /dev/null +++ b/client/components/settings/attachments.jade @@ -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'}} diff --git a/client/components/settings/attachments.js b/client/components/settings/attachments.js new file mode 100644 index 000000000..c101ffcc7 --- /dev/null +++ b/client/components/settings/attachments.js @@ -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'); diff --git a/client/components/settings/attachments.styl b/client/components/settings/attachments.styl new file mode 100644 index 000000000..df3635995 --- /dev/null +++ b/client/components/settings/attachments.styl @@ -0,0 +1,8 @@ +.move-attachment-buttons + display: flex + gap: 10px + +.attachments-content + hr + height: 0px + border: 1px solid black diff --git a/client/components/settings/peopleBody.styl b/client/components/settings/peopleBody.styl index 9ab1c1f4b..2d524b336 100644 --- a/client/components/settings/peopleBody.styl +++ b/client/components/settings/peopleBody.styl @@ -2,8 +2,6 @@ overflow: scroll; table - border-collapse: collapse; - width: 100%; color: #000; td, th @@ -22,14 +20,13 @@ table .ext-box-left display: flex; width: 100% + gap: 10px span vertical-align: center; line-height: 34px; - margin-right: 10px; input, button - margin: 0 10px 0 0; padding: 0; button diff --git a/client/components/settings/settingBody.styl b/client/components/settings/settingBody.styl index fed061b76..6905f6182 100644 --- a/client/components/settings/settingBody.styl +++ b/client/components/settings/settingBody.styl @@ -7,11 +7,9 @@ display: flex .setting-content - padding 30px color: #727479 background: #dedede width 100% - height calc(100% - 80px) position: absolute; .content-title @@ -21,6 +19,7 @@ display flex padding-top 15px height 100% + gap: 10px; .side-menu background-color: #f7f7f7; @@ -54,7 +53,6 @@ margin-right: 20px .main-body - padding: 0.1em 1em -webkit-user-select: text // Safari 3.1+ -moz-user-select: text // Firefox 2+ -ms-user-select: text // IE 10+ diff --git a/client/components/settings/settingHeader.jade b/client/components/settings/settingHeader.jade index 14de3ab1f..72ed611c8 100644 --- a/client/components/settings/settingHeader.jade +++ b/client/components/settings/settingHeader.jade @@ -16,6 +16,10 @@ template(name="settingHeaderBar") i.fa(class="fa-list") 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'}}") i.fa(class="fa-info-circle") span {{_ 'info'}} diff --git a/config/router.js b/config/router.js index 7e8f78660..43900da69 100644 --- a/config/router.js +++ b/config/router.js @@ -355,6 +355,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 = { action() { BlazeLayout.render('defaultLayout', { content: 'notFound' }); diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 374a78834..a4f2d204c 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -1086,7 +1086,6 @@ "custom-field-stringtemplate-item-placeholder": "Press enter to add more items", "creator": "Creator", "filesReportTitle": "Files Report", - "orphanedFilesReportTitle": "Orphaned Files Report", "reports": "Reports", "rulesReportTitle": "Rules Report", "boardsReportTitle": "Boards Report", @@ -1164,5 +1163,19 @@ "copyChecklist": "Copy Checklist", "copyChecklistPopup-title": "Copy Checklist", "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" } diff --git a/models/activities.js b/models/activities.js index ff036a037..2b1406753 100644 --- a/models/activities.js +++ b/models/activities.js @@ -247,9 +247,8 @@ if (Meteor.isServer) { params.commentId = comment._id; } if (activity.attachmentId) { - const attachment = activity.attachment(); - params.attachment = attachment.name; - params.attachmentId = attachment._id; + params.attachment = activity.attachmentName; + params.attachmentId = activity.attachmentId; } if (activity.checklistId) { const checklist = activity.checklist(); diff --git a/models/attachments.js b/models/attachments.js index 6d2e95f0e..bbbf8e596 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -1,31 +1,19 @@ import { Meteor } from 'meteor/meteor'; import { FilesCollection } from 'meteor/ostrio:files'; +import { createBucket } from './lib/grid/createBucket'; import fs from 'fs'; import path from 'path'; -import { createBucket } from './lib/grid/createBucket'; -import { createOnAfterUpload } from './lib/fsHooks/createOnAfterUpload'; -import { createInterceptDownload } from './lib/fsHooks/createInterceptDownload'; -import { createOnAfterRemove } from './lib/fsHooks/createOnAfterRemove'; +import { AttachmentStoreStrategyFilesystem, AttachmentStoreStrategyGridFs} from '/models/lib/attachmentStoreStrategy'; +import FileStoreStrategyFactory, {moveToStorage, STORAGE_NAME_FILESYSTEM, STORAGE_NAME_GRIDFS} from '/models/lib/fileStoreStrategy'; let attachmentBucket; +let storagePath; if (Meteor.isServer) { attachmentBucket = createBucket('attachments'); + storagePath = path.join(process.env.WRITABLE_PATH, 'attachments'); } -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, - }); +export const fileStoreStrategyFactory = new FileStoreStrategyFactory(AttachmentStoreStrategyFilesystem, storagePath, AttachmentStoreStrategyGridFs, attachmentBucket); // XXX Enforce a schema for the Attachments FilesCollection // see: https://github.com/VeliovGroup/Meteor-Files/wiki/Schema @@ -34,26 +22,34 @@ Attachments = new FilesCollection({ debug: false, // Change to `true` for debugging collectionName: 'attachments', 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() { - if (process.env.WRITABLE_PATH) { - return path.join(process.env.WRITABLE_PATH, 'uploads', 'attachments'); - } - return path.normalize(`assets/app/uploads/${this.collectionName}`); + const ret = fileStoreStrategyFactory.storagePath; + return ret; }, - onAfterUpload: function onAfterUpload(fileRef) { - createOnAfterUpload(attachmentBucket).call(this, fileRef); - // If the attachment doesn't have a source field - // or its source is different than import - if (!fileRef.meta.source || fileRef.meta.source !== 'import') { - // Add activity about adding the attachment - insertActivity(fileRef, 'addAttachment'); - } + onAfterUpload(fileObj) { + // current storage is the filesystem, update object and database + Object.keys(fileObj.versions).forEach(versionName => { + fileObj.versions[versionName].storage = STORAGE_NAME_FILESYSTEM; + }); + Attachments.update({ _id: fileObj._id }, { $set: { "versions" : fileObj.versions } }); + moveToStorage(fileObj, STORAGE_NAME_GRIDFS, fileStoreStrategyFactory); }, - interceptDownload: createInterceptDownload(attachmentBucket), - onAfterRemove: function onAfterRemove(files) { - createOnAfterRemove(attachmentBucket).call(this, files); + interceptDownload(http, fileObj, versionName) { + const ret = fileStoreStrategyFactory.getFileStrategy(fileObj, versionName).interceptDownload(http, this.cacheControl); + return ret; + }, + onAfterRemove(files) { files.forEach(fileObj => { - insertActivity(fileObj, 'deleteAttachment'); + Object.keys(fileObj.versions).forEach(versionName => { + fileStoreStrategyFactory.getFileStrategy(fileObj, versionName).onAfterRemove(); + }); }); }, // We authorize the attachment download either: @@ -82,9 +78,19 @@ if (Meteor.isServer) { 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(() => { Attachments.collection._ensureIndex({ 'meta.cardId': 1 }); - const storagePath = Attachments.storagePath(); + const storagePath = fileStoreStrategyFactory.storagePath; if (!fs.existsSync(storagePath)) { console.log("create storagePath because it doesn't exist: " + storagePath); fs.mkdirSync(storagePath, { recursive: true }); diff --git a/models/avatars.js b/models/avatars.js index ff47aa150..49fd4c8af 100644 --- a/models/avatars.js +++ b/models/avatars.js @@ -1,26 +1,26 @@ import { Meteor } from 'meteor/meteor'; import { FilesCollection } from 'meteor/ostrio:files'; +import { createBucket } from './lib/grid/createBucket'; import fs from 'fs'; import path from 'path'; -import { createBucket } from './lib/grid/createBucket'; -import { createOnAfterUpload } from './lib/fsHooks/createOnAfterUpload'; -import { createInterceptDownload } from './lib/fsHooks/createInterceptDownload'; -import { createOnAfterRemove } from './lib/fsHooks/createOnAfterRemove'; +import FileStoreStrategyFactory, { FileStoreStrategyFilesystem, FileStoreStrategyGridFs} from '/models/lib/fileStoreStrategy'; let avatarsBucket; +let storagePath; if (Meteor.isServer) { avatarsBucket = createBucket('avatars'); + storagePath = path.join(process.env.WRITABLE_PATH, 'avatars'); } +const fileStoreStrategyFactory = new FileStoreStrategyFactory(FileStoreStrategyFilesystem, storagePath, FileStoreStrategyGridFs, avatarsBucket); + 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}`);; + const ret = fileStoreStrategyFactory.storagePath; + return ret; }, onBeforeUpload(file) { if (file.size <= 72000 && file.type.startsWith('image/')) { @@ -28,9 +28,24 @@ Avatars = new FilesCollection({ } return 'avatar-too-big'; }, - onAfterUpload: createOnAfterUpload(avatarsBucket), - interceptDownload: createInterceptDownload(avatarsBucket), - onAfterRemove: createOnAfterRemove(avatarsBucket), + onAfterUpload(fileObj) { + // current storage is the filesystem, update object and database + 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) { @@ -46,7 +61,7 @@ if (Meteor.isServer) { }); Meteor.startup(() => { - const storagePath = Avatars.storagePath(); + const storagePath = fileStoreStrategyFactory.storagePath; if (!fs.existsSync(storagePath)) { console.log("create storagePath because it doesn't exist: " + storagePath); fs.mkdirSync(storagePath, { recursive: true }); diff --git a/models/lib/attachmentStoreStrategy.js b/models/lib/attachmentStoreStrategy.js new file mode 100644 index 000000000..6e7ebb2c8 --- /dev/null +++ b/models/lib/attachmentStoreStrategy.js @@ -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'); + } +} diff --git a/models/lib/fileStoreStrategy.js b/models/lib/fileStoreStrategy.js new file mode 100644 index 000000000..d8f0cc15b --- /dev/null +++ b/models/lib/fileStoreStrategy.js @@ -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); + } + }); +}; diff --git a/models/lib/fsHooks/createInterceptDownload.js b/models/lib/fsHooks/createInterceptDownload.js deleted file mode 100644 index c375d03f8..000000000 --- a/models/lib/fsHooks/createInterceptDownload.js +++ /dev/null @@ -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}`; -}; diff --git a/models/lib/fsHooks/createOnAfterRemove.js b/models/lib/fsHooks/createOnAfterRemove.js deleted file mode 100644 index f2e0a4ba7..000000000 --- a/models/lib/fsHooks/createOnAfterRemove.js +++ /dev/null @@ -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); - }); - } - }); - }); - }; diff --git a/models/lib/fsHooks/createOnAfterUpload.js b/models/lib/fsHooks/createOnAfterUpload.js deleted file mode 100644 index 3c6c54036..000000000 --- a/models/lib/fsHooks/createOnAfterUpload.js +++ /dev/null @@ -1,51 +0,0 @@ -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 - }), - ); - }); - }; diff --git a/models/lib/httpStream.js b/models/lib/httpStream.js new file mode 100644 index 000000000..4b156a9c8 --- /dev/null +++ b/models/lib/httpStream.js @@ -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}`; +}; diff --git a/server/migrations.js b/server/migrations.js index 8486de1f9..55f3eecb3 100644 --- a/server/migrations.js +++ b/server/migrations.js @@ -1178,8 +1178,7 @@ Migrations.add('add-card-details-show-lists', () => { }); Migrations.add('migrate-attachments-collectionFS-to-ostrioFiles', () => { - //const storagePath = Attachments.storagePath(); - const storagePath = process.env.WRITABLE_PATH || `./wekan-uploads`; + const storagePath = Attachments.storagePath(); if (!fs.existsSync(storagePath)) { console.log("create storagePath because it doesn't exist: " + storagePath); fs.mkdirSync(storagePath, { recursive: true }); @@ -1221,7 +1220,7 @@ Migrations.add('migrate-attachments-collectionFS-to-ostrioFiles', () => { cardId: fileObj.cardId, listId: fileObj.listId, swimlaneId: fileObj.swimlaneId, - source: 'import,' + source: 'import' }, userId, size: fileSize, @@ -1246,8 +1245,7 @@ Migrations.add('migrate-attachments-collectionFS-to-ostrioFiles', () => { }); Migrations.add('migrate-avatars-collectionFS-to-ostrioFiles', () => { - //const storagePath = Avatars.storagePath(); - const storagePath = process.env.WRITABLE_PATH || `./wekan-uploads`; + const storagePath = Avatars.storagePath(); if (!fs.existsSync(storagePath)) { console.log("create storagePath because it doesn't exist: " + storagePath); fs.mkdirSync(storagePath, { recursive: true }); @@ -1334,3 +1332,12 @@ Migrations.add('migrate-attachment-drop-index-cardId', () => { } 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 + ); +}); diff --git a/server/publications/attachments.js b/server/publications/attachments.js index 18566f7da..d6eab790c 100644 --- a/server/publications/attachments.js +++ b/server/publications/attachments.js @@ -1,65 +1,24 @@ -import Attachments, { AttachmentStorage } from '/models/attachments'; +import Attachments from '/models/attachments'; import { ObjectID } from 'bson'; -Meteor.publish('attachmentsList', function() { - // eslint-disable-next-line no-console - // console.log('attachments:', AttachmentStorage.find()); - const files = AttachmentStorage.find( +Meteor.publish('attachmentsList', function(limit) { + const ret = Attachments.find( {}, { fields: { _id: 1, - filename: 1, - md5: 1, - length: 1, - contentType: 1, - metadata: 1, + name: 1, + size: 1, + type: 1, + meta: 1, + path: 1, + versions: 1, }, sort: { - filename: 1, + name: 1, }, - limit: 250, + limit: limit, }, - ); - const attIds = []; - 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 []; - } + ).cursor; + return ret; });