UNPKG

box-ui-elements-mlh

Version:
1,356 lines (1,126 loc) 49.2 kB
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } /** * * @file Multiput upload * @author Box */ import noop from 'lodash/noop'; import isNaN from 'lodash/isNaN'; import { getFileLastModifiedAsISONoMSIfPossible, getBoundedExpBackoffRetryDelay } from '../../utils/uploads'; import { retryNumOfTimes } from '../../utils/function'; import { digest } from '../../utils/webcrypto'; import hexToBase64 from '../../utils/base64'; import createWorker from '../../utils/uploadsSHA1Worker'; import Browser from '../../utils/Browser'; import { DEFAULT_RETRY_DELAY_MS, ERROR_CODE_UPLOAD_STORAGE_LIMIT_EXCEEDED, HTTP_STATUS_CODE_FORBIDDEN, MS_IN_S } from '../../constants'; import MultiputPart, { PART_STATE_UPLOADED, PART_STATE_UPLOADING, PART_STATE_DIGEST_READY, PART_STATE_NOT_STARTED } from './MultiputPart'; import BaseMultiput from './BaseMultiput'; // Constants used for specifying log event types. // This type is a catch-all for create session errors that aren't 5xx's (for which we'll do // retries) and aren't specific 4xx's we know how to specifically handle (e.g. out of storage). var LOG_EVENT_TYPE_CREATE_SESSION_MISC_ERROR = 'create_session_misc_error'; var LOG_EVENT_TYPE_CREATE_SESSION_RETRIES_EXCEEDED = 'create_session_retries_exceeded'; var LOG_EVENT_TYPE_FILE_CHANGED_DURING_UPLOAD = 'file_changed_during_upload'; var LOG_EVENT_TYPE_PART_UPLOAD_RETRIES_EXCEEDED = 'part_upload_retries_exceeded'; var LOG_EVENT_TYPE_COMMIT_RETRIES_EXCEEDED = 'commit_retries_exceeded'; var LOG_EVENT_TYPE_WEB_WORKER_ERROR = 'web_worker_error'; var LOG_EVENT_TYPE_FILE_READER_RECEIVED_NOT_FOUND_ERROR = 'file_reader_received_not_found_error'; var LOG_EVENT_TYPE_PART_DIGEST_RETRIES_EXCEEDED = 'part_digest_retries_exceeded'; var MultiputUpload = /*#__PURE__*/function (_BaseMultiput) { _inherits(MultiputUpload, _BaseMultiput); var _super = _createSuper(MultiputUpload); /** * [constructor] * * @param {Options} options * @param {MultiputConfig} [config] */ function MultiputUpload(options, config) { var _this; _classCallCheck(this, MultiputUpload); _this = _super.call(this, options, { createSession: null, uploadPart: null, listParts: null, commit: null, abort: null, logEvent: null }, config); _defineProperty(_assertThisInitialized(_this), "getBaseUploadUrlFromPreflightResponse", function (_ref) { var data = _ref.data; if (!data || !data.upload_url) { return _this.getBaseUploadUrl(); } var splitUrl = data.upload_url.split('/'); // splitUrl[0] is the protocol (e.g., https:), splitUrl[2] is hostname (e.g., www.box.com) _this.uploadHost = "".concat(splitUrl[0], "//").concat(splitUrl[2]); return _this.getBaseUploadUrl(); }); _defineProperty(_assertThisInitialized(_this), "preflightSuccessHandler", /*#__PURE__*/function () { var _ref2 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(preflightResponse) { var uploadUrl, createSessionUrl, postData, response, errorData; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: if (!_this.isDestroyed()) { _context.next = 2; break; } return _context.abrupt("return"); case 2: uploadUrl = _this.getBaseUploadUrlFromPreflightResponse(preflightResponse); createSessionUrl = "".concat(uploadUrl, "/files/upload_sessions"); // Parallelism is currently detrimental to multiput upload performance in Zones, so set it to 1. if (createSessionUrl.includes('fupload-ec2')) { _this.config.parallelism = 1; } // Set up post body postData = { file_size: _this.file.size, file_name: _this.fileName }; if (_this.fileId) { createSessionUrl = createSessionUrl.replace('upload_sessions', "".concat(_this.fileId, "/upload_sessions")); } else { postData.folder_id = _this.folderId; } _context.prev = 7; _context.next = 10; return _this.xhr.post({ url: createSessionUrl, data: postData }); case 10: response = _context.sent; _this.createSessionSuccessHandler(response.data); _context.next = 31; break; case 14: _context.prev = 14; _context.t0 = _context["catch"](7); errorData = _this.getErrorResponse(_context.t0); if (!(errorData && errorData.status >= 500 && errorData.status < 600)) { _context.next = 20; break; } _this.createSessionErrorHandler(_context.t0); return _context.abrupt("return"); case 20: if (!(errorData && errorData.status === 409 && errorData.code === 'session_conflict')) { _context.next = 23; break; } _this.createSessionSuccessHandler(errorData.context_info.session); return _context.abrupt("return"); case 23: if (!(errorData && errorData.status === HTTP_STATUS_CODE_FORBIDDEN && errorData.code === ERROR_CODE_UPLOAD_STORAGE_LIMIT_EXCEEDED || errorData.status === HTTP_STATUS_CODE_FORBIDDEN && errorData.code === 'access_denied_insufficient_permissions')) { _context.next = 26; break; } _this.errorCallback(errorData); return _context.abrupt("return"); case 26: if (!(errorData && errorData.status === 409)) { _context.next = 30; break; } _this.resolveConflict(errorData); _this.createSessionRetry(); return _context.abrupt("return"); case 30: // All other cases get treated as an upload failure. _this.sessionErrorHandler(_context.t0, LOG_EVENT_TYPE_CREATE_SESSION_MISC_ERROR, JSON.stringify(_context.t0)); case 31: case "end": return _context.stop(); } } }, _callee, null, [[7, 14]]); })); return function (_x) { return _ref2.apply(this, arguments); }; }()); _defineProperty(_assertThisInitialized(_this), "createSessionErrorHandler", function (error) { if (_this.isDestroyed()) { return; } if (_this.createSessionNumRetriesPerformed < _this.config.retries) { _this.createSessionRetry(); return; } _this.consoleLog('Too many create session failures, failing upload'); _this.sessionErrorHandler(error, LOG_EVENT_TYPE_CREATE_SESSION_RETRIES_EXCEEDED, JSON.stringify(error)); }); _defineProperty(_assertThisInitialized(_this), "getSessionInfo", /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() { var uploadUrl, sessionUrl, response; return regeneratorRuntime.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: uploadUrl = _this.getBaseUploadUrl(); sessionUrl = "".concat(uploadUrl, "/files/upload_sessions/").concat(_this.sessionId); _context2.prev = 2; _context2.next = 5; return _this.xhr.get({ url: sessionUrl }); case 5: response = _context2.sent; _this.getSessionSuccessHandler(response.data); _context2.next = 12; break; case 9: _context2.prev = 9; _context2.t0 = _context2["catch"](2); _this.getSessionErrorHandler(_context2.t0); case 12: case "end": return _context2.stop(); } } }, _callee2, null, [[2, 9]]); }))); _defineProperty(_assertThisInitialized(_this), "partUploadSuccessHandler", function (part) { _this.numPartsUploading -= 1; _this.numPartsUploaded += 1; _this.updateProgress(part.uploadedBytes, _this.partSize); _this.processNextParts(); }); _defineProperty(_assertThisInitialized(_this), "partUploadErrorHandler", function (error, eventInfo) { _this.sessionErrorHandler(error, LOG_EVENT_TYPE_PART_UPLOAD_RETRIES_EXCEEDED, eventInfo); // Pause the rest of the parts. // can't cancel parts because cancel destroys the part and parts are only created in createSession call if (_this.isResumableUploadsEnabled) { // Reset uploading process for parts that were in progress when the upload failed var nextUploadIndex = _this.firstUnuploadedPartIndex; while (_this.numPartsUploading > 0) { var part = _this.parts[nextUploadIndex]; if (part && part.state === PART_STATE_UPLOADING) { part.reset(); part.pause(); _this.numPartsUploading -= 1; _this.numPartsDigestReady += 1; } nextUploadIndex += 1; } } }); _defineProperty(_assertThisInitialized(_this), "updateProgress", function (prevUploadedBytes, newUploadedBytes) { if (_this.isDestroyed()) { return; } _this.totalUploadedBytes += newUploadedBytes - prevUploadedBytes; _this.progressCallback({ loaded: _this.totalUploadedBytes, total: _this.file.size }); }); _defineProperty(_assertThisInitialized(_this), "processNextParts", function () { if (_this.failSessionIfFileChangeDetected()) { return; } if (_this.numPartsUploaded === _this.parts.length && _this.fileSha1) { _this.commitSession(); return; } _this.updateFirstUnuploadedPartIndex(); while (_this.canStartMorePartUploads()) { _this.uploadNextPart(); } if (_this.shouldComputeDigestForNextPart()) { _this.computeDigestForNextPart(); } }); _defineProperty(_assertThisInitialized(_this), "onWorkerMessage", function (event) { if (_this.isDestroyed()) { return; } var data = event.data; if (data.type === 'partDone') { _this.numPartsDigestComputing -= 1; var part = data.part; _this.parts[part.index].timing.fileDigestTime = data.duration; _this.processNextParts(); } else if (data.type === 'done') { _this.fileSha1 = hexToBase64(data.sha1); _this.sha1Worker.terminate(); _this.processNextParts(); } else if (data.type === 'error') { _this.sessionErrorHandler(null, LOG_EVENT_TYPE_WEB_WORKER_ERROR, JSON.stringify(data)); } }); _defineProperty(_assertThisInitialized(_this), "sendPartToWorker", function (part, buffer) { if (_this.isDestroyed()) { return; } // Don't send entire part since XHR can't be cloned var partInformation = { index: part.index, offset: part.offset, size: part.partSize }; _this.sha1Worker.postMessage({ part: partInformation, fileSize: _this.file.size, partContents: buffer }, [buffer] // This transfers the ArrayBuffer to the worker context without copying contents. ); _this.consoleLog("Part sent to worker: ".concat(JSON.stringify(part), ".}")); }); _defineProperty(_assertThisInitialized(_this), "onPartDigestError", function (error, part) { _this.consoleLog("Error computing digest for part ".concat(JSON.stringify(part), ": ").concat(JSON.stringify(error))); // When a FileReader is processing a file that changes on disk, Chrome reports a 'NotFoundError' // and Safari reports a 'NOT_FOUND_ERR'. (Other browsers seem to allow the reader to keep // going, either with the old version of the new file or the new one.) Since the error name // implies that retrying will not help, we fail the session. if (error.name === 'NotFoundError' || error.name === 'NOT_FOUND_ERR') { _this.sessionErrorHandler(null, LOG_EVENT_TYPE_FILE_READER_RECEIVED_NOT_FOUND_ERROR, JSON.stringify(error)); return; } if (_this.failSessionIfFileChangeDetected()) { return; } if (part.numDigestRetriesPerformed >= _this.config.retries) { _this.sessionErrorHandler(null, LOG_EVENT_TYPE_PART_DIGEST_RETRIES_EXCEEDED, JSON.stringify(error)); return; } var retryDelayMs = getBoundedExpBackoffRetryDelay(_this.config.initialRetryDelayMs, _this.config.maxRetryDelayMs, part.numDigestRetriesPerformed); part.numDigestRetriesPerformed += 1; _this.consoleLog("Retrying digest work for part ".concat(JSON.stringify(part), " in ").concat(retryDelayMs, " ms")); setTimeout(function () { _this.computeDigestForPart(part); }, retryDelayMs); }); _defineProperty(_assertThisInitialized(_this), "commitSession", function () { if (_this.isDestroyed()) { return; } var stats = { totalPartReadTime: 0, totalPartDigestTime: 0, totalFileDigestTime: 0, totalPartUploadTime: 0 }; var data = { parts: _this.parts.map(function (part) { stats.totalPartReadTime += part.timing.readTime; stats.totalPartDigestTime += part.timing.subtleCryptoTime; stats.totalFileDigestTime += part.timing.fileDigestTime; stats.totalPartUploadTime += part.timing.uploadTime; return part.getPart(); }).sort(function (part1, part2) { return part1.offset - part2.offset; }), attributes: {} }; var fileLastModified = getFileLastModifiedAsISONoMSIfPossible(_this.file); if (fileLastModified) { data.attributes.content_modified_at = fileLastModified; } if (_this.fileDescription) { data.attributes.description = _this.fileDescription; } var clientEventInfo = { avg_part_read_time: Math.round(stats.totalPartReadTime / _this.parts.length), avg_part_digest_time: Math.round(stats.totalPartDigestTime / _this.parts.length), avg_file_digest_time: Math.round(stats.totalFileDigestTime / _this.parts.length), avg_part_upload_time: Math.round(stats.totalPartUploadTime / _this.parts.length) }; // To make flow stop complaining about this.fileSha1 could potentially be undefined/null var fileSha1 = _this.fileSha1; var headers = { Digest: "sha=".concat(fileSha1), 'X-Box-Client-Event-Info': JSON.stringify(clientEventInfo) }; _this.xhr.post({ url: _this.sessionEndpoints.commit, data: data, headers: headers }).then(_this.commitSessionSuccessHandler).catch(_this.commitSessionErrorHandler); }); _defineProperty(_assertThisInitialized(_this), "commitSessionSuccessHandler", function (response) { if (_this.isDestroyed()) { return; } var status = response.status, data = response.data; if (status === 202) { _this.commitSessionRetry(response); return; } var entries = data.entries; // v2.1 API response format is different from v2.0. v2.1 returns individual upload entry directly inside data, // while v2.0 returns a collection of entries under data.entries if (!entries && data.id) { entries = [data]; } _this.destroy(); if (_this.successCallback && entries) { _this.successCallback(entries); } }); _defineProperty(_assertThisInitialized(_this), "commitSessionErrorHandler", function (error) { if (_this.isDestroyed()) { return; } var response = error.response; if (!response) { // Some random error happened _this.consoleError(error); return; } if (_this.commitRetryCount >= _this.config.retries) { _this.consoleLog('Too many commit failures, failing upload'); _this.sessionErrorHandler(error, LOG_EVENT_TYPE_COMMIT_RETRIES_EXCEEDED, JSON.stringify(error)); return; } _this.commitSessionRetry(response); }); _defineProperty(_assertThisInitialized(_this), "getNumPartsUploading", function () { return _this.numPartsUploading; }); _this.parts = []; _this.options = options; _this.fileSha1 = null; _this.totalUploadedBytes = 0; _this.numPartsNotStarted = 0; // # of parts yet to be processed _this.numPartsDigestComputing = 0; // # of parts sent to the digest worker _this.numPartsDigestReady = 0; // # of parts with digest finished that are waiting to be uploaded. _this.numPartsUploading = 0; // # of parts with upload requests currently inflight _this.numPartsUploaded = 0; // # of parts successfully uploaded _this.firstUnuploadedPartIndex = 0; // Index of first part that hasn't been uploaded yet. _this.createSessionNumRetriesPerformed = 0; _this.partSize = 0; _this.commitRetryCount = 0; _this.clientId = null; _this.isResumableUploadsEnabled = false; _this.numResumeRetries = 0; return _this; } /** * Reset values for uploading process. */ _createClass(MultiputUpload, [{ key: "reset", value: function reset() { this.parts = []; this.fileSha1 = null; this.totalUploadedBytes = 0; this.numPartsNotStarted = 0; // # of parts yet to be processed this.numPartsDigestComputing = 0; // # of parts sent to the digest worker this.numPartsDigestReady = 0; // # of parts with digest finished that are waiting to be uploaded. this.numPartsUploading = 0; // # of parts with upload requests currently inflight this.numPartsUploaded = 0; // # of parts successfully uploaded this.firstUnuploadedPartIndex = 0; // Index of first part that hasn't been uploaded yet. this.createSessionNumRetriesPerformed = 0; this.partSize = 0; this.commitRetryCount = 0; this.numResumeRetries = 0; } /** * Set information about file being uploaded * * * @param {Object} options * @param {File} options.file * @param {string} options.folderId - Untyped folder id (e.g. no "folder_" prefix) * @param {string} [options.fileId] - Untyped file id (e.g. no "file_" prefix) * @param {string} options.sessionId * @param {Function} [options.errorCallback] * @param {Function} [options.progressCallback] * @param {Function} [options.successCallback] * @return {void} */ }, { key: "setFileInfo", value: function setFileInfo(_ref4) { var file = _ref4.file, folderId = _ref4.folderId, errorCallback = _ref4.errorCallback, progressCallback = _ref4.progressCallback, successCallback = _ref4.successCallback, _ref4$overwrite = _ref4.overwrite, overwrite = _ref4$overwrite === void 0 ? true : _ref4$overwrite, conflictCallback = _ref4.conflictCallback, fileId = _ref4.fileId; this.file = file; this.fileName = this.file.name; this.folderId = folderId; this.errorCallback = errorCallback || noop; this.progressCallback = progressCallback || noop; this.successCallback = successCallback || noop; this.overwrite = overwrite; this.conflictCallback = conflictCallback; this.fileId = fileId; } /** * Upload a given file * * * @param {Object} options * @param {File} options.file * @param {string} options.folderId - Untyped folder id (e.g. no "folder_" prefix) * @param {string} [options.fileId] - Untyped file id (e.g. no "file_" prefix) * @param {Function} [options.errorCallback] * @param {Function} [options.progressCallback] * @param {Function} [options.successCallback] * @return {void} */ }, { key: "upload", value: function upload(_ref5) { var file = _ref5.file, fileDescription = _ref5.fileDescription, folderId = _ref5.folderId, errorCallback = _ref5.errorCallback, progressCallback = _ref5.progressCallback, successCallback = _ref5.successCallback, _ref5$overwrite = _ref5.overwrite, overwrite = _ref5$overwrite === void 0 ? true : _ref5$overwrite, conflictCallback = _ref5.conflictCallback, fileId = _ref5.fileId; this.file = file; this.fileName = this.file.name; // These values are used as part of our (best effort) attempt to abort uploads if we detect // a file change during the upload. this.initialFileSize = this.file.size; this.initialFileLastModified = getFileLastModifiedAsISONoMSIfPossible(this.file); this.folderId = folderId; this.errorCallback = errorCallback || noop; this.progressCallback = progressCallback || noop; this.successCallback = successCallback || noop; this.sha1Worker = createWorker(); this.sha1Worker.addEventListener('message', this.onWorkerMessage); this.conflictCallback = conflictCallback; this.overwrite = overwrite; this.fileId = fileId; this.fileDescription = fileDescription; this.makePreflightRequest(); } /** * Update uploadHost with preflight response and return the base uploadUrl * * @private * @param {Object} response * @param {Object} [response.data] * @return {string} */ }, { key: "createSessionRetry", value: /** * Schedule a retry for create session request upon failure * * @private * @return {void} */ function createSessionRetry() { var retryDelayMs = getBoundedExpBackoffRetryDelay(this.config.initialRetryDelayMs, this.config.maxRetryDelayMs, this.createSessionNumRetriesPerformed); this.createSessionNumRetriesPerformed += 1; this.consoleLog("Retrying create session in ".concat(retryDelayMs, " ms")); this.createSessionTimeout = setTimeout(this.makePreflightRequest, retryDelayMs); } /** * Handles a upload session success response * * @private * @param {Object} data - Upload session creation success data * @return {void} */ }, { key: "createSessionSuccessHandler", value: function createSessionSuccessHandler(data) { if (this.isDestroyed()) { return; } var id = data.id, part_size = data.part_size, session_endpoints = data.session_endpoints; this.sessionId = id; this.partSize = part_size; this.sessionEndpoints = _objectSpread(_objectSpread({}, this.sessionEndpoints), {}, { uploadPart: session_endpoints.upload_part, listParts: session_endpoints.list_parts, commit: session_endpoints.commit, abort: session_endpoints.abort, logEvent: session_endpoints.log_event }); this.populateParts(); this.processNextParts(); } /** * Resume uploading the given file * * * @param {Object} options * @param {File} options.file * @param {string} options.folderId - Untyped folder id (e.g. no "folder_" prefix) * @param {string} [options.fileId] - Untyped file id (e.g. no "file_" prefix) * @param {string} options.sessionId * @param {Function} [options.errorCallback] * @param {Function} [options.progressCallback] * @param {Function} [options.successCallback] * @param {Function} [options.conflictCallback] * @return {void} */ }, { key: "resume", value: function resume(_ref6) { var file = _ref6.file, folderId = _ref6.folderId, errorCallback = _ref6.errorCallback, progressCallback = _ref6.progressCallback, sessionId = _ref6.sessionId, successCallback = _ref6.successCallback, _ref6$overwrite = _ref6.overwrite, overwrite = _ref6$overwrite === void 0 ? true : _ref6$overwrite, conflictCallback = _ref6.conflictCallback, fileId = _ref6.fileId; this.setFileInfo({ file: file, folderId: folderId, errorCallback: errorCallback, progressCallback: progressCallback, successCallback: successCallback, conflictCallback: conflictCallback, overwrite: overwrite, fileId: fileId }); this.sessionId = sessionId; if (!this.sha1Worker) { this.sha1Worker = createWorker(); } this.sha1Worker.addEventListener('message', this.onWorkerMessage); this.getSessionInfo(); } /** * Get session information from API. * Uses session info to commit a complete session or continue an in-progress session. * * @private * @return {void} */ }, { key: "getSessionSuccessHandler", value: /** * Handles a getSessionInfo success and either commits the session or continues to process * the parts that still need to be uploaded. * * @param response * @return {void} */ function getSessionSuccessHandler(data) { var part_size = data.part_size, session_endpoints = data.session_endpoints; // Set session information gotten from API response this.partSize = part_size; this.sessionEndpoints = _objectSpread(_objectSpread({}, this.sessionEndpoints), {}, { uploadPart: session_endpoints.upload_part, listParts: session_endpoints.list_parts, commit: session_endpoints.commit, abort: session_endpoints.abort, logEvent: session_endpoints.log_event }); this.processNextParts(); } /** * Handle error from getting upload session. * Restart uploads without valid sessions from the beginning of the upload process. * * @param error * @return {void} */ }, { key: "getSessionErrorHandler", value: function getSessionErrorHandler(error) { if (this.isDestroyed()) { return; } var errorData = this.getErrorResponse(error); if (this.numResumeRetries > this.config.retries) { this.errorCallback(errorData); return; } if (errorData && errorData.status === 429) { var retryAfterMs = DEFAULT_RETRY_DELAY_MS; if (errorData.headers) { var retryAfterSec = parseInt(errorData.headers['retry-after'] || errorData.headers.get('Retry-After'), 10); if (!isNaN(retryAfterSec)) { retryAfterMs = retryAfterSec * MS_IN_S; } } this.retryTimeout = setTimeout(this.getSessionInfo, retryAfterMs); this.numResumeRetries += 1; } else if (errorData && errorData.status >= 400 && errorData.status < 500) { // Restart upload process for errors resulting from invalid/expired session or no permission this.parts.forEach(function (part) { part.cancel(); }); this.reset(); // Abort session clearTimeout(this.createSessionTimeout); clearTimeout(this.commitSessionTimeout); this.abortSession(); // Restart the uploading process from the beginning var uploadOptions = { file: this.file, folderId: this.folderId, errorCallback: this.errorCallback, progressCallback: this.progressCallback, successCallback: this.successCallback, overwrite: this.overwrite, fileId: this.fileId }; this.upload(uploadOptions); } else { // Handle internet disconnects (error.request && !error.response) and (!error.request) // Also handle any 500 error messages this.retryTimeout = setTimeout(this.getSessionInfo, Math.pow(2, this.numResumeRetries) * MS_IN_S); this.numResumeRetries += 1; } } /** * Session error handler. * Retries the create session request or fails the upload. * * @private * @param {?Error} error * @param {string} logEventType * @param {string} [logMessage] * @return {Promise} */ }, { key: "sessionErrorHandler", value: function () { var _sessionErrorHandler = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee3(error, logEventType, logMessage) { var _this2 = this; var errorData; return regeneratorRuntime.wrap(function _callee3$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: if (!this.isResumableUploadsEnabled) { this.destroy(); } errorData = this.getErrorResponse(error); this.errorCallback(errorData); _context3.prev = 3; if (this.sessionEndpoints.logEvent) { _context3.next = 6; break; } throw new Error('logEvent endpoint not found'); case 6: _context3.next = 8; return retryNumOfTimes(function (resolve, reject) { _this2.logEvent(logEventType, logMessage).then(resolve).catch(reject); }, this.config.retries, this.config.initialRetryDelayMs); case 8: if (!this.isResumableUploadsEnabled) { this.abortSession(); } _context3.next = 14; break; case 11: _context3.prev = 11; _context3.t0 = _context3["catch"](3); if (!this.isResumableUploadsEnabled) { this.abortSession(); } case 14: case "end": return _context3.stop(); } } }, _callee3, this, [[3, 11]]); })); function sessionErrorHandler(_x2, _x3, _x4) { return _sessionErrorHandler.apply(this, arguments); } return sessionErrorHandler; }() /** * Aborts the upload session * * @private * @return {void} */ }, { key: "abortSession", value: function abortSession() { var _this3 = this; if (this.sha1Worker) { this.sha1Worker.terminate(); } if (this.sessionEndpoints.abort && this.sessionId) { this.xhr.delete({ url: this.sessionEndpoints.abort }).then(function () { _this3.sessionId = ''; }); } } /** * Part upload success handler * * @private * @param {MultiputPart} part * @return {void} */ }, { key: "shouldComputeDigestForNextPart", value: /** * We compute digest for parts one at a time. This is done for simplicity and also to guarantee that * we send parts in order to the web sha1Worker (which is computing the digest for the entire file). * * @private * @return {boolean} true if there is work to do, false otherwise. */ function shouldComputeDigestForNextPart() { return !this.isDestroyed() && this.numPartsDigestComputing === 0 && this.numPartsNotStarted > 0 && this.numPartsDigestReady < this.config.digestReadahead; } /** * Find first part in parts array that doesn't have a digest, and compute its digest. * @private * @return {void} */ }, { key: "computeDigestForNextPart", value: function computeDigestForNextPart() { for (var i = this.firstUnuploadedPartIndex; i < this.parts.length; i += 1) { var part = this.parts[i]; if (part.state === PART_STATE_NOT_STARTED) { // Update the counters here instead of computeDigestForPart because computeDigestForPart // can get called on retries this.numPartsNotStarted -= 1; this.numPartsDigestComputing += 1; this.computeDigestForPart(part); return; } } } /** * Compute digest for this part * * @private * @param {MultiputPart} part * @return {Promise} */ }, { key: "computeDigestForPart", value: function () { var _computeDigestForPart = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee4(part) { var blob, reader, startTimestamp, _yield$this$readFile, buffer, readCompleteTimestamp, sha256ArrayBuffer, sha256, digestCompleteTimestamp; return regeneratorRuntime.wrap(function _callee4$(_context4) { while (1) { switch (_context4.prev = _context4.next) { case 0: blob = this.file.slice(part.offset, part.offset + this.partSize); reader = new window.FileReader(); startTimestamp = Date.now(); _context4.prev = 3; _context4.next = 6; return this.readFile(reader, blob); case 6: _yield$this$readFile = _context4.sent; buffer = _yield$this$readFile.buffer; readCompleteTimestamp = _yield$this$readFile.readCompleteTimestamp; _context4.next = 11; return digest('SHA-256', buffer); case 11: sha256ArrayBuffer = _context4.sent; sha256 = btoa([].reduce.call(new Uint8Array(sha256ArrayBuffer), function (data, byte) { return data + String.fromCharCode(byte); }, '')); this.sendPartToWorker(part, buffer); part.sha256 = sha256; part.state = PART_STATE_DIGEST_READY; part.blob = blob; this.numPartsDigestReady += 1; digestCompleteTimestamp = Date.now(); part.timing = { partDigestTime: digestCompleteTimestamp - startTimestamp, readTime: readCompleteTimestamp - startTimestamp, subtleCryptoTime: digestCompleteTimestamp - readCompleteTimestamp }; this.processNextParts(); _context4.next = 26; break; case 23: _context4.prev = 23; _context4.t0 = _context4["catch"](3); this.onPartDigestError(_context4.t0, part); case 26: case "end": return _context4.stop(); } } }, _callee4, this, [[3, 23]]); })); function computeDigestForPart(_x5) { return _computeDigestForPart.apply(this, arguments); } return computeDigestForPart; }() /** * Deal with a message from the worker (either a part sha-1 ready, file sha-1 ready, or error). * * @private * @param {object} event * @return {void} */ }, { key: "commitSessionRetry", value: /** * Retry commit. * Retries the commit or fails the multiput session. * * @private * @param {Object} response * @return {void} */ function commitSessionRetry(response) { var status = response.status, headers = response.headers; var retryAfterMs = DEFAULT_RETRY_DELAY_MS; if (headers) { var retryAfterSec = parseInt(headers['retry-after'], 10); if (!Number.isNaN(retryAfterSec)) { retryAfterMs = retryAfterSec * 1000; } } var defaultRetryDelayMs = getBoundedExpBackoffRetryDelay(this.config.initialRetryDelayMs, this.config.maxRetryDelayMs, this.commitRetryCount); // If status is 202 then don't increment the retry count. // In this case, frontend will keep retrying until it gets another status code. // Retry interval = value specified for the Retry-After header in 202 response. if (status !== 202) { this.commitRetryCount += 1; } var retryDelayMs = retryAfterMs || defaultRetryDelayMs; this.consoleLog("Retrying commit in ".concat(retryDelayMs, " ms")); this.commitSessionTimeout = setTimeout(this.commitSession, retryDelayMs); } /** * Find first part in parts array that we can upload, and upload it. * * @private * @return {void} */ }, { key: "uploadNextPart", value: function uploadNextPart() { for (var i = this.firstUnuploadedPartIndex; i < this.parts.length; i += 1) { var part = this.parts[i]; if (part.state === PART_STATE_DIGEST_READY) { // Update the counters here instead of uploadPart because uploadPart // can get called on retries this.numPartsDigestReady -= 1; this.numPartsUploading += 1; if (part.isPaused) { part.unpause(); } else { part.upload(); } break; } } } /** * Checks if upload pipeline is full * * @private * @return {boolean} */ }, { key: "canStartMorePartUploads", value: function canStartMorePartUploads() { return !this.isDestroyed() && this.numPartsUploading < this.config.parallelism && this.numPartsDigestReady > 0; } /** * Functions that walk the parts array get called a lot, so we cache which part we should * start work at to avoid always iterating through entire parts list. * * @private * @return {void} */ }, { key: "updateFirstUnuploadedPartIndex", value: function updateFirstUnuploadedPartIndex() { var part = this.parts[this.firstUnuploadedPartIndex]; while (part && part.state === PART_STATE_UPLOADED) { this.firstUnuploadedPartIndex += 1; part = this.parts[this.firstUnuploadedPartIndex]; } } /** * Get number of parts being uploaded * * @return {number} */ }, { key: "populateParts", value: /** * After session is created and we know the part size, populate the parts * array. * * @private * @return {void} */ function populateParts() { this.numPartsNotStarted = Math.ceil(this.file.size / this.partSize); for (var i = 0; i < this.numPartsNotStarted; i += 1) { var offset = i * this.partSize; var currentPartSize = Math.min(offset + this.partSize, this.file.size) - offset; var part = new MultiputPart(this.options, i, offset, currentPartSize, this.file.size, this.sessionId, this.sessionEndpoints, this.config, this.getNumPartsUploading, this.partUploadSuccessHandler, this.updateProgress, this.partUploadErrorHandler); this.parts.push(part); } } /** * Fails the session if the file's size or last modified has changed since the upload process * began. * * This ensures that we don't upload a file that has parts from one file version and parts from * another file version. * * This logic + the "not found" error logic in onWorkerError() is best effort and will not * detect all possible file changes. This is because of browser differences. For example, * -- In Safari, size and last modified will update when a file changes, and workers will * get "not found" errors. * -- In Chrome, size and last modified will update, but not in legacy drag and drop (that * code path constructs a different file object). Workers will still get "not found" errors, * though, so we can still detect changes even in legacy drag and drop. * -- In IE 11/Edge, size will update but last modified will not. Workers will not get * "not found" errors, but they may get a generic error saying that some bytes failed to be * read. * -- In Firefox, neither last modified nor size will update. Workers don't seem to get errors. * (Not a whole lot we can do here...) * * Unfortunately, alternative solutions to catch more cases don't have a clear ROI (for * example, doing a SHA-1 of the file before and after the upload is very expensive), so * this is the best solution we have. We can revisit this if data shows that we need a better * solution. * * @private * @return {boolean} True if the session was failed, false if no action was taken */ }, { key: "failSessionIfFileChangeDetected", value: function failSessionIfFileChangeDetected() { var currentFileSize = this.file.size; var currentFileLastModified = getFileLastModifiedAsISONoMSIfPossible(this.file); if (currentFileSize !== this.initialFileSize || currentFileLastModified !== this.initialFileLastModified) { var changeJSON = JSON.stringify({ oldSize: this.initialFileSize, newSize: currentFileSize, oldLastModified: this.initialFileLastModified, newLastModified: currentFileLastModified }); // Leave IE with old behavior and kill upload if (Browser.isIE()) { this.sessionErrorHandler(null, LOG_EVENT_TYPE_FILE_CHANGED_DURING_UPLOAD, changeJSON); return true; } // for evergreen browsers where the file change check does not work, log and continue with upload // https://w3c.github.io/FileAPI/#file-section this.consoleLog("file properties changed during upload: ".concat(changeJSON)); return false; } return false; } /** * Cancels an upload in progress by cancelling all upload parts. * This cannot be undone or resumed. * * @private * @return {void} */ }, { key: "cancel", value: function cancel() { if (this.isDestroyed()) { return; } // Cancel individual upload parts this.parts.forEach(function (part) { part.cancel(); }); this.parts = []; clearTimeout(this.createSessionTimeout); clearTimeout(this.commitSessionTimeout); this.abortSession(); this.destroy(); } /** * Resolves upload conflict by overwriting or renaming * * @param {Object} response data * @return {Promise} */ }, { key: "resolveConflict", value: function () { var _resolveConflict = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee5(data) { var extension; return regeneratorRuntime.wrap(function _callee5$(_context5) { while (1) { switch (_context5.prev = _context5.next) { case 0: if (!(this.overwrite && data.context_info)) { _context5.next = 3; break; } this.fileId = data.context_info.conflicts.id; return _context5.abrupt("return"); case 3: if (!this.conflictCallback) { _context5.next = 6; break; } this.fileName = this.conflictCallback(this.fileName); return _context5.abrupt("return"); case 6: extension = this.fileName.substr(this.fileName.lastIndexOf('.')) || ''; // foo.txt => foo-1513385827917.txt this.fileName = "".concat(this.fileName.substr(0, this.fileName.lastIndexOf('.')), "-").concat(Date.now()).concat(extension); case 8: case "end": return _context5.stop(); } } }, _callee5, this); })); function resolveConflict(_x6) { return _resolveConflict.apply(this, arguments); } return resolveConflict; }() /** * Returns detailed error response * * @param {Object} error * @return {Object} */ }, { key: "getErrorResponse", value: function getErrorResponse(error) { if (!error) { return {}; } var response = error.response; if (!response) { return {}; } if (response.status === 401) { return response; } return response.data; } }]); return MultiputUpload; }(BaseMultiput); export default MultiputUpload; //# sourceMappingURL=MultiputUpload.js.map