├── .gitignore ├── index.js ├── package.json ├── LICENSE.md ├── ReactS3Uploader.js ├── s3router.js ├── README.md └── s3upload.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | *.iml 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./ReactS3Uploader'); 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-s3-uploader", 3 | "version": "1.1.12", 4 | "description": "React component that renders a file input and automatically uploads to an S3 bucket", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:odysseyscience/react-s3-uploader.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "upload", 16 | "component", 17 | "s3", 18 | "bucket" 19 | ], 20 | "author": "Sean Adkinson", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/odysseyscience/react-s3-uploader/issues" 24 | }, 25 | "homepage": "https://github.com/odysseyscience/react-s3-uploader", 26 | "dependencies": { 27 | "aws-sdk": "2.x", 28 | "node-uuid": "1.x", 29 | "object-assign": "^2.0.0" 30 | }, 31 | "peerDependencies": { 32 | "express": "4.x", 33 | "react": "*" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) {{{year}}} {{{fullname}}} 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 | -------------------------------------------------------------------------------- /ReactS3Uploader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react'), 4 | S3Upload = require('./s3upload.js'), 5 | objectAssign = require('object-assign'); 6 | 7 | var ReactS3Uploader = React.createClass({ 8 | 9 | propTypes: { 10 | signingUrl: React.PropTypes.string.isRequired, 11 | onProgress: React.PropTypes.func, 12 | onFinish: React.PropTypes.func, 13 | onError: React.PropTypes.func 14 | }, 15 | 16 | getDefaultProps: function() { 17 | return { 18 | onProgress: function(percent, message) { 19 | console.log('Upload progress: ' + percent + '% ' + message); 20 | }, 21 | onFinish: function(signResult) { 22 | console.log("Upload finished: " + signResult.publicUrl) 23 | }, 24 | onError: function(message) { 25 | console.log("Upload error: " + message); 26 | } 27 | }; 28 | }, 29 | 30 | uploadFile: function() { 31 | new S3Upload({ 32 | fileElement: this.getDOMNode(), 33 | signingUrl: this.props.signingUrl, 34 | onProgress: this.props.onProgress, 35 | onFinishS3Put: this.props.onFinish, 36 | onError: this.props.onError 37 | }); 38 | }, 39 | 40 | render: function() { 41 | return React.DOM.input(objectAssign({}, this.props, {type: 'file', onChange: this.uploadFile})); 42 | } 43 | 44 | }); 45 | 46 | 47 | module.exports = ReactS3Uploader; 48 | -------------------------------------------------------------------------------- /s3router.js: -------------------------------------------------------------------------------- 1 | 2 | var uuid = require('node-uuid'), 3 | aws = require('aws-sdk'), 4 | express = require('express'); 5 | 6 | 7 | function checkTrailingSlash(path) { 8 | if (path && path[path.length-1] != '/') { 9 | path += '/'; 10 | } 11 | return path; 12 | } 13 | 14 | function S3Router(options) { 15 | 16 | var S3_BUCKET = options.bucket, 17 | getFileKeyDir = options.getFileKeyDir || function() { return ""; }; 18 | 19 | if (!S3_BUCKET) { 20 | throw new Error("S3_BUCKET is required."); 21 | } 22 | if (options.region) { 23 | aws.config.update({region: options.region}); 24 | } 25 | 26 | var router = express.Router(); 27 | 28 | /** 29 | * Redirects image requests with a temporary signed URL, giving access 30 | * to GET an upload. 31 | */ 32 | function tempRedirect(req, res) { 33 | var params = { 34 | Bucket: S3_BUCKET, 35 | Key: checkTrailingSlash(getFileKeyDir(req)) + req.params[0] 36 | }; 37 | var s3 = new aws.S3(); 38 | s3.getSignedUrl('getObject', params, function(err, url) { 39 | res.redirect(url); 40 | }); 41 | }; 42 | 43 | /** 44 | * Image specific route. 45 | */ 46 | router.get(/\/img\/(.*)/, function(req, res) { 47 | return tempRedirect(req, res); 48 | }); 49 | 50 | /** 51 | * Other file type(s) route. 52 | */ 53 | router.get(/\/uploads\/(.*)/, function(req, res) { 54 | return tempRedirect(req, res); 55 | }); 56 | 57 | /** 58 | * Returns an object with `signedUrl` and `publicUrl` properties that 59 | * give temporary access to PUT an object in an S3 bucket. 60 | */ 61 | router.get('/sign', function(req, res) { 62 | var filename = uuid.v4() + "_" + req.query.objectName; 63 | var mimeType = req.query.contentType; 64 | var fileKey = checkTrailingSlash(getFileKeyDir(req)) + filename; 65 | 66 | var s3 = new aws.S3(); 67 | var params = { 68 | Bucket: S3_BUCKET, 69 | Key: fileKey, 70 | Expires: 60, 71 | ContentType: mimeType, 72 | ACL: options.ACL || 'private' 73 | }; 74 | s3.getSignedUrl('putObject', params, function(err, data) { 75 | if (err) { 76 | console.log(err); 77 | return res.send(500, "Cannot create S3 signed URL"); 78 | } 79 | res.json({ 80 | signedUrl: data, 81 | publicUrl: '/s3/uploads/' + filename, 82 | filename: filename 83 | }); 84 | }); 85 | }); 86 | 87 | return router; 88 | } 89 | 90 | module.exports = S3Router; 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-s3-uploader 2 | =========================== 3 | 4 | Provides a `React` component that automatically uploads to an S3 Bucket. 5 | 6 | Install 7 | ----------- 8 | 9 | $ npm install react-s3-uploader 10 | 11 | From Browser 12 | ------------ 13 | 14 | var ReactS3Uploader = require('react-s3-uploader'); 15 | 16 | ... 17 | 18 | 24 | 25 | The above example shows all supported `props`. 26 | 27 | This expects a request to `/s3/sign` to return JSON with a `signedUrl` property that can be used 28 | to PUT the file in S3. 29 | 30 | The resulting DOM is essentially: 31 | 32 | 33 | 34 | When a file is chosen, it will immediately be uploaded to S3. You can listen for progress (and 35 | create a status bar, for example) by providing an `onProgress` function to the component. 36 | 37 | Server-Side 38 | ----------- 39 | ### Bundled router 40 | You can use the Express router that is bundled with this module to answer calls to `/s3/sign` 41 | 42 | app.use('/s3', require('react-s3-uploader/s3router')({ 43 | bucket: "MyS3Bucket", 44 | region: 'us-east-1', //optional 45 | ACL: 'private' // this is default 46 | })); 47 | 48 | This also provides another endpoint: `GET /s3/img/(.*)` and `GET /s3/uploads/(.*)`. This will create a temporary URL 49 | that provides access to the uploaded file (which are uploaded privately at the moment). The 50 | request is then redirected to the URL, so that the image is served to the client. 51 | 52 | #### Access/Secret Keys 53 | 54 | The `aws-sdk` must be configured with your account's Access Key and Secret Access Key. [There are a number of ways to provide these](http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/node-configuring.html), but setting up environment variables is the quickest. You just have to configure environment variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`, and AWS automatically picks them up. 55 | 56 | ### Other Types of Servers 57 | 58 | ##### Boto for Python, in a Django project 59 | 60 | import boto 61 | import mimetypes 62 | import json 63 | 64 | ... 65 | conn = boto.connect_s3('AWS_KEY', 'AWS_SECRET') 66 | 67 | def sign_s3_upload(request): 68 | object_name = request.GET['objectName'] 69 | content_type = mimetypes.guess_type(object_name)[0] 70 | 71 | signed_url = conn.generate_url( 72 | 300, 73 | "PUT", 74 | 'BUCKET_NAME', 75 | 'FOLDER_NAME' + object_name, 76 | headers = {'Content-Type': content_type, 'x-amz-acl':'public-read'}) 77 | 78 | return HttpResponse(json.dumps({'signedUrl': signed_url})) 79 | 80 | #### Ruby on Rails, assuming FOG usage 81 | 82 | # Usual fog config, set as an initializer 83 | FOG = Fog::Storage.new({ 84 | :provider => 'AWS', 85 | :aws_access_key_id => ENV['AWS_ACCESS_KEY_ID'], 86 | :aws_secret_access_key => ENV['AWS_SECRET_ACCESS_KEY'] 87 | }) 88 | 89 | # In the controller 90 | options = {path_style: true} 91 | headers = {"Content-Type" => params[:contentType], "x-amz-acl" => "public-read"} 92 | 93 | @url = FOG.put_object_url(ENV['S3_BUCKET_NAME'], "user_uploads/#{params[:objectName]}", 15.minutes.from_now.to_time.to_i, headers, options) 94 | 95 | respond_to do |format| 96 | format.json { render json: {signedUrl: @url} } 97 | end 98 | 99 | 100 | ##### Other Servers 101 | 102 | If you do some work on another server, and would love to contribute documentation, please send us a PR! 103 | -------------------------------------------------------------------------------- /s3upload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Taken, CommonJS-ified, and heavily modified from: 3 | * https://github.com/flyingsparx/NodeDirectUploader 4 | */ 5 | 6 | S3Upload.prototype.signingUrl = '/sign-s3'; 7 | S3Upload.prototype.fileElement = null; 8 | 9 | S3Upload.prototype.onFinishS3Put = function(signResult) { 10 | return console.log('base.onFinishS3Put()', signResult.publicUrl); 11 | }; 12 | 13 | S3Upload.prototype.onProgress = function(percent, status) { 14 | return console.log('base.onProgress()', percent, status); 15 | }; 16 | 17 | S3Upload.prototype.onError = function(status) { 18 | return console.log('base.onError()', status); 19 | }; 20 | 21 | function S3Upload(options) { 22 | if (options == null) { 23 | options = {}; 24 | } 25 | for (option in options) { 26 | if (options.hasOwnProperty(option)) { 27 | this[option] = options[option]; 28 | } 29 | } 30 | this.handleFileSelect(this.fileElement); 31 | } 32 | 33 | S3Upload.prototype.handleFileSelect = function(fileElement) { 34 | this.onProgress(0, 'Upload started.'); 35 | var files = fileElement.files; 36 | var result = []; 37 | for (var i=0; i < files.length; i++) { 38 | var f = files[i]; 39 | result.push(this.uploadFile(f)); 40 | } 41 | return result; 42 | }; 43 | 44 | S3Upload.prototype.createCORSRequest = function(method, url) { 45 | var xhr = new XMLHttpRequest(); 46 | 47 | if (xhr.withCredentials != null) { 48 | xhr.open(method, url, true); 49 | } 50 | else if (typeof XDomainRequest !== "undefined") { 51 | xhr = new XDomainRequest(); 52 | xhr.open(method, url); 53 | } 54 | else { 55 | xhr = null; 56 | } 57 | return xhr; 58 | }; 59 | 60 | S3Upload.prototype.executeOnSignedUrl = function(file, callback) { 61 | var xhr = new XMLHttpRequest(); 62 | var fileName = file.name.replace(/\s+/g, "_"); 63 | xhr.open('GET', this.signingUrl + '?objectName=' + fileName + '&contentType=' + file.type, true); 64 | xhr.overrideMimeType && xhr.overrideMimeType('text/plain; charset=x-user-defined'); 65 | xhr.onreadystatechange = function() { 66 | if (xhr.readyState === 4 && xhr.status === 200) { 67 | var result; 68 | try { 69 | result = JSON.parse(xhr.responseText); 70 | } catch (error) { 71 | this.onError('Invalid signing server response JSON: ' + xhr.responseText); 72 | return false; 73 | } 74 | return callback(result); 75 | } else if (xhr.readyState === 4 && xhr.status !== 200) { 76 | return this.onError('Could not contact request signing server. Status = ' + xhr.status); 77 | } 78 | }.bind(this); 79 | return xhr.send(); 80 | }; 81 | 82 | S3Upload.prototype.uploadToS3 = function(file, signResult) { 83 | var xhr = this.createCORSRequest('PUT', signResult.signedUrl); 84 | if (!xhr) { 85 | this.onError('CORS not supported'); 86 | } else { 87 | xhr.onload = function() { 88 | if (xhr.status === 200) { 89 | this.onProgress(100, 'Upload completed.'); 90 | return this.onFinishS3Put(signResult); 91 | } else { 92 | return this.onError('Upload error: ' + xhr.status); 93 | } 94 | }.bind(this); 95 | xhr.onerror = function() { 96 | return this.onError('XHR error.'); 97 | }.bind(this); 98 | xhr.upload.onprogress = function(e) { 99 | var percentLoaded; 100 | if (e.lengthComputable) { 101 | percentLoaded = Math.round((e.loaded / e.total) * 100); 102 | return this.onProgress(percentLoaded, percentLoaded === 100 ? 'Finalizing.' : 'Uploading.'); 103 | } 104 | }.bind(this); 105 | } 106 | xhr.setRequestHeader('Content-Type', file.type); 107 | xhr.setRequestHeader('x-amz-acl', 'public-read'); 108 | return xhr.send(file); 109 | }; 110 | 111 | S3Upload.prototype.uploadFile = function(file) { 112 | return this.executeOnSignedUrl(file, function(signResult) { 113 | return this.uploadToS3(file, signResult); 114 | }.bind(this)); 115 | }; 116 | 117 | 118 | module.exports = S3Upload; 119 | --------------------------------------------------------------------------------