Skip to content

Commit e6671c4

Browse files
authoredMar 23, 2017
Add a means of specifying tags for s3.upload that applies to both single part and multipart uploads (#1425)
* Add a means of specifying tags for s3.upload that applies to both single part and multipart uploads * Add an managed upload example with tags * Fix test sporadic test failure for s3.createPresignedPost * Throw an error when invalid tags are passed in rather than validating when generating header
1 parent 1a46ee6 commit e6671c4

File tree

6 files changed

+173
-37
lines changed

6 files changed

+173
-37
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "feature",
3+
"category": "S3",
4+
"description": "Adds a means of specifying tags to apply to objects of any size uploaded with AWS.S3.ManagedUploader"
5+
}

‎lib/s3/managed_upload.d.ts

+14-10
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class ManagedUpload {
1919
send(callback?: (err: AWSError, data: ManagedUpload.SendData) => void): void;
2020
/**
2121
* Adds a listener that is triggered when theuploader has uploaded more data.
22-
*
22+
*
2323
* @param {string} event - httpUploadProgress: triggered when the uploader has uploaded more data.
2424
* @param {function} listener - Callback to run when the uploader has uploaded more data.
2525
*/
@@ -44,45 +44,49 @@ export namespace ManagedUpload {
4444
/**
4545
* URL of the uploaded object.
4646
*/
47-
Location: string
47+
Location: string;
4848
/**
4949
* ETag of the uploaded object.
5050
*/
51-
ETag: string
51+
ETag: string;
5252
/**
5353
* Bucket to which the object was uploaded.
5454
*/
55-
Bucket: string
55+
Bucket: string;
5656
/**
5757
* Key to which the object was uploaded.
5858
*/
59-
Key: string
59+
Key: string;
6060
}
6161
export interface ManagedUploadOptions {
6262
/**
6363
* A map of parameters to pass to the upload requests.
6464
* The "Body" parameter is required to be specified either on the service or in the params option.
6565
*/
66-
params?: S3.Types.PutObjectRequest
66+
params?: S3.Types.PutObjectRequest;
6767
/**
6868
* The size of the concurrent queue manager to upload parts in parallel. Set to 1 for synchronous uploading of parts. Note that the uploader will buffer at most queueSize * partSize bytes into memory at any given time.
6969
* default: 4
7070
*/
71-
queueSize?: number
71+
queueSize?: number;
7272
/**
7373
* Default: 5 mb
7474
* The size in bytes for each individual part to be uploaded. Adjust the part size to ensure the number of parts does not exceed maxTotalParts. See minPartSize for the minimum allowed part size.
7575
*/
76-
partSize?: number
76+
partSize?: number;
7777
/**
7878
* Default: false
7979
* Whether to abort the multipart upload if an error occurs. Set to true if you want to handle failures manually.
8080
*/
81-
leavePartsOnError?: boolean
81+
leavePartsOnError?: boolean;
8282
/**
8383
* An optional S3 service object to use for requests.
8484
* This object might have bound parameters used by the uploader.
8585
*/
86-
service?: S3
86+
service?: S3;
87+
/**
88+
* The tags to apply to the object.
89+
*/
90+
tags?: Array<S3.Tag>;
8791
}
8892
}

‎lib/s3/managed_upload.js

