@triply/tus-js-client
Version:
A pure JavaScript client for the tus resumable upload protocol
1,184 lines (936 loc) • 39.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _jsBase = require("js-base64");
var _urlParse = _interopRequireDefault(require("url-parse"));
var _error = _interopRequireDefault(require("./error"));
var _logger = require("./logger");
var _uuid = _interopRequireDefault(require("./uuid"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: 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 _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;
}
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;
}
/* global window */
var defaultOptions = {
endpoint: null,
uploadUrl: null,
metadata: {},
fingerprint: null,
uploadSize: null,
onProgress: null,
onChunkComplete: null,
onSuccess: null,
onError: null,
_onUploadUrlAvailable: null,
overridePatchMethod: false,
headers: {},
addRequestId: false,
onBeforeRequest: null,
onAfterResponse: null,
onShouldRetry: null,
chunkSize: Infinity,
retryDelays: [0, 1000, 3000, 5000],
parallelUploads: 1,
storeFingerprintForResuming: true,
removeFingerprintOnSuccess: false,
uploadLengthDeferred: false,
uploadDataDuringCreation: false,
urlStorage: null,
fileReader: null,
httpStack: null
};
var BaseUpload = /*#__PURE__*/function () {
function BaseUpload(file, options) {
_classCallCheck(this, BaseUpload); // Warn about removed options from previous versions
if ('resume' in options) {
console.log('tus: The `resume` option has been removed in tus-js-client v2. Please use the URL storage API instead.'); // eslint-disable-line no-console
} // The default options will already be added from the wrapper classes.
this.options = options; // The storage module used to store URLs
this._urlStorage = this.options.urlStorage; // The underlying File/Blob object
this.file = file; // The URL against which the file will be uploaded
this.url = null; // The underlying request object for the current PATCH request
this._req = null; // The fingerpinrt for the current file (set after start())
this._fingerprint = null; // The key that the URL storage returned when saving an URL with a fingerprint,
this._urlStorageKey = null; // The offset used in the current PATCH request
this._offset = null; // True if the current PATCH request has been aborted
this._aborted = false; // The file's size in bytes
this._size = null; // The Source object which will wrap around the given file and provides us
// with a unified interface for getting its size and slice chunks from its
// content allowing us to easily handle Files, Blobs, Buffers and Streams.
this._source = null; // The current count of attempts which have been made. Zero indicates none.
this._retryAttempt = 0; // The timeout's ID which is used to delay the next retry
this._retryTimeout = null; // The offset of the remote upload before the latest attempt was started.
this._offsetBeforeRetry = 0; // An array of BaseUpload instances which are used for uploading the different
// parts, if the parallelUploads option is used.
this._parallelUploads = null; // An array of upload URLs which are used for uploading the different
// parts, if the parallelUploads option is used.
this._parallelUploadUrls = null;
}
/**
* Use the Termination extension to delete an upload from the server by sending a DELETE
* request to the specified upload URL. This is only possible if the server supports the
* Termination extension. If the `options.retryDelays` property is set, the method will
* also retry if an error ocurrs.
*
* @param {String} url The upload's URL which will be terminated.
* @param {object} options Optional options for influencing HTTP requests.
* @return {Promise} The Promise will be resolved/rejected when the requests finish.
*/
_createClass(BaseUpload, [{
key: "findPreviousUploads",
value: function findPreviousUploads() {
var _this = this;
return this.options.fingerprint(this.file, this.options).then(function (fingerprint) {
return _this._urlStorage.findUploadsByFingerprint(fingerprint);
});
}
}, {
key: "resumeFromPreviousUpload",
value: function resumeFromPreviousUpload(previousUpload) {
this.url = previousUpload.uploadUrl || null;
this._parallelUploadUrls = previousUpload.parallelUploadUrls || null;
this._urlStorageKey = previousUpload.urlStorageKey;
}
}, {
key: "start",
value: function start() {
var _this2 = this;
var file = this.file;
if (!file) {
this._emitError(new Error('tus: no file or stream to upload provided'));
return;
}
if (!this.options.endpoint && !this.options.uploadUrl) {
this._emitError(new Error('tus: neither an endpoint or an upload URL is provided'));
return;
}
var retryDelays = this.options.retryDelays;
if (retryDelays != null && Object.prototype.toString.call(retryDelays) !== '[object Array]') {
this._emitError(new Error('tus: the `retryDelays` option must either be an array or null'));
return;
}
if (this.options.parallelUploads > 1) {
// Test which options are incompatible with parallel uploads.
['uploadUrl', 'uploadSize', 'uploadLengthDeferred'].forEach(function (optionName) {
if (_this2.options[optionName]) {
_this2._emitError(new Error("tus: cannot use the ".concat(optionName, " option when parallelUploads is enabled")));
}
});
}
this.options.fingerprint(file, this.options).then(function (fingerprint) {
if (fingerprint == null) {
(0, _logger.log)('No fingerprint was calculated meaning that the upload cannot be stored in the URL storage.');
} else {
(0, _logger.log)("Calculated fingerprint: ".concat(fingerprint));
}
_this2._fingerprint = fingerprint;
if (_this2._source) {
return _this2._source;
}
return _this2.options.fileReader.openFile(file, _this2.options.chunkSize);
}).then(function (source) {
_this2._source = source; // If the upload was configured to use multiple requests or if we resume from
// an upload which used multiple requests, we start a parallel upload.
if (_this2.options.parallelUploads > 1 || _this2._parallelUploadUrls != null) {
_this2._startParallelUpload();
} else {
_this2._startSingleUpload();
}
})["catch"](function (err) {
_this2._emitError(err);
});
}
/**
* Initiate the uploading procedure for a parallelized upload, where one file is split into
* multiple request which are run in parallel.
*
* @api private
*/
}, {
key: "_startParallelUpload",
value: function _startParallelUpload() {
var _this3 = this;
var totalSize = this._size = this._source.size;
var totalProgress = 0;
this._parallelUploads = [];
var partCount = this._parallelUploadUrls != null ? this._parallelUploadUrls.length : this.options.parallelUploads; // The input file will be split into multiple slices which are uploaded in separate
// requests. Here we generate the start and end position for the slices.
var parts = splitSizeIntoParts(this._source.size, partCount, this._parallelUploadUrls); // Create an empty list for storing the upload URLs
this._parallelUploadUrls = new Array(parts.length); // Generate a promise for each slice that will be resolve if the respective
// upload is completed.
var uploads = parts.map(function (part, index) {
var lastPartProgress = 0;
return _this3._source.slice(part.start, part.end).then(function (_ref) {
var value = _ref.value;
return new Promise(function (resolve, reject) {
// Merge with the user supplied options but overwrite some values.
var options = _objectSpread(_objectSpread({}, _this3.options), {}, {
// If available, the partial upload should be resumed from a previous URL.
uploadUrl: part.uploadUrl || null,
// We take manually care of resuming for partial uploads, so they should
// not be stored in the URL storage.
storeFingerprintForResuming: false,
removeFingerprintOnSuccess: false,
// Reset the parallelUploads option to not cause recursion.
parallelUploads: 1,
metadata: {},
// Add the header to indicate the this is a partial upload.
headers: _objectSpread(_objectSpread({}, _this3.options.headers), {}, {
'Upload-Concat': 'partial'
}),
// Reject or resolve the promise if the upload errors or completes.
onSuccess: resolve,
onError: reject,
// Based in the progress for this partial upload, calculate the progress
// for the entire final upload.
onProgress: function onProgress(newPartProgress) {
totalProgress = totalProgress - lastPartProgress + newPartProgress;
lastPartProgress = newPartProgress;
_this3._emitProgress(totalProgress, totalSize);
},
// Wait until every partial upload has an upload URL, so we can add
// them to the URL storage.
_onUploadUrlAvailable: function _onUploadUrlAvailable() {
_this3._parallelUploadUrls[index] = upload.url; // Test if all uploads have received an URL
if (_this3._parallelUploadUrls.filter(function (u) {
return !!u;
}).length === parts.length) {
_this3._saveUploadInUrlStorage();
}
}
});
var upload = new BaseUpload(value, options);
upload.start(); // Store the upload in an array, so we can later abort them if necessary.
_this3._parallelUploads.push(upload);
});
});
});
var req; // Wait until all partial uploads are finished and we can send the POST request for
// creating the final upload.
Promise.all(uploads).then(function () {
req = _this3._openRequest('POST', _this3.options.endpoint);
req.setHeader('Upload-Concat', "final;".concat(_this3._parallelUploadUrls.join(' '))); // Add metadata if values have been added
var metadata = encodeMetadata(_this3.options.metadata);
if (metadata !== '') {
req.setHeader('Upload-Metadata', metadata);
}
return _this3._sendRequest(req, null);
}).then(function (res) {
if (!inStatusCategory(res.getStatus(), 200)) {
_this3._emitHttpError(req, res, 'tus: unexpected response while creating upload');
return;
}
var location = res.getHeader('Location');
if (location == null) {
_this3._emitHttpError(req, res, 'tus: invalid or missing Location header');
return;
}
_this3.url = resolveUrl(_this3.options.endpoint, location);
(0, _logger.log)("Created upload at ".concat(_this3.url));
_this3._emitSuccess();
})["catch"](function (err) {
_this3._emitError(err);
});
}
/**
* Initiate the uploading procedure for a non-parallel upload. Here the entire file is
* uploaded in a sequential matter.
*
* @api private
*/
}, {
key: "_startSingleUpload",
value: function _startSingleUpload() {
// First, we look at the uploadLengthDeferred option.
// Next, we check if the caller has supplied a manual upload size.
// Finally, we try to use the calculated size from the source object.
if (this.options.uploadLengthDeferred) {
this._size = null;
} else if (this.options.uploadSize != null) {
this._size = +this.options.uploadSize;
if (isNaN(this._size)) {
this._emitError(new Error('tus: cannot convert `uploadSize` option into a number'));
return;
}
} else {
this._size = this._source.size;
if (this._size == null) {
this._emitError(new Error("tus: cannot automatically derive upload's size from input and must be specified manually using the `uploadSize` option"));
return;
}
} // Reset the aborted flag when the upload is started or else the
// _performUpload will stop before sending a request if the upload has been
// aborted previously.
this._aborted = false; // The upload had been started previously and we should reuse this URL.
if (this.url != null) {
(0, _logger.log)("Resuming upload from previous URL: ".concat(this.url));
this._resumeUpload();
return;
} // A URL has manually been specified, so we try to resume
if (this.options.uploadUrl != null) {
(0, _logger.log)("Resuming upload from provided URL: ".concat(this.options.url));
this.url = this.options.uploadUrl;
this._resumeUpload();
return;
} // An upload has not started for the file yet, so we start a new one
(0, _logger.log)('Creating a new upload');
this._createUpload();
}
/**
* Abort any running request and stop the current upload. After abort is called, no event
* handler will be invoked anymore. You can use the `start` method to resume the upload
* again.
* If `shouldTerminate` is true, the `terminate` function will be called to remove the
* current upload from the server.
*
* @param {boolean} shouldTerminate True if the upload should be deleted from the server.
* @return {Promise} The Promise will be resolved/rejected when the requests finish.
*/
}, {
key: "abort",
value: function abort(shouldTerminate) {
var _this4 = this; // Count the number of arguments to see if a callback is being provided in the old style required by tus-js-client 1.x, then throw an error if it is.
// `arguments` is a JavaScript built-in variable that contains all of the function's arguments.
if (arguments.length > 1 && typeof arguments[1] === 'function') {
throw new Error('tus: the abort function does not accept a callback since v2 anymore; please use the returned Promise instead');
} // Stop any parallel partial uploads, that have been started in _startParallelUploads.
if (this._parallelUploads != null) {
this._parallelUploads.forEach(function (upload) {
upload.abort(shouldTerminate);
});
} // Stop any current running request.
if (this._req !== null) {
this._req.abort();
this._source.close();
}
this._aborted = true; // Stop any timeout used for initiating a retry.
if (this._retryTimeout != null) {
clearTimeout(this._retryTimeout);
this._retryTimeout = null;
}
if (!shouldTerminate || this.url == null) {
return Promise.resolve();
}
return BaseUpload.terminate(this.url, this.options) // Remove entry from the URL storage since the upload URL is no longer valid.
.then(function () {
return _this4._removeFromUrlStorage();
});
}
}, {
key: "_emitHttpError",
value: function _emitHttpError(req, res, message, causingErr) {
this._emitError(new _error.default(message, causingErr, req, res));
}
}, {
key: "_emitError",
value: function _emitError(err) {
var _this5 = this; // Do not emit errors, e.g. from aborted HTTP requests, if the upload has been stopped.
if (this._aborted) return; // Check if we should retry, when enabled, before sending the error to the user.
if (this.options.retryDelays != null) {
// We will reset the attempt counter if
// - we were already able to connect to the server (offset != null) and
// - we were able to upload a small chunk of data to the server
var shouldResetDelays = this._offset != null && this._offset > this._offsetBeforeRetry;
if (shouldResetDelays) {
this._retryAttempt = 0;
}
if (shouldRetry(err, this._retryAttempt, this.options)) {
var delay = this.options.retryDelays[this._retryAttempt++];
this._offsetBeforeRetry = this._offset;
this._retryTimeout = setTimeout(function () {
_this5.start();
}, delay);
return;
}
}
if (typeof this.options.onError === 'function') {
this.options.onError(err);
} else {
throw err;
}
}
/**
* Publishes notification if the upload has been successfully completed.
*
* @api private
*/
}, {
key: "_emitSuccess",
value: function _emitSuccess(data) {
if (this.options.removeFingerprintOnSuccess) {
// Remove stored fingerprint and corresponding endpoint. This causes
// new uploads of the same file to be treated as a different file.
this._removeFromUrlStorage();
}
if (typeof this.options.onSuccess === 'function') {
this.options.onSuccess(data);
}
}
/**
* Publishes notification when data has been sent to the server. This
* data may not have been accepted by the server yet.
*
* @param {number} bytesSent Number of bytes sent to the server.
* @param {number} bytesTotal Total number of bytes to be sent to the server.
* @api private
*/
}, {
key: "_emitProgress",
value: function _emitProgress(bytesSent, bytesTotal) {
if (typeof this.options.onProgress === 'function') {
this.options.onProgress(bytesSent, bytesTotal);
}
}
/**
* Publishes notification when a chunk of data has been sent to the server
* and accepted by the server.
* @param {number} chunkSize Size of the chunk that was accepted by the server.
* @param {number} bytesAccepted Total number of bytes that have been
* accepted by the server.
* @param {number} bytesTotal Total number of bytes to be sent to the server.
* @api private
*/
}, {
key: "_emitChunkComplete",
value: function _emitChunkComplete(chunkSize, bytesAccepted, bytesTotal) {
if (typeof this.options.onChunkComplete === 'function') {
this.options.onChunkComplete(chunkSize, bytesAccepted, bytesTotal);
}
}
/**
* Create a new upload using the creation extension by sending a POST
* request to the endpoint. After successful creation the file will be
* uploaded
*
* @api private
*/
}, {
key: "_createUpload",
value: function _createUpload() {
var _this6 = this;
if (!this.options.endpoint) {
this._emitError(new Error('tus: unable to create upload because no endpoint is provided'));
return;
}
var req = this._openRequest('POST', this.options.endpoint);
if (this.options.uploadLengthDeferred) {
req.setHeader('Upload-Defer-Length', 1);
} else {
req.setHeader('Upload-Length', this._size);
} // Add metadata if values have been added
var metadata = encodeMetadata(this.options.metadata);
if (metadata !== '') {
req.setHeader('Upload-Metadata', metadata);
}
var promise;
if (this.options.uploadDataDuringCreation && !this.options.uploadLengthDeferred) {
this._offset = 0;
promise = this._addChunkToRequest(req);
} else {
promise = this._sendRequest(req, null);
}
promise.then(function (res) {
if (!inStatusCategory(res.getStatus(), 200)) {
_this6._emitHttpError(req, res, 'tus: unexpected response while creating upload');
return;
}
var location = res.getHeader('Location');
if (location == null) {
_this6._emitHttpError(req, res, 'tus: invalid or missing Location header');
return;
}
if (_this6.options.mapUrl) {
_this6.url = _this6.options.mapUrl(resolveUrl(_this6.options.endpoint, location));
} else {
_this6.url = resolveUrl(_this6.options.endpoint, location);
}
(0, _logger.log)("Created upload at ".concat(_this6.url));
if (typeof _this6.options._onUploadUrlAvailable === 'function') {
_this6.options._onUploadUrlAvailable();
}
if (_this6._size === 0) {
// Nothing to upload and file was successfully created
_this6._emitSuccess(res.getBody());
_this6._source.close();
return;
}
_this6._saveUploadInUrlStorage();
if (_this6.options.uploadDataDuringCreation) {
_this6._handleUploadResponse(req, res);
} else {
_this6._offset = 0;
_this6._performUpload();
}
})["catch"](function (err) {
_this6._emitHttpError(req, null, 'tus: failed to create upload', err);
});
}
/*
* Try to resume an existing upload. First a HEAD request will be sent
* to retrieve the offset. If the request fails a new upload will be
* created. In the case of a successful response the file will be uploaded.
*
* @api private
*/
}, {
key: "_resumeUpload",
value: function _resumeUpload() {
var _this7 = this;
var req = this._openRequest('HEAD', this.url);
var promise = this._sendRequest(req, null);
promise.then(function (res) {
var status = res.getStatus();
if (!inStatusCategory(status, 200)) {
if (inStatusCategory(status, 400)) {
// Remove stored fingerprint and corresponding endpoint,
// on client errors since the file can not be found
_this7._removeFromUrlStorage();
} // If the upload is locked (indicated by the 423 Locked status code), we
// emit an error instead of directly starting a new upload. This way the
// retry logic can catch the error and will retry the upload. An upload
// is usually locked for a short period of time and will be available
// afterwards.
if (status === 423) {
_this7._emitHttpError(req, res, 'tus: upload is currently locked; retry later');
return;
}
if (!_this7.options.endpoint) {
// Don't attempt to create a new upload if no endpoint is provided.
_this7._emitHttpError(req, res, 'tus: unable to resume upload (new upload cannot be created without an endpoint)');
return;
} // Try to create a new upload
_this7.url = null;
_this7._createUpload();
return;
}
var offset = parseInt(res.getHeader('Upload-Offset'), 10);
if (isNaN(offset)) {
_this7._emitHttpError(req, res, 'tus: invalid or missing offset value');
return;
}
var length = parseInt(res.getHeader('Upload-Length'), 10);
if (isNaN(length) && !_this7.options.uploadLengthDeferred) {
_this7._emitHttpError(req, res, 'tus: invalid or missing length value');
return;
}
if (typeof _this7.options._onUploadUrlAvailable === 'function') {
_this7.options._onUploadUrlAvailable();
} // Upload has already been completed and we do not need to send additional
// data to the server
if (offset === length) {
_this7._emitProgress(length, length);
_this7._emitSuccess(res.getBody());
return;
}
_this7._offset = offset;
_this7._performUpload();
})["catch"](function (err) {
_this7._emitHttpError(req, null, 'tus: failed to resume upload', err);
});
}
/**
* Start uploading the file using PATCH requests. The file will be divided
* into chunks as specified in the chunkSize option. During the upload
* the onProgress event handler may be invoked multiple times.
*
* @api private
*/
}, {
key: "_performUpload",
value: function _performUpload() {
var _this8 = this; // If the upload has been aborted, we will not send the next PATCH request.
// This is important if the abort method was called during a callback, such
// as onChunkComplete or onProgress.
if (this._aborted) {
return;
}
var req; // Some browser and servers may not support the PATCH method. For those
// cases, you can tell tus-js-client to use a POST request with the
// X-HTTP-Method-Override header for simulating a PATCH request.
if (this.options.overridePatchMethod) {
req = this._openRequest('POST', this.url);
req.setHeader('X-HTTP-Method-Override', 'PATCH');
} else {
req = this._openRequest('PATCH', this.url);
}
req.setHeader('Upload-Offset', this._offset);
var promise = this._addChunkToRequest(req);
promise.then(function (res) {
if (!inStatusCategory(res.getStatus(), 200)) {
_this8._emitHttpError(req, res, 'tus: unexpected response while uploading chunk');
return;
}
_this8._handleUploadResponse(req, res);
})["catch"](function (err) {
// Don't emit an error if the upload was aborted manually
if (_this8._aborted) {
return;
}
_this8._emitHttpError(req, null, "tus: failed to upload chunk at offset ".concat(_this8._offset), err);
});
}
/**
* _addChunktoRequest reads a chunk from the source and sends it using the
* supplied request object. It will not handle the response.
*
* @api private
*/
}, {
key: "_addChunkToRequest",
value: function _addChunkToRequest(req) {
var _this9 = this;
var start = this._offset;
var end = this._offset + this.options.chunkSize;
req.setProgressHandler(function (bytesSent) {
_this9._emitProgress(start + bytesSent, _this9._size);
});
req.setHeader('Content-Type', 'application/offset+octet-stream'); // The specified chunkSize may be Infinity or the calcluated end position
// may exceed the file's size. In both cases, we limit the end position to
// the input's total size for simpler calculations and correctness.
if ((end === Infinity || end > this._size) && !this.options.uploadLengthDeferred) {
end = this._size;
}
return this._source.slice(start, end).then(function (_ref2) {
var value = _ref2.value,
done = _ref2.done; // If the upload length is deferred, the upload size was not specified during
// upload creation. So, if the file reader is done reading, we know the total
// upload size and can tell the tus server.
if (_this9.options.uploadLengthDeferred && done) {
_this9._size = _this9._offset + (value && value.size ? value.size : 0);
req.setHeader('Upload-Length', _this9._size);
}
if (value === null) {
return _this9._sendRequest(req);
}
_this9._emitProgress(_this9._offset, _this9._size);
return _this9._sendRequest(req, value);
});
}
/**
* _handleUploadResponse is used by requests that haven been sent using _addChunkToRequest
* and already have received a response.
*
* @api private
*/
}, {
key: "_handleUploadResponse",
value: function _handleUploadResponse(req, res) {
var offset = parseInt(res.getHeader('Upload-Offset'), 10);
if (isNaN(offset)) {
this._emitHttpError(req, res, 'tus: invalid or missing offset value');
return;
}
this._emitProgress(offset, this._size);
this._emitChunkComplete(offset - this._offset, offset, this._size);
this._offset = offset;
if (offset == this._size) {
// Yay, finally done :)
this._emitSuccess(res.getBody());
this._source.close();
return;
}
this._performUpload();
}
/**
* Create a new HTTP request object with the given method and URL.
*
* @api private
*/
}, {
key: "_openRequest",
value: function _openRequest(method, url) {
var req = openRequest(method, url, this.options);
this._req = req;
return req;
}
/**
* Remove the entry in the URL storage, if it has been saved before.
*
* @api private
*/
}, {
key: "_removeFromUrlStorage",
value: function _removeFromUrlStorage() {
var _this10 = this;
if (!this._urlStorageKey) return;
this._urlStorage.removeUpload(this._urlStorageKey)["catch"](function (err) {
_this10._emitError(err);
});
this._urlStorageKey = null;
}
/**
* Add the upload URL to the URL storage, if possible.
*
* @api private
*/
}, {
key: "_saveUploadInUrlStorage",
value: function _saveUploadInUrlStorage() {
var _this11 = this; // Only if a fingerprint was calculated for the input (i.e. not a stream), we can store the upload URL.
if (!this.options.storeFingerprintForResuming || !this._fingerprint) {
return;
}
var storedUpload = {
size: this._size,
metadata: this.options.metadata,
creationTime: new Date().toString()
};
if (this._parallelUploads) {
// Save multiple URLs if the parallelUploads option is used ...
storedUpload.parallelUploadUrls = this._parallelUploadUrls;
} else {
// ... otherwise we just save the one available URL.
storedUpload.uploadUrl = this.url;
}
this._urlStorage.addUpload(this._fingerprint, storedUpload).then(function (urlStorageKey) {
return _this11._urlStorageKey = urlStorageKey;
})["catch"](function (err) {
_this11._emitError(err);
});
}
/**
* Send a request with the provided body.
*
* @api private
*/
}, {
key: "_sendRequest",
value: function _sendRequest(req) {
var body = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
return sendRequest(req, body, this.options);
}
}], [{
key: "terminate",
value: function terminate(url, options) {
// Count the number of arguments to see if a callback is being provided as the last
// argument in the old style required by tus-js-client 1.x, then throw an error if it is.
// `arguments` is a JavaScript built-in variable that contains all of the function's arguments.
if (arguments.length > 1 && typeof arguments[arguments.length - 1] === 'function') {
throw new Error('tus: the terminate function does not accept a callback since v2 anymore; please use the returned Promise instead');
} // Note that in order for the trick above to work, a default value cannot be set for `options`,
// so the check below replaces the old default `{}`.
if (options === undefined) {
options = {};
}
var req = openRequest('DELETE', url, options);
return sendRequest(req, null, options).then(function (res) {
// A 204 response indicates a successfull request
if (res.getStatus() === 204) {
return;
}
throw new _error.default('tus: unexpected response while terminating upload', null, req, res);
})["catch"](function (err) {
if (!(err instanceof _error.default)) {
err = new _error.default('tus: failed to terminate upload', err, req, null);
}
if (!shouldRetry(err, 0, options)) {
throw err;
} // Instead of keeping track of the retry attempts, we remove the first element from the delays
// array. If the array is empty, all retry attempts are used up and we will bubble up the error.
// We recursively call the terminate function will removing elements from the retryDelays array.
var delay = options.retryDelays[0];
var remainingDelays = options.retryDelays.slice(1);
var newOptions = _objectSpread(_objectSpread({}, options), {}, {
retryDelays: remainingDelays
});
return new Promise(function (resolve) {
return setTimeout(resolve, delay);
}).then(function () {
return BaseUpload.terminate(url, newOptions);
});
});
}
}]);
return BaseUpload;
}();
function encodeMetadata(metadata) {
var encoded = [];
for (var key in metadata) {
encoded.push("".concat(key, " ").concat(_jsBase.Base64.encode(metadata[key])));
}
return encoded.join(',');
}
/**
* Checks whether a given status is in the range of the expected category.
* For example, only a status between 200 and 299 will satisfy the category 200.
*
* @api private
*/
function inStatusCategory(status, category) {
return status >= category && status < category + 100;
}
/**
* Create a new HTTP request with the specified method and URL.
* The necessary headers that are included in every request
* will be added, including the request ID.
*
* @api private
*/
function openRequest(method, url, options) {
var req = options.httpStack.createRequest(method, url);
req.setHeader('Tus-Resumable', '1.0.0');
var headers = options.headers || {};
for (var name in headers) {
req.setHeader(name, headers[name]);
}
if (options.addRequestId) {
var requestId = (0, _uuid.default)();
req.setHeader('X-Request-ID', requestId);
}
return req;
}
/**
* Send a request with the provided body while invoking the onBeforeRequest
* and onAfterResponse callbacks.
*
* @api private
*/
function sendRequest(req, body, options) {
var onBeforeRequestPromise = typeof options.onBeforeRequest === 'function' ? Promise.resolve(options.onBeforeRequest(req)) : Promise.resolve();
return onBeforeRequestPromise.then(function () {
return req.send(body).then(function (res) {
var onAfterResponsePromise = typeof options.onAfterResponse === 'function' ? Promise.resolve(options.onAfterResponse(req, res)) : Promise.resolve();
return onAfterResponsePromise.then(function () {
return res;
});
});
});
}
/**
* Checks whether the browser running this code has internet access.
* This function will always return true in the node.js environment
*
* @api private
*/
function isOnline() {
var online = true;
if (typeof window !== 'undefined' && 'navigator' in window && window.navigator.onLine === false) {
online = false;
}
return online;
}
/**
* Checks whether or not it is ok to retry a request.
* @param {Error} err the error returned from the last request
* @param {number} retryAttempt the number of times the request has already been retried
* @param {object} options tus Upload options
*
* @api private
*/
function shouldRetry(err, retryAttempt, options) {
// We only attempt a retry if
// - retryDelays option is set
// - we didn't exceed the maxium number of retries, yet, and
// - this error was caused by a request or it's response and
// - the error is server error (i.e. not a status 4xx except a 409 or 423) or
// a onShouldRetry is specified and returns true
// - the browser does not indicate that we are offline
if (options.retryDelays == null || retryAttempt >= options.retryDelays.length || err.originalRequest == null) {
return false;
}
if (options && typeof options.onShouldRetry === 'function') {
return options.onShouldRetry(err, retryAttempt, options);
}
var status = err.originalResponse ? err.originalResponse.getStatus() : 0;
return (!inStatusCategory(status, 400) || status === 409 || status === 423) && isOnline();
}
/**
* Resolve a relative link given the origin as source. For example,
* if a HTTP request to http://example.com/files/ returns a Location
* header with the value /upload/abc, the resolved URL will be:
* http://example.com/upload/abc
*/
function resolveUrl(origin, link) {
return new _urlParse.default(link, origin).toString();
}
/**
* Calculate the start and end positions for the parts if an upload
* is split into multiple parallel requests.
*
* @param {number} totalSize The byte size of the upload, which will be split.
* @param {number} partCount The number in how many parts the upload will be split.
* @param {string[]} previousUrls The upload URLs for previous parts.
* @return {object[]}
* @api private
*/
function splitSizeIntoParts(totalSize, partCount, previousUrls) {
var partSize = Math.floor(totalSize / partCount);
var parts = [];
for (var i = 0; i < partCount; i++) {
parts.push({
start: partSize * i,
end: partSize * (i + 1)
});
}
parts[partCount - 1].end = totalSize; // Attach URLs from previous uploads, if available.
if (previousUrls) {
parts.forEach(function (part, index) {
part.uploadUrl = previousUrls[index] || null;
});
}
return parts;
}
BaseUpload.defaultOptions = defaultOptions;
var _default = BaseUpload;
exports.default = _default;