├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── changelog.md ├── package.json ├── readme.md ├── s3router.js ├── src ├── DropzoneS3Uploader.js └── index.js └── test ├── .env ├── mocha.opts └── todo.tests.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "loose": "all", 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "founderlab" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | .tmp/ 4 | bower_components/ 5 | node_modules/ 6 | npm-debug.log 7 | /lib -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | .tmp/ 4 | bower_components/ 5 | node_modules/ 6 | npm-debug.log 7 | .babelrc 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 founderlab 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 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | 2 | ## [Unreleased] 3 | 4 | ## [1.0.0] 5 | - PropTypes via the prop-types package (thanks @13colours). 6 | 7 | ## [1.0.0-rc.3] 8 | - Fixed a bug with file url creation (thanks @davidascher). 9 | - Fixed a build error caused by babel picking up the wrong config. 10 | 11 | ## [1.0.0-rc.2] 12 | - The prop `upload` is used to specify options for `react-s3-uploader` (replaces `uploaderOptions`). 13 | - Readme is better. 14 | 15 | ## [1.0.0-rc.1] 16 | - Refactoring to clean up this abomination. 17 | - Props have been cleaned up. 18 | - Other props are pased to `react-dropzone`. 19 | - The `fileUrls` and `filenames` props have been replaced by `uploadedFile` objects. Each `uploadedFile` object has the filename, full s3 url (as `fileUrl`) and a reference to the original file descriptor from the upload. 20 | - Children have state information passed again via the `uploadedFiles` prop. 21 | - Passing children props can be disabled by setting the `passChildrenProps` prop to false to avoid React warnings about unused props. 22 | 23 | ## [0.11.0] 24 | - Upgraded react-s3-uploader to ^4.0.0 25 | 26 | ## [0.10.0] 27 | - Removed underscored props in favour of camelCase only. 28 | 29 | ## [0.9.0] 30 | - Upgraded `react-s3-uploader` to v3.3.0 31 | - Added some props: `uploaderOptions` and `preprocess` 32 | 33 | ## [0.8.1] 34 | - Fix bug caused by using _.map without importing it 35 | 36 | ## [0.8.0] 37 | - props.children no longer receive the `fileUrl`, `s3Url`, `filename`, `progress`, `error`, `imageStyle` props. If the `fileComponent` prop is specified it will receive these props. 38 | - maxFileSize and minFileSize are passed to the `react-dropzone` component, which handles validation 39 | - multiple files are handled better. Props named `fileUrls` and `filenames` are passed to the `fileComponent`, with an entry per file uploaded. 40 | 41 | ## [0.7.3] 42 | - Accepts an prop named `onDrop`, a function to be called with the files object when files are dropped. 43 | 44 | ## [0.7.0] 45 | - Removed dependency on react-bootstrap 46 | - New props: 47 | - `progressComponent`, a react component to render progress. Is provided a prop called `progress` with the current uploader progress percentage as an int (0-100). 48 | - `fileComponent` prop to do the same for rendering an uploaded file (not an image). 49 | - `isImage` a function that should return true if a filename represents an image. Default is `filename => filename && filename.match(/\.(jpeg|jpg|gif|png|svg)/i)` 50 | - If a child component is present it's only passed these props: `fileUrl, s3Url, filename, progress, error, imageStyle` 51 | 52 | ## [0.5.3] 53 | - Update React dependecy to include 15.x.x 54 | 55 | ## [0.5.0] 56 | - react-bootstrap dependency updated to ^0.29.0 57 | 58 | ## [0.4.2] 59 | - Renamed `host` option to `server` to match react-s3-uploader 60 | 61 | ## [0.4.1] 62 | - Pass accept prop to Dropzone 63 | 64 | ## [0.4.0] 65 | - Supports a display component via a child element. 66 | 67 | ## [0.3.1] 68 | - readme 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dropzone-s3-uploader", 3 | "version": "1.1.0", 4 | "description": "Drag and drop s3 file uploader via react-dropzone + react-s3-uploader", 5 | "main": "lib/index.js", 6 | "author": { 7 | "name": "Gwilym Humphreys", 8 | "url": "https://github.com/gwilymhumphreys" 9 | }, 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/founderlab/react-dropzone-s3-uploader" 14 | }, 15 | "scripts": { 16 | "prepublish": "rm -rf ./lib && babel ./src --ignore '/node_modules/' --out-dir ./lib", 17 | "build": "rm -rf ./lib && babel ./src --ignore '/node_modules/' --out-dir ./lib", 18 | "watch": "rm -rf ./lib && babel ./src --ignore '/node_modules/' --watch --out-dir ./lib", 19 | "test": "eval $(cat test/.env) mocha test/**/*.tests.js" 20 | }, 21 | "dependencies": { 22 | "prop-types": "^15.5.8", 23 | "react-dropzone": "^4.0.0", 24 | "react-s3-uploader": "4.5.0" 25 | }, 26 | "devDependencies": { 27 | "babel": "^5.6.14", 28 | "babel-core": "^5.6.15", 29 | "babel-eslint": "^4.1.3", 30 | "eslint": "^1.5.1", 31 | "eslint-config-founderlab": "^0.1.0", 32 | "eslint-plugin-react": "^3.4.2", 33 | "expect": "^1.12.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Drag and drop s3 file uploader for React 2 | 3 | This component uploads files dropped into [react-dropzone](https://github.com/okonet/react-dropzone) to s3 with [react-s3-uploader](https://github.com/odysseyscience/react-s3-uploader). 4 | 5 | For more detailed docs see the source packages 6 | - [react-dropzone](https://github.com/okonet/react-dropzone) 7 | - [react-s3-uploader](https://github.com/odysseyscience/react-s3-uploader) 8 | 9 | 10 | 11 | ## Usage (client) 12 | 13 | 14 | #### Available props 15 | 16 | `s3Url` and `upload` are the only props that require configuration. All others have sensible defaults that may be overridden. 17 | 18 | 19 | Prop | Type | Description 20 | ----------------- | ----------------- | ------------------------------------------- 21 | s3Url | string.isRequired | The url of your s3 bucket (`https://my-bucket.s3.amazonaws.com`) 22 | upload | object.isRequired | Upload options passed to react-s3-uploader. See [react-s3-uploader](https://github.com/odysseyscience/react-s3-uploader) for available options. Don't set `onProgress`, `onError` or `onFinish` here - use the ones below 23 | filename | string | Used as the default value if present. Filename of an image already hosted on s3 (i.e. one that was uploaded previously) 24 | notDropzoneProps | array | A list of props to *not* pass to `react-dropzone` 25 | isImage | func | A function that takes a filename and returns true if it's an image 26 | imageComponent | func | Component used to render an uploaded image 27 | fileComponent | func | Component used to render an uploaded file 28 | progressComponent | func | Component used to render upload progress 29 | errorComponent | func | Component used to render an error 30 | children | node \|\| func | If present the above components will be ignored in favour of the children 31 | passChildrenProps | bool | If true we pass the current state to children of this component. Default is true. Set to false to avoid React warnings about unused props. 32 | onDrop | func | Called when a file is dropped onto the uploader 33 | onError | func | Called when an upload error occurs 34 | onProgress | func | Called when a chunk has been uploaded 35 | onFinish | func | Called when a file has completed uploading. Called once per file if multi=true 36 | ...rest | | All other props are passed on to the `react-dropzone` component 37 | 38 | 39 | #### Example 40 | ```javascript 41 | import DropzoneS3Uploader from 'react-dropzone-s3-uploader' 42 | 43 | export default class S3Uploader extends React.Component { 44 | 45 | handleFinishedUpload = info => { 46 | console.log('File uploaded with filename', info.filename) 47 | console.log('Access it on s3 at', info.fileUrl) 48 | } 49 | 50 | render() { 51 | const uploadOptions = { 52 | server: 'http://localhost:4000', 53 | signingUrlQueryParams: {uploadType: 'avatar'}, 54 | } 55 | const s3Url = 'https://my-bucket.s3.amazonaws.com' 56 | 57 | return ( 58 | 64 | ) 65 | } 66 | } 67 | ``` 68 | 69 | #### Custom display component 70 | Specify your own component to display uploaded files. Passed a list of `uploadedFile` objects. 71 | 72 | ```javascript 73 | 74 | // elsewhere 75 | class UploadDisplay extends React.Component { 76 | 77 | renderFileUpload = (uploadedFile, i) => { 78 | const { 79 | filename, // s3 filename 80 | fileUrl, // full s3 url of the file 81 | file, // file descriptor from the upload 82 | } = uploadedFile 83 | 84 | return ( 85 |
86 | 87 |

{file.name}

88 |
89 | ) 90 | } 91 | 92 | render() { 93 | const {uploadedFiles, s3Url} = this.props 94 | return ( 95 |
96 | {uploadedFiles.map(this.renderFileUpload)} 97 |
98 | ) 99 | } 100 | } 101 | 102 | // back in your uploader... 103 | class S3Uploader extends React.Component { 104 | 105 | //... 106 | 107 | render() { 108 | return ( 109 | 113 | 114 | 115 | ) 116 | } 117 | } 118 | ``` 119 | 120 | 121 | ## Usage (server) 122 | 123 | - Use s3Router from react-s3-uploader to get signed urls for uploads. 124 | - `react-dropzone-s3-uploader/s3router` can be used as an alias for `react-s3-uploader/s3router`. 125 | - See [react-s3-uploader](https://github.com/odysseyscience/react-s3-uploader) for more details. 126 | 127 | ```javascript 128 | app.use('/s3', require('react-dropzone-s3-uploader/s3router')({ 129 | bucket: 'MyS3Bucket', // required 130 | region: 'us-east-1', // optional 131 | headers: {'Access-Control-Allow-Origin': '*'}, // optional 132 | ACL: 'private', // this is the default - set to `public-read` to let anyone view uploads 133 | })); 134 | ``` 135 | -------------------------------------------------------------------------------- /s3router.js: -------------------------------------------------------------------------------- 1 | module.exports = require('react-s3-uploader/s3router') -------------------------------------------------------------------------------- /src/DropzoneS3Uploader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import S3Upload from 'react-s3-uploader/s3upload' 4 | import Dropzone from 'react-dropzone' 5 | 6 | export default class DropzoneS3Uploader extends React.Component { 7 | 8 | static propTypes = { 9 | filename: PropTypes.string, 10 | s3Url: PropTypes.string.isRequired, 11 | notDropzoneProps: PropTypes.array.isRequired, 12 | isImage: PropTypes.func.isRequired, 13 | passChildrenProps: PropTypes.bool, 14 | 15 | imageComponent: PropTypes.func, 16 | fileComponent: PropTypes.func, 17 | progressComponent: PropTypes.func, 18 | errorComponent: PropTypes.func, 19 | 20 | children: PropTypes.oneOfType([ 21 | PropTypes.node, 22 | PropTypes.func, 23 | ]), 24 | 25 | onDrop: PropTypes.func, 26 | onError: PropTypes.func, 27 | onProgress: PropTypes.func, 28 | onFinish: PropTypes.func, 29 | 30 | // Passed to react-s3-uploader 31 | upload: PropTypes.object.isRequired, 32 | 33 | // Default styles for react-dropzone 34 | className: PropTypes.oneOfType([ 35 | PropTypes.string, 36 | PropTypes.object, 37 | ]), 38 | style: PropTypes.object, 39 | activeStyle: PropTypes.object, 40 | rejectStyle: PropTypes.object, 41 | } 42 | 43 | static defaultProps = { 44 | upload: {}, 45 | className: 'react-dropzone-s3-uploader', 46 | passChildrenProps: true, 47 | isImage: filename => filename && filename.match(/\.(jpeg|jpg|gif|png|svg)/i), 48 | notDropzoneProps: ['onFinish', 's3Url', 'filename', 'host', 'upload', 'isImage', 'notDropzoneProps'], 49 | style: { 50 | width: 200, 51 | height: 200, 52 | border: 'dashed 2px #999', 53 | borderRadius: 5, 54 | position: 'relative', 55 | cursor: 'pointer', 56 | overflow: 'hidden', 57 | }, 58 | activeStyle: { 59 | borderStyle: 'solid', 60 | backgroundColor: '#eee', 61 | }, 62 | rejectStyle: { 63 | borderStyle: 'solid', 64 | backgroundColor: '#ffdddd', 65 | }, 66 | } 67 | 68 | constructor(props) { 69 | super() 70 | const uploadedFiles = [] 71 | const {filename} = props 72 | if (filename) { 73 | uploadedFiles.push({ 74 | filename, 75 | fileUrl: this.fileUrl(props.s3Url, filename), 76 | default: true, 77 | file: {}, 78 | }) 79 | } 80 | this.state = {uploadedFiles} 81 | } 82 | 83 | componentWillMount = () => this.setUploaderOptions(this.props) 84 | componentWillReceiveProps = props => this.setUploaderOptions(props) 85 | 86 | setUploaderOptions = props => { 87 | this.setState({ 88 | uploaderOptions: Object.assign({ 89 | signingUrl: '/s3/sign', 90 | s3path: '', 91 | contentDisposition: 'auto', 92 | uploadRequestHeaders: {'x-amz-acl': 'public-read'}, 93 | onFinishS3Put: this.handleFinish, 94 | onProgress: this.handleProgress, 95 | onError: this.handleError, 96 | }, props.upload), 97 | }) 98 | } 99 | 100 | handleProgress = (progress, textState, file) => { 101 | this.props.onProgress && this.props.onProgress(progress, textState, file) 102 | this.setState({progress}) 103 | } 104 | 105 | handleError = (err, file) => { 106 | this.props.onError && this.props.onError(err, file) 107 | this.setState({error: err, progress: null}) 108 | } 109 | 110 | handleFinish = (info, file) => { 111 | const uploadedFile = Object.assign({ 112 | file, 113 | fileUrl: this.fileUrl(this.props.s3Url, info.filename), 114 | }, info) 115 | 116 | const uploadedFiles = this.state.uploadedFiles 117 | uploadedFiles.push(uploadedFile) 118 | this.setState({uploadedFiles, error: null, progress: null}, () => { 119 | this.props.onFinish && this.props.onFinish(uploadedFile) 120 | }) 121 | } 122 | 123 | handleDrop = (files, rejectedFiles) => { 124 | this.setState({uploadedFiles: [], error: null, progress: null}) 125 | const options = { 126 | files, 127 | ...this.state.uploaderOptions, 128 | } 129 | new S3Upload(options) // eslint-disable-line 130 | this.props.onDrop && this.props.onDrop(files, rejectedFiles) 131 | } 132 | 133 | fileUrl = (s3Url, filename) => `${s3Url.endsWith('/') ? s3Url.slice(0, -1) : s3Url}/${filename}` 134 | 135 | renderImage = ({uploadedFile}) => (
) 136 | 137 | renderFile = ({uploadedFile}) => ( 138 |
139 |
140 |
{uploadedFile.file.name}
141 |
142 | ) 143 | 144 | renderProgress = ({progress}) => (progress ? (
{progress}
) : null) 145 | 146 | renderError = ({error}) => (error ? (
{error}
) : null) 147 | 148 | render() { 149 | const { 150 | s3Url, 151 | passChildrenProps, 152 | children, 153 | imageComponent, 154 | fileComponent, 155 | progressComponent, 156 | errorComponent, 157 | ...dropzoneProps, 158 | } = this.props 159 | 160 | const ImageComponent = imageComponent || this.renderImage 161 | const FileComponent = fileComponent || this.renderFile 162 | const ProgressComponent = progressComponent || this.renderProgress 163 | const ErrorComponent = errorComponent || this.renderError 164 | 165 | const {uploadedFiles} = this.state 166 | const childProps = {s3Url, ...this.state} 167 | this.props.notDropzoneProps.forEach(prop => delete dropzoneProps[prop]) 168 | 169 | let content = null 170 | if (children) { 171 | content = passChildrenProps ? 172 | React.Children.map(children, child => React.cloneElement(child, childProps)) : 173 | this.props.children 174 | } 175 | else { 176 | content = ( 177 |
178 | {uploadedFiles.map(uploadedFile => { 179 | const props = { 180 | key: uploadedFile.filename, 181 | uploadedFile: uploadedFile, 182 | ...childProps, 183 | } 184 | return this.props.isImage(uploadedFile.fileUrl) ? 185 | () : 186 | () 187 | })} 188 | 189 | 190 |
191 | ) 192 | } 193 | 194 | return ( 195 | this._dropzone = c} onDrop={this.handleDrop} {...dropzoneProps}> 196 | {content} 197 | 198 | ) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import DropzoneS3Uploader from './DropzoneS3Uploader' 2 | export default DropzoneS3Uploader 3 | -------------------------------------------------------------------------------- /test/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=test 2 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel/register 2 | --recursive 3 | -------------------------------------------------------------------------------- /test/todo.tests.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import {DropzoneS3Uploader} from '../src' 3 | 4 | describe('DropzoneS3Uploader', () => { 5 | 6 | it('doesnt have tests', () => { 7 | }) 8 | 9 | }) 10 | --------------------------------------------------------------------------------