UNPKG

mongoose-gridfs

Version:

[![Build Status](https://app.travis-ci.com/lykmapipo/mongoose-gridfs.svg?branch=master)](https://app.travis-ci.com/lykmapipo/mongoose-gridfs) [![Dependencies Status](https://david-dm.org/lykmapipo/mongoose-gridfs.svg)](https://david-dm.org/lykmapipo/mongo

643 lines (586 loc) 19.8 kB
import _ from 'lodash'; import read from 'stream-read'; import mongoose from 'mongoose'; import { GridFSBucket, MongooseTypes, toCollectionName, model, } from '@lykmapipo/mongoose-common'; import createFileSchema from './schema'; const { ObjectId } = MongooseTypes; /* constants */ const DEFAULT_BUCKET_MODEL_NAME = 'File'; const DEFAULT_BUCKET_NAME = 'fs'; const DEFAULT_BUCKET_CHUNK_SIZE = 255 * 1024; const DEFAULT_BUCKET_OPTIONS = { modelName: DEFAULT_BUCKET_MODEL_NAME, bucketName: DEFAULT_BUCKET_NAME, chunkSizeBytes: DEFAULT_BUCKET_CHUNK_SIZE, }; /* getters */ /** * @name bucketName * @description Bucket name used to prefix 'files' and 'chunks' collections. * @returns {string} * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 1.0.0 * @version 0.1.0 * @instance * @example * * const bucket = createBucket(); * const bucketName = bucket.bucketName; * //=> fs */ Object.defineProperty(GridFSBucket.prototype, 'bucketName', { get: function getBucketName() { return this.s.options.bucketName; }, }); /** * @name collection * @description Collection used to store file data. * @returns {object} Valid mongodb collection * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 0.1.0 * @version 0.6.0 * @instance * @example * * const bucket = createBucket(); * const collection = bucket.collection; * //=> Collection { ... } */ Object.defineProperty(GridFSBucket.prototype, 'collection', { get: function getCollection() { // eslint-disable-next-line no-underscore-dangle return this.s._filesCollection; }, }); /** * @name collectionName * @description Name of a collection used to store file. * @returns {string} valid mongodb collection name * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 0.1.0 * @version 0.7.0 * @instance * @example * * const bucket = createBucket(); * const collectionName = bucket.collectionName; * //=> fs.files */ Object.defineProperty(GridFSBucket.prototype, 'collectionName', { get: function getCollectionName() { // eslint-disable-next-line no-underscore-dangle return this.s._filesCollection.s.namespace.collection; }, }); /* streams */ /** * @function createWriteStream * @name createWriteStream * @description Creates a writable stream (GridFSBucketWriteStream) for writing * buffers to GridFS for a custom file id. The stream's 'id' property contains * the resulting file's id. * @param {object} optns Valid options for write file. * @param {ObjectId} [optns._id] A custom id used to identify file doc. * @param {string} optns.filename The value of the 'filename' key in the files * doc. * @param {number} [optns.chunkSizeBytes] Optional overwrite this bucket's * chunkSizeBytes for this file. * @param {object} [optns.metadata] Optional object to store in the file * document's `metadata` field. * @param {string} [optns.contentType] Optional string to store in the file * document's `contentType` field. * @param {Array} [optns.aliases] Optional array of strings to store in the * file document's `aliases` field. * @param {boolean} [optns.disableMD5=false] If true, disables adding an * md5 field to file data. * @returns {object} Valid mongodb `GridFSBucketWriteStream` * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 1.0.0 * @version 0.1.0 * @instance * @example * * const bucket = createBucket(); * const _id = new ObjectId(); * const filename = 'filename.txt'; * const readStream = fs.createReadStream(filename); * const writeStream = bucket.createWriteStream({_id, filename}); * readStream.pipe(writeStream); */ GridFSBucket.prototype.createWriteStream = function createWriteStream(optns) { // ensure options const defaults = { _id: new ObjectId() }; const options = _.merge({}, defaults, optns); const { _id, filename } = options; // ensure filename if (_.isEmpty(filename)) { const error = new Error('Missing filename'); error.status = 400; throw error; } // open write stream const writeStream = this.openUploadStreamWithId(_id, filename, options); // return writeStream return writeStream; }; /** * @function createReadStream * @name createReadStream * @description Creates a readable stream (GridFSBucketReadStream) for * streaming the file with the given name from GridFS. If there are multiple * files with the same name, this will stream the most recent file with the * given name (as determined by the `uploadDate` field). You can set the * `revision` option to change this behavior. * @param {object} [optns] Valid options for read existing file. * @param {ObjectId} optns._id The id of the file doc * @param {string} [optns.filename] The name of the file doc to stream * @param {number} [optns.revision=-1] The revision number relative to the * oldest file with the given filename. 0 gets you the oldest file, 1 gets you * the 2nd oldest, -1 gets you the newest. * @param {number} [optns.start] Optional 0-based offset in bytes to start * streaming from. * @param {number} [optns.end] Optional 0-based offset in bytes to stop * streaming before. * @returns {object} Valid mongodb `GridFSBucketReadStream` * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 1.0.0 * @version 0.1.0 * @instance * @example * * const bucket = createBucket(); * const _id = new ObjectId(); * const filename = 'filename.txt'; * const writeStream = fs.createWriteStream(filename); * const readStream = bucket.createReadStream({_id, filename}); * readStream.pipe(writeStream); */ GridFSBucket.prototype.createReadStream = function createReadStream(optns) { // ensure options const options = _.merge({}, optns); const { _id, filename } = options; // ensure filename or _id if (_.isEmpty(filename) && !_id) { const error = Error('Missing filename or file id'); error.status = 400; throw error; } // initalize file read stream let readstream; // open download stream by file id if (_id) { readstream = this.openDownloadStream(_id, options); } // open download stream by file name if (!_.isEmpty(filename) && !readstream) { readstream = this.openDownloadStreamByName(filename, options); } // return read stream return readstream; }; /* writers */ /** * @function writeFile * @name writeFile * @description Write provided file into MongoDB GridFS * @param {object} file valid file details * @param {object} readstream valid nodejs `ReadableStream` * @param {Function} [done] a callback to invoke on success or error * @fires GridFSBucketWriteStream#error * @fires GridFSBucketWriteStream#finish * @returns {object} Valid mongodb `GridFSBucketWriteStream` instance. * @see {@link https://docs.mongodb.com/manual/core/gridfs/} * @see {@link http://mongodb.github.io/node-mongodb-native} * @see {@link https://nodejs.org/api/stream.html#stream_writable_streams} * @see {@link https://nodejs.org/api/stream.html#stream_readable_streams} * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 1.0.0 * @version 0.1.0 * @instance * @example * * // large file * const bucket = createBucket(); * const filename = 'filename.txt'; * const readStream = fs.createReadStream(filename); * const writeStream = bucket.writeFile({ filename }, readStream); * * // small file * const bucket = createBucket(); * const filename = 'filename.txt'; * const readStream = fs.createReadStream(filename); * bucket.writeFile({ filename }, readStream, (error, file) => { ... }); */ GridFSBucket.prototype.writeFile = function writeFile(file, readstream, done) { // ensure file details const $file = _.merge({}, { _id: new ObjectId() }, file); // create file write stream const writestream = this.createWriteStream($file); // stream file into mongodb gridfs bucket readstream.pipe(writestream); // work on the stream if (done && _.isFunction(done)) { // handle errors writestream.on('error', function onWriteFileError(error) { return done(error); }); // finalize write writestream.on('finish', function onWriteFileFinish($$file) { return done(null, $$file); }); } // return writestream return writestream; }; /* readers */ /** * @function readFile * @name readFile * @description Read file from MongoDB GridFS * @param {object} optns valid criteria for read existing file. * @param {object} optns._id The id of the file doc * @param {string} [optns.filename] The name of the file doc to stream * @param {number} [optns.revision=-1] The revision number relative to the * oldest file with the given filename. 0 gets you the oldest file, 1 gets you * the 2nd oldest, -1 gets you the newest. * @param {number} [optns.start] Optional 0-based offset in bytes to start * streaming from. * @param {number} [optns.end] Optional 0-based offset in bytes to stop * streaming before. * @param {Function} [done] a callback to invoke on success or error. * * Warn!: Pass callback if filesize is small enough. * Otherwise consider using stream instead. * @fires GridFSBucketReadStream#error * @fires GridFSBucketReadStream#file * @returns {object} Valid mongodb `GridFSBucketReadStream` instance. * @see {@link https://docs.mongodb.com/manual/core/gridfs/} * @see {@link http://mongodb.github.io/node-mongodb-native} * @see {@link https://nodejs.org/api/stream.html#stream_writable_streams} * @see {@link https://nodejs.org/api/stream.html#stream_readable_streams} * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 1.0.0 * @version 0.1.0 * @instance * @example * * // large file * const bucket = createBucket(); * const filename = 'filename.txt'; * const readStream = bucket.readFile({ filename }); * * // small file * const bucket = createBucket(); * const filename = 'filename.txt'; * bucket.readFile({ filename }, (error, buffer) => { ... }); */ GridFSBucket.prototype.readFile = function readFile(optns, done) { // ensure options const $optns = _.merge({}, optns); // create file read stream const readstream = this.createReadStream($optns); // pipe the whole stream into buffer if callback provided if (done && _.isFunction(done)) { return read(readstream, done); } // return stream return readstream; }; /* removers */ /** * @function deleteFile * @name deleteFile * @alias unlink * @description Remove an existing file and its chunks. * @param {ObjectId} _id The id of the file doc * @param {Function} done a callback to invoke on success or error * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 1.0.0 * @version 0.1.0 * @instance * @example * * const bucket = createBucket(); * bucket.deleteFile(_id, (error, results) => { ... }); */ GridFSBucket.prototype.deleteFile = function deleteFile(_id, done) { this.delete(_id, function afterDelete(error) { return done(error, _id); }); }; GridFSBucket.prototype.unlink = GridFSBucket.prototype.deleteFile; /* finders */ /** * @function findOne * @name findOne * @description find an existing file using options provided * @param {object} optns valid find criteria * @param {Function} done a callback to invoke on success or error * @returns {object} existing file details * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 0.1.0 * @version 0.7.0 * @instance * @example * * const bucket = createBucket(); * bucket.findOne({ _id }, (error, file) => { ... }); * bucket.findOne({ filename }, (error, file) => { ... }); */ GridFSBucket.prototype.findOne = function findOne(optns, done) { // ensure file find criteria const options = _.merge({}, optns); // find one existing file try { const cursor = this.find(options, { limit: 1 }); if (!cursor) { const error = new Error('Collection not found'); error.status = 400; return done(error); } return cursor.next(done); } catch (error) { // catch find errors return done(error); } }; /** * @function findById * @name findById * @description find an existing file with given objectid * @param {ObjectId} _id valid objectid of the existing file * @param {Function} done a callback to invoke on success or error * @returns {object} existing file details * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 0.1.0 * @version 0.7.0 * @instance * @example * * const bucket = createBucket(); * bucket.findById(_id, (error, file) => { ... }); */ GridFSBucket.prototype.findById = function findById(_id, done) { return this.findOne({ _id }, done); }; /* multer */ /** * @function _handleFile * @name _handleFile * @description write file to the bucket and return information on how to * access the file in the future * @param {object} request injected request from multer * @param {object} file injected file object from multer * @param {Function} done a callback to invoke on success or error * @author lally elias <lallyelias87@mail.com> * @see {@link https://github.com/expressjs/multer/blob/master/StorageEngine.md} * @since 0.1.0 * @version 0.5.0 * @instance * @example * * const express = require('express'); * const multer = require('multer'); * const { createBucket } = require('mongoose-gridfs'); * const app = express(); * const storage = createBucket(); // createBucket(optns) * const upload = multer({ storage }); * * app.post('/profile', upload.single('avatar'), (req, res, next) => { * // req.file is the `avatar` file * // req.body contains the text fields * }); */ // eslint-disable-next-line no-underscore-dangle GridFSBucket.prototype._handleFile = function _handleFile(request, file, done) { // obtain file readable stream const { stream } = file; // prepare file details const { aliases } = request.body || {}; const $file = _.merge( {}, { _id: new ObjectId(), filename: file.originalname, contentType: file.mimetype, aliases: aliases ? [].concat(aliases) : aliases, metadata: (request.body || {}).metadata, } ); // obtain bucket details const { bucketName } = this; // write file this.writeFile($file, stream, function afterHandleFile(error, $$file) { if ($$file) { // eslint-disable-next-line no-param-reassign $$file.size = $$file.size || $$file.length; // eslint-disable-next-line no-param-reassign $$file.bucketName = bucketName; } return done(error, $$file); }); }; /** * @function _removeFile * @name _removeFile * @description remove existing file from bucket * @param {object} request injected request from multer * @param {object} file injected file object from multer * @param {Function} done a callback to invoke on success or error * @returns {object|Error} error or deleted file * @author lally elias <lallyelias87@mail.com> * @see {@link https://github.com/expressjs/multer/blob/master/StorageEngine.md} * @since 0.1.0 * @version 0.7.0 * @instance * @example * * const express = require('express'); * const multer = require('multer'); * const { createBucket } = require('mongoose-gridfs'); * const app = express(); * const storage = createBucket(); // createBucket(optns) * const upload = multer({ storage }); * * app.post('/profile', upload.single('avatar'), (req, res, next) => { * // req.file is the `avatar` file * // req.body contains the text fields * }); */ // eslint-disable-next-line no-underscore-dangle GridFSBucket.prototype._removeFile = function _removeFile(request, file, done) { // remove file // eslint-disable-next-line no-underscore-dangle if (file._id) { // eslint-disable-next-line no-underscore-dangle return this.deleteFile(file._id, done); } // no operation return done(null, null); }; /* statics */ /** * @function createBucket * @name createBucket * @description Create GridFSBucket * @param {object} [optns] Optional settings. * @param {object} [optns.connection = mongoose.connection] A valid * instance of mongoose connection. * @param {string} [optns.bucketName="fs"] The 'files' and 'chunks' collections * will be prefixed with the bucket name followed by a dot. * @param {number} [optns.chunkSizeBytes=255 * 1024] Number of bytes stored in * each chunk. Defaults to 255KB * @param {object} [optns.writeConcern] Optional write concern to be passed to * write operations, for instance `{ w: 1 }` * @param {object} [optns.readPreference] Optional read preference to be passed * to read operations * @returns {object} an instance of `GridFSBucket` * @author lally elias <lallyelias87@mail.com> * @since 1.0.0 * @version 0.1.0 * @public * @static * @example * * const bucket = createBucket(); * const bucket = createBucket({ bucketName }); * const bucket = createBucket({ buketName, connection }); */ function createBucket(optns = {}) { // ensure options let { connection } = optns; connection = connection || mongoose.connection; const options = _.merge( {}, DEFAULT_BUCKET_OPTIONS, _.omit(optns, 'connection') ); // create GridFSBucket const { db } = connection; const bucket = new GridFSBucket(db, options); // return bucket return bucket; } /** * @function createModel * @name createModel * @description Create GridFSBucket files collection model * @param {object} [optns] Optional settings. * @param {object} [optns.connection = mongoose.connection] A valid instance * of mongoose connection. * @param {string} [optns.modelName="File"] Valid model name to use with * mongoose * @param {string} [optns.bucketName="fs"] The 'files' and 'chunks' collections * will be prefixed with the bucket name followed by a dot. * @param {number} [optns.chunkSizeBytes=255 * 1024] Number of bytes stored in * each chunk. Defaults to 255KB * @param {object} [optns.writeConcern] Optional write concern to be passed to * write operations, for instance `{ w: 1 }` * @param {object} [optns.readPreference] Optional read preference to be passed * to read operations * @param {...Function} [plugins] list of valid mongoose plugin to apply to * file schema * @returns {object} an instance of `GridFSBucket` * @author lally elias <lallyelias87@mail.com> * @since 1.0.0 * @version 0.2.0 * @public * @static * @example * * const File = createModel(); // => fs.files * const Photo = createModel({ modelName }); // => photos.files * const Photo = createModel({ modelName, connection }); // => photos.files */ function createModel(optns, ...plugins) { // ensure options let { connection, modelName, bucketName } = _.merge({}, optns); connection = connection || mongoose.connection; modelName = _.isEmpty(modelName) ? DEFAULT_BUCKET_MODEL_NAME : modelName; bucketName = toCollectionName(modelName); bucketName = modelName === DEFAULT_BUCKET_MODEL_NAME ? DEFAULT_BUCKET_NAME : bucketName; // create bucket const options = _.merge({}, { connection, modelName, bucketName }, optns); const bucket = createBucket(options); // construct file schema const schema = createFileSchema(bucket); // apply schema plugins with model options const schemaOptions = _.merge({}, schema.options); _.forEach([...plugins], (plugin) => { schema.plugin(plugin, schemaOptions); }); // hack(to be removed): fake timestamp fields schema.statics.CREATED_AT_FIELD = 'uploadDate'; schema.statics.UPDATED_AT_FIELD = 'uploadDate'; // compile file model const fileModel = model(modelName, schema, connection); // return model return fileModel; } /* export GridFSBucket */ export { DEFAULT_BUCKET_NAME, DEFAULT_BUCKET_MODEL_NAME, DEFAULT_BUCKET_CHUNK_SIZE, DEFAULT_BUCKET_OPTIONS, GridFSBucket, createBucket, createModel, };