tus-js-client-olalonde
Version:
A pure JavaScript client for the tus resumable upload protocol
891 lines (694 loc) • 28 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.tus = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.encode = encode;
/* global: window */
var _window = window;
var btoa = _window.btoa;
function encode(data) {
return btoa(unescape(encodeURIComponent(data)));
}
var isSupported = exports.isSupported = "btoa" in window;
},{}],2:[function(_dereq_,module,exports){
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.newRequest = newRequest;
exports.resolveUrl = resolveUrl;
var _resolveUrl = _dereq_("resolve-url");
var _resolveUrl2 = _interopRequireDefault(_resolveUrl);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function newRequest() {
return new window.XMLHttpRequest();
} /* global window */
function resolveUrl(origin, link) {
return (0, _resolveUrl2.default)(origin, link);
}
},{"resolve-url":10}],3:[function(_dereq_,module,exports){
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = 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); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
exports.getSource = getSource;
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var FileSource = function () {
function FileSource(file) {
_classCallCheck(this, FileSource);
this._file = file;
this.size = file.size;
}
_createClass(FileSource, [{
key: "slice",
value: function slice(start, end) {
return this._file.slice(start, end);
}
}, {
key: "close",
value: function close() {}
}]);
return FileSource;
}();
function getSource(input) {
// Since we emulate the Blob type in our tests (not all target browsers
// support it), we cannot use `instanceof` for testing whether the input value
// can be handled. Instead, we simply check is the slice() function and the
// size property are available.
if (typeof input.slice === "function" && typeof input.size !== "undefined") {
return new FileSource(input);
}
throw new Error("source object may only be an instance of File or Blob in this environment");
}
},{}],4:[function(_dereq_,module,exports){
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.setItem = setItem;
exports.getItem = getItem;
exports.removeItem = removeItem;
/* global window, localStorage */
var hasStorage = false;
try {
hasStorage = "localStorage" in window;
// Attempt to access localStorage
localStorage.length;
} catch (e) {
// If we try to access localStorage inside a sandboxed iframe, a SecurityError
// is thrown.
if (e.code === e.SECURITY_ERR) {
hasStorage = false;
} else {
throw e;
}
}
var canStoreURLs = exports.canStoreURLs = hasStorage;
function setItem(key, value) {
if (!hasStorage) return;
return localStorage.setItem(key, value);
}
function getItem(key) {
if (!hasStorage) return;
return localStorage.getItem(key);
}
function removeItem(key) {
if (!hasStorage) return;
return localStorage.removeItem(key);
}
},{}],5:[function(_dereq_,module,exports){
;
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var DetailedError = function (_Error) {
_inherits(DetailedError, _Error);
function DetailedError(error) {
var causingErr = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1];
var xhr = arguments.length <= 2 || arguments[2] === undefined ? null : arguments[2];
_classCallCheck(this, DetailedError);
var _this = _possibleConstructorReturn(this, (DetailedError.__proto__ || Object.getPrototypeOf(DetailedError)).call(this, error.message));
_this.originalRequest = xhr;
_this.causingError = causingErr;
var message = error.message;
if (causingErr != null) {
message += ", caused by " + causingErr.toString();
}
if (xhr != null) {
message += ", originated from request (response code: " + xhr.status + ", response text: " + xhr.responseText + ")";
}
_this.message = message;
return _this;
}
return DetailedError;
}(Error);
exports.default = DetailedError;
},{}],6:[function(_dereq_,module,exports){
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = fingerprint;
/**
* Generate a fingerprint for a file which will be used the store the endpoint
*
* @param {File} file
* @return {String}
*/
function fingerprint(file) {
return ["tus", file.name, file.type, file.size, file.lastModified].join("-");
}
},{}],7:[function(_dereq_,module,exports){
;
var _upload = _dereq_("./upload");
var _upload2 = _interopRequireDefault(_upload);
var _storage = _dereq_("./node/storage");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/* global window */
var defaultOptions = _upload2.default.defaultOptions;
if (typeof window !== "undefined") {
// Browser environment using XMLHttpRequest
var _window = window;
var XMLHttpRequest = _window.XMLHttpRequest;
var Blob = _window.Blob;
var isSupported = XMLHttpRequest && Blob && typeof Blob.prototype.slice === "function";
} else {
// Node.js environment using http module
var isSupported = true;
}
// The usage of the commonjs exporting syntax instead of the new ECMAScript
// one is actually inteded and prevents weird behaviour if we are trying to
// import this module in another module using Babel.
module.exports = {
Upload: _upload2.default,
isSupported: isSupported,
canStoreURLs: _storage.canStoreURLs,
defaultOptions: defaultOptions
};
},{"./node/storage":4,"./upload":8}],8:[function(_dereq_,module,exports){
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = 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); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* global window */
// We import the files used inside the Node environment which are rewritten
// for browsers using the rules defined in the package.json
var _fingerprint = _dereq_("./fingerprint");
var _fingerprint2 = _interopRequireDefault(_fingerprint);
var _error = _dereq_("./error");
var _error2 = _interopRequireDefault(_error);
var _extend = _dereq_("extend");
var _extend2 = _interopRequireDefault(_extend);
var _request = _dereq_("./node/request");
var _source = _dereq_("./node/source");
var _base = _dereq_("./node/base64");
var Base64 = _interopRequireWildcard(_base);
var _storage = _dereq_("./node/storage");
var Storage = _interopRequireWildcard(_storage);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var defaultOptions = {
endpoint: "",
fingerprint: _fingerprint2.default,
resume: true,
onProgress: null,
onChunkComplete: null,
onSuccess: null,
onError: null,
headers: {},
chunkSize: Infinity,
withCredentials: false,
uploadUrl: null,
uploadSize: null,
overridePatchMethod: false,
retryDelays: null
};
var Upload = function () {
function Upload(file, options) {
_classCallCheck(this, Upload);
this.options = (0, _extend2.default)(true, {}, defaultOptions, options);
// The underlying File/Blob object
this.file = file;
// The URL against which the file will be uploaded
this.url = null;
// The underlying XHR object for the current PATCH request
this._xhr = null;
// The fingerpinrt for the current file (set after start())
this._fingerprint = 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. Null 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;
}
_createClass(Upload, [{
key: "start",
value: function start() {
var _this = 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._emitError(new Error("tus: no endpoint provided"));
return;
}
var source = this._source = (0, _source.getSource)(file, this.options.chunkSize);
// Firstly, check if the caller has supplied a manual upload size or else
// we will use the calculated size by the source object.
if (this.options.uploadSize != null) {
var size = +this.options.uploadSize;
if (isNaN(size)) {
throw new Error("tus: cannot convert `uploadSize` option into a number");
}
this._size = size;
} else {
var _size = source.size;
// The size property will be null if we cannot calculate the file's size,
// for example if you handle a stream.
if (_size == null) {
throw new Error("tus: cannot automatically derive upload's size from input and must be specified manually using the `uploadSize` option");
}
this._size = _size;
}
var retryDelays = this.options.retryDelays;
if (retryDelays != null) {
if (Object.prototype.toString.call(retryDelays) !== "[object Array]") {
throw new Error("tus: the `retryDelays` option must either be an array or null");
} else {
(function () {
var errorCallback = _this.options.onError;
_this.options.onError = function (err) {
// Restore the original error callback which may have been set.
_this.options.onError = errorCallback;
// 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;
}
var isOnline = true;
if (typeof window !== "undefined" && "navigator" in window && window.navigator.onLine === false) {
isOnline = false;
}
// We only attempt a retry if
// - we didn't exceed the maxium number of retries, yet, and
// - this error was caused by a request or it's response and
// - the browser does not indicate that we are offline
var shouldRetry = _this._retryAttempt < retryDelays.length && err.originalRequest != null && isOnline;
if (!shouldRetry) {
_this._emitError(err);
return;
}
var delay = retryDelays[_this._retryAttempt++];
_this._offsetBeforeRetry = _this._offset;
_this.options.uploadUrl = _this.url;
_this._retryTimeout = setTimeout(function () {
_this.start();
}, delay);
};
})();
}
}
// A URL has manually been specified, so we try to resume
if (this.options.uploadUrl != null) {
this.url = this.options.uploadUrl;
this._resumeUpload();
return;
}
// Try to find the endpoint for the file in the storage
if (this.options.resume) {
this._fingerprint = this.options.fingerprint(file);
var resumedUrl = Storage.getItem(this._fingerprint);
if (resumedUrl != null) {
this.url = resumedUrl;
this._resumeUpload();
return;
}
}
// An upload has not started for the file yet, so we start a new one
this._createUpload();
}
}, {
key: "abort",
value: function abort() {
if (this._xhr !== null) {
this._xhr.abort();
this._source.close();
this._aborted = true;
}
if (this._retryTimeout != null) {
clearTimeout(this._retryTimeout);
this._retryTimeout = null;
}
}
}, {
key: "_emitXhrError",
value: function _emitXhrError(xhr, err, causingErr) {
this._emitError(new _error2.default(err, causingErr, xhr));
}
}, {
key: "_emitError",
value: function _emitError(err) {
if (typeof this.options.onError === "function") {
this.options.onError(err);
} else {
throw err;
}
}
}, {
key: "_emitSuccess",
value: function _emitSuccess() {
if (typeof this.options.onSuccess === "function") {
this.options.onSuccess();
}
}
/**
* 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.
*/
}, {
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.
*/
}, {
key: "_emitChunkComplete",
value: function _emitChunkComplete(chunkSize, bytesAccepted, bytesTotal) {
if (typeof this.options.onChunkComplete === "function") {
this.options.onChunkComplete(chunkSize, bytesAccepted, bytesTotal);
}
}
/**
* Set the headers used in the request and the withCredentials property
* as defined in the options
*
* @param {XMLHttpRequest} xhr
*/
}, {
key: "_setupXHR",
value: function _setupXHR(xhr) {
xhr.setRequestHeader("Tus-Resumable", "1.0.0");
var headers = this.options.headers;
for (var name in headers) {
xhr.setRequestHeader(name, headers[name]);
}
xhr.withCredentials = this.options.withCredentials;
}
/**
* 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 _this2 = this;
var xhr = (0, _request.newRequest)();
xhr.open("POST", this.options.endpoint, true);
xhr.onload = function () {
if (!(xhr.status >= 200 && xhr.status < 300)) {
_this2._emitXhrError(xhr, new Error("tus: unexpected response while creating upload"));
return;
}
_this2.url = (0, _request.resolveUrl)(_this2.options.endpoint, xhr.getResponseHeader("Location"));
if (_this2.options.resume) {
Storage.setItem(_this2._fingerprint, _this2.url);
}
_this2._offset = 0;
_this2._startUpload();
};
xhr.onerror = function (err) {
_this2._emitXhrError(xhr, new Error("tus: failed to create upload"), err);
};
this._setupXHR(xhr);
xhr.setRequestHeader("Upload-Length", this._size);
// Add metadata if values have been added
var metadata = encodeMetadata(this.options.metadata);
if (metadata !== "") {
xhr.setRequestHeader("Upload-Metadata", metadata);
}
xhr.send(null);
}
/*
* 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 _this3 = this;
var xhr = (0, _request.newRequest)();
xhr.open("HEAD", this.url, true);
xhr.onload = function () {
if (!(xhr.status >= 200 && xhr.status < 300)) {
if (_this3.options.resume) {
// Remove stored fingerprint and corresponding endpoint,
// since the file can not be found
Storage.removeItem(_this3._fingerprint);
}
// Try to create a new upload
_this3.url = null;
_this3._createUpload();
return;
}
var offset = parseInt(xhr.getResponseHeader("Upload-Offset"), 10);
if (isNaN(offset)) {
_this3._emitXhrError(xhr, new Error("tus: invalid or missing offset value"));
return;
}
var length = parseInt(xhr.getResponseHeader("Upload-Length"), 10);
if (isNaN(length)) {
_this3._emitXhrError(xhr, new Error("tus: invalid or missing length value"));
return;
}
// Upload has already been completed and we do not need to send additional
// data to the server
if (offset === length) {
_this3._emitProgress(length, length);
_this3._emitSuccess();
return;
}
_this3._offset = offset;
_this3._startUpload();
};
xhr.onerror = function (err) {
_this3._emitXhrError(xhr, new Error("tus: failed to resume upload"), err);
};
this._setupXHR(xhr);
xhr.send(null);
}
/**
* 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: "_startUpload",
value: function _startUpload() {
var _this4 = this;
var xhr = this._xhr = (0, _request.newRequest)();
// 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) {
xhr.open("POST", this.url, true);
xhr.setRequestHeader("X-HTTP-Method-Override", "PATCH");
} else {
xhr.open("PATCH", this.url, true);
}
xhr.onload = function () {
if (!(xhr.status >= 200 && xhr.status < 300)) {
_this4._emitXhrError(xhr, new Error("tus: unexpected response while uploading chunk"));
return;
}
var offset = parseInt(xhr.getResponseHeader("Upload-Offset"), 10);
if (isNaN(offset)) {
_this4._emitXhrError(xhr, new Error("tus: invalid or missing offset value"));
return;
}
_this4._emitProgress(offset, _this4._size);
_this4._emitChunkComplete(offset - _this4._offset, offset, _this4._size);
_this4._offset = offset;
if (offset == _this4._size) {
// Yay, finally done :)
_this4._emitSuccess();
_this4._source.close();
return;
}
_this4._startUpload();
};
xhr.onerror = function (err) {
// Don't emit an error if the upload was aborted manually
if (_this4._aborted) {
return;
}
_this4._emitXhrError(xhr, new Error("tus: failed to upload chunk at offset " + _this4._offset), err);
};
// Test support for progress events before attaching an event listener
if ("upload" in xhr) {
xhr.upload.onprogress = function (e) {
if (!e.lengthComputable) {
return;
}
_this4._emitProgress(start + e.loaded, _this4._size);
};
}
this._setupXHR(xhr);
xhr.setRequestHeader("Upload-Offset", this._offset);
xhr.setRequestHeader("Content-Type", "application/offset+octet-stream");
var start = this._offset;
var end = this._offset + this.options.chunkSize;
// 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) {
end = this._size;
}
xhr.send(this._source.slice(start, end));
}
}]);
return Upload;
}();
function encodeMetadata(metadata) {
if (!Base64.isSupported) {
return "";
}
var encoded = [];
for (var key in metadata) {
encoded.push(key + " " + Base64.encode(metadata[key]));
}
return encoded.join(",");
}
Upload.defaultOptions = defaultOptions;
exports.default = Upload;
},{"./error":5,"./fingerprint":6,"./node/base64":1,"./node/request":2,"./node/source":3,"./node/storage":4,"extend":9}],9:[function(_dereq_,module,exports){
;
var hasOwn = Object.prototype.hasOwnProperty;
var toStr = Object.prototype.toString;
var isArray = function isArray(arr) {
if (typeof Array.isArray === 'function') {
return Array.isArray(arr);
}
return toStr.call(arr) === '[object Array]';
};
var isPlainObject = function isPlainObject(obj) {
if (!obj || toStr.call(obj) !== '[object Object]') {
return false;
}
var hasOwnConstructor = hasOwn.call(obj, 'constructor');
var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf');
// Not own constructor property must be Object
if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) {
return false;
}
// Own properties are enumerated firstly, so to speed up,
// if last one is own, then all properties are own.
var key;
for (key in obj) {/**/}
return typeof key === 'undefined' || hasOwn.call(obj, key);
};
module.exports = function extend() {
var options, name, src, copy, copyIsArray, clone,
target = arguments[0],
i = 1,
length = arguments.length,
deep = false;
// Handle a deep copy situation
if (typeof target === 'boolean') {
deep = target;
target = arguments[1] || {};
// skip the boolean and the target
i = 2;
} else if ((typeof target !== 'object' && typeof target !== 'function') || target == null) {
target = {};
}
for (; i < length; ++i) {
options = arguments[i];
// Only deal with non-null/undefined values
if (options != null) {
// Extend the base object
for (name in options) {
src = target[name];
copy = options[name];
// Prevent never-ending loop
if (target !== copy) {
// Recurse if we're merging plain objects or arrays
if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) {
if (copyIsArray) {
copyIsArray = false;
clone = src && isArray(src) ? src : [];
} else {
clone = src && isPlainObject(src) ? src : {};
}
// Never move original objects, clone them
target[name] = extend(deep, clone, copy);
// Don't bring in undefined values
} else if (typeof copy !== 'undefined') {
target[name] = copy;
}
}
}
}
}
// Return the modified object
return target;
};
},{}],10:[function(_dereq_,module,exports){
// Copyright 2014 Simon Lydell
// X11 (“MIT”) Licensed. (See LICENSE.)
void (function(root, factory) {
if (typeof define === "function" && define.amd) {
define(factory)
} else if (typeof exports === "object") {
module.exports = factory()
} else {
root.resolveUrl = factory()
}
}(this, function() {
function resolveUrl(/* ...urls */) {
var numUrls = arguments.length
if (numUrls === 0) {
throw new Error("resolveUrl requires at least one argument; got none.")
}
var base = document.createElement("base")
base.href = arguments[0]
if (numUrls === 1) {
return base.href
}
var head = document.getElementsByTagName("head")[0]
head.insertBefore(base, head.firstChild)
var a = document.createElement("a")
var resolved
for (var index = 1; index < numUrls; index++) {
a.href = arguments[index]
resolved = a.href
base.href = resolved
}
head.removeChild(base)
return resolved
}
return resolveUrl
}));
},{}]},{},[7])(7)
});
//# sourceMappingURL=tus.js.map