├── .gitignore ├── README.md ├── examples └── server.js ├── index.js ├── lib └── express-uploader.js ├── package.json ├── tea.yaml └── tests └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | examples/tmp/ 4 | examples/public/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## File uploader 2 | 3 | Uploading files middleware for NodeJS, Express, TrinteJS, Connect. 4 | 5 | ## Installation 6 | 7 | Installation is done using the Node Package Manager (npm). If you don't have npm installed on your system you can download it from [npmjs.org](http://npmjs.org/) 8 | To install express-uploader: 9 | 10 | $ npm install -g express-uploader 11 | 12 | ## Usage overview 13 | 14 | ### for TrinteJS 15 | 16 | #### manual setup in project config/routes.js 17 | 18 | ```js 19 | 20 | var Uploader = require('express-uploader'); 21 | 22 | module.exports = function routes(map) { 23 | ... 24 | map.all('/upload', function(req, res, next) { 25 | var uploader = new Uploader({ 26 | debug: true, 27 | validate: true, 28 | thumbnails: true, 29 | thumbToSubDir: true, 30 | tmpDir: __dirname + '/tmp', 31 | publicDir: __dirname + '/public', 32 | uploadDir: __dirname + '/public/files', 33 | uploadUrl: '/files/', 34 | thumbSizes: [140, [100, 100]] 35 | }); 36 | uploader.uploadFile(req, function(data) { 37 | res.send(JSON.stringify(data), {'Content-Type': 'text/plain'}, 200); 38 | }); 39 | }); 40 | }; 41 | ``` 42 | 43 | ### for ExpressJS 44 | 45 | ```js 46 | 47 | var Uploader = require('express-uploader'); 48 | 49 | app.all('/upload', function(req, res, next) { 50 | var uploader = new Uploader({ 51 | debug: true, 52 | validate: true, 53 | thumbnails: true, 54 | thumbToSubDir: true, 55 | tmpDir: __dirname + '/tmp', 56 | publicDir: __dirname + '/public', 57 | uploadDir: __dirname + '/public/files', 58 | uploadUrl: '/files/', 59 | thumbSizes: [140,[100, 100]] 60 | }); 61 | uploader.uploadFile(req, function(data) { 62 | res.send(JSON.stringify(data), {'Content-Type': 'text/plain'}, 200); 63 | }); 64 | }); 65 | 66 | ``` 67 | 68 | Options 69 | ----------------- 70 | 71 | | Name | Type | Default | Description | 72 | | --- | --- | --- | --- | 73 | | debug | boolean | false | 74 | | safeName | boolean | true | 75 | | validate | boolean | false | 76 | | quality | number | 80 | 77 | | thumbnails | boolean | false | 78 | | thumbToSubDir | boolean | false | 79 | | tmpDir | string | `/tmp` | 80 | | publicDir | string | `/public` | 81 | | uploadDir | string | `/public/files` | 82 | | uploadUrl | string | `/files/` | 83 | | maxPostSize | integer | 11000000 | 84 | | minFileSize | integer | 1 | 85 | | maxFileSize | integer | 10000000 | 86 | | acceptFileTypes | regexp | `/.+/i` | 87 | | thumbSizes | array | [[100, 100]] | [width, neight] | 88 | | imageTypes | regexp | `/\.(gif|jpe?g|png)$/i` | 89 | | resize | boolean | false | if need resize image | 90 | | newSize | mixed | `[800, 600]` | new size for image [width, height] | 91 | | crop | boolean | false | if need crop image | 92 | | coordinates | object | `{width:800,height:600,x:0,y:0}` | coordinates for crop image { width:1200, height:800, x:0, y:0 } | 93 | 94 | ## In the Wild 95 | 96 | The following projects use express-uploader. 97 | 98 | If you are using express-uploader in a project, app, or module, get on the list below 99 | by getting in touch or submitting a pull request with changes to the README. 100 | 101 | ### Recommend extensions 102 | 103 | - [Bootstrap Fancy File Plugin](http://biggora.github.io/bootstrap-fancyfile/) 104 | - [Bootstrap Ajax Typeahead Plugin](https://github.com/biggora/bootstrap-ajax-typeahead) 105 | - [TrinteJS - Javascrpt MVC Framework for Node.JS](http://www.trintejs.com/) 106 | - [CaminteJS - Cross-db ORM for NodeJS](http://www.camintejs.com/) 107 | - [MongoDB Session Storage for ExpressJS](https://github.com/biggora/express-mongodb) 108 | - [2CO NodeJS adapter for 2checkout API payment gateway](https://github.com/biggora/2co) 109 | 110 | ## Author 111 | 112 | Aleksej Gordejev (aleksej@gordejev.lv). 113 | 114 | 115 | ## License 116 | 117 | (The MIT License) 118 | 119 | Copyright (c) 2012 Aleksej Gordejev 120 | 121 | Permission is hereby granted, free of charge, to any person obtaining 122 | a copy of this software and associated documentation files (the 123 | 'Software'), to deal in the Software without restriction, including 124 | without limitation the rights to use, copy, modify, merge, publish, 125 | distribute, sublicense, and/or sell copies of the Software, and to 126 | permit persons to whom the Software is furnished to do so, subject to 127 | the following conditions: 128 | 129 | The above copyright notice and this permission notice shall be 130 | included in all copies or substantial portions of the Software. 131 | 132 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 133 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 134 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 135 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 136 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 137 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 138 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 139 | 140 | 141 | ## Resources 142 | 143 | - Visit the [author website](http://www.gordejev.lv). 144 | - Follow [@biggora](https://twitter.com/#!/biggora) on Twitter for updates. 145 | - Report issues on the [github issues](https://github.com/biggora/express-uploader/issues) page. 146 | 147 | [![Analytics](https://ga-beacon.appspot.com/UA-22788134-5/express-uploader/readme)](https://github.com/igrigorik/ga-beacon) 148 | 149 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/biggora/express-uploader/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 150 | 151 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013 Alexey Gordeyev . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | var fs = require('fs'); 26 | var express = require('express'); 27 | var multiparty = require('connect-multiparty'); 28 | var bodyParser = require('body-parser'); 29 | var methodOverride = require('method-override'); 30 | var cookieParser = require('cookie-parser'); 31 | var morgan = require('morgan'); 32 | var Uploader = require('../lib/express-uploader'); 33 | var app = express(); 34 | 35 | // Configuration 36 | 37 | app.use(morgan('dev')); 38 | // We need the bodyParser to form parsing old style uploads 39 | app.use(multiparty({ 40 | uploadDir: __dirname + '/tmp', 41 | keepExtensions: true, 42 | encoding: 'utf8' 43 | })); 44 | // parse application/x-www-form-urlencoded 45 | app.use(bodyParser.urlencoded({extended: false})); 46 | // parse application/json 47 | app.use(bodyParser.json()); 48 | // parse application/vnd.api+json as json 49 | app.use(bodyParser.json({type: 'application/vnd.api+json'})); 50 | app.use(methodOverride('X-HTTP-Method')); // Microsoft 51 | app.use(methodOverride('X-HTTP-Method-Override')); // Google/GData 52 | app.use(methodOverride('X-Method-Override')); // IBM 53 | app.use(methodOverride('_method')); // simulate DELETE and PUT 54 | app.use(methodOverride(function(req, res) { 55 | if (req.body && typeof req.body === 'object' && '_method' in req.body) { 56 | // look in urlencoded POST bodies and delete it 57 | var method = req.body._method; 58 | delete req.body._method; 59 | return method; 60 | } 61 | })); 62 | app.use(cookieParser('weritas10')); 63 | 64 | 65 | /* 66 | * Display upload form 67 | */ 68 | app.all('/', function(req, res) { 69 | res.send( 70 | '
' + 71 | ' ' + 72 | ' ' + 73 | '
' 74 | ); 75 | }); 76 | 77 | /* 78 | * Route that takes the post upload request and sends the server response 79 | */ 80 | app.all('/upload', function(req, res, next) { 81 | var uploader = new Uploader({ 82 | debug: true, 83 | validate: true, 84 | thumbnails: true, 85 | thumbToSubDir: true, 86 | tmpDir: __dirname + '/tmp', 87 | publicDir: __dirname + '/public', 88 | uploadDir: __dirname + '/public/files', 89 | uploadUrl: '/files/', 90 | thumbSizes: [140, [100, 100]] 91 | }); 92 | 93 | uploader.uploadFile(req, function(data) { 94 | res.send(JSON.stringify(data), {'Content-Type': 'text/plain'}, 200); 95 | }); 96 | }); 97 | 98 | // Create tmp dir 99 | if (!fs.existsSync(__dirname + '/tmp')) { 100 | fs.mkdirSync(__dirname + '/tmp', 0755); 101 | } 102 | 103 | app.listen(3000, '127.0.0.1'); 104 | console.log("Express server listening on %s:%d for uploads", '127.0.0.1', 3000); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013 Alexey Gordeyev . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | module.exports = require('./lib/express-uploader'); -------------------------------------------------------------------------------- /lib/express-uploader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013 Alexey Gordeyev . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | var fs = require( 'fs' ); 26 | var path = require( 'path' ); 27 | var util = require( 'util' ); 28 | var uuid = require( 'uuid' ); 29 | var gm = require( 'gm' ); 30 | 31 | var defaultOptions = { 32 | debug: false, 33 | safeName: true, 34 | validate: false, 35 | resize: false, 36 | crop: false, 37 | quality: 80, 38 | thumbnails: false, 39 | thumbToSubDir: false, 40 | osSep: /^win/i.test( process.platform ) ? '\\' : '/', 41 | tmpDir: __dirname + '/tmp', 42 | publicDir: __dirname + '/public', 43 | uploadDir: __dirname + '/public/files', 44 | uploadUrl: '/files/', 45 | maxPostSize: 11000000, //000 110 MB 46 | minFileSize: 1, 47 | maxFileSize: 10000000, //0000 100 MB 48 | acceptFileTypes: /.+/i, 49 | thumbSizes: [ 50 | [ 100, 100 ] 51 | ], 52 | newSize: [ 800, 600 ], 53 | coordinates: { width: 800, height: 600, x: 0, y: 0 }, 54 | inlineFileTypes: /\.(gif|jpe?g|png)$/i, 55 | imageTypes: /\.(gif|jpe?g|png)$/i 56 | }; 57 | 58 | module.exports = function Uploader(options) { 59 | var settings = {}, osSep = defaultOptions.osSep; 60 | Object.keys( defaultOptions ).forEach( function (key) { 61 | settings[ key ] = defaultOptions[ key ]; 62 | } ); 63 | if ( options ) { 64 | Object.keys( options ).forEach( function (key) { 65 | settings[ key ] = options[ key ]; 66 | } ); 67 | } 68 | Object.keys( settings ).forEach( function (key) { 69 | switch ( key ) { 70 | case 'tmpDir': 71 | case 'publicDir': 72 | case 'uploadDir': 73 | settings[ key ] = path.normalize( settings[ key ] ); 74 | if ( !new RegExp( settings.osSep + '$' ).test( settings[ key ] ) ) { 75 | settings[ key ] = settings[ key ] + settings.osSep; 76 | } 77 | break; 78 | } 79 | } ); 80 | 81 | settings.nameCountRegexp = /(?:(?:_([\d]+))?(\.[^.]+))?$/; 82 | settings.nameCountFunc = function (s, index, ext) { 83 | return '_' + ((parseInt( index, 10 ) || 0) + 1) + '' + (ext || ''); 84 | }; 85 | 86 | return { 87 | settings: settings, 88 | pathToRoot: function () { 89 | return __dirname; 90 | }, 91 | _existsSync: fs.existsSync || path.existsSync, 92 | utf8encode: function (str) { 93 | return unescape( encodeURIComponent( str ) ); 94 | }, 95 | removeFile: function (filename, callback) { 96 | var self = this; 97 | var fName = path.basename( filename ); 98 | if ( fName && fName !== "" ) { 99 | if ( fs.existsSync( self.settings.uploadDir + osSep + fName ) ) { 100 | fs.unlink( self.settings.uploadDir + osSep + fName ); 101 | } 102 | } 103 | return callback && callback(); 104 | }, 105 | uploadFile: function (req, done) { 106 | var self = this, totalFiles = 0, files = [], info = [], toUpload = req.files; 107 | self.logging( "Start Uploader!" ); 108 | self.safeCreateDirectory( self.settings.tmpDir ); 109 | self.safeCreateDirectory( self.settings.publicDir ); 110 | self.safeCreateDirectory( self.settings.uploadDir ); 111 | req.files = {}; 112 | // Direct async xhr stream data upload, yeah baby. 113 | if ( req.xhr && !Object.keys( toUpload ).length ) { 114 | var fname = req.header( 'x-file-name' ); 115 | var fsize = parseInt( req.header( 'x-file-size' ), 10 ); 116 | var extension = path.extname( fname ).toLowerCase(); 117 | // Be sure you can write to '/tmp/' 118 | var tmpfile = self.settings.tmpDir + uuid.v1() + extension; 119 | var file = { 120 | path: tmpfile, 121 | name: fname, 122 | size: fsize, 123 | type: "" 124 | }; 125 | // Open a temporary writestream 126 | var ws = fs.createWriteStream( tmpfile, { 127 | flags: 'w', 128 | encoding: 'binary', 129 | mode: 0755 130 | } ); 131 | 132 | ws.on( 'error', function (err) { 133 | self.logging( " uploadFile() - req.xhr - could not open writestream." ); 134 | file.success = false; 135 | file.error = "Sorry, could not open writestream."; 136 | done( file ); 137 | } ); 138 | ws.on( 'close', function (err) { 139 | var inValid = false; 140 | if ( self.settings.validate ) { 141 | self.logging( " Validate File!" ); 142 | inValid = self.validate( file ); 143 | } 144 | self.moveFile( file, self.settings.uploadDir, inValid, function (finfo) { 145 | self.uploadInfo( finfo ); 146 | self.logging( "Uploader closed!" ); 147 | done( finfo ); 148 | } ); 149 | } ); 150 | ws.on( 'open', function () { 151 | self.logging( "Stream Open!" ); 152 | req.pipe( ws ); 153 | } ); 154 | // Writing filedata into writestream 155 | req.on( 'data', function (data) { 156 | self.logging( "Uploader onData!" ); 157 | // ws.write(data); 158 | } ); 159 | req.on( 'end', function () { 160 | self.logging( "Uploader onEnd!" ); 161 | ws.end(); 162 | } ); 163 | // req.pipe(ws); 164 | } else { 165 | Object.keys( toUpload ).forEach( function (key) { 166 | if ( Object.prototype.toString.call( toUpload[ key ] ) === '[object Array]' ) { 167 | toUpload[ key ].forEach( function (rfile) { 168 | if ( typeof rfile.path !== 'undefined' ) { 169 | ++totalFiles; 170 | files.push( rfile ); 171 | } else if ( typeof rfile === 'object' ) { 172 | for ( var i in rfile ) { 173 | if ( typeof rfile[ i ].path !== 'undefined' ) { 174 | ++totalFiles; 175 | files.push( rfile[ i ] ); 176 | } 177 | } 178 | } 179 | } ); 180 | } else { 181 | if ( typeof toUpload[ key ].path !== 'undefined' ) { 182 | ++totalFiles; 183 | files.push( toUpload[ key ] ); 184 | } else if ( typeof toUpload[ key ] === 'object' ) { 185 | var iFile = toUpload[ key ]; 186 | for ( var i in iFile ) { 187 | if ( typeof iFile[ i ].path !== 'undefined' ) { 188 | ++totalFiles; 189 | files.push( iFile[ i ] ); 190 | } 191 | } 192 | } 193 | } 194 | } ); 195 | self.logging( ' Received files: ' + totalFiles ); 196 | if ( totalFiles > 0 ) { 197 | fs.readdir( self.settings.uploadDir, function (err, uploadedFiles) { 198 | files.forEach( function (file) { 199 | var inValid = false; 200 | if ( self.settings.validate ) { 201 | self.logging( " Validate File!" ); 202 | inValid = self.validate( file ); 203 | } 204 | self.safeName( uploadedFiles, file.name, function (safeName) { 205 | if(self.settings.safeName){ 206 | file.safeName = safeName; 207 | } else { 208 | file.safeName = path.basename(file.path); 209 | } 210 | 211 | self.moveFile( file, self.settings.uploadDir, inValid, function (fInfo) { 212 | self.createThumbnail( fInfo, function (finfo) { 213 | info.push( finfo ); 214 | if ( --totalFiles === 0 ) { 215 | var totalUploaded = 0; 216 | info.forEach( function (inf) { 217 | self.uploadInfo( inf ); 218 | if ( inf.success ) { 219 | ++totalUploaded; 220 | } 221 | } ); 222 | self.logging( ' Total uploaded files: ' + totalUploaded ); 223 | self.logging( "Uploader closed!" ); 224 | done( info ); 225 | } 226 | } ); 227 | } ); 228 | } ); 229 | } ); 230 | } ); 231 | } else { 232 | done( { 233 | error: 'Not files found!' 234 | } ); 235 | } 236 | } 237 | }, 238 | moveFile: function (file, dest, inValid, callback) { 239 | var self = this, source = file.path, info = { 240 | originalName: file.name, 241 | name: file.safeName, 242 | size: file.size, 243 | type: file.type, 244 | destinationDir: dest, 245 | url: '', 246 | thumbnails: [], 247 | thumbnailObj: {} 248 | }; 249 | self.logging( ' moveFile() - Start moving.' ); 250 | 251 | info.url = self.settings.uploadUrl + info.name; 252 | 253 | if ( !inValid ) { 254 | try { 255 | var is = fs.createReadStream( source ); 256 | is.on( 'error', function (err) { 257 | self.logging( ' moveFile() - Could not open readstream.' ); 258 | info.success = false; 259 | info.error = 'Sorry, could not open readstream.'; 260 | callback( info ); 261 | } ); 262 | 263 | is.on( 'open', function () { 264 | var os = fs.createWriteStream( dest + info.name ); 265 | os.on( 'error', function (err) { 266 | self.logging( ' moveFile() - Could not open writestream.', err ); 267 | info.success = false; 268 | info.error = 'Sorry, could not open writestream.'; 269 | callback( info ); 270 | } ); 271 | 272 | os.on( 'open', function () { 273 | if ( self.settings.imageTypes.test( info.originalName ) ) { 274 | if ( self.settings.resize && self.settings.imageTypes.test( info.originalName ) ) { 275 | self.logging( " Resize image: ", self.settings.newSize ); 276 | var gM = gm( is, info.originalName ); 277 | if ( Object.prototype.toString.call( self.settings.newSize ) === '[object Array]' ) { 278 | if ( self.settings.newSize[ 1 ] ) { 279 | gM.resize( self.settings.newSize[ 0 ], self.settings.newSize[ 1 ] ); 280 | } else { 281 | gM.resize( self.settings.newSize[ 0 ] ); 282 | } 283 | } else { 284 | gM.resize( self.settings.newSize ); 285 | } 286 | gM 287 | .quality( self.settings.quality ) 288 | .stream() 289 | .pipe( os ); 290 | } else if ( self.settings.crop && self.settings.coordinates && self.settings.imageTypes.test( info.originalName ) ) { 291 | self.logging( " Crop image: ", self.settings.coordinates ); 292 | var cO = self.settings.coordinates; 293 | var gM = gm( is, info.originalName ); 294 | gM 295 | .crop( cO.width, cO.height, cO.x, cO.y ) 296 | .quality( self.settings.quality ) 297 | .stream() 298 | .pipe( os ); 299 | } else { 300 | is.pipe( os ); 301 | } 302 | } else { 303 | is.pipe( os ); 304 | } 305 | os.on( 'close', function () { 306 | info.success = true; 307 | info.error = null; 308 | self.logging( ' moveFile() - End moving.' ); 309 | process.nextTick( function () { 310 | fs.unlinkSync( source ); 311 | callback( info ); 312 | } ); 313 | } ); 314 | } ); 315 | } ); 316 | } catch ( err ) { 317 | self.logging( err ); 318 | info.success = false; 319 | info.error = 'moveFile() - Exception.'; 320 | callback( info ); 321 | } 322 | } else { 323 | fs.unlinkSync( source ); 324 | info.success = false; 325 | info.error = inValid; 326 | callback( info ); 327 | } 328 | }, 329 | safeCreateDirectory: function (dir) { 330 | var self = this; 331 | var fullPath = /^win/i.test( process.platform ) ? '' : '/'; 332 | var parts = path.normalize( dir ).split( self.settings.osSep ); 333 | parts.forEach( function (part) { 334 | if ( part !== '' ) { 335 | fullPath = path.normalize( path.join( fullPath, part ) ); 336 | if ( /\.$/.test( fullPath ) ) { 337 | fullPath = fullPath.replace( /\.$/, self.settings.osSep ); 338 | } 339 | if ( part !== "" && !self._existsSync( fullPath ) ) { 340 | try { 341 | fs.mkdirSync( fullPath, 0755 ); 342 | self.logging( " Create target directory: " + fullPath ); 343 | } catch ( err ) { 344 | 345 | } 346 | } 347 | } 348 | } ); 349 | }, 350 | safeName: function (files, name, cb) { 351 | var self = this, total = files.length; 352 | // Prevent directory traversal and creating hidden system files: 353 | name = path.basename( name ).replace( /^\.+/, '' ); 354 | // Prevent overwriting existing files: 355 | for ( var f in files ) { 356 | while ( new RegExp( name + '$', 'i' ).test( files[ f ] ) ) { 357 | name = name.toString().replace( self.settings.nameCountRegexp, self.settings.nameCountFunc ); 358 | } 359 | if ( --total === 0 ) { 360 | self.logging( ' final: ' + name ); 361 | cb( name ); 362 | } 363 | } 364 | if ( files.length === 0 ) { 365 | self.logging( ' final: ' + name ); 366 | cb( name ); 367 | } 368 | }, 369 | validate: function (file) { 370 | var self = this, error = false; 371 | if ( self.settings.minFileSize && self.settings.minFileSize > file.size ) { 372 | error = 'File is too small'; 373 | } else if ( self.settings.maxFileSize && self.settings.maxFileSize < file.size ) { 374 | error = 'File is too big'; 375 | } else if ( !self.settings.acceptFileTypes.test( file.name ) ) { 376 | error = 'Filetype not allowed'; 377 | } 378 | return error; 379 | }, 380 | createThumbnail: function (info, cb) { 381 | var self = this; 382 | if ( self.settings.thumbnails && self.settings.imageTypes.test( info.originalName ) ) { 383 | self.logging( "Create Thumbnails!" ); 384 | var thumbSizes = (self.settings.thumbSizes || []); 385 | var totalSizes = thumbSizes.length; 386 | if ( totalSizes > 0 ) { 387 | thumbSizes.forEach( function (thumbSize) { 388 | self.logging( "Create Thumbnail: ", thumbSize ); 389 | var thumbSubDir = self.settings.uploadDir; 390 | var thumbSubUrl = self.settings.uploadUrl; 391 | var thumbName = ""; 392 | var imgData = {}; 393 | if ( Object.prototype.toString.call( thumbSize ) === '[object Array]' ) { 394 | imgData.width = thumbSize[ 0 ]; 395 | if ( !thumbSize[ 1 ] ) { 396 | thumbSize.push( thumbSize[ 0 ] ) 397 | } 398 | imgData.height = thumbSize[ 1 ]; 399 | var sizesStr = thumbSize.join( 'x' ); 400 | thumbSubDir += self.settings.thumbToSubDir ? sizesStr : ''; 401 | thumbSubUrl += self.settings.thumbToSubDir ? sizesStr + '/' : ''; 402 | thumbName += 'thumb_' + sizesStr + '_'; 403 | } else { 404 | imgData.width = thumbSize; 405 | thumbSubDir += self.settings.thumbToSubDir ? thumbSize : ''; 406 | thumbSubUrl += self.settings.thumbToSubDir ? thumbSize + '/' : ''; 407 | thumbName += 'thumb_' + thumbSize + '_'; 408 | } 409 | if ( self.settings.thumbToSubDir ) { 410 | thumbName = info.name; 411 | self.safeCreateDirectory( thumbSubDir ); 412 | } else { 413 | thumbName += info.name; 414 | } 415 | var destinationDir = info.destinationDir.replace( /\/$|\\$/, '' ); 416 | if ( imgData.height ) { 417 | gm( destinationDir + osSep + info.name ) 418 | .type( 'Optimize' ) 419 | .thumb( imgData.width, imgData.height, thumbSubDir + osSep + thumbName, 90, 420 | function (err) { 421 | if ( err ) { 422 | console.log( 'optimize: ', err ); 423 | // throw err; 424 | } 425 | info.thumbnails.push( thumbSubUrl + thumbName ); 426 | var key = util.format( '%s_%s', imgData.width, imgData.height ); 427 | var path = util.format( '%s%s', thumbSubUrl, thumbName ); 428 | info.thumbnailObj[ key ] = thumbSubUrl + thumbName; 429 | if ( --totalSizes === 0 ) { 430 | cb( info ); 431 | } 432 | } ); 433 | } else { 434 | gm( destinationDir + osSep + info.name ) 435 | .resize( imgData.width ) 436 | .quality( self.settings.quality ) 437 | .write( thumbSubDir + osSep + thumbName, function (err) { 438 | if ( err ) { 439 | console.log( 'resize: ', err ); 440 | // throw err; 441 | } 442 | info.thumbnails.push( thumbSubUrl + thumbName ); 443 | var key = util.format( '%s', imgData.width ); 444 | var path = util.format( '%s%s', thumbSubUrl, thumbName ); 445 | info.thumbnailObj[ key ] = thumbSubUrl + thumbName; 446 | if ( --totalSizes === 0 ) { 447 | cb( info ); 448 | } 449 | } ); 450 | } 451 | } ); 452 | } else { 453 | cb( info ); 454 | } 455 | } else { 456 | cb( info ); 457 | } 458 | }, 459 | logging: function () { 460 | if ( this.settings.debug ) { 461 | for ( var arg in arguments ) { 462 | console.log( util.inspect( arguments[ arg ], { colors: true, depth: null } ) ); 463 | } 464 | } 465 | }, 466 | uploadInfo: function (finfo) { 467 | var self = this; 468 | self.logging( " File: " + finfo.originalName ); 469 | self.logging( ' Upload: ' + (finfo.success ? "Completed" : "Failed") ); 470 | if ( finfo.success ) { 471 | self.logging( " Destination Directory: " + finfo.destinationDir ); 472 | self.logging( " Destination name: " + finfo.name ); 473 | } else { 474 | self.logging( " Error: " + finfo.error ); 475 | } 476 | } 477 | }; 478 | }; 479 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-uploader", 3 | "description": "File uploads with NodeJS", 4 | "version": "0.1.4", 5 | "main": "./index.js", 6 | "scripts": { 7 | "test": "node tests/test.js" 8 | }, 9 | "directories": { 10 | "lib": "lib", 11 | "media": "examples", 12 | "tests": "tests" 13 | }, 14 | "homepage": "https://github.com/biggora/express-uploader/", 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/biggora/express-uploader.git" 18 | }, 19 | "author": "Aleksej Gordejev (https://github.com/biggora/)", 20 | "keywords": [ 21 | "trinte", 22 | "upload", 23 | "uploading", 24 | "uploader", 25 | "express", 26 | "middleware", 27 | "connect" 28 | ], 29 | "license": "MIT", 30 | "engines": { 31 | "node": ">=4.3.0", 32 | "npm": ">=2.15.11" 33 | }, 34 | "dependencies": { 35 | "gm": "^1.23.0", 36 | "uuid": "^3.1.0" 37 | }, 38 | "devDependencies": { 39 | "express": ">=3.0", 40 | "connect-multiparty": ">=1.0.3", 41 | "morgan": "^1.8.2", 42 | "errorhandler": "^1.5.0", 43 | "compression": "^1.7.0", 44 | "express-session": "^1.15.4", 45 | "body-parser": "^1.17.2", 46 | "method-override": "^1.0.0", 47 | "cookie-parser": "^1.4.3", 48 | "gm": "^1.23.0" 49 | } 50 | } -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0xdC30512ad161D6D26959cC706916Ae515d97fB26' 6 | - '0x9913f84AA726d98B168D86399cf7e9BfD2a3BBd3' 7 | - '0x0b3619350E7C7A37B83F10F72000c63293177F1F' 8 | quorum: 1 9 | -------------------------------------------------------------------------------- /tests/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2013 Alexey Gordeyev . 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | var express = require('express'); 26 | var Uploader = require('../lib/express-uploader'); 27 | --------------------------------------------------------------------------------