diff --git a/.gitignore b/.gitignore index 40b878d..0ca9404 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,25 @@ -node_modules/ \ No newline at end of file +### intellij ### +*.iml +*.ipr +*.iws +.idea/ + +### node ### +lib-cov +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz + +pids +logs +results + +npm-debug.log +node_modules + +### example ### +public/lib/ \ No newline at end of file diff --git a/History.md b/History.md index b47f606..8a03709 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,49 @@ +0.1.7 / 2014-10-09 +================== + + * #58 improved support for IE9 + +0.1.6 / 2014-10-04 +================== + + * #56 avoid path traversal in DELETE requests + +0.1.5 / 2014-08-02 +================== + + * #31 request and response objects passed to event handlers + +0.1.4 / 2014-07-06 +================== + + * #53 documentation update + +0.1.3 / 2014-07-05 +================== + + * #46 merged in (reduce synchronous disk IO operations) + +0.1.2 / 2014-06-08 +================== + + * #31 merged in + +0.1.1 / 2014-02-04 +================== + + * #5 fixed + +0.1.0 / 2013-10-07 +================== + + * documentation cleanup + + +0.0.9 / 2013-10-07 +================== + + * compatibility with jQuery File Upload Plugin 5.32.6 + 0.0.8 / 2013-01-01 ================== diff --git a/README.md b/README.md index 0aaac0a..204c7a3 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,38 @@ On the frontend: ``` +To prevent access to /upload except for post (for security) +```javascript +upload.configure({ + uploadDir: __dirname + '/public/uploads/', + uploadUrl: '/uploads' +}); + +/// Redirect all to home except post +app.get('/upload', function( req, res ){ + res.redirect('/'); +}); + +app.put('/upload', function( req, res ){ + res.redirect('/'); +}); + +app.delete('/upload', function( req, res ){ + res.redirect('/'); +}); + +app.use('/upload', function(req, res, next){ + upload.fileHandler({ + uploadDir: function () { + return __dirname + '/public/uploads/' + }, + uploadUrl: function () { + return '/uploads' + } + })(req, res, next); +}); +``` + Overriding global configuration ```javascript @@ -68,7 +100,7 @@ More sophisticated example - Events app.use('/upload', upload.fileHandler()); // events - upload.on('begin', function (fileInfo) { + upload.on('begin', function (fileInfo, req, res) { // fileInfo structure is the same as returned to browser // { // name: '3 (3).jpg', @@ -81,10 +113,10 @@ More sophisticated example - Events // thumbnail_url: 'http://youhost/uploads/thumbnail/3%20(3).jpg' // } }); - upload.on('abort', function (fileInfo) { ... }); - upload.on('end', function (fileInfo) { ... }); - upload.on('delete', function (fileInfo) { ... }); - upload.on('error', function (e) { + upload.on('abort', function (fileInfo, req, res) { ... }); + upload.on('end', function (fileInfo, req, res) { ... }); + upload.on('delete', function (fileInfo, req, res) { ... }); + upload.on('error', function (e, req, res) { console.log(e.message); }); ``` @@ -245,6 +277,10 @@ Other options and their default values: ## Contributors * [@soomtong](http://github.com/soomtong) + * [@gsarwohadi](https://github.com/gsarwohadi) + * [@peecky](https://github.com/peecky) + * [@tonyspiro](https://github.com/tonyspiro) + * [@derjust](https://github.com/derjust) ## License Copyright (c) 2012 [Aleksandr Guidrevitch](http://aguidrevitch.blogspot.com/) diff --git a/examples/.bowerrc b/examples/.bowerrc new file mode 100644 index 0000000..0f29f6a --- /dev/null +++ b/examples/.bowerrc @@ -0,0 +1,4 @@ +{ + "directory": "public/lib", + "json": "bower.json" +} \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index 3f8c0d8..32c2010 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,9 +1,14 @@ ## YET ANOTHER JQUERY FILE UPLOAD MIDDLEWARE EXAMPLES +- use bower to install jquery-file-upload script (tested version 8.8.5) + to run ``` +project > npm insatll +project > cd examples project/examples > npm insatll +project/examples > bower insatll project/examples > node app.js ``` diff --git a/examples/app.js b/examples/app.js index 4667fcd..429526d 100644 --- a/examples/app.js +++ b/examples/app.js @@ -8,11 +8,9 @@ * */ var express = require('express'), http = require('http'), - //upload = require('../index'); - upload = require('jquery-file-upload-middleware'); // don't work 0.0.8 && update > 0.0.8 + upload = require('../'); -var cons = require('consolidate'), - swig = require('swig'); +var swig = require('swig'); // configuration @@ -26,15 +24,12 @@ var app = express(); // set template engine -app.engine('html', cons.swig); -swig.init({ - root: __dirname + '/views', - allowErrors: true, - cache: false +app.engine('html', swig.renderFile); +swig.setDefaults({ + cache: false // default 'memory' }); - // jquery-file-upload helper app.use('/upload/default', function (req, res, next) { upload.fileHandler({ @@ -74,6 +69,7 @@ upload.on('end', function (fileInfo) { upload.on('delete', function (fileName) { // remove file info + console.log("files remove complete"); console.log(fileName); }); @@ -114,7 +110,11 @@ app.configure('production', function () { * routes * */ app.get('/', function (req, res) { - res.send('Call this url in browser : http://localhost:3001/location/input'); + var html = [ + '

