keystone
Version:
Web Application Framework and Admin GUI / Content Management System built on Express.js and Mongoose
393 lines (354 loc) • 11.5 kB
JavaScript
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;