UNPKG

@modbox/s3-uploads-client

Version:
449 lines (448 loc) 23.1 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { 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; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UploadManager = void 0; var types_1 = require("./types"); var UploadManager = /** @class */ (function () { function UploadManager(_a) { var _this = this; var uploads = _a.uploads, maxConcurrent = _a.maxConcurrent, _b = _a.cancelAllOnError, cancelAllOnError = _b === void 0 ? false : _b, onProgress = _a.onProgress, onComplete = _a.onComplete, onUploadComplete = _a.onUploadComplete, onError = _a.onError, getPartRequest = _a.getPartRequest; this.onProgress = null; this.onComplete = null; this.onUploadComplete = null; this.onError = null; this.getPartRequest = null; this.chunks = []; this.totalChunksCount = 0; this.completedChunksCount = 0; this.isCanceled = false; this.pendingChunkUploads = []; this.completedChunks = []; this.completedUploads = []; this.resolvePromise = null; this.rejectPromise = null; this.onProgress = onProgress || null; this.onComplete = onComplete || null; this.onUploadComplete = onUploadComplete || null; this.onError = onError || null; this.getPartRequest = getPartRequest || null; this.maxConcurrent = maxConcurrent; this.cancelAllOnError = cancelAllOnError; this.uploads = uploads; this.uploadPromise = new Promise(function (resolve, reject) { _this.resolvePromise = resolve; _this.rejectPromise = reject; }); } UploadManager.prototype.prepareChunks = function () { return __awaiter(this, void 0, void 0, function () { var chunks; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.uploads.reduce(function (acc, u) { return __awaiter(_this, void 0, void 0, function () { var otherParts, parts; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, acc // For single file upload, the file represents a single chunk ]; case 1: otherParts = _a.sent(); // For single file upload, the file represents a single chunk if (u.uploadMode === types_1.UploadMode.Single) { return [2 /*return*/, __spreadArray(__spreadArray([], otherParts, true), [ { uploadMode: types_1.UploadMode.Single, uploadType: u.uploadType, uploadId: u.uploadId, partNumber: 0, request: u.presignedRequest, file: u.file, key: u.key, }, ], false)]; } parts = Array(u.partsCount) .fill(null) .map(function (_, idx) { var start = idx * u.chunkSize; var end = start + u.chunkSize; return { uploadMode: types_1.UploadMode.Multipart, uploadType: u.uploadType, start: start, end: end, file: u.file, key: u.key, uploadId: u.uploadId, partNumber: idx + 1, }; }, []); return [2 /*return*/, __spreadArray(__spreadArray([], otherParts, true), parts, true)]; } }); }); }, Promise.resolve([]))]; case 1: chunks = _a.sent(); this.chunks = chunks; this.totalChunksCount = chunks.length; this.onProgress && this.onProgress({ completedChunksCount: this.completedChunksCount, totalChunksCount: this.totalChunksCount, }); return [2 /*return*/]; } }); }); }; UploadManager.prototype.start = function () { return __awaiter(this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.prepareChunks()]; case 1: _a.sent(); this.upload(); return [2 /*return*/, this.uploads.map(function (u) { return (__assign(__assign({}, u), { cancel: function () { return _this.cancelOne(u.uploadId); } })); })]; } }); }); }; UploadManager.prototype.upload = function () { var _a; var _this = this; if (this.isCanceled) { return; } if (this.completedChunks.length === this.totalChunksCount) { this.complete(); return; } var countToLaunch = this.maxConcurrent - this.pendingChunkUploads.length; var toLaunch = this.chunks.slice(this.completedChunksCount + this.pendingChunkUploads.length, this.completedChunksCount + this.pendingChunkUploads.length + countToLaunch); (_a = this.pendingChunkUploads).push.apply(_a, toLaunch.map(function (chunk) { var pending = _this.uploadChunk(chunk); void pending.promise .then(function (completedChunk) { _this.chunkCompleted(completedChunk); _this.upload(); }) .catch(function (e) { _this.chunkErrored(pending, e); _this.upload(); }); return pending; })); }; UploadManager.prototype.completeOne = function (upload) { var uploadedFileProps = { uploadId: upload.uploadId, bucket: upload.bucket, key: encodeURIComponent(encodeURIComponent(upload.key)), filename: upload.filename, mimetype: upload.mimetype, size: upload.size, }; // If uploadMode is multipart, we gather the uploaded chunks // and add them to the uploaded file object var parts = upload.uploadMode === types_1.UploadMode.Multipart ? this.completedChunks.reduce(function (fileParts, c) { var uploadId = c.uploadId, part = __rest(c, ["uploadId"]); return uploadId === upload.uploadId ? fileParts.concat(part) : fileParts; }, []) : []; // Sort parts to ensure they are in ascending order parts.sort(function (a, b) { return a.partNumber - b.partNumber; }); // Add the completed upload to the list of completed uploads var uploadedFile = upload.uploadMode === types_1.UploadMode.Multipart ? __assign(__assign({ uploadMode: upload.uploadMode }, uploadedFileProps), { parts: parts }) : __assign({ uploadMode: upload.uploadMode }, uploadedFileProps); this.completedUploads.push(uploadedFile); // Send completed upload event if callback is provided this.onUploadComplete && this.onUploadComplete(uploadedFile, upload.file); }; UploadManager.prototype.complete = function () { this.onComplete && this.onComplete(this.completedUploads); this.resolvePromise && this.resolvePromise(this.completedUploads); }; UploadManager.prototype.promise = function () { return this.uploadPromise; }; UploadManager.prototype.cancel = function (reason) { this.isCanceled = true; this.rejectPromise && this.rejectPromise(new Error(reason || 'upload_cancelled')); this.pendingChunkUploads.forEach(function (u) { return u.cancel(); }); }; UploadManager.prototype.cancelOne = function (uploadId) { // Remove all the chunks associated with the uploadId of the errored chunk // From the completed chunks list this.completedChunks = this.completedChunks.filter(function (uploadedChunk) { return uploadedChunk.uploadId !== uploadId; }); // From the queued chunks list this.chunks = this.chunks.filter(function (chunk) { return chunk.uploadId !== uploadId; }); // From the pending chunks list, and cancel those as well this.pendingChunkUploads .filter(function (chunk) { return chunk.uploadId === uploadId; }) .forEach(function (chunk) { return chunk.cancel(); }); this.pendingChunkUploads = this.pendingChunkUploads.filter(function (pendingChunkUpload) { return pendingChunkUpload.uploadId !== uploadId; }); // Remove the upload itself from the uploads list this.uploads = this.uploads.filter(function (u) { return u.uploadId !== uploadId; }); // Update the counters this.completedChunksCount = this.completedChunks.length; this.totalChunksCount = this.chunks.length; }; UploadManager.prototype.chunkCompleted = function (uploadedChunk) { // Get the chunk associated upload var upload = this.uploads.find(function (u) { return u.uploadId === uploadedChunk.uploadId; }); if (!upload) { throw new Error('upload_internal_error'); } // Remove the completed upload from the pending upload list var pendingUploadIdx = this.pendingChunkUploads.findIndex(function (pendingUpload) { return pendingUpload.uploadId === uploadedChunk.uploadId && pendingUpload.partNumber === uploadedChunk.partNumber; }); this.pendingChunkUploads.splice(pendingUploadIdx, 1); // Push completed upload in the completed uploads list this.completedChunks.push(uploadedChunk); this.completedChunksCount++; // Determine if this specific upload is complete var uploadCompletedChunks = this.completedChunks.filter(function (c) { return c.uploadId === uploadedChunk.uploadId; }); var isUploadComplete = uploadCompletedChunks.length === upload.partsCount; if (isUploadComplete) { this.completeOne(upload); } // If onProgress callback is provided, send progress this.onProgress && this.onProgress({ completedChunksCount: this.completedChunksCount, totalChunksCount: this.totalChunksCount, }); }; UploadManager.prototype.chunkErrored = function (erroredChunk, error) { // Find the upload associated with the errored chunk // If the upload is not found, we cancel it everything as the internal state is corrupted var erroredUpload = this.uploads.find(function (u) { return u.uploadId === erroredChunk.uploadId; }); if (!erroredUpload) { return this.cancel('upload_internal_error'); } // If onError callback is provided, send error this.onError && this.onError({ error: error, erroredUpload: erroredUpload, }); // We cancel all the operations if the settings demand it if (this.cancelAllOnError) { return this.cancel('upload_internal_error'); } // Else, we only cancel the errored upload this.cancelOne(erroredChunk.uploadId); // If onProgress callback is provided, send updated progress this.onProgress && this.onProgress({ completedChunksCount: this.completedChunksCount, totalChunksCount: this.totalChunksCount, }); }; UploadManager.prototype.uploadChunk = function (chunk) { var abortController = typeof AbortController !== 'undefined' ? new AbortController() : { abort: function () { return; }, signal: null, }; return { uploadMode: chunk.uploadMode, uploadId: chunk.uploadId, partNumber: chunk.uploadMode === types_1.UploadMode.Multipart ? chunk.partNumber : 0, promise: chunk.uploadMode === types_1.UploadMode.Multipart ? this.performMultipartUpload(abortController, chunk) : this.performSingleFileUpload(abortController, chunk), cancel: abortController.abort.bind(abortController), }; }; UploadManager.prototype.performSingleFileUpload = function (abortController, chunk) { var _a; return __awaiter(this, void 0, void 0, function () { var form, response, textBody; return __generator(this, function (_b) { switch (_b.label) { case 0: form = new FormData(); (_a = chunk.request.fields) === null || _a === void 0 ? void 0 : _a.forEach(function (_a) { var key = _a.key, value = _a.value; form.append(key, value); }); form.append('file', chunk.file); form.append('key', chunk.key); form.append('Content-Type', chunk.file.type); return [4 /*yield*/, fetch(chunk.request.url, { method: 'POST', body: form, signal: abortController.signal, })]; case 1: response = _b.sent(); if (!!response.ok) return [3 /*break*/, 3]; return [4 /*yield*/, response.text()]; case 2: textBody = _b.sent(); if (textBody.includes('EntityTooLarge')) { throw new Error('file_too_large'); } else if (textBody.includes('$Content-Type')) { throw new Error('unauthorized_file_type'); } throw new Error('upload_internal_error'); case 3: return [2 /*return*/, { uploadId: chunk.uploadId, partNumber: chunk.partNumber, etag: '', }]; } }); }); }; UploadManager.prototype.performMultipartUpload = function (abortController, chunk) { var _a; return __awaiter(this, void 0, void 0, function () { var getPartRequest, request, headers, response, etag, textBody; var _this = this; return __generator(this, function (_b) { switch (_b.label) { case 0: getPartRequest = function () { return __awaiter(_this, void 0, void 0, function () { var result; return __generator(this, function (_a) { switch (_a.label) { case 0: // If no getPartRequest handler is provided, we throw an error as we cannot // handle multipart uploads if (!this.getPartRequest) { throw new Error('no_get_part_request_handler_provided'); } return [4 /*yield*/, this.getPartRequest(chunk)]; case 1: result = _a.sent(); return [2 /*return*/, { url: result.url, headers: result.headers, fields: result.fields, }]; } }); }); }; return [4 /*yield*/, getPartRequest()]; case 1: request = _b.sent(); headers = (_a = request.headers) === null || _a === void 0 ? void 0 : _a.reduce(function (acc, h) { var _a; return (__assign(__assign({}, acc), (_a = {}, _a[h.key] = h.value, _a))); }, {}); return [4 /*yield*/, fetch(request.url, { method: 'PUT', body: chunk.file.slice(chunk.start, chunk.end), headers: headers, signal: abortController.signal, })]; case 2: response = _b.sent(); etag = response.headers.get('ETag'); if (!(!response.ok || !etag)) return [3 /*break*/, 4]; return [4 /*yield*/, response.text()]; case 3: textBody = _b.sent(); if (textBody.includes('EntityTooLarge')) { throw new Error('file_too_large'); } else if (textBody.includes('$Content-Type')) { throw new Error('unauthorized_file_type'); } throw new Error('upload_internal_error'); case 4: return [2 /*return*/, { etag: etag, partNumber: chunk.partNumber, uploadId: chunk.uploadId, }]; } }); }); }; return UploadManager; }()); exports.UploadManager = UploadManager;