├── README.md ├── config └── default.json ├── .gitignore ├── package.json ├── LICENSE └── create-thumbnail.js /README.md: -------------------------------------------------------------------------------- 1 | # aws-lambda-create-thumbnail 2 | Automatically create thumbnails when image added to an AWS S3 bucket 3 | 4 | Deploy to AWS Lambda by creating a zip file containing the contents of 5 | this repo. 6 | 7 | For callback on success, change config/default.json as appropriate 8 | depending on which environment the zip file is being uploaded for. 9 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "buckets": { 3 | "trinket-dev-user-assets": { 4 | "host" : "https://dev.trinket.io", 5 | "bucket" : "userassets", 6 | "secret" : "supersecret" 7 | }, 8 | "trinket-user-assets": { 9 | "host" : "https://trinket.io", 10 | "bucket" : "userassets", 11 | "secret" : "superlambdasecret" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-lambda-create-thumbnail", 3 | "version": "0.0.0", 4 | "description": "Automatically create thumbnails when image added to an AWS S3 bucket", 5 | "main": "create-thumbnail.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/trinketapp/aws-lambda-create-thumbnail.git" 12 | }, 13 | "keywords": [ 14 | "AWS", 15 | "ImageMagick", 16 | "S3" 17 | ], 18 | "author": "brianpmarks", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/trinketapp/aws-lambda-create-thumbnail/issues" 22 | }, 23 | "homepage": "https://github.com/trinketapp/aws-lambda-create-thumbnail", 24 | "dependencies": { 25 | "async": "^0.9.0", 26 | "aws-sdk": "^2.1.21", 27 | "config": "^1.12.0", 28 | "gm": "^1.17.0", 29 | "q": "^1.2.0", 30 | "request": "^2.55.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Trinket 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /create-thumbnail.js: -------------------------------------------------------------------------------- 1 | // dependencies 2 | var async = require('async') 3 | , AWS = require('aws-sdk') 4 | , gm = require('gm').subClass({ imageMagick: true }) // Enable ImageMagick integration. 5 | , util = require('util') 6 | , request = require('request') 7 | , config = require('config'); 8 | 9 | // constants 10 | var MAX_WIDTH = 100 11 | , MAX_HEIGHT = 100; 12 | 13 | // get reference to S3 client 14 | var s3 = new AWS.S3(); 15 | 16 | exports.handler = function(event, context) { 17 | // Read options from the event. 18 | console.log("Reading options from event:\n", util.inspect(event, {depth: 5})); 19 | var srcBucket = event.Records[0].s3.bucket.name; 20 | var srcKey = event.Records[0].s3.object.key; 21 | var dstBucket = srcBucket + "-thumbnails"; 22 | var dstKey = "thumb-" + srcKey; 23 | 24 | // Sanity check: validate that source and destination are different buckets. 25 | if (srcBucket == dstBucket) { 26 | console.error("Destination bucket must not match source bucket."); 27 | return; 28 | } 29 | 30 | // Infer the image type. 31 | var typeMatch = srcKey.match(/\.([^.]*)$/); 32 | if (!typeMatch) { 33 | console.error('unable to infer image type for key ' + srcKey); 34 | return; 35 | } 36 | 37 | var validImageTypes = ['png', 'jpg', 'jpeg', 'gif']; 38 | var imageType = typeMatch[1]; 39 | if (validImageTypes.indexOf(imageType.toLowerCase()) < 0) { 40 | console.log('skipping non-image ' + srcKey); 41 | return; 42 | } 43 | 44 | // Download the image from S3, transform, and upload to a different S3 bucket. 45 | async.waterfall([ 46 | function download(next) { 47 | // Download the image from S3 into a buffer. 48 | s3.getObject({ 49 | Bucket : srcBucket, 50 | Key : srcKey 51 | }, next); 52 | }, 53 | function tranform(response, next) { 54 | gm(response.Body).size(function(err, size) { 55 | // Infer the scaling factor to avoid stretching the image unnaturally. 56 | var scalingFactor = Math.min( 57 | MAX_WIDTH / size.width, 58 | MAX_HEIGHT / size.height 59 | ); 60 | var width = scalingFactor * size.width; 61 | var height = scalingFactor * size.height; 62 | 63 | // Transform the image buffer in memory. 64 | this.resize(width, height) 65 | .toBuffer(imageType, function(err, buffer) { 66 | if (err) { 67 | next(err); 68 | } else { 69 | next(null, response.ContentType, buffer); 70 | } 71 | }); 72 | }); 73 | }, 74 | function upload(contentType, data, next) { 75 | // Stream the transformed image to a different S3 bucket. 76 | s3.putObject({ 77 | Bucket : dstBucket, 78 | Key : dstKey, 79 | Body : data, 80 | ContentType : contentType 81 | }, next); 82 | }], 83 | function (err) { 84 | if (err) { 85 | console.error( 86 | 'Unable to resize ' + srcBucket + '/' + srcKey + 87 | ' and upload to ' + dstBucket + '/' + dstKey + 88 | ' due to an error: ' + err 89 | ); 90 | context.done(); 91 | } else { 92 | console.log( 93 | 'Successfully resized ' + srcBucket + '/' + srcKey + 94 | ' and uploaded to ' + dstBucket + '/' + dstKey 95 | ); 96 | 97 | // hash-fileId.ext 98 | var fileMatch = srcKey.match(/\-([^.]*)\./); 99 | 100 | if (!fileMatch) { 101 | context.done(); 102 | } else { 103 | var fileId = fileMatch[1]; 104 | 105 | var bucketConfig = config.buckets[srcBucket]; 106 | request.post(bucketConfig.host + '/api/files/' + fileId + '/thumbnail', { 107 | form : { 108 | bucket : bucketConfig.bucket, 109 | secret : bucketConfig.secret 110 | } 111 | }, function(err, response, body) { 112 | err && console.log('could not make request back: ' + err); 113 | context.done(); 114 | }); 115 | } 116 | } 117 | } 118 | ); 119 | }; 120 | --------------------------------------------------------------------------------