diff --git a/index.js b/index.js index 056dd89..0a1a7ee 100644 --- a/index.js +++ b/index.js @@ -24,6 +24,8 @@ JqueryFileUploadMiddleware.prototype.prepareOptions = function (options) { // height: 80 // } }, + fallbackUrl: false, // string/function + fallbackType: 'png', accessControl: { allowOrigin: '*', allowMethods: 'OPTIONS, HEAD, GET, POST, PUT, DELETE' @@ -46,12 +48,16 @@ JqueryFileUploadMiddleware.prototype.configure = function (options) { this.options = this.prepareOptions(options); }; -JqueryFileUploadMiddleware.prototype.fileHandler = function (options) { - return require('./lib/filehandler')(this, this.prepareOptions(_.extend(this.options, options))); +JqueryFileUploadMiddleware.prototype.fileHandler = function (options, callback) { + return require('./lib/filehandler')(this, this.prepareOptions(_.extend({}, this.options, options)), callback); }; JqueryFileUploadMiddleware.prototype.fileManager = function (options) { - return require('./lib/filemanager')(this, this.prepareOptions(_.extend(this.options, options))); + return require('./lib/filemanager')(this, this.prepareOptions(_.extend({}, this.options, options))); }; -module.exports = new JqueryFileUploadMiddleware(); +module.exports = function(options) { + var middleware = new JqueryFileUploadMiddleware(); + if (options) middleware.configure(options); + return middleware; +}; \ No newline at end of file diff --git a/lib/filehandler.js b/lib/filehandler.js index ce5a578..893ba4a 100644 --- a/lib/filehandler.js +++ b/lib/filehandler.js @@ -1,4 +1,4 @@ -module.exports = function (middleware, options) { +module.exports = function (middleware, options, callback) { return function (req, res, next) { res.set({ @@ -6,18 +6,18 @@ module.exports = function (middleware, options) { 'Access-Control-Allow-Methods': options.accessControl.allowMethods }); var UploadHandler = require('./uploadhandler')(options); - var handler = new UploadHandler(req, res, function (result, redirect) { + var handler = new UploadHandler(req, res, callback || function (result, files, redirect) { + var data = { files: result }; if (redirect) { - files = {files: result}; - res.redirect(redirect.replace(/%s/, encodeURIComponent(JSON.stringify(files)))); + res.redirect(redirect.replace(/%s/, encodeURIComponent(JSON.stringify(data)))); } else { res.set({ 'Content-Type': (req.headers.accept || '').indexOf('application/json') !== -1 ? 'application/json' : 'text/plain' }); - files = {files: result}; - res.json(200, { files: files }); + if (req.method == 'HEAD') return res.send(200); + res.json(200, data); } }); @@ -36,6 +36,15 @@ module.exports = function (middleware, options) { handler.on('delete', function (fileName) { middleware.emit('delete', fileName); }); + handler.on('file', function (fileInfo) { + middleware.emit('file', fileInfo); + }); + handler.on('image', function (fileInfo) { + middleware.emit('image', fileInfo); + }); + handler.on('processed', function (fileInfo, files) { + middleware.emit('processed', fileInfo, files); + }); switch (req.method) { case 'OPTIONS': diff --git a/lib/fileinfo.js b/lib/fileinfo.js index 0944269..513b373 100644 --- a/lib/fileinfo.js +++ b/lib/fileinfo.js @@ -1,5 +1,6 @@ var fs = require('fs'), - _ = require('lodash'); + _ = require('lodash'), + fileNameRegexp = /(?:(?:\-([\d]+))?(\.[^.]+))?$/; module.exports = function (options) { @@ -10,6 +11,11 @@ module.exports = function (options) { this.type = file.type; this.delete_type = 'DELETE'; }; + + FileInfo.prototype.isImage = function(regexp) { + regexp = regexp || options.imageTypes; + return regexp && regexp.test(this.name); + }; FileInfo.prototype.validate = function () { if (options.minFileSize && options.minFileSize > this.size) { @@ -27,8 +33,8 @@ module.exports = function (options) { this.name = require('path').basename(this.name).replace(/^\.+/, ''); // Prevent overwriting existing files: while (fs.existsSync(options.baseDir() + '/' + this.name)) { - this.name = this.name.replace(/(?:(?: \(([\d]+)\))?(\.[^.]+))?$/, function (s, index, ext) { - return ' (' + ((parseInt(index, 10) || 0) + 1) + ')' + (ext || ''); + this.name = this.name.replace(fileNameRegexp, function (s, index, ext) { + return '-' + ((parseInt(index, 10) || 0) + 1) + (ext || ''); }); } }; @@ -36,7 +42,16 @@ module.exports = function (options) { FileInfo.prototype.setUrl = function (type, baseUrl) { var key = type ? type + '_url' : 'url'; this[key] = baseUrl + '/' + encodeURIComponent(this.name); - } + }; + + FileInfo.prototype.setVersionUrl = function(type, url) { + var key = type ? type + '_url' : 'url'; + this[key] = url; + }; + + FileInfo.prototype.toResponse = function() { + return _.omit(this, 'processedFiles', 'metadata'); + }; return FileInfo; }; \ No newline at end of file diff --git a/lib/filemanager.js b/lib/filemanager.js index 5e2ff7f..78595a0 100644 --- a/lib/filemanager.js +++ b/lib/filemanager.js @@ -3,16 +3,16 @@ var _ = require('lodash'), path = require('path'), mkdirp = require('mkdirp'); -module.exports = function (middleware, options) { +module.exports = function (middleware, opts) { - options = _.extend({ + options = _.extend({}, { targetDir: function () { - return options.uploadDir(); + return opts.uploadDir(); }, targetUrl: function () { - return options.uploadUrl(); + return opts.uploadUrl(); } - }, options); + }, opts); _.each(['targetDir', 'targetUrl'], function (key) { if (!_.isFunction(options[key])) { @@ -37,19 +37,21 @@ module.exports = function (middleware, options) { fs.readdir(options.uploadDir(), _.bind(function (err, list) { _.each(list, function (name) { - var stats = fs.statSync(options.uploadDir() + '/' + name); - if (stats.isFile()) { - files[name] = { - path: options.uploadDir() + '/' + name - }; - _.each(options.imageVersions, function (value, version) { - counter++; - fs.exists(options.uploadDir() + '/' + version + '/' + name, function (exists) { - if (exists) - files[name][version] = options.uploadDir() + '/' + version + '/' + name; - finish(); + if (name.indexOf('.') != 0) { + var stats = fs.statSync(options.uploadDir() + '/' + name); + if (stats.isFile()) { + files[name] = { + path: options.uploadDir() + '/' + name + }; + _.each(options.imageVersions, function (value, version) { + counter++; + fs.exists(options.uploadDir() + '/' + version + '/' + name, function (exists) { + if (exists) + files[name][version] = options.uploadDir() + '/' + version + '/' + name; + finish(); + }); }); - }); + } } }, this); finish(); diff --git a/lib/uploadhandler.js b/lib/uploadhandler.js index bdab992..747f36d 100644 --- a/lib/uploadhandler.js +++ b/lib/uploadhandler.js @@ -4,14 +4,25 @@ var EventEmitter = require('events').EventEmitter, formidable = require('formidable'), imageMagick = require('imagemagick'), mkdirp = require('mkdirp'), + async = require('async'), _ = require('lodash'); + +var convertArgs = [ + 'srcPath', 'srcData', 'srcFormat', + 'dstPath', 'quality', 'format', + 'progressive', 'colorspace', + 'width', 'height', + 'strip', 'filter', + 'sharpening', 'customArgs', + 'timeout', 'gravity' +]; module.exports = function (options) { var FileInfo = require('./fileinfo')( _.extend({ baseDir: options.uploadDir - }, _.pick(options, 'minFileSize', 'maxFileSize', 'acceptFileTypes')) + }, _.pick(options, 'minFileSize', 'maxFileSize', 'acceptFileTypes', 'imageTypes')) ); var UploadHandler = function (req, res, callback) { @@ -35,15 +46,19 @@ module.exports = function (options) { var files = []; fs.readdir(options.uploadDir(), _.bind(function (err, list) { _.each(list, function (name) { - var stats = fs.statSync(options.uploadDir() + '/' + name), - fileInfo; - if (stats.isFile()) { - fileInfo = new FileInfo({ - name: name, - size: stats.size - }); - this.initUrls(fileInfo); - files.push(fileInfo); + if (name.indexOf('.') != 0) { + var stats = fs.statSync(options.uploadDir() + '/' + name), + fileInfo; + if (stats.isFile()) { + fileInfo = new FileInfo({ + name: name, + size: stats.size + }); + if (fileInfo.validate()) { + this.initUrls(fileInfo); + files.push(fileInfo); + } + } } }, this); this.callback(files); @@ -51,23 +66,25 @@ module.exports = function (options) { }; UploadHandler.prototype.post = function () { - + var self = this, form = new formidable.IncomingForm(), tmpFiles = [], files = [], map = {}, - counter = 1, redirect, - finish = _.bind(function () { + counter = 1, + finish = function() { if (!--counter) { + var data = []; _.each(files, function (fileInfo) { - this.initUrls(fileInfo); + this.initUrls(fileInfo, true); this.emit('end', fileInfo); + data.push(fileInfo.toResponse()); }, this); - this.callback(files, redirect); + this.callback(data, files, redirect); } - }, this); + }.bind(this); this.noCache(); @@ -87,51 +104,36 @@ module.exports = function (options) { } }) .on('file', function (name, file) { - var fileInfo = map[path.basename(file.path)]; + var mapKey = path.basename(file.path); + var fileInfo = map[mapKey]; if (fs.existsSync(file.path)) { fileInfo.size = file.size; if (!fileInfo.validate()) { fs.unlink(file.path); return; + } else { + counter++; } - - var generatePreviews = function () { - if (options.imageTypes.test(fileInfo.name)) { - _.each(options.imageVersions, function (value, version) { - // creating directory recursive - if (!fs.existsSync(options.uploadDir() + '/' + version + '/')) - mkdirp.sync(options.uploadDir() + '/' + version + '/'); - - counter++; - var opts = options.imageVersions[version]; - imageMagick.resize({ - width: opts.width, - height: opts.height, - srcPath: options.uploadDir() + '/' + fileInfo.name, - dstPath: options.uploadDir() + '/' + version + '/' + fileInfo.name, - customArgs: opts.imageArgs || ['-auto-orient'] - }, finish); - }); - } + + var handledFile = function(err, fileInfo, processedFiles) { + fileInfo.processedFiles = processedFiles || []; + finish(); } - if (!fs.existsSync(options.uploadDir() + '/')) - mkdirp.sync(options.uploadDir() + '/'); - - counter++; + if (!fs.existsSync(options.uploadDir() + '/')) mkdirp.sync(options.uploadDir() + '/'); + fs.rename(file.path, options.uploadDir() + '/' + fileInfo.name, function (err) { if (!err) { - generatePreviews(); - finish(); + self.processFile(fileInfo, handledFile); } else { var is = fs.createReadStream(file.path); var os = fs.createWriteStream(options.uploadDir() + '/' + fileInfo.name); is.on('end', function (err) { if (!err) { fs.unlinkSync(file.path); - generatePreviews(); + return self.processFile(fileInfo, handledFile); } - finish(); + handledFile(fileInfo, []); }); is.pipe(os); } @@ -149,35 +151,154 @@ module.exports = function (options) { self.emit('error', e); }) .on('progress', function (bytesReceived, bytesExpected) { - if (bytesReceived > options.maxPostSize) - self.req.connection.destroy(); + if (bytesReceived > options.maxPostSize) { + self.req.pause(); + } }) .on('end', finish) .parse(self.req); }; UploadHandler.prototype.destroy = function () { - var self = this, - fileName = path.basename(decodeURIComponent(this.req.url)); - - fs.unlink(options.uploadDir() + '/' + fileName, function (ex) { - _.each(options.imageVersions, function (value, version) { - fs.unlink(options.uploadDir() + '/' + version + '/' + fileName); - }); - self.emit('delete', fileName); - self.callback(!ex); - }); + var self = this, url = path.join(this.req.app.path() || '/', this.req.url); + var uploadUrl = options.uploadUrl(); + if (url.slice(0, uploadUrl.length) === uploadUrl) { + var fileName = path.basename(decodeURIComponent(this.req.url)); + if (fileName.indexOf('.') != 0) { + fs.unlink(options.uploadDir() + '/' + fileName, function (ex) { + _.each(options.imageVersions, function (value, version) { + fs.unlink(options.uploadDir() + '/' + version + '/' + fileName); + }); + self.emit('delete', fileName); + self.callback(!ex); + }); + } + } else { + self.callback(false); + } }; - UploadHandler.prototype.initUrls = function (fileInfo) { + UploadHandler.prototype.initUrls = function (fileInfo, noCheck) { + var fallbackType = options.fallbackType || 'png'; var baseUrl = (options.ssl ? 'https:' : 'http:') + '//' + (options.hostname || this.req.get('Host')); fileInfo.setUrl(null, baseUrl + options.uploadUrl()); fileInfo.setUrl('delete', baseUrl + this.req.originalUrl); - _.each(options.imageVersions, function (value, version) { - if (fs.existsSync(options.uploadDir() + '/' + version + '/' + fileInfo.name)) { - fileInfo.setUrl(version, baseUrl + options.uploadUrl() + '/' + version); - } - }, this); + if (fileInfo.isImage()) { + _.each(options.imageVersions, function (value, version) { + if (noCheck || fs.existsSync(options.uploadDir() + '/' + version + '/' + fileInfo.name)) { + fileInfo.setUrl(version, baseUrl + options.uploadUrl() + '/' + version); + } + }, this); + } else if (_.isString(options.fallbackUrl)) { + _.each(options.imageVersions, function (value, version) { + fileInfo.setVersionUrl(version, options.fallbackUrl + '/' + version + '.' + fallbackType); + }, this); + } else if (_.isFunction(options.fallbackUrl)) { + _.each(options.imageVersions, function (value, version) { + fileInfo.setVersionUrl(version, options.fallbackUrl(fileInfo, version, options));; + }, this); + } + }; + + UploadHandler.prototype.processFile = function(fileInfo, processOpts, callback) { + if (_.isFunction(processOpts)) { + callback = processOpts; + processOpts = _.extend({}, options); // use global options + } + var self = this; + var files = []; + var uploadDir = _.result(processOpts, 'uploadDir'); + var srcPath = uploadDir + '/' + fileInfo.name; + var isImage = fileInfo.isImage(processOpts.imageTypes); + var commands = []; + fileInfo.metadata = {}; + + // File metadata + if (processOpts.identify) { + if (isImage) { + commands.push(function(next) { + imageMagick.identify(srcPath, function(err, features) { + fileInfo.metadata = err ? {} : features; + fileInfo.metadata.fromOriginal = true; + next(); + }); + }); + } // could add generic file identify fn here + } + + // Generic processing, after images have been processed + _.each([].concat(processOpts.process || []), function(cmd) { + commands.push(function(next) { + cmd.call(null, fileInfo, srcPath, function(err, result) { + var info = _.extend({}, fileInfo, { srcPath: srcPath, result: result }); + if (err && !info.error) info.error = err; + if (_.isObject(result) && result instanceof FileInfo) { + files.push(result); + } + next(info.error); + }); + }); + }); + + // Image processing + if (isImage) { + commands.push(function(next) { + async.mapSeries(_.keys(processOpts.imageVersions || {}), function (version, done) { + var identify = processOpts.identify; + var dstPath = uploadDir + '/' + version + '/' + fileInfo.name; + var cb = function(err) { + var args = arguments; + var info = _.extend({}, fileInfo, { + srcPath: srcPath, dstPath: dstPath, version: version + }); + if (err) info.error = err; + if (!err && identify) { + imageMagick.identify(dstPath, function(err, features) { + info.metadata = err ? {} : features; + info.metadata.fromOriginal = false; + files.push(info); + done.apply(null, args); + }); + } else { + files.push(info); + done.apply(null, args); + } + }; + + var process = function(err) { + if (err) return cb(err); + var opts = processOpts.imageVersions[version] || {}; + if (_.isObject(fileInfo.error)) { + cb(fileInfo.error); + } else if (_.isFunction(opts)) { + opts.call(imageMagick, fileInfo, srcPath, dstPath, cb); + } else if (_.isArray(opts)) { // raw imagemagick convert + imageMagick.convert(opts, cb); + } else if (_.isObject(opts)) { + identify = (identify || opts.identify) && opts.identify !== false; + var m = opts.crop ? 'crop' : 'resize'; + var args = _.pick(opts, convertArgs); + args.srcPath = args.srcPath || srcPath; + args.dstPath = args.dstPath || dstPath; + args.customArgs = args.customArgs || opts.imageArgs || ['-auto-orient']; + imageMagick[m](args, cb); + } else { + cb(new Error('Invalid image version config: ' + version)); + } + } + + var versionDir = uploadDir + '/' + version + '/'; + fs.exists(versionDir, function(exists) { + exists ? process() : mkdirp(versionDir, process); + }); + }, next); + }); + } + + async.series(commands, function(err) { + if (!err) self.emit('processed', fileInfo, files); + callback(err, fileInfo, files); + }); }; return UploadHandler; diff --git a/package.json b/package.json index 11f4056..0526348 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "formidable": ">=1.0.11", "imagemagick": ">=0.1.2", "lodash": ">= 0.9.2", - "mkdirp": ">= 0.3.4" + "mkdirp": ">= 0.3.4", + "async": ">= 0.2.9" }, "engines": { "node": ">= 0.8.8"