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

Adds Automatic Support to Not Use Multipart Uploads If File Size <= partSize #387

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
58 changes: 49 additions & 9 deletions evaporate.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
logging: true,
maxConcurrentParts: 5,
partSize: 6 * 1024 * 1024,
enablePartSizeOptimization: true,
retryBackoffPower: 2,
maxRetryBackoffSecs: 300,
progressIntervalMS: 1000,
Expand Down Expand Up @@ -459,6 +460,7 @@
this.id = decodeURIComponent(this.con.bucket + '/' + this.name);

this.signParams = con.signParams;
this.numParts = Math.ceil(this.sizeBytes / this.con.partSize) || 1; // issue #58
}
FileUpload.prototype.con = undefined;
FileUpload.prototype.evaporate = undefined;
Expand Down Expand Up @@ -677,7 +679,6 @@
});
};
FileUpload.prototype.makeParts = function (firstPart) {
this.numParts = Math.ceil(this.sizeBytes / this.con.partSize) || 1; // issue #58
var partsDeferredPromises = [];

var self = this;
Expand Down Expand Up @@ -711,7 +712,7 @@
} else {
s3Part = this.makePart(part, PENDING, this.sizeBytes);
}
s3Part.awsRequest = new PutPart(this, s3Part);
s3Part.awsRequest = this.numParts === 1 && this.con.enablePartSizeOptimization ? new PutObject(this, s3Part) : new PutPart(this, s3Part);
s3Part.awsRequest.awsDeferred.promise
.then(resolve(s3Part), reject(s3Part));

Expand Down Expand Up @@ -862,7 +863,11 @@
.send()
.then(
function (xhr) {
self.eTag = elementText(xhr.responseText, "ETag").replace(/&quot;/g, '"');
if (self.numParts === 1 && self.con.enablePartSizeOptimization) {
self.eTag = self.partsOnS3[0].eTag;
} else {
self.eTag = elementText(xhr.responseText, "ETag").replace(/&quot;/g, '"');
}
self.completeUploadFile(xhr);
});
};
Expand Down Expand Up @@ -1209,6 +1214,20 @@
return [ABORTED, CANCELED].indexOf(this.fileUpload.status) > -1;
};

function CancelableS3MultipartRequest(fileUpload, request) {
CancelableS3AWSRequest.call(this, fileUpload, request);
}
CancelableS3MultipartRequest.prototype = Object.create(CancelableS3AWSRequest.prototype);
CancelableS3MultipartRequest.prototype.constructor = CancelableS3MultipartRequest;
CancelableS3MultipartRequest.prototype.send = function () {
if (this.fileUpload.numParts === 1 && this.con.enablePartSizeOptimization) {
this.awsDeferred.resolve();
} else {
this.trySend();
}
return this.awsDeferred.promise;
};

function SignedS3AWSRequestWithRetryLimit(fileUpload, request, maxRetries) {
if (maxRetries > -1) {
this.maxRetries = maxRetries;
Expand Down Expand Up @@ -1243,10 +1262,10 @@
response_match: '<UploadId>(.+)<\/UploadId>'
};

CancelableS3AWSRequest.call(this, fileUpload, request);
CancelableS3MultipartRequest.call(this, fileUpload, request);
this.awsKey = awsKey;
}
InitiateMultipartUpload.prototype = Object.create(CancelableS3AWSRequest.prototype);
InitiateMultipartUpload.prototype = Object.create(CancelableS3MultipartRequest.prototype);
InitiateMultipartUpload.prototype.constructor = InitiateMultipartUpload;
InitiateMultipartUpload.prototype.success = function () {
var match = this.currentXhr.response.match(new RegExp(this.request.response_match));
Expand All @@ -1267,9 +1286,9 @@
x_amz_headers: fileUpload.xAmzHeadersCommon || fileUpload.xAmzHeadersAtComplete,
step: 'complete'
};
CancelableS3AWSRequest.call(this, fileUpload, request);
CancelableS3MultipartRequest.call(this, fileUpload, request);
}
CompleteMultipartUpload.prototype = Object.create(CancelableS3AWSRequest.prototype);
CompleteMultipartUpload.prototype = Object.create(CancelableS3MultipartRequest.prototype);
CompleteMultipartUpload.prototype.constructor = CompleteMultipartUpload;
CompleteMultipartUpload.prototype.getPayload = function () {
return Promise.resolve(this.fileUpload.getCompletedPayload());
Expand Down Expand Up @@ -1381,8 +1400,8 @@
return new Promise(function (resolve, reject) {
if (self.con.computeContentMd5 && !part.md5_digest) {
self.getPayload()
.then(function (data) {
var md5_digest = self.con.cryptoMd5Method(data);
.then(self.con.cryptoMd5Method)
.then(function (md5_digest) {
if (self.partNumber === 1 && self.con.computeContentMd5 && typeof self.fileUpload.firstMd5Digest === "undefined") {
self.fileUpload.firstMd5Digest = md5_digest;
self.fileUpload.updateUploadFile({firstMd5Digest: md5_digest})
Expand Down Expand Up @@ -1577,6 +1596,27 @@
};


//http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html
function PutObject(fileUpload, part) {
this.part = part;

this.partNumber = 1;
this.start = 0;
this.end = fileUpload.sizeBytes;

var request = {
method: 'PUT',
step: 'upload #' + this.partNumber,
x_amz_headers: fileUpload.xAmzHeadersCommon || fileUpload.xAmzHeadersAtUpload,
onProgress: this.onProgress.bind(this)
};

SignedS3AWSRequest.call(this, fileUpload, request);
}
PutObject.prototype = Object.create(PutPart.prototype);
PutObject.prototype.constructor = PutObject;


//http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadAbort.html
function DeleteMultipartUpload(fileUpload) {
fileUpload.info('will attempt to abort the upload');
Expand Down
6 changes: 4 additions & 2 deletions test/helpers/browser-env.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ const baseConfig = {
bucket: AWS_BUCKET,
logging: false,
maxRetryBackoffSecs: 0.1,
abortCompletionThrottlingMs: 0
abortCompletionThrottlingMs: 0,
enablePartSizeOptimization: false
}

function LocalStorage() {
Expand Down Expand Up @@ -74,7 +75,8 @@ let requestMap = {
'POST:uploads': 'initiate',
'POST:uploadId': 'complete',
'DELETE:uploadId': 'cancel',
'GET:uploadId': 'check for parts'
'GET:uploadId': 'check for parts',
'PUT': 'put object'
}

global.requestOrder = function (t) {
Expand Down
173 changes: 173 additions & 0 deletions test/optimized-part-size.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { expect } from 'chai'
import sinon from 'sinon'
import test from 'ava'

// constants

let server


function testCommon(t, addCfg, initConfig) {
let addConfig = Object.assign({}, { file: new File({
path: '/tmp/file',
size: 50,
name: 'tests'
})}, addCfg)

let evapConfig = Object.assign({}, {awsSignatureVersion: '2', enablePartSizeOptimization: true}, initConfig)
return testBase(t, addConfig, evapConfig)
}
function testMd5V2(t) {
return testCommon(t, {}, { awsSignatureVersion: '2', computeContentMd5: true })
}

function testMd5V4(t) {
return testCommon(t, {}, {
computeContentMd5: true,
cryptoHexEncodedHash256: function (d) { return d; }
})
}



test.before(() => {
sinon.xhr.supportsCORS = true
global.XMLHttpRequest = sinon.useFakeXMLHttpRequest()
global.window = {
localStorage: {},
console: console
};

server = serverCommonCase()
})

test.beforeEach((t) => {
beforeEachSetup(t)
})

// Callbacks
test('should call a callback on successful add()', (t) => {
return testCommon(t)
.then(function () {
expect(t.context.config.started.withArgs('bucket/' + t.context.requestedAwsObjectKey).calledOnce).to.be.true
})
})
test('should call a progress with stats callback on successful add()', (t) => {
return testCommon(t, {progress: sinon.spy()})
.then(function () {
expect(t.context.config.progress.firstCall.args.length).to.equal(2)
expect(typeof t.context.config.progress.firstCall.args[1]).to.equal('object')
})
})
test('should return the object key in the complete callback', (t) => {
let complete_id

let config = Object.assign({}, {}, {
name: AWS_UPLOAD_KEY,
complete: sinon.spy(function (xhr, name) { complete_id = name; })
})

return testCommon(t, config)
.then(function () {
expect(complete_id).to.equal(config.name)
expect(t.context.config.complete.firstCall.args.length).to.equal(3)
expect(t.context.config.complete.firstCall.args[0]).to.be.undefined
expect(typeof t.context.config.complete.firstCall.args[1]).to.equal('string')
expect(typeof t.context.config.complete.firstCall.args[2]).to.equal('object')
})

})


// Default Setup: V2 signatures: Common Case
test('should not call cryptoMd5 upload a file with defaults and V2 signature', (t) => {
return testCommon(t, {}, { awsSignatureVersion: '2' })
.then(function () {
expect(t.context.cryptoMd5.callCount).to.equal(0)
})
})
test('should upload a file with S3 requests in the correct order', (t) => {
return testCommon(t)
.then(function () {
expect(requestOrder(t)).to.equal('put object')
})
})
test('should upload a file and return the correct file upload ID', (t) => {
return testCommon(t)
.then(function () {
expect(t.context.completedAwsKey).to.equal(t.context.requestedAwsObjectKey)
})
})
test('should upload a file and callback complete once', (t) => {
return testCommon(t)
.then(function () {
expect(t.context.config.complete.calledOnce).to.be.true
})
})
test('should upload a file and callback complete with second param the awsKey', (t) => {
return testCommon(t)
.then(function () {
expect(t.context.config.complete.firstCall.args[1]).to.equal(t.context.requestedAwsObjectKey)
})
})
test('should upload a file and not callback with a changed object name', (t) => {
return testCommon(t, {nameChanged: sinon.spy()})
.then(function () {
expect(t.context.config.nameChanged.callCount).to.equal(0)
})
})

// md5Digest tests
test('V2 should call cryptoMd5 when uploading a file with defaults', (t) => {
return testMd5V2(t)
.then(function () {
expect(t.context.cryptoMd5.callCount).to.equal(1)
})
})
test('V2 should upload a file with MD5Digests with S3 requests in the correct order', (t) => {
return testMd5V2(t)
.then(function () {
expect(requestOrder(t)).to.equal('put object')
})
})
test('V2 should upload a file and return the correct file upload ID', (t) => {
return testMd5V2(t)
.then(function () {
expect(t.context.completedAwsKey).to.equal(t.context.requestedAwsObjectKey)
})
})

test('V4 should call cryptoMd5 when uploading a file with defaults', (t) => {
return testMd5V4(t)
.then(function () {
expect(t.context.cryptoMd5.callCount).to.equal(1)
})
})
test('V4 should upload a file with MD5Digests with S3 requests in the correct order', (t) => {
return testMd5V4(t)
.then(function () {
expect(requestOrder(t)).to.equal('put object')
})
})
test('V4 should upload a file and return the correct file upload ID', (t) => {
return testMd5V4(t)
.then(function () {
expect(t.context.completedAwsKey).to.equal(t.context.requestedAwsObjectKey)
})
})

test('should retry Upload Part', (t) => {
t.context.retry = function (type) {
return type === 'part'
}

return testCommon(t, { file: new File({
path: '/tmp/file',
size: 50,
name: 'tests'
})
})
.then(function () {
expect(requestOrder(t)).to.equal('put object,put object')
})
})