diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index 7a3245320..8c78b363d 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -147,11 +147,11 @@ BlazeComponent.extendComponent({ Popup.back(); }, 'click .js-move-storage-fs'() { - Meteor.call('moveToStorage', this.data()._id, "fs"); + Meteor.call('moveAttachmentToStorage', this.data()._id, "fs"); Popup.back(); }, 'click .js-move-storage-gridfs'() { - Meteor.call('moveToStorage', this.data()._id, "gridfs"); + Meteor.call('moveAttachmentToStorage', this.data()._id, "gridfs"); Popup.back(); }, } diff --git a/models/attachments.js b/models/attachments.js index e8df5a333..1465092fe 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -1,8 +1,17 @@ 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 AttachmentStoreStrategy from '/models/lib/attachmentStoreStrategy'; +import { AttachmentStoreStrategyFilesystem, AttachmentStoreStrategyGridFs} from '/models/lib/attachmentStoreStrategy'; +import FileStoreStrategyFactory, {moveToStorage} from '/models/lib/fileStoreStrategy'; + +let attachmentBucket; +if (Meteor.isServer) { + attachmentBucket = createBucket('attachments'); +} + +const fileStoreStrategyFactory = new FileStoreStrategyFactory(AttachmentStoreStrategyFilesystem, AttachmentStoreStrategyGridFs, attachmentBucket); // XXX Enforce a schema for the Attachments FilesCollection // see: https://github.com/VeliovGroup/Meteor-Files/wiki/Schema @@ -26,17 +35,17 @@ Attachments = new FilesCollection({ }, onAfterUpload(fileObj) { Object.keys(fileObj.versions).forEach(versionName => { - AttachmentStoreStrategy.getFileStrategy(this, fileObj, versionName).onAfterUpload(); + fileStoreStrategyFactory.getFileStrategy(this, fileObj, versionName).onAfterUpload(); }) }, interceptDownload(http, fileObj, versionName) { - const ret = AttachmentStoreStrategy.getFileStrategy(this, fileObj, versionName).interceptDownload(http); + const ret = fileStoreStrategyFactory.getFileStrategy(this, fileObj, versionName).interceptDownload(http); return ret; }, onAfterRemove(files) { files.forEach(fileObj => { Object.keys(fileObj.versions).forEach(versionName => { - AttachmentStoreStrategy.getFileStrategy(this, fileObj, versionName).onAfterRemove(); + fileStoreStrategyFactory.getFileStrategy(this, fileObj, versionName).onAfterRemove(); }); }); }, @@ -67,41 +76,12 @@ if (Meteor.isServer) { }); Meteor.methods({ - moveToStorage(fileObjId, storageDestination) { + moveAttachmentToStorage(fileObjId, storageDestination) { check(fileObjId, String); check(storageDestination, String); const fileObj = Attachments.findOne({_id: fileObjId}); - - Object.keys(fileObj.versions).forEach(versionName => { - const strategyRead = AttachmentStoreStrategy.getFileStrategy(this, fileObj, versionName); - const strategyWrite = AttachmentStoreStrategy.getFileStrategy(this, fileObj, versionName, storageDestination); - - if (strategyRead.constructor.name != strategyWrite.constructor.name) { - const readStream = strategyRead.getReadStream(); - const writeStream = strategyWrite.getWriteStream(); - - 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() } }); - strategyRead.unlink(); - })); - - readStream.pipe(writeStream); - } - }); + moveToStorage(fileObj, storageDestination, fileStoreStrategyFactory); }, }); diff --git a/models/lib/attachmentStoreStrategy.js b/models/lib/attachmentStoreStrategy.js index c86aa9e1e..e525647af 100644 --- a/models/lib/attachmentStoreStrategy.js +++ b/models/lib/attachmentStoreStrategy.js @@ -1,9 +1,5 @@ import fs from 'fs'; -import { createBucket } from './grid/createBucket'; -import { createObjectId } from './grid/createObjectId'; -import { createOnAfterUpload } from './fsHooks/createOnAfterUpload'; -import { createInterceptDownload } from './fsHooks/createInterceptDownload'; -import { createOnAfterRemove } from './fsHooks/createOnAfterRemove'; +import FileStoreStrategy, {FileStoreStrategyFilesystem, FileStoreStrategyGridFs} from './fileStoreStrategy' const insertActivity = (fileObj, activityType) => Activities.insert({ @@ -18,27 +14,22 @@ const insertActivity = (fileObj, activityType) => swimlaneId: fileObj.meta.swimlaneId, }); -let attachmentBucket; -if (Meteor.isServer) { - attachmentBucket = createBucket('attachments'); -} - -/** Strategy to store attachments */ -class AttachmentStoreStrategy { +/** Strategy to store attachments at GridFS (MongoDB) */ +export class AttachmentStoreStrategyGridFs extends FileStoreStrategyGridFs { /** constructor + * @param gridFsBucket use this GridFS Bucket * @param filesCollection the current FilesCollection instance * @param fileObj the current file object * @param versionName the current version */ - constructor(filesCollection, fileObj, versionName) { - this.filesCollection = filesCollection; - this.fileObj = fileObj; - this.versionName = versionName; + constructor(gridFsBucket, filesCollection, fileObj, versionName) { + super(gridFsBucket, filesCollection, 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 @@ -46,64 +37,15 @@ class AttachmentStoreStrategy { } } - /** download the file - * @param http the current http request - */ - interceptDownload(http) { - } - /** after file remove */ onAfterRemove() { + super.onAfterRemove(); insertActivity(this.fileObj, 'deleteAttachment'); } - - /** returns a read stream - * @return the read stream - */ - getReadStream() { - } - - /** returns a write stream - * @return the write stream - */ - getWriteStream() { - } - - /** writing finished - * @param finishedData the data of the write stream finish event - */ - writeStreamFinished(finishedData) { - } - - /** remove the file */ - unlink() { - } - - /** return the storage name - * @return the storage name - */ - getStorageName() { - } - - static getFileStrategy(filesCollection, fileObj, versionName, storage) { - if (!storage) { - storage = fileObj.versions[versionName].storage || "gridfs"; - } - let ret; - if (["fs", "gridfs"].includes(storage)) { - if (storage == "fs") { - ret = new AttachmentStoreStrategyFilesystem(filesCollection, fileObj, versionName); - } else if (storage == "gridfs") { - ret = new AttachmentStoreStrategyGridFs(filesCollection, fileObj, versionName); - } - } - console.log("getFileStrategy: ", ret.constructor.name); - return ret; - } } -/** Strategy to store attachments at GridFS (MongoDB) */ -class AttachmentStoreStrategyGridFs extends AttachmentStoreStrategy { +/** Strategy to store attachments at filesystem */ +export class AttachmentStoreStrategyFilesystem extends FileStoreStrategyFilesystem { /** constructor * @param filesCollection the current FilesCollection instance @@ -116,131 +58,17 @@ class AttachmentStoreStrategyGridFs extends AttachmentStoreStrategy { /** after successfull upload */ onAfterUpload() { - createOnAfterUpload(this.filesCollection, attachmentBucket, this.fileObj, this.versionName); super.onAfterUpload(); - } - - /** download the file - * @param http the current http request - */ - interceptDownload(http) { - const ret = createInterceptDownload(this.filesCollection, attachmentBucket, this.fileObj, http, this.versionName); - return ret; + // 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() { - this.unlink(); super.onAfterRemove(); - } - - /** returns a read stream - * @return the read stream - */ - getReadStream() { - const gridFsFileId = (this.fileObj.versions[this.versionName].meta || {}) - .gridFsFileId; - let ret; - if (gridFsFileId) { - const gfsId = createObjectId({ gridFsFileId }); - ret = attachmentBucket.openDownloadStream(gfsId); - } - return ret; - } - - /** returns a write stream - * @return the write stream - */ - getWriteStream() { - const fileObj = this.fileObj; - const versionName = this.versionName; - const metadata = { ...fileObj.meta, versionName, fileId: fileObj._id }; - const ret = attachmentBucket.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() { - createOnAfterRemove(this.filesCollection, attachmentBucket, this.fileObj, this.versionName); - const gridFsFileIdName = this.getGridFsFileIdName(); - Attachments.update({ _id: this.fileObj._id }, { $unset: { [gridFsFileIdName]: 1 } }); - } - - /** return the storage name - * @return the storage name - */ - getStorageName() { - return "gridfs"; - } - - /** returns the property name of gridFsFileId - * @return the property name of gridFsFileId - */ - getGridFsFileIdName() { - const ret = `versions.${this.versionName}.meta.gridFsFileId`; - return ret; + insertActivity(this.fileObj, 'deleteAttachment'); } } - -/** Strategy to store attachments at filesystem */ -class AttachmentStoreStrategyFilesystem extends AttachmentStoreStrategy { - - /** constructor - * @param filesCollection the current FilesCollection instance - * @param fileObj the current file object - * @param versionName the current version - */ - constructor(filesCollection, fileObj, versionName) { - super(filesCollection, 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 - * @return the write stream - */ - getWriteStream() { - const newFileName = this.fileObj.name; - const 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 "fs"; - } -} - -export default AttachmentStoreStrategy; diff --git a/models/lib/fileStoreStrategy.js b/models/lib/fileStoreStrategy.js new file mode 100644 index 000000000..ff8cae7a2 --- /dev/null +++ b/models/lib/fileStoreStrategy.js @@ -0,0 +1,277 @@ +import fs from 'fs'; +import { createObjectId } from './grid/createObjectId'; +import { createOnAfterUpload } from './fsHooks/createOnAfterUpload'; +import { createInterceptDownload } from './fsHooks/createInterceptDownload'; +import { createOnAfterRemove } from './fsHooks/createOnAfterRemove'; + +/** Factory for FileStoreStrategy */ +export default class FileStoreStrategyFactory { + + /** constructor + * @param classFileStoreStrategyFilesystem use this strategy for filesystem storage + * @param classFileStoreStrategyGridFs use this strategy for GridFS storage + * @param gridFsBucket use this GridFS Bucket as GridFS Storage + */ + constructor(classFileStoreStrategyFilesystem, classFileStoreStrategyGridFs, gridFsBucket) { + this.classFileStoreStrategyFilesystem = classFileStoreStrategyFilesystem; + this.classFileStoreStrategyGridFs = classFileStoreStrategyGridFs; + this.gridFsBucket = gridFsBucket; + } + + /** returns the right FileStoreStrategy + * @param filesCollection the current FilesCollection instance + * @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(filesCollection, fileObj, versionName, storage) { + if (!storage) { + storage = fileObj.versions[versionName].storage || "gridfs"; + } + let ret; + if (["fs", "gridfs"].includes(storage)) { + if (storage == "fs") { + ret = new this.classFileStoreStrategyFilesystem(filesCollection, fileObj, versionName); + } else if (storage == "gridfs") { + ret = new this.classFileStoreStrategyGridFs(this.gridFsBucket, filesCollection, fileObj, versionName); + } + } + return ret; + } +} + +/** Strategy to store files */ +class FileStoreStrategy { + + /** constructor + * @param filesCollection the current FilesCollection instance + * @param fileObj the current file object + * @param versionName the current version + */ + constructor(filesCollection, fileObj, versionName) { + this.filesCollection = filesCollection; + this.fileObj = fileObj; + this.versionName = versionName; + } + + /** after successfull upload */ + onAfterUpload() { + } + + /** download the file + * @param http the current http request + */ + interceptDownload(http) { + } + + /** after file remove */ + onAfterRemove() { + } + + /** returns a read stream + * @return the read stream + */ + getReadStream() { + } + + /** returns a write stream + * @return the write stream + */ + getWriteStream() { + } + + /** writing finished + * @param finishedData the data of the write stream finish event + */ + writeStreamFinished(finishedData) { + } + + /** 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 filesCollection the current FilesCollection instance + * @param fileObj the current file object + * @param versionName the current version + */ + constructor(gridFsBucket, filesCollection, fileObj, versionName) { + super(filesCollection, fileObj, versionName); + this.gridFsBucket = gridFsBucket; + } + + /** after successfull upload */ + onAfterUpload() { + createOnAfterUpload(this.filesCollection, this.gridFsBucket, this.fileObj, this.versionName); + super.onAfterUpload(); + } + + /** download the file + * @param http the current http request + */ + interceptDownload(http) { + const ret = createInterceptDownload(this.filesCollection, this.gridFsBucket, this.fileObj, http, this.versionName); + return ret; + } + + /** after file remove */ + onAfterRemove() { + this.unlink(); + super.onAfterRemove(); + } + + /** returns a read stream + * @return the read stream + */ + getReadStream() { + const gridFsFileId = (this.fileObj.versions[this.versionName].meta || {}) + .gridFsFileId; + let ret; + if (gridFsFileId) { + const gfsId = createObjectId({ gridFsFileId }); + ret = this.gridFsBucket.openDownloadStream(gfsId); + } + return ret; + } + + /** returns a write stream + * @return the write stream + */ + getWriteStream() { + 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() { + createOnAfterRemove(this.filesCollection, this.gridFsBucket, this.fileObj, this.versionName); + const gridFsFileIdName = this.getGridFsFileIdName(); + Attachments.update({ _id: this.fileObj._id }, { $unset: { [gridFsFileIdName]: 1 } }); + } + + /** return the storage name + * @return the storage name + */ + getStorageName() { + return "gridfs"; + } + + /** 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 filesCollection the current FilesCollection instance + * @param fileObj the current file object + * @param versionName the current version + */ + constructor(filesCollection, fileObj, versionName) { + super(filesCollection, 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 + * @return the write stream + */ + getWriteStream() { + const 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 "fs"; + } +} + +/** 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(this, fileObj, versionName); + const strategyWrite = fileStoreStrategyFactory.getFileStrategy(this, fileObj, versionName, storageDestination); + + if (strategyRead.constructor.name != strategyWrite.constructor.name) { + const readStream = strategyRead.getReadStream(); + const writeStream = strategyWrite.getWriteStream(); + + 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() } }); + strategyRead.unlink(); + })); + + readStream.pipe(writeStream); + } + }); +};