Call this url in browser : http://localhost:3001/location/input Go

', + '

Call this url in browser : http://localhost:3001/upload/location/list Go

' + ].join(''); + res.send(html); }); @@ -140,4 +140,5 @@ app.post('/location/input', function (req, res) { http.createServer(app).listen(app.get('port'), function () { console.log("Express server listening on port " + app.get('port')); console.log("access url /location/input"); + console.log("access url /upload/location/list"); }); diff --git a/examples/bower.json b/examples/bower.json new file mode 100644 index 0000000..1c7639f --- /dev/null +++ b/examples/bower.json @@ -0,0 +1,13 @@ +{ + "name": "jquery-file-upload-middleware", + "version": "0.0.1", + "ignore": [ + ".jshintrc", + "**/*.txt" + ], + "dependencies": { + "jquery-file-upload": "8.8.5" + }, + "devDependencies": { + } +} \ No newline at end of file diff --git a/examples/config.js b/examples/config.js index 129c04e..b4138cc 100644 --- a/examples/config.js +++ b/examples/config.js @@ -46,7 +46,7 @@ exports.resizeVersion = { }; exports.directors = { - temp: './public/tmp', + temp: './tmp', default: '/public/uploads/default', default_url: '/uploads/default', diff --git a/examples/package.json b/examples/package.json index d65805b..c435dee 100644 --- a/examples/package.json +++ b/examples/package.json @@ -2,9 +2,7 @@ "name":"examples", "version":"0.0.1", "dependencies":{ - "express":">= 3.0.0", - "consolidate":">= 0.4.0", - "swig":">= 0.12.0", - "jquery-file-upload-middleware": "> 0.0.8" + "express":"3.3.x", + "swig":"1.0.x" } } \ No newline at end of file diff --git a/examples/public/scripts/file_upload.js b/examples/public/scripts/file_upload.js index ea8d49d..603c48f 100644 --- a/examples/public/scripts/file_upload.js +++ b/examples/public/scripts/file_upload.js @@ -66,13 +66,13 @@ $('#location_gallery').fileupload({ row.find('.name a').text(file.name); if (file.url) { row.find('.preview').append('') - .find('img').prop('src', file.small_url); + .find('img').prop('src', file.smallUrl); row.find('a').prop('rel', 'gallery'); } row.find('a').prop('href', file.url); - row.find('.delete button') + row.find('.delete') .attr('data-type', file.delete_type) - .attr('data-url', file.delete_url); + .attr('data-url', file.deleteUrl); // add file data input row.append('') .find('input[name="galleryImage[]"]').val(file.name); @@ -151,13 +151,13 @@ $('#location_cover').fileupload({ row.find('.name a').text(file.name); if (file.url) { row.find('.preview').append('') - .find('img').prop('src', file.medium_url); + .find('img').prop('src', file.mediumUrl); row.find('a').prop('rel', 'gallery'); } row.find('a').prop('href', file.url); row.find('.delete button') .attr('data-type', file.delete_type) - .attr('data-url', file.delete_url); + .attr('data-url', file.deleteUrl); // add file data input row.append('') .find('input[name="excerptImage"]').val(file.name); diff --git a/examples/tmp/readme.md b/examples/tmp/readme.md new file mode 100644 index 0000000..f63726f --- /dev/null +++ b/examples/tmp/readme.md @@ -0,0 +1 @@ +temp folder \ No newline at end of file diff --git a/examples/views/form.html b/examples/views/form.html index 4a6b78d..1fe465d 100644 --- a/examples/views/form.html +++ b/examples/views/form.html @@ -1,8 +1,9 @@ {{title}} + + -
@@ -54,8 +55,8 @@
- - + +
@@ -65,16 +66,18 @@
- - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/index.js b/index.js index 056dd89..0ebaf48 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ var _ = require('lodash'), - EventEmitter = require('events').EventEmitter; + EventEmitter = require('events').EventEmitter, + os = require("os"); var JqueryFileUploadMiddleware = function () { EventEmitter.call(this); @@ -10,7 +11,7 @@ require('util').inherits(JqueryFileUploadMiddleware, EventEmitter); JqueryFileUploadMiddleware.prototype.prepareOptions = function (options) { options = _.extend({ - tmpDir: '/tmp', + tmpDir: os.tmpdir(), uploadDir: __dirname + '/public/files', uploadUrl: '/files/', maxPostSize: 11000000000, // 11 GB @@ -54,4 +55,4 @@ JqueryFileUploadMiddleware.prototype.fileManager = function (options) { return require('./lib/filemanager')(this, this.prepareOptions(_.extend(this.options, options))); }; -module.exports = new JqueryFileUploadMiddleware(); +module.exports = new JqueryFileUploadMiddleware(); \ No newline at end of file diff --git a/lib/filehandler.js b/lib/filehandler.js index 6870b5d..e7e227a 100644 --- a/lib/filehandler.js +++ b/lib/filehandler.js @@ -8,31 +8,32 @@ module.exports = function (middleware, options) { var UploadHandler = require('./uploadhandler')(options); var handler = new UploadHandler(req, res, function (result, redirect) { if (redirect) { - res.redirect(redirect.replace(/%s/, encodeURIComponent(JSON.stringify(result)))); + files = {files: result}; + res.redirect(redirect.replace(/%s/, encodeURIComponent(JSON.stringify(files)))); } else { res.set({ 'Content-Type': (req.headers.accept || '').indexOf('application/json') !== -1 ? 'application/json' : 'text/plain' }); - res.json(200, result); + res.status(200).json(result); } }); handler.on('begin', function (fileInfo) { - middleware.emit('begin', fileInfo); + middleware.emit('begin', fileInfo, req, res); }); handler.on('end', function (fileInfo) { - middleware.emit('end', fileInfo); + middleware.emit('end', fileInfo, req, res); }); handler.on('abort', function (fileInfo) { - middleware.emit('abort', fileInfo); + middleware.emit('abort', fileInfo, req, res); }); handler.on('error', function (e) { - middleware.emit('abort', e); + middleware.emit('abort', e, req, res); }); handler.on('delete', function (fileName) { - middleware.emit('delete', fileName); + middleware.emit('delete', fileName, req, res); }); switch (req.method) { diff --git a/lib/fileinfo.js b/lib/fileinfo.js index 0944269..873bcd0 100644 --- a/lib/fileinfo.js +++ b/lib/fileinfo.js @@ -8,7 +8,7 @@ module.exports = function (options) { this.originalName = file.name; this.size = file.size; this.type = file.type; - this.delete_type = 'DELETE'; + this.deleteType = 'DELETE'; }; FileInfo.prototype.validate = function () { @@ -34,7 +34,7 @@ module.exports = function (options) { }; FileInfo.prototype.setUrl = function (type, baseUrl) { - var key = type ? type + '_url' : 'url'; + var key = type ? type + 'Url' : 'url'; this[key] = baseUrl + '/' + encodeURIComponent(this.name); } diff --git a/lib/filemanager.js b/lib/filemanager.js index 5e2ff7f..d384c16 100644 --- a/lib/filemanager.js +++ b/lib/filemanager.js @@ -158,7 +158,7 @@ module.exports = function (middleware, options) { if (exists) { move(options.uploadDir() + '/' + version + '/' + filename, targetDir + '/' + version + '/', function (err, safename) { if (!err && relative) - urls[version + '_url'] = targetUrl + '/' + version + '/' + safename; + urls[version + 'Url'] = targetUrl + '/' + version + '/' + safename; finish(err); }); } diff --git a/lib/uploadhandler.js b/lib/uploadhandler.js index 5989002..070f018 100644 --- a/lib/uploadhandler.js +++ b/lib/uploadhandler.js @@ -4,7 +4,8 @@ var EventEmitter = require('events').EventEmitter, formidable = require('formidable'), imageMagick = require('imagemagick'), mkdirp = require('mkdirp'), - _ = require('lodash'); + _ = require('lodash'), + async = require('async'); module.exports = function (options) { @@ -25,33 +26,44 @@ module.exports = function (options) { UploadHandler.prototype.noCache = function () { this.res.set({ 'Pragma': 'no-cache', - 'Cache-Control': 'no-store, no-cache, must-revalidate', - 'Content-Disposition': 'inline; filename="files.json"' + 'Cache-Control': 'no-store, no-cache, must-revalidate' }); + if ((this.req.get("Accept") || "").indexOf("application/json") != -1) { + this.res.set({ + 'Content-Type': 'application/json', + 'Content-Disposition': 'inline; filename="files.json"' + }); + } else { + this.res.set({ 'Content-Type': 'text/plain' }); + } }; UploadHandler.prototype.get = function () { this.noCache(); 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); - } - }, this); - this.callback({files: files}); + async.each(list, _.bind(function(name, cb) { + fs.stat(options.uploadDir() + '/' + name, _.bind(function(err, stats) { + if (!err && stats.isFile()) { + var fileInfo = new FileInfo({ + name: name, + size: stats.size + }); + this.initUrls(fileInfo, function(err) { + files.push(fileInfo); + cb(err); + }); + } + else cb(err); + }, this)); + }, this), + _.bind(function(err) { + this.callback({files: files}); + }, this)); }, this)); }; UploadHandler.prototype.post = function () { - var self = this, form = new formidable.IncomingForm(), tmpFiles = [], @@ -61,11 +73,15 @@ module.exports = function (options) { redirect, finish = _.bind(function () { if (!--counter) { - _.each(files, function (fileInfo) { - this.initUrls(fileInfo); - this.emit('end', fileInfo); - }, this); - this.callback({files: files}, redirect); + async.each(files, _.bind(function(fileInfo, cb) { + this.initUrls(fileInfo, _.bind(function(err) { + this.emit('end', fileInfo); + cb(err); + }, this)); + }, this), + _.bind(function(err) { + this.callback({files: files}, redirect); + }, this)); } }, this); @@ -85,58 +101,63 @@ module.exports = function (options) { if (name === 'redirect') { redirect = value; } + if ( !self.req.fields ) + self.req.fields = {}; + self.req.fields[name] = value; }) .on('file', function (name, file) { + counter++; var fileInfo = map[path.basename(file.path)]; - if (fs.existsSync(file.path)) { - fileInfo.size = file.size; - if (!fileInfo.validate()) { - fs.unlink(file.path); - return; - } - - 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); - }); + fs.exists(file.path, function(exists) { + if (exists) { + fileInfo.size = file.size; + if (!fileInfo.validate()) { + fs.unlink(file.path); + finish(); + return; } - } - if (!fs.existsSync(options.uploadDir() + '/')) - mkdirp.sync(options.uploadDir() + '/'); + var generatePreviews = function () { + if (options.imageTypes.test(fileInfo.name)) { + _.each(options.imageVersions, function (value, version) { + counter++; + // creating directory recursive + mkdirp(options.uploadDir() + '/' + version + '/', function (err, made) { + 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); + }); + }); + } + } - counter++; - fs.rename(file.path, options.uploadDir() + '/' + fileInfo.name, function (err) { - if (!err) { - generatePreviews(); - finish(); - } else { - var is = fs.createReadStream(file.path); - var os = fs.createWriteStream(options.uploadDir() + '/' + fileInfo.name); - is.on('end', function (err) { + mkdirp(options.uploadDir() + '/', function(err, made) { + fs.rename(file.path, options.uploadDir() + '/' + fileInfo.name, function (err) { if (!err) { - fs.unlinkSync(file.path); generatePreviews(); + finish(); + } else { + var is = fs.createReadStream(file.path); + var os = fs.createWriteStream(options.uploadDir() + '/' + fileInfo.name); + is.on('end', function (err) { + if (!err) { + fs.unlink(file.path); + generatePreviews(); + } + finish(); + }); + is.pipe(os); } - finish(); }); - is.pipe(os); - } - }); - } + }); + } + else finish(); + }); }) .on('aborted', function () { _.each(tmpFiles, function (file) { @@ -160,24 +181,32 @@ module.exports = function (options) { var self = this, fileName = path.basename(decodeURIComponent(this.req.url)); - fs.unlink(options.uploadDir() + '/' + fileName, function (ex) { + var filepath = path.join(options.uploadDir(), fileName); + if (filepath.indexOf(options.uploadDir()) !== 0) { + self.emit('delete', fileName); + self.callback({success: false}); + return; + } + fs.unlink(filepath, function (ex) { _.each(options.imageVersions, function (value, version) { - fs.unlink(options.uploadDir() + '/' + version + '/' + fileName); + fs.unlink(path.join(options.uploadDir(), version, fileName)); }); self.emit('delete', fileName); self.callback({success: !ex}); }); }; - UploadHandler.prototype.initUrls = function (fileInfo) { + UploadHandler.prototype.initUrls = function (fileInfo, cb) { 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); + async.each(Object.keys(options.imageVersions), function(version, cb) { + fs.exists(options.uploadDir() + '/' + version + '/' + fileInfo.name, function(exists) { + if (exists) fileInfo.setUrl(version, baseUrl + options.uploadUrl() + '/' + version); + cb(null); + }) + }, + cb); }; return UploadHandler; diff --git a/package.json b/package.json index 11f4056..dc11e0c 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,13 @@ "express", "middleware" ], - "version": "0.0.8", + "version": "0.1.7", "dependencies": { "formidable": ">=1.0.11", "imagemagick": ">=0.1.2", "lodash": ">= 0.9.2", - "mkdirp": ">= 0.3.4" + "mkdirp": ">= 0.3.4", + "async": "*" }, "engines": { "node": ">= 0.8.8" @@ -28,6 +29,6 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, - "_id": "jquery-file-upload-middleware@0.0.8", + "_id": "jquery-file-upload-middleware@0.1.7", "license": "MIT" }