diff --git a/.gitignore b/.gitignore index cfdef68..16178d8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,11 @@ lib-cov *.out *.pid *.gz +*.iml pids logs results node_modules +coverage diff --git a/README.md b/README.md index 54210bb..cc5f932 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,63 @@ PostSchema.plugin(attachments, { var Post = mongoose.model('Post', PostSchema); ``` +#### Arrays of attachments + +To store attachment metadata in arrays, set the `array` flag in the plugin definition to `true`: + +```javascript +var mongoose = require('mongoose'); +var attachments = require('mongoose-attachments-localfs'); + +var PostSchema = new mongoose.Schema({ + title: String, + description: String +}); + +PostSchema.plugin(attachments, { + directory: 'achievements', + storage: { + providerName: 'localfs' + }, + properties: { + images: { + array: true, + styles: { + small: { + resize: '150x150' + }, + medium: { + resize: '120x120' + } + } + } + } +}); + +var Post = mongoose.model('Post', PostSchema); +``` + +..then later: + +```javascript +var post = new Post(); +// post.images.length == 0 + +// attach an image +post.attach('images', { + path: '/path/to/file' + }, function(error) { + // post.images.length == 1 + + // attach another image + post.attach('images', { + path: '/path/to/another/file' + }, function(error) { + // post.images.length == 2 + }); +}); +``` + #### Using with Express.js uploads Assuming that the HTML form sent a file in a field called 'image': diff --git a/lib/attachments.js b/lib/attachments.js index 4f7efbe..bf37677 100644 --- a/lib/attachments.js +++ b/lib/attachments.js @@ -23,6 +23,7 @@ var fs = require('fs'); var path = require('path'); var async = require('async'); var existsFn = fs.exists || path.exists; +var os = require('os'); // keeps a global registry of storage providers var providersRegistry = {}; @@ -90,7 +91,7 @@ function findImageMagickFormats(options, callback) { var plugin = function(schema, options) { options = options || {}; - if(typeof(options.directory) !== 'string') throw new Error('option "directory" is required'); + if(typeof(options.directory) !== 'string') options.directory = os.tmpdir(); 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; @@ -115,6 +116,7 @@ var plugin = function(schema, options) { if(!propertyOptions) throw new Error('property "' + propertyName + '" requires an specification'); var styles = propertyOptions.styles || {}; + var isArray = !!propertyOptions.array; var styleNames = Object.keys(styles); if(styleNames.length == 0) throw new Error('property "' + propertyName + '" needs to define at least one style'); @@ -137,6 +139,11 @@ var plugin = function(schema, options) { }; }); + if(isArray) { + // wrap the object literal in an array + addOp[propertyName] = [addOp[propertyName]]; + } + // Add the Property schema.add(addOp); }); // for each property name @@ -158,6 +165,12 @@ var plugin = function(schema, options) { // No original name provided? We infer it from the path attachmentInfo.name = path.basename(attachmentInfo.path); } + + if(propertyOptions.array) { + var propModel = selfModel[propertyName]; + var arrayEntryModel = propModel.create(); + } + existsFn(attachmentInfo.path, function(exists) { if(!exists) return cb(new Error('file to attach at path "' + attachmentInfo.path + '" does not exists')); fs.stat(attachmentInfo.path, function(err, stats) { @@ -178,7 +191,12 @@ var plugin = function(schema, options) { var finishConversion = function(styleFilePath, atts, cb) { var ext = path.extname(styleFilePath); var filenameId = options.filenameId ? selfModel[options.filenameId] : selfModel.id; - var storageStylePath = '/' + options.directory + '/' + [filenameId,styleName].join( options.idAsDirectory ? "/":"-") + ext; + + if(propertyOptions.array) { + filenameId = options.filenameId ? arrayEntryModel[options.filenameId] : arrayEntryModel.id; + } + + var storageStylePath = path.join(options.directory, [filenameId, propertyName + "-" + styleName].join(options.idAsDirectory ? "/" : "-") + ext); fs.stat(styleFilePath, function(err, stats) { if(err) return cb(err); @@ -209,7 +227,7 @@ var plugin = function(schema, options) { 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 styleFilePath = path.join(path.dirname(options.directory), styleFileName); var convertArgs = [attachmentInfo.path]; // source file name // add all the transformations args @@ -274,6 +292,11 @@ var plugin = function(schema, options) { // Finally Update the Model var propModel = selfModel[propertyName]; + + if(propertyOptions.array) { + propModel = arrayEntryModel; + } + if(storageResults.length > 0) { // only update the model if a transformation was performed. storageResults.forEach(function(styleStorage) { var modelStyle = propModel[styleStorage.style.name]; @@ -291,6 +314,10 @@ var plugin = function(schema, options) { modelStyle.dims.w = styleStorage.features.width; } }); + + if(propertyOptions.array) { + selfModel[propertyName].push(propModel); + } } stylesToReset.forEach(function(resetStyleName) { diff --git a/package.json b/package.json index a9585eb..9bd1479 100644 --- a/package.json +++ b/package.json @@ -4,19 +4,25 @@ "description": "Mongoose.js Attachments plugin. Supports ImageMagick styles", "keywords": ["mongoose", "s3", "imagemagick", "uploads", "attachments"], "version": "0.1.0", - "homepage": "https://github.com/firebaseco/mongoose-attachments", + "homepage": "https://github.com/heapsource/mongoose-attachments", "repository": { "type": "git", - "url": "git://github.com/firebaseco/mongoose-attachments.git" + "url": "git://github.com/heapsource/mongoose-attachments.git" }, "main": "index.js", "scripts": { - "test": "make test" + "test": "istanbul cover ./node_modules/mocha/bin/_mocha" }, "dependencies": { "async": "0.1.x", "imagemagick": "0.1.x" }, + "devDependencies": { + "mocha": "^1.18", + "should": "^3.2", + "mongoose": "^3.8", + "istanbul": "^0.2" + }, "engines": { "node": "*" } diff --git a/test/fixtures/StubSchema.js b/test/fixtures/StubSchema.js new file mode 100644 index 0000000..e71da92 --- /dev/null +++ b/test/fixtures/StubSchema.js @@ -0,0 +1,27 @@ +var attachments = require('./StubStorageProvider'); +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +var StubSchema = new Schema({ + name: { + type: String, + required: true + } +}); + +StubSchema.plugin(attachments, { + storage: { + providerName: 'testProvider' + }, + properties: { + image: { + styles: { + image: { + '$format': 'jpg' + } + } + } + } +}); + +module.exports = mongoose.model('Foo', StubSchema); diff --git a/test/fixtures/StubSchemaWithArrayProperty.js b/test/fixtures/StubSchemaWithArrayProperty.js new file mode 100644 index 0000000..3e01c28 --- /dev/null +++ b/test/fixtures/StubSchemaWithArrayProperty.js @@ -0,0 +1,28 @@ +var attachments = require('./StubStorageProvider'); +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +var StubSchemaWithArrayProperty = new Schema({ + name: { + type: String, + required: true + } +}); + +StubSchemaWithArrayProperty.plugin(attachments, { + storage: { + providerName: 'testProvider' + }, + properties: { + images: { + array: true, + styles: { + image: { + '$format': 'jpg' + } + } + } + } +}); + +module.exports = mongoose.model('Bar', StubSchemaWithArrayProperty); diff --git a/test/fixtures/StubSchemaWithMultipleFields.js b/test/fixtures/StubSchemaWithMultipleFields.js new file mode 100644 index 0000000..0ef67d4 --- /dev/null +++ b/test/fixtures/StubSchemaWithMultipleFields.js @@ -0,0 +1,34 @@ +var attachments = require('./StubStorageProvider'); +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +var StubSchema = new Schema({ + name: { + type: String, + required: true + } +}); + +StubSchema.plugin(attachments, { + storage: { + providerName: 'testProvider' + }, + properties: { + image1: { + styles: { + image: { + '$format': 'jpg' + } + } + }, + image2: { + styles: { + image: { + '$format': 'jpg' + } + } + } + } +}); + +module.exports = mongoose.model('Baz', StubSchema); diff --git a/test/fixtures/StubStorageProvider.js b/test/fixtures/StubStorageProvider.js new file mode 100644 index 0000000..339453f --- /dev/null +++ b/test/fixtures/StubStorageProvider.js @@ -0,0 +1,19 @@ +var attachments = require('../../lib/attachments'), + util = require('util'); + +function StubStorageProvider(options) { + attachments.StorageProvider.call(this, options); +} +util.inherits(StubStorageProvider, attachments.StorageProvider); + +StubStorageProvider.prototype.createOrReplace = function(attachment, callback) { + callback(null, attachment); +}; + +StubStorageProvider.prototype.getUrl = function( path ){ + return path; +}; + +attachments.registerStorageProvider('testProvider', StubStorageProvider); + +module.exports = attachments; diff --git a/test/fixtures/node_js_logo.png b/test/fixtures/node_js_logo.png new file mode 100644 index 0000000..10a9908 Binary files /dev/null and b/test/fixtures/node_js_logo.png differ diff --git a/test/testAttachments.js b/test/testAttachments.js new file mode 100644 index 0000000..3e82f9e --- /dev/null +++ b/test/testAttachments.js @@ -0,0 +1,82 @@ +var should = require('should'); +var Foo = require('./fixtures/StubSchema'); +var Bar = require('./fixtures/StubSchemaWithArrayProperty'); +var Baz = require('./fixtures/StubSchemaWithMultipleFields'); +var path = require('path'); +var async = require('async'); + +describe('Attachments', function() { + + it('should add an attachment', function(done) { + var foo = new Foo(); + foo.attach('image', { + path: path.resolve(__dirname, "./fixtures/node_js_logo.png") + }, function(error) { + should(error).be.null; + foo.image.image.oname.should.equal("node_js_logo.png"); + + done(); + }); + }) + + it('should add an attachment to an array', function(done) { + var bar = new Bar(); + bar.attach('images', { + path: path.resolve(__dirname, './fixtures/node_js_logo.png') + }, function(error) { + should(error).be.null; + Array.isArray(bar.images).should.be.true; + bar.images.length.should.equal(1); + bar.images[0].image.oname.should.equal('node_js_logo.png'); + + done(); + }); + }) + + it('should create unique names for multiple attachments', function(done) { + var bar = new Bar(); + + async.parallel([function(callback) { + bar.attach('images', { + path: path.resolve(__dirname, './fixtures/node_js_logo.png') + }, callback); + }, function(callback) { + bar.attach('images', { + path: path.resolve(__dirname, './fixtures/node_js_logo.png') + }, callback); + }], function(error) { + if(error) throw error; + + bar.images.length.should.equal(2); + bar.images[0].image.path.should.not.equal(bar.images[1].image.path); + + // should have used the sub-document id for the path name + path.basename(bar.images[0].image.path).should.startWith(bar.images[0].id); + + // and not the containing document's id + path.basename(bar.images[0].image.path).should.not.startWith(bar.id); + + done(); + }); + }) + + it('should allow multiple fields with the same style names', function(done) { + var baz = new Baz(); + + async.parallel([function(callback) { + baz.attach('image1', { + path: path.resolve(__dirname, './fixtures/node_js_logo.png') + }, callback); + }, function(callback) { + baz.attach('image2', { + path: path.resolve(__dirname, './fixtures/node_js_logo.png') + }, callback); + }], function(error) { + if(error) throw error; + + baz.image1.image.path.should.not.equal(baz.image2.image.path); + + done(); + }); + }) +}) diff --git a/test/testFindImageMagickFormats.js b/test/testFindImageMagickFormats.js index 3201d3f..79a344a 100644 --- a/test/testFindImageMagickFormats.js +++ b/test/testFindImageMagickFormats.js @@ -10,69 +10,84 @@ var plugin = require('../lib/attachments'); 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"); - } -}); +describe('ImageMagick formats', function() { + it('should be able to read the right formats', function (done) { + plugin.registerImageMagickFormats({ read: true }, function (error, formats) { + if (error) throw error; -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"); - } -}); + if (formats.indexOf('DOT') >= 0) { + throw new Error('DOT has no blob,read,write,multi support'); + } -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"); - } -}); + 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'); + } + + done(); + }); + }); + + it('should be able to write the right formats', function (done) { + plugin.registerImageMagickFormats({ write: true }, function (error, formats) { + if (error) throw error; + + 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'); + } + + done(); + }); + }); + + it('should be able to write the right formats with native blob support', function (done) { + plugin.registerImageMagickFormats({ write: true, blob: true }, function (error, formats) { + if (error) throw error; + + 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'); + } + + done(); + }); + }); + + it('should be able to read multiples of the right formats', function (done) { + plugin.registerImageMagickFormats({ read: true, multi: true }, function (error, formats) { + if (error) throw error; + + 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'); + } -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"); - } + done(); + }); + }); });