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=