diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eaf87c..3844d4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,61 @@ +##### 5.0.0 + +* Update several dependencies, moving some to `optional` and `dev` +* Update many typescript definitions +* Added `mime-types` fallback when browser doesn't provide `file.type` [#223] + +##### 4.9.3 + +* Add missing typescript definitions [#216] + +##### 4.9.2 + +* Added typescript definitions + +##### 4.9.0 + +* Better error messaging [#207] +* Configurable Expires time [#208] + +##### 4.8.0 + +* Allow 201 response code from PUT [#174] + +##### 4.7.0 + +* Add an `onSignedUrl` lifecycle hook [#170] + +##### 4.6.2 + +* Fix `undefined` in file path when not providing `s3path` prop [#160] + +##### 4.6.1 + +* Fix `s3path` and `uniquePrefix` ordering when applied together [#156] + +##### 4.6.0 + +* Add `autoUpload` prop, which can be set to `false` to disable automatic upload [#95] [#107] [#155] + +##### 4.5.1 + +* Add `inputRef` prop [#153] + +##### 4.5.0 + +* Removed `peerDependencies` on react and react-dom [#136] + +##### 4.4.0 + +* Support `getS3` function in bundled router options [#139] +* Support `s3Path` string prefix prop [#140] + +##### 4.3.0 + +* Support middleware in express module with optional second arg +* Allow 200 or 201 for success on sign request + ##### 4.2.0 * Switch to `uuid` instead of `node-uuid` [#115] diff --git a/README.md b/README.md index 8cde3a5..cd295ff 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,9 @@ var ReactS3Uploader = require('react-s3-uploader'); signingUrl="/s3/sign" signingUrlMethod="GET" accept="image/*" + s3path="/uploads/" preprocess={this.onUploadStart} + onSignedUrl={this.onSignedUrl} onProgress={this.onUploadProgress} onError={this.onUploadError} onFinish={this.onUploadFinish} @@ -30,7 +32,10 @@ var ReactS3Uploader = require('react-s3-uploader'); uploadRequestHeaders={{ 'x-amz-acl': 'public-read' }} // this is the default contentDisposition="auto" scrubFilename={(filename) => filename.replace(/[^\w\d_\-.]+/ig, '')} - server="http://cross-origin-server.com" /> + server="http://cross-origin-server.com" + inputRef={cmp => this.uploadInput = cmp} + autoUpload={true} + /> ``` The above example shows all supported `props`. @@ -58,8 +63,7 @@ The resulting DOM is essentially: The `preprocess(file, next)` prop provides an opportunity to do something before the file upload begins, modify the file (scaling the image for example), or abort the upload by not calling `next(file)`. -When a file is chosen, it will immediately be uploaded to S3. You can listen for progress (and -create a status bar, for example) by providing an `onProgress` function to the component. +When a file is chosen, it will immediately be uploaded to S3 (unless `autoUpload` is `false`). You can listen for progress (and create a status bar, for example) by providing an `onProgress` function to the component. ### Extra props You can pass any extra props to `` and these will be passed down to the final ``. which means that if you give the ReactS3Uploader a className or a name prop the input will have those as well. @@ -114,6 +118,7 @@ app.use('/s3', require('react-s3-uploader/s3router')({ bucket: "MyS3Bucket", region: 'us-east-1', //optional signatureVersion: 'v4', //optional (use for some amazon regions: frankfurt and others) + signatureExpires: 60, //optional, number of seconds the upload signed URL should be valid for (defaults to 60) headers: {'Access-Control-Allow-Origin': '*'}, // optional ACL: 'private', // this is default uniquePrefix: true // (4.0.2 and above) default is true, setting the attribute to false preserves the original filename in S3 @@ -124,6 +129,9 @@ This also provides another endpoint: `GET /s3/img/(.*)` and `GET /s3/uploads/(.* that provides access to the uploaded file (which are uploaded privately by default). The request is then redirected to the URL, so that the image is served to the client. +If you need to use pass more than region and signatureVersion to S3 instead use the `getS3` param. `getS3` accepts a +function that returns a new AWS.S3 instance. This is also useful if you want to mock S3 for testing purposes. + **To use this you will need to include the [express module](https://www.npmjs.com/package/express) in your package.json dependencies.** #### Access/Secret Keys @@ -178,6 +186,53 @@ respond_to do |format| end ``` +#### [Micro](https://github.com/zeit/micro) + +```javascript +const aws = require('aws-sdk') +const uuidv4 = require('uuid/v4') +const { createError } = require('micro') + +const options = { + bucket: 'S3_BUCKET_NAME', + region: 'S3_REGION', + signatureVersion: 'v4', + ACL: 'public-read' +} + +const s3 = new aws.S3(options) + +module.exports = (req, res) => { + const originalFilename = req.query.objectName + + // custom filename using random uuid + file extension + const fileExtension = originalFilename.split('.').pop() + const filename = `${uuidv4()}.${fileExtension}` + + const params = { + Bucket: options.bucket, + Key: filename, + Expires: 60, + ContentType: req.query.contentType, + ACL: options.ACL + } + + const signedUrl = s3.getSignedUrl('putObject', params) + + if (signedUrl) { + // you may also simply return the signed url, i.e. `return { signedUrl }` + return { + signedUrl, + filename, + originalFilename, + publicUrl: signedUrl.split('?').shift() + } + } else { + throw createError(500, 'Cannot create S3 signed URL') + } +} +``` + ##### Other Servers diff --git a/ReactS3Uploader.js b/ReactS3Uploader.js index 080bf5a..4ec64a2 100644 --- a/ReactS3Uploader.js +++ b/ReactS3Uploader.js @@ -13,6 +13,7 @@ var ReactS3Uploader = createReactClass({ signingUrl: PropTypes.string, getSignedUrl: PropTypes.func, preprocess: PropTypes.func, + onSignedUrl: PropTypes.func, onProgress: PropTypes.func, onFinish: PropTypes.func, onError: PropTypes.func, @@ -29,29 +30,40 @@ var ReactS3Uploader = createReactClass({ uploadRequestHeaders: PropTypes.object, contentDisposition: PropTypes.string, server: PropTypes.string, - scrubFilename: PropTypes.func + scrubFilename: PropTypes.func, + s3path: PropTypes.string, + inputRef: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.func + ]), + autoUpload: PropTypes.bool }, getDefaultProps: function() { return { preprocess: function(file, next) { - console.log('Pre-process: ' + file.name); + console.log('Pre-process: ',file.name); next(file); }, - onProgress: function(percent, message) { - console.log('Upload progress: ' + percent + '% ' + message); + onSignedUrl: function( signingServerResponse ) { + console.log('Signing server response: ', signingServerResponse); + }, + onProgress: function(percent, message, file) { + console.log('Upload progress: ',`${percent} % ${message}`); }, onFinish: function(signResult) { - console.log("Upload finished: " + signResult.publicUrl) + console.log("Upload finished: ",signResult.publicUrl) }, onError: function(message) { - console.log("Upload error: " + message); + console.log("Upload error: ",message); }, server: '', signingUrlMethod: 'GET', scrubFilename: function(filename) { return filename.replace(/[^\w\d_\-\.]+/ig, ''); - } + }, + s3path: '', + autoUpload: true }; }, @@ -61,6 +73,7 @@ var ReactS3Uploader = createReactClass({ signingUrl: this.props.signingUrl, getSignedUrl: this.props.getSignedUrl, preprocess: this.props.preprocess, + onSignedUrl: this.props.onSignedUrl, onProgress: this.props.onProgress, onFinishS3Put: this.props.onFinish, onError: this.props.onError, @@ -71,7 +84,8 @@ var ReactS3Uploader = createReactClass({ uploadRequestHeaders: this.props.uploadRequestHeaders, contentDisposition: this.props.contentDisposition, server: this.props.server, - scrubFilename: this.props.scrubFilename + scrubFilename: this.props.scrubFilename, + s3path: this.props.s3path }); }, @@ -88,7 +102,18 @@ var ReactS3Uploader = createReactClass({ }, getInputProps: function() { - var temporaryProps = objectAssign({}, this.props, {type: 'file', onChange: this.uploadFile}); + // declare ref beforehand and filter out + // `inputRef` by `ReactS3Uploader.propTypes` + var additional = { + type: 'file', + ref: this.props.inputRef + }; + + if ( this.props.autoUpload ) { + additional.onChange = this.uploadFile; + } + + var temporaryProps = objectAssign({}, this.props, additional); var inputProps = {}; var invalidProps = Object.keys(ReactS3Uploader.propTypes); diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..29b0195 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,77 @@ +declare module 'react-s3-uploader' { + import { Component } from 'react'; + + export interface S3Response { + signedUrl: string; + publicUrl: string; + filename: string; + fileKey: string; + } + + export interface ReactS3UploaderProps { + signingUrl?: string; + signingUrlMethod?: 'GET' | 'POST'; + getSignedUrl?: (file: File, callback: (params: S3Response) => void) => void; + accept?: string; + s3path?: string; + preprocess?: (file: File, next: (file: File) => void) => void; + onSignedUrl?: (response: S3Response) => void; + onProgress?: (percent: number, status: string, file: File) => void; + onError?: (message: string) => void; + onFinish?: (result: S3Response, file: File) => void; + signingUrlHeaders?: { + additional: object; + }; + signingUrlQueryParams?: { + additional: object; + }; + signingUrlWithCredentials?: boolean; + uploadRequestHeaders?: object; + contentDisposition?: string; + server?: string; + inputRef?: (ref: HTMLInputElement) => any; + autoUpload?: boolean; + scrubFilename?: (filename: string) => string; + [key: string]: any; + } + + class ReactS3Uploader extends Component { } + + export default ReactS3Uploader; +} + +declare module 'react-s3-uploader/s3upload' { + import { ReactS3UploaderProps, S3Response } from 'react-s3-uploader'; + + export interface S3UploadOptions extends Pick< + ReactS3UploaderProps, + | 'contentDisposition' + | 'getSignedUrl' + | 'onProgress' + | 'onError' + | 'onSignedUrl' + | 'preprocess' + | 's3path' + | 'server' + | 'signingUrl' + | 'signingUrlHeaders' + | 'signingUrlMethod' + | 'signingUrlQueryParams' + | 'signingUrlWithCredentials' + | 'uploadRequestHeaders'> { + fileElement?: HTMLInputElement | null; + files?: HTMLInputElement['files'] | null; + onFinishS3Put?: ReactS3UploaderProps['onFinish']; + successResponses?: number[]; + scrubFilename?: (filename: string) => string; + } + + class S3Upload { + constructor(options: ReactS3UploaderProps); + abortUpload(): void; + uploadFile(file: File): Promise; + uploadToS3(file: File, signResult: S3Response): void; + } + + export default S3Upload; +} diff --git a/package-lock.json b/package-lock.json index 3c0eeec..f1a55a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,23 @@ { "name": "react-s3-uploader", - "version": "4.2.0", + "version": "4.9.3", "lockfileVersion": 1, + "requires": true, "dependencies": { + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" + }, + "@types/react": { + "version": "16.9.19", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.19.tgz", + "integrity": "sha512-LJV97//H+zqKWMms0kvxaKYJDG05U2TtQB3chRLF8MPNs+MQh/H1aGlyDUxjaHvu08EAGerdX2z4LTBc7ns77A==", + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, "asap": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz", @@ -12,6 +27,17 @@ "version": "2.80.0", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.80.0.tgz", "integrity": "sha1-Yc7XR+uYFglIOuxT6NZU08ydFDU=", + "requires": { + "buffer": "4.9.1", + "crypto-browserify": "1.0.9", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.0.1", + "xml2js": "0.4.17", + "xmlbuilder": "4.2.1" + }, "dependencies": { "uuid": { "version": "3.0.1", @@ -28,7 +54,12 @@ "buffer": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=" + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } }, "core-js": { "version": "1.2.7", @@ -39,6 +70,11 @@ "version": "15.6.0", "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.0.tgz", "integrity": "sha1-q0SEl8JlZuHilBPogyB9V8/nvtQ=", + "requires": { + "fbjs": "^0.8.9", + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + }, "dependencies": { "object-assign": { "version": "4.1.1", @@ -52,15 +88,32 @@ "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz", "integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA=" }, + "csstype": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.8.tgz", + "integrity": "sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA==" + }, "encoding": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=" + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "~0.4.13" + } }, "fbjs": { "version": "0.8.12", "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.12.tgz", "integrity": "sha1-ELXZL3bUVXX9Y6IX1OoCvqL47QQ=", + "requires": { + "core-js": "^1.0.0", + "isomorphic-fetch": "^2.1.1", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.9" + }, "dependencies": { "object-assign": { "version": "4.1.1", @@ -92,7 +145,11 @@ "isomorphic-fetch": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=" + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "^1.0.1", + "whatwg-fetch": ">=0.10.0" + } }, "jmespath": { "version": "0.15.0", @@ -105,19 +162,26 @@ "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" }, "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "loose-envify": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=" + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "requires": { + "js-tokens": "^3.0.0" + } }, "node-fetch": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.1.tgz", - "integrity": "sha512-j8XsFGCLw79vWXkZtMSmmLaOk9z5SQ9bV/tkbZVCqvgwzrjAGq66igobLofHtF63NvMTp2WjytpsNTGKa+XRIQ==" + "integrity": "sha512-j8XsFGCLw79vWXkZtMSmmLaOk9z5SQ9bV/tkbZVCqvgwzrjAGq66igobLofHtF63NvMTp2WjytpsNTGKa+XRIQ==", + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } }, "object-assign": { "version": "2.1.1", @@ -127,12 +191,19 @@ "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==" + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } }, "prop-types": { "version": "15.5.10", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.10.tgz", - "integrity": "sha1-J5ffwxJhguOpXj37suiT3ddFYVQ=" + "integrity": "sha1-J5ffwxJhguOpXj37suiT3ddFYVQ=", + "requires": { + "fbjs": "^0.8.9", + "loose-envify": "^1.3.1" + } }, "punycode": { "version": "1.3.2", @@ -154,6 +225,11 @@ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, + "typescript": { + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", + "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==" + }, "ua-parser-js": { "version": "0.7.13", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.13.tgz", @@ -162,7 +238,11 @@ "url": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=" + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } }, "uuid": { "version": "3.1.0", @@ -177,12 +257,19 @@ "xml2js": { "version": "0.4.17", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz", - "integrity": "sha1-F76T6q4/O3eTWceVtBlwWogX6Gg=" + "integrity": "sha1-F76T6q4/O3eTWceVtBlwWogX6Gg=", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "^4.1.0" + } }, "xmlbuilder": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz", - "integrity": "sha1-qlijBBoGb5DqoWwvU4n/GfP0YaU=" + "integrity": "sha1-qlijBBoGb5DqoWwvU4n/GfP0YaU=", + "requires": { + "lodash": "^4.0.0" + } } } } diff --git a/package.json b/package.json index 0f89515..6b05543 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-s3-uploader", - "version": "4.2.0", + "version": "4.9.3", "description": "React component that renders a file input and automatically uploads to an S3 bucket", "main": "index.js", "scripts": { @@ -10,6 +10,7 @@ "type": "git", "url": "git@github.com:odysseyscience/react-s3-uploader.git" }, + "types": "index.d.ts", "keywords": [ "react", "upload", @@ -24,14 +25,17 @@ }, "homepage": "https://github.com/odysseyscience/react-s3-uploader", "dependencies": { - "aws-sdk": "2.x", "create-react-class": "^15.5.2", + "mime-types": "^2.1.24", "object-assign": "^2.0.0", "prop-types": "^15.5.8", "uuid": "^3.1.0" }, - "peerDependencies": { - "react": "*", - "react-dom": "*" + "devDependencies": { + "@types/react": "*", + "typescript": "^3.7.5" + }, + "optionalDependencies": { + "aws-sdk": "2.x" } } diff --git a/s3router.js b/s3router.js index 96faa23..65f28f9 100644 --- a/s3router.js +++ b/s3router.js @@ -10,7 +10,11 @@ function checkTrailingSlash(path) { return path; } -function S3Router(options) { +function S3Router(options, middleware) { + + if (!middleware) { + middleware = []; + } var S3_BUCKET = options.bucket, getFileKeyDir = options.getFileKeyDir || function() { return ""; }; @@ -19,13 +23,21 @@ function S3Router(options) { throw new Error("S3_BUCKET is required."); } - var s3Options = {}; - if (options.region) { - s3Options.region = options.region; - } - if (options.signatureVersion) { + var getS3 = options.getS3; + if (!getS3) { + var s3Options = {}; + if (options.region) { + s3Options.region = options.region; + } + if (options.signatureVersion) { s3Options.signatureVersion = options.signatureVersion; + } + + getS3 = function() { + return new aws.S3(s3Options); + }; } + if (options.uniquePrefix === undefined) { options.uniquePrefix = true; } @@ -41,7 +53,7 @@ function S3Router(options) { Bucket: S3_BUCKET, Key: checkTrailingSlash(getFileKeyDir(req)) + req.params[0] }; - var s3 = new aws.S3(s3Options); + var s3 = getS3(); s3.getSignedUrl('getObject', params, function(err, url) { res.redirect(url); }); @@ -50,14 +62,14 @@ function S3Router(options) { /** * Image specific route. */ - router.get(/\/img\/(.*)/, function(req, res) { + router.get(/\/img\/(.*)/, middleware, function(req, res) { return tempRedirect(req, res); }); /** * Other file type(s) route. */ - router.get(/\/uploads\/(.*)/, function(req, res) { + router.get(/\/uploads\/(.*)/, middleware, function(req, res) { return tempRedirect(req, res); }); @@ -65,8 +77,8 @@ function S3Router(options) { * Returns an object with `signedUrl` and `publicUrl` properties that * give temporary access to PUT an object in an S3 bucket. */ - router.get('/sign', function(req, res) { - var filename = (options.uniquePrefix ? uuidv4() + "_" : "") + req.query.objectName; + router.get('/sign', middleware, function(req, res) { + var filename = (req.query.path || '') + (options.uniquePrefix ? uuidv4() + "_" : "") + req.query.objectName; var mimeType = req.query.contentType; var fileKey = checkTrailingSlash(getFileKeyDir(req)) + filename; // Set any custom headers @@ -74,11 +86,11 @@ function S3Router(options) { res.set(options.headers); } - var s3 = new aws.S3(s3Options); + var s3 = getS3(); var params = { Bucket: S3_BUCKET, Key: fileKey, - Expires: 60, + Expires: options.signatureExpires || 60, ContentType: mimeType, ACL: options.ACL || 'private' }; diff --git a/s3upload.js b/s3upload.js index 988b3db..7a30cb4 100644 --- a/s3upload.js +++ b/s3upload.js @@ -3,9 +3,12 @@ * https://github.com/flyingsparx/NodeDirectUploader */ +var mime = require('mime-types'); + S3Upload.prototype.server = ''; S3Upload.prototype.signingUrl = '/sign-s3'; S3Upload.prototype.signingUrlMethod = 'GET'; +S3Upload.prototype.successResponses = [200, 201]; S3Upload.prototype.fileElement = null; S3Upload.prototype.files = null; @@ -26,6 +29,8 @@ S3Upload.prototype.onError = function(status, file) { return console.log('base.onError()', status); }; +S3Upload.prototype.onSignedUrl = function(result) {}; + S3Upload.prototype.scrubFilename = function(filename) { return filename.replace(/[^\w\d_\-\.]+/ig, ''); }; @@ -43,14 +48,18 @@ function S3Upload(options) { this.handleFileSelect(files); } +function getFileMimeType(file) { + return file.type || mime.lookup(file.name); +} + S3Upload.prototype.handleFileSelect = function(files) { var result = []; for (var i=0; i < files.length; i++) { var file = files[i]; this.preprocess(file, function(processedFile){ - this.onProgress(0, 'Waiting', processedFile); - result.push(this.uploadFile(processedFile)); - return result; + this.onProgress(0, 'Waiting', processedFile); + result.push(this.uploadFile(processedFile)); + return result; }.bind(this)); } }; @@ -75,9 +84,21 @@ S3Upload.prototype.createCORSRequest = function(method, url, opts) { return xhr; }; +S3Upload.prototype._getErrorRequestContext = function (xhr) { + return { + response: xhr.responseText, + status: xhr.status, + statusText: xhr.statusText, + readyState: xhr.readyState + }; +} + S3Upload.prototype.executeOnSignedUrl = function(file, callback) { var fileName = this.scrubFilename(file.name); - var queryString = '?objectName=' + fileName + '&contentType=' + encodeURIComponent(file.type); + var queryString = '?objectName=' + fileName + '&contentType=' + encodeURIComponent(getFileMimeType(file)); + if (this.s3path) { + queryString += '&path=' + encodeURIComponent(this.s3path); + } if (this.signingUrlQueryParams) { var signingUrlQueryParams = typeof this.signingUrlQueryParams === 'function' ? this.signingUrlQueryParams() : this.signingUrlQueryParams; Object.keys(signingUrlQueryParams).forEach(function(key) { @@ -86,7 +107,7 @@ S3Upload.prototype.executeOnSignedUrl = function(file, callback) { }); } var xhr = this.createCORSRequest(this.signingUrlMethod, - this.server + this.signingUrl + queryString, { withCredentials: this.signingUrlWithCredentials }); + this.server + this.signingUrl + queryString, { withCredentials: this.signingUrlWithCredentials }); if (this.signingUrlHeaders) { var signingUrlHeaders = typeof this.signingUrlHeaders === 'function' ? this.signingUrlHeaders() : this.signingUrlHeaders; Object.keys(signingUrlHeaders).forEach(function(key) { @@ -96,17 +117,26 @@ S3Upload.prototype.executeOnSignedUrl = function(file, callback) { } xhr.overrideMimeType && xhr.overrideMimeType('text/plain; charset=x-user-defined'); xhr.onreadystatechange = function() { - if (xhr.readyState === 4 && xhr.status === 200) { + if (xhr.readyState === 4 && this.successResponses.indexOf(xhr.status) >= 0) { var result; try { result = JSON.parse(xhr.responseText); + this.onSignedUrl( result ); } catch (error) { - this.onError('Invalid response from server', file); + this.onError( + 'Invalid response from server', + file, + this._getErrorRequestContext(xhr) + ); return false; } return callback(result); - } else if (xhr.readyState === 4 && xhr.status !== 200) { - return this.onError('Could not contact request signing server. Status = ' + xhr.status, file); + } else if (xhr.readyState === 4 && this.successResponses.indexOf(xhr.status) < 0) { + return this.onError( + 'Could not contact request signing server. Status = ' + xhr.status, + file, + this._getErrorRequestContext(xhr) + ); } }.bind(this); return xhr.send(); @@ -115,18 +145,26 @@ S3Upload.prototype.executeOnSignedUrl = function(file, callback) { S3Upload.prototype.uploadToS3 = function(file, signResult) { var xhr = this.createCORSRequest('PUT', signResult.signedUrl); if (!xhr) { - this.onError('CORS not supported', file); + this.onError('CORS not supported', file, {}); } else { xhr.onload = function() { - if (xhr.status === 200) { + if (this.successResponses.indexOf(xhr.status) >= 0) { this.onProgress(100, 'Upload completed', file); return this.onFinishS3Put(signResult, file); } else { - return this.onError('Upload error: ' + xhr.status, file); + return this.onError( + 'Upload error: ' + xhr.status, + file, + this._getErrorRequestContext(xhr) + ); } }.bind(this); xhr.onerror = function() { - return this.onError('XHR error', file); + return this.onError( + 'XHR error', + file, + this._getErrorRequestContext(xhr) + ); }.bind(this); xhr.upload.onprogress = function(e) { var percentLoaded; @@ -136,11 +174,16 @@ S3Upload.prototype.uploadToS3 = function(file, signResult) { } }.bind(this); } - xhr.setRequestHeader('Content-Type', file.type); + + var fileType = getFileMimeType(file); + var headers = { + 'content-type': fileType + }; + if (this.contentDisposition) { var disposition = this.contentDisposition; if (disposition === 'auto') { - if (file.type.substr(0, 6) === 'image/') { + if (fileType.substr(0, 6) === 'image/') { disposition = 'inline'; } else { disposition = 'attachment'; @@ -148,24 +191,19 @@ S3Upload.prototype.uploadToS3 = function(file, signResult) { } var fileName = this.scrubFilename(file.name) - xhr.setRequestHeader('Content-Disposition', disposition + '; filename="' + fileName + '"'); - } - if (signResult.headers) { - var signResultHeaders = signResult.headers - Object.keys(signResultHeaders).forEach(function(key) { - var val = signResultHeaders[key]; - xhr.setRequestHeader(key, val); - }) + headers['content-disposition'] = disposition + '; filename="' + fileName + '"'; } - if (this.uploadRequestHeaders) { - var uploadRequestHeaders = this.uploadRequestHeaders; - Object.keys(uploadRequestHeaders).forEach(function(key) { - var val = uploadRequestHeaders[key]; - xhr.setRequestHeader(key, val); - }); - } else { + if (!this.uploadRequestHeaders) { xhr.setRequestHeader('x-amz-acl', 'public-read'); } + [signResult.headers, this.uploadRequestHeaders].filter(Boolean).forEach(function (hdrs) { + Object.entries(hdrs).forEach(function(pair) { + headers[pair[0].toLowerCase()] = pair[1]; + }) + }); + Object.entries(headers).forEach(function (pair) { + xhr.setRequestHeader(pair[0], pair[1]); + }) this.httprequest = xhr; return xhr.send(file); }; diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..06d9048 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,187 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/prop-types@*": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + +"@types/react@*": + version "16.9.55" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.55.tgz#47078587f5bfe028a23b6b46c7b94ac0d436acff" + integrity sha512-6KLe6lkILeRwyyy7yG9rULKJ0sXplUsl98MGoCfpteXf9sPWFWWMknDcsvubcpaTdBuxtsLF6HDUwdApZL/xIg== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +aws-sdk@2.x: + version "2.784.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.784.0.tgz#70136a537d5c977a9e9a2e8b6a234b45ec2da0a1" + integrity sha512-+KBkqH7t/XE91Fqn8eyJeNIWsnhSWL8bSUqFD7TfE3FN07MTlC0nprGYp+2WfcYNz5i8Bus1vY2DHNVhtTImnw== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + +buffer@4.9.2: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +create-react-class@^15.5.2: + version "15.7.0" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.7.0.tgz#7499d7ca2e69bb51d13faf59bd04f0c65a1d6c1e" + integrity sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng== + dependencies: + loose-envify "^1.3.1" + object-assign "^4.1.1" + +csstype@^3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.4.tgz#b156d7be03b84ff425c9a0a4b1e5f4da9c5ca888" + integrity sha512-xc8DUsCLmjvCfoD7LTGE0ou2MIWLx0K9RCZwSHMOdynqRsP4MtUcLeqh1HcQ2dInwDTqn+3CE0/FZh1et+p4jA== + +events@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= + +ieee754@1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +ieee754@^1.1.4: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +isarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +loose-envify@^1.3.1, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-types@^2.1.24: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +object-assign@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-2.1.1.tgz#43c36e5d569ff8e4816c4efa8be02d26967c18aa" + integrity sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo= + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +prop-types@^15.5.8: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +react-is@^16.8.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= + +sax@>=0.6.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +typescript@^3.7.5: + version "3.9.7" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" + integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== + +url@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +uuid@^3.1.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +xml2js@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=