Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AWS.S3.getSignedUrlPromise #2827

Merged
merged 11 commits into from
Aug 29, 2019
5 changes: 5 additions & 0 deletions .changes/next-release/feature-S3-989eccff.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "S3",
"description": "add support for AWS.S3.getSignedUrlPromise which returns a promise for S3 presigned url"
}
7 changes: 6 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,12 @@ AWS.Config = AWS.util.inherit({
PromisesDependency = Promise;
}
var constructors = [AWS.Request, AWS.Credentials, AWS.CredentialProviderChain];
if (AWS.S3 && AWS.S3.ManagedUpload) constructors.push(AWS.S3.ManagedUpload);
if (AWS.S3) {
constructors.push(AWS.S3);
if (AWS.S3.ManagedUpload) {
constructors.push(AWS.S3.ManagedUpload);
}
}
AWS.util.addPromises(constructors, PromisesDependency);
},

Expand Down
5 changes: 5 additions & 0 deletions lib/services/s3.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export class S3Customizations extends Service {
*/
getSignedUrl(operation: string, params: any): string;

/**
* Returns a 'thenable' promise that will be resolved with a pre-signed URL for a given operation name.
*/
getSignedUrlPromise(operation: string, params: any): Promise<string>;

/**
* Get the form fields and target URL for direct POST uploading.
*/
Expand Down
62 changes: 62 additions & 0 deletions lib/services/s3.js
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,52 @@ AWS.util.update(AWS.S3.prototype, {
}
},

/**
* @!method getSignedUrlPromise()
* Returns a 'thenable' promise that will be resolved with a pre-signed URL
* for a given operation name.
*
* Two callbacks can be provided to the `then` method on the returned promise.
* The first callback will be called if the promise is fulfilled, and the second
* callback will be called if the promise is rejected.
* @note Not all operation parameters are supported when using pre-signed
* URLs. Certain parameters, such as `SSECustomerKey`, `ACL`, `Expires`,
* `ContentLength`, or `Tagging` must be provided as headers when sending a
* request. If you are using pre-signed URLs to upload from a browser and
* need to use these fields, see {createPresignedPost}.
* @param operation [String] the name of the operation to call
* @param params [map] parameters to pass to the operation. See the given
* operation for the expected operation parameters. In addition, you can
* also pass the "Expires" parameter to inform S3 how long the URL should
* work for.
* @option params Expires [Integer] (900) the number of seconds to expire
* the pre-signed URL operation in. Defaults to 15 minutes.
* @callback fulfilledCallback function(url)
* Called if the promise is fulfilled.
* @param url [String] the signed url
* @callback rejectedCallback function(err)
* Called if the promise is rejected.
* @param err [Error] if an error occurred, this value will be filled
* @return [Promise] A promise that represents the state of the `refresh` call.
* @example Pre-signing a getObject operation
* var params = {Bucket: 'bucket', Key: 'key'};
* var promise = s3.getSignedUrlPromise('getObject', params);
* promise.then(function(url) {
* console.log('The URL is', url);
* }, function(err) { ... });
* @example Pre-signing a putObject operation with a specific payload
* var params = {Bucket: 'bucket', Key: 'key', Body: 'body'};
* var promise = s3.getSignedUrlPromise('putObject', params);
* promise.then(function(url) {
* console.log('The URL is', url);
* }, function(err) { ... });
* @example Passing in a 1-minute expiry time for a pre-signed URL
* var params = {Bucket: 'bucket', Key: 'key', Expires: 60};
* var promise = s3.getSignedUrlPromise('getObject', params);
* promise.then(function(url) {
* console.log('The URL is', url);
* }, function(err) { ... });
*/

/**
* Get a pre-signed POST policy to support uploading to S3 directly from an
Expand Down Expand Up @@ -1089,3 +1135,19 @@ AWS.util.update(AWS.S3.prototype, {
return uploader;
}
});

/**
* @api private
*/
AWS.S3.addPromisesToClass = function addPromisesToClass(PromiseDependency) {
this.prototype.getSignedUrlPromise = AWS.util.promisifyMethod('getSignedUrl', PromiseDependency);
};

/**
* @api private
*/
AWS.S3.deletePromisesFromClass = function deletePromisesFromClass() {
delete this.prototype.getSignedUrlPromise;
};

AWS.util.addPromises(AWS.S3);
9 changes: 8 additions & 1 deletion lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -813,18 +813,25 @@ var util = {

/**
* @api private
* Return a function that will return a promise whose fate is decided by the
* callback behavior of the given method with `methodName`. The method to be
* promisified should conform to node.js convention of accepting a callback as
* last argument and calling that callback with error as the first argument
* and success value on the second argument.
*/
promisifyMethod: function promisifyMethod(methodName, PromiseDependency) {
return function promise() {
var self = this;
var args = Array.prototype.slice.call(arguments);
return new PromiseDependency(function(resolve, reject) {
self[methodName](function(err, data) {
args.push(function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
self[methodName].apply(self, args);
});
};
},
Expand Down
2 changes: 1 addition & 1 deletion test/config.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ describe('AWS.config', function() {
AWS.config.setPromisesDependency(function() {});
expect(utilSpy.calls.length).to.equal(1);
expect(Array.isArray(utilSpy.calls[0]['arguments'][0])).to.be['true'];
expect(utilSpy.calls[0]['arguments'][0].length).to.equal(4);
expect(utilSpy.calls[0]['arguments'][0].length).to.equal(5);
});

if (typeof Promise !== 'undefined') {
Expand Down
73 changes: 72 additions & 1 deletion test/services/s3.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2926,10 +2926,81 @@ describe('AWS.S3', function() {
);
done();
});

invocationDeferred = true;
});
});
describe('getSignedUrlPromise', function() {
var catchFunction, resolveFunction, err, url, date;
err = null;
url = null;
date = null;

catchFunction = function(e) {
err = e;
};

resolveFunction = function(u) {
url = u;
};

beforeEach(function(done) {
err = null;
url = null;
originalDate = s3.getSkewCorrectedDate;
s3.getSkewCorrectedDate = function() {
return new Date(0);
};
AWS.config.setPromisesDependency();
return done();
});

afterEach(function(done) {
s3.getSkewCorrectedDate = originalDate;
done();
});

it('exists if Promises are available', function() {
if (typeof Promise === 'undefined') {
expect(typeof s3.getSignedUrlPromise).to.equal('undefined');
} else {
expect(typeof s3.getSignedUrlPromise).to.equal('function');
}
});

it('returns a promise when called', function() {
var P = function() {};
AWS.config.setPromisesDependency(P);
var operation = s3.getSignedUrlPromise;
expect(typeof operation).to.equal('function');
var urlPromise = s3.getSignedUrlPromise('getObject', {
Bucket: 'bucket',
Key: 'key'
});
expect(urlPromise instanceof P).to.equal(true);
});

if (typeof Promise === 'function') {
it('resolves when getSignedUrl is successful', function() {
s3.getSignedUrlPromise('getObject', {
Bucket: 'bucket',
Key: 'key'
}).then(resolveFunction).catch(catchFunction).then(function() {
expect(url).to.equal('https://bucket.s3.amazonaws.com/key?AWSAccessKeyId=akid&Expires=900&Signature=4mlYnRmz%2BBFEPrgYz5tXcl9Wc4w%3D&x-amz-security-token=session');
expect(err).to.be['null'];
});
});

it('rejects when getSignedUrl is unsuccessful', function() {
s3.getSignedUrlPromise('invalidOperation', {
Bucket: 'bucket',
Key: 'key',
}).then(resolveFunction).catch(catchFunction).then(function() {
expect(url).to.be['null'];
expect(err).to.not.be['null'];
});
});
}
});

describe('createPresignedPost', function() {
it('should include a url and a hash of form fields', function(done) {
Expand Down
8 changes: 8 additions & 0 deletions ts/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,14 @@ s3.putObject({
Body: fs.createReadStream('/fake/path')
});

const printUrl = (url: string)=> {
console.log(url);
}
s3.getSignedUrlPromise('getObject', {
Bucket: 'bucket',
Key: 'key'
}).then(printUrl);

const upload = s3.upload(
{
Bucket: 'BUCKET',
Expand Down