├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── coffeelint.json ├── gulpfile.js ├── index.js ├── lib └── index.js ├── logotype.png ├── package.json ├── src ├── lib │ └── index.coffee └── test │ └── index.coffee └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .npmignore -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased] 4 | 5 | ## [0.1.2] - 2015.12.07 6 | ### Added 7 | - Support for condition matching (all types) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Swing Development sp. z o.o. 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logotype](logotype.png) 2 | 3 | # Node Amazon S3 Browser Direct Upload 4 | 5 | `s3-browser-direct-upload` is a node.js library which gives you the ability to upload files to Amazon S3 easily using: 6 | 7 | * browser/mobile-based straight-to-S3 uploads using POST 8 | * S3.upload method 9 | * S3.putObject method 10 | * works with v4 signature version 11 | 12 | In addition you can limit allowed file extensions. 13 | 14 | 15 | ![amazon s3 browser post](http://docs.aws.amazon.com/AmazonS3/latest/dev/images/s3_post.png)
16 | Image source:http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingHTTPPOST.html 17 | 18 | ## Install 19 | ```npm install s3-browser-direct-upload``` 20 | 21 | ## Usage examples 22 | 23 | ### Create a client 24 | ``` 25 | var s3BrowserDirectUpload = require('s3-browser-direct-upload'); 26 | 27 | var s3clientOptions = { 28 | accessKeyId: 'accessKeyId', // required 29 | secretAccessKey: 'secretAccessKey', // required 30 | region: 'eu-central-1', // required 31 | signatureVersion: 'v4' // optional 32 | }; 33 | 34 | var allowedTypes = ['jpg', 'png']; 35 | 36 | var s3client = new s3BrowserDirectUpload(s3clientOptions, allowedTypes); // allowedTypes is optional 37 | ``` 38 | For more information check API documentation. 39 | 40 | ### Upload using s3client.uploadPostForm (Browser-based uploads using POST) 41 | ``` 42 | var uploadPostFormOptions = { 43 | key: 'filename.ext', // required 44 | bucket: 'bucketName', // required 45 | extension: 'ext', // optional (pass if You want to check with allowed extensions or set ContentType) 46 | acl: 'public-read', // optional, default: 'public-read' 47 | expires: new Date('2018-01-01'), // optional (date object with expiration date for urls), default: +60 minutes 48 | algorithm: 'AWS4-HMAC-SHA256', // optional, default: 'AWS4-HMAC-SHA256' 49 | region: 'eu-central-1', // optional, default: s3client.region 50 | conditionMatching: [ 51 | {"success_action_redirect": "http://google.com"}, 52 | {"x-amz-meta-metadatafield": ""}, 53 | ["starts-with", "$key", "user/betty/"], 54 | ["condition", "key", "pattern"] 55 | ] // optional 56 | }; 57 | 58 | s3client.uploadPostForm(uploadPostFormOptions, function(err, params){ 59 | console.log(params); // params contain all the data required to build browser-based form for direct upload (check API Documentation) 60 | }); 61 | ``` 62 | For more information check API documentation. 63 | 64 | ### Upload using s3client.upload (S3#upload) 65 | ``` 66 | var fs = require('fs'); 67 | 68 | var uploadOptions = { 69 | data: fs.createReadStream('/path/to/a/file'), // required 70 | key: 'filename.ext', // required 71 | bucket: 'bucketName', // required 72 | extension: 'ext', // optional (pass if You want to check with allowed extensions or set ContentType) 73 | acl: 'public-read' // optional 74 | }; 75 | 76 | s3client.upload(uploadOptions, function(err, url) { 77 | console.log(url); // url to uploaded data 78 | }); 79 | ``` 80 | For more information check API documentation. 81 | 82 | ### Upload using s3client.put (S3#putObject) 83 | ``` 84 | var uploadOptions = { 85 | key: 'filename.ext', // required 86 | bucket: 'bucketName', // required 87 | extension: 'ext', // optional (pass if You want to check with allowed extensions or set ContentType) 88 | acl: 'public-read', // optional 89 | expires: new Date('2018-01-01') // optional (date object with expiration date for urls) 90 | }; 91 | 92 | s3client.put(uploadOptions, function(err, data){ 93 | console.log(data); // data contains public url and signed url 94 | }); 95 | ``` 96 | For more information check API documentation. 97 | 98 | ## API Documentation 99 | ### s3client constructor parameters 100 | `options` (JSON or AWS.Config object): 101 | 102 | * accessKeyId (String, required) 103 | * secretAccessKey (String, required) 104 | * region (String, required) 105 | * signatureVersion (String, optional) 106 | * maxRetries (Integer, optional) 107 | * maxRedirects (Integer, optional) 108 | * systemClockOffset (Number, optional) 109 | * sslEnabled (Boolean, optional) 110 | * paramValidation (Boolean, optional) 111 | * computeChecksums (Boolean, optional) 112 | * convertResponseTypes (Boolean, optional) 113 | * s3ForcePathStyle (Boolean, optional) 114 | * s3BucketEndpoint (Boolean, optional) 115 | * httpOptions (JSON {proxy, agent, timeout, xhrAsync, xhrWithCredentials}, optional) 116 | * apiVersions (JSON {versions}, optional) 117 | * apiVersion (String/Date, optional) 118 | * sessionToken (AWS.Credentials, optional) 119 | * credentials (AWS.Credentials, optional) 120 | * credentialProvider (AWS.CredentialsProviderChain, optional) 121 | * logger (Logger object with #write,#log methods, optional) 122 | 123 | `arrayOfAllowedTypes` (array of strings ex. ["jpg"]) 124 | 125 | ### s3client.uploadPostForm 126 | `options` (JSON): 127 | 128 | * key (String, required) 129 | * bucket (String, required) 130 | * extension (String, optional) 131 | * expires (String/Date, optional, default: +60 minutes) 132 | * acl (String, optional, default: 'public-read') 133 | * contentLength (Integer, optional) 134 | * algorithm (String, optional, default: 'AWS4-HMAC-SHA256') 135 | * region (String, optional, default: s3client.region) 136 | * conditionMatching (Array, optional) 137 | 138 | `callback` (err, params), returned params (JSON): 139 | 140 | * params: 141 | - key 142 | - acl 143 | - x-amz-algorithm 144 | - x-amz-credential 145 | - x-amz-date 146 | - policy 147 | - x-amz-signature 148 | - content-type 149 | * public_url 150 | * form_url 151 | * conditions 152 | 153 | ### s3client.upload 154 | `options` (JSON): 155 | 156 | * data (File, String, Buffer, ReadableStream, ..., required) 157 | * key (String, required) 158 | * bucket (String, required) 159 | * extension (String, optional) 160 | * expires (String/Date, optional) 161 | * acl (String, optional) 162 | * contentLength (Integer, optional) 163 | 164 | `callback` (err, url), returned url (String) 165 | 166 | ### s3client.put 167 | `options` (JSON): 168 | 169 | * key (String, required) 170 | * bucket (String, required) 171 | * extension (String, optional) 172 | * expires (String/Date, optional) 173 | * acl (String, optional) 174 | 175 | `callback` (err, urls), returned urls (JSON): 176 | 177 | * urls: 178 | - signed_url 179 | - public_url 180 | 181 | ## License 182 | MIT 183 | 184 | Copyright Gabriel Oczkowski 185 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "no_tabs" : { 3 | "level" : "warn" 4 | }, 5 | 6 | "no_trailing_whitespace" : { 7 | "level" : "warn" 8 | }, 9 | 10 | "max_line_length" : { 11 | "value": 150, 12 | "level" : "warn" 13 | }, 14 | 15 | "camel_case_classes" : { 16 | "level" : "warn" 17 | }, 18 | 19 | "indentation" : { 20 | "value" : 2, 21 | "level" : "warn" 22 | }, 23 | 24 | "no_trailing_semicolons" : { 25 | "level" : "warn" 26 | }, 27 | 28 | "no_throwing_strings" : { 29 | "level" : "ignore" 30 | }, 31 | 32 | "no_backticks": { 33 | "level": "ignore" 34 | }, 35 | 36 | "line_endings" : { 37 | "value" : "unix", 38 | "level" : "ignore" 39 | } 40 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | gutil = require('gulp-util') 3 | coffee = require('gulp-coffee'); 4 | watch = require('gulp-watch'); 5 | merge = require('merge-stream'); 6 | coffeelint = require('gulp-coffeelint'); 7 | plumber = require('gulp-plumber'); 8 | map = require('map-stream'); 9 | concat = require('gulp-concat'); 10 | uglify = require('gulp-uglify'); 11 | scss = require('gulp-sass'); 12 | runSeq = require('run-sequence'); 13 | chmod = require('gulp-chmod'); 14 | fs = require('fs'); 15 | del = require('del'); 16 | sourcemaps = require('gulp-sourcemaps'); 17 | glob = require('glob'); 18 | runSequence = require('run-sequence'); 19 | 20 | var onError = function (err) { 21 | gutil.beep(); 22 | gutil.log(err); 23 | }; 24 | 25 | 26 | gulp.task("clean", function(cb) { 27 | del(['lib'], cb); 28 | }); 29 | 30 | gulp.task('compile_src', function() { 31 | return merge( 32 | gulp.src('./src/**/*.coffee') 33 | .pipe(plumber({errorHandler: onError})) 34 | .pipe(coffeelint()) 35 | .pipe(coffeelint.reporter()) 36 | .pipe(sourcemaps.init()) 37 | .pipe(coffee({bare: true})) 38 | .pipe(sourcemaps.write()) 39 | .pipe(gulp.dest('./')), 40 | gulp.src(['./src/**/*', '!./src/**/*.coffee']) 41 | .pipe(plumber({errorHandler: onError})) 42 | .pipe(gulp.dest('./'))) 43 | }); 44 | 45 | gulp.task('compile', function(callback) { 46 | runSequence('clean', 47 | ['compile_src'], 48 | callback); 49 | }); 50 | 51 | gulp.task("watch", function() { 52 | watch(glob.sync('src/**/*.coffee'), function(files, cb) { 53 | gulp.start('compile_src', cb); 54 | }); 55 | }); 56 | 57 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib') -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var S3Client, _, crypto, mime, moment, 2 | indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 3 | 4 | _ = require('lodash'); 5 | 6 | mime = require('mime'); 7 | 8 | moment = require('moment'); 9 | 10 | crypto = require('crypto'); 11 | 12 | S3Client = (function() { 13 | function S3Client(options, arrAllowedDataExtensions) { 14 | var aws; 15 | if (options == null) { 16 | options = {}; 17 | } 18 | aws = require('aws-sdk'); 19 | if (!(options instanceof aws.Config)) { 20 | this._checkOptions(options); 21 | } 22 | aws.config.update(options); 23 | this.s3 = new aws.S3(); 24 | this.arrAllowedDataExtensions = null; 25 | if (arrAllowedDataExtensions && this._checkAllowedDataExtensions(arrAllowedDataExtensions)) { 26 | this.arrAllowedDataExtensions = arrAllowedDataExtensions; 27 | } 28 | } 29 | 30 | S3Client.prototype.uploadPostForm = function(options, cb) { 31 | var acl, algorithm, arrAlgorithm, bucket, conditionMatching, contentLength, contentType, dateKey, dateLongPolicy, dateRegionKey, dateRegionServiceKey, dateShortPolicy, expires, extension, hashalg, key, policy, policyDoc, ref, ref1, ref2, ref3, ref4, ref5, ref6, region, signature, signingKey, sigver, stream; 32 | if (options == null) { 33 | options = {}; 34 | } 35 | if (!cb) { 36 | throw new Error('Callback is required'); 37 | } 38 | extension = options.extension, key = options.key, bucket = options.bucket, expires = options.expires, acl = options.acl, contentLength = options.contentLength, algorithm = options.algorithm, region = options.region, conditionMatching = options.conditionMatching; 39 | key = options.key; 40 | bucket = options.bucket; 41 | extension = (ref = options.extension) != null ? ref : null; 42 | expires = (ref1 = options.expires) != null ? ref1 : moment.utc().add(60, 'minutes').toDate(); 43 | acl = (ref2 = options.acl) != null ? ref2 : 'public-read'; 44 | contentLength = (ref3 = options.contentLength) != null ? ref3 : null; 45 | algorithm = (ref4 = options.algorithm) != null ? ref4 : 'AWS4-HMAC-SHA256'; 46 | region = (ref5 = options.region) != null ? ref5 : this.region; 47 | conditionMatching = (ref6 = options.conditionMatching) != null ? ref6 : null; 48 | if (!(key && bucket)) { 49 | return cb(new Error('key and bucket are required')); 50 | } 51 | if (extension) { 52 | contentType = this._checkDataExtension(extension); 53 | if (!contentType) { 54 | return cb(new Error('Data extension not allowed')); 55 | } 56 | } 57 | if (algorithm.split('-').length === 3) { 58 | arrAlgorithm = algorithm.split('-'); 59 | sigver = arrAlgorithm[0]; 60 | hashalg = arrAlgorithm[2].toLowerCase(); 61 | } else { 62 | sigver = "AWS4"; 63 | hashalg = "sha256"; 64 | } 65 | policyDoc = {}; 66 | if (expires && _.isDate(expires)) { 67 | policyDoc["expiration"] = moment.utc(expires).format("YYYY-MM-DD[T]HH:mm:ss[Z]"); 68 | } 69 | policyDoc["conditions"] = []; 70 | dateShortPolicy = moment.utc().format('YYYYMMDD'); 71 | dateLongPolicy = moment.utc().format('YYYYMMDD[T]HHmmss[Z]'); 72 | policyDoc.conditions.push({ 73 | 'bucket': bucket 74 | }); 75 | policyDoc.conditions.push(['starts-with', '$key', key]); 76 | policyDoc.conditions.push({ 77 | 'acl': acl 78 | }); 79 | if (contentType) { 80 | policyDoc.conditions.push(['starts-with', '$Content-Type', contentType]); 81 | } 82 | if (contentLength) { 83 | policyDoc.conditions.push(['content-length-range', 0, contentLength]); 84 | } 85 | policyDoc.conditions.push({ 86 | "x-amz-algorithm": algorithm 87 | }); 88 | policyDoc.conditions.push({ 89 | "x-amz-credential": this.accessKeyId + "/" + dateShortPolicy + "/" + region + "/s3/aws4_request" 90 | }); 91 | policyDoc.conditions.push({ 92 | "x-amz-date": dateLongPolicy 93 | }); 94 | if (conditionMatching && _.isArray(conditionMatching)) { 95 | policyDoc.conditions = _.union(conditionMatching, policyDoc.conditions); 96 | } 97 | dateKey = crypto.createHmac(hashalg, "" + sigver + this.secretAccessKey).update(dateShortPolicy).digest(); 98 | dateRegionKey = crypto.createHmac(hashalg, dateKey).update(region).digest(); 99 | dateRegionServiceKey = crypto.createHmac(hashalg, dateRegionKey).update('s3').digest(); 100 | signingKey = crypto.createHmac(hashalg, dateRegionServiceKey).update((sigver.toLowerCase()) + "_request").digest(); 101 | policy = new Buffer(JSON.stringify(policyDoc)).toString('base64'); 102 | signature = crypto.createHmac(hashalg, signingKey).update(policy).digest('hex'); 103 | stream = {}; 104 | stream['params'] = { 105 | "key": key, 106 | "acl": acl, 107 | "x-amz-algorithm": algorithm, 108 | "x-amz-credential": this.accessKeyId + "/" + dateShortPolicy + "/" + region + "/s3/" + (sigver.toLowerCase()) + "_request", 109 | "x-amz-date": dateLongPolicy, 110 | "policy": policy, 111 | "x-amz-signature": signature 112 | }; 113 | if (contentType) { 114 | stream.params['content-type'] = contentType; 115 | } 116 | if (conditionMatching) { 117 | stream['conditions'] = conditionMatching; 118 | } 119 | stream['public_url'] = "https://" + bucket + ".s3.amazonaws.com/" + key; 120 | stream['form_url'] = "https://" + bucket + ".s3.amazonaws.com/"; 121 | return cb(null, stream); 122 | }; 123 | 124 | S3Client.prototype.upload = function(options, cb) { 125 | var acl, bucket, contentLength, contentType, data, expires, extension, key, params, ref, ref1, ref2, ref3; 126 | if (options == null) { 127 | options = {}; 128 | } 129 | if (!cb) { 130 | throw new Error('Callback is required'); 131 | } 132 | data = options.data, extension = options.extension, key = options.key, bucket = options.bucket, expires = options.expires, acl = options.acl, contentLength = options.contentLength; 133 | data = options.data; 134 | key = options.key; 135 | bucket = options.bucket; 136 | extension = (ref = options.extension) != null ? ref : null; 137 | expires = (ref1 = options.expires) != null ? ref1 : null; 138 | acl = (ref2 = options.acl) != null ? ref2 : null; 139 | contentLength = (ref3 = options.contentLength) != null ? ref3 : null; 140 | if (!(data && key && bucket)) { 141 | return cb(new Error('data, key and bucket are required')); 142 | } 143 | params = { 144 | Bucket: bucket, 145 | Key: key, 146 | Body: data 147 | }; 148 | if (extension) { 149 | contentType = this._checkDataExtension(extension); 150 | if (!contentType) { 151 | return cb(new Error('Data extension not allowed')); 152 | } 153 | params["ContentType"] = contentType; 154 | } 155 | if (expires && _.isDate(expires)) { 156 | params["Expires"] = moment.utc(expires); 157 | } 158 | if (acl) { 159 | params["ACL"] = acl; 160 | } 161 | if (contentLength) { 162 | params["ContentLength"] = contentLength; 163 | } 164 | return this.s3.upload(params, function(err, data) { 165 | if (err) { 166 | return cb(err); 167 | } 168 | return cb(null, "https://" + bucket + ".s3.amazonaws.com/" + key); 169 | }); 170 | }; 171 | 172 | S3Client.prototype.put = function(options, cb) { 173 | var acl, bucket, contentLength, contentType, expires, extension, key, params, ref, ref1, ref2; 174 | if (options == null) { 175 | options = {}; 176 | } 177 | if (!cb) { 178 | throw new Error('Callback is required'); 179 | } 180 | extension = options.extension, key = options.key, bucket = options.bucket, expires = options.expires, acl = options.acl, contentLength = options.contentLength; 181 | key = options.key; 182 | bucket = options.bucket; 183 | extension = (ref = options.extension) != null ? ref : null; 184 | expires = (ref1 = options.expires) != null ? ref1 : null; 185 | acl = (ref2 = options.acl) != null ? ref2 : null; 186 | if (!(key && bucket)) { 187 | return cb(new Error('key and bucket are required')); 188 | } 189 | params = { 190 | Bucket: bucket, 191 | Key: key 192 | }; 193 | if (extension) { 194 | contentType = this._checkDataExtension(extension); 195 | if (!contentType) { 196 | return cb(new Error('Data extension not allowed')); 197 | } 198 | params["ContentType"] = contentType; 199 | } 200 | if (expires && _.isDate(expires)) { 201 | params["Expires"] = moment.utc(expires); 202 | } 203 | if (acl) { 204 | params["ACL"] = acl; 205 | } 206 | return this.s3.getSignedUrl("putObject", params, function(err, data) { 207 | var put; 208 | if (err) { 209 | return cb(err); 210 | } 211 | put = { 212 | 'signed_url': data, 213 | 'public_url': "https://" + bucket + ".s3.amazonaws.com/" + key 214 | }; 215 | return cb(null, put); 216 | }); 217 | }; 218 | 219 | S3Client.prototype._checkDataExtension = function(dataExtension) { 220 | if (!dataExtension || (this.arrAllowedDataExtensions && indexOf.call(this.arrAllowedDataExtensions, dataExtension) < 0)) { 221 | return false; 222 | } 223 | return mime.lookup(dataExtension); 224 | }; 225 | 226 | S3Client.prototype._checkAllowedDataExtensions = function(arrAllowedDataExtensions) { 227 | var ext; 228 | if (!arrAllowedDataExtensions) { 229 | return false; 230 | } 231 | if (!_.isArray(arrAllowedDataExtensions)) { 232 | throw new Error("Allowed data extensions must be array of strings"); 233 | } 234 | for (ext in arrAllowedDataExtensions) { 235 | if (!_.isString(ext)) { 236 | throw new Error("Extensions must be a strings"); 237 | } 238 | } 239 | return true; 240 | }; 241 | 242 | S3Client.prototype._checkOptions = function(options) { 243 | if (options == null) { 244 | options = {}; 245 | } 246 | this.accessKeyId = options.accessKeyId, this.secretAccessKey = options.secretAccessKey, this.region = options.region, this.signatureVersion = options.signatureVersion, this.maxRetries = options.maxRetries, this.maxRedirects = options.maxRedirects, this.systemClockOffset = options.systemClockOffset, this.sslEnabled = options.sslEnabled, this.paramValidation = options.paramValidation, this.computeChecksums = options.computeChecksums, this.convertResponseTypes = options.convertResponseTypes, this.s3ForcePathStyle = options.s3ForcePathStyle, this.s3BucketEndpoint = options.s3BucketEndpoint, this.apiVersion = options.apiVersion, this.httpOptions = options.httpOptions, this.apiVersions = options.apiVersions, this.sessionToken = options.sessionToken, this.credentials = options.credentials, this.credentialProvider = options.credentialProvider, this.logger = options.logger; 247 | if (!this.accessKeyId) { 248 | throw new Error("accessKeyId is required"); 249 | } 250 | if (!this.secretAccessKey) { 251 | throw new Error("secretAccessKey is required"); 252 | } 253 | if (!this.region) { 254 | throw new Error("region is required"); 255 | } 256 | if (!_.isString(this.accessKeyId)) { 257 | throw new Error("accessKeyId must be a string"); 258 | } 259 | if (!_.isString(this.secretAccessKey)) { 260 | throw new Error("secretAccessKey must be a string"); 261 | } 262 | if (!_.isString(this.region)) { 263 | throw new Error("region must be a string"); 264 | } 265 | if (this.signatureVersion && !_.isString(this.signatureVersion)) { 266 | throw new Error("signatureVersion must be a string"); 267 | } 268 | if (this.maxRetries && !_.isInteger(this.maxRetries)) { 269 | throw new Error('maxRetries must be a integer'); 270 | } 271 | if (this.maxRedirects && !_.isInteger(this.maxRedirects)) { 272 | throw new Error('maxRedirects must be a integer'); 273 | } 274 | if (this.systemClockOffset && !_.isNumber(this.systemClockOffset)) { 275 | throw new Error('systemClockOffset must be a number'); 276 | } 277 | if (this.sslEnabled && !_.isBoolean(this.sslEnabled)) { 278 | throw new Error('sslEnabled must be a boolean'); 279 | } 280 | if (this.paramValidation && !_.isBoolean(this.paramValidation)) { 281 | throw new Error('paramValidation must be a boolean'); 282 | } 283 | if (this.computeChecksums && !_.isBoolean(this.computeChecksums)) { 284 | throw new Error('computeChecksums must be a boolean'); 285 | } 286 | if (this.convertResponseTypes && !_.isBoolean(this.convertResponseTypes)) { 287 | throw new Error('convertResponseTypes must be a boolean'); 288 | } 289 | if (this.s3ForcePathStyle && !_.isBoolean(this.s3ForcePathStyle)) { 290 | throw new Error('s3ForcePathStyle must be a boolean'); 291 | } 292 | if (this.s3BucketEndpoint && !_.isBoolean(this.s3BucketEndpoint)) { 293 | throw new Error('s3BucketEndpoint must be a boolean'); 294 | } 295 | if (this.httpOptions && !_.isPlainObject(this.httpOptions)) { 296 | throw new Error('httpOptions must be a dict with params: proxy, agent, timeout, xhrAsync, xhrWithCredentials'); 297 | } 298 | if (this.apiVersions && !_.isPlainObject(this.apiVersions)) { 299 | throw new Error('apiVersions must be a dict with versions'); 300 | } 301 | if (this.apiVersion && !(_.isString(this.apiVersion || _.isDate(this.apiVersion)))) { 302 | throw new Error('apiVersion must be a string or date'); 303 | } 304 | if (this.sessionToken && !this.sessionToken instanceof aws.Credentials) { 305 | throw new Error('sessionToken must be a AWS.Credentials'); 306 | } 307 | if (this.credentials && !this.credentials instanceof aws.Credentials) { 308 | throw new Error('credentials must be a AWS.Credentials'); 309 | } 310 | if (this.credentialProvider && !this.credentialProvider instanceof aws.CredentialsProviderChain) { 311 | throw new Error('credentialProvider must be a AWS.CredentialsProviderChain'); 312 | } 313 | if (this.logger && !(this.logger.write && this.logger.log)) { 314 | throw new Error('logger must have #write or #log methods'); 315 | } 316 | }; 317 | 318 | return S3Client; 319 | 320 | })(); 321 | 322 | module.exports = S3Client; 323 | 324 | //# sourceMappingURL=data:application/json;base64,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Iiwic291cmNlUm9vdCI6Ii9zb3VyY2UvIiwic291cmNlc0NvbnRlbnQiOlsiIyBzMy1icm93c2VyLWRpcmVjdC11cGxvYWRcbl8gICAgICAgPSByZXF1aXJlKCdsb2Rhc2gnKVxubWltZSAgICA9IHJlcXVpcmUoJ21pbWUnKVxubW9tZW50ICA9IHJlcXVpcmUoJ21vbWVudCcpXG5jcnlwdG8gID0gcmVxdWlyZSgnY3J5cHRvJylcblxuXG5jbGFzcyBTM0NsaWVudFxuICBjb25zdHJ1Y3RvcjogKG9wdGlvbnMgPSB7fSwgYXJyQWxsb3dlZERhdGFFeHRlbnNpb25zKSAtPlxuICAgIGF3cyA9IHJlcXVpcmUoJ2F3cy1zZGsnKVxuXG4gICAgQF9jaGVja09wdGlvbnMgb3B0aW9ucyB1bmxlc3Mgb3B0aW9ucyBpbnN0YW5jZW9mIGF3cy5Db25maWdcbiAgICBhd3MuY29uZmlnLnVwZGF0ZSBvcHRpb25zXG5cbiAgICBAczMgPSBuZXcgYXdzLlMzKClcblxuICAgIEBhcnJBbGxvd2VkRGF0YUV4dGVuc2lvbnMgPSBudWxsXG4gICAgaWYgYXJyQWxsb3dlZERhdGFFeHRlbnNpb25zIGFuZCBAX2NoZWNrQWxsb3dlZERhdGFFeHRlbnNpb25zIGFyckFsbG93ZWREYXRhRXh0ZW5zaW9uc1xuICAgICAgQGFyckFsbG93ZWREYXRhRXh0ZW5zaW9ucyA9IGFyckFsbG93ZWREYXRhRXh0ZW5zaW9uc1xuXG5cbiAgIyBCcm93c2VyIGZvcm0gcG9zdCBwYXJhbXMgZm9yIHVwbG9hZGluZ1xuICB1cGxvYWRQb3N0Rm9ybTogKG9wdGlvbnMgPSB7fSwgY2IpIC0+XG4gICAgdGhyb3cgbmV3IEVycm9yICdDYWxsYmFjayBpcyByZXF1aXJlZCcgdW5sZXNzIGNiXG4gICAgeyBleHRlbnNpb24sIGtleSwgYnVja2V0LCBleHBpcmVzLCBhY2wsIGNvbnRlbnRMZW5ndGgsIGFsZ29yaXRobSwgcmVnaW9uLCBjb25kaXRpb25NYXRjaGluZyB9ID0gb3B0aW9uc1xuICAgIGtleSA9IG9wdGlvbnMua2V5XG4gICAgYnVja2V0ID0gb3B0aW9ucy5idWNrZXRcbiAgICBleHRlbnNpb24gPSBvcHRpb25zLmV4dGVuc2lvbiA/IG51bGxcbiAgICBleHBpcmVzID0gb3B0aW9ucy5leHBpcmVzID8gbW9tZW50LnV0YygpLmFkZCg2MCwgJ21pbnV0ZXMnKS50b0RhdGUoKVxuICAgIGFjbCA9IG9wdGlvbnMuYWNsID8gJ3B1YmxpYy1yZWFkJ1xuICAgIGNvbnRlbnRMZW5ndGggPSBvcHRpb25zLmNvbnRlbnRMZW5ndGggPyBudWxsXG4gICAgYWxnb3JpdGhtID0gb3B0aW9ucy5hbGdvcml0aG0gPyAnQVdTNC1ITUFDLVNIQTI1NidcbiAgICByZWdpb24gPSBvcHRpb25zLnJlZ2lvbiA/IEByZWdpb25cbiAgICBjb25kaXRpb25NYXRjaGluZyA9IG9wdGlvbnMuY29uZGl0aW9uTWF0Y2hpbmcgPyBudWxsXG5cbiAgICAjIEBUT0RPIG9wdGlvbnMgdHlwZSBjaGVja1xuICAgIHVubGVzcyBrZXkgYW5kIGJ1Y2tldFxuICAgICAgcmV0dXJuIGNiIG5ldyBFcnJvciAna2V5IGFuZCBidWNrZXQgYXJlIHJlcXVpcmVkJ1xuXG4gICAgaWYgZXh0ZW5zaW9uXG4gICAgICBjb250ZW50VHlwZSA9IEBfY2hlY2tEYXRhRXh0ZW5zaW9uIGV4dGVuc2lvblxuICAgICAgcmV0dXJuIGNiIG5ldyBFcnJvciAnRGF0YSBleHRlbnNpb24gbm90IGFsbG93ZWQnIHVubGVzcyBjb250ZW50VHlwZVxuXG4gICAgaWYgYWxnb3JpdGhtLnNwbGl0KCctJykubGVuZ3RoID09IDNcbiAgICAgIGFyckFsZ29yaXRobSA9IGFsZ29yaXRobS5zcGxpdCgnLScpXG4gICAgICBzaWd2ZXIgPSBhcnJBbGdvcml0aG1bMF1cbiAgICAgIGhhc2hhbGcgPSBhcnJBbGdvcml0aG1bMl0udG9Mb3dlckNhc2UoKVxuICAgIGVsc2VcbiAgICAgIHNpZ3ZlciA9IFwiQVdTNFwiXG4gICAgICBoYXNoYWxnID0gXCJzaGEyNTZcIlxuXG4gICAgcG9saWN5RG9jID0ge31cblxuICAgIHBvbGljeURvY1tcImV4cGlyYXRpb25cIl0gPSBtb21lbnQudXRjKGV4cGlyZXMpLmZvcm1hdChcIllZWVktTU0tRERbVF1ISDpNTTpTU1taXVwiKSBpZiBleHBpcmVzIGFuZCBfLmlzRGF0ZSBleHBpcmVzXG4gICAgcG9saWN5RG9jW1wiY29uZGl0aW9uc1wiXSA9IFtdXG5cbiAgICBkYXRlU2hvcnRQb2xpY3kgPSBtb21lbnQudXRjKCkuZm9ybWF0KCdZWVlZTU1ERCcpXG4gICAgZGF0ZUxvbmdQb2xpY3kgPSBtb21lbnQudXRjKCkuZm9ybWF0KCdZWVlZTU1ERFtUXUhITU1TU1taXScpXG5cbiAgICBwb2xpY3lEb2MuY29uZGl0aW9ucy5wdXNoIHsgJ2J1Y2tldCc6IGJ1Y2tldCB9XG4gICAgcG9saWN5RG9jLmNvbmRpdGlvbnMucHVzaCBbICdzdGFydHMtd2l0aCcsICcka2V5Jywga2V5IF1cbiAgICBwb2xpY3lEb2MuY29uZGl0aW9ucy5wdXNoIHsgJ2FjbCc6IGFjbCB9XG4gICAgcG9saWN5RG9jLmNvbmRpdGlvbnMucHVzaCBbICdzdGFydHMtd2l0aCcsICckQ29udGVudC1UeXBlJywgY29udGVudFR5cGUgXSBpZiBjb250ZW50VHlwZVxuICAgIHBvbGljeURvYy5jb25kaXRpb25zLnB1c2ggWyAnY29udGVudC1sZW5ndGgtcmFuZ2UnLCAwLCBjb250ZW50TGVuZ3RoIF0gaWYgY29udGVudExlbmd0aFxuICAgIHBvbGljeURvYy5jb25kaXRpb25zLnB1c2ggeyBcIngtYW16LWFsZ29yaXRobVwiOiBhbGdvcml0aG0gfVxuICAgIHBvbGljeURvYy5jb25kaXRpb25zLnB1c2ggeyBcIngtYW16LWNyZWRlbnRpYWxcIjogXCIje0BhY2Nlc3NLZXlJZH0vI3tkYXRlU2hvcnRQb2xpY3l9LyN7cmVnaW9ufS9zMy9hd3M0X3JlcXVlc3RcIiB9XG4gICAgcG9saWN5RG9jLmNvbmRpdGlvbnMucHVzaCB7IFwieC1hbXotZGF0ZVwiOiBkYXRlTG9uZ1BvbGljeX1cblxuICAgIGlmIGNvbmRpdGlvbk1hdGNoaW5nIGFuZCBfLmlzQXJyYXkgY29uZGl0aW9uTWF0Y2hpbmdcbiAgICAgIHBvbGljeURvYy5jb25kaXRpb25zID0gXy51bmlvbiBjb25kaXRpb25NYXRjaGluZywgcG9saWN5RG9jLmNvbmRpdGlvbnNcblxuICAgIGRhdGVLZXkgPSBjcnlwdG8uY3JlYXRlSG1hYyhoYXNoYWxnLCBcIiN7c2lndmVyfSN7QHNlY3JldEFjY2Vzc0tleX1cIikudXBkYXRlKGRhdGVTaG9ydFBvbGljeSkuZGlnZXN0KClcbiAgICBkYXRlUmVnaW9uS2V5ID0gY3J5cHRvLmNyZWF0ZUhtYWMoaGFzaGFsZywgZGF0ZUtleSkudXBkYXRlKHJlZ2lvbikuZGlnZXN0KClcbiAgICBkYXRlUmVnaW9uU2VydmljZUtleSA9IGNyeXB0by5jcmVhdGVIbWFjKGhhc2hhbGcsIGRhdGVSZWdpb25LZXkpLnVwZGF0ZSgnczMnKS5kaWdlc3QoKVxuICAgIHNpZ25pbmdLZXkgPSBjcnlwdG8uY3JlYXRlSG1hYyhoYXNoYWxnLCBkYXRlUmVnaW9uU2VydmljZUtleSkudXBkYXRlKFwiI3tzaWd2ZXIudG9Mb3dlckNhc2UoKX1fcmVxdWVzdFwiKS5kaWdlc3QoKVxuICAgIHBvbGljeSA9IG5ldyBCdWZmZXIoSlNPTi5zdHJpbmdpZnkocG9saWN5RG9jKSkudG9TdHJpbmcoJ2Jhc2U2NCcpXG4gICAgc2lnbmF0dXJlID0gY3J5cHRvLmNyZWF0ZUhtYWMoaGFzaGFsZyxzaWduaW5nS2V5KS51cGRhdGUocG9saWN5KS5kaWdlc3QoJ2hleCcpXG5cbiAgICBzdHJlYW0gPSB7fVxuICAgIHN0cmVhbVsncGFyYW1zJ10gPVxuICAgICAgXCJrZXlcIjoga2V5XG4gICAgICBcImFjbFwiOiBhY2xcbiAgICAgIFwieC1hbXotYWxnb3JpdGhtXCI6IGFsZ29yaXRobVxuICAgICAgXCJ4LWFtei1jcmVkZW50aWFsXCI6IFwiI3tAYWNjZXNzS2V5SWR9LyN7ZGF0ZVNob3J0UG9saWN5fS8je3JlZ2lvbn0vczMvI3tzaWd2ZXIudG9Mb3dlckNhc2UoKX1fcmVxdWVzdFwiXG4gICAgICBcIngtYW16LWRhdGVcIjogZGF0ZUxvbmdQb2xpY3lcbiAgICAgIFwicG9saWN5XCI6IHBvbGljeVxuICAgICAgXCJ4LWFtei1zaWduYXR1cmVcIjogc2lnbmF0dXJlXG4gICAgc3RyZWFtLnBhcmFtc1snY29udGVudC10eXBlJ10gPSBjb250ZW50VHlwZSBpZiBjb250ZW50VHlwZVxuICAgIHN0cmVhbVsnY29uZGl0aW9ucyddICA9IGNvbmRpdGlvbk1hdGNoaW5nIGlmIGNvbmRpdGlvbk1hdGNoaW5nXG4gICAgc3RyZWFtWydwdWJsaWNfdXJsJ10gID0gXCJodHRwczovLyN7YnVja2V0fS5zMy5hbWF6b25hd3MuY29tLyN7a2V5fVwiXG4gICAgc3RyZWFtWydmb3JtX3VybCddICAgID0gXCJodHRwczovLyN7YnVja2V0fS5zMy5hbWF6b25hd3MuY29tL1wiXG5cbiAgICBjYiBudWxsLCBzdHJlYW1cblxuXG4gICMgUzMudXBsb2FkXG4gIHVwbG9hZDogKG9wdGlvbnMgPSB7fSwgY2IpIC0+XG4gICAgdGhyb3cgbmV3IEVycm9yICdDYWxsYmFjayBpcyByZXF1aXJlZCcgdW5sZXNzIGNiXG4gICAgeyBkYXRhLCBleHRlbnNpb24sIGtleSwgYnVja2V0LCBleHBpcmVzLCBhY2wsIGNvbnRlbnRMZW5ndGggfSA9IG9wdGlvbnNcbiAgICBkYXRhID0gb3B0aW9ucy5kYXRhXG4gICAga2V5ID0gb3B0aW9ucy5rZXlcbiAgICBidWNrZXQgPSBvcHRpb25zLmJ1Y2tldFxuICAgIGV4dGVuc2lvbiA9IG9wdGlvbnMuZXh0ZW5zaW9uID8gbnVsbFxuICAgIGV4cGlyZXMgPSBvcHRpb25zLmV4cGlyZXMgPyBudWxsXG4gICAgYWNsID0gb3B0aW9ucy5hY2wgPyBudWxsXG4gICAgY29udGVudExlbmd0aCA9IG9wdGlvbnMuY29udGVudExlbmd0aCA/IG51bGxcbiAgICBcbiAgICAjIEBUT0RPIG9wdGlvbnMgdHlwZSBjaGVja1xuICAgIHVubGVzcyBkYXRhIGFuZCBrZXkgYW5kIGJ1Y2tldFxuICAgICAgcmV0dXJuIGNiIG5ldyBFcnJvciAnZGF0YSwga2V5IGFuZCBidWNrZXQgYXJlIHJlcXVpcmVkJ1xuXG4gICAgcGFyYW1zID1cbiAgICAgIEJ1Y2tldDogYnVja2V0XG4gICAgICBLZXk6IGtleVxuICAgICAgQm9keTogZGF0YVxuXG4gICAgaWYgZXh0ZW5zaW9uXG4gICAgICBjb250ZW50VHlwZSA9IEBfY2hlY2tEYXRhRXh0ZW5zaW9uIGV4dGVuc2lvblxuICAgICAgcmV0dXJuIGNiIG5ldyBFcnJvciAnRGF0YSBleHRlbnNpb24gbm90IGFsbG93ZWQnIHVubGVzcyBjb250ZW50VHlwZVxuICAgICAgcGFyYW1zW1wiQ29udGVudFR5cGVcIl0gPSBjb250ZW50VHlwZVxuXG4gICAgcGFyYW1zW1wiRXhwaXJlc1wiXSA9IG1vbWVudC51dGMoZXhwaXJlcykgaWYgZXhwaXJlcyBhbmQgXy5pc0RhdGUgZXhwaXJlc1xuICAgIHBhcmFtc1tcIkFDTFwiXSA9IGFjbCBpZiBhY2xcbiAgICBwYXJhbXNbXCJDb250ZW50TGVuZ3RoXCJdID0gY29udGVudExlbmd0aCBpZiBjb250ZW50TGVuZ3RoXG5cbiAgICBAczMudXBsb2FkIHBhcmFtcywgKGVyciwgZGF0YSkgLT5cbiAgICAgIHJldHVybiBjYiBlcnIgaWYgZXJyXG4gICAgICBjYiBudWxsLCBcImh0dHBzOi8vI3tidWNrZXR9LnMzLmFtYXpvbmF3cy5jb20vI3trZXl9XCJcblxuXG4gICMgUzMucHV0T2JqZWN0XG4gIHB1dDogKG9wdGlvbnMgPSB7fSwgY2IpIC0+XG4gICAgdGhyb3cgbmV3IEVycm9yICdDYWxsYmFjayBpcyByZXF1aXJlZCcgdW5sZXNzIGNiXG4gICAgeyBleHRlbnNpb24sIGtleSwgYnVja2V0LCBleHBpcmVzLCBhY2wsIGNvbnRlbnRMZW5ndGggfSA9IG9wdGlvbnNcbiAgICBrZXkgPSBvcHRpb25zLmtleVxuICAgIGJ1Y2tldCA9IG9wdGlvbnMuYnVja2V0XG4gICAgZXh0ZW5zaW9uID0gb3B0aW9ucy5leHRlbnNpb24gPyBudWxsXG4gICAgZXhwaXJlcyA9IG9wdGlvbnMuZXhwaXJlcyA/IG51bGxcbiAgICBhY2wgPSBvcHRpb25zLmFjbCA/IG51bGxcblxuICAgICMgQFRPRE8gb3B0aW9ucyB0eXBlIGNoZWNrXG4gICAgdW5sZXNzIGtleSBhbmQgYnVja2V0XG4gICAgICByZXR1cm4gY2IgbmV3IEVycm9yICdrZXkgYW5kIGJ1Y2tldCBhcmUgcmVxdWlyZWQnXG5cbiAgICBwYXJhbXMgPVxuICAgICAgQnVja2V0OiBidWNrZXRcbiAgICAgIEtleToga2V5XG5cbiAgICBpZiBleHRlbnNpb25cbiAgICAgIGNvbnRlbnRUeXBlID0gQF9jaGVja0RhdGFFeHRlbnNpb24gZXh0ZW5zaW9uXG4gICAgICByZXR1cm4gY2IgbmV3IEVycm9yICdEYXRhIGV4dGVuc2lvbiBub3QgYWxsb3dlZCcgdW5sZXNzIGNvbnRlbnRUeXBlXG4gICAgICBwYXJhbXNbXCJDb250ZW50VHlwZVwiXSA9IGNvbnRlbnRUeXBlXG5cbiAgICBwYXJhbXNbXCJFeHBpcmVzXCJdID0gbW9tZW50LnV0YyhleHBpcmVzKSBpZiBleHBpcmVzIGFuZCBfLmlzRGF0ZSBleHBpcmVzXG4gICAgcGFyYW1zW1wiQUNMXCJdID0gYWNsIGlmIGFjbFxuXG4gICAgQHMzLmdldFNpZ25lZFVybCBcInB1dE9iamVjdFwiLCBwYXJhbXMsIChlcnIsIGRhdGEpIC0+XG4gICAgICByZXR1cm4gY2IgZXJyIGlmIGVyclxuXG4gICAgICBwdXQgPVxuICAgICAgICAnc2lnbmVkX3VybCc6IGRhdGFcbiAgICAgICAgJ3B1YmxpY191cmwnOiBcImh0dHBzOi8vI3tidWNrZXR9LnMzLmFtYXpvbmF3cy5jb20vI3trZXl9XCJcblxuICAgICAgY2IgbnVsbCwgcHV0XG5cblxuICAjIENoZWNrIGRhdGEgdHlwZSBmcm9tIGFyckFsbG93ZWREYXRhRXh0ZW5zaW9uc1xuICBfY2hlY2tEYXRhRXh0ZW5zaW9uOiAoZGF0YUV4dGVuc2lvbikgLT5cbiAgICByZXR1cm4gZmFsc2UgaWYgbm90IGRhdGFFeHRlbnNpb24gb3IgKEBhcnJBbGxvd2VkRGF0YUV4dGVuc2lvbnMgYW5kIGRhdGFFeHRlbnNpb24gbm90IGluIEBhcnJBbGxvd2VkRGF0YUV4dGVuc2lvbnMpXG4gICAgcmV0dXJuIG1pbWUubG9va3VwIGRhdGFFeHRlbnNpb25cblxuXG4gICMgQ2hlY2sgYWxsb3dlZCBkYXRhIHR5cGVzXG4gIF9jaGVja0FsbG93ZWREYXRhRXh0ZW5zaW9uczogKGFyckFsbG93ZWREYXRhRXh0ZW5zaW9ucykgLT5cbiAgICByZXR1cm4gZmFsc2UgdW5sZXNzIGFyckFsbG93ZWREYXRhRXh0ZW5zaW9uc1xuXG4gICAgdW5sZXNzIF8uaXNBcnJheSBhcnJBbGxvd2VkRGF0YUV4dGVuc2lvbnNcbiAgICAgIHRocm93IG5ldyBFcnJvciBcIkFsbG93ZWQgZGF0YSBleHRlbnNpb25zIG11c3QgYmUgYXJyYXkgb2Ygc3RyaW5nc1wiXG5cbiAgICBmb3IgZXh0IG9mIGFyckFsbG93ZWREYXRhRXh0ZW5zaW9uc1xuICAgICAgdW5sZXNzIF8uaXNTdHJpbmcgZXh0XG4gICAgICAgIHRocm93IG5ldyBFcnJvciBcIkV4dGVuc2lvbnMgbXVzdCBiZSBhIHN0cmluZ3NcIlxuXG4gICAgcmV0dXJuIHRydWVcblxuXG4gICMgQ2hlY2sgb3B0aW9ucyBwYXJhbXNcbiAgX2NoZWNrT3B0aW9uczogKG9wdGlvbnMgPSB7fSkgLT5cbiAgICB7XG4gICAgICBAYWNjZXNzS2V5SWQsIEBzZWNyZXRBY2Nlc3NLZXksIEByZWdpb24sIEBzaWduYXR1cmVWZXJzaW9uLCBAbWF4UmV0cmllcywgQG1heFJlZGlyZWN0cywgQHN5c3RlbUNsb2NrT2Zmc2V0LFxuICAgICAgQHNzbEVuYWJsZWQsIEBwYXJhbVZhbGlkYXRpb24sIEBjb21wdXRlQ2hlY2tzdW1zLCBAY29udmVydFJlc3BvbnNlVHlwZXMsIEBzM0ZvcmNlUGF0aFN0eWxlLCBAczNCdWNrZXRFbmRwb2ludCxcbiAgICAgIEBhcGlWZXJzaW9uLCBAaHR0cE9wdGlvbnMsIEBhcGlWZXJzaW9ucywgQHNlc3Npb25Ub2tlbiwgQGNyZWRlbnRpYWxzLCBAY3JlZGVudGlhbFByb3ZpZGVyLCBAbG9nZ2VyXG4gICAgfSA9IG9wdGlvbnNcblxuICAgIHVubGVzcyBAYWNjZXNzS2V5SWRcbiAgICAgIHRocm93IG5ldyBFcnJvciBcImFjY2Vzc0tleUlkIGlzIHJlcXVpcmVkXCJcblxuICAgIHVubGVzcyBAc2VjcmV0QWNjZXNzS2V5XG4gICAgICB0aHJvdyBuZXcgRXJyb3IgXCJzZWNyZXRBY2Nlc3NLZXkgaXMgcmVxdWlyZWRcIlxuXG4gICAgdW5sZXNzIEByZWdpb25cbiAgICAgIHRocm93IG5ldyBFcnJvciBcInJlZ2lvbiBpcyByZXF1aXJlZFwiXG5cbiAgICB1bmxlc3MgXy5pc1N0cmluZyBAYWNjZXNzS2V5SWRcbiAgICAgIHRocm93IG5ldyBFcnJvciBcImFjY2Vzc0tleUlkIG11c3QgYmUgYSBzdHJpbmdcIlxuXG4gICAgdW5sZXNzIF8uaXNTdHJpbmcgQHNlY3JldEFjY2Vzc0tleVxuICAgICAgdGhyb3cgbmV3IEVycm9yIFwic2VjcmV0QWNjZXNzS2V5IG11c3QgYmUgYSBzdHJpbmdcIlxuXG4gICAgdW5sZXNzIF8uaXNTdHJpbmcgQHJlZ2lvblxuICAgICAgdGhyb3cgbmV3IEVycm9yIFwicmVnaW9uIG11c3QgYmUgYSBzdHJpbmdcIlxuXG4gICAgaWYgQHNpZ25hdHVyZVZlcnNpb24gYW5kIG5vdCBfLmlzU3RyaW5nIEBzaWduYXR1cmVWZXJzaW9uXG4gICAgICB0aHJvdyBuZXcgRXJyb3IgXCJzaWduYXR1cmVWZXJzaW9uIG11c3QgYmUgYSBzdHJpbmdcIlxuXG4gICAgaWYgQG1heFJldHJpZXMgYW5kIG5vdCBfLmlzSW50ZWdlciBAbWF4UmV0cmllc1xuICAgICAgdGhyb3cgbmV3IEVycm9yICdtYXhSZXRyaWVzIG11c3QgYmUgYSBpbnRlZ2VyJ1xuXG4gICAgaWYgQG1heFJlZGlyZWN0cyBhbmQgbm90IF8uaXNJbnRlZ2VyIEBtYXhSZWRpcmVjdHNcbiAgICAgIHRocm93IG5ldyBFcnJvciAnbWF4UmVkaXJlY3RzIG11c3QgYmUgYSBpbnRlZ2VyJ1xuXG4gICAgaWYgQHN5c3RlbUNsb2NrT2Zmc2V0IGFuZCBub3QgXy5pc051bWJlciBAc3lzdGVtQ2xvY2tPZmZzZXRcbiAgICAgIHRocm93IG5ldyBFcnJvciAnc3lzdGVtQ2xvY2tPZmZzZXQgbXVzdCBiZSBhIG51bWJlcidcblxuICAgIGlmIEBzc2xFbmFibGVkIGFuZCBub3QgXy5pc0Jvb2xlYW4gQHNzbEVuYWJsZWRcbiAgICAgIHRocm93IG5ldyBFcnJvciAnc3NsRW5hYmxlZCBtdXN0IGJlIGEgYm9vbGVhbidcblxuICAgIGlmIEBwYXJhbVZhbGlkYXRpb24gYW5kIG5vdCBfLmlzQm9vbGVhbiBAcGFyYW1WYWxpZGF0aW9uXG4gICAgICB0aHJvdyBuZXcgRXJyb3IgJ3BhcmFtVmFsaWRhdGlvbiBtdXN0IGJlIGEgYm9vbGVhbidcblxuICAgIGlmIEBjb21wdXRlQ2hlY2tzdW1zIGFuZCBub3QgXy5pc0Jvb2xlYW4gQGNvbXB1dGVDaGVja3N1bXNcbiAgICAgIHRocm93IG5ldyBFcnJvciAnY29tcHV0ZUNoZWNrc3VtcyBtdXN0IGJlIGEgYm9vbGVhbidcblxuICAgIGlmIEBjb252ZXJ0UmVzcG9uc2VUeXBlcyBhbmQgbm90IF8uaXNCb29sZWFuIEBjb252ZXJ0UmVzcG9uc2VUeXBlc1xuICAgICAgdGhyb3cgbmV3IEVycm9yICdjb252ZXJ0UmVzcG9uc2VUeXBlcyBtdXN0IGJlIGEgYm9vbGVhbidcblxuICAgIGlmIEBzM0ZvcmNlUGF0aFN0eWxlIGFuZCBub3QgXy5pc0Jvb2xlYW4gQHMzRm9yY2VQYXRoU3R5bGVcbiAgICAgIHRocm93IG5ldyBFcnJvciAnczNGb3JjZVBhdGhTdHlsZSBtdXN0IGJlIGEgYm9vbGVhbidcblxuICAgIGlmIEBzM0J1Y2tldEVuZHBvaW50IGFuZCBub3QgXy5pc0Jvb2xlYW4gQHMzQnVja2V0RW5kcG9pbnRcbiAgICAgIHRocm93IG5ldyBFcnJvciAnczNCdWNrZXRFbmRwb2ludCBtdXN0IGJlIGEgYm9vbGVhbidcblxuICAgIGlmIEBodHRwT3B0aW9ucyBhbmQgbm90IF8uaXNQbGFpbk9iamVjdCBAaHR0cE9wdGlvbnNcbiAgICAgIHRocm93IG5ldyBFcnJvciAnaHR0cE9wdGlvbnMgbXVzdCBiZSBhIGRpY3Qgd2l0aCBwYXJhbXM6IHByb3h5LCBhZ2VudCwgdGltZW91dCwgeGhyQXN5bmMsIHhocldpdGhDcmVkZW50aWFscydcblxuICAgIGlmIEBhcGlWZXJzaW9ucyBhbmQgbm90IF8uaXNQbGFpbk9iamVjdCBAYXBpVmVyc2lvbnNcbiAgICAgIHRocm93IG5ldyBFcnJvciAnYXBpVmVyc2lvbnMgbXVzdCBiZSBhIGRpY3Qgd2l0aCB2ZXJzaW9ucydcblxuICAgIGlmIEBhcGlWZXJzaW9uIGFuZCBub3QgKF8uaXNTdHJpbmcgQGFwaVZlcnNpb24gb3IgXy5pc0RhdGUgQGFwaVZlcnNpb24pXG4gICAgICB0aHJvdyBuZXcgRXJyb3IgJ2FwaVZlcnNpb24gbXVzdCBiZSBhIHN0cmluZyBvciBkYXRlJ1xuXG4gICAgaWYgQHNlc3Npb25Ub2tlbiBhbmQgbm90IEBzZXNzaW9uVG9rZW4gaW5zdGFuY2VvZiBhd3MuQ3JlZGVudGlhbHNcbiAgICAgIHRocm93IG5ldyBFcnJvciAnc2Vzc2lvblRva2VuIG11c3QgYmUgYSBBV1MuQ3JlZGVudGlhbHMnXG5cbiAgICBpZiBAY3JlZGVudGlhbHMgYW5kIG5vdCBAY3JlZGVudGlhbHMgaW5zdGFuY2VvZiBhd3MuQ3JlZGVudGlhbHNcbiAgICAgIHRocm93IG5ldyBFcnJvciAnY3JlZGVudGlhbHMgbXVzdCBiZSBhIEFXUy5DcmVkZW50aWFscydcblxuICAgIGlmIEBjcmVkZW50aWFsUHJvdmlkZXIgYW5kIG5vdCBAY3JlZGVudGlhbFByb3ZpZGVyIGluc3RhbmNlb2YgYXdzLkNyZWRlbnRpYWxzUHJvdmlkZXJDaGFpblxuICAgICAgdGhyb3cgbmV3IEVycm9yICdjcmVkZW50aWFsUHJvdmlkZXIgbXVzdCBiZSBhIEFXUy5DcmVkZW50aWFsc1Byb3ZpZGVyQ2hhaW4nXG5cbiAgICBpZiBAbG9nZ2VyIGFuZCBub3QgKEBsb2dnZXIud3JpdGUgYW5kIEBsb2dnZXIubG9nKVxuICAgICAgdGhyb3cgbmV3IEVycm9yICdsb2dnZXIgbXVzdCBoYXZlICN3cml0ZSBvciAjbG9nIG1ldGhvZHMnXG5cblxuIyBFeHBvcnRzXG5tb2R1bGUuZXhwb3J0cyA9IFMzQ2xpZW50XG5cbiJdfQ== -------------------------------------------------------------------------------- /logotype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwingDev/s3-browser-direct-upload/07eba7bfc59c85215920ec278e5cb82f74843d40/logotype.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "s3-browser-direct-upload", 3 | "version": "0.1.2", 4 | "description": "s3-browser-direct-upload for node", 5 | "main": "index.js", 6 | "author": "SWINGDEV - Gabriel Oczkowski", 7 | "license": "MIT", 8 | "scripts": { 9 | "test": "mocha --reporter spec --timeout 10000 --recursive test/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/SwingDev/s3-browser-direct-upload.git" 14 | }, 15 | "keywords": [ 16 | "s3-browser-direct-upload", 17 | "s3", 18 | "browser", 19 | "direct", 20 | "upload" 21 | ], 22 | "dependencies": { 23 | "aws-sdk": "^2.1.17", 24 | "crypto": "0.0.3", 25 | "lodash": "^4.17.15", 26 | "mime": "^1.3.4", 27 | "moment": "^2.9.0" 28 | }, 29 | "devDependencies": { 30 | "chai": "^2.1.2", 31 | "chai-things": "^0.2.0", 32 | "coffeelint": "^1.6.1", 33 | "del": "^0.1.3", 34 | "fs": "0.0.2", 35 | "glob": "^4.0.6", 36 | "gulp": "^3.8.10", 37 | "gulp-chmod": "^1.2.0", 38 | "gulp-coffee": "^2.2.0", 39 | "gulp-coffeelint": "^0.4.0", 40 | "gulp-concat": "^2.4.1", 41 | "gulp-plumber": "^0.6.6", 42 | "gulp-sass": "^1.1.0", 43 | "gulp-sourcemaps": "^1.2.8", 44 | "gulp-uglify": "^1.0.1", 45 | "gulp-util": "^3.0.1", 46 | "gulp-watch": "^1.2.0", 47 | "map-stream": "0.0.5", 48 | "merge-stream": "^0.1.6", 49 | "moment": "^2.9.0", 50 | "run-sequence": "^1.0.1", 51 | "sinon": "^1.14.1", 52 | "sinon-chai": "^2.7.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/index.coffee: -------------------------------------------------------------------------------- 1 | # s3-browser-direct-upload 2 | _ = require('lodash') 3 | mime = require('mime') 4 | moment = require('moment') 5 | crypto = require('crypto') 6 | 7 | 8 | class S3Client 9 | constructor: (options = {}, arrAllowedDataExtensions) -> 10 | aws = require('aws-sdk') 11 | 12 | @_checkOptions options unless options instanceof aws.Config 13 | aws.config.update options 14 | 15 | @s3 = new aws.S3() 16 | 17 | @arrAllowedDataExtensions = null 18 | if arrAllowedDataExtensions and @_checkAllowedDataExtensions arrAllowedDataExtensions 19 | @arrAllowedDataExtensions = arrAllowedDataExtensions 20 | 21 | 22 | # Browser form post params for uploading 23 | uploadPostForm: (options = {}, cb) -> 24 | throw new Error 'Callback is required' unless cb 25 | { extension, key, bucket, expires, acl, contentLength, algorithm, region, conditionMatching } = options 26 | key = options.key 27 | bucket = options.bucket 28 | extension = options.extension ? null 29 | expires = options.expires ? moment.utc().add(60, 'minutes').toDate() 30 | acl = options.acl ? 'public-read' 31 | contentLength = options.contentLength ? null 32 | algorithm = options.algorithm ? 'AWS4-HMAC-SHA256' 33 | region = options.region ? @region 34 | conditionMatching = options.conditionMatching ? null 35 | 36 | # @TODO options type check 37 | unless key and bucket 38 | return cb new Error 'key and bucket are required' 39 | 40 | if extension 41 | contentType = @_checkDataExtension extension 42 | return cb new Error 'Data extension not allowed' unless contentType 43 | 44 | if algorithm.split('-').length == 3 45 | arrAlgorithm = algorithm.split('-') 46 | sigver = arrAlgorithm[0] 47 | hashalg = arrAlgorithm[2].toLowerCase() 48 | else 49 | sigver = "AWS4" 50 | hashalg = "sha256" 51 | 52 | policyDoc = {} 53 | 54 | policyDoc["expiration"] = moment.utc(expires).format("YYYY-MM-DD[T]HH:mm:ss[Z]") if expires and _.isDate expires 55 | policyDoc["conditions"] = [] 56 | 57 | dateShortPolicy = moment.utc().format('YYYYMMDD') 58 | dateLongPolicy = moment.utc().format('YYYYMMDD[T]HHmmss[Z]') 59 | 60 | policyDoc.conditions.push { 'bucket': bucket } 61 | policyDoc.conditions.push [ 'starts-with', '$key', key ] 62 | policyDoc.conditions.push { 'acl': acl } 63 | policyDoc.conditions.push [ 'starts-with', '$Content-Type', contentType ] if contentType 64 | policyDoc.conditions.push [ 'content-length-range', 0, contentLength ] if contentLength 65 | policyDoc.conditions.push { "x-amz-algorithm": algorithm } 66 | policyDoc.conditions.push { "x-amz-credential": "#{@accessKeyId}/#{dateShortPolicy}/#{region}/s3/aws4_request" } 67 | policyDoc.conditions.push { "x-amz-date": dateLongPolicy} 68 | 69 | if conditionMatching and _.isArray conditionMatching 70 | policyDoc.conditions = _.union conditionMatching, policyDoc.conditions 71 | 72 | dateKey = crypto.createHmac(hashalg, "#{sigver}#{@secretAccessKey}").update(dateShortPolicy).digest() 73 | dateRegionKey = crypto.createHmac(hashalg, dateKey).update(region).digest() 74 | dateRegionServiceKey = crypto.createHmac(hashalg, dateRegionKey).update('s3').digest() 75 | signingKey = crypto.createHmac(hashalg, dateRegionServiceKey).update("#{sigver.toLowerCase()}_request").digest() 76 | policy = new Buffer(JSON.stringify(policyDoc)).toString('base64') 77 | signature = crypto.createHmac(hashalg,signingKey).update(policy).digest('hex') 78 | 79 | stream = {} 80 | stream['params'] = 81 | "key": key 82 | "acl": acl 83 | "x-amz-algorithm": algorithm 84 | "x-amz-credential": "#{@accessKeyId}/#{dateShortPolicy}/#{region}/s3/#{sigver.toLowerCase()}_request" 85 | "x-amz-date": dateLongPolicy 86 | "policy": policy 87 | "x-amz-signature": signature 88 | stream.params['content-type'] = contentType if contentType 89 | stream['conditions'] = conditionMatching if conditionMatching 90 | stream['public_url'] = "https://#{bucket}.s3.amazonaws.com/#{key}" 91 | stream['form_url'] = "https://#{bucket}.s3.amazonaws.com/" 92 | 93 | cb null, stream 94 | 95 | 96 | # S3.upload 97 | upload: (options = {}, cb) -> 98 | throw new Error 'Callback is required' unless cb 99 | { data, extension, key, bucket, expires, acl, contentLength } = options 100 | data = options.data 101 | key = options.key 102 | bucket = options.bucket 103 | extension = options.extension ? null 104 | expires = options.expires ? null 105 | acl = options.acl ? null 106 | contentLength = options.contentLength ? null 107 | 108 | # @TODO options type check 109 | unless data and key and bucket 110 | return cb new Error 'data, key and bucket are required' 111 | 112 | params = 113 | Bucket: bucket 114 | Key: key 115 | Body: data 116 | 117 | if extension 118 | contentType = @_checkDataExtension extension 119 | return cb new Error 'Data extension not allowed' unless contentType 120 | params["ContentType"] = contentType 121 | 122 | params["Expires"] = moment.utc(expires) if expires and _.isDate expires 123 | params["ACL"] = acl if acl 124 | params["ContentLength"] = contentLength if contentLength 125 | 126 | @s3.upload params, (err, data) -> 127 | return cb err if err 128 | cb null, "https://#{bucket}.s3.amazonaws.com/#{key}" 129 | 130 | 131 | # S3.putObject 132 | put: (options = {}, cb) -> 133 | throw new Error 'Callback is required' unless cb 134 | { extension, key, bucket, expires, acl, contentLength } = options 135 | key = options.key 136 | bucket = options.bucket 137 | extension = options.extension ? null 138 | expires = options.expires ? null 139 | acl = options.acl ? null 140 | 141 | # @TODO options type check 142 | unless key and bucket 143 | return cb new Error 'key and bucket are required' 144 | 145 | params = 146 | Bucket: bucket 147 | Key: key 148 | 149 | if extension 150 | contentType = @_checkDataExtension extension 151 | return cb new Error 'Data extension not allowed' unless contentType 152 | params["ContentType"] = contentType 153 | 154 | params["Expires"] = moment.utc(expires) if expires and _.isDate expires 155 | params["ACL"] = acl if acl 156 | 157 | @s3.getSignedUrl "putObject", params, (err, data) -> 158 | return cb err if err 159 | 160 | put = 161 | 'signed_url': data 162 | 'public_url': "https://#{bucket}.s3.amazonaws.com/#{key}" 163 | 164 | cb null, put 165 | 166 | 167 | # Check data type from arrAllowedDataExtensions 168 | _checkDataExtension: (dataExtension) -> 169 | return false if not dataExtension or (@arrAllowedDataExtensions and dataExtension not in @arrAllowedDataExtensions) 170 | return mime.lookup dataExtension 171 | 172 | 173 | # Check allowed data types 174 | _checkAllowedDataExtensions: (arrAllowedDataExtensions) -> 175 | return false unless arrAllowedDataExtensions 176 | 177 | unless _.isArray arrAllowedDataExtensions 178 | throw new Error "Allowed data extensions must be array of strings" 179 | 180 | for ext of arrAllowedDataExtensions 181 | unless _.isString ext 182 | throw new Error "Extensions must be a strings" 183 | 184 | return true 185 | 186 | 187 | # Check options params 188 | _checkOptions: (options = {}) -> 189 | { 190 | @accessKeyId, @secretAccessKey, @region, @signatureVersion, @maxRetries, @maxRedirects, @systemClockOffset, 191 | @sslEnabled, @paramValidation, @computeChecksums, @convertResponseTypes, @s3ForcePathStyle, @s3BucketEndpoint, 192 | @apiVersion, @httpOptions, @apiVersions, @sessionToken, @credentials, @credentialProvider, @logger 193 | } = options 194 | 195 | unless @accessKeyId 196 | throw new Error "accessKeyId is required" 197 | 198 | unless @secretAccessKey 199 | throw new Error "secretAccessKey is required" 200 | 201 | unless @region 202 | throw new Error "region is required" 203 | 204 | unless _.isString @accessKeyId 205 | throw new Error "accessKeyId must be a string" 206 | 207 | unless _.isString @secretAccessKey 208 | throw new Error "secretAccessKey must be a string" 209 | 210 | unless _.isString @region 211 | throw new Error "region must be a string" 212 | 213 | if @signatureVersion and not _.isString @signatureVersion 214 | throw new Error "signatureVersion must be a string" 215 | 216 | if @maxRetries and not _.isInteger @maxRetries 217 | throw new Error 'maxRetries must be a integer' 218 | 219 | if @maxRedirects and not _.isInteger @maxRedirects 220 | throw new Error 'maxRedirects must be a integer' 221 | 222 | if @systemClockOffset and not _.isNumber @systemClockOffset 223 | throw new Error 'systemClockOffset must be a number' 224 | 225 | if @sslEnabled and not _.isBoolean @sslEnabled 226 | throw new Error 'sslEnabled must be a boolean' 227 | 228 | if @paramValidation and not _.isBoolean @paramValidation 229 | throw new Error 'paramValidation must be a boolean' 230 | 231 | if @computeChecksums and not _.isBoolean @computeChecksums 232 | throw new Error 'computeChecksums must be a boolean' 233 | 234 | if @convertResponseTypes and not _.isBoolean @convertResponseTypes 235 | throw new Error 'convertResponseTypes must be a boolean' 236 | 237 | if @s3ForcePathStyle and not _.isBoolean @s3ForcePathStyle 238 | throw new Error 's3ForcePathStyle must be a boolean' 239 | 240 | if @s3BucketEndpoint and not _.isBoolean @s3BucketEndpoint 241 | throw new Error 's3BucketEndpoint must be a boolean' 242 | 243 | if @httpOptions and not _.isPlainObject @httpOptions 244 | throw new Error 'httpOptions must be a dict with params: proxy, agent, timeout, xhrAsync, xhrWithCredentials' 245 | 246 | if @apiVersions and not _.isPlainObject @apiVersions 247 | throw new Error 'apiVersions must be a dict with versions' 248 | 249 | if @apiVersion and not (_.isString @apiVersion or _.isDate @apiVersion) 250 | throw new Error 'apiVersion must be a string or date' 251 | 252 | if @sessionToken and not @sessionToken instanceof aws.Credentials 253 | throw new Error 'sessionToken must be a AWS.Credentials' 254 | 255 | if @credentials and not @credentials instanceof aws.Credentials 256 | throw new Error 'credentials must be a AWS.Credentials' 257 | 258 | if @credentialProvider and not @credentialProvider instanceof aws.CredentialsProviderChain 259 | throw new Error 'credentialProvider must be a AWS.CredentialsProviderChain' 260 | 261 | if @logger and not (@logger.write and @logger.log) 262 | throw new Error 'logger must have #write or #log methods' 263 | 264 | 265 | # Exports 266 | module.exports = S3Client 267 | 268 | -------------------------------------------------------------------------------- /src/test/index.coffee: -------------------------------------------------------------------------------- 1 | # s3-browser-direct-upload 2 | _ = require('lodash') 3 | fs = require('fs') 4 | moment = require('moment') 5 | 6 | chai = require('chai') 7 | path = require('path') 8 | sinon = require('sinon') 9 | sinonChai = require('sinon-chai') 10 | 11 | assert = chai.assert 12 | expect = chai.expect 13 | 14 | chai.use(sinonChai) 15 | s3BrowserUpload = require('../lib') 16 | 17 | 18 | # TESTS 19 | describe 's3-uploadPostForm tests', () -> 20 | 21 | describe '#uploadPostForm tests', () -> 22 | 23 | s3client = null 24 | 25 | before -> 26 | s3client = new s3BrowserUpload 27 | accessKeyId: 'rHiziprP5FLOL5DpLaRc' 28 | secretAccessKey: 'dGudXJxDvtgZ2oRvzuMY1uWA/tsziUXwkd3tnJBk' 29 | signatureVersion: "v4" 30 | region: "eu-central-1" 31 | 32 | it 'should return json with all parameters required to build a form', (done) -> 33 | uploadPostFormOptions = 34 | key: "testKey.jpg" 35 | bucket: 'testBucket' 36 | expires: moment().add(60, 'minutes').toDate() 37 | extension: 'jpg' 38 | 39 | s3client.uploadPostForm uploadPostFormOptions, (err, params) -> 40 | expect(params).to.have.deep.property 'params.key' 41 | expect(params).to.have.deep.property 'params.acl' 42 | expect(params).to.have.deep.property 'params.content-type' 43 | expect(params).to.have.deep.property 'params.x-amz-algorithm' 44 | expect(params).to.have.deep.property 'params.x-amz-credential' 45 | expect(params).to.have.deep.property 'params.x-amz-date' 46 | expect(params).to.have.deep.property 'params.policy' 47 | expect(params).to.have.deep.property 'params.x-amz-signature' 48 | expect(params).to.have.deep.property 'public_url' 49 | expect(params).to.have.deep.property 'form_url' 50 | expect(params).to.not.have.deep.property 'conditions' 51 | 52 | done() 53 | 54 | it 'should return json with all parameters required to build a form if custom conditionMatching used', (done) -> 55 | uploadPostFormOptions = 56 | key: "testKey.jpg" 57 | bucket: 'testBucket' 58 | expires: moment().add(60, 'minutes').toDate() 59 | extension: 'jpg' 60 | conditionMatching: [ 61 | {"success_action_redirect": "http://google.com"} 62 | ] 63 | 64 | s3client.uploadPostForm uploadPostFormOptions, (err, params) -> 65 | expect(params).to.have.deep.property 'params.key' 66 | expect(params).to.have.deep.property 'params.acl' 67 | expect(params).to.have.deep.property 'params.content-type' 68 | expect(params).to.have.deep.property 'params.x-amz-algorithm' 69 | expect(params).to.have.deep.property 'params.x-amz-credential' 70 | expect(params).to.have.deep.property 'params.x-amz-date' 71 | expect(params).to.have.deep.property 'params.policy' 72 | expect(params).to.have.deep.property 'params.x-amz-signature' 73 | expect(params).to.have.deep.property 'public_url' 74 | expect(params).to.have.deep.property 'form_url' 75 | expect(params).to.have.deep.property 'conditions' 76 | 77 | done() 78 | 79 | describe '#upload tests', () -> 80 | 81 | s3client = null 82 | 83 | before -> 84 | s3client = new s3BrowserUpload 85 | accessKeyId: 'rHiziprP5FLOL5DpLaRc' 86 | secretAccessKey: 'dGudXJxDvtgZ2oRvzuMY1uWA/tsziUXwkd3tnJBk' 87 | signatureVersion: "v4" 88 | region: "eu-central-1" 89 | 90 | sinon.stub s3client.s3, 'upload', (params, cb) -> 91 | cb() 92 | 93 | after -> 94 | s3client.s3.upload.restore() 95 | 96 | it 'should return url of uploaded file', (done) -> 97 | uploadOptions = 98 | data: "String Object data" 99 | key: "testKey.txt" 100 | bucket: 'testBucket' 101 | extension: 'txt' 102 | acl: 'public-read' 103 | 104 | s3client.upload uploadOptions, (err, url) -> 105 | expect(url).to.exists 106 | expect(url).to.equal 'https://testBucket.s3.amazonaws.com/testKey.txt' 107 | done() 108 | 109 | describe '#put tests', () -> 110 | 111 | s3client = null 112 | 113 | before -> 114 | s3client = new s3BrowserUpload 115 | accessKeyId: 'rHiziprP5FLOL5DpLaRc' 116 | secretAccessKey: 'dGudXJxDvtgZ2oRvzuMY1uWA/tsziUXwkd3tnJBk' 117 | signatureVersion: "v4" 118 | region: "eu-central-1" 119 | 120 | sinon.stub s3client.s3, 'getSignedUrl', (typeName, params, cb) -> 121 | cb(null, 'https://testBucket.s3.amazonaws.com/testKey.txt') 122 | 123 | after -> 124 | s3client.s3.getSignedUrl.restore() 125 | 126 | it 'should return json with signed and public urls', (done) -> 127 | uploadOptions = 128 | key: "testKey.txt" 129 | bucket: 'testBucket' 130 | extension: 'txt' 131 | acl: 'public-read' 132 | 133 | s3client.put uploadOptions, (err, urls) -> 134 | expect(urls).to.have.property 'signed_url' 135 | expect(urls).to.have.property 'public_url' 136 | done() 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var _, assert, chai, expect, fs, moment, path, s3BrowserUpload, sinon, sinonChai; 2 | 3 | _ = require('lodash'); 4 | 5 | fs = require('fs'); 6 | 7 | moment = require('moment'); 8 | 9 | chai = require('chai'); 10 | 11 | path = require('path'); 12 | 13 | sinon = require('sinon'); 14 | 15 | sinonChai = require('sinon-chai'); 16 | 17 | assert = chai.assert; 18 | 19 | expect = chai.expect; 20 | 21 | chai.use(sinonChai); 22 | 23 | s3BrowserUpload = require('../lib'); 24 | 25 | describe('s3-uploadPostForm tests', function() { 26 | describe('#uploadPostForm tests', function() { 27 | var s3client; 28 | s3client = null; 29 | before(function() { 30 | return s3client = new s3BrowserUpload({ 31 | accessKeyId: 'rHiziprP5FLOL5DpLaRc', 32 | secretAccessKey: 'dGudXJxDvtgZ2oRvzuMY1uWA/tsziUXwkd3tnJBk', 33 | signatureVersion: "v4", 34 | region: "eu-central-1" 35 | }); 36 | }); 37 | it('should return json with all parameters required to build a form', function(done) { 38 | var uploadPostFormOptions; 39 | uploadPostFormOptions = { 40 | key: "testKey.jpg", 41 | bucket: 'testBucket', 42 | expires: moment().add(60, 'minutes').toDate(), 43 | extension: 'jpg' 44 | }; 45 | return s3client.uploadPostForm(uploadPostFormOptions, function(err, params) { 46 | expect(params).to.have.deep.property('params.key'); 47 | expect(params).to.have.deep.property('params.acl'); 48 | expect(params).to.have.deep.property('params.content-type'); 49 | expect(params).to.have.deep.property('params.x-amz-algorithm'); 50 | expect(params).to.have.deep.property('params.x-amz-credential'); 51 | expect(params).to.have.deep.property('params.x-amz-date'); 52 | expect(params).to.have.deep.property('params.policy'); 53 | expect(params).to.have.deep.property('params.x-amz-signature'); 54 | expect(params).to.have.deep.property('public_url'); 55 | expect(params).to.have.deep.property('form_url'); 56 | expect(params).to.not.have.deep.property('conditions'); 57 | return done(); 58 | }); 59 | }); 60 | return it('should return json with all parameters required to build a form if custom conditionMatching used', function(done) { 61 | var uploadPostFormOptions; 62 | uploadPostFormOptions = { 63 | key: "testKey.jpg", 64 | bucket: 'testBucket', 65 | expires: moment().add(60, 'minutes').toDate(), 66 | extension: 'jpg', 67 | conditionMatching: [ 68 | { 69 | "success_action_redirect": "http://google.com" 70 | } 71 | ] 72 | }; 73 | return s3client.uploadPostForm(uploadPostFormOptions, function(err, params) { 74 | expect(params).to.have.deep.property('params.key'); 75 | expect(params).to.have.deep.property('params.acl'); 76 | expect(params).to.have.deep.property('params.content-type'); 77 | expect(params).to.have.deep.property('params.x-amz-algorithm'); 78 | expect(params).to.have.deep.property('params.x-amz-credential'); 79 | expect(params).to.have.deep.property('params.x-amz-date'); 80 | expect(params).to.have.deep.property('params.policy'); 81 | expect(params).to.have.deep.property('params.x-amz-signature'); 82 | expect(params).to.have.deep.property('public_url'); 83 | expect(params).to.have.deep.property('form_url'); 84 | expect(params).to.have.deep.property('conditions'); 85 | return done(); 86 | }); 87 | }); 88 | }); 89 | describe('#upload tests', function() { 90 | var s3client; 91 | s3client = null; 92 | before(function() { 93 | s3client = new s3BrowserUpload({ 94 | accessKeyId: 'rHiziprP5FLOL5DpLaRc', 95 | secretAccessKey: 'dGudXJxDvtgZ2oRvzuMY1uWA/tsziUXwkd3tnJBk', 96 | signatureVersion: "v4", 97 | region: "eu-central-1" 98 | }); 99 | return sinon.stub(s3client.s3, 'upload', function(params, cb) { 100 | return cb(); 101 | }); 102 | }); 103 | after(function() { 104 | return s3client.s3.upload.restore(); 105 | }); 106 | return it('should return url of uploaded file', function(done) { 107 | var uploadOptions; 108 | uploadOptions = { 109 | data: "String Object data", 110 | key: "testKey.txt", 111 | bucket: 'testBucket', 112 | extension: 'txt', 113 | acl: 'public-read' 114 | }; 115 | return s3client.upload(uploadOptions, function(err, url) { 116 | expect(url).to.exists; 117 | expect(url).to.equal('https://testBucket.s3.amazonaws.com/testKey.txt'); 118 | return done(); 119 | }); 120 | }); 121 | }); 122 | return describe('#put tests', function() { 123 | var s3client; 124 | s3client = null; 125 | before(function() { 126 | s3client = new s3BrowserUpload({ 127 | accessKeyId: 'rHiziprP5FLOL5DpLaRc', 128 | secretAccessKey: 'dGudXJxDvtgZ2oRvzuMY1uWA/tsziUXwkd3tnJBk', 129 | signatureVersion: "v4", 130 | region: "eu-central-1" 131 | }); 132 | return sinon.stub(s3client.s3, 'getSignedUrl', function(typeName, params, cb) { 133 | return cb(null, 'https://testBucket.s3.amazonaws.com/testKey.txt'); 134 | }); 135 | }); 136 | after(function() { 137 | return s3client.s3.getSignedUrl.restore(); 138 | }); 139 | return it('should return json with signed and public urls', function(done) { 140 | var uploadOptions; 141 | uploadOptions = { 142 | key: "testKey.txt", 143 | bucket: 'testBucket', 144 | extension: 'txt', 145 | acl: 'public-read' 146 | }; 147 | return s3client.put(uploadOptions, function(err, urls) { 148 | expect(urls).to.have.property('signed_url'); 149 | expect(urls).to.have.property('public_url'); 150 | return done(); 151 | }); 152 | }); 153 | }); 154 | }); 155 | 156 | //# sourceMappingURL=data:application/json;base64,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LENBQVksQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLFFBQXJCLENBQThCLFlBQTlCO1FBQ0EsTUFBQSxDQUFPLElBQVAsQ0FBWSxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsUUFBckIsQ0FBOEIsWUFBOUI7ZUFDQSxJQUFBLENBQUE7TUFIMEIsQ0FBNUI7SUFQbUQsQ0FBckQ7RUFqQnFCLENBQXZCO0FBMUZrQyxDQUFwQyIsImZpbGUiOiJ0ZXN0L2luZGV4LmpzIiwic291cmNlUm9vdCI6Ii9zb3VyY2UvIiwic291cmNlc0NvbnRlbnQiOlsiIyBzMy1icm93c2VyLWRpcmVjdC11cGxvYWRcbl8gICAgICAgICAgID0gcmVxdWlyZSgnbG9kYXNoJylcbmZzICAgICAgICAgID0gcmVxdWlyZSgnZnMnKVxubW9tZW50ICAgICAgPSByZXF1aXJlKCdtb21lbnQnKVxuXG5jaGFpICAgICAgICA9IHJlcXVpcmUoJ2NoYWknKVxucGF0aCAgICAgICAgPSByZXF1aXJlKCdwYXRoJylcbnNpbm9uICAgICAgID0gcmVxdWlyZSgnc2lub24nKVxuc2lub25DaGFpICAgPSByZXF1aXJlKCdzaW5vbi1jaGFpJylcblxuYXNzZXJ0ICAgID0gY2hhaS5hc3NlcnRcbmV4cGVjdCAgICA9IGNoYWkuZXhwZWN0XG5cbmNoYWkudXNlKHNpbm9uQ2hhaSlcbnMzQnJvd3NlclVwbG9hZCA9IHJlcXVpcmUoJy4uL2xpYicpXG5cblxuIyBURVNUU1xuZGVzY3JpYmUgJ3MzLXVwbG9hZFBvc3RGb3JtIHRlc3RzJywgKCkgLT5cblxuICBkZXNjcmliZSAnI3VwbG9hZFBvc3RGb3JtIHRlc3RzJywgKCkgLT5cblxuICAgIHMzY2xpZW50ID0gbnVsbFxuXG4gICAgYmVmb3JlIC0+XG4gICAgICBzM2NsaWVudCA9IG5ldyBzM0Jyb3dzZXJVcGxvYWRcbiAgICAgICAgYWNjZXNzS2V5SWQ6ICdySGl6aXByUDVGTE9MNURwTGFSYydcbiAgICAgICAgc2VjcmV0QWNjZXNzS2V5OiAnZEd1ZFhKeER2dGdaMm9Sdnp1TVkxdVdBL3RzemlVWHdrZDN0bkpCaydcbiAgICAgICAgc2lnbmF0dXJlVmVyc2lvbjogXCJ2NFwiXG4gICAgICAgIHJlZ2lvbjogXCJldS1jZW50cmFsLTFcIlxuXG4gICAgaXQgJ3Nob3VsZCByZXR1cm4ganNvbiB3aXRoIGFsbCBwYXJhbWV0ZXJzIHJlcXVpcmVkIHRvIGJ1aWxkIGEgZm9ybScsIChkb25lKSAtPlxuICAgICAgdXBsb2FkUG9zdEZvcm1PcHRpb25zID1cbiAgICAgICAga2V5OiBcInRlc3RLZXkuanBnXCJcbiAgICAgICAgYnVja2V0OiAndGVzdEJ1Y2tldCdcbiAgICAgICAgZXhwaXJlczogbW9tZW50KCkuYWRkKDYwLCAnbWludXRlcycpLnRvRGF0ZSgpXG4gICAgICAgIGV4dGVuc2lvbjogJ2pwZydcblxuICAgICAgczNjbGllbnQudXBsb2FkUG9zdEZvcm0gdXBsb2FkUG9zdEZvcm1PcHRpb25zLCAoZXJyLCBwYXJhbXMpIC0+XG4gICAgICAgIGV4cGVjdChwYXJhbXMpLnRvLmhhdmUuZGVlcC5wcm9wZXJ0eSAncGFyYW1zLmtleSdcbiAgICAgICAgZXhwZWN0KHBhcmFtcykudG8uaGF2ZS5kZWVwLnByb3BlcnR5ICdwYXJhbXMuYWNsJ1xuICAgICAgICBleHBlY3QocGFyYW1zKS50by5oYXZlLmRlZXAucHJvcGVydHkgJ3BhcmFtcy5jb250ZW50LXR5cGUnXG4gICAgICAgIGV4cGVjdChwYXJhbXMpLnRvLmhhdmUuZGVlcC5wcm9wZXJ0eSAncGFyYW1zLngtYW16LWFsZ29yaXRobSdcbiAgICAgICAgZXhwZWN0KHBhcmFtcykudG8uaGF2ZS5kZWVwLnByb3BlcnR5ICdwYXJhbXMueC1hbXotY3JlZGVudGlhbCdcbiAgICAgICAgZXhwZWN0KHBhcmFtcykudG8uaGF2ZS5kZWVwLnByb3BlcnR5ICdwYXJhbXMueC1hbXotZGF0ZSdcbiAgICAgICAgZXhwZWN0KHBhcmFtcykudG8uaGF2ZS5kZWVwLnByb3BlcnR5ICdwYXJhbXMucG9saWN5J1xuICAgICAgICBleHBlY3QocGFyYW1zKS50by5oYXZlLmRlZXAucHJvcGVydHkgJ3BhcmFtcy54LWFtei1zaWduYXR1cmUnXG4gICAgICAgIGV4cGVjdChwYXJhbXMpLnRvLmhhdmUuZGVlcC5wcm9wZXJ0eSAncHVibGljX3VybCdcbiAgICAgICAgZXhwZWN0KHBhcmFtcykudG8uaGF2ZS5kZWVwLnByb3BlcnR5ICdmb3JtX3VybCdcbiAgICAgICAgZXhwZWN0KHBhcmFtcykudG8ubm90LmhhdmUuZGVlcC5wcm9wZXJ0eSAnY29uZGl0aW9ucydcblxuICAgICAgICBkb25lKClcblxuICAgIGl0ICdzaG91bGQgcmV0dXJuIGpzb24gd2l0aCBhbGwgcGFyYW1ldGVycyByZXF1aXJlZCB0byBidWlsZCBhIGZvcm0gaWYgY3VzdG9tIGNvbmRpdGlvbk1hdGNoaW5nIHVzZWQnLCAoZG9uZSkgLT5cbiAgICAgIHVwbG9hZFBvc3RGb3JtT3B0aW9ucyA9XG4gICAgICAgIGtleTogXCJ0ZXN0S2V5LmpwZ1wiXG4gICAgICAgIGJ1Y2tldDogJ3Rlc3RCdWNrZXQnXG4gICAgICAgIGV4cGlyZXM6IG1vbWVudCgpLmFkZCg2MCwgJ21pbnV0ZXMnKS50b0RhdGUoKVxuICAgICAgICBleHRlbnNpb246ICdqcGcnXG4gICAgICAgIGNvbmRpdGlvbk1hdGNoaW5nOiBbXG4gICAgICAgICAge1wic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3RcIjogXCJodHRwOi8vZ29vZ2xlLmNvbVwifVxuICAgICAgICBdXG5cbiAgICAgIHMzY2xpZW50LnVwbG9hZFBvc3RGb3JtIHVwbG9hZFBvc3RGb3JtT3B0aW9ucywgKGVyciwgcGFyYW1zKSAtPlxuICAgICAgICBleHBlY3QocGFyYW1zKS50by5oYXZlLmRlZXAucHJvcGVydHkgJ3BhcmFtcy5rZXknXG4gICAgICAgIGV4cGVjdChwYXJhbXMpLnRvLmhhdmUuZGVlcC5wcm9wZXJ0eSAncGFyYW1zLmFjbCdcbiAgICAgICAgZXhwZWN0KHBhcmFtcykudG8uaGF2ZS5kZWVwLnByb3BlcnR5ICdwYXJhbXMuY29udGVudC10eXBlJ1xuICAgICAgICBleHBlY3QocGFyYW1zKS50by5oYXZlLmRlZXAucHJvcGVydHkgJ3BhcmFtcy54LWFtei1hbGdvcml0aG0nXG4gICAgICAgIGV4cGVjdChwYXJhbXMpLnRvLmhhdmUuZGVlcC5wcm9wZXJ0eSAncGFyYW1zLngtYW16LWNyZWRlbnRpYWwnXG4gICAgICAgIGV4cGVjdChwYXJhbXMpLnRvLmhhdmUuZGVlcC5wcm9wZXJ0eSAncGFyYW1zLngtYW16LWRhdGUnXG4gICAgICAgIGV4cGVjdChwYXJhbXMpLnRvLmhhdmUuZGVlcC5wcm9wZXJ0eSAncGFyYW1zLnBvbGljeSdcbiAgICAgICAgZXhwZWN0KHBhcmFtcykudG8uaGF2ZS5kZWVwLnByb3BlcnR5ICdwYXJhbXMueC1hbXotc2lnbmF0dXJlJ1xuICAgICAgICBleHBlY3QocGFyYW1zKS50by5oYXZlLmRlZXAucHJvcGVydHkgJ3B1YmxpY191cmwnXG4gICAgICAgIGV4cGVjdChwYXJhbXMpLnRvLmhhdmUuZGVlcC5wcm9wZXJ0eSAnZm9ybV91cmwnXG4gICAgICAgIGV4cGVjdChwYXJhbXMpLnRvLmhhdmUuZGVlcC5wcm9wZXJ0eSAnY29uZGl0aW9ucydcblxuICAgICAgICBkb25lKClcblxuICBkZXNjcmliZSAnI3VwbG9hZCB0ZXN0cycsICgpIC0+XG5cbiAgICBzM2NsaWVudCA9IG51bGxcblxuICAgIGJlZm9yZSAtPlxuICAgICAgczNjbGllbnQgPSBuZXcgczNCcm93c2VyVXBsb2FkXG4gICAgICAgIGFjY2Vzc0tleUlkOiAnckhpemlwclA1RkxPTDVEcExhUmMnXG4gICAgICAgIHNlY3JldEFjY2Vzc0tleTogJ2RHdWRYSnhEdnRnWjJvUnZ6dU1ZMXVXQS90c3ppVVh3a2QzdG5KQmsnXG4gICAgICAgIHNpZ25hdHVyZVZlcnNpb246IFwidjRcIlxuICAgICAgICByZWdpb246IFwiZXUtY2VudHJhbC0xXCJcblxuICAgICAgc2lub24uc3R1YiBzM2NsaWVudC5zMywgJ3VwbG9hZCcsIChwYXJhbXMsIGNiKSAtPlxuICAgICAgICBjYigpXG5cbiAgICBhZnRlciAtPlxuICAgICAgczNjbGllbnQuczMudXBsb2FkLnJlc3RvcmUoKVxuXG4gICAgaXQgJ3Nob3VsZCByZXR1cm4gdXJsIG9mIHVwbG9hZGVkIGZpbGUnLCAoZG9uZSkgLT5cbiAgICAgIHVwbG9hZE9wdGlvbnMgPVxuICAgICAgICBkYXRhOiBcIlN0cmluZyBPYmplY3QgZGF0YVwiXG4gICAgICAgIGtleTogXCJ0ZXN0S2V5LnR4dFwiXG4gICAgICAgIGJ1Y2tldDogJ3Rlc3RCdWNrZXQnXG4gICAgICAgIGV4dGVuc2lvbjogJ3R4dCdcbiAgICAgICAgYWNsOiAncHVibGljLXJlYWQnXG5cbiAgICAgIHMzY2xpZW50LnVwbG9hZCB1cGxvYWRPcHRpb25zLCAoZXJyLCB1cmwpIC0+XG4gICAgICAgIGV4cGVjdCh1cmwpLnRvLmV4aXN0c1xuICAgICAgICBleHBlY3QodXJsKS50by5lcXVhbCAnaHR0cHM6Ly90ZXN0QnVja2V0LnMzLmFtYXpvbmF3cy5jb20vdGVzdEtleS50eHQnXG4gICAgICAgIGRvbmUoKVxuXG4gIGRlc2NyaWJlICcjcHV0IHRlc3RzJywgKCkgLT5cblxuICAgIHMzY2xpZW50ID0gbnVsbFxuXG4gICAgYmVmb3JlIC0+XG4gICAgICBzM2NsaWVudCA9IG5ldyBzM0Jyb3dzZXJVcGxvYWRcbiAgICAgICAgYWNjZXNzS2V5SWQ6ICdySGl6aXByUDVGTE9MNURwTGFSYydcbiAgICAgICAgc2VjcmV0QWNjZXNzS2V5OiAnZEd1ZFhKeER2dGdaMm9Sdnp1TVkxdVdBL3RzemlVWHdrZDN0bkpCaydcbiAgICAgICAgc2lnbmF0dXJlVmVyc2lvbjogXCJ2NFwiXG4gICAgICAgIHJlZ2lvbjogXCJldS1jZW50cmFsLTFcIlxuXG4gICAgICBzaW5vbi5zdHViIHMzY2xpZW50LnMzLCAnZ2V0U2lnbmVkVXJsJywgKHR5cGVOYW1lLCBwYXJhbXMsIGNiKSAtPlxuICAgICAgICBjYihudWxsLCAnaHR0cHM6Ly90ZXN0QnVja2V0LnMzLmFtYXpvbmF3cy5jb20vdGVzdEtleS50eHQnKVxuXG4gICAgYWZ0ZXIgLT5cbiAgICAgIHMzY2xpZW50LnMzLmdldFNpZ25lZFVybC5yZXN0b3JlKClcblxuICAgIGl0ICdzaG91bGQgcmV0dXJuIGpzb24gd2l0aCBzaWduZWQgYW5kIHB1YmxpYyB1cmxzJywgKGRvbmUpIC0+XG4gICAgICB1cGxvYWRPcHRpb25zID1cbiAgICAgICAga2V5OiBcInRlc3RLZXkudHh0XCJcbiAgICAgICAgYnVja2V0OiAndGVzdEJ1Y2tldCdcbiAgICAgICAgZXh0ZW5zaW9uOiAndHh0J1xuICAgICAgICBhY2w6ICdwdWJsaWMtcmVhZCdcblxuICAgICAgczNjbGllbnQucHV0IHVwbG9hZE9wdGlvbnMsIChlcnIsIHVybHMpIC0+XG4gICAgICAgIGV4cGVjdCh1cmxzKS50by5oYXZlLnByb3BlcnR5ICdzaWduZWRfdXJsJ1xuICAgICAgICBleHBlY3QodXJscykudG8uaGF2ZS5wcm9wZXJ0eSAncHVibGljX3VybCdcbiAgICAgICAgZG9uZSgpXG5cblxuXG5cbiJdfQ== --------------------------------------------------------------------------------