UNPKG

22.7 kBJavaScriptView Raw
1/*
2 * Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
5 * the License. A copy of the License is located at
6 *
7 * http://aws.amazon.com/apache2.0/
8 *
9 * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
10 * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
11 * and limitations under the License.
12 */
13var __assign = (this && this.__assign) || function () {
14 __assign = Object.assign || function(t) {
15 for (var s, i = 1, n = arguments.length; i < n; i++) {
16 s = arguments[i];
17 for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
18 t[p] = s[p];
19 }
20 return t;
21 };
22 return __assign.apply(this, arguments);
23};
24var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
25 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
26 return new (P || (P = Promise))(function (resolve, reject) {
27 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
28 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
29 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
30 step((generator = generator.apply(thisArg, _arguments || [])).next());
31 });
32};
33var __generator = (this && this.__generator) || function (thisArg, body) {
34 var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
35 return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
36 function verb(n) { return function (v) { return step([n, v]); }; }
37 function step(op) {
38 if (f) throw new TypeError("Generator is already executing.");
39 while (_) try {
40 if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
41 if (y = 0, t) op = [op[0] & 2, t.value];
42 switch (op[0]) {
43 case 0: case 1: t = op; break;
44 case 4: _.label++; return { value: op[1], done: false };
45 case 5: _.label++; y = op[1]; op = [0]; continue;
46 case 7: op = _.ops.pop(); _.trys.pop(); continue;
47 default:
48 if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
49 if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
50 if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
51 if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
52 if (t[2]) _.ops.pop();
53 _.trys.pop(); continue;
54 }
55 op = body.call(thisArg, _);
56 } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
57 if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
58 }
59};
60import { ConsoleLogger as Logger, getAmplifyUserAgent, Credentials, } from '@aws-amplify/core';
61import { S3Client, PutObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, ListPartsCommand, AbortMultipartUploadCommand, } from '@aws-sdk/client-s3';
62import { AxiosHttpHandler, SEND_PROGRESS_EVENT } from './axios-http-handler';
63import * as events from 'events';
64var logger = new Logger('AWSS3ProviderManagedUpload');
65var localTestingStorageEndpoint = 'http://localhost:20005';
66var SET_CONTENT_LENGTH_HEADER = 'contentLengthMiddleware';
67var AWSS3ProviderManagedUpload = /** @class */ (function () {
68 function AWSS3ProviderManagedUpload(params, opts, emitter) {
69 // Defaults
70 this.minPartSize = 5 * 1024 * 1024; // in MB
71 this.queueSize = 4;
72 // Data for current upload
73 this.body = null;
74 this.params = null;
75 this.opts = null;
76 this.completedParts = [];
77 this.cancel = false;
78 // Progress reporting
79 this.bytesUploaded = 0;
80 this.totalBytesToUpload = 0;
81 this.emitter = null;
82 this.params = params;
83 this.opts = opts;
84 this.emitter = emitter;
85 }
86 AWSS3ProviderManagedUpload.prototype.upload = function () {
87 return __awaiter(this, void 0, void 0, function () {
88 var _a, putObjectCommand, s3, uploadId, numberOfPartsToUpload, parts, start;
89 var _this = this;
90 return __generator(this, function (_b) {
91 switch (_b.label) {
92 case 0:
93 _a = this;
94 return [4 /*yield*/, this.validateAndSanitizeBody(this.params.Body)];
95 case 1:
96 _a.body = _b.sent();
97 this.totalBytesToUpload = this.byteLength(this.body);
98 if (!(this.totalBytesToUpload <= this.minPartSize)) return [3 /*break*/, 3];
99 // Multipart upload is not required. Upload the sanitized body as is
100 this.params.Body = this.body;
101 putObjectCommand = new PutObjectCommand(this.params);
102 return [4 /*yield*/, this._createNewS3Client(this.opts, this.emitter)];
103 case 2:
104 s3 = _b.sent();
105 return [2 /*return*/, s3.send(putObjectCommand)];
106 case 3: return [4 /*yield*/, this.createMultiPartUpload()];
107 case 4:
108 uploadId = _b.sent();
109 numberOfPartsToUpload = Math.ceil(this.totalBytesToUpload / this.minPartSize);
110 parts = this.createParts();
111 start = 0;
112 _b.label = 5;
113 case 5:
114 if (!(start < numberOfPartsToUpload)) return [3 /*break*/, 10];
115 /** This first block will try to cancel the upload if the cancel
116 * request came before any parts uploads have started.
117 **/
118 return [4 /*yield*/, this.checkIfUploadCancelled(uploadId)];
119 case 6:
120 /** This first block will try to cancel the upload if the cancel
121 * request came before any parts uploads have started.
122 **/
123 _b.sent();
124 // Upload as many as `queueSize` parts simultaneously
125 return [4 /*yield*/, this.uploadParts(uploadId, parts.slice(start, start + this.queueSize))];
126 case 7:
127 // Upload as many as `queueSize` parts simultaneously
128 _b.sent();
129 /** Call cleanup a second time in case there were part upload requests
130 * in flight. This is to ensure that all parts are cleaned up.
131 */
132 return [4 /*yield*/, this.checkIfUploadCancelled(uploadId)];
133 case 8:
134 /** Call cleanup a second time in case there were part upload requests
135 * in flight. This is to ensure that all parts are cleaned up.
136 */
137 _b.sent();
138 _b.label = 9;
139 case 9:
140 start += this.queueSize;
141 return [3 /*break*/, 5];
142 case 10:
143 parts.map(function (part) {
144 _this.removeEventListener(part);
145 });
146 return [4 /*yield*/, this.finishMultiPartUpload(uploadId)];
147 case 11:
148 // Step 3: Finalize the upload such that S3 can recreate the file
149 return [2 /*return*/, _b.sent()];
150 }
151 });
152 });
153 };
154 AWSS3ProviderManagedUpload.prototype.createParts = function () {
155 var parts = [];
156 for (var bodyStart = 0; bodyStart < this.totalBytesToUpload;) {
157 var bodyEnd = Math.min(bodyStart + this.minPartSize, this.totalBytesToUpload);
158 parts.push({
159 bodyPart: this.body.slice(bodyStart, bodyEnd),
160 partNumber: parts.length + 1,
161 emitter: new events.EventEmitter(),
162 _lastUploadedBytes: 0,
163 });
164 bodyStart += this.minPartSize;
165 }
166 return parts;
167 };
168 AWSS3ProviderManagedUpload.prototype.createMultiPartUpload = function () {
169 return __awaiter(this, void 0, void 0, function () {
170 var createMultiPartUploadCommand, s3, response;
171 var _this = this;
172 return __generator(this, function (_a) {
173 switch (_a.label) {
174 case 0:
175 createMultiPartUploadCommand = new CreateMultipartUploadCommand(this.params);
176 return [4 /*yield*/, this._createNewS3Client(this.opts)];
177 case 1:
178 s3 = _a.sent();
179 // @aws-sdk/client-s3 seems to be ignoring the `ContentType` parameter, so we
180 // are explicitly adding it via middleware.
181 // https://github.com/aws/aws-sdk-js-v3/issues/2000
182 s3.middlewareStack.add(function (next) { return function (args) {
183 if (_this.params.ContentType &&
184 args &&
185 args.request &&
186 args.request.headers) {
187 args.request.headers['Content-Type'] = _this.params.ContentType;
188 }
189 return next(args);
190 }; }, {
191 step: 'build',
192 });
193 return [4 /*yield*/, s3.send(createMultiPartUploadCommand)];
194 case 2:
195 response = _a.sent();
196 logger.debug(response.UploadId);
197 return [2 /*return*/, response.UploadId];
198 }
199 });
200 });
201 };
202 /**
203 * @private Not to be extended outside of tests
204 * @VisibleFotTesting
205 */
206 AWSS3ProviderManagedUpload.prototype.uploadParts = function (uploadId, parts) {
207 return __awaiter(this, void 0, void 0, function () {
208 var allResults, i, error_1;
209 var _this = this;
210 return __generator(this, function (_a) {
211 switch (_a.label) {
212 case 0:
213 _a.trys.push([0, 2, , 3]);
214 return [4 /*yield*/, Promise.all(parts.map(function (part) { return __awaiter(_this, void 0, void 0, function () {
215 var s3;
216 return __generator(this, function (_a) {
217 switch (_a.label) {
218 case 0:
219 this.setupEventListener(part);
220 return [4 /*yield*/, this._createNewS3Client(this.opts, part.emitter)];
221 case 1:
222 s3 = _a.sent();
223 return [2 /*return*/, s3.send(new UploadPartCommand({
224 PartNumber: part.partNumber,
225 Body: part.bodyPart,
226 UploadId: uploadId,
227 Key: this.params.Key,
228 Bucket: this.params.Bucket,
229 }))];
230 }
231 });
232 }); }))];
233 case 1:
234 allResults = _a.sent();
235 // The order of resolved promises is the same as input promise order.
236 for (i = 0; i < allResults.length; i++) {
237 this.completedParts.push({
238 PartNumber: parts[i].partNumber,
239 ETag: allResults[i].ETag,
240 });
241 }
242 return [3 /*break*/, 3];
243 case 2:
244 error_1 = _a.sent();
245 logger.error('error happened while uploading a part. Cancelling the multipart upload', error_1);
246 this.cancelUpload();
247 return [2 /*return*/];
248 case 3: return [2 /*return*/];
249 }
250 });
251 });
252 };
253 AWSS3ProviderManagedUpload.prototype.finishMultiPartUpload = function (uploadId) {
254 return __awaiter(this, void 0, void 0, function () {
255 var input, completeUploadCommand, s3, data, error_2;
256 return __generator(this, function (_a) {
257 switch (_a.label) {
258 case 0:
259 input = {
260 Bucket: this.params.Bucket,
261 Key: this.params.Key,
262 UploadId: uploadId,
263 MultipartUpload: { Parts: this.completedParts },
264 };
265 completeUploadCommand = new CompleteMultipartUploadCommand(input);
266 return [4 /*yield*/, this._createNewS3Client(this.opts)];
267 case 1:
268 s3 = _a.sent();
269 _a.label = 2;
270 case 2:
271 _a.trys.push([2, 4, , 5]);
272 return [4 /*yield*/, s3.send(completeUploadCommand)];
273 case 3:
274 data = _a.sent();
275 return [2 /*return*/, data.Key];
276 case 4:
277 error_2 = _a.sent();
278 logger.error('error happened while finishing the upload. Cancelling the multipart upload', error_2);
279 this.cancelUpload();
280 return [2 /*return*/];
281 case 5: return [2 /*return*/];
282 }
283 });
284 });
285 };
286 AWSS3ProviderManagedUpload.prototype.checkIfUploadCancelled = function (uploadId) {
287 return __awaiter(this, void 0, void 0, function () {
288 var errorMessage, error_3;
289 return __generator(this, function (_a) {
290 switch (_a.label) {
291 case 0:
292 if (!this.cancel) return [3 /*break*/, 5];
293 errorMessage = 'Upload was cancelled.';
294 _a.label = 1;
295 case 1:
296 _a.trys.push([1, 3, , 4]);
297 return [4 /*yield*/, this.cleanup(uploadId)];
298 case 2:
299 _a.sent();
300 return [3 /*break*/, 4];
301 case 3:
302 error_3 = _a.sent();
303 errorMessage += error_3.errorMessage;
304 return [3 /*break*/, 4];
305 case 4: throw new Error(errorMessage);
306 case 5: return [2 /*return*/];
307 }
308 });
309 });
310 };
311 AWSS3ProviderManagedUpload.prototype.cancelUpload = function () {
312 this.cancel = true;
313 };
314 AWSS3ProviderManagedUpload.prototype.cleanup = function (uploadId) {
315 return __awaiter(this, void 0, void 0, function () {
316 var input, s3, data;
317 return __generator(this, function (_a) {
318 switch (_a.label) {
319 case 0:
320 // Reset this's state
321 this.body = null;
322 this.completedParts = [];
323 this.bytesUploaded = 0;
324 this.totalBytesToUpload = 0;
325 input = {
326 Bucket: this.params.Bucket,
327 Key: this.params.Key,
328 UploadId: uploadId,
329 };
330 return [4 /*yield*/, this._createNewS3Client(this.opts)];
331 case 1:
332 s3 = _a.sent();
333 return [4 /*yield*/, s3.send(new AbortMultipartUploadCommand(input))];
334 case 2:
335 _a.sent();
336 return [4 /*yield*/, s3.send(new ListPartsCommand(input))];
337 case 3:
338 data = _a.sent();
339 if (data && data.Parts && data.Parts.length > 0) {
340 throw new Error('Multi Part upload clean up failed');
341 }
342 return [2 /*return*/];
343 }
344 });
345 });
346 };
347 AWSS3ProviderManagedUpload.prototype.removeEventListener = function (part) {
348 part.emitter.removeAllListeners(SEND_PROGRESS_EVENT);
349 };
350 AWSS3ProviderManagedUpload.prototype.setupEventListener = function (part) {
351 var _this = this;
352 part.emitter.on(SEND_PROGRESS_EVENT, function (progress) {
353 _this.progressChanged(part.partNumber, progress.loaded - part._lastUploadedBytes);
354 part._lastUploadedBytes = progress.loaded;
355 });
356 };
357 AWSS3ProviderManagedUpload.prototype.progressChanged = function (partNumber, incrementalUpdate) {
358 this.bytesUploaded += incrementalUpdate;
359 this.emitter.emit(SEND_PROGRESS_EVENT, {
360 loaded: this.bytesUploaded,
361 total: this.totalBytesToUpload,
362 part: partNumber,
363 key: this.params.Key,
364 });
365 };
366 AWSS3ProviderManagedUpload.prototype.byteLength = function (input) {
367 if (input === null || input === undefined)
368 return 0;
369 if (typeof input.byteLength === 'number') {
370 return input.byteLength;
371 }
372 else if (typeof input.length === 'number') {
373 return input.length;
374 }
375 else if (typeof input.size === 'number') {
376 return input.size;
377 }
378 else if (typeof input.path === 'string') {
379 /* NodeJs Support
380 return require('fs').lstatSync(input.path).size;
381 */
382 }
383 else {
384 throw new Error('Cannot determine length of ' + input);
385 }
386 };
387 AWSS3ProviderManagedUpload.prototype.validateAndSanitizeBody = function (body) {
388 return __awaiter(this, void 0, void 0, function () {
389 return __generator(this, function (_a) {
390 if (this.isGenericObject(body)) {
391 // Any javascript object
392 return [2 /*return*/, JSON.stringify(body)];
393 }
394 else {
395 // Files, arrayBuffer etc
396 return [2 /*return*/, body];
397 }
398 return [2 /*return*/];
399 });
400 });
401 };
402 AWSS3ProviderManagedUpload.prototype.isGenericObject = function (body) {
403 if (body !== null && typeof body === 'object') {
404 try {
405 return !(this.byteLength(body) >= 0);
406 }
407 catch (error) {
408 // If we cannot determine the length of the body, consider it
409 // as a generic object and upload a stringified version of it
410 return true;
411 }
412 }
413 return false;
414 };
415 /**
416 * @private
417 * creates an S3 client with new V3 aws sdk
418 */
419 AWSS3ProviderManagedUpload.prototype._createNewS3Client = function (config, emitter) {
420 return __awaiter(this, void 0, void 0, function () {
421 var credentials, region, dangerouslyConnectToHttpEndpointForTesting, cancelTokenSource, localTestingConfig, client;
422 return __generator(this, function (_a) {
423 switch (_a.label) {
424 case 0: return [4 /*yield*/, this._getCredentials()];
425 case 1:
426 credentials = _a.sent();
427 region = config.region, dangerouslyConnectToHttpEndpointForTesting = config.dangerouslyConnectToHttpEndpointForTesting, cancelTokenSource = config.cancelTokenSource;
428 localTestingConfig = {};
429 if (dangerouslyConnectToHttpEndpointForTesting) {
430 localTestingConfig = {
431 endpoint: localTestingStorageEndpoint,
432 tls: false,
433 bucketEndpoint: false,
434 forcePathStyle: true,
435 };
436 }
437 client = new S3Client(__assign(__assign({ region: region,
438 credentials: credentials }, localTestingConfig), { requestHandler: new AxiosHttpHandler({}, emitter, cancelTokenSource), customUserAgent: getAmplifyUserAgent() }));
439 client.middlewareStack.remove(SET_CONTENT_LENGTH_HEADER);
440 return [2 /*return*/, client];
441 }
442 });
443 });
444 };
445 /**
446 * @private
447 */
448 AWSS3ProviderManagedUpload.prototype._getCredentials = function () {
449 return Credentials.get()
450 .then(function (credentials) {
451 if (!credentials)
452 return false;
453 var cred = Credentials.shear(credentials);
454 logger.debug('set credentials for storage', cred);
455 return cred;
456 })
457 .catch(function (error) {
458 logger.warn('ensure credentials error', error);
459 return false;
460 });
461 };
462 return AWSS3ProviderManagedUpload;
463}());
464export { AWSS3ProviderManagedUpload };
465//# sourceMappingURL=AWSS3ProviderManagedUpload.js.map
\No newline at end of file