├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.idea 2 | # Logs 3 | logs 4 | *.log 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # Compiled binary addons (http://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directory 24 | # Commenting this out is preferred by some people, see 25 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 26 | node_modules 27 | 28 | # Users Environment Variables 29 | .lock-wscript 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # changelog 2 | 3 | ## 0.0.6 4 | 5 | - Fix bug. Define 'self' in 'writeImage_'. 6 | 7 | ## 0.0.7 8 | 9 | - Fix bug. Remove 'self' in 'writeImage_' and websockets messaging in this function as it's not needed. 10 | 11 | ## 0.0.8 12 | 13 | - Allow resize of only one side (width or height), and the other side to be sized according to aspect ratio. Also, remove logging. 14 | 15 | ## 0.0.9 16 | 17 | - Allow max file size on upload function. 18 | 19 | ## 0.1.0 20 | 21 | - Refactor WebSocket functionality and add logging option. 22 | 23 | ## 0.1.13 24 | 25 | - Fix logging. 26 | 27 | ## 0.1.16 28 | 29 | - Fix file validation. Catch undefined header. 30 | 31 | ## 0.1.17 32 | 33 | - Addition to 0.1.16 - add mimetype catch. 34 | 35 | ## 0.1.18 36 | 37 | - Fix undefined parameter. 38 | 39 | ## 0.1.19 40 | 41 | - Fix issue #1. Now a developer can extend the S3 object with valid params. 42 | 43 | ## 1.0.0 44 | 45 | - Made setup easier by replacing required `server` option with optional `websocketServer` option. 46 | - Renamed options to be more intuitive. Replaced `port` with `websocketServerPort`. 47 | - Removed boolean `websocket` option and replaced the check to simply read the `websocketServer` option for existence. 48 | 49 | ## 1.0.1 50 | 51 | - Add `getSize` method. 52 | 53 | ## 1.0.2 54 | 55 | - Updated readme. 56 | 57 | ## 1.0.3 58 | 59 | - Add `delete` method. 60 | 61 | ## 1.0.4 62 | 63 | - Badges. 64 | 65 | ## 1.0.5 66 | 67 | - Fix documentation typo. 68 | 69 | ## 1.0.7 70 | 71 | - Provide another parameter in `Uploader.upload` `errorCallback` to provide the error stack trace. Fixes #2. 72 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | > Copyright (c) 2014-2015 Adam Henson 2 | > 3 | > Permission is hereby granted, free of charge, to any person obtaining 4 | > a copy of this software and associated documentation files (the 5 | > "Software"), to deal in the Software without restriction, including 6 | > without limitation the rights to use, copy, modify, merge, publish, 7 | > distribute, sublicense, and/or sell copies of the Software, and to 8 | > permit persons to whom the Software is furnished to do so, subject to 9 | > the following conditions: 10 | > 11 | > The above copyright notice and this permission notice shall be 12 | > included in all copies or substantial portions of the Software. 13 | > 14 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | > LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | > OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | > WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #s3-image-uploader 2 | 3 | [![npm](https://img.shields.io/npm/v/s3-image-uploader.svg)]() 4 | [![npm](https://img.shields.io/npm/dm/s3-image-uploader.svg)]() 5 | [![npm](https://img.shields.io/npm/dt/s3-image-uploader.svg)]() 6 | 7 | > A Node.js module for resizing, and uploading files to Amazon S3 with capability to track progress using websockets. 8 | > 9 | > This module was created to use with little setup and customization as it's simply a wrapper of [AWS SDK](http://aws.amazon.com/sdk-for-node-js/) and [gm](https://github.com/aheckmann/gm). This module also utilizes [Websockets](https://github.com/einaros/ws), which can be optionally enabled to allow the server to send the client messages such as file upload completion and upload progress. 10 | 11 | ## Installation 12 | 13 | Install package with NPM and add it to your dependencies. 14 | 15 | ``` 16 | $ npm install s3-image-uploader --save 17 | ``` 18 | 19 | ## Dependencies 20 | 21 | When you npm install this module - the module dependencies are added ([s3](https://github.com/andrewrk/node-s3-client), [gm](https://github.com/aheckmann/gm), [ws](https://www.npmjs.org/package/ws)), however you'll need to make sure [GraphicsMagick](http://www.graphicsmagick.org/) is installed on your server. GraphicsMagick is the image manipulation library this module uses. 22 | 23 | Also, you'll need to pay attention to how you're server handles timeouts. 24 | 25 | I used the following code in my Express application to make sure the post didn't timeout: 26 | 27 | ```javascript 28 | app.post('/post-image', function(req, res, next){ 29 | 30 | res.connection.setTimeout(0); // this could take a while 31 | // code to execute post here 32 | 33 | }); 34 | ``` 35 | 36 | ## Usage 37 | 38 | Below is the basic configuration, but you can see [full example code here](https://github.com/adamhenson/example-s3-image-uploader) 39 | 40 | ### Server Side (Node) 41 | 42 | Include the module. 43 | 44 | ```javascript 45 | var Uploader = require('s3-image-uploader'); 46 | ``` 47 | 48 | #### Instantiation 49 | Instantiate the uploader with options. Note that if we didn't want to use websockets functionality - we would add to our options ```websockets : false```. 50 | 51 | Also, note that we're using properties of the user [environment](http://nodejs.org/api/process.html#process_process_env), but these could be variables or hard coded if preferred (not ideal for security). 52 | 53 | ```javascript 54 | var uploader = new Uploader({ 55 | aws : { 56 | key : process.env.NODE_AWS_KEY, 57 | secret : process.env.NODE_AWS_SECRET 58 | }, 59 | websocketServer : server, 60 | websocketServerPort : 3004, 61 | }); 62 | ``` 63 | 64 | #### Resize 65 | 66 | Width and height options denote the maximum size for the dimension (will be exact if the other dimension is set to 'auto'... but upsizing will not happen). If not defined or set to 'auto' - the dimension will be resized based on aspect ratio of the other. Aspect ratio is always maintained. If ```square : true``` is set and width/height are equal, the smaller dimension will be sized down and the larger will be trimmed off outside of the center. 67 | 68 | ```fileId``` is important for the websockets functionality. It's referenced in messages sent to the client about the status. Therefore you may want to use this same identifier as a DOM selector in your client side code (maybe a data attribute) to target visual representations of the messages. 69 | 70 | ```javascript 71 | uploader.resize({ 72 | fileId : 'someUniqueIdentifier', 73 | width : 600, 74 | height : 'auto', 75 | source : './public/tmp/myoldimage.jpg', 76 | destination : './public/uploads/mynewimage.jpg' 77 | }, function(destination){ 78 | console.error('resize success - new image here: ', destination); 79 | // execute success code 80 | }, function(errMsg){ 81 | console.error('unable to resize: ', errMsg); 82 | // execute error code 83 | }); 84 | ``` 85 | 86 | #### Validate File Type 87 | 88 | This validates the content type referenced in the header of the file. 89 | 90 | ```fileId``` is again referenced in messages sent to the client about the status. 91 | 92 | ```javascript 93 | if(uploader.validateType(file, fileId, ['image/jpeg', 'image/gif', 'image/png'])) { 94 | console.log('validation passed!'); 95 | // execute success code 96 | } 97 | ``` 98 | 99 | #### Get Exif Data 100 | 101 | Get the exif data object. 102 | 103 | ```javascript 104 | uploader.getExifData(filePath, function(data){ 105 | 106 | // normally I'd do something with this... like store it in a database 107 | console.log('exif data', data); 108 | 109 | }); 110 | ``` 111 | 112 | #### Get Image Size 113 | 114 | Get dimension object from image. 115 | The below code will log something like this: `{ width: 1200, height: 900 }` 116 | This method uses the GraphicsMagick `size` method. Find [more documentation here](http://aheckmann.github.io/gm/docs.html). 117 | 118 | ```javascript 119 | uploader.getSize(filePath, function(data){ 120 | 121 | console.log('image size data', data); 122 | 123 | }); 124 | ``` 125 | 126 | #### Upload 127 | 128 | Upload the file to s3. 129 | 130 | ```fileId``` is again referenced in messages sent to the client about the status. 131 | 132 | ```javascript 133 | uploader.upload({ 134 | fileId : 'someUniqueIdentifier', 135 | bucket : 'somebucket', 136 | source : './public/tmp/myoldimage.jpg', 137 | name : 'mynewimage.jpg' 138 | }, 139 | function(data){ // success 140 | console.log('upload success:', data); 141 | // execute success code 142 | }, 143 | function(errMsg, errObject){ //error 144 | console.error('unable to upload: ' + errMsg + ':', errObject); 145 | // execute error code 146 | }); 147 | ``` 148 | 149 | #### Delete 150 | 151 | Delete an array of files from AWS (array can include only one file if desired). 152 | 153 | ```javascript 154 | uploader.delete('somebucket', ['cat.jpg', 'dog.png', 'turtle.gif'], function(data){ 155 | console.log('yay!', data); 156 | }, function(err){ 157 | console.log('fail!', err); 158 | }); 159 | ``` 160 | 161 | ## Options 162 | 163 | ### new Uploader 164 | * @param {object} options - Configuration object. Required. 165 | * {object} options.aws - aws object. Required. 166 | * {string} options.aws.key - aws key string. Required. 167 | * {string} options.aws.secret - aws secret string. Required. 168 | * {object} options.websocketServer - WebSocket server object. Optional. 169 | * {number} options.websocketServerPort - WebSocket server port. Optional. 170 | * {object} options.s3Params - object that can extend the S3 parameters listed here http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html. Optional. Example: `s3Params : { 'CacheControl' : 'max-age=3600'}` 171 | 172 | ### resize 173 | * @param {object} options - Configuration object. Required. 174 | * {string} options.fileId - Used to uniquely identify file. Required. 175 | * {number || 'auto'} options.width - Maximum width allowed for resized image. Otherwise if not defined or set to 'auto' - width will be resized based on aspect ratio of height. Optional. Default is 'auto'. 176 | * {number || 'auto'} options.height - Maximum height allowed for resized image. Otherwise if not defined or set to 'auto' - height will be resized based on aspect ratio of width. Optional. Default is 'auto'. 177 | * {string} options.source - Path to the image to be resized. Required. 178 | * {string} options.destination - Path to new image after resize. Required. 179 | * {number} options.quality - Quality for resized image (1-100... 100 is best). Optional. Default is 100. 180 | * {boolean} options.square - boolean flag set to true if the image needs to be square. Optional. Default is false. 181 | * {boolean} options.noProfile - boolean flag set to true if exif data should be removed (minimizing file size). Optional. Default is true. 182 | * {number || boolean} options.maxFileSize - can be a number or boolean false. The number represents file size in MegaBytes. Optional. Default is false. 183 | * @param {function} successCallback - Callback function. Receives one argument - {string} path to resized file. Required. 184 | * @param {function} errorCallback - Callback function. Receives one argument - {string} error message. Required. 185 | 186 | ### validateType 187 | * @param {object} file - Post object. Required. 188 | * @param {string} id - Used to uniquely identify file. Required. 189 | * @param {array} types - Array of string file content types (example: ['image/jpeg', 'image/gif', 'image/png']). Required. 190 | 191 | ### getExifData 192 | * @param {string} source - Path of image. Required. 193 | * @param {function} callback - Callback that receives argument of false or data object. Required. 194 | 195 | ### getSize 196 | * @param {string} source - Path of image. Required. 197 | * @param {function} callback - Callback that receives argument of false or data object. Required. The received data object will be in a format similar to this: `{ width: 1200, height: 900 }` 198 | 199 | ### upload 200 | * @param {object} options - Configuration object. Required. 201 | * {string} options.fileId - Used to uniquely identify file. Required. 202 | * {string} options.bucket - S3 bucket. Required. 203 | * {string} options.source - Path to the image to be uploaded. Required. 204 | * {string} options.name - Name to be used for new file uploaded to S3. Required. 205 | * {number || boolean} options.maxFileSize - can be a number or boolean false. The number represents file size in MegaBytes. Optional. Default is false. 206 | * @param {function} successCallback - Callback function. Receives one argument - {object} status object. Required. 207 | * @param {function} errorCallback - Callback function. Receives two arguments - {string} error message. {object} error stack trace. Required. 208 | 209 | ### delete 210 | * @param {string} bucket - AWS bucket name. Required. 211 | * @param {array} fileNames - Array of string filenames (example: ['cat.jpg', 'dog.png', 'turtle.gif']). Required. 212 | * @param {function} successCallback - Callback that receives data object. Required. 213 | * @param {function} errorCallback - Callback that receives error object. Optional. 214 | 215 | ## On the Client Side 216 | 217 | Please see a [full example here](https://github.com/adamhenson/example-s3-image-uploader/blob/master/public/js/uploader.js). 218 | 219 | The most important thing to consider here is that we're receiving ```fileId``` from the server as ```id``` to uniquely identify the upload. We receive message objects via websockets. Below are examples of different messages we might receive on the client. 220 | 221 | > Error message 222 | 223 | ```javascript 224 | { 225 | type : 'error', 226 | id : 'someUniqueIdentifier', 227 | message : 'There was a problem uploading this file.' 228 | } 229 | ``` 230 | 231 | > Upload progress message 232 | 233 | ```javascript 234 | { 235 | type : 'progress', 236 | id : 'someUniqueIdentifier', 237 | progressAmount : 5276653, // represents bytes 238 | progressTotal : 6276653 // represents bytes 239 | } 240 | ``` 241 | 242 | > Upload success message 243 | 244 | ```javascript 245 | { 246 | type : 'result', 247 | id : 'someUniqueIdentifier', 248 | path : '/mybucket/myimage.jpg' 249 | } 250 | ``` 251 | 252 | > Resize success message 253 | 254 | ```javascript 255 | { 256 | type : 'resize', 257 | id : 'someUniqueIdentifier', 258 | size : '100x100' 259 | } 260 | ``` 261 | 262 | So, a simple implementation of this might look something like this. 263 | 264 | Make the websocket connection. 265 | 266 | ```javascript 267 | var host = window.document.location.host.replace(/:.*/, ''); 268 | var ws = new WebSocket('ws://' + host + ':8080'); 269 | ``` 270 | 271 | Handle messages from the server about the progress of our upload/s and resizing. 272 | 273 | ```javascript 274 | ws.onmessage = function(event){ 275 | var message = JSON.parse(event.data); 276 | if(typeof message.type !== 'undefined') { 277 | if(message.type === 'progress') // execute code for progress 278 | else if(message.type === 'result') // execute code for result 279 | else if(message.type === 'resize') // execute code for resize status 280 | else if(message.type === 'error') // execute code for error messages 281 | } 282 | }; 283 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // ws - A Websocket library: https://www.npmjs.org/package/ws 4 | var WebSocketServer = require('ws').Server; 5 | // Let's not re-invent the wheel. A high level S3 uploader: https://www.npmjs.org/package/s3 6 | var s3 = require('s3'); 7 | // gm (GraphicsMagick) - For image manipulation: https://github.com/aheckmann/gm 8 | var gm = require('gm'); 9 | // other helper requires below 10 | var extend = require('extend'); 11 | 12 | /** 13 | * Websocket constructor. 14 | * @param {object} options - Object for options. Required. 15 | */ 16 | function Websocket(options){ 17 | // the default ws connection object 18 | this.ws = { 19 | readyState : 0 20 | }; 21 | // more defaults and options 22 | this.wss = false; 23 | this.server = options.websocketServer; 24 | this.port = (options.port) 25 | ? options.port 26 | : false; 27 | this.log = (options.log) 28 | ? options.log 29 | : false; 30 | }; 31 | 32 | /** 33 | * Start the WebSocket connection. 34 | */ 35 | Websocket.prototype.start = function(){ 36 | 37 | var self = this; 38 | var options = { 39 | 'server' : self.server, 40 | }; 41 | if(self.port) options.port = self.port; 42 | if(!self.wss) self.wss = new WebSocketServer(options); 43 | 44 | // bind to the connection event 45 | self.wss.on('connection', function connection(ws) { 46 | if(self.log) console.log('s3-image-uploader: Websocket: websocket connected'); 47 | ws.on('close', function close() { 48 | if(self.log) console.log('s3-image-uploader: Websocket: websocket disconnected'); 49 | }); 50 | // assign the connection object to the instance 51 | self.ws = ws; 52 | }); 53 | 54 | }; 55 | 56 | /** 57 | * Send message to client 58 | * @param {object || string || boolean} message - Message to send to client. Required. 59 | * @param {function} callback - Callback function. 60 | */ 61 | Websocket.prototype.send = function(message, callback){ 62 | 63 | var self = this; 64 | 65 | // if the connection is open (readyState 1) - send messages 66 | if(self.ws.readyState === 1) { 67 | self.ws.send(message, function sendError(err){ 68 | if(typeof err !== 'undefined') console.log('s3-image-uploader: Websocket.send: ws send error.', err.stack); 69 | if(typeof callback !== 'undefined') callback(); 70 | }); 71 | } else { 72 | if(typeof callback !== 'undefined') callback(); 73 | } 74 | 75 | }; 76 | 77 | /** 78 | * Uploader constructor. 79 | * @param {object} options - Configuration object. Required. 80 | * {object} options.aws - aws object. Required. 81 | * {string} options.aws.key - aws key string. Required. 82 | * {string} options.aws.secret - aws secret string. Required. 83 | * {object} options.websocketServer - WebSocket server object. Optional. 84 | * {number} options.websocketServerPort - WebSocket server port. Optional. 85 | */ 86 | var Uploader = function(options){ 87 | 88 | var self = this; 89 | 90 | if(typeof options.aws.key === 'undefined') throw new Error('s3-image-uploader: Uploader: "aws.key" is not defined.'); 91 | if(typeof options.aws.secret === 'undefined') throw new Error('s3-image-uploader: Uploader: "aws.secret" is not defined.'); 92 | // default 93 | if(typeof options.port === 'undefined') options.port = false; 94 | if(typeof options.websocketServerPort === 'undefined') options.websocketServerPort = false; 95 | if(typeof options.log === 'undefined') options.log = true; 96 | 97 | self.options = options; 98 | // support older versions of this module 99 | if(options.server) self.options.websocketServer = options.server; 100 | if(options.port) self.options.websocketServerPort = options.port; 101 | 102 | // websockets 103 | if(self.options.websocketServer) { 104 | var webSocketOptions = { 105 | 'server' : self.options.websocketServer, 106 | }; 107 | if(self.options.websocketServerPort) webSocketOptions.port = self.options.websocketServerPort; 108 | if(self.options.log) webSocketOptions.log = self.options.log; 109 | self.ws = new Websocket(webSocketOptions); 110 | self.ws.start(); 111 | } 112 | 113 | // create the s3 client 114 | self.client = s3.createClient({ 115 | s3Options: { 116 | accessKeyId: self.options.aws.key, 117 | secretAccessKey: self.options.aws.secret 118 | } 119 | }); 120 | 121 | }; 122 | 123 | /** 124 | * Resize image and add to destination directory. 125 | * @param {object} options - Configuration object. Required. 126 | * @param {function} successCallback - Callback function. Receives one argument - {string} path to resized file. Required. 127 | * @param {function} errorCallback - Callback function. Receives one argument - {string} error message. Required. 128 | * {string} options.fileId - Used to uniquely identify file. Required. 129 | * {number || 'auto'} options.width - Maximum width allowed for resized image. Otherwise if not defined or set to 'auto' - width will be resized based on aspect ratio of height. Optional. Default is 'auto'. 130 | * {number || 'auto'} options.height - Maximum height allowed for resized image. Otherwise if not defined or set to 'auto' - height will be resized based on aspect ratio of width. Optional. Default is 'auto'. 131 | * {string} options.source - Path to the image to be resized. Required. 132 | * {string} options.destination - Path to new image after resize. Required. 133 | * {number} options.quality - Quality for resized image (1-100... 100 is best). Optional. Default is 100. 134 | * {boolean} options.square - boolean flag set to true if the image needs to be square. Optional. Default is false. 135 | * {boolean} options.noProfile - boolean flag set to true if exif data should be removed (minimizing file size). Optional. Default is true. 136 | * {number || boolean} options.maxFileSize - can be a number or boolean false. The number represents file size in MegaBytes. Optional. Default is false. 137 | */ 138 | Uploader.prototype.resize = function(options, successCallback, errorCallback){ 139 | 140 | if(typeof options.fileId === 'undefined') throw new Error('s3-image-uploader: Uploader.resize: "fileId" is not defined.'); 141 | if(typeof options.source === 'undefined') throw new Error('s3-image-uploader: Uploader.resize: "source" is not defined.'); 142 | if(typeof options.destination === 'undefined') throw new Error('s3-image-uploader: Uploader.resize: "destination" is not defined.'); 143 | // defaults 144 | if(typeof options.width === 'undefined') options.width = 'auto'; 145 | if(typeof options.height === 'undefined') options.height = 'auto'; 146 | if(typeof options.quality === 'undefined') options.quality = 100; 147 | if(typeof options.square === 'undefined') options.square = false; 148 | if(typeof options.noProfile === 'undefined') options.noProfile = true; 149 | if(typeof options.maxFileSize === 'undefined') options.maxFileSize = false; // unlimited by default 150 | 151 | var self = this; 152 | 153 | // get image size and execute callback 154 | imageSize_(options.source, function(err, size){ 155 | 156 | var startResize_ = function(){ 157 | 158 | resize_(options, size, function(img, destination){ 159 | 160 | var status = { 161 | type : 'resize', 162 | id : options.fileId, 163 | size : options.width + 'x' + options.height 164 | }; 165 | 166 | if(self.ws){ 167 | self.ws.send(JSON.stringify(status), function(){ 168 | successCallback(destination); 169 | }); 170 | } else { 171 | successCallback(destination); 172 | } 173 | 174 | }, errorCallback); 175 | 176 | }; 177 | 178 | // if maxFileSize is set - get the filesize info and validate 179 | if(options.maxFileSize){ 180 | 181 | validateImageFileSize_(options, startResize_, function(message){ 182 | 183 | var status = { 184 | type : 'error', 185 | id : options.fileId, 186 | message : message 187 | }; 188 | 189 | if(self.ws){ 190 | self.ws.send(JSON.stringify(status), function(){ 191 | errorCallback(message); 192 | }); 193 | } else { 194 | errorCallback(message); 195 | } 196 | 197 | }); 198 | 199 | } else { 200 | 201 | startResize_(); 202 | 203 | } 204 | 205 | }); 206 | 207 | }; 208 | 209 | /** 210 | * Upload to S3. 211 | * @param {object} options - Configuration object. Required. 212 | * @param {function} successCallback - Callback function. Receives one argument - {object} status object. Required. 213 | * @param {function} errorCallback - Callback function. Receives two arguments - (argument 1) {string} error message. 214 | * (argument 2) {object} error stack trace. Required. 215 | * {string} options.fileId - Used to uniquely identify file. Required. 216 | * {string} options.bucket - S3 bucket. Required. 217 | * {string} options.source - Path to the image to be uploaded. Required. 218 | * {string} options.name - Name to be used for new file uploaded to S3. Required. 219 | * {number || boolean} options.maxFileSize - can be a number or boolean false. The number represents file size in MegaBytes. Optional. Default is false. 220 | */ 221 | Uploader.prototype.upload = function(options, successCallback, errorCallback){ 222 | 223 | if(typeof options.fileId === 'undefined') throw new Error('s3-image-uploader: Uploader.upload: "fileId" is not defined.'); 224 | if(typeof options.bucket === 'undefined') throw new Error('s3-image-uploader: Uploader.upload: "bucket" is not defined.'); 225 | if(typeof options.source === 'undefined') throw new Error('s3-image-uploader: Uploader.upload: "source" is not defined.'); 226 | if(typeof options.name === 'undefined') throw new Error('s3-image-uploader: Uploader.upload: "name" is not defined.'); 227 | if(typeof successCallback === 'undefined') throw new Error('s3-image-uploader: Uploader.upload: "successCallback" is not defined.'); 228 | if(typeof errorCallback === 'undefined') throw new Error('s3-image-uploader: Uploader.upload: "errorCallback" is not defined.'); 229 | 230 | var self = this; 231 | 232 | var params = { 233 | localFile: options.source, 234 | s3Params: { 235 | ACL : (typeof options.acl !== 'undefined') ? options.acl : 'public-read', 236 | Bucket: options.bucket, 237 | Key: options.name 238 | } 239 | }; 240 | 241 | // if more s3 params are set, extend the object 242 | if(options.s3Params) params.s3Params = extend(params.s3Params, options.s3Params); 243 | 244 | var initialize_ = function(){ 245 | 246 | var uploader = self.client.uploadFile(params); 247 | 248 | // when there is progress send a message through our websocket connection 249 | uploader.on('progress', function(){ 250 | var status = { 251 | type : 'progress', 252 | id : options.fileId, 253 | progressAmount : uploader.progressAmount, 254 | progressTotal : uploader.progressTotal 255 | }; 256 | if(self.ws) self.ws.send(JSON.stringify(status)); 257 | }); 258 | 259 | // on upload error call error callback 260 | uploader.on('error', function(err){ 261 | var errorMessage = 'There was a problem uploading this file.'; 262 | var status = { 263 | type : 'error', 264 | id : options.fileId, 265 | message : errorMessage 266 | }; 267 | if(self.ws){ 268 | self.ws.send(JSON.stringify(status), function(){ 269 | errorCallback(errorMessage, err); 270 | }); 271 | } else { 272 | errorCallback(errorMessage, err); 273 | } 274 | }); 275 | 276 | // when the upload has finished call the success callback and send a message through our websocket 277 | uploader.on('end', function(obj){ 278 | var status = { 279 | type : 'result', 280 | id : options.fileId, 281 | path : '/' + options.bucket + '/' + options.name 282 | }; 283 | if(self.ws){ 284 | self.ws.send(JSON.stringify(status), function(){ 285 | successCallback(status); 286 | }); 287 | } else { 288 | successCallback(status); 289 | } 290 | }); 291 | 292 | }; 293 | 294 | // if maxFileSize is set - get the filesize info and validate 295 | if(options.maxFileSize){ 296 | 297 | validateImageFileSize_(options, function(){ 298 | 299 | initialize_(); 300 | 301 | }, function(message){ 302 | 303 | var status = { 304 | type : 'error', 305 | id : options.fileId, 306 | message : message 307 | }; 308 | 309 | if(self.ws){ 310 | self.ws.send(JSON.stringify(status), function(){ 311 | successCallback(status); 312 | }); 313 | } else { 314 | successCallback(status); 315 | } 316 | 317 | }); 318 | 319 | } else { 320 | 321 | initialize_(); 322 | 323 | } 324 | 325 | }; 326 | 327 | /** 328 | * Delete an array of files (array can include only one file if desired). 329 | * @param {string} bucket - AWS bucket. Required. 330 | * @param {array} fileNames - Array of string filenames (example: ['cat.jpg', 'dog.png', 'turtle.gif']). Required. 331 | * @param {function} successCallback - Callback that receives data object. Required. 332 | * @param {function} errorCallback - Callback that receives error object. Optional. 333 | */ 334 | Uploader.prototype.delete = function(bucket, fileNames, successCallback, errorCallback){ 335 | 336 | var self = this; 337 | var objects = []; 338 | 339 | fileNames.forEach(function(fileName){ 340 | objects.push({ 'Key' : fileName }); 341 | }); 342 | 343 | var s3Params = { 344 | Bucket : bucket, 345 | Delete : { 346 | Objects : objects 347 | } 348 | }; 349 | 350 | var deleter = self.client.deleteObjects(s3Params); 351 | 352 | deleter.on('error', function(err){ 353 | if (errorCallback) errorCallback(err); 354 | }); 355 | 356 | deleter.on('end', function(obj){ 357 | successCallback(obj); 358 | }); 359 | 360 | }; 361 | 362 | /** 363 | * Get the Exif data of a file. 364 | * @param {string} source - Path of image. Required. 365 | * @param {function} callback - Callback that receives argument of false or data object. Required. 366 | */ 367 | Uploader.prototype.getExifData = function(source, callback){ 368 | 369 | gm(source) 370 | .identify(function (dataErr, data) { 371 | if (!dataErr) { 372 | callback.call(this, data); 373 | } else { // no exif data 374 | callback.call(this, false); 375 | } 376 | }); 377 | 378 | }; 379 | 380 | /** 381 | * Get the size of an image. 382 | * @param {string} source - Path of image. Required. 383 | * @param {function} callback - Callback that receives argument of false or data object. Required. 384 | */ 385 | Uploader.prototype.getSize = function(source, callback){ 386 | 387 | gm(source) 388 | .size(function (dataErr, data) { 389 | if (!dataErr) { 390 | callback.call(this, data); 391 | } else { // no size data 392 | callback.call(this, false); 393 | } 394 | }); 395 | 396 | }; 397 | 398 | /** 399 | * Validate file type and return boolean of validity. 400 | * @param {object} file - Post object. Required. 401 | * @param {string} id - Used to uniquely identify file. Required. 402 | * @param {array} types - Array of string file content types (example: ['image/jpeg', 'image/gif', 'image/png']). Required. 403 | */ 404 | Uploader.prototype.validateType = function(file, id, types){ 405 | 406 | var self = this; 407 | var valid = false; 408 | 409 | var contentType = (!file.headers || !file.headers['content-type']) 410 | ? false 411 | : file.headers['content-type']; 412 | 413 | // Sometimes mimetype. TODO: Research this more 414 | if(!contentType && file.mimetype) contentType = file.mimetype; 415 | 416 | for(var i in types) { 417 | if(types[i] === contentType) { 418 | valid = true; 419 | break; 420 | } 421 | } 422 | 423 | if(!valid) { 424 | var status = { 425 | type : 'error', 426 | id : id, 427 | message : "The file isn't a valid type." 428 | }; 429 | if(self.ws){ 430 | self.ws.send(JSON.stringify(status)); 431 | } 432 | } 433 | 434 | return valid; 435 | 436 | }; 437 | 438 | // Get image file size and call callback function 439 | var validateImageFileSize_ = function(options, successCallback, errorCallback){ 440 | 441 | gm(options.source).filesize(function(err, fileSize){ 442 | 443 | var validate_ = function(size){ 444 | if(options.maxFileSize < size) { 445 | var message = 'File is larger than the allowed size of ' + options.maxFileSize + ' MB.'; 446 | errorCallback.call(this, message); 447 | } else { 448 | successCallback.call(this); 449 | } 450 | }; 451 | 452 | if(err){ 453 | 454 | errorCallback.call(this, err); 455 | 456 | } else { 457 | 458 | if(fileSize.indexOf('M') !== -1) { 459 | var fileSize = fileSize.replace('M', ''); 460 | validate_(fileSize); 461 | } else if(fileSize.indexOf('K') !== -1){ 462 | var fileSize = fileSize.replace('K', ''); 463 | fileSize = parseFloat(fileSize/1024).toFixed(2); 464 | validate_(fileSize); 465 | } else if(fileSize.indexOf('G') !== -1){ 466 | var fileSize = fileSize.replace('G', ''); 467 | fileSize = parseFloat(fileSize*1024).toFixed(2); 468 | validate_(fileSize); 469 | } else { 470 | successCallback.call(this); 471 | } 472 | 473 | } 474 | 475 | }); 476 | 477 | }; 478 | 479 | // Get image size and call callback function 480 | // Callback returns width and height properties 481 | var imageSize_ = function(source, callback){ 482 | 483 | gm(source).size(function(err, value){ 484 | callback.call(this, err, value); 485 | }); 486 | 487 | }; 488 | 489 | // Write image to directory 490 | var writeImage_ = function(img, options, successCallback, errorCallback){ 491 | 492 | img.write(options.destination, function(uploadErr){ 493 | if(!uploadErr) successCallback.call(this, img, options.destination); 494 | else errorCallback.call(this, img, uploadErr); 495 | }); 496 | 497 | }; 498 | 499 | // Resize image - depends on size and options 500 | var resize_ = function(options, size, successCallback, errorCallback){ 501 | 502 | var img = gm(options.source); 503 | 504 | var newWidth = options.width; 505 | var newHeight = options.height; 506 | 507 | // if width or height dimension is unspecified 508 | if(options.width === 'auto' || options.height === 'auto') { 509 | 510 | if(options.width === 'auto') newWidth = null; 511 | if(options.height === 'auto') newHeight = null; 512 | 513 | img.resize(newWidth, newHeight); 514 | 515 | } else if(options.square && options.width === options.height) { // if this needs to be square 516 | 517 | // if we have size info 518 | if(typeof size !== 'undefined') { 519 | // if the width is more than height we make it null so that 520 | // we pass the height to be used by gm, so the outcome 521 | // is an image with a height set to the max 522 | // and the width is the aspect ratio adjusted... but will be bigger, 523 | // and then the gm crop method trims off the width overage. 524 | // the same would occur in vice versa if height is bigger than width. 525 | if(size.width >= size.height) newWidth = null; 526 | else newHeight = null; 527 | } 528 | 529 | img 530 | .resize(newWidth, newHeight) 531 | .gravity('Center') 532 | .crop(options.width, options.height, 0, 0) 533 | .quality(options.quality); 534 | 535 | } else { // else it doesn't need to be square 536 | 537 | // if we have size info 538 | if(typeof size !== 'undefined') { 539 | // if the the image width is larger than height... else height is larger 540 | if(size.width >= size.height){ 541 | // if new height is less than options.height - we're good and we use options.width 542 | // as the max value pass to the gm resize function... 543 | if((size.height / size.width) * options.width <= options.height) newHeight = null; 544 | // ...else we use options.height as the max value to pass into the gm resize 545 | else newWidth = null 546 | } else { 547 | // same logic as if block... just reversed 548 | if((size.width / size.height) * options.height <= options.width) newWidth = null; 549 | else newHeight = null 550 | } 551 | } 552 | 553 | img.resize(newWidth, newHeight); 554 | 555 | } 556 | 557 | img 558 | .quality(options.quality) 559 | .autoOrient(); 560 | 561 | if(options.noProfile) img.noProfile(); 562 | 563 | writeImage_(img, options, successCallback, errorCallback); 564 | 565 | }; 566 | 567 | module.exports = Uploader; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "s3-image-uploader", 3 | "description": "A Node.js module for resizing, and uploading files to Amazon S3 with capability to track progress using websockets.", 4 | "version": "1.0.7", 5 | "author": { 6 | "name": "Adam Henson", 7 | "email": "adamhenson1979@gmail.com" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/adamhenson/s3-image-uploader/issues" 11 | }, 12 | "engines": { 13 | "node": ">= 0.10" 14 | }, 15 | "homepage": "https://github.com/adamhenson/s3-image-uploader/issues", 16 | "keywords": [ 17 | "s3", 18 | "websockets", 19 | "image", 20 | "uploader", 21 | "aws", 22 | "resize" 23 | ], 24 | "license": "MIT", 25 | "main": "index.js", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/adamhenson/s3-image-uploader.git" 29 | }, 30 | "dependencies": { 31 | "ws": "~0.4.32", 32 | "s3": "~4.2.0", 33 | "gm": "~1.16.0", 34 | "extend": "^3.0.0" 35 | } 36 | } 37 | --------------------------------------------------------------------------------