diff --git a/models/lib/fsHooks/createInterceptDownload.js b/models/lib/fsHooks/createInterceptDownload.js new file mode 100644 index 000000000..5d1a7ee84 --- /dev/null +++ b/models/lib/fsHooks/createInterceptDownload.js @@ -0,0 +1,47 @@ +import { createObjectId } from '../grid/createObjectId'; + +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(fileName); + 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 new file mode 100644 index 000000000..297d49b5a --- /dev/null +++ b/models/lib/fsHooks/createOnAfterRemove.js @@ -0,0 +1,17 @@ +import { createObjectId } from '../grid/createObjectId'; + +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 new file mode 100644 index 000000000..f3efa652b --- /dev/null +++ b/models/lib/fsHooks/createOnAfterUpload.js @@ -0,0 +1,51 @@ +import { Meteor } from 'meteor/meteor'; +import fs from 'fs'; + +export const createOnAfterUpload = bucket => + function onAfterUpload(file) { + const self = this; + + // here you could manipulate your file + // and create a new version, for example a scaled 'thumbnail' + // ... + + // then we read all versions we have got so far + Object.keys(file.versions).forEach(versionName => { + const metadata = { ...file.meta, versionName, fileId: file._id }; + fs.createReadStream(file.versions[versionName].path) + + // this is where we upload the binary to the bucket using bucket.openUploadStream + // see http://mongodb.github.io/node-mongodb-native/3.2/api/GridFSBucket.html#openUploadStream + .pipe( + bucket.openUploadStream(file.name, { + contentType: file.type || 'binary/octet-stream', + metadata, + }), + ) + + // and we unlink the file from the fs on any error + // that occurred during the upload to prevent zombie files + .on('error', err => { + // console.error(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/grid/createBucket.js b/models/lib/grid/createBucket.js new file mode 100644 index 000000000..d5da5a170 --- /dev/null +++ b/models/lib/grid/createBucket.js @@ -0,0 +1,9 @@ +import { MongoInternals } from 'meteor/mongo'; + +export const createBucket = bucketName => { + const options = bucketName ? { bucketName } : void 0; + return new MongoInternals.NpmModule.GridFSBucket( + MongoInternals.defaultRemoteCollectionDriver().mongo.db, + options, + ); +}; diff --git a/models/lib/grid/createObjectId.js b/models/lib/grid/createObjectId.js new file mode 100644 index 000000000..2e6703008 --- /dev/null +++ b/models/lib/grid/createObjectId.js @@ -0,0 +1,4 @@ +import { MongoInternals } from 'meteor/mongo'; + +export const createObjectId = ({ gridFsFileId }) => + new MongoInternals.NpmModule.ObjectID(gridFsFileId);