UNPKG

@lykmapipo/file

Version:

Store and serve file content i.e photos, videos etc on top of MongoDB GridFS

484 lines (444 loc) 13.9 kB
/** * @module File * @name File * @description A representation of stored and served file content i.e photos, * videos etc. on top of MongoDB GridFS. * * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.1.0 * @version 0.1.0 * @public * @example * * import fs from 'fs'; * import { File } from '@lykmapipo/file'; * * const in = fs.createReadStream('filename.txt'); * File.wite({ filename }, in, (error, file) => { ... }); * */ import { find, forEach, get, isEmpty, values } from 'lodash'; import { mergeObjects, uniq } from '@lykmapipo/common'; import { ObjectId, validationErrorFor } from '@lykmapipo/mongoose-common'; import { createModel, createBucket } from 'mongoose-gridfs'; import multer from '@lykmapipo/multer'; import actions from 'mongoose-rest-actions'; export const AUTOPOPULATE_OPTIONS = { select: { filename: 1, contentType: 1, stream: 1, download: 1, length: 1 }, maxDepth: 1, }; /** * @constant Buckets * @name Buckets * @descriptions common allowed GridFS file buckets * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.1.0 * @version 0.1.0 * @public * @static * @example * * import { Buckets } from '@lykmapipo/file'; * Buckets.File; //=> { modelName: 'File', bucketName: 'fs', field:'file' } * */ export const Buckets = { File: { modelName: 'File', bucketName: 'fs', fieldName: 'file' }, Image: { modelName: 'Image', bucketName: 'images', fieldName: 'image' }, Audio: { modelName: 'Audio', bucketName: 'audios', fieldName: 'audio' }, Video: { modelName: 'Video', bucketName: 'videos', fieldName: 'video' }, Document: { modelName: 'Document', bucketName: 'documents', fieldName: 'document', }, }; /** * @constant bucketInfoFor * @name bucketInfoFor * @descriptions Obtain bucket information of a specified bucket name. * @param {String} [bucket='fs'] Valid bucket name * @return {Object} Valid bucket information * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.1.0 * @version 0.1.0 * @public * @static * @example * * import { bucketInfoFor } from '@lykmapipo/file'; * const bucketInfo = bucketInfoFor('fs'); * //=> { modelName: 'File', bucketName: 'fs', field: 'file' } * */ export const bucketInfoFor = (bucket = 'fs') => { // obtain bucket info or default const info = find(values(Buckets), { bucketName: bucket }) || Buckets.File; // return bucket info copy return mergeObjects(info); }; /** * @function FileTypes * @name FileTypes * @description SchemaType definitions for use with models to reference files * @return {Object} SchemaType definitions * @return {Object} SchemaType.File generic file path definition * @return {Object} SchemaType.Image image path definition * @return {Object} SchemaType.Audio audio path definition * @return {Object} SchemaType.Video Video path definition * @return {Object} SchemaType.Document document path definition * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * import { FileTypes } from '@lykmapipo/file'; * * const Profile = new Schema({ avatar: FileTypes.Image }); * const Song = new Schema({ stream: FileTypes.Audio }); * const Movie = new Schema({ stream: FileTypes.Video }); * const Invoice = new Schema({ document: FileTypes.Document }); * */ export const FileTypes = { File: { type: ObjectId, ref: 'File', autopopulate: AUTOPOPULATE_OPTIONS }, Image: { type: ObjectId, ref: 'Image', autopopulate: AUTOPOPULATE_OPTIONS }, Audio: { type: ObjectId, ref: 'Audio', autopopulate: AUTOPOPULATE_OPTIONS }, Video: { type: ObjectId, ref: 'Video', autopopulate: AUTOPOPULATE_OPTIONS }, Document: { type: ObjectId, ref: 'Document', autopopulate: AUTOPOPULATE_OPTIONS, }, }; /** * @function createBuckets * @name createBuckets * @description Create common GridFS buckets * @return {Object} Buckets valid GridFS buckets * @return {Model} Buckets.files valid File GridFS bucket * @return {Model} Buckets.images valid Image GridFS bucket * @return {Model} Buckets.audios valid Audio GridFS bucket * @return {Model} Buckets.videos valid Video GridFS bucket * @return {Model} Buckets.documents valid Document GridFS bucket * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 0.1.0 * @version 0.1.0 * @static * @public * @example * * import { createBuckets } from '@lykmapipo/file'; * * const { files, images, audio, video, documents } = createBuckets(); * * files.writeFile({ filename }, stream, (error, file) => { ... }); * files.readFile({ _id }, (error, file) => { ... }); * files.unlink(_id, (error, _id) => { ... }); * */ export const createBuckets = () => { // create common GridFS buckets const files = createBucket(Buckets.File); const images = createBucket(Buckets.Image); const audios = createBucket(Buckets.Audio); const videos = createBucket(Buckets.Video); const documents = createBucket(Buckets.Document); // return GridFS buckets return { files, images, audios, videos, documents }; }; /** * @function createModels * @name createModels * @description Create common GridFS file models * @return {Object} Models valid mongoose models * @return {Model} Models.File valid File mongoose model * @return {Model} Models.Image valid Image mongoose model * @return {Model} Models.Audio valid Audio mongoose model * @return {Model} Models.Video valid Video mongoose model * @return {Model} Models.Document valid Document mongoose model * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 0.1.0 * @version 0.1.0 * @static * @public * @example * * import { createModels } from '@lykmapipo/file'; * * const { File, Image, Audio, Video, Document } = createModels(); * * File.write({ filename }, stream, (error, file) => { ... }); * File.read({ _id }, (error, file) => { ... }); * File.unlink(_id, (error, file) => { ... }); * File.find((error, files) => { ... }); * */ export const createModels = () => { // schema plugin for file stream and download urls const urlsFor = (bucketInfo) => (schema) => { // obtain bucket name const { bucketName } = bucketInfo; // plugin stream url schema.virtual('stream').get(function stream() { const id = get(this, '_id'); return `/files/${bucketName}/${id}/chunks`; }); // plugin download url schema.virtual('download').get(function download() { const id = get(this, '_id'); return `/files/${bucketName}/${id}/download`; }); }; // create common file models const File = createModel(Buckets.File, urlsFor(Buckets.File), actions); const Image = createModel(Buckets.Image, urlsFor(Buckets.Image), actions); const Audio = createModel(Buckets.Audio, urlsFor(Buckets.Audio), actions); const Video = createModel(Buckets.Video, urlsFor(Buckets.Video), actions); const Document = createModel( Buckets.Document, urlsFor(Buckets.Document), actions ); // return file models return { File, Image, Audio, Video, Document }; }; /** * @function modelFor * @name modelFor * @description Derive model for a given bucket name * @return {Model} valid mongoose models * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 0.1.0 * @version 0.1.0 * @static * @public * @example * * import { modelFor } from '@lykmapipo/file'; * * const Image = modelFor('images'); * * Image.write({ filename }, stream, (error, file) => { ... }); * Image.read({ _id }, (error, file) => { ... }); * Image.unlink(_id, (error, file) => { ... }); * Image.find((error, files) => { ... }); * */ export const modelFor = (bucket = 'fs') => { // create models const models = createModels(); // obtain model name for specified bucket const { modelName } = bucketInfoFor(bucket); // obtain GridFS model instace for specified bucket const Model = get(models, modelName); // return found GridFS model instance return Model; }; /** * @function bucketFor * @name bucketFor * @description Derive GridFS instance for a given bucket name * @return {GridFSBucket} valid instance of GridFSBucket * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 0.1.0 * @version 0.1.0 * @static * @public * @example * * import { bucketFor } from '@lykmapipo/file'; * * const images = bucketFor('images'); * * images.writeFile({ filename }, stream, (error, file) => { ... }); * images.readFile({ _id }, (error, file) => { ... }); * images.unlink(_id, (error, _id) => { ... }); * */ export const bucketFor = (bucket = 'fs') => { // create buckets const buckets = createBuckets(); // obtain options for specified bucket const { bucketName } = bucketInfoFor(bucket); // obtain GridFSBucket instance const Bucket = get(buckets, bucketName) || buckets.files; // return found GridFSBucket instance return Bucket; }; /** * @function fileFilterFor * @name fileFilterFor * @description Derive multer file filter for a given bucket name * @return {Function} valid multer file filter * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 0.1.0 * @version 0.1.0 * @static * @public * @example * * import multer from 'multer'; * import { fileFilterFor } from '@lykmapipo/file'; * * const fileFilter = fileFilterFor('images'); * const uploader = multer({ fileFilter }).any(); * */ export const fileFilterFor = (bucket = 'fs') => { // obtain bucket field name const { fieldName } = bucketInfoFor(bucket); // construct file filter const fileFilter = (request, file, cb) => { const isAllowed = file && file.fieldname === fieldName; cb(null, isAllowed); }; // return bucket file filter return fileFilter; }; /** * @function uploadErrorFor * @name uploadErrorFor * @description Derive upload validation error for a given paths. * @return {...String} path list of required field names not found while upload * @return {ValidationError} valid mongoose validation error * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 0.1.0 * @version 0.1.0 * @static * @public * @example * * import { uploadErrorFor } from '@lykmapipo/file'; * * const uploadError = uploadErrorFor('image'); * //=> { name: 'ValidationError', ... } * */ export const uploadErrorFor = (...path) => { // prepare required paths validator options const paths = {}; forEach(uniq([...path]), (pathName) => { paths[pathName] = { type: 'required', path: pathName, value: undefined, reason: 'Not provided', message: 'Path `{PATH}` is required.', }; }); // build and return validation error const error = validationErrorFor({ paths }); return error; }; /** * @function bucketUploaderFor * @name bucketUploaderFor * @description Derive multer uploader for a given bucket name * @return {Object} uploader Derived bucket uploader details * @return {Model} uploader.File valid file model for the bucket * @return {String} uploader.fieldName expected file field name on the request * @return {Function} uploader.upload valid multer file uploader * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 0.1.0 * @version 0.1.0 * @static * @private * @example * * import { bucketUploaderFor } from '@lykmapipo/file'; * * const { upload }= bucketUploaderFor('images'); * upload(request, response, error => { ... }); * */ export const bucketUploaderFor = (bucket = 'fs') => { // obtain bucket options const { fieldName, bucketName } = bucketInfoFor(bucket); // obtain GridFSBucket storage const storage = bucketFor(bucketName); // obtain model for the bucket const File = modelFor(bucketName); // const file filter const fileFilter = fileFilterFor(bucketName); // create multer file uploader const upload = multer({ storage, fileFilter }).any(); // return multer upload handler with bucket info return { fieldName, bucketName, upload, File }; }; /** * @function uploderFor * @name uploderFor * @description Conventional upload middleware for use with file types * @return {Function} valid multer file uploader for file, image, audio, video, * document field names. * @author lally elias <lallyelias87@mail.com> * @license MIT * @since 0.1.0 * @version 0.1.0 * @static * @public * @example * * import { post } from '@lykmapipo/express-common'; * import { uploderFor } from '@lykmapipo/file'; * * post('/v1/changelogs', uploadFor(), (request, response, next) => { * console.log(request.body); * //=> { image: ..., audio: ..., video: ..., ... } * }); * */ export const uploaderFor = () => { // build file upload middleware return (request, response, next) => { // create buckets const { files, images, audios, videos, documents } = createBuckets(); // prepare per field storage bucket const storage = files; const storages = { file: files, image: images, audio: audios, video: videos, document: documents, }; // TODO: handle different fieldnames // TODO: handle required fieldname // TODO: handle array of files per fieldname // create multi storage multer uploader const upload = multer({ storage, storages }).any(); // handle bucket file upload upload(request, response, (error) => { // backoff on error if (error) { return next(error); } // ignore if no file uploaded if (isEmpty(request.files)) { return next(); } // attach uploaded file to request body request.body = !isEmpty(request.body) ? request.body : {}; forEach(request.files, (file) => { const File = modelFor(file.fieldname); request.body[file.fieldname] = new File(file); }); // continue after upload return next(); }); }; };