+44-3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ AWS.S3.ManagedUpload = AWS.util.inherit({
6060
* failures manually.
6161
* @option options service [AWS.S3] an optional S3 service object to use for
6262
* requests. This object might have bound parameters used by the uploader.
63+
* @option options tags [Array<map>] The tags to apply to the uploaded object.
64+
* Each tag should have a `Key` and `Value` keys.
6365
* @example Creating a default uploader for a stream object
6466
* var upload = new AWS.S3.ManagedUpload({
6567
* params: {Bucket: 'bucket', Key: 'key', Body: stream}
@@ -69,6 +71,11 @@ AWS.S3.ManagedUpload = AWS.util.inherit({
6971
* partSize: 10 * 1024 * 1024, queueSize: 1,
7072
* params: {Bucket: 'bucket', Key: 'key', Body: stream}
7173
* });
74+
* @example Creating an uploader with tags
75+
* var upload = new AWS.S3.ManagedUpload({
76+
* params: {Bucket: 'bucket', Key: 'key', Body: stream},
77+
* tags: [{Key: 'tag1', Value: 'value1'}, {Key: 'tag2', Value: 'value2'}]
78+
* });
7279
* @see send
7380
*/
7481
constructor: function ManagedUpload(options) {
@@ -96,6 +103,13 @@ AWS.S3.ManagedUpload = AWS.util.inherit({
96103
if (options.queueSize) this.queueSize = options.queueSize;
97104
if (options.partSize) this.partSize = options.partSize;
98105
if (options.leavePartsOnError) this.leavePartsOnError = true;
106+
if (options.tags) {
107+
if (!Array.isArray(options.tags)) {
108+
throw new Error('Tags must be specified as an array; ' +
109+
typeof options.tags + ' provided.');
110+
}
111+
this.tags = options.tags;
112+
}
99113

100114
if (this.partSize < this.minPartSize) {
101115
throw new Error('partSize must be greater than ' +
@@ -448,7 +462,11 @@ AWS.S3.ManagedUpload = AWS.util.inherit({
448462

449463
var partNumber = ++self.totalPartNumbers;
450464
if (self.isDoneChunking && partNumber === 1) {
451-
var req = self.service.putObject({Body: chunk});
465+
var params = {Body: chunk};
466+
if (this.tags) {
467+
params.Tagging = this.getTaggingHeader();
468+
}
469+
var req = self.service.putObject(params);
452470
req._managedUpload = self;
453471
req.on('httpUploadProgress', self.progress).send(self.finishSinglePart);
454472
return null;
@@ -487,6 +505,19 @@ AWS.S3.ManagedUpload = AWS.util.inherit({
487505
}
488506
},
489507

508+
/**
509+
* @api private
510+
*/
511+
getTaggingHeader: function getTaggingHeader() {
512+
var kvPairStrings = [];
513+
for (var i = 0; i < this.tags.length; i++) {
514+
kvPairStrings.push(AWS.util.uriEscape(this.tags[i].Key) + '=' +
515+
AWS.util.uriEscape(this.tags[i].Value));
516+
}
517+
518+
return kvPairStrings.join('&');
519+
},
520+
490521
/**
491522
* @api private
492523
*/
@@ -583,8 +614,18 @@ AWS.S3.ManagedUpload = AWS.util.inherit({
583614
var self = this;
584615
var completeParams = { MultipartUpload: { Parts: self.completeInfo.slice(1) } };
585616
self.service.completeMultipartUpload(completeParams, function(err, data) {
586-
if (err) return self.cleanup(err);
587-
else self.callback(err, data);
617+
if (err) {
618+
return self.cleanup(err);
619+
}
620+
621+
if (Array.isArray(self.tags)) {
622+
self.service.putObjectTagging(
623+
{Tagging: {TagSet: self.tags}},
624+
self.callback
625+
);
626+
} else {
627+
self.callback(err, data);
628+
}
588629
});
589630
},
590631

‎test/s3/managed_upload.spec.coffee

+78
Original file line numberDiff line numberDiff line change
@@ -530,3 +530,81 @@ describe 'AWS.S3.ManagedUpload', ->
530530
return upload.promise().then(thenFunction).catch(catchFunction).then ->
531531
expect(data).not.to.exist
532532
expect(err.message).to.equal('ERROR')
533+
534+
describe 'tagging', ->
535+
it 'should embed tags in PutObject request for single part uploads', (done) ->
536+
reqs = helpers.mockResponses [
537+
data: ETag: 'ETAG'
538+
]
539+
540+
upload = new AWS.S3.ManagedUpload(
541+
service: s3
542+
params: {Body: smallbody}
543+
tags: [
544+
{Key: 'tag1', Value: 'value1'}
545+
{Key: 'tag2', Value: 'value2'}
546+
{Key: 'étiquette', Value: 'valeur à être encodé'}
547+
]
548+
)
549+
550+
send {}, ->
551+
expect(err).not.to.exist
552+
expect(reqs[0].httpRequest.headers['x-amz-tagging']).to.equal('tag1=value1&tag2=value2&%C3%A9tiquette=valeur%20%C3%A0%20%C3%AAtre%20encod%C3%A9')
553+
done()
554+
555+
it 'should send a PutObjectTagging request following a successful multipart upload with tags', (done) ->
556+
reqs = helpers.mockResponses [
557+
{ data: UploadId: 'uploadId' }
558+
{ data: ETag: 'ETAG1' }
559+
{ data: ETag: 'ETAG2' }
560+
{ data: ETag: 'ETAG3' }
561+
{ data: ETag: 'ETAG4' }
562+
{ data: ETag: 'FINAL_ETAG', Location: 'FINAL_LOCATION' }
563+
{}
564+
]
565+
566+
upload = new AWS.S3.ManagedUpload(
567+
service: s3
568+
params: {Body: bigbody}
569+
tags: [
570+
{Key: 'tag1', Value: 'value1'}
571+
{Key: 'tag2', Value: 'value2'}
572+
{Key: 'étiquette', Value: 'valeur à être encodé'}
573+
]
574+
)
575+
576+
send {}, ->
577+
expect(helpers.operationsForRequests(reqs)).to.eql [
578+
's3.createMultipartUpload'
579+
's3.uploadPart'
580+
's3.uploadPart'
581+
's3.uploadPart'
582+
's3.uploadPart'
583+
's3.completeMultipartUpload'
584+
's3.putObjectTagging'
585+
]
586+
expect(err).not.to.exist
587+
expect(reqs[6].params.Tagging).to.deep.equal({
588+
TagSet: [
589+
{Key: 'tag1', Value: 'value1'}
590+
{Key: 'tag2', Value: 'value2'}
591+
{Key: 'étiquette', Value: 'valeur à être encodé'}
592+
]
593+
})
594+
done()
595+
596+
it 'should throw when tags are not provided as an array', (done) ->
597+
reqs = helpers.mockResponses [
598+
data: ETag: 'ETAG'
599+
]
600+
601+
try
602+
upload = new AWS.S3.ManagedUpload(
603+
service: s3
604+
params: {Body: smallbody}
605+
tags: 'tag1=value1&tag2=value2&%C3%A9tiquette=valeur%20%C3%A0%20%C3%AAtre%20encod%C3%A9'
606+
)
607+
done(new Error('AWS.S3.ManagedUpload should have thrown when passed a string for tags'))
608+
catch e
609+
done()
610+

‎test/services/s3.spec.coffee

+13-19
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe 'AWS.S3', ->
9797
return 'v4'
9898
else if (signer == AWS.Signers.V2)
9999
return 'v2'
100-
100+
101101
describe 'when using presigned requests', ->
102102
req = null
103103

@@ -152,7 +152,7 @@ describe 'AWS.S3', ->
152152
it 'user does not specify a signatureVersion and region supports v2', (done) ->
153153
s3 = new AWS.S3({region: 'us-east-1'})
154154
expect(getVersion(s3.getSignerClass())).to.equal('s3')
155-
done()
155+
done()
156156

157157
describe 'will return a v4 signer when', ->
158158

@@ -247,21 +247,21 @@ describe 'AWS.S3', ->
247247
describe 'with useAccelerateEndpoint and dualstack set to true', ->
248248
beforeEach ->
249249
s3 = new AWS.S3(useAccelerateEndpoint: true, useDualstack: true)
250-
250+
251251
it 'changes the hostname to use s3-accelerate for dns-comaptible buckets', ->
252252
req = build('getObject', {Bucket: 'foo', Key: 'bar'})
253253
expect(req.endpoint.hostname).to.equal('foo.s3-accelerate.dualstack.amazonaws.com')
254-
254+
255255
it 'overrides s3BucketEndpoint configuration when s3BucketEndpoint is set', ->
256256
s3 = new AWS.S3(useAccelerateEndpoint: true, useDualstack: true, s3BucketEndpoint: true, endpoint: 'foo.region.amazonaws.com')
257257
req = build('getObject', {Bucket: 'foo', Key: 'baz'})
258258
expect(req.endpoint.hostname).to.equal('foo.s3-accelerate.dualstack.amazonaws.com')
259-
259+
260260
describe 'does not use s3-accelerate.dualstack or s3-accelerate', ->
261261
it 'on dns-incompatible buckets', ->
262262
req = build('getObject', {Bucket: 'foo.baz', Key: 'bar'})
263263
expect(req.endpoint.hostname).to.not.contain('s3-accelerate')
264-
264+
265265
it 'on excluded operations', ->
266266
req = build('listBuckets')
267267
expect(req.endpoint.hostname).to.not.contain('s3-accelerate')
@@ -902,7 +902,7 @@ describe 'AWS.S3', ->
902902
s3.bucketRegionCache.name = 'eu-west-1'
903903
fn()
904904
req = callNetworkingErrorListener()
905-
expect(spy.calls.length).to.equal(1)
905+
expect(spy.calls.length).to.equal(1)
906906
expect(regionReq.httpRequest.region).to.equal('us-east-1')
907907
expect(regionReq.httpRequest.endpoint.hostname).to.equal('name.s3.amazonaws.com')
908908
expect(req.httpRequest.region).to.equal('eu-west-1')
@@ -1253,7 +1253,7 @@ describe 'AWS.S3', ->
12531253
s3 = new AWS.S3()
12541254
params = Bucket: 'name'
12551255
s3.bucketRegionCache.name = 'rg-fake-1'
1256-
helpers.mockHttpResponse 204, {}, ''
1256+
helpers.mockHttpResponse 204, {}, ''
12571257
s3.deleteBucket params, ->
12581258
expect(s3.bucketRegionCache.name).to.not.exist
12591259

@@ -1323,7 +1323,7 @@ describe 'AWS.S3', ->
13231323
req = s3.putObject(Bucket: 'example', Key: 'key', Body: new Stream.Stream, ContentLength: 10)
13241324
req.send (err) ->
13251325
expect(err).not.to.exist
1326-
done()
1326+
done()
13271327

13281328
it 'opens separate stream if a file object is provided (signed payload)', (done) ->
13291329
hash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
@@ -1542,26 +1542,20 @@ describe 'AWS.S3', ->
15421542
done()
15431543

15441544
it 'should default to expiration in one hour', (done) ->
1545+
helpers.spyOn(AWS.util.date, 'getDate').andReturn(new Date(946684800 * 1000))
15451546
s3 = new AWS.S3()
15461547
s3.createPresignedPost {Bucket: 'bucket'}, (err, data) ->
15471548
decoded = JSON.parse(AWS.util.base64.decode(data.fields.Policy))
1548-
expiration = new Date(decoded.expiration)
1549-
validForMs = expiration.valueOf() - (new Date()).valueOf()
1550-
# allow one second of leeway
1551-
expect(validForMs).to.be.above((60 * 60 - 1) * 1000)
1552-
expect(validForMs).to.be.below((60 * 60 + 1) * 1000)
1549+
expect(decoded.expiration).to.equal('2000-01-01T01:00:00Z')
15531550
done()
15541551

15551552
it 'should allow users to provide a custom expiration', (done) ->
1553+
helpers.spyOn(AWS.util.date, 'getDate').andReturn(new Date(946684800 * 1000))
15561554
customTtl = 900
15571555
s3 = new AWS.S3()
15581556
s3.createPresignedPost {Bucket: 'bucket', Expires: customTtl}, (err, data) ->
15591557
decoded = JSON.parse(AWS.util.base64.decode(data.fields.Policy))
1560-
expiration = new Date(decoded.expiration)
1561-
validForMs = expiration.valueOf() - (new Date()).valueOf()
1562-
# allow one second of leeway
1563-
expect(validForMs).to.be.above((customTtl - 1) * 1000)
1564-
expect(validForMs).to.be.below((customTtl + 1) * 1000)
1558+
expect(decoded.expiration).to.equal('2000-01-01T00:15:00Z')
15651559
done()
15661560

15671561
it 'should include signature metadata as conditions', (done) ->

‎ts/s3.ts

+19-5
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,25 @@ s3.putObject({
126126
Body: fs.createReadStream('/fake/path')
127127
});
128128

129-
const upload = s3.upload({
130-
Bucket: 'BUCKET',
131-
Key: 'KEY',
132-
Body: new Buffer('some data')
133-
});
129+
const upload = s3.upload(
130+
{
131+
Bucket: 'BUCKET',
132+
Key: 'KEY',
133+
Body: new Buffer('some data')
134+
},
135+
{
136+
tags: [
137+
{
138+
Key: 'key',
139+
Value: 'value',
140+
},
141+
{
142+
Key: 'otherKey',
143+
Value: 'otherValue',
144+
},
145+
],
146+
}
147+
);
134148

135149
// test managed upload promise support
136150
upload.promise()

0 commit comments

Comments
 (0)
Please sign in to comment.