UNPKG

keystone

Version:

Web Application Framework and Admin GUI / Content Management System built on Express.js and Mongoose

393 lines (354 loc) 11.5 kB
var _ = require('lodash'); var assign = require('object-assign'); var async = require('async'); var FieldType = require('../Type'); var keystone = require('../../../'); var util = require('util'); function getEmptyValue () { return { public_id: '', version: 0, signature: '', format: '', resource_type: '', url: '', width: 0, height: 0, secure_url: '', }; } function truthy (value) { return value; } /* * Uses a before and after snapshot of the images array to find out what images are no longer included */ function cleanUp (oldValues, newValues) { var cloudinary = require('cloudinary'); var oldvalIds = oldValues.map(function (val) { return val.public_id; }); var newValIds = newValues.map(function (val) { return val.public_id; }); var removedItemsCloudinaryIds = _.difference(oldvalIds, newValIds); // We never wait to return on the images being removed async.map(removedItemsCloudinaryIds, function (id, next) { cloudinary.uploader.destroy(id, function (result) { next(); }); }); }; /** * CloudinaryImages FieldType Constructor */ function cloudinaryimages (list, path, options) { this._underscoreMethods = ['format']; this._fixedSize = 'full'; this._properties = ['select', 'selectPrefix', 'autoCleanup', 'publicID', 'folder', 'filenameAsPublicID']; cloudinaryimages.super_.call(this, list, path, options); // validate cloudinary config if (!keystone.get('cloudinary config')) { throw new Error('Invalid Configuration\n\n' + 'CloudinaryImages fields (' + list.key + '.' + this.path + ') require the "cloudinary config" option to be set.\n\n' + 'See http://keystonejs.com/docs/configuration/#services-cloudinary for more information.\n'); } } cloudinaryimages.properName = 'CloudinaryImages'; util.inherits(cloudinaryimages, FieldType); /** * Gets the folder for images in this field */ cloudinaryimages.prototype.getFolder = function () { var folder = null; if (keystone.get('cloudinary folders') || this.options.folder) { if (typeof this.options.folder === 'string') { folder = this.options.folder; } else { folder = this.list.path + '/' + this.path; } } return folder; }; /** * Registers the field on the List's Mongoose Schema. */ cloudinaryimages.prototype.addToSchema = function (schema) { var cloudinary = require('cloudinary'); var mongoose = keystone.mongoose; var field = this; this.paths = { // virtuals folder: this.path + '.folder', // form paths upload: this.path + '_upload', uploads: this.path + '_uploads', action: this.path + '_action', }; var ImageSchema = new mongoose.Schema({ public_id: String, version: Number, signature: String, format: String, resource_type: String, url: String, width: Number, height: Number, secure_url: String, }); // Generate cloudinary folder used to upload/select images var folder = function (item) { // eslint-disable-line no-unused-vars var folderValue = ''; if (keystone.get('cloudinary folders')) { if (field.options.folder) { folderValue = field.options.folder; } else { var folderList = keystone.get('cloudinary prefix') ? [keystone.get('cloudinary prefix')] : []; folderList.push(field.list.path); folderList.push(field.path); folderValue = folderList.join('/'); } } return folderValue; }; // The .folder virtual returns the cloudinary folder used to upload/select images schema.virtual(field.paths.folder).get(function () { return folder(this); }); var src = function (img, options) { if (keystone.get('cloudinary secure')) { options = options || {}; options.secure = true; } options.format = options.format || img.format; return img.public_id ? cloudinary.url(img.public_id, options) : ''; }; var addSize = function (options, width, height, other) { if (width) options.width = width; if (height) options.height = height; if (typeof other === 'object') { assign(options, other); } return options; }; ImageSchema.method('src', function (options) { return src(this, options); }); ImageSchema.method('scale', function (width, height, options) { return src(this, addSize({ crop: 'scale' }, width, height, options)); }); ImageSchema.method('fill', function (width, height, options) { return src(this, addSize({ crop: 'fill', gravity: 'faces' }, width, height, options)); }); ImageSchema.method('lfill', function (width, height, options) { return src(this, addSize({ crop: 'lfill', gravity: 'faces' }, width, height, options)); }); ImageSchema.method('fit', function (width, height, options) { return src(this, addSize({ crop: 'fit' }, width, height, options)); }); ImageSchema.method('limit', function (width, height, options) { return src(this, addSize({ crop: 'limit' }, width, height, options)); }); ImageSchema.method('pad', function (width, height, options) { return src(this, addSize({ crop: 'pad' }, width, height, options)); }); ImageSchema.method('lpad', function (width, height, options) { return src(this, addSize({ crop: 'lpad' }, width, height, options)); }); ImageSchema.method('crop', function (width, height, options) { return src(this, addSize({ crop: 'crop', gravity: 'faces' }, width, height, options)); }); ImageSchema.method('thumbnail', function (width, height, options) { return src(this, addSize({ crop: 'thumb', gravity: 'faces' }, width, height, options)); }); schema.add(this._path.addTo({}, [ImageSchema])); this.removeImage = function (item, id, method, callback) { var images = item.get(field.path); if (typeof id !== 'number') { for (var i = 0; i < images.length; i++) { if (images[i].public_id === id) { id = i; break; } } } var img = images[id]; if (!img) return; if (method === 'delete') { cloudinary.uploader.destroy(img.public_id, function () {}); } images.splice(id, 1); if (callback) { item.save((typeof callback !== 'function') ? callback : undefined); } }; this.underscoreMethod('remove', function (id, callback) { field.removeImage(this, id, 'remove', callback); }); this.underscoreMethod('delete', function (id, callback) { field.removeImage(this, id, 'delete', callback); }); this.bindUnderscoreMethods(); }; /** * Formats the field value */ cloudinaryimages.prototype.format = function (item) { return _.map(item.get(this.path), function (img) { return img.src(); }).join(', '); }; /** * Gets the field's data from an Item, as used by the React components */ cloudinaryimages.prototype.getData = function (item) { var value = item.get(this.path); return Array.isArray(value) ? value : []; }; /** * Validates that a value for this field has been provided in a data object * * Deprecated */ cloudinaryimages.prototype.inputIsValid = function (data) { // eslint-disable-line no-unused-vars // TODO - how should image field input be validated? return true; }; cloudinaryimages.prototype._originalGetOptions = cloudinaryimages.prototype.getOptions; cloudinaryimages.prototype.getOptions = function () { this._originalGetOptions(); // We are performing the check here, so that if cloudinary secure is added // to keystone after the model is registered, it will still be respected. // Setting secure overrides default `cloudinary secure` if ('secure' in this.options) { this.__options.secure = this.options.secure; } else if (keystone.get('cloudinary secure')) { this.__options.secure = keystone.get('cloudinary secure'); } return this.__options; }; /** * Updates the value for this field in the item from a data object */ cloudinaryimages.prototype.updateItem = function (item, data, files, callback) { if (typeof files === 'function') { callback = files; files = {}; } else if (!files) { files = {}; } var cloudinary = require('cloudinary'); var field = this; var values = this.getValueFromData(data); var oldValues = item.get(this.path); // TODO: This logic needs to block uploading of files from the data argument, // see CloudinaryImage for a reference on how it should be implemented // Early exit path: reset value when falsy, or bail if no value was provided if (!values) { if (values !== undefined) { if (field.options.autoCleanup) { cleanUp(oldValues, []); } item.set(field.path, []); } return process.nextTick(callback); } // When the value exists, but isn't an array, turn it into one (this just // means a single field was submitted in the formdata) if (!Array.isArray(values)) { values = [values]; } // We cache options to avoid recalculating them on each iteration in the map below var cachedUploadOptions; function getUploadOptions () { if (cachedUploadOptions) { return cachedUploadOptions; } var tagPrefix = keystone.get('cloudinary prefix') || ''; var uploadOptions = { tags: [], }; if (tagPrefix.length) { uploadOptions.tags.push(tagPrefix); tagPrefix += '_'; } uploadOptions.tags.push(tagPrefix + field.list.path + '_' + field.path); if (keystone.get('env') !== 'production') { uploadOptions.tags.push(tagPrefix + 'dev'); } var folder = field.getFolder(); if (folder) { uploadOptions.folder = folder; } cachedUploadOptions = uploadOptions; return uploadOptions; } // Preprocess values to deserialise JSON, detect mappings to uploaded files // and flatten out arrays values = values.map(function (value) { // When the value is a string, it may be JSON serialised data. if (typeof value === 'string' && value.charAt(0) === '{' && value.charAt(value.length - 1) === '}' ) { try { return JSON.parse(value); } catch (e) { /* value isn't JSON */ } } if (typeof value === 'string') { // detect file upload (field value must be a reference to a field in the // uploaded files object provided by multer) if (value.substr(0, 7) === 'upload:') { var uploadFieldPath = value.substr(7); return files[uploadFieldPath]; } // detect a URL or Base64 Data else if (/^(data:[a-z\/]+;base64)|(https?\:\/\/)/.test(value)) { return { path: value }; } } return value; }); values = _.flatten(values); async.map(values, function (value, next) { if (typeof value === 'object' && 'public_id' in value) { // Cloudinary Image data provided if (value.public_id) { // Default the object with empty values var v = assign(getEmptyValue(), value); return next(null, v); } else { // public_id is falsy, remove the value return next(); } } else if (typeof value === 'object' && value.path) { // File provided - upload it var uploadOptions = getUploadOptions(); // NOTE: field.options.publicID has been deprecated (tbc) if (field.options.filenameAsPublicID && value.originalname && typeof value.originalname === 'string') { uploadOptions = assign({}, uploadOptions, { public_id: value.originalname.substring(0, value.originalname.lastIndexOf('.')), }); } cloudinary.uploader.upload(value.path, function (result) { if (result.error) { next(result.error); } else { next(null, result); } }, uploadOptions); } else { // Nothing to do // TODO: We should really also support deleting images from cloudinary, // see the CloudinaryImageType field for reference return next(); } }, function (err, result) { cleanUp(oldValues, values); if (err) return callback(err); result = result.filter(truthy); item.set(field.path, result); return callback(); }); }; module.exports = cloudinaryimages;