UNPKG

tus-js-client-olalonde

Version:

A pure JavaScript client for the tus resumable upload protocol

891 lines (694 loc) 28 kB
(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){ "use strict"; 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){ "use strict"; 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){ "use strict"; 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){ "use strict"; 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){ "use strict"; 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){ "use strict"; 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){ "use strict"; 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){ "use strict"; 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){ 'use strict'; 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