diff --git a/.gitignore b/.gitignore index cfdef68..6bbe58a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ logs results node_modules +test/tmp/* +test/fixtures/*-thumbnail* +test/fixtures/*-original* diff --git a/README.md b/README.md index 54210bb..5dc30e3 100644 --- a/README.md +++ b/README.md @@ -52,13 +52,23 @@ PostSchema.plugin(attachments, { // keep the original file }, small: { - resize: '150x150' + transform: function(image) { + return image + .resize(150, 150) + ; + } }, medium: { - resize: '120x120' + transform: function(image) { + return image + .resize(120, 120) + ; + } }, medium_jpg: { - '$format': 'jpg' // this one changes the format of the image to jpg + options: { + format: 'jpg' // this one changes the format of the image to jpg + } } } } @@ -124,14 +134,24 @@ MySchema.plugin(attachments, { // keep the original file }, thumb: { - thumbnail: '100x100^', - gravity: 'center', - extent: '100x100', - '$format': 'jpg' + options: { + format: 'jpg' + }, + transform: function(image) { + return image + .thumbnail(100, 100) + .gravity('center') + .extend(100, 100) + } }, detail: { - resize: '400x400>', - '$format': 'jpg' + options: { + format: 'jpg' + }, + transform: function(image) { + return image + .resize(400, 400, '>') + } } } } @@ -171,25 +191,9 @@ Example: } ``` -### Styles and ImageMagick Transformations +### Styles and Transformations -Transformations are achieved by invoking the **convert** command from ImageMagick and passing all the properties of the style as arguments. - -For more information about convert, take a look at http://www.imagemagick.org/script/command-line-options.php - -Example in convert command: - - convert source.png -resize '50%' output.png - -Example in plugin options: - -```javascript -styles: { - small: { - resize: '50%' - } -} -``` +Transformations are achieved using the [gm library](https://github.com/aheckmann/gm). #### Keeping the Original File @@ -201,30 +205,19 @@ styles: { } ``` -#### Multiples Transformations - -Use another properties under the style to provide more transformations - -```javascript -styles: { - small: { - crop: '120x120', - blur: '5x10' //radius x stigma - } -} -``` - More information about 'blur' at the [ImageMagick website] http://www.imagemagick.org/script/command-line-options.php#blur #### Changing the Destination Format -You can change the destination format by using the special transformation '$format' with a known file extension like *png*, *jpg*, *gif*, etc. +You can specify a format option to change the format of the output. Example: styles: { as_jpeg: { - '$format': 'jpg' + options: { + format: 'jpg' + } } } @@ -252,45 +245,6 @@ To add a format call the following method before using the plugin in the mongoos attachments.registerDecodingFormat('BMP'); ``` -##### Formats Provided by ImageMagick - -ImageMagick (or GraphicsMagick) list the supported formats when calling `convert -list format` (or `identify`). -The formats are flagged to show which operations are supported with each: - -* `*` native blob support (only ImageMagick, not GraphicsMagick) -* `r` read support -* `w` write support -* `+` support for multiple images - -You can register the formats that are supported for read operation like so: - -```javascript -attachments.registerImageMagickDecodingFormats(); -``` - -To register formats supporting different operations there is a more general function. Specifying certain operations will select only those formats that support all of them. Formats supporting only a subset won't be included. The following call yields the list of formats that support `read`,`write`,`multi`: - -```javascript -attachments.registerImageMagickFormats({ read: true, write: true, multi: true }); -``` - -If you want to use the output list that was generated for your own benefit you can specify a callback as second argument to that above method. Note, however, that in that case the supported decoding formats won't be changed on the plugin. - -You could use that callback to assure that the formats you want your client to support are indeed supported by the backing ImageMagick (or GraphicsMagick) installation. For example, checking TIFF support: - -```javascript -attachments.registerImageMagickFormats({ read: true }, function(error, formats) { - if (error) throw new Error(error); - else if (formats && formats.length > 0) { - if (formats.indexOf('TIFF') < 0) { - throw new Error('No TIFF support!'); - } - } else { - throw new Error("No formats supported for decoding!"); - } -}); -``` - ### Contributors * [Johan Hernandez](https://github.com/thepumpkin1979) diff --git a/lib/attachments.js b/lib/attachments.js index 42f7b0c..0062666 100644 --- a/lib/attachments.js +++ b/lib/attachments.js @@ -1,3 +1,5 @@ +/* jshint node:true */ + // Copyright (c) 2011-2013 Firebase.co - http://www.firebase.co // // Permission is hereby granted, free of charge, to any person obtaining a copy @@ -18,11 +20,15 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -var im = require('imagemagick'); +'use strict'; + +var gmSuper = require('gm'); var fs = require('fs'); var path = require('path'); +var _ = require('lodash'); var async = require('async'); var existsFn = fs.exists || path.exists; +var Q = require('q'); // keeps a global registry of storage providers var providersRegistry = {}; @@ -36,101 +42,65 @@ var supportedDecodingFormats = [ function findProvider(name) { var provider = providersRegistry[name]; - if(!provider) throw new Error('Storage Provider "' + name + '" can not be found'); + if (!provider) throw new Error('Storage Provider "' + name + '" can not be found'); return provider; } -function findImageMagickFormats(options, callback) { - var opts = { read: true }; - if (typeof options === 'function') { - callback = options; - } else if (options.read || options.write || options.multi || options.blob ) { - opts = options; - } else { - callback(new Error("Options have to contain one or more of 'read', 'write', 'multi', 'blob'")); - } - im.convert(['-list','format'], function(err, stdout, stderr) { - if (err) return callback(err); - if (stderr && stderr.search(/\S/) >= 0) return callback(new Error(stderr)); - if (stdout && stdout.search(/\S/) >= 0) { - // capture groups: - // 0: all - // 1: format - // 2: if '*' = native blob support; if ' ' (whitespace) none. Not set with graphicsmagick - therefore optional in regex - // 3: module - // 4: if 'r' = read support; if '-' none - // 5: if 'w' = write support; if '-' none - // 6: if '+' = support for multiple images; if '-' none - // 7: description - var regex = /^\s*([^\*\s]+)(\*|\s)?\s(\S+)\s+([-r])([-w])([-+])\s+(.*)$/; - var lines = stdout.split("\n"); - var comps = []; - var formats = []; - var i, currentLine; - for (i in lines) { - currentLine = lines[i]; - comps = regex.exec(currentLine); - if (comps) { - if ((!opts.read || comps[4] === 'r') && - (!opts.write || comps[5] === 'w') && - (!opts.multi || comps[6] === '+') && - (!opts.blob || comps[2] === '*')) { - formats.push(comps[1]); - } - } - } - return callback(null,formats); - } else { - return callback(new Error("No format supports the requested operation(s): " - + Object.keys(opts).toString() - + " . Check 'convert -list format'")); - } - }); -} - var plugin = function(schema, options) { + options = options || {}; - if(typeof(options.directory) !== 'string') throw new Error('option "directory" is required'); - if(typeof(options.properties) !== 'object') throw new Error('option "properties" is required'); - if(typeof(options.storage) !== 'object') throw new Error('option "storage" is required'); - if(typeof(options.idAsDirectory) !== 'boolean') options.idAsDirectory = false; + + if (typeof(options.directory) !== 'string') throw new Error('option "directory" is required'); + if (typeof(options.properties) !== 'object') throw new Error('option "properties" is required'); + if (typeof(options.storage) !== 'object') throw new Error('option "storage" is required'); + _.defaults(options, { idAsDirectory: false, gm: {} }); + _.defaults(options.gm, { imageMagick: true }); + + var gm = gmSuper.subClass(options.gm); var storageOptions = options.storage; storageOptions.schema = schema; - if(typeof(storageOptions.providerName) !== 'string') throw new Error('option "storage.providerName" is required'); - var providerPrototype = findProvider(storageOptions.providerName); - + if (typeof(storageOptions.providerName) !== 'string') throw new Error('option "storage.providerName" is required'); + var ProviderPrototype = findProvider(storageOptions.providerName); var providerOptions = storageOptions.options || {}; - var providerInstance = new providerPrototype(providerOptions); - if(typeof providerInstance.getUrl !== 'function') { - throw new Error('Provider ' + storageOptions.providerName + ' does not have a method getUrl'); + var providerInstance = new ProviderPrototype(providerOptions); + + if (typeof providerInstance.getUrl !== 'function') { + throw new Error('Provider ' + storageOptions.providerName + ' does not have a method getUrl'); } - if(typeof providerInstance.createOrReplace !== 'function') { - throw new Error('Provider ' + storageOptions.providerName + ' does not have a method createOrReplace'); + + if (typeof providerInstance.createOrReplace !== 'function') { + throw new Error('Provider ' + storageOptions.providerName + ' does not have a method createOrReplace'); } + var propertyNames = Object.keys(options.properties); propertyNames.forEach(function(propertyName) { + var propertyOptions = options.properties[propertyName]; - if(!propertyOptions) throw new Error('property "' + propertyName + '" requires an specification'); + if (!propertyOptions) throw new Error('property "' + propertyName + '" requires an specification'); var styles = propertyOptions.styles || {}; var styleNames = Object.keys(styles); - if(styleNames.length == 0) throw new Error('property "' + propertyName + '" needs to define at least one style'); + + if (styleNames.length === 0) { + throw new Error('property "' + propertyName + '" needs to define at least one style'); + } var addOp = {}; var propSchema = addOp[propertyName] = {}; + styleNames.forEach(function(styleName) { propSchema[styleName] = { - size: Number // Size of the File - , oname: String // Original name of the file - , mtime: Date - , ctime: Date - , path: String // Storage Path - , defaultUrl: String // Default (non-secure, most of the time public) Url - , format: String // Format of the File(provided by identify). - , depth: Number - , dims: { // Dimensions of the Image + size: Number, // Size of the File + oname: String, // Original name of the file + mtime: Date, + ctime: Date, + path: String, // Storage Path + defaultUrl: String, // Default (non-secure, most of the time public) Url + format: String, // Format of the File(provided by identify). + depth: Number, + dims: { // Dimensions of the Image h: Number, // Height w: Number // Width } @@ -139,227 +109,212 @@ var plugin = function(schema, options) { // Add the Property schema.add(addOp); + }); // for each property name // Finally we set the method 'attach' // => propertyName: String. Name of the property to attach the file to. // => attachmentInfo: { - // path: String(required). Path to the file in the file system. - // name: String(optional). Original Name of the file. - // mime: String(optional). Mime type of the file. + // path: String(required). Path to the file in the file system. + // name: String(optional). Original Name of the file. + // mime: String(optional). Mime type of the file. + // } schema.methods.attach = function(propertyName, attachmentInfo, cb) { + var selfModel = this; - if(propertyNames.indexOf(propertyName) == -1) return cb(new Error('property "' + propertyName + '" was not registered as an attachment property')); + if (propertyNames.indexOf(propertyName) == -1) return cb(new Error('property "' + propertyName + '" was not registered as an attachment property')); var propertyOptions = options.properties[propertyName]; var styles = propertyOptions.styles || {}; - if(!attachmentInfo || typeof(attachmentInfo) !== 'object') return cb(new Error('attachmentInfo is not valid')); - if(typeof(attachmentInfo.path) !== 'string') return cb(new Error('attachmentInfo has no valid path')); - if(!attachmentInfo.name) { + + if (!attachmentInfo || typeof(attachmentInfo) !== 'object') return cb(new Error('attachmentInfo is not valid')); + if (typeof(attachmentInfo.path) !== 'string') return cb(new Error('attachmentInfo has no valid path')); + if (!attachmentInfo.name) { // No original name provided? We infer it from the path attachmentInfo.name = path.basename(attachmentInfo.path); } + existsFn(attachmentInfo.path, function(exists) { - if(!exists) return cb(new Error('file to attach at path "' + attachmentInfo.path + '" does not exists')); + + if (!exists) { + return cb(new Error('file to attach at path "' + attachmentInfo.path + '" does not exists')); + } + fs.stat(attachmentInfo.path, function(err, stats) { - if(!stats.isFile()) return cb(new Error('path to attach from "' + attachmentInfo.path + '" is not a file')); - im.identify(attachmentInfo.path, function(err, atts) { - if(err) return cb(new Error('identify didn\'t work. Maybe imagemagick is not installed? "' + err + '"')); - // if 'identify' fails, that probably means the file is not an image. - var canTransform = !!atts && supportedDecodingFormats.indexOf(atts.format) != -1; + if (!stats.isFile()) { + return cb(new Error('path to attach from "' + attachmentInfo.path + '" is not a file')); + } + + Q.ninvoke(gm(attachmentInfo.path), 'format') + .then(function(format) { + // First we need to check whether or not the format is supported. + // If it's not, throw an error + return supportedDecodingFormats.indexOf(format) !== -1; + }) + + .fail(function(err) { + // Failing here means that the file format is not a supported image. + // So return false for `canTransform` + return false; + }) + + .then(function(canTransform) { + var fileExt = path.extname(attachmentInfo.path); var styles = propertyOptions.styles || {}; - var styleNames = Object.keys(styles); - - var tasks = []; - var stylesToReset = []; // names of the style that needs to be reset at the end of the process. - styleNames.forEach(function(styleName) { - var styleOptions = styles[styleName] || {}; - var finishConversion = function(styleFilePath, atts, cb) { - var ext = path.extname(styleFilePath); - var filenameId = options.filenameId ? selfModel[options.filenameId] : selfModel.id; - var storageStylePath = [ options.directory, propertyName, [ filenameId, styleName + ext].join( options.idAsDirectory ? '/':'-') ].join('/'); - if(storageStylePath[0] != '/'){ storageStylePath = '/' + storageStylePath; } - - fs.stat(styleFilePath, function(err, stats) { - if(err) return cb(err); - cb(null, { - style: { - name: styleName, - options: styleOptions - }, - filename: styleFilePath, - stats: stats, - propertyName: propertyName, - model: selfModel, - path: storageStylePath, - defaultUrl: null, // let the storage assign this - features: atts - }); - }); - }; - var optionKeys = Object.keys(styleOptions); - var transformationNames = []; - optionKeys.forEach(function(transformationName) { - if(transformationName.indexOf('$') != 0) { // if is not special command, add it as an special transformation argument - transformationNames.push(transformationName); - } + + return Q.all(_.map(styles, function(style, name) { + _.defaults(style, { + options: {}, + transform: function(i) { return i; } }); - if(optionKeys.length != 0) { - if(canTransform) { - var styleFileExt = styleOptions['$format'] ? ('.' + styleOptions['$format']) : fileExt; - var styleFileName = path.basename(attachmentInfo.path, fileExt); - styleFileName += '-' + styleName + styleFileExt; - var styleFilePath = path.join(path.dirname(attachmentInfo.path), styleFileName); - var convertArgs = [attachmentInfo.path]; // source file name - - // add all the transformations args - - transformationNames.forEach(function(transformationName) { - convertArgs.push('-' + transformationName); - if (styleOptions[transformationName] instanceof Array) { - styleOptions[transformationName].forEach(function (arg) { - convertArgs.push(arg); - }); - } else { - convertArgs.push(styleOptions[transformationName]); - } - }); - - convertArgs.push(styleFilePath); - tasks.push(function(cb) { - - // invoke 'convert' - im.convert(convertArgs, function(err, stdout, stderr) { - if(err) return cb(err); - - // run identify in the styled image - im.identify(styleFilePath, function(err, atts) { - if(err) return cb(err); - finishConversion(styleFilePath, atts, cb); - }); - }); - - }); // tasks.push - } else { - stylesToReset.push(styleName); - }// if can decode - } else { - // keep the file as original - tasks.push(function(cb) { - finishConversion(attachmentInfo.path, atts, cb); + + return Q.when({ + image: canTransform ? style.transform(gm(attachmentInfo.path)) : null, + file: canTransform ? null : fs.createReadStream(attachmentInfo.path), + style: style, + styleName: name, + attachmentInfo: attachmentInfo, + fileExt: fileExt + }); + })); + + }) + + .then(function(variants) { + + // Now write files to the temporary path. + // @todo: in the future, this should probably just pass the gm image/stream + // object to the provider. + return Q.all(_.map(variants, function(variant) { + var styleFileExt = variant.style.options.format ? ('.' + variant.style.options.format) : variant.fileExt; + var styleFileName = path.basename(variant.attachmentInfo.path, variant.fileExt); + styleFileName += '-' + variant.styleName + styleFileExt; + var styleFilePath = path.join(path.dirname(variant.attachmentInfo.path), styleFileName); + + return ( + variant.image ? + Q.ninvoke(variant.image, 'write', styleFilePath) : + (function() { + var deferred = Q.defer(); + variant.file.pipe(fs.createWriteStream(styleFilePath)) + .on('finish', deferred.resolve); + return deferred.promise; + })() + ) + .then(function() { + return _.merge(variant, { styleFilePath: styleFilePath }); }); - } + })); - }); // for each style + }) - async.parallel(tasks, function(err, convertResults) { - if(err) return cb(err); + .then(function(variants) { - //console.log(convertResults); - tasks = []; - convertResults.forEach(function(convertResult) { - tasks.push(function(cb) { + // Pass each individual file off to the registered provider + return Q.all(_.map(variants, function(variant) { - // tell the provider to create or replace the attachment - providerInstance.createOrReplace(convertResult, function(err, attachment) { - if(err) return cb(err); - cb(null, attachment); - }); + var ext = path.extname(variant.styleFilePath); + var filenameId = options.filenameId ? selfModel[options.filenameId] : selfModel.id; + var storageStylePath = path.join( + options.directory, + propertyName, + [filenameId, variant.styleName + ext].join(options.idAsDirectory ? '/':'-') + ); + if (storageStylePath[0] != '/') { + storageStylePath = '/' + storageStylePath; + } + + // Providers expect both stat and identify results for the output image + return Q.all([ + Q.ninvoke(fs, 'stat', variant.styleFilePath), + Q.ninvoke(gm(variant.styleFilePath).options(options.gm), 'identify') + .fail(function() { return null; }) + ]) + .spread(function(stats, atts) { + return { + style: { + name: variant.styleName, + options: variant.style + }, + filename: variant.styleFilePath, + stats: stats, + propertyName: propertyName, + model: selfModel, + path: storageStylePath, + defaultUrl: null, // let the storage assign this + features: atts + }; + }) + .then(function(providerInput) { + return Q.ninvoke(providerInstance, 'createOrReplace', providerInput); + }) + .then(function(storageResult) { + return _.merge(variant, { + storageResult: storageResult, + propertyName: propertyName }); }); - async.parallel(tasks, function(err, storageResults) { - if(err) return cb(err); - - // Finally Update the Model - var propModel = selfModel[propertyName]; - if(storageResults.length > 0) { // only update the model if a transformation was performed. - storageResults.forEach(function(styleStorage) { - var modelStyle = propModel[styleStorage.style.name]; - modelStyle.defaultUrl = styleStorage.defaultUrl; - modelStyle.path = styleStorage.path; - modelStyle.size = styleStorage.stats.size; - modelStyle.mime = styleStorage.mime; - modelStyle.ctime = styleStorage.stats.ctime; - modelStyle.mtime = styleStorage.stats.mtime; - modelStyle.oname = attachmentInfo.name; // original name of the file - if(atts) { - modelStyle.format = styleStorage.features.format; - modelStyle.depth = styleStorage.features.depth; - modelStyle.dims.h = styleStorage.features.height; - modelStyle.dims.w = styleStorage.features.width; - } - }); + })); + + }) + + .then(function(variants) { + + _.forEach(variants, function(variant) { + var propModel = selfModel[variant.propertyName]; + var modelStyle = propModel[variant.storageResult.style.name]; + + _.merge(modelStyle, { + defaultUrl: variant.storageResult.defaultUrl, + path: variant.storageResult.path, + size: variant.storageResult.stats.size, + mime: variant.storageResult.mime, + ctime: variant.storageResult.stats.ctime, + mtime: variant.storageResult.stats.mtime, + oname: variant.attachmentInfo.name, // original name of the file + }, variant.storageResult.features ? { + format: variant.storageResult.features.format, + depth: variant.storageResult.features.depth, + dims: { + h: variant.storageResult.features.size.height, + w: variant.storageResult.features.size.width, } + } : {}); + }); - stylesToReset.forEach(function(resetStyleName) { - var path = [propertyName, resetStyleName].join('.'); - selfModel.set(path, null); - }); + return variants; - cb(null); - }); + }) + + .then(function() { cb(null); }) + .fail(function(err) { return cb(err); }) + .done(); - }); - }); }); + }); + }; // method attach }; -// Prototype for Storage Providers -function StorageProvider(options) { - this.options = options; -} -StorageProvider.prototype.update = function(attachment, cb) { - throw new Error('method update implemented'); -}; -plugin.StorageProvider = StorageProvider; +plugin.StorageProvider = require('./storage_provider.js'); // Method to Register Storage Providers plugin.registerStorageProvider = function(name, provider) { - if(typeof(name) !== 'string') throw new Error('storage engine name is required'); - if(provider && provider._super == StorageProvider) throw new Error('provider is not valid. it does not inherits from StorageEngine'); + if (typeof(name) !== 'string') throw new Error('storage engine name is required'); + if (provider && provider._super == plugin.StorageProvider) throw new Error('provider is not valid. it does not inherits from StorageEngine'); providersRegistry[name] = provider; -} +}; + +plugin.findProvider = findProvider; // Register a Known Decoding Format(e.g 'PNG') plugin.registerDecodingFormat = function(name) { supportedDecodingFormats.push(name); -} - -/* - * Use this to register all formats for which your local ImageMagick installation supports - * read operations. - */ -plugin.registerImageMagickDecodingFormats = function() { - plugin.registerImageMagickFormats({ read: true }); -} - -/* - * You can register formats based on certain modes or a combination of those: - * 'read' : true|false - * 'write': true|false - * 'multi': true|false - * 'blob' : true|false - * options is optional and defaults to { read: true }. If several modes with value true are given, - * only formats supporting all of them are included. - */ -plugin.registerImageMagickFormats = function(options, callback) { - if (!callback) { - callback = function(error, formats) { - if (error) throw new Error(error); - else if (formats && formats.length > 0) { - supportedDecodingFormats = formats; - } else { - throw new Error("No formats supported for decoding!"); - } - }; - } - findImageMagickFormats(options, callback); -} +}; // Export the Plugin for mongoose.js module.exports = plugin; diff --git a/lib/storage_provider.js b/lib/storage_provider.js new file mode 100644 index 0000000..129872e --- /dev/null +++ b/lib/storage_provider.js @@ -0,0 +1,14 @@ +/* jshint node:true */ + +'use strict'; + +// Prototype for Storage Providers +function StorageProvider(options) { + this.options = options; +} + +StorageProvider.prototype.update = function(attachment, cb) { + throw new Error('method update implemented'); +}; + +module.exports = StorageProvider; diff --git a/package.json b/package.json index 9aaa4f1..c3063b9 100644 --- a/package.json +++ b/package.json @@ -17,17 +17,22 @@ }, "main": "index.js", "scripts": { - "test": "npm run specs && node ./test/testFindImageMagickFormats.js", - "specs": "./node_modules/.bin/mocha test/**/*.spec.js" + "test": "npm run specs", + "specs": "./node_modules/.bin/mocha -r test/bootstrap.js test/**/*.spec.js", + "watch": "./node_modules/.bin/mocha -r test/bootstrap.js test/**/*.spec.js --watch **/*.js" }, "dependencies": { "async": "0.1.x", - "imagemagick": "0.1.x" + "gm": "^1.17.0", + "lodash": "^2.4.1", + "q": "^1.0.1" }, "devDependencies": { - "mongoose": "^3.8.9", - "mocha": "^1.18.2", - "chai": "^1.9.1" + "chai": "^1.9.2", + "checksum": "^0.1.1", + "file": "^0.2.2", + "mocha": "^2.0.1", + "mongoose": "^3.8.18" }, "engines": { "node": "*" diff --git a/test/bootstrap.js b/test/bootstrap.js new file mode 100644 index 0000000..f0e73fc --- /dev/null +++ b/test/bootstrap.js @@ -0,0 +1,69 @@ +global.chai = require('chai'); +global.expect = chai.expect; + +var fs = require('fs'); +var path = require('path'); +var file = require('file'); +var mongoose = require('mongoose'); +var plugin = require('../lib/attachments.js'); + +try { + plugin.findProvider('fakeProvider'); +} catch (err) { + + var fakeProvider = function(){}; + + fakeProvider.prototype.getUrl = function(path){ + return path; + }; + + fakeProvider.prototype.createOrReplace = function(attachment, next){ + attachment.defaultUrl = this.getUrl(attachment.path); + file.mkdirsSync(path.dirname(attachment.path)); + fs.createReadStream(attachment.filename).pipe( + fs.createWriteStream(attachment.path) + .on('finish', function() { + next(null, attachment); + }) + ); + }; + + plugin.registerStorageProvider('fakeProvider', fakeProvider); + +} + +if (!mongoose.models.User) { + + UserSchema = new mongoose.Schema({ }); + + UserSchema.plugin(plugin, { + directory: path.join(process.cwd(), 'test', 'tmp'), + storage: { providerName: 'fakeProvider', options: { } }, + gm: { imageMagick: true, }, + properties: { + document: { + styles: { + original: {} + } + }, + profile: { styles: { original: { } } }, + avatar: { + styles: { + original: { }, + thumbnail: { + transform: function(image) { + return image + .thumbnail(25, 25) + .gravity('center') + ; + } + } + } + }, + } + }); + + mongoose.model('User', UserSchema); + +} + diff --git a/test/defaultUrl.spec.js b/test/defaultUrl.spec.js deleted file mode 100644 index 9402fe2..0000000 --- a/test/defaultUrl.spec.js +++ /dev/null @@ -1,41 +0,0 @@ -var mongoose = require('mongoose'); -var expect = require('chai').expect; -var fs = require('fs'); -var plugin = require('../lib/attachments'); - -var fakeProvider = function(){}; -fakeProvider.prototype.getUrl = function(path){ - return path; -}; -fakeProvider.prototype.createOrReplace = function(attachment, next){ - attachment.defaultUrl = this.getUrl(attachment.path); - next(null, attachment); -}; - -plugin.registerStorageProvider('fakeProvider', fakeProvider); - -UserSchema = new mongoose.Schema({ }); -UserSchema.plugin(plugin, { - directory: process.cwd() + '/public/users', - storage: { providerName: 'fakeProvider', options: { } }, - properties: { - profile: { styles: { original: { } } }, - avatar: { styles: { original: { } } } - } -}); -var User = mongoose.model('User', UserSchema); - -describe('path', function(){ - it('adds the propertyName in the attached image path', function(done){ - var user = new User({}); - var path = { path: process.cwd() + '/test/fixtures/mongodb.png' }; - user.attach('profile', path, function(err){ - user.attach('avatar', path, function(err){ - expect(user.avatar.original.defaultUrl).to.not.eql(user.profile.original.defaultUrl); - expect(user.avatar.original.defaultUrl).to.include('users/avatar/' + user.id + '-original.png'); - expect(user.profile.original.defaultUrl).to.include('users/profile/' + user.id + '-original.png'); - done(); - }) - }); - }); -}); diff --git a/test/fixtures/mongodb-original.png b/test/fixtures/mongodb-original.png new file mode 100644 index 0000000..e3d0848 Binary files /dev/null and b/test/fixtures/mongodb-original.png differ diff --git a/test/fixtures/mongodb-thumbnail-expected.png b/test/fixtures/mongodb-thumbnail-expected.png new file mode 100644 index 0000000..8a12d2c Binary files /dev/null and b/test/fixtures/mongodb-thumbnail-expected.png differ diff --git a/test/fixtures/sample.pdf b/test/fixtures/sample.pdf new file mode 100644 index 0000000..7f8996f Binary files /dev/null and b/test/fixtures/sample.pdf differ diff --git a/test/notimage.spec.js b/test/notimage.spec.js new file mode 100644 index 0000000..f50e851 --- /dev/null +++ b/test/notimage.spec.js @@ -0,0 +1,16 @@ +var mongoose = require('mongoose'); +var User = mongoose.model('User'); +var plugin = require('../lib/attachments.js'); + +describe('not an image', function(){ + + it('uploads pdf files correctly', function(done){ + var user = new User({}); + var path = { path: process.cwd() + '/test/fixtures/sample.pdf' }; + user.attach('document', path, function(err) { + expect(user.document.original.defaultUrl).to.include('tmp/document/' + user.id + '-original.pdf'); + done(); + }); + }); + +}); diff --git a/test/path.spec.js b/test/path.spec.js new file mode 100644 index 0000000..e380d62 --- /dev/null +++ b/test/path.spec.js @@ -0,0 +1,20 @@ +var mongoose = require('mongoose'); +var User = mongoose.model('User'); +var plugin = require('../lib/attachments.js'); + +describe('path', function(){ + + it('adds the propertyName in the attached image path', function(done){ + var user = new User({}); + var path = { path: process.cwd() + '/test/fixtures/mongodb.png' }; + user.attach('profile', path, function(err) { + user.attach('avatar', path, function(err) { + expect(user.avatar.original.defaultUrl).to.not.eql(user.profile.original.defaultUrl); + expect(user.avatar.original.defaultUrl).to.include('tmp/avatar/' + user.id + '-original.png'); + expect(user.profile.original.defaultUrl).to.include('tmp/profile/' + user.id + '-original.png'); + done(); + }); + }); + }); + +}); diff --git a/test/resampling.spec.js b/test/resampling.spec.js new file mode 100644 index 0000000..19679b4 --- /dev/null +++ b/test/resampling.spec.js @@ -0,0 +1,21 @@ +var mongoose = require('mongoose'); +var User = mongoose.model('User'); +var plugin = require('../lib/attachments.js'); +var checksum = require('checksum'); + +describe('resampling', function() { + + it('creates a separate image', function(done) { + var user = new User({}); + var path = { path: process.cwd() + '/test/fixtures/mongodb.png' }; + user.attach('avatar', path, function(err) { + checksum.file(user.avatar.thumbnail.path, function(err, generated) { + checksum.file(process.cwd() + '/test/fixtures/mongodb-thumbnail-expected.png', function(err, expected) { + expect(generated).to.equal(expected); + }); + }); + done(); + }); + }); + +}); diff --git a/test/testFindImageMagickFormats.js b/test/testFindImageMagickFormats.js deleted file mode 100644 index 3201d3f..0000000 --- a/test/testFindImageMagickFormats.js +++ /dev/null @@ -1,78 +0,0 @@ -var plugin = require('../lib/attachments'); - -// NOTE that these tests have been created on MacOS 10.7.5 with ImageMagick 6.7.7-6 installed via homebrew -// Check that the formats in the tests match your installation by calling -// 'convert -list format' and comparing the output -// -// CALL for test: node test/testFindImageMagickFormats.js -// -// Tests have passed if no error is thrown and the " support passed" messages are printed. - -plugin.registerImageMagickDecodingFormats(); - -plugin.registerImageMagickFormats({ read: true }, function(error, formats) { - if (error) console.log(error); - else { - if (formats.indexOf('DOT') >= 0) { - throw new Error ('DOT has no blob,read,write,multi support'); - } - if (formats.indexOf('XPS') < 0) { - throw new Error ('XPS has read support'); - } - if (formats.indexOf('UIL') >= 0) { - throw new Error ('UIL has no read,multi support'); - } - console.log("read support passed"); - } -}); - -plugin.registerImageMagickFormats({ write: true }, function(error, formats) { - if (error) console.log(error); - else { - if (formats.indexOf('DOT') >= 0) { - throw new Error ('DOT has no blob,read,write,multi support'); - } - if (formats.indexOf('XPS') >= 0) { - throw new Error ('XPS has no write,multi support'); - } - if (formats.indexOf('UIL') < 0) { - throw new Error ('UIL has write support'); - } - console.log("write support passed"); - } -}); - -plugin.registerImageMagickFormats({ write: true, blob: true }, function(error, formats) { - if (error) console.log(error); - else { - if (formats.indexOf('DOT') >= 0) { - throw new Error ('DOT has no blob,read,write,multi support'); - } - if (formats.indexOf('WMV') >= 0) { - throw new Error ('XPS has write but no blob support'); - } - if (formats.indexOf('UIL') < 0) { - throw new Error ('UIL has write and blob support'); - } - console.log("write and blob support passed"); - } -}); - -plugin.registerImageMagickFormats({ read: true, multi: true }, function(error, formats) { - if (error) console.log(error); - else { - if (formats.indexOf('DOT') >= 0) { - throw new Error ('DOT has no blob,read,write,multi support'); - } - if (formats.indexOf('XPS') >= 0) { - throw new Error ('XPS has read but no multi support'); - } - if (formats.indexOf('UIL') >= 0) { - throw new Error ('UIL has no read,multi support'); - } - if (formats.indexOf('X') < 0) { - throw new Error ('X has read and multi support'); - } - console.log("read and multi support passed"); - } -});