(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i= 10 || num % 1 === 0) { // Do not show decimals when the number is two-digit, or if the number has no // decimal component. return (neg ? '-' : '') + num.toFixed(0) + ' ' + unit } else { return (neg ? '-' : '') + num.toFixed(1) + ' ' + unit } } },{}],3:[function(require,module,exports){ /** * cuid.js * Collision-resistant UID generator for browsers and node. * Sequential for fast db lookups and recency sorting. * Safe for element IDs and server-side lookups. * * Extracted from CLCTR * * Copyright (c) Eric Elliott 2012 * MIT License */ var fingerprint = require('./lib/fingerprint.js'); var pad = require('./lib/pad.js'); var getRandomValue = require('./lib/getRandomValue.js'); var c = 0, blockSize = 4, base = 36, discreteValues = Math.pow(base, blockSize); function randomBlock () { return pad((getRandomValue() * discreteValues << 0) .toString(base), blockSize); } function safeCounter () { c = c < discreteValues ? c : 0; c++; // this is not subliminal return c - 1; } function cuid () { // Starting with a lowercase letter makes // it HTML element ID friendly. var letter = 'c', // hard-coded allows for sequential access // timestamp // warning: this exposes the exact date and time // that the uid was created. timestamp = (new Date().getTime()).toString(base), // Prevent same-machine collisions. counter = pad(safeCounter().toString(base), blockSize), // A few chars to generate distinct ids for different // clients (so different computers are far less // likely to generate the same id) print = fingerprint(), // Grab some more chars from Math.random() random = randomBlock() + randomBlock(); return letter + timestamp + counter + print + random; } cuid.slug = function slug () { var date = new Date().getTime().toString(36), counter = safeCounter().toString(36).slice(-4), print = fingerprint().slice(0, 1) + fingerprint().slice(-1), random = randomBlock().slice(-2); return date.slice(-2) + counter + print + random; }; cuid.isCuid = function isCuid (stringToCheck) { if (typeof stringToCheck !== 'string') return false; if (stringToCheck.startsWith('c')) return true; return false; }; cuid.isSlug = function isSlug (stringToCheck) { if (typeof stringToCheck !== 'string') return false; var stringLength = stringToCheck.length; if (stringLength >= 7 && stringLength <= 10) return true; return false; }; cuid.fingerprint = fingerprint; module.exports = cuid; },{"./lib/fingerprint.js":4,"./lib/getRandomValue.js":5,"./lib/pad.js":6}],4:[function(require,module,exports){ var pad = require('./pad.js'); var env = typeof window === 'object' ? window : self; var globalCount = Object.keys(env).length; var mimeTypesLength = navigator.mimeTypes ? navigator.mimeTypes.length : 0; var clientId = pad((mimeTypesLength + navigator.userAgent.length).toString(36) + globalCount.toString(36), 4); module.exports = function fingerprint () { return clientId; }; },{"./pad.js":6}],5:[function(require,module,exports){ var getRandomValue; var crypto = typeof window !== 'undefined' && (window.crypto || window.msCrypto) || typeof self !== 'undefined' && self.crypto; if (crypto) { var lim = Math.pow(2, 32) - 1; getRandomValue = function () { return Math.abs(crypto.getRandomValues(new Uint32Array(1))[0] / lim); }; } else { getRandomValue = Math.random; } module.exports = getRandomValue; },{}],6:[function(require,module,exports){ module.exports = function pad (num, size) { var s = '000000000' + num; return s.substr(s.length - size); }; },{}],7:[function(require,module,exports){ (function (global){(function (){ /** * lodash (Custom Build) * Build: `lodash modularize exports="npm" -o ./` * Copyright jQuery Foundation and other contributors * Released under MIT license * Based on Underscore.js 1.8.3 * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors */ /** Used as the `TypeError` message for "Functions" methods. */ var FUNC_ERROR_TEXT = 'Expected a function'; /** Used as references for various `Number` constants. */ var NAN = 0 / 0; /** `Object#toString` result references. */ var symbolTag = '[object Symbol]'; /** Used to match leading and trailing whitespace. */ var reTrim = /^\s+|\s+$/g; /** Used to detect bad signed hexadecimal string values. */ var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; /** Used to detect binary string values. */ var reIsBinary = /^0b[01]+$/i; /** Used to detect octal string values. */ var reIsOctal = /^0o[0-7]+$/i; /** Built-in method references without a dependency on `root`. */ var freeParseInt = parseInt; /** Detect free variable `global` from Node.js. */ var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; /** Detect free variable `self`. */ var freeSelf = typeof self == 'object' && self && self.Object === Object && self; /** Used as a reference to the global object. */ var root = freeGlobal || freeSelf || Function('return this')(); /** Used for built-in method references. */ var objectProto = Object.prototype; /** * Used to resolve the * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) * of values. */ var objectToString = objectProto.toString; /* Built-in method references for those with the same name as other `lodash` methods. */ var nativeMax = Math.max, nativeMin = Math.min; /** * Gets the timestamp of the number of milliseconds that have elapsed since * the Unix epoch (1 January 1970 00:00:00 UTC). * * @static * @memberOf _ * @since 2.4.0 * @category Date * @returns {number} Returns the timestamp. * @example * * _.defer(function(stamp) { * console.log(_.now() - stamp); * }, _.now()); * // => Logs the number of milliseconds it took for the deferred invocation. */ var now = function() { return root.Date.now(); }; /** * Creates a debounced function that delays invoking `func` until after `wait` * milliseconds have elapsed since the last time the debounced function was * invoked. The debounced function comes with a `cancel` method to cancel * delayed `func` invocations and a `flush` method to immediately invoke them. * Provide `options` to indicate whether `func` should be invoked on the * leading and/or trailing edge of the `wait` timeout. The `func` is invoked * with the last arguments provided to the debounced function. Subsequent * calls to the debounced function return the result of the last `func` * invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the debounced function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until to the next tick, similar to `setTimeout` with a timeout of `0`. * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `_.debounce` and `_.throttle`. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {Function} func The function to debounce. * @param {number} [wait=0] The number of milliseconds to delay. * @param {Object} [options={}] The options object. * @param {boolean} [options.leading=false] * Specify invoking on the leading edge of the timeout. * @param {number} [options.maxWait] * The maximum time `func` is allowed to be delayed before it's invoked. * @param {boolean} [options.trailing=true] * Specify invoking on the trailing edge of the timeout. * @returns {Function} Returns the new debounced function. * @example * * // Avoid costly calculations while the window size is in flux. * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); * * // Invoke `sendMail` when clicked, debouncing subsequent calls. * jQuery(element).on('click', _.debounce(sendMail, 300, { * 'leading': true, * 'trailing': false * })); * * // Ensure `batchLog` is invoked once after 1 second of debounced calls. * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); * var source = new EventSource('/stream'); * jQuery(source).on('message', debounced); * * // Cancel the trailing debounced invocation. * jQuery(window).on('popstate', debounced.cancel); */ function debounce(func, wait, options) { var lastArgs, lastThis, maxWait, result, timerId, lastCallTime, lastInvokeTime = 0, leading = false, maxing = false, trailing = true; if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } wait = toNumber(wait) || 0; if (isObject(options)) { leading = !!options.leading; maxing = 'maxWait' in options; maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; trailing = 'trailing' in options ? !!options.trailing : trailing; } function invokeFunc(time) { var args = lastArgs, thisArg = lastThis; lastArgs = lastThis = undefined; lastInvokeTime = time; result = func.apply(thisArg, args); return result; } function leadingEdge(time) { // Reset any `maxWait` timer. lastInvokeTime = time; // Start the timer for the trailing edge. timerId = setTimeout(timerExpired, wait); // Invoke the leading edge. return leading ? invokeFunc(time) : result; } function remainingWait(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime, result = wait - timeSinceLastCall; return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result; } function shouldInvoke(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime; // Either this is the first call, activity has stopped and we're at the // trailing edge, the system time has gone backwards and we're treating // it as the trailing edge, or we've hit the `maxWait` limit. return (lastCallTime === undefined || (timeSinceLastCall >= wait) || (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); } function timerExpired() { var time = now(); if (shouldInvoke(time)) { return trailingEdge(time); } // Restart the timer. timerId = setTimeout(timerExpired, remainingWait(time)); } function trailingEdge(time) { timerId = undefined; // Only invoke if we have `lastArgs` which means `func` has been // debounced at least once. if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; } function cancel() { if (timerId !== undefined) { clearTimeout(timerId); } lastInvokeTime = 0; lastArgs = lastCallTime = lastThis = timerId = undefined; } function flush() { return timerId === undefined ? result : trailingEdge(now()); } function debounced() { var time = now(), isInvoking = shouldInvoke(time); lastArgs = arguments; lastThis = this; lastCallTime = time; if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime); } if (maxing) { // Handle invocations in a tight loop. timerId = setTimeout(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timerId === undefined) { timerId = setTimeout(timerExpired, wait); } return result; } debounced.cancel = cancel; debounced.flush = flush; return debounced; } /** * Creates a throttled function that only invokes `func` at most once per * every `wait` milliseconds. The throttled function comes with a `cancel` * method to cancel delayed `func` invocations and a `flush` method to * immediately invoke them. Provide `options` to indicate whether `func` * should be invoked on the leading and/or trailing edge of the `wait` * timeout. The `func` is invoked with the last arguments provided to the * throttled function. Subsequent calls to the throttled function return the * result of the last `func` invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the throttled function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until to the next tick, similar to `setTimeout` with a timeout of `0`. * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `_.throttle` and `_.debounce`. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {Function} func The function to throttle. * @param {number} [wait=0] The number of milliseconds to throttle invocations to. * @param {Object} [options={}] The options object. * @param {boolean} [options.leading=true] * Specify invoking on the leading edge of the timeout. * @param {boolean} [options.trailing=true] * Specify invoking on the trailing edge of the timeout. * @returns {Function} Returns the new throttled function. * @example * * // Avoid excessively updating the position while scrolling. * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); * * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); * jQuery(element).on('click', throttled); * * // Cancel the trailing throttled invocation. * jQuery(window).on('popstate', throttled.cancel); */ function throttle(func, wait, options) { var leading = true, trailing = true; if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } if (isObject(options)) { leading = 'leading' in options ? !!options.leading : leading; trailing = 'trailing' in options ? !!options.trailing : trailing; } return debounce(func, wait, { 'leading': leading, 'maxWait': wait, 'trailing': trailing }); } /** * Checks if `value` is the * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is an object, else `false`. * @example * * _.isObject({}); * // => true * * _.isObject([1, 2, 3]); * // => true * * _.isObject(_.noop); * // => true * * _.isObject(null); * // => false */ function isObject(value) { var type = typeof value; return !!value && (type == 'object' || type == 'function'); } /** * Checks if `value` is object-like. A value is object-like if it's not `null` * and has a `typeof` result of "object". * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is object-like, else `false`. * @example * * _.isObjectLike({}); * // => true * * _.isObjectLike([1, 2, 3]); * // => true * * _.isObjectLike(_.noop); * // => false * * _.isObjectLike(null); * // => false */ function isObjectLike(value) { return !!value && typeof value == 'object'; } /** * Checks if `value` is classified as a `Symbol` primitive or object. * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. * @example * * _.isSymbol(Symbol.iterator); * // => true * * _.isSymbol('abc'); * // => false */ function isSymbol(value) { return typeof value == 'symbol' || (isObjectLike(value) && objectToString.call(value) == symbolTag); } /** * Converts `value` to a number. * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to process. * @returns {number} Returns the number. * @example * * _.toNumber(3.2); * // => 3.2 * * _.toNumber(Number.MIN_VALUE); * // => 5e-324 * * _.toNumber(Infinity); * // => Infinity * * _.toNumber('3.2'); * // => 3.2 */ function toNumber(value) { if (typeof value == 'number') { return value; } if (isSymbol(value)) { return NAN; } if (isObject(value)) { var other = typeof value.valueOf == 'function' ? value.valueOf() : value; value = isObject(other) ? (other + '') : other; } if (typeof value != 'string') { return value === 0 ? value : +value; } value = value.replace(reTrim, ''); var isBinary = reIsBinary.test(value); return (isBinary || reIsOctal.test(value)) ? freeParseInt(value.slice(2), isBinary ? 2 : 8) : (reIsBadHex.test(value) ? NAN : +value); } module.exports = throttle; }).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{}],8:[function(require,module,exports){ var wildcard = require('wildcard'); var reMimePartSplit = /[\/\+\.]/; /** # mime-match A simple function to checker whether a target mime type matches a mime-type pattern (e.g. image/jpeg matches image/jpeg OR image/*). ## Example Usage <<< example.js **/ module.exports = function(target, pattern) { function test(pattern) { var result = wildcard(pattern, target, reMimePartSplit); // ensure that we have a valid mime type (should have two parts) return result && result.length >= 2; } return pattern ? test(pattern.split(';')[0]) : test; }; },{"wildcard":12}],9:[function(require,module,exports){ /** * Create an event emitter with namespaces * @name createNamespaceEmitter * @example * var emitter = require('./index')() * * emitter.on('*', function () { * console.log('all events emitted', this.event) * }) * * emitter.on('example', function () { * console.log('example event emitted') * }) */ module.exports = function createNamespaceEmitter () { var emitter = {} var _fns = emitter._fns = {} /** * Emit an event. Optionally namespace the event. Handlers are fired in the order in which they were added with exact matches taking precedence. Separate the namespace and event with a `:` * @name emit * @param {String} event – the name of the event, with optional namespace * @param {...*} data – up to 6 arguments that are passed to the event listener * @example * emitter.emit('example') * emitter.emit('demo:test') * emitter.emit('data', { example: true}, 'a string', 1) */ emitter.emit = function emit (event, arg1, arg2, arg3, arg4, arg5, arg6) { var toEmit = getListeners(event) if (toEmit.length) { emitAll(event, toEmit, [arg1, arg2, arg3, arg4, arg5, arg6]) } } /** * Create en event listener. * @name on * @param {String} event * @param {Function} fn * @example * emitter.on('example', function () {}) * emitter.on('demo', function () {}) */ emitter.on = function on (event, fn) { if (!_fns[event]) { _fns[event] = [] } _fns[event].push(fn) } /** * Create en event listener that fires once. * @name once * @param {String} event * @param {Function} fn * @example * emitter.once('example', function () {}) * emitter.once('demo', function () {}) */ emitter.once = function once (event, fn) { function one () { fn.apply(this, arguments) emitter.off(event, one) } this.on(event, one) } /** * Stop listening to an event. Stop all listeners on an event by only passing the event name. Stop a single listener by passing that event handler as a callback. * You must be explicit about what will be unsubscribed: `emitter.off('demo')` will unsubscribe an `emitter.on('demo')` listener, * `emitter.off('demo:example')` will unsubscribe an `emitter.on('demo:example')` listener * @name off * @param {String} event * @param {Function} [fn] – the specific handler * @example * emitter.off('example') * emitter.off('demo', function () {}) */ emitter.off = function off (event, fn) { var keep = [] if (event && fn) { var fns = this._fns[event] var i = 0 var l = fns ? fns.length : 0 for (i; i < l; i++) { if (fns[i] !== fn) { keep.push(fns[i]) } } } keep.length ? this._fns[event] = keep : delete this._fns[event] } function getListeners (e) { var out = _fns[e] ? _fns[e] : [] var idx = e.indexOf(':') var args = (idx === -1) ? [e] : [e.substring(0, idx), e.substring(idx + 1)] var keys = Object.keys(_fns) var i = 0 var l = keys.length for (i; i < l; i++) { var key = keys[i] if (key === '*') { out = out.concat(_fns[key]) } if (args.length === 2 && args[0] === key) { out = out.concat(_fns[key]) break } } return out } function emitAll (e, fns, args) { var i = 0 var l = fns.length for (i; i < l; i++) { if (!fns[i]) break fns[i].event = e fns[i].apply(fns[i], args) } } return emitter } },{}],10:[function(require,module,exports){ var n,l,u,t,i,o,r,f,e={},c=[],s=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function a(n,l){for(var u in l)n[u]=l[u];return n}function p(n){var l=n.parentNode;l&&l.removeChild(n)}function v(l,u,t){var i,o,r,f={};for(r in u)"key"==r?i=u[r]:"ref"==r?o=u[r]:f[r]=u[r];if(arguments.length>2&&(f.children=arguments.length>3?n.call(arguments,2):t),"function"==typeof l&&null!=l.defaultProps)for(r in l.defaultProps)void 0===f[r]&&(f[r]=l.defaultProps[r]);return h(l,f,i,o,null)}function h(n,t,i,o,r){var f={type:n,props:t,key:i,ref:o,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==r?++u:r};return null!=l.vnode&&l.vnode(f),f}function y(n){return n.children}function d(n,l){this.props=n,this.context=l}function _(n,l){if(null==l)return n.__?_(n.__,n.__.__k.indexOf(n)+1):null;for(var u;l0?h(k.type,k.props,k.key,null,k.__v):k)){if(k.__=u,k.__b=u.__b+1,null===(d=A[p])||d&&k.key==d.key&&k.type===d.type)A[p]=void 0;else for(v=0;v2&&(f.children=arguments.length>3?n.call(arguments,2):t),h(l.type,f,i||l.key,o||l.ref,null)},exports.createContext=function(n,l){var u={__c:l="__cC"+f++,__:n,Consumer:function(n,l){return n.children(l)},Provider:function(n){var u,t;return this.getChildContext||(u=[],(t={})[l]=this,this.getChildContext=function(){return t},this.shouldComponentUpdate=function(n){this.props.value!==n.value&&u.some(x)},this.sub=function(n){u.push(n);var l=n.componentWillUnmount;n.componentWillUnmount=function(){u.splice(u.indexOf(n),1),l&&l.call(n)}}),n.children}};return u.Provider.__=u.Consumer.contextType=u},exports.toChildArray=function n(l,u){return u=u||[],null==l||"boolean"==typeof l||(Array.isArray(l)?l.some(function(l){n(l,u)}):u.push(l)),u},exports.options=l; },{}],11:[function(require,module,exports){ var has = Object.prototype.hasOwnProperty /** * Stringify an object for use in a query string. * * @param {Object} obj - The object. * @param {string} prefix - When nesting, the parent key. * keys in `obj` will be stringified as `prefix[key]`. * @returns {string} */ module.exports = function queryStringify (obj, prefix) { var pairs = [] for (var key in obj) { if (!has.call(obj, key)) { continue } var value = obj[key] var enkey = encodeURIComponent(key) var pair if (typeof value === 'object') { pair = queryStringify(value, prefix ? prefix + '[' + enkey + ']' : enkey) } else { pair = (prefix ? prefix + '[' + enkey + ']' : enkey) + '=' + encodeURIComponent(value) } pairs.push(pair) } return pairs.join('&') } },{}],12:[function(require,module,exports){ /* jshint node: true */ 'use strict'; /** # wildcard Very simple wildcard matching, which is designed to provide the same functionality that is found in the [eve](https://github.com/adobe-webplatform/eve) eventing library. ## Usage It works with strings: <<< examples/strings.js Arrays: <<< examples/arrays.js Objects (matching against keys): <<< examples/objects.js While the library works in Node, if you are are looking for file-based wildcard matching then you should have a look at: **/ function WildcardMatcher(text, separator) { this.text = text = text || ''; this.hasWild = ~text.indexOf('*'); this.separator = separator; this.parts = text.split(separator); } WildcardMatcher.prototype.match = function(input) { var matches = true; var parts = this.parts; var ii; var partsCount = parts.length; var testParts; if (typeof input == 'string' || input instanceof String) { if (!this.hasWild && this.text != input) { matches = false; } else { testParts = (input || '').split(this.separator); for (ii = 0; matches && ii < partsCount; ii++) { if (parts[ii] === '*') { continue; } else if (ii < testParts.length) { matches = parts[ii] === testParts[ii]; } else { matches = false; } } // If matches, then return the component parts matches = matches && testParts; } } else if (typeof input.splice == 'function') { matches = []; for (ii = input.length; ii--; ) { if (this.match(input[ii])) { matches[matches.length] = input[ii]; } } } else if (typeof input == 'object') { matches = {}; for (var key in input) { if (this.match(key)) { matches[key] = input[key]; } } } return matches; }; module.exports = function(text, test, separator) { var matcher = new WildcardMatcher(text, separator || /[\/\.]/); if (typeof test != 'undefined') { return matcher.match(test); } return matcher; }; },{}],13:[function(require,module,exports){ "use strict"; function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } var id = 0; function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; } const { AbortController, createAbortError } = require('@uppy/utils/lib/AbortController'); const delay = require('@uppy/utils/lib/delay'); const MB = 1024 * 1024; const defaultOptions = { limit: 1, retryDelays: [0, 1000, 3000, 5000], getChunkSize(file) { return Math.ceil(file.size / 10000); }, onStart() {}, onProgress() {}, onPartComplete() {}, onSuccess() {}, onError(err) { throw err; } }; function ensureInt(value) { if (typeof value === 'string') { return parseInt(value, 10); } if (typeof value === 'number') { return value; } throw new TypeError('Expected a number'); } var _aborted = /*#__PURE__*/_classPrivateFieldLooseKey("aborted"); var _initChunks = /*#__PURE__*/_classPrivateFieldLooseKey("initChunks"); var _createUpload = /*#__PURE__*/_classPrivateFieldLooseKey("createUpload"); var _resumeUpload = /*#__PURE__*/_classPrivateFieldLooseKey("resumeUpload"); var _uploadParts = /*#__PURE__*/_classPrivateFieldLooseKey("uploadParts"); var _retryable = /*#__PURE__*/_classPrivateFieldLooseKey("retryable"); var _prepareUploadParts = /*#__PURE__*/_classPrivateFieldLooseKey("prepareUploadParts"); var _uploadPartRetryable = /*#__PURE__*/_classPrivateFieldLooseKey("uploadPartRetryable"); var _uploadPart = /*#__PURE__*/_classPrivateFieldLooseKey("uploadPart"); var _onPartProgress = /*#__PURE__*/_classPrivateFieldLooseKey("onPartProgress"); var _onPartComplete = /*#__PURE__*/_classPrivateFieldLooseKey("onPartComplete"); var _uploadPartBytes = /*#__PURE__*/_classPrivateFieldLooseKey("uploadPartBytes"); var _completeUpload = /*#__PURE__*/_classPrivateFieldLooseKey("completeUpload"); var _abortUpload = /*#__PURE__*/_classPrivateFieldLooseKey("abortUpload"); var _onError = /*#__PURE__*/_classPrivateFieldLooseKey("onError"); class MultipartUploader { constructor(file, options) { Object.defineProperty(this, _onError, { value: _onError2 }); Object.defineProperty(this, _abortUpload, { value: _abortUpload2 }); Object.defineProperty(this, _completeUpload, { value: _completeUpload2 }); Object.defineProperty(this, _uploadPartBytes, { value: _uploadPartBytes2 }); Object.defineProperty(this, _onPartComplete, { value: _onPartComplete2 }); Object.defineProperty(this, _onPartProgress, { value: _onPartProgress2 }); Object.defineProperty(this, _uploadPart, { value: _uploadPart2 }); Object.defineProperty(this, _uploadPartRetryable, { value: _uploadPartRetryable2 }); Object.defineProperty(this, _prepareUploadParts, { value: _prepareUploadParts2 }); Object.defineProperty(this, _retryable, { value: _retryable2 }); Object.defineProperty(this, _uploadParts, { value: _uploadParts2 }); Object.defineProperty(this, _resumeUpload, { value: _resumeUpload2 }); Object.defineProperty(this, _createUpload, { value: _createUpload2 }); Object.defineProperty(this, _initChunks, { value: _initChunks2 }); Object.defineProperty(this, _aborted, { value: _aborted2 }); this.options = { ...defaultOptions, ...options }; // Use default `getChunkSize` if it was null or something if (!this.options.getChunkSize) { this.options.getChunkSize = defaultOptions.getChunkSize; } this.file = file; this.abortController = new AbortController(); this.key = this.options.key || null; this.uploadId = this.options.uploadId || null; this.parts = []; // Do `this.createdPromise.then(OP)` to execute an operation `OP` _only_ if the // upload was created already. That also ensures that the sequencing is right // (so the `OP` definitely happens if the upload is created). // // This mostly exists to make `#abortUpload` work well: only sending the abort request if // the upload was already created, and if the createMultipartUpload request is still in flight, // aborting it immediately after it finishes. this.createdPromise = Promise.reject(); // eslint-disable-line prefer-promise-reject-errors this.isPaused = false; this.partsInProgress = 0; this.chunks = null; this.chunkState = null; this.lockedCandidatesForBatch = []; _classPrivateFieldLooseBase(this, _initChunks)[_initChunks](); this.createdPromise.catch(() => {}); // silence uncaught rejection warning } /** * Was this upload aborted? * * If yes, we may need to throw an AbortError. * * @returns {boolean} */ start() { this.isPaused = false; if (this.uploadId) { _classPrivateFieldLooseBase(this, _resumeUpload)[_resumeUpload](); } else { _classPrivateFieldLooseBase(this, _createUpload)[_createUpload](); } } pause() { this.abortController.abort(); // Swap it out for a new controller, because this instance may be resumed later. this.abortController = new AbortController(); this.isPaused = true; } abort(opts = {}) { const really = opts.really || false; if (!really) return this.pause(); _classPrivateFieldLooseBase(this, _abortUpload)[_abortUpload](); } } function _aborted2() { return this.abortController.signal.aborted; } function _initChunks2() { const chunks = []; const desiredChunkSize = this.options.getChunkSize(this.file); // at least 5MB per request, at most 10k requests const minChunkSize = Math.max(5 * MB, Math.ceil(this.file.size / 10000)); const chunkSize = Math.max(desiredChunkSize, minChunkSize); // Upload zero-sized files in one zero-sized chunk if (this.file.size === 0) { chunks.push(this.file); } else { for (let i = 0; i < this.file.size; i += chunkSize) { const end = Math.min(this.file.size, i + chunkSize); chunks.push(this.file.slice(i, end)); } } this.chunks = chunks; this.chunkState = chunks.map(() => ({ uploaded: 0, busy: false, done: false })); } function _createUpload2() { this.createdPromise = Promise.resolve().then(() => this.options.createMultipartUpload()); return this.createdPromise.then(result => { if (_classPrivateFieldLooseBase(this, _aborted)[_aborted]()) throw createAbortError(); const valid = typeof result === 'object' && result && typeof result.uploadId === 'string' && typeof result.key === 'string'; if (!valid) { throw new TypeError('AwsS3/Multipart: Got incorrect result from `createMultipartUpload()`, expected an object `{ uploadId, key }`.'); } this.key = result.key; this.uploadId = result.uploadId; this.options.onStart(result); _classPrivateFieldLooseBase(this, _uploadParts)[_uploadParts](); }).catch(err => { _classPrivateFieldLooseBase(this, _onError)[_onError](err); }); } async function _resumeUpload2() { try { const parts = await this.options.listParts({ uploadId: this.uploadId, key: this.key }); if (_classPrivateFieldLooseBase(this, _aborted)[_aborted]()) throw createAbortError(); parts.forEach(part => { const i = part.PartNumber - 1; this.chunkState[i] = { uploaded: ensureInt(part.Size), etag: part.ETag, done: true }; // Only add if we did not yet know about this part. if (!this.parts.some(p => p.PartNumber === part.PartNumber)) { this.parts.push({ PartNumber: part.PartNumber, ETag: part.ETag }); } }); _classPrivateFieldLooseBase(this, _uploadParts)[_uploadParts](); } catch (err) { _classPrivateFieldLooseBase(this, _onError)[_onError](err); } } function _uploadParts2() { if (this.isPaused) return; // All parts are uploaded. if (this.chunkState.every(state => state.done)) { _classPrivateFieldLooseBase(this, _completeUpload)[_completeUpload](); return; } // For a 100MB file, with the default min chunk size of 5MB and a limit of 10: // // Total 20 parts // --------- // Need 1 is 10 // Need 2 is 5 // Need 3 is 5 const need = this.options.limit - this.partsInProgress; const completeChunks = this.chunkState.filter(state => state.done).length; const remainingChunks = this.chunks.length - completeChunks; let minNeeded = Math.ceil(this.options.limit / 2); if (minNeeded > remainingChunks) { minNeeded = remainingChunks; } if (need < minNeeded) return; const candidates = []; for (let i = 0; i < this.chunkState.length; i++) { if (this.lockedCandidatesForBatch.includes(i)) continue; const state = this.chunkState[i]; if (state.done || state.busy) continue; candidates.push(i); if (candidates.length >= need) { break; } } if (candidates.length === 0) return; _classPrivateFieldLooseBase(this, _prepareUploadParts)[_prepareUploadParts](candidates).then(result => { candidates.forEach(index => { const partNumber = index + 1; const prePreparedPart = { url: result.presignedUrls[partNumber], headers: result.headers }; _classPrivateFieldLooseBase(this, _uploadPartRetryable)[_uploadPartRetryable](index, prePreparedPart).then(() => { _classPrivateFieldLooseBase(this, _uploadParts)[_uploadParts](); }, err => { _classPrivateFieldLooseBase(this, _onError)[_onError](err); }); }); }); } function _retryable2({ before, attempt, after }) { const { retryDelays } = this.options; const { signal } = this.abortController; if (before) before(); function shouldRetry(err) { if (err.source && typeof err.source.status === 'number') { const { status } = err.source; // 0 probably indicates network failure return status === 0 || status === 409 || status === 423 || status >= 500 && status < 600; } return false; } const doAttempt = retryAttempt => attempt().catch(err => { if (_classPrivateFieldLooseBase(this, _aborted)[_aborted]()) throw createAbortError(); if (shouldRetry(err) && retryAttempt < retryDelays.length) { return delay(retryDelays[retryAttempt], { signal }).then(() => doAttempt(retryAttempt + 1)); } throw err; }); return doAttempt(0).then(result => { if (after) after(); return result; }, err => { if (after) after(); throw err; }); } async function _prepareUploadParts2(candidates) { this.lockedCandidatesForBatch.push(...candidates); const result = await this.options.prepareUploadParts({ key: this.key, uploadId: this.uploadId, partNumbers: candidates.map(index => index + 1) }); const valid = typeof (result == null ? void 0 : result.presignedUrls) === 'object'; if (!valid) { throw new TypeError('AwsS3/Multipart: Got incorrect result from `prepareUploadParts()`, expected an object `{ presignedUrls }`.'); } return result; } function _uploadPartRetryable2(index, prePreparedPart) { return _classPrivateFieldLooseBase(this, _retryable)[_retryable]({ before: () => { this.partsInProgress += 1; }, attempt: () => _classPrivateFieldLooseBase(this, _uploadPart)[_uploadPart](index, prePreparedPart), after: () => { this.partsInProgress -= 1; } }); } function _uploadPart2(index, prePreparedPart) { const body = this.chunks[index]; this.chunkState[index].busy = true; const valid = typeof (prePreparedPart == null ? void 0 : prePreparedPart.url) === 'string'; if (!valid) { throw new TypeError('AwsS3/Multipart: Got incorrect result for `prePreparedPart`, expected an object `{ url }`.'); } const { url, headers } = prePreparedPart; if (_classPrivateFieldLooseBase(this, _aborted)[_aborted]()) { this.chunkState[index].busy = false; throw createAbortError(); } return _classPrivateFieldLooseBase(this, _uploadPartBytes)[_uploadPartBytes](index, url, headers); } function _onPartProgress2(index, sent, total) { this.chunkState[index].uploaded = ensureInt(sent); const totalUploaded = this.chunkState.reduce((n, c) => n + c.uploaded, 0); this.options.onProgress(totalUploaded, this.file.size); } function _onPartComplete2(index, etag) { this.chunkState[index].etag = etag; this.chunkState[index].done = true; const part = { PartNumber: index + 1, ETag: etag }; this.parts.push(part); this.options.onPartComplete(part); } function _uploadPartBytes2(index, url, headers) { const body = this.chunks[index]; const { signal } = this.abortController; let defer; const promise = new Promise((resolve, reject) => { defer = { resolve, reject }; }); const xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); if (headers) { Object.keys(headers).map(key => { xhr.setRequestHeader(key, headers[key]); }); } xhr.responseType = 'text'; function cleanup() { signal.removeEventListener('abort', onabort); } function onabort() { xhr.abort(); } signal.addEventListener('abort', onabort); xhr.upload.addEventListener('progress', ev => { if (!ev.lengthComputable) return; _classPrivateFieldLooseBase(this, _onPartProgress)[_onPartProgress](index, ev.loaded, ev.total); }); xhr.addEventListener('abort', ev => { cleanup(); this.chunkState[index].busy = false; defer.reject(createAbortError()); }); xhr.addEventListener('load', ev => { cleanup(); this.chunkState[index].busy = false; if (ev.target.status < 200 || ev.target.status >= 300) { const error = new Error('Non 2xx'); error.source = ev.target; defer.reject(error); return; } _classPrivateFieldLooseBase(this, _onPartProgress)[_onPartProgress](index, body.size, body.size); // NOTE This must be allowed by CORS. const etag = ev.target.getResponseHeader('ETag'); if (etag === null) { defer.reject(new Error('AwsS3/Multipart: Could not read the ETag header. This likely means CORS is not configured correctly on the S3 Bucket. See https://uppy.io/docs/aws-s3-multipart#S3-Bucket-Configuration for instructions.')); return; } _classPrivateFieldLooseBase(this, _onPartComplete)[_onPartComplete](index, etag); defer.resolve(); }); xhr.addEventListener('error', ev => { cleanup(); this.chunkState[index].busy = false; const error = new Error('Unknown error'); error.source = ev.target; defer.reject(error); }); xhr.send(body); return promise; } async function _completeUpload2() { // Parts may not have completed uploading in sorted order, if limit > 1. this.parts.sort((a, b) => a.PartNumber - b.PartNumber); try { const result = await this.options.completeMultipartUpload({ key: this.key, uploadId: this.uploadId, parts: this.parts }); this.options.onSuccess(result); } catch (err) { _classPrivateFieldLooseBase(this, _onError)[_onError](err); } } function _abortUpload2() { this.abortController.abort(); this.createdPromise.then(() => { this.options.abortMultipartUpload({ key: this.key, uploadId: this.uploadId }); }, () => {// if the creation failed we do not need to abort }); } function _onError2(err) { if (err && err.name === 'AbortError') { return; } this.options.onError(err); } module.exports = MultipartUploader; },{"@uppy/utils/lib/AbortController":23,"@uppy/utils/lib/delay":27}],14:[function(require,module,exports){ "use strict"; var _class, _temp; const { BasePlugin } = require('@uppy/core'); const { Socket, Provider, RequestClient } = require('@uppy/companion-client'); const EventTracker = require('@uppy/utils/lib/EventTracker'); const emitSocketProgress = require('@uppy/utils/lib/emitSocketProgress'); const getSocketHost = require('@uppy/utils/lib/getSocketHost'); const { RateLimitedQueue } = require('@uppy/utils/lib/RateLimitedQueue'); const Uploader = require('./MultipartUploader'); function assertServerError(res) { if (res && res.error) { const error = new Error(res.message); Object.assign(error, res.error); throw error; } return res; } module.exports = (_temp = _class = class AwsS3Multipart extends BasePlugin { constructor(uppy, opts) { super(uppy, opts); this.type = 'uploader'; this.id = this.opts.id || 'AwsS3Multipart'; this.title = 'AWS S3 Multipart'; this.client = new RequestClient(uppy, opts); const defaultOptions = { timeout: 30 * 1000, limit: 0, retryDelays: [0, 1000, 3000, 5000], createMultipartUpload: this.createMultipartUpload.bind(this), listParts: this.listParts.bind(this), prepareUploadParts: this.prepareUploadParts.bind(this), abortMultipartUpload: this.abortMultipartUpload.bind(this), completeMultipartUpload: this.completeMultipartUpload.bind(this) }; this.opts = { ...defaultOptions, ...opts }; this.upload = this.upload.bind(this); this.requests = new RateLimitedQueue(this.opts.limit); this.uploaders = Object.create(null); this.uploaderEvents = Object.create(null); this.uploaderSockets = Object.create(null); } /** * Clean up all references for a file's upload: the MultipartUploader instance, * any events related to the file, and the Companion WebSocket connection. * * Set `opts.abort` to tell S3 that the multipart upload is cancelled and must be removed. * This should be done when the user cancels the upload, not when the upload is completed or errored. */ resetUploaderReferences(fileID, opts = {}) { if (this.uploaders[fileID]) { this.uploaders[fileID].abort({ really: opts.abort || false }); this.uploaders[fileID] = null; } if (this.uploaderEvents[fileID]) { this.uploaderEvents[fileID].remove(); this.uploaderEvents[fileID] = null; } if (this.uploaderSockets[fileID]) { this.uploaderSockets[fileID].close(); this.uploaderSockets[fileID] = null; } } assertHost(method) { if (!this.opts.companionUrl) { throw new Error(`Expected a \`companionUrl\` option containing a Companion address, or if you are not using Companion, a custom \`${method}\` implementation.`); } } createMultipartUpload(file) { this.assertHost('createMultipartUpload'); const metadata = {}; Object.keys(file.meta).map(key => { if (file.meta[key] != null) { metadata[key] = file.meta[key].toString(); } }); return this.client.post('s3/multipart', { filename: file.name, type: file.type, metadata }).then(assertServerError); } listParts(file, { key, uploadId }) { this.assertHost('listParts'); const filename = encodeURIComponent(key); return this.client.get(`s3/multipart/${uploadId}?key=${filename}`).then(assertServerError); } prepareUploadParts(file, { key, uploadId, partNumbers }) { this.assertHost('prepareUploadParts'); const filename = encodeURIComponent(key); return this.client.get(`s3/multipart/${uploadId}/batch?key=${filename}?partNumbers=${partNumbers.join(',')}`).then(assertServerError); } completeMultipartUpload(file, { key, uploadId, parts }) { this.assertHost('completeMultipartUpload'); const filename = encodeURIComponent(key); const uploadIdEnc = encodeURIComponent(uploadId); return this.client.post(`s3/multipart/${uploadIdEnc}/complete?key=${filename}`, { parts }).then(assertServerError); } abortMultipartUpload(file, { key, uploadId }) { this.assertHost('abortMultipartUpload'); const filename = encodeURIComponent(key); const uploadIdEnc = encodeURIComponent(uploadId); return this.client.delete(`s3/multipart/${uploadIdEnc}?key=${filename}`).then(assertServerError); } uploadFile(file) { return new Promise((resolve, reject) => { const onStart = data => { const cFile = this.uppy.getFile(file.id); this.uppy.setFileState(file.id, { s3Multipart: { ...cFile.s3Multipart, key: data.key, uploadId: data.uploadId } }); }; const onProgress = (bytesUploaded, bytesTotal) => { this.uppy.emit('upload-progress', file, { uploader: this, bytesUploaded, bytesTotal }); }; const onError = err => { this.uppy.log(err); this.uppy.emit('upload-error', file, err); queuedRequest.done(); this.resetUploaderReferences(file.id); reject(err); }; const onSuccess = result => { const uploadResp = { body: { ...result }, uploadURL: result.location }; queuedRequest.done(); this.resetUploaderReferences(file.id); const cFile = this.uppy.getFile(file.id); this.uppy.emit('upload-success', cFile || file, uploadResp); if (result.location) { this.uppy.log(`Download ${upload.file.name} from ${result.location}`); } resolve(upload); }; const onPartComplete = part => { const cFile = this.uppy.getFile(file.id); if (!cFile) { return; } this.uppy.emit('s3-multipart:part-uploaded', cFile, part); }; const upload = new Uploader(file.data, { // .bind to pass the file object to each handler. createMultipartUpload: this.opts.createMultipartUpload.bind(this, file), listParts: this.opts.listParts.bind(this, file), prepareUploadParts: this.opts.prepareUploadParts.bind(this, file), completeMultipartUpload: this.opts.completeMultipartUpload.bind(this, file), abortMultipartUpload: this.opts.abortMultipartUpload.bind(this, file), getChunkSize: this.opts.getChunkSize ? this.opts.getChunkSize.bind(this) : null, onStart, onProgress, onError, onSuccess, onPartComplete, limit: this.opts.limit || 5, retryDelays: this.opts.retryDelays || [], ...file.s3Multipart }); this.uploaders[file.id] = upload; this.uploaderEvents[file.id] = new EventTracker(this.uppy); let queuedRequest = this.requests.run(() => { if (!file.isPaused) { upload.start(); } // Don't do anything here, the caller will take care of cancelling the upload itself // using resetUploaderReferences(). This is because resetUploaderReferences() has to be // called when this request is still in the queue, and has not been started yet, too. At // that point this cancellation function is not going to be called. return () => {}; }); this.onFileRemove(file.id, removed => { queuedRequest.abort(); this.resetUploaderReferences(file.id, { abort: true }); resolve(`upload ${removed.id} was removed`); }); this.onCancelAll(file.id, () => { queuedRequest.abort(); this.resetUploaderReferences(file.id, { abort: true }); resolve(`upload ${file.id} was canceled`); }); this.onFilePause(file.id, isPaused => { if (isPaused) { // Remove this file from the queue so another file can start in its place. queuedRequest.abort(); upload.pause(); } else { // Resuming an upload should be queued, else you could pause and then resume a queued upload to make it skip the queue. queuedRequest.abort(); queuedRequest = this.requests.run(() => { upload.start(); return () => {}; }); } }); this.onPauseAll(file.id, () => { queuedRequest.abort(); upload.pause(); }); this.onResumeAll(file.id, () => { queuedRequest.abort(); if (file.error) { upload.abort(); } queuedRequest = this.requests.run(() => { upload.start(); return () => {}; }); }); // Don't double-emit upload-started for Golden Retriever-restored files that were already started if (!file.progress.uploadStarted || !file.isRestored) { this.uppy.emit('upload-started', file); } }); } uploadRemote(file) { this.resetUploaderReferences(file.id); // Don't double-emit upload-started for Golden Retriever-restored files that were already started if (!file.progress.uploadStarted || !file.isRestored) { this.uppy.emit('upload-started', file); } if (file.serverToken) { return this.connectToServerSocket(file); } return new Promise((resolve, reject) => { const Client = file.remote.providerOptions.provider ? Provider : RequestClient; const client = new Client(this.uppy, file.remote.providerOptions); client.post(file.remote.url, { ...file.remote.body, protocol: 's3-multipart', size: file.data.size, metadata: file.meta }).then(res => { this.uppy.setFileState(file.id, { serverToken: res.token }); file = this.uppy.getFile(file.id); return file; }).then(file => { return this.connectToServerSocket(file); }).then(() => { resolve(); }).catch(err => { this.uppy.emit('upload-error', file, err); reject(err); }); }); } connectToServerSocket(file) { return new Promise((resolve, reject) => { const token = file.serverToken; const host = getSocketHost(file.remote.companionUrl); const socket = new Socket({ target: `${host}/api/${token}`, autoOpen: false }); this.uploaderSockets[file.id] = socket; this.uploaderEvents[file.id] = new EventTracker(this.uppy); this.onFileRemove(file.id, removed => { queuedRequest.abort(); socket.send('pause', {}); this.resetUploaderReferences(file.id, { abort: true }); resolve(`upload ${file.id} was removed`); }); this.onFilePause(file.id, isPaused => { if (isPaused) { // Remove this file from the queue so another file can start in its place. queuedRequest.abort(); socket.send('pause', {}); } else { // Resuming an upload should be queued, else you could pause and then resume a queued upload to make it skip the queue. queuedRequest.abort(); queuedRequest = this.requests.run(() => { socket.send('resume', {}); return () => {}; }); } }); this.onPauseAll(file.id, () => { queuedRequest.abort(); socket.send('pause', {}); }); this.onCancelAll(file.id, () => { queuedRequest.abort(); socket.send('pause', {}); this.resetUploaderReferences(file.id); resolve(`upload ${file.id} was canceled`); }); this.onResumeAll(file.id, () => { queuedRequest.abort(); if (file.error) { socket.send('pause', {}); } queuedRequest = this.requests.run(() => { socket.send('resume', {}); }); }); this.onRetry(file.id, () => { // Only do the retry if the upload is actually in progress; // else we could try to send these messages when the upload is still queued. // We may need a better check for this since the socket may also be closed // for other reasons, like network failures. if (socket.isOpen) { socket.send('pause', {}); socket.send('resume', {}); } }); this.onRetryAll(file.id, () => { if (socket.isOpen) { socket.send('pause', {}); socket.send('resume', {}); } }); socket.on('progress', progressData => emitSocketProgress(this, progressData, file)); socket.on('error', errData => { this.uppy.emit('upload-error', file, new Error(errData.error)); this.resetUploaderReferences(file.id); queuedRequest.done(); reject(new Error(errData.error)); }); socket.on('success', data => { const uploadResp = { uploadURL: data.url }; this.uppy.emit('upload-success', file, uploadResp); this.resetUploaderReferences(file.id); queuedRequest.done(); resolve(); }); let queuedRequest = this.requests.run(() => { socket.open(); if (file.isPaused) { socket.send('pause', {}); } return () => {}; }); }); } upload(fileIDs) { if (fileIDs.length === 0) return Promise.resolve(); const promises = fileIDs.map(id => { const file = this.uppy.getFile(id); if (file.isRemote) { return this.uploadRemote(file); } return this.uploadFile(file); }); return Promise.all(promises); } onFileRemove(fileID, cb) { this.uploaderEvents[fileID].on('file-removed', file => { if (fileID === file.id) cb(file.id); }); } onFilePause(fileID, cb) { this.uploaderEvents[fileID].on('upload-pause', (targetFileID, isPaused) => { if (fileID === targetFileID) { // const isPaused = this.uppy.pauseResume(fileID) cb(isPaused); } }); } onRetry(fileID, cb) { this.uploaderEvents[fileID].on('upload-retry', targetFileID => { if (fileID === targetFileID) { cb(); } }); } onRetryAll(fileID, cb) { this.uploaderEvents[fileID].on('retry-all', filesToRetry => { if (!this.uppy.getFile(fileID)) return; cb(); }); } onPauseAll(fileID, cb) { this.uploaderEvents[fileID].on('pause-all', () => { if (!this.uppy.getFile(fileID)) return; cb(); }); } onCancelAll(fileID, cb) { this.uploaderEvents[fileID].on('cancel-all', () => { if (!this.uppy.getFile(fileID)) return; cb(); }); } onResumeAll(fileID, cb) { this.uploaderEvents[fileID].on('resume-all', () => { if (!this.uppy.getFile(fileID)) return; cb(); }); } install() { const { capabilities } = this.uppy.getState(); this.uppy.setState({ capabilities: { ...capabilities, resumableUploads: true } }); this.uppy.addUploader(this.upload); } uninstall() { const { capabilities } = this.uppy.getState(); this.uppy.setState({ capabilities: { ...capabilities, resumableUploads: false } }); this.uppy.removeUploader(this.upload); } }, _class.VERSION = require('../package.json').version, _temp); },{"../package.json":32,"./MultipartUploader":13,"@uppy/companion-client":20,"@uppy/core":60,"@uppy/utils/lib/EventTracker":24,"@uppy/utils/lib/RateLimitedQueue":26,"@uppy/utils/lib/emitSocketProgress":28,"@uppy/utils/lib/getSocketHost":31}],15:[function(require,module,exports){ 'use strict'; class AuthError extends Error { constructor() { super('Authorization required'); this.name = 'AuthError'; this.isAuthError = true; } } module.exports = AuthError; },{}],16:[function(require,module,exports){ 'use strict'; const qsStringify = require('qs-stringify'); const RequestClient = require('./RequestClient'); const tokenStorage = require('./tokenStorage'); const _getName = id => { return id.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' '); }; module.exports = class Provider extends RequestClient { constructor(uppy, opts) { super(uppy, opts); this.provider = opts.provider; this.id = this.provider; this.name = this.opts.name || _getName(this.id); this.pluginId = this.opts.pluginId; this.tokenKey = `companion-${this.pluginId}-auth-token`; this.companionKeysParams = this.opts.companionKeysParams; this.preAuthToken = null; } headers() { return Promise.all([super.headers(), this.getAuthToken()]).then(([headers, token]) => { const authHeaders = {}; if (token) { authHeaders['uppy-auth-token'] = token; } if (this.companionKeysParams) { authHeaders['uppy-credentials-params'] = btoa(JSON.stringify({ params: this.companionKeysParams })); } return { ...headers, ...authHeaders }; }); } onReceiveResponse(response) { response = super.onReceiveResponse(response); const plugin = this.uppy.getPlugin(this.pluginId); const oldAuthenticated = plugin.getPluginState().authenticated; const authenticated = oldAuthenticated ? response.status !== 401 : response.status < 400; plugin.setPluginState({ authenticated }); return response; } setAuthToken(token) { return this.uppy.getPlugin(this.pluginId).storage.setItem(this.tokenKey, token); } getAuthToken() { return this.uppy.getPlugin(this.pluginId).storage.getItem(this.tokenKey); } authUrl(queries = {}) { if (this.preAuthToken) { queries.uppyPreAuthToken = this.preAuthToken; } let strigifiedQueries = qsStringify(queries); strigifiedQueries = strigifiedQueries ? `?${strigifiedQueries}` : strigifiedQueries; return `${this.hostname}/${this.id}/connect${strigifiedQueries}`; } fileUrl(id) { return `${this.hostname}/${this.id}/get/${id}`; } fetchPreAuthToken() { if (!this.companionKeysParams) { return Promise.resolve(); } return this.post(`${this.id}/preauth/`, { params: this.companionKeysParams }).then(res => { this.preAuthToken = res.token; }).catch(err => { this.uppy.log(`[CompanionClient] unable to fetch preAuthToken ${err}`, 'warning'); }); } list(directory) { return this.get(`${this.id}/list/${directory || ''}`); } logout() { return this.get(`${this.id}/logout`).then(response => Promise.all([response, this.uppy.getPlugin(this.pluginId).storage.removeItem(this.tokenKey)])).then(([response]) => response); } static initPlugin(plugin, opts, defaultOpts) { plugin.type = 'acquirer'; plugin.files = []; if (defaultOpts) { plugin.opts = { ...defaultOpts, ...opts }; } if (opts.serverUrl || opts.serverPattern) { throw new Error('`serverUrl` and `serverPattern` have been renamed to `companionUrl` and `companionAllowedHosts` respectively in the 0.30.5 release. Please consult the docs (for example, https://uppy.io/docs/instagram/ for the Instagram plugin) and use the updated options.`'); } if (opts.companionAllowedHosts) { const pattern = opts.companionAllowedHosts; // validate companionAllowedHosts param if (typeof pattern !== 'string' && !Array.isArray(pattern) && !(pattern instanceof RegExp)) { throw new TypeError(`${plugin.id}: the option "companionAllowedHosts" must be one of string, Array, RegExp`); } plugin.opts.companionAllowedHosts = pattern; } else { // does not start with https:// if (/^(?!https?:\/\/).*$/i.test(opts.companionUrl)) { plugin.opts.companionAllowedHosts = `https://${opts.companionUrl.replace(/^\/\//, '')}`; } else { plugin.opts.companionAllowedHosts = new URL(opts.companionUrl).origin; } } plugin.storage = plugin.opts.storage || tokenStorage; } }; },{"./RequestClient":17,"./tokenStorage":21,"qs-stringify":11}],17:[function(require,module,exports){ 'use strict'; var _class, _getPostResponseFunc, _getUrl, _errorHandler, _temp; function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } var id = 0; function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; } const fetchWithNetworkError = require('@uppy/utils/lib/fetchWithNetworkError'); const AuthError = require('./AuthError'); // Remove the trailing slash so we can always safely append /xyz. function stripSlash(url) { return url.replace(/\/$/, ''); } async function handleJSONResponse(res) { if (res.status === 401) { throw new AuthError(); } const jsonPromise = res.json(); if (res.status < 200 || res.status > 300) { let errMsg = `Failed request with status: ${res.status}. ${res.statusText}`; try { const errData = await jsonPromise; errMsg = errData.message ? `${errMsg} message: ${errData.message}` : errMsg; errMsg = errData.requestId ? `${errMsg} request-Id: ${errData.requestId}` : errMsg; } finally { // eslint-disable-next-line no-unsafe-finally throw new Error(errMsg); } } return jsonPromise; } module.exports = (_temp = (_getPostResponseFunc = /*#__PURE__*/_classPrivateFieldLooseKey("getPostResponseFunc"), _getUrl = /*#__PURE__*/_classPrivateFieldLooseKey("getUrl"), _errorHandler = /*#__PURE__*/_classPrivateFieldLooseKey("errorHandler"), _class = class RequestClient { // eslint-disable-next-line global-require constructor(uppy, opts) { Object.defineProperty(this, _errorHandler, { value: _errorHandler2 }); Object.defineProperty(this, _getUrl, { value: _getUrl2 }); Object.defineProperty(this, _getPostResponseFunc, { writable: true, value: skip => response => skip ? response : this.onReceiveResponse(response) }); this.uppy = uppy; this.opts = opts; this.onReceiveResponse = this.onReceiveResponse.bind(this); this.allowedHeaders = ['accept', 'content-type', 'uppy-auth-token']; this.preflightDone = false; } get hostname() { const { companion } = this.uppy.getState(); const host = this.opts.companionUrl; return stripSlash(companion && companion[host] ? companion[host] : host); } headers() { const userHeaders = this.opts.companionHeaders || {}; return Promise.resolve({ ...RequestClient.defaultHeaders, ...userHeaders }); } onReceiveResponse(response) { const state = this.uppy.getState(); const companion = state.companion || {}; const host = this.opts.companionUrl; const { headers } = response; // Store the self-identified domain name for the Companion instance we just hit. if (headers.has('i-am') && headers.get('i-am') !== companion[host]) { this.uppy.setState({ companion: { ...companion, [host]: headers.get('i-am') } }); } return response; } preflight(path) { if (this.preflightDone) { return Promise.resolve(this.allowedHeaders.slice()); } return fetch(_classPrivateFieldLooseBase(this, _getUrl)[_getUrl](path), { method: 'OPTIONS' }).then(response => { if (response.headers.has('access-control-allow-headers')) { this.allowedHeaders = response.headers.get('access-control-allow-headers').split(',').map(headerName => headerName.trim().toLowerCase()); } this.preflightDone = true; return this.allowedHeaders.slice(); }).catch(err => { this.uppy.log(`[CompanionClient] unable to make preflight request ${err}`, 'warning'); this.preflightDone = true; return this.allowedHeaders.slice(); }); } preflightAndHeaders(path) { return Promise.all([this.preflight(path), this.headers()]).then(([allowedHeaders, headers]) => { // filter to keep only allowed Headers Object.keys(headers).forEach(header => { if (!allowedHeaders.includes(header.toLowerCase())) { this.uppy.log(`[CompanionClient] excluding disallowed header ${header}`); delete headers[header]; // eslint-disable-line no-param-reassign } }); return headers; }); } get(path, skipPostResponse) { const method = 'get'; return this.preflightAndHeaders(path).then(headers => fetchWithNetworkError(_classPrivateFieldLooseBase(this, _getUrl)[_getUrl](path), { method, headers, credentials: this.opts.companionCookiesRule || 'same-origin' })).then(_classPrivateFieldLooseBase(this, _getPostResponseFunc)[_getPostResponseFunc](skipPostResponse)).then(handleJSONResponse).catch(_classPrivateFieldLooseBase(this, _errorHandler)[_errorHandler](method, path)); } post(path, data, skipPostResponse) { const method = 'post'; return this.preflightAndHeaders(path).then(headers => fetchWithNetworkError(_classPrivateFieldLooseBase(this, _getUrl)[_getUrl](path), { method, headers, credentials: this.opts.companionCookiesRule || 'same-origin', body: JSON.stringify(data) })).then(_classPrivateFieldLooseBase(this, _getPostResponseFunc)[_getPostResponseFunc](skipPostResponse)).then(handleJSONResponse).catch(_classPrivateFieldLooseBase(this, _errorHandler)[_errorHandler](method, path)); } delete(path, data, skipPostResponse) { const method = 'delete'; return this.preflightAndHeaders(path).then(headers => fetchWithNetworkError(`${this.hostname}/${path}`, { method, headers, credentials: this.opts.companionCookiesRule || 'same-origin', body: data ? JSON.stringify(data) : null })).then(_classPrivateFieldLooseBase(this, _getPostResponseFunc)[_getPostResponseFunc](skipPostResponse)).then(handleJSONResponse).catch(_classPrivateFieldLooseBase(this, _errorHandler)[_errorHandler](method, path)); } }), _class.VERSION = require('../package.json').version, _class.defaultHeaders = { Accept: 'application/json', 'Content-Type': 'application/json', 'Uppy-Versions': `@uppy/companion-client=${_class.VERSION}` }, _temp); function _getUrl2(url) { if (/^(https?:|)\/\//.test(url)) { return url; } return `${this.hostname}/${url}`; } function _errorHandler2(method, path) { return err => { var _err; if (!((_err = err) != null && _err.isAuthError)) { const error = new Error(`Could not ${method} ${_classPrivateFieldLooseBase(this, _getUrl)[_getUrl](path)}`); error.cause = err; err = error; // eslint-disable-line no-param-reassign } return Promise.reject(err); }; } },{"../package.json":22,"./AuthError":15,"@uppy/utils/lib/fetchWithNetworkError":29}],18:[function(require,module,exports){ 'use strict'; const RequestClient = require('./RequestClient'); const _getName = id => { return id.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' '); }; module.exports = class SearchProvider extends RequestClient { constructor(uppy, opts) { super(uppy, opts); this.provider = opts.provider; this.id = this.provider; this.name = this.opts.name || _getName(this.id); this.pluginId = this.opts.pluginId; } fileUrl(id) { return `${this.hostname}/search/${this.id}/get/${id}`; } search(text, queries) { queries = queries ? `&${queries}` : ''; return this.get(`search/${this.id}/list?q=${encodeURIComponent(text)}${queries}`); } }; },{"./RequestClient":17}],19:[function(require,module,exports){ "use strict"; var _queued, _emitter, _isOpen, _socket, _handleMessage; let _Symbol$for, _Symbol$for2; function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } var id = 0; function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; } const ee = require('namespace-emitter'); module.exports = (_queued = /*#__PURE__*/_classPrivateFieldLooseKey("queued"), _emitter = /*#__PURE__*/_classPrivateFieldLooseKey("emitter"), _isOpen = /*#__PURE__*/_classPrivateFieldLooseKey("isOpen"), _socket = /*#__PURE__*/_classPrivateFieldLooseKey("socket"), _handleMessage = /*#__PURE__*/_classPrivateFieldLooseKey("handleMessage"), _Symbol$for = Symbol.for('uppy test: getSocket'), _Symbol$for2 = Symbol.for('uppy test: getQueued'), class UppySocket { constructor(opts) { Object.defineProperty(this, _queued, { writable: true, value: [] }); Object.defineProperty(this, _emitter, { writable: true, value: ee() }); Object.defineProperty(this, _isOpen, { writable: true, value: false }); Object.defineProperty(this, _socket, { writable: true, value: void 0 }); Object.defineProperty(this, _handleMessage, { writable: true, value: e => { try { const message = JSON.parse(e.data); this.emit(message.action, message.payload); } catch (err) { // TODO: use a more robust error handler. console.log(err); // eslint-disable-line no-console } } }); this.opts = opts; if (!opts || opts.autoOpen !== false) { this.open(); } } get isOpen() { return _classPrivateFieldLooseBase(this, _isOpen)[_isOpen]; } [_Symbol$for]() { return _classPrivateFieldLooseBase(this, _socket)[_socket]; } [_Symbol$for2]() { return _classPrivateFieldLooseBase(this, _queued)[_queued]; } open() { _classPrivateFieldLooseBase(this, _socket)[_socket] = new WebSocket(this.opts.target); _classPrivateFieldLooseBase(this, _socket)[_socket].onopen = () => { _classPrivateFieldLooseBase(this, _isOpen)[_isOpen] = true; while (_classPrivateFieldLooseBase(this, _queued)[_queued].length > 0 && _classPrivateFieldLooseBase(this, _isOpen)[_isOpen]) { const first = _classPrivateFieldLooseBase(this, _queued)[_queued].shift(); this.send(first.action, first.payload); } }; _classPrivateFieldLooseBase(this, _socket)[_socket].onclose = () => { _classPrivateFieldLooseBase(this, _isOpen)[_isOpen] = false; }; _classPrivateFieldLooseBase(this, _socket)[_socket].onmessage = _classPrivateFieldLooseBase(this, _handleMessage)[_handleMessage]; } close() { var _classPrivateFieldLoo; (_classPrivateFieldLoo = _classPrivateFieldLooseBase(this, _socket)[_socket]) == null ? void 0 : _classPrivateFieldLoo.close(); } send(action, payload) { // attach uuid if (!_classPrivateFieldLooseBase(this, _isOpen)[_isOpen]) { _classPrivateFieldLooseBase(this, _queued)[_queued].push({ action, payload }); return; } _classPrivateFieldLooseBase(this, _socket)[_socket].send(JSON.stringify({ action, payload })); } on(action, handler) { _classPrivateFieldLooseBase(this, _emitter)[_emitter].on(action, handler); } emit(action, payload) { _classPrivateFieldLooseBase(this, _emitter)[_emitter].emit(action, payload); } once(action, handler) { _classPrivateFieldLooseBase(this, _emitter)[_emitter].once(action, handler); } }); },{"namespace-emitter":9}],20:[function(require,module,exports){ 'use strict'; /** * Manages communications with Companion */ const RequestClient = require('./RequestClient'); const Provider = require('./Provider'); const SearchProvider = require('./SearchProvider'); const Socket = require('./Socket'); module.exports = { RequestClient, Provider, SearchProvider, Socket }; },{"./Provider":16,"./RequestClient":17,"./SearchProvider":18,"./Socket":19}],21:[function(require,module,exports){ 'use strict'; /** * This module serves as an Async wrapper for LocalStorage */ module.exports.setItem = (key, value) => { return new Promise(resolve => { localStorage.setItem(key, value); resolve(); }); }; module.exports.getItem = key => { return Promise.resolve(localStorage.getItem(key)); }; module.exports.removeItem = key => { return new Promise(resolve => { localStorage.removeItem(key); resolve(); }); }; },{}],22:[function(require,module,exports){ module.exports={ "name": "@uppy/companion-client", "description": "Client library for communication with Companion. Intended for use in Uppy plugins.", "version": "2.0.0-alpha.0", "license": "MIT", "main": "lib/index.js", "types": "types/index.d.ts", "keywords": [ "file uploader", "uppy", "uppy-plugin", "companion", "provider" ], "homepage": "https://uppy.io", "bugs": { "url": "https://github.com/transloadit/uppy/issues" }, "repository": { "type": "git", "url": "git+https://github.com/transloadit/uppy.git" }, "dependencies": { "@uppy/utils": "file:../utils", "namespace-emitter": "^2.0.1", "qs-stringify": "^1.1.0" } } },{}],23:[function(require,module,exports){ "use strict"; /** * Little AbortController proxy module so we can swap out the implementation easily later. */ exports.AbortController = AbortController; exports.AbortSignal = AbortSignal; exports.createAbortError = (message = 'Aborted') => new DOMException(message, 'AbortError'); },{}],24:[function(require,module,exports){ "use strict"; /** * Create a wrapper around an event emitter with a `remove` method to remove * all events that were added using the wrapped emitter. */ module.exports = class EventTracker { constructor(emitter) { this._events = []; this._emitter = emitter; } on(event, fn) { this._events.push([event, fn]); return this._emitter.on(event, fn); } remove() { this._events.forEach(([event, fn]) => { this._emitter.off(event, fn); }); } }; },{}],25:[function(require,module,exports){ "use strict"; class NetworkError extends Error { constructor(error, xhr = null) { super(`This looks like a network error, the endpoint might be blocked by an internet provider or a firewall.\n\nSource error: [${error}]`); this.isNetworkError = true; this.request = xhr; } } module.exports = NetworkError; },{}],26:[function(require,module,exports){ "use strict"; const findIndex = require('./findIndex'); function createCancelError() { return new Error('Cancelled'); } class RateLimitedQueue { constructor(limit) { if (typeof limit !== 'number' || limit === 0) { this.limit = Infinity; } else { this.limit = limit; } this.activeRequests = 0; this.queuedHandlers = []; } _call(fn) { this.activeRequests += 1; let done = false; let cancelActive; try { cancelActive = fn(); } catch (err) { this.activeRequests -= 1; throw err; } return { abort: () => { if (done) return; done = true; this.activeRequests -= 1; cancelActive(); this._queueNext(); }, done: () => { if (done) return; done = true; this.activeRequests -= 1; this._queueNext(); } }; } _queueNext() { // Do it soon but not immediately, this allows clearing out the entire queue synchronously // one by one without continuously _advancing_ it (and starting new tasks before immediately // aborting them) Promise.resolve().then(() => { this._next(); }); } _next() { if (this.activeRequests >= this.limit) { return; } if (this.queuedHandlers.length === 0) { return; } // Dispatch the next request, and update the abort/done handlers // so that cancelling it does the Right Thing (and doesn't just try // to dequeue an already-running request). const next = this.queuedHandlers.shift(); const handler = this._call(next.fn); next.abort = handler.abort; next.done = handler.done; } _queue(fn, options = {}) { const handler = { fn, priority: options.priority || 0, abort: () => { this._dequeue(handler); }, done: () => { throw new Error('Cannot mark a queued request as done: this indicates a bug'); } }; const index = findIndex(this.queuedHandlers, other => { return handler.priority > other.priority; }); if (index === -1) { this.queuedHandlers.push(handler); } else { this.queuedHandlers.splice(index, 0, handler); } return handler; } _dequeue(handler) { const index = this.queuedHandlers.indexOf(handler); if (index !== -1) { this.queuedHandlers.splice(index, 1); } } run(fn, queueOptions) { if (this.activeRequests < this.limit) { return this._call(fn); } return this._queue(fn, queueOptions); } wrapPromiseFunction(fn, queueOptions) { return (...args) => { let queuedRequest; const outerPromise = new Promise((resolve, reject) => { queuedRequest = this.run(() => { let cancelError; let innerPromise; try { innerPromise = Promise.resolve(fn(...args)); } catch (err) { innerPromise = Promise.reject(err); } innerPromise.then(result => { if (cancelError) { reject(cancelError); } else { queuedRequest.done(); resolve(result); } }, err => { if (cancelError) { reject(cancelError); } else { queuedRequest.done(); reject(err); } }); return () => { cancelError = createCancelError(); }; }, queueOptions); }); outerPromise.abort = () => { queuedRequest.abort(); }; return outerPromise; }; } } module.exports = { RateLimitedQueue, internalRateLimitedQueue: Symbol('__queue') }; },{"./findIndex":30}],27:[function(require,module,exports){ "use strict"; const { createAbortError } = require('./AbortController'); /** * Return a Promise that resolves after `ms` milliseconds. * * @param {number} ms - Number of milliseconds to wait. * @param {{ signal?: AbortSignal }} [opts] - An abort signal that can be used to cancel the delay early. * @returns {Promise} A Promise that resolves after the given amount of `ms`. */ module.exports = function delay(ms, opts) { return new Promise((resolve, reject) => { if (opts && opts.signal && opts.signal.aborted) { return reject(createAbortError()); } function onabort() { clearTimeout(timeout); cleanup(); reject(createAbortError()); } const timeout = setTimeout(() => { cleanup(); resolve(); }, ms); if (opts && opts.signal) { opts.signal.addEventListener('abort', onabort); } function cleanup() { if (opts && opts.signal) { opts.signal.removeEventListener('abort', onabort); } } }); }; },{"./AbortController":23}],28:[function(require,module,exports){ "use strict"; const throttle = require('lodash.throttle'); function _emitSocketProgress(uploader, progressData, file) { const { progress, bytesUploaded, bytesTotal } = progressData; if (progress) { uploader.uppy.log(`Upload progress: ${progress}`); uploader.uppy.emit('upload-progress', file, { uploader, bytesUploaded, bytesTotal }); } } module.exports = throttle(_emitSocketProgress, 300, { leading: true, trailing: true }); },{"lodash.throttle":7}],29:[function(require,module,exports){ "use strict"; const NetworkError = require('./NetworkError'); /** * Wrapper around window.fetch that throws a NetworkError when appropriate */ module.exports = function fetchWithNetworkError(...options) { return fetch(...options).catch(err => { if (err.name === 'AbortError') { throw err; } else { throw new NetworkError(err); } }); }; },{"./NetworkError":25}],30:[function(require,module,exports){ "use strict"; /** * Array.prototype.findIndex ponyfill for old browsers. * * @param {Array} array * @param {Function} predicate * @returns {number} */ module.exports = function findIndex(array, predicate) { for (let i = 0; i < array.length; i++) { if (predicate(array[i])) return i; } return -1; }; },{}],31:[function(require,module,exports){ "use strict"; module.exports = function getSocketHost(url) { // get the host domain const regex = /^(?:https?:\/\/|\/\/)?(?:[^@\n]+@)?(?:www\.)?([^\n]+)/i; const host = regex.exec(url)[1]; const socketProtocol = /^http:\/\//i.test(url) ? 'ws' : 'wss'; return `${socketProtocol}://${host}`; }; },{}],32:[function(require,module,exports){ module.exports={ "name": "@uppy/aws-s3-multipart", "description": "Upload to Amazon S3 with Uppy and S3's Multipart upload strategy", "version": "2.0.0-alpha.0", "license": "MIT", "main": "lib/index.js", "types": "types/index.d.ts", "keywords": [ "file uploader", "aws s3", "amazon s3", "s3", "uppy", "uppy-plugin", "multipart" ], "homepage": "https://uppy.io", "bugs": { "url": "https://github.com/transloadit/uppy/issues" }, "repository": { "type": "git", "url": "git+https://github.com/transloadit/uppy.git" }, "dependencies": { "@uppy/companion-client": "file:../companion-client", "@uppy/utils": "file:../utils" }, "devDependencies": { "whatwg-fetch": "3.6.2", "nock": "^13.1.0" }, "peerDependencies": { "@uppy/core": "^1.0.0" } } },{}],33:[function(require,module,exports){ "use strict"; const cuid = require('cuid'); const { Provider, RequestClient, Socket } = require('@uppy/companion-client'); const emitSocketProgress = require('@uppy/utils/lib/emitSocketProgress'); const getSocketHost = require('@uppy/utils/lib/getSocketHost'); const EventTracker = require('@uppy/utils/lib/EventTracker'); const ProgressTimeout = require('@uppy/utils/lib/ProgressTimeout'); const NetworkError = require('@uppy/utils/lib/NetworkError'); const isNetworkError = require('@uppy/utils/lib/isNetworkError'); const { internalRateLimitedQueue } = require('@uppy/utils/lib/RateLimitedQueue'); // See XHRUpload function buildResponseError(xhr, error) { // No error message if (!error) error = new Error('Upload error'); // Got an error message string if (typeof error === 'string') error = new Error(error); // Got something else if (!(error instanceof Error)) { error = Object.assign(new Error('Upload error'), { data: error }); } if (isNetworkError(xhr)) { error = new NetworkError(error, xhr); return error; } error.request = xhr; return error; } // See XHRUpload function setTypeInBlob(file) { const dataWithUpdatedType = file.data.slice(0, file.data.size, file.meta.type); return dataWithUpdatedType; } module.exports = class MiniXHRUpload { constructor(uppy, opts) { this.uppy = uppy; this.opts = { validateStatus(status, responseText, response) { return status >= 200 && status < 300; }, ...opts }; this.requests = opts[internalRateLimitedQueue]; this.uploaderEvents = Object.create(null); this.i18n = opts.i18n; } _getOptions(file) { const { uppy } = this; const overrides = uppy.getState().xhrUpload; const opts = { ...this.opts, ...(overrides || {}), ...(file.xhrUpload || {}), headers: {} }; Object.assign(opts.headers, this.opts.headers); if (overrides) { Object.assign(opts.headers, overrides.headers); } if (file.xhrUpload) { Object.assign(opts.headers, file.xhrUpload.headers); } return opts; } uploadFile(id, current, total) { const file = this.uppy.getFile(id); if (file.error) { throw new Error(file.error); } else if (file.isRemote) { return this._uploadRemoteFile(file, current, total); } return this._uploadLocalFile(file, current, total); } _addMetadata(formData, meta, opts) { const metaFields = Array.isArray(opts.metaFields) ? opts.metaFields // Send along all fields by default. : Object.keys(meta); metaFields.forEach(item => { formData.append(item, meta[item]); }); } _createFormDataUpload(file, opts) { const formPost = new FormData(); this._addMetadata(formPost, file.meta, opts); const dataWithUpdatedType = setTypeInBlob(file); if (file.name) { formPost.append(opts.fieldName, dataWithUpdatedType, file.meta.name); } else { formPost.append(opts.fieldName, dataWithUpdatedType); } return formPost; } _createBareUpload(file, opts) { return file.data; } _onFileRemoved(fileID, cb) { this.uploaderEvents[fileID].on('file-removed', file => { if (fileID === file.id) cb(file.id); }); } _onRetry(fileID, cb) { this.uploaderEvents[fileID].on('upload-retry', targetFileID => { if (fileID === targetFileID) { cb(); } }); } _onRetryAll(fileID, cb) { this.uploaderEvents[fileID].on('retry-all', filesToRetry => { if (!this.uppy.getFile(fileID)) return; cb(); }); } _onCancelAll(fileID, cb) { this.uploaderEvents[fileID].on('cancel-all', () => { if (!this.uppy.getFile(fileID)) return; cb(); }); } _uploadLocalFile(file, current, total) { const opts = this._getOptions(file); this.uppy.log(`uploading ${current} of ${total}`); return new Promise((resolve, reject) => { // This is done in index.js in the S3 plugin. // this.uppy.emit('upload-started', file) const data = opts.formData ? this._createFormDataUpload(file, opts) : this._createBareUpload(file, opts); const xhr = new XMLHttpRequest(); this.uploaderEvents[file.id] = new EventTracker(this.uppy); const timer = new ProgressTimeout(opts.timeout, () => { xhr.abort(); queuedRequest.done(); const error = new Error(this.i18n('timedOut', { seconds: Math.ceil(opts.timeout / 1000) })); this.uppy.emit('upload-error', file, error); reject(error); }); const id = cuid(); xhr.upload.addEventListener('loadstart', ev => { this.uppy.log(`[AwsS3/XHRUpload] ${id} started`); }); xhr.upload.addEventListener('progress', ev => { this.uppy.log(`[AwsS3/XHRUpload] ${id} progress: ${ev.loaded} / ${ev.total}`); // Begin checking for timeouts when progress starts, instead of loading, // to avoid timing out requests on browser concurrency queue timer.progress(); if (ev.lengthComputable) { this.uppy.emit('upload-progress', file, { uploader: this, bytesUploaded: ev.loaded, bytesTotal: ev.total }); } }); xhr.addEventListener('load', ev => { this.uppy.log(`[AwsS3/XHRUpload] ${id} finished`); timer.done(); queuedRequest.done(); if (this.uploaderEvents[file.id]) { this.uploaderEvents[file.id].remove(); this.uploaderEvents[file.id] = null; } if (opts.validateStatus(ev.target.status, xhr.responseText, xhr)) { const body = opts.getResponseData(xhr.responseText, xhr); const uploadURL = body[opts.responseUrlFieldName]; const uploadResp = { status: ev.target.status, body, uploadURL }; this.uppy.emit('upload-success', file, uploadResp); if (uploadURL) { this.uppy.log(`Download ${file.name} from ${uploadURL}`); } return resolve(file); } const body = opts.getResponseData(xhr.responseText, xhr); const error = buildResponseError(xhr, opts.getResponseError(xhr.responseText, xhr)); const response = { status: ev.target.status, body }; this.uppy.emit('upload-error', file, error, response); return reject(error); }); xhr.addEventListener('error', ev => { this.uppy.log(`[AwsS3/XHRUpload] ${id} errored`); timer.done(); queuedRequest.done(); if (this.uploaderEvents[file.id]) { this.uploaderEvents[file.id].remove(); this.uploaderEvents[file.id] = null; } const error = buildResponseError(xhr, opts.getResponseError(xhr.responseText, xhr)); this.uppy.emit('upload-error', file, error); return reject(error); }); xhr.open(opts.method.toUpperCase(), opts.endpoint, true); // IE10 does not allow setting `withCredentials` and `responseType` // before `open()` is called. xhr.withCredentials = opts.withCredentials; if (opts.responseType !== '') { xhr.responseType = opts.responseType; } Object.keys(opts.headers).forEach(header => { xhr.setRequestHeader(header, opts.headers[header]); }); const queuedRequest = this.requests.run(() => { xhr.send(data); return () => { timer.done(); xhr.abort(); }; }, { priority: 1 }); this._onFileRemoved(file.id, () => { queuedRequest.abort(); reject(new Error('File removed')); }); this._onCancelAll(file.id, () => { queuedRequest.abort(); reject(new Error('Upload cancelled')); }); }); } _uploadRemoteFile(file, current, total) { const opts = this._getOptions(file); return new Promise((resolve, reject) => { // This is done in index.js in the S3 plugin. // this.uppy.emit('upload-started', file) const fields = {}; const metaFields = Array.isArray(opts.metaFields) ? opts.metaFields // Send along all fields by default. : Object.keys(file.meta); metaFields.forEach(name => { fields[name] = file.meta[name]; }); const Client = file.remote.providerOptions.provider ? Provider : RequestClient; const client = new Client(this.uppy, file.remote.providerOptions); client.post(file.remote.url, { ...file.remote.body, endpoint: opts.endpoint, size: file.data.size, fieldname: opts.fieldName, metadata: fields, httpMethod: opts.method, useFormData: opts.formData, headers: opts.headers }).then(res => { const { token } = res; const host = getSocketHost(file.remote.companionUrl); const socket = new Socket({ target: `${host}/api/${token}`, autoOpen: false }); this.uploaderEvents[file.id] = new EventTracker(this.uppy); this._onFileRemoved(file.id, () => { socket.send('pause', {}); queuedRequest.abort(); resolve(`upload ${file.id} was removed`); }); this._onCancelAll(file.id, () => { socket.send('pause', {}); queuedRequest.abort(); resolve(`upload ${file.id} was canceled`); }); this._onRetry(file.id, () => { socket.send('pause', {}); socket.send('resume', {}); }); this._onRetryAll(file.id, () => { socket.send('pause', {}); socket.send('resume', {}); }); socket.on('progress', progressData => emitSocketProgress(this, progressData, file)); socket.on('success', data => { const body = opts.getResponseData(data.response.responseText, data.response); const uploadURL = body[opts.responseUrlFieldName]; const uploadResp = { status: data.response.status, body, uploadURL, bytesUploaded: data.bytesUploaded }; this.uppy.emit('upload-success', file, uploadResp); queuedRequest.done(); if (this.uploaderEvents[file.id]) { this.uploaderEvents[file.id].remove(); this.uploaderEvents[file.id] = null; } return resolve(); }); socket.on('error', errData => { const resp = errData.response; const error = resp ? opts.getResponseError(resp.responseText, resp) : Object.assign(new Error(errData.error.message), { cause: errData.error }); this.uppy.emit('upload-error', file, error); queuedRequest.done(); if (this.uploaderEvents[file.id]) { this.uploaderEvents[file.id].remove(); this.uploaderEvents[file.id] = null; } reject(error); }); const queuedRequest = this.requests.run(() => { socket.open(); if (file.isPaused) { socket.send('pause', {}); } return () => socket.close(); }); }).catch(err => { this.uppy.emit('upload-error', file, err); reject(err); }); }); } }; },{"@uppy/companion-client":41,"@uppy/utils/lib/EventTracker":44,"@uppy/utils/lib/NetworkError":45,"@uppy/utils/lib/ProgressTimeout":46,"@uppy/utils/lib/RateLimitedQueue":47,"@uppy/utils/lib/emitSocketProgress":49,"@uppy/utils/lib/getSocketHost":52,"@uppy/utils/lib/isNetworkError":54,"cuid":3}],34:[function(require,module,exports){ "use strict"; var _class, _temp; /** * This plugin is currently a A Big Hack™! The core reason for that is how this plugin * interacts with Uppy's current pipeline design. The pipeline can handle files in steps, * including preprocessing, uploading, and postprocessing steps. This plugin initially * was designed to do its work in a preprocessing step, and let XHRUpload deal with the * actual file upload as an uploading step. However, Uppy runs steps on all files at once, * sequentially: first, all files go through a preprocessing step, then, once they are all * done, they go through the uploading step. * * For S3, this causes severely broken behaviour when users upload many files. The * preprocessing step will request S3 upload URLs that are valid for a short time only, * but it has to do this for _all_ files, which can take a long time if there are hundreds * or even thousands of files. By the time the uploader step starts, the first URLs may * already have expired. If not, the uploading might take such a long time that later URLs * will expire before some files can be uploaded. * * The long-term solution to this problem is to change the upload pipeline so that files * can be sent to the next step individually. That requires a breaking change, so it is * planned for some future Uppy version. * * In the mean time, this plugin is stuck with a hackier approach: the necessary parts * of the XHRUpload implementation were copied into this plugin, as the MiniXHRUpload * class, and this plugin calls into it immediately once it receives an upload URL. * This isn't as nicely modular as we'd like and requires us to maintain two copies of * the XHRUpload code, but at least it's not horrifically broken :) */ const { BasePlugin } = require('@uppy/core'); const Translator = require('@uppy/utils/lib/Translator'); const { RateLimitedQueue, internalRateLimitedQueue } = require('@uppy/utils/lib/RateLimitedQueue'); const settle = require('@uppy/utils/lib/settle'); const hasProperty = require('@uppy/utils/lib/hasProperty'); const { RequestClient } = require('@uppy/companion-client'); const qsStringify = require('qs-stringify'); const MiniXHRUpload = require('./MiniXHRUpload'); const isXml = require('./isXml'); function resolveUrl(origin, link) { return new URL(link, origin || undefined).toString(); } /** * Get the contents of a named tag in an XML source string. * * @param {string} source - The XML source string. * @param {string} tagName - The name of the tag. * @returns {string} The contents of the tag, or the empty string if the tag does not exist. */ function getXmlValue(source, tagName) { const start = source.indexOf(`<${tagName}>`); const end = source.indexOf(``, start); return start !== -1 && end !== -1 ? source.slice(start + tagName.length + 2, end) : ''; } function assertServerError(res) { if (res && res.error) { const error = new Error(res.message); Object.assign(error, res.error); throw error; } return res; } // warning deduplication flag: see `getResponseData()` XHRUpload option definition let warnedSuccessActionStatus = false; module.exports = (_temp = _class = class AwsS3 extends BasePlugin { constructor(uppy, opts) { super(uppy, opts); this.type = 'uploader'; this.id = this.opts.id || 'AwsS3'; this.title = 'AWS S3'; this.defaultLocale = { strings: { timedOut: 'Upload stalled for %{seconds} seconds, aborting.' } }; const defaultOptions = { timeout: 30 * 1000, limit: 0, metaFields: [], // have to opt in getUploadParameters: this.getUploadParameters.bind(this) }; this.opts = { ...defaultOptions, ...opts }; this.client = new RequestClient(uppy, opts); this.handleUpload = this.handleUpload.bind(this); this.requests = new RateLimitedQueue(this.opts.limit); } getUploadParameters(file) { if (!this.opts.companionUrl) { throw new Error('Expected a `companionUrl` option containing a Companion address.'); } const filename = file.meta.name; const { type } = file.meta; const metadata = {}; this.opts.metaFields.forEach(key => { if (file.meta[key] != null) { metadata[key] = file.meta[key].toString(); } }); const query = qsStringify({ filename, type, metadata }); return this.client.get(`s3/params?${query}`).then(assertServerError); } validateParameters(file, params) { const valid = typeof params === 'object' && params && typeof params.url === 'string' && (typeof params.fields === 'object' || params.fields == null); if (!valid) { const err = new TypeError(`AwsS3: got incorrect result from 'getUploadParameters()' for file '${file.name}', expected an object '{ url, method, fields, headers }' but got '${JSON.stringify(params)}' instead.\nSee https://uppy.io/docs/aws-s3/#getUploadParameters-file for more on the expected format.`); console.error(err); throw err; } const methodIsValid = params.method == null || /^(put|post)$/i.test(params.method); if (!methodIsValid) { const err = new TypeError(`AwsS3: got incorrect method from 'getUploadParameters()' for file '${file.name}', expected 'put' or 'post' but got '${params.method}' instead.\nSee https://uppy.io/docs/aws-s3/#getUploadParameters-file for more on the expected format.`); console.error(err); throw err; } } handleUpload(fileIDs) { /** * keep track of `getUploadParameters()` responses * so we can cancel the calls individually using just a file ID * * @type {object.} */ const paramsPromises = Object.create(null); function onremove(file) { const { id } = file; if (hasProperty(paramsPromises, id)) { paramsPromises[id].abort(); } } this.uppy.on('file-removed', onremove); fileIDs.forEach(id => { const file = this.uppy.getFile(id); this.uppy.emit('upload-started', file); }); const getUploadParameters = this.requests.wrapPromiseFunction(file => { return this.opts.getUploadParameters(file); }); const numberOfFiles = fileIDs.length; return settle(fileIDs.map((id, index) => { paramsPromises[id] = getUploadParameters(this.uppy.getFile(id)); return paramsPromises[id].then(params => { delete paramsPromises[id]; const file = this.uppy.getFile(id); this.validateParameters(file, params); const { method = 'post', url, fields, headers } = params; const xhrOpts = { method, formData: method.toLowerCase() === 'post', endpoint: url, metaFields: fields ? Object.keys(fields) : [] }; if (headers) { xhrOpts.headers = headers; } this.uppy.setFileState(file.id, { meta: { ...file.meta, ...fields }, xhrUpload: xhrOpts }); return this._uploader.uploadFile(file.id, index, numberOfFiles); }).catch(error => { delete paramsPromises[id]; const file = this.uppy.getFile(id); this.uppy.emit('upload-error', file, error); }); })).then(settled => { // cleanup. this.uppy.off('file-removed', onremove); return settled; }); } install() { const { uppy } = this; this.uppy.addUploader(this.handleUpload); // Get the response data from a successful XMLHttpRequest instance. // `content` is the S3 response as a string. // `xhr` is the XMLHttpRequest instance. function defaultGetResponseData(content, xhr) { const opts = this; // If no response, we've hopefully done a PUT request to the file // in the bucket on its full URL. if (!isXml(content, xhr)) { if (opts.method.toUpperCase() === 'POST') { if (!warnedSuccessActionStatus) { uppy.log('[AwsS3] No response data found, make sure to set the success_action_status AWS SDK option to 201. See https://uppy.io/docs/aws-s3/#POST-Uploads', 'warning'); warnedSuccessActionStatus = true; } // The responseURL won't contain the object key. Give up. return { location: null }; } // responseURL is not available in older browsers. if (!xhr.responseURL) { return { location: null }; } // Trim the query string because it's going to be a bunch of presign // parameters for a PUT request—doing a GET request with those will // always result in an error return { location: xhr.responseURL.replace(/\?.*$/, '') }; } return { // Some S3 alternatives do not reply with an absolute URL. // Eg DigitalOcean Spaces uses /$bucketName/xyz location: resolveUrl(xhr.responseURL, getXmlValue(content, 'Location')), bucket: getXmlValue(content, 'Bucket'), key: getXmlValue(content, 'Key'), etag: getXmlValue(content, 'ETag') }; } // Get the error data from a failed XMLHttpRequest instance. // `content` is the S3 response as a string. // `xhr` is the XMLHttpRequest instance. function defaultGetResponseError(content, xhr) { // If no response, we don't have a specific error message, use the default. if (!isXml(content, xhr)) { return; } const error = getXmlValue(content, 'Message'); return new Error(error); } const xhrOptions = { fieldName: 'file', responseUrlFieldName: 'location', timeout: this.opts.timeout, // Share the rate limiting queue with XHRUpload. [internalRateLimitedQueue]: this.requests, responseType: 'text', getResponseData: this.opts.getResponseData || defaultGetResponseData, getResponseError: defaultGetResponseError }; // Only for MiniXHRUpload, remove once we can depend on XHRUpload directly again xhrOptions.i18n = this.i18n; // Revert to `this.uppy.use(XHRUpload)` once the big comment block at the top of // this file is solved this._uploader = new MiniXHRUpload(this.uppy, xhrOptions); } uninstall() { this.uppy.removeUploader(this.handleUpload); } }, _class.VERSION = require('../package.json').version, _temp); },{"../package.json":56,"./MiniXHRUpload":33,"./isXml":35,"@uppy/companion-client":41,"@uppy/core":60,"@uppy/utils/lib/RateLimitedQueue":47,"@uppy/utils/lib/Translator":48,"@uppy/utils/lib/hasProperty":53,"@uppy/utils/lib/settle":55,"qs-stringify":11}],35:[function(require,module,exports){ "use strict"; /** * Remove parameters like `charset=utf-8` from the end of a mime type string. * * @param {string} mimeType - The mime type string that may have optional parameters. * @returns {string} The "base" mime type, i.e. only 'category/type'. */ function removeMimeParameters(mimeType) { return mimeType.replace(/;.*$/, ''); } /** * Check if a response contains XML based on the response object and its text content. * * @param {string} content - The text body of the response. * @param {object|XMLHttpRequest} xhr - The XHR object or response object from Companion. * @returns {bool} Whether the content is (probably) XML. */ function isXml(content, xhr) { const rawContentType = xhr.headers ? xhr.headers['content-type'] : xhr.getResponseHeader('Content-Type'); if (typeof rawContentType === 'string') { const contentType = removeMimeParameters(rawContentType).toLowerCase(); if (contentType === 'application/xml' || contentType === 'text/xml') { return true; } // GCS uses text/html for some reason // https://github.com/transloadit/uppy/issues/896 if (contentType === 'text/html' && /^<\?xml /.test(content)) { return true; } } return false; } module.exports = isXml; },{}],36:[function(require,module,exports){ arguments[4][15][0].apply(exports,arguments) },{"dup":15}],37:[function(require,module,exports){ arguments[4][16][0].apply(exports,arguments) },{"./RequestClient":38,"./tokenStorage":42,"dup":16,"qs-stringify":11}],38:[function(require,module,exports){ arguments[4][17][0].apply(exports,arguments) },{"../package.json":43,"./AuthError":36,"@uppy/utils/lib/fetchWithNetworkError":50,"dup":17}],39:[function(require,module,exports){ arguments[4][18][0].apply(exports,arguments) },{"./RequestClient":38,"dup":18}],40:[function(require,module,exports){ arguments[4][19][0].apply(exports,arguments) },{"dup":19,"namespace-emitter":9}],41:[function(require,module,exports){ arguments[4][20][0].apply(exports,arguments) },{"./Provider":37,"./RequestClient":38,"./SearchProvider":39,"./Socket":40,"dup":20}],42:[function(require,module,exports){ arguments[4][21][0].apply(exports,arguments) },{"dup":21}],43:[function(require,module,exports){ arguments[4][22][0].apply(exports,arguments) },{"dup":22}],44:[function(require,module,exports){ arguments[4][24][0].apply(exports,arguments) },{"dup":24}],45:[function(require,module,exports){ arguments[4][25][0].apply(exports,arguments) },{"dup":25}],46:[function(require,module,exports){ "use strict"; /** * Helper to abort upload requests if there has not been any progress for `timeout` ms. * Create an instance using `timer = new ProgressTimeout(10000, onTimeout)` * Call `timer.progress()` to signal that there has been progress of any kind. * Call `timer.done()` when the upload has completed. */ class ProgressTimeout { constructor(timeout, timeoutHandler) { this._timeout = timeout; this._onTimedOut = timeoutHandler; this._isDone = false; this._aliveTimer = null; this._onTimedOut = this._onTimedOut.bind(this); } progress() { // Some browsers fire another progress event when the upload is // cancelled, so we have to ignore progress after the timer was // told to stop. if (this._isDone) return; if (this._timeout > 0) { if (this._aliveTimer) clearTimeout(this._aliveTimer); this._aliveTimer = setTimeout(this._onTimedOut, this._timeout); } } done() { if (this._aliveTimer) { clearTimeout(this._aliveTimer); this._aliveTimer = null; } this._isDone = true; } } module.exports = ProgressTimeout; },{}],47:[function(require,module,exports){ arguments[4][26][0].apply(exports,arguments) },{"./findIndex":51,"dup":26}],48:[function(require,module,exports){ "use strict"; const has = require('./hasProperty'); /** * Translates strings with interpolation & pluralization support. * Extensible with custom dictionaries and pluralization functions. * * Borrows heavily from and inspired by Polyglot https://github.com/airbnb/polyglot.js, * basically a stripped-down version of it. Differences: pluralization functions are not hardcoded * and can be easily added among with dictionaries, nested objects are used for pluralization * as opposed to `||||` delimeter * * Usage example: `translator.translate('files_chosen', {smart_count: 3})` */ module.exports = class Translator { /** * @param {object|Array} locales - locale or list of locales. */ constructor(locales) { this.locale = { strings: {}, pluralize(n) { if (n === 1) { return 0; } return 1; } }; if (Array.isArray(locales)) { locales.forEach(locale => this._apply(locale)); } else { this._apply(locales); } } _apply(locale) { if (!locale || !locale.strings) { return; } const prevLocale = this.locale; this.locale = { ...prevLocale, strings: { ...prevLocale.strings, ...locale.strings } }; this.locale.pluralize = locale.pluralize || prevLocale.pluralize; } /** * Takes a string with placeholder variables like `%{smart_count} file selected` * and replaces it with values from options `{smart_count: 5}` * * @license https://github.com/airbnb/polyglot.js/blob/master/LICENSE * taken from https://github.com/airbnb/polyglot.js/blob/master/lib/polyglot.js#L299 * * @param {string} phrase that needs interpolation, with placeholders * @param {object} options with values that will be used to replace placeholders * @returns {any[]} interpolated */ interpolate(phrase, options) { const dollarRegex = /\$/g; const dollarBillsYall = '$$$$'; let interpolated = [phrase]; for (const arg in options) { if (arg !== '_' && has(options, arg)) { // Ensure replacement value is escaped to prevent special $-prefixed // regex replace tokens. the "$$$$" is needed because each "$" needs to // be escaped with "$" itself, and we need two in the resulting output. let replacement = options[arg]; if (typeof replacement === 'string') { replacement = dollarRegex[Symbol.replace](replacement, dollarBillsYall); } // We create a new `RegExp` each time instead of using a more-efficient // string replace so that the same argument can be replaced multiple times // in the same phrase. interpolated = insertReplacement(interpolated, new RegExp(`%\\{${arg}\\}`, 'g'), replacement); } } return interpolated; function insertReplacement(source, rx, replacement) { const newParts = []; source.forEach(chunk => { // When the source contains multiple placeholders for interpolation, // we should ignore chunks that are not strings, because those // can be JSX objects and will be otherwise incorrectly turned into strings. // Without this condition we’d get this: [object Object] hello [object Object] my