├── package.sh ├── README.md ├── package.json ├── .gitignore ├── LICENSE └── index.js /package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm install 4 | 5 | zip -r package.zip index.js node_modules LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lambda-thumbnailer 2 | AWS Lambda thumbnailer with support for GIFs and PDFs (first page/frame) 3 | 4 | Saves the thumbnails to SOURCE_BUCKET/thumbnails/SOURCE_KEY.png 5 | 6 | Can easily be modified to save thumbnails as a different format, bucket, etc 7 | 8 | Adds some metadata, and enables public-read for the thumbnail. 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-thumbnailer", 3 | "version": "0.1.0", 4 | "description": "A simple AWS Lambda script to thumbnail images and PDFs", 5 | "author": "Franklyn Tackitt ", 6 | "main": "index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/DisruptiveLabs/lambda-thumbnailer" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/DisruptiveLabs/lambda-thumbnailer/issues" 13 | }, 14 | "keywords": [ 15 | "AWS", 16 | "lambda", 17 | "thumbnail" 18 | ], 19 | "dependencies": { 20 | "async": "~0.8.0", 21 | "gm": "*", 22 | "mktemp": "*" 23 | }, 24 | "license": "MIT" 25 | } -------------------------------------------------------------------------------- /.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 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | *.zip 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Disruptive Labs 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 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var async = require("async"); 2 | var AWS = require("aws-sdk"); 3 | var gm = require("gm").subClass({imageMagick: true}); 4 | var fs = require("fs"); 5 | var mktemp = require("mktemp"); 6 | 7 | var THUMB_KEY_PREFIX = "thumbnails/", 8 | THUMB_WIDTH = 150, 9 | THUMB_HEIGHT = 150, 10 | ALLOWED_FILETYPES = ['png', 'jpg', 'jpeg', 'bmp', 'tiff', 'pdf', 'gif']; 11 | 12 | var utils = { 13 | decodeKey: function(key) { 14 | return decodeURIComponent(key).replace(/\+/g, ' '); 15 | } 16 | }; 17 | 18 | 19 | var s3 = new AWS.S3(); 20 | 21 | 22 | exports.handler = function(event, context) { 23 | var bucket = event.Records[0].s3.bucket.name, 24 | srcKey = utils.decodeKey(event.Records[0].s3.object.key), 25 | dstKey = THUMB_KEY_PREFIX + srcKey.replace(/\.\w+$/, ".png"), 26 | fileType = srcKey.match(/\.\w+$/); 27 | 28 | if(srcKey.indexOf(THUMB_KEY_PREFIX) === 0) { 29 | return; 30 | } 31 | 32 | if (fileType === null) { 33 | console.error("Invalid filetype found for key: " + srcKey); 34 | return; 35 | } 36 | 37 | fileType = fileType[0].substr(1); 38 | 39 | if (ALLOWED_FILETYPES.indexOf(fileType) === -1) { 40 | console.error("Filetype " + fileType + " not valid for thumbnail, exiting"); 41 | return; 42 | } 43 | 44 | async.waterfall([ 45 | 46 | function download(next) { 47 | //Download the image from S3 48 | s3.getObject({ 49 | Bucket: bucket, 50 | Key: srcKey 51 | }, next); 52 | }, 53 | 54 | function createThumbnail(response, next) { 55 | var temp_file, image; 56 | 57 | if(fileType === "pdf") { 58 | temp_file = mktemp.createFileSync("/tmp/XXXXXXXXXX.pdf") 59 | fs.writeFileSync(temp_file, response.Body); 60 | image = gm(temp_file + "[0]"); 61 | } else if (fileType === 'gif') { 62 | temp_file = mktemp.createFileSync("/tmp/XXXXXXXXXX.gif") 63 | fs.writeFileSync(temp_file, response.Body); 64 | image = gm(temp_file + "[0]"); 65 | } else { 66 | image = gm(response.Body); 67 | } 68 | 69 | image.size(function(err, size) { 70 | /* 71 | * scalingFactor should be calculated to fit either the width or the height 72 | * within 150x150 optimally, keeping the aspect ratio. Additionally, if the image 73 | * is smaller than 150px in both dimensions, keep the original image size and just 74 | * convert to png for the thumbnail's display 75 | */ 76 | var scalingFactor = Math.min(1, THUMB_WIDTH / size.width, THUMB_HEIGHT / size.height), 77 | width = scalingFactor * size.width, 78 | height = scalingFactor * size.height; 79 | 80 | this.resize(width, height) 81 | .toBuffer("png", function(err, buffer) { 82 | if(temp_file) { 83 | fs.unlinkSync(temp_file); 84 | } 85 | 86 | if (err) { 87 | next(err); 88 | } else { 89 | next(null, response.contentType, buffer); 90 | } 91 | }); 92 | }); 93 | }, 94 | 95 | function uploadThumbnail(contentType, data, next) { 96 | s3.putObject({ 97 | Bucket: bucket, 98 | Key: dstKey, 99 | Body: data, 100 | ContentType: "image/png", 101 | ACL: 'public-read', 102 | Metadata: { 103 | thumbnail: 'TRUE' 104 | } 105 | }, next); 106 | } 107 | 108 | ], 109 | function(err) { 110 | if (err) { 111 | console.error( 112 | "Unable to generate thumbnail for '" + bucket + "/" + srcKey + "'" + 113 | " due to error: " + err 114 | ); 115 | } else { 116 | console.log("Created thumbnail for '" + bucket + "/" + srcKey + "'"); 117 | } 118 | 119 | context.done(); 120 | }); 121 | }; 122 | --------------------------------------------------------------------------------