UNPKG

58.7 kBJavaScriptView Raw
1function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
2
3function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
4
5function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
6
7function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; }
8
9function _wrapNativeSuper(Class) { var _cache = typeof Map === "function" ? new Map() : undefined; _wrapNativeSuper = function _wrapNativeSuper(Class) { if (Class === null || !_isNativeFunction(Class)) return Class; if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() { return _construct(Class, arguments, _getPrototypeOf(this).constructor); } Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); return _setPrototypeOf(Wrapper, Class); }; return _wrapNativeSuper(Class); }
10
11function _construct(Parent, args, Class) { if (_isNativeReflectConstruct()) { _construct = Reflect.construct; } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); }
12
13function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }
14
15function _isNativeFunction(fn) { return Function.toString.call(fn).indexOf("[native code]") !== -1; }
16
17function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
18
19function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
20
21var Translator = require('@uppy/utils/lib/Translator');
22
23var ee = require('namespace-emitter');
24
25var cuid = require('cuid');
26
27var throttle = require('lodash.throttle');
28
29var prettierBytes = require('@transloadit/prettier-bytes');
30
31var match = require('mime-match');
32
33var DefaultStore = require('@uppy/store-default');
34
35var getFileType = require('@uppy/utils/lib/getFileType');
36
37var getFileNameAndExtension = require('@uppy/utils/lib/getFileNameAndExtension');
38
39var generateFileID = require('@uppy/utils/lib/generateFileID');
40
41var supportsUploadProgress = require('./supportsUploadProgress');
42
43var _require = require('./loggers'),
44 justErrorsLogger = _require.justErrorsLogger,
45 debugLogger = _require.debugLogger;
46
47var Plugin = require('./Plugin'); // Exported from here.
48
49
50var RestrictionError = /*#__PURE__*/function (_Error) {
51 _inheritsLoose(RestrictionError, _Error);
52
53 function RestrictionError() {
54 var _this;
55
56 for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
57 args[_key] = arguments[_key];
58 }
59
60 _this = _Error.call.apply(_Error, [this].concat(args)) || this;
61 _this.isRestriction = true;
62 return _this;
63 }
64
65 return RestrictionError;
66}( /*#__PURE__*/_wrapNativeSuper(Error));
67/**
68 * Uppy Core module.
69 * Manages plugins, state updates, acts as an event bus,
70 * adds/removes files and metadata.
71 */
72
73
74var Uppy = /*#__PURE__*/function () {
75 /**
76 * Instantiate Uppy
77 *
78 * @param {object} opts — Uppy options
79 */
80 function Uppy(opts) {
81 var _this2 = this;
82
83 this.defaultLocale = {
84 strings: {
85 addBulkFilesFailed: {
86 0: 'Failed to add %{smart_count} file due to an internal error',
87 1: 'Failed to add %{smart_count} files due to internal errors'
88 },
89 youCanOnlyUploadX: {
90 0: 'You can only upload %{smart_count} file',
91 1: 'You can only upload %{smart_count} files'
92 },
93 youHaveToAtLeastSelectX: {
94 0: 'You have to select at least %{smart_count} file',
95 1: 'You have to select at least %{smart_count} files'
96 },
97 // The default `exceedsSize2` string only combines the `exceedsSize` string (%{backwardsCompat}) with the size.
98 // Locales can override `exceedsSize2` to specify a different word order. This is for backwards compat with
99 // Uppy 1.9.x and below which did a naive concatenation of `exceedsSize2 + size` instead of using a locale-specific
100 // substitution.
101 // TODO: In 2.0 `exceedsSize2` should be removed in and `exceedsSize` updated to use substitution.
102 exceedsSize2: '%{backwardsCompat} %{size}',
103 exceedsSize: 'This file exceeds maximum allowed size of',
104 inferiorSize: 'This file is smaller than the allowed size of %{size}',
105 youCanOnlyUploadFileTypes: 'You can only upload: %{types}',
106 noNewAlreadyUploading: 'Cannot add new files: already uploading',
107 noDuplicates: 'Cannot add the duplicate file \'%{fileName}\', it already exists',
108 companionError: 'Connection with Companion failed',
109 companionUnauthorizeHint: 'To unauthorize to your %{provider} account, please go to %{url}',
110 failedToUpload: 'Failed to upload %{file}',
111 noInternetConnection: 'No Internet connection',
112 connectedToInternet: 'Connected to the Internet',
113 // Strings for remote providers
114 noFilesFound: 'You have no files or folders here',
115 selectX: {
116 0: 'Select %{smart_count}',
117 1: 'Select %{smart_count}'
118 },
119 selectAllFilesFromFolderNamed: 'Select all files from folder %{name}',
120 unselectAllFilesFromFolderNamed: 'Unselect all files from folder %{name}',
121 selectFileNamed: 'Select file %{name}',
122 unselectFileNamed: 'Unselect file %{name}',
123 openFolderNamed: 'Open folder %{name}',
124 cancel: 'Cancel',
125 logOut: 'Log out',
126 filter: 'Filter',
127 resetFilter: 'Reset filter',
128 loading: 'Loading...',
129 authenticateWithTitle: 'Please authenticate with %{pluginName} to select files',
130 authenticateWith: 'Connect to %{pluginName}',
131 searchImages: 'Search for images',
132 enterTextToSearch: 'Enter text to search for images',
133 backToSearch: 'Back to Search',
134 emptyFolderAdded: 'No files were added from empty folder',
135 folderAdded: {
136 0: 'Added %{smart_count} file from %{folder}',
137 1: 'Added %{smart_count} files from %{folder}'
138 }
139 }
140 };
141 var defaultOptions = {
142 id: 'uppy',
143 autoProceed: false,
144 allowMultipleUploads: true,
145 debug: false,
146 restrictions: {
147 maxFileSize: null,
148 minFileSize: null,
149 maxTotalFileSize: null,
150 maxNumberOfFiles: null,
151 minNumberOfFiles: null,
152 allowedFileTypes: null
153 },
154 meta: {},
155 onBeforeFileAdded: function onBeforeFileAdded(currentFile, files) {
156 return currentFile;
157 },
158 onBeforeUpload: function onBeforeUpload(files) {
159 return files;
160 },
161 store: DefaultStore(),
162 logger: justErrorsLogger,
163 infoTimeout: 5000
164 }; // Merge default options with the ones set by user,
165 // making sure to merge restrictions too
166
167 this.opts = _extends({}, defaultOptions, opts, {
168 restrictions: _extends({}, defaultOptions.restrictions, opts && opts.restrictions)
169 }); // Support debug: true for backwards-compatability, unless logger is set in opts
170 // opts instead of this.opts to avoid comparing objects — we set logger: justErrorsLogger in defaultOptions
171
172 if (opts && opts.logger && opts.debug) {
173 this.log('You are using a custom `logger`, but also set `debug: true`, which uses built-in logger to output logs to console. Ignoring `debug: true` and using your custom `logger`.', 'warning');
174 } else if (opts && opts.debug) {
175 this.opts.logger = debugLogger;
176 }
177
178 this.log("Using Core v" + this.constructor.VERSION);
179
180 if (this.opts.restrictions.allowedFileTypes && this.opts.restrictions.allowedFileTypes !== null && !Array.isArray(this.opts.restrictions.allowedFileTypes)) {
181 throw new TypeError('`restrictions.allowedFileTypes` must be an array');
182 }
183
184 this.i18nInit(); // Container for different types of plugins
185
186 this.plugins = {};
187 this.getState = this.getState.bind(this);
188 this.getPlugin = this.getPlugin.bind(this);
189 this.setFileMeta = this.setFileMeta.bind(this);
190 this.setFileState = this.setFileState.bind(this);
191 this.log = this.log.bind(this);
192 this.info = this.info.bind(this);
193 this.hideInfo = this.hideInfo.bind(this);
194 this.addFile = this.addFile.bind(this);
195 this.removeFile = this.removeFile.bind(this);
196 this.pauseResume = this.pauseResume.bind(this);
197 this.validateRestrictions = this.validateRestrictions.bind(this); // ___Why throttle at 500ms?
198 // - We must throttle at >250ms for superfocus in Dashboard to work well (because animation takes 0.25s, and we want to wait for all animations to be over before refocusing).
199 // [Practical Check]: if thottle is at 100ms, then if you are uploading a file, and click 'ADD MORE FILES', - focus won't activate in Firefox.
200 // - We must throttle at around >500ms to avoid performance lags.
201 // [Practical Check] Firefox, try to upload a big file for a prolonged period of time. Laptop will start to heat up.
202
203 this._calculateProgress = throttle(this._calculateProgress.bind(this), 500, {
204 leading: true,
205 trailing: true
206 });
207 this.updateOnlineStatus = this.updateOnlineStatus.bind(this);
208 this.resetProgress = this.resetProgress.bind(this);
209 this.pauseAll = this.pauseAll.bind(this);
210 this.resumeAll = this.resumeAll.bind(this);
211 this.retryAll = this.retryAll.bind(this);
212 this.cancelAll = this.cancelAll.bind(this);
213 this.retryUpload = this.retryUpload.bind(this);
214 this.upload = this.upload.bind(this);
215 this.emitter = ee();
216 this.on = this.on.bind(this);
217 this.off = this.off.bind(this);
218 this.once = this.emitter.once.bind(this.emitter);
219 this.emit = this.emitter.emit.bind(this.emitter);
220 this.preProcessors = [];
221 this.uploaders = [];
222 this.postProcessors = [];
223 this.store = this.opts.store;
224 this.setState({
225 plugins: {},
226 files: {},
227 currentUploads: {},
228 allowNewUpload: true,
229 capabilities: {
230 uploadProgress: supportsUploadProgress(),
231 individualCancellation: true,
232 resumableUploads: false
233 },
234 totalProgress: 0,
235 meta: _extends({}, this.opts.meta),
236 info: {
237 isHidden: true,
238 type: 'info',
239 message: ''
240 }
241 });
242 this._storeUnsubscribe = this.store.subscribe(function (prevState, nextState, patch) {
243 _this2.emit('state-update', prevState, nextState, patch);
244
245 _this2.updateAll(nextState);
246 }); // Exposing uppy object on window for debugging and testing
247
248 if (this.opts.debug && typeof window !== 'undefined') {
249 window[this.opts.id] = this;
250 }
251
252 this._addListeners(); // Re-enable if we’ll need some capabilities on boot, like isMobileDevice
253 // this._setCapabilities()
254
255 } // _setCapabilities = () => {
256 // const capabilities = {
257 // isMobileDevice: isMobileDevice()
258 // }
259 // this.setState({
260 // ...this.getState().capabilities,
261 // capabilities
262 // })
263 // }
264
265
266 var _proto = Uppy.prototype;
267
268 _proto.on = function on(event, callback) {
269 this.emitter.on(event, callback);
270 return this;
271 };
272
273 _proto.off = function off(event, callback) {
274 this.emitter.off(event, callback);
275 return this;
276 }
277 /**
278 * Iterate on all plugins and run `update` on them.
279 * Called each time state changes.
280 *
281 */
282 ;
283
284 _proto.updateAll = function updateAll(state) {
285 this.iteratePlugins(function (plugin) {
286 plugin.update(state);
287 });
288 }
289 /**
290 * Updates state with a patch
291 *
292 * @param {object} patch {foo: 'bar'}
293 */
294 ;
295
296 _proto.setState = function setState(patch) {
297 this.store.setState(patch);
298 }
299 /**
300 * Returns current state.
301 *
302 * @returns {object}
303 */
304 ;
305
306 _proto.getState = function getState() {
307 return this.store.getState();
308 }
309 /**
310 * Back compat for when uppy.state is used instead of uppy.getState().
311 */
312 ;
313
314 /**
315 * Shorthand to set state for a specific file.
316 */
317 _proto.setFileState = function setFileState(fileID, state) {
318 var _extends2;
319
320 if (!this.getState().files[fileID]) {
321 throw new Error("Can\u2019t set state for " + fileID + " (the file could have been removed)");
322 }
323
324 this.setState({
325 files: _extends({}, this.getState().files, (_extends2 = {}, _extends2[fileID] = _extends({}, this.getState().files[fileID], state), _extends2))
326 });
327 };
328
329 _proto.i18nInit = function i18nInit() {
330 this.translator = new Translator([this.defaultLocale, this.opts.locale]);
331 this.locale = this.translator.locale;
332 this.i18n = this.translator.translate.bind(this.translator);
333 this.i18nArray = this.translator.translateArray.bind(this.translator);
334 };
335
336 _proto.setOptions = function setOptions(newOpts) {
337 this.opts = _extends({}, this.opts, newOpts, {
338 restrictions: _extends({}, this.opts.restrictions, newOpts && newOpts.restrictions)
339 });
340
341 if (newOpts.meta) {
342 this.setMeta(newOpts.meta);
343 }
344
345 this.i18nInit();
346
347 if (newOpts.locale) {
348 this.iteratePlugins(function (plugin) {
349 plugin.setOptions();
350 });
351 }
352
353 this.setState(); // so that UI re-renders with new options
354 };
355
356 _proto.resetProgress = function resetProgress() {
357 var defaultProgress = {
358 percentage: 0,
359 bytesUploaded: 0,
360 uploadComplete: false,
361 uploadStarted: null
362 };
363
364 var files = _extends({}, this.getState().files);
365
366 var updatedFiles = {};
367 Object.keys(files).forEach(function (fileID) {
368 var updatedFile = _extends({}, files[fileID]);
369
370 updatedFile.progress = _extends({}, updatedFile.progress, defaultProgress);
371 updatedFiles[fileID] = updatedFile;
372 });
373 this.setState({
374 files: updatedFiles,
375 totalProgress: 0
376 });
377 this.emit('reset-progress');
378 };
379
380 _proto.addPreProcessor = function addPreProcessor(fn) {
381 this.preProcessors.push(fn);
382 };
383
384 _proto.removePreProcessor = function removePreProcessor(fn) {
385 var i = this.preProcessors.indexOf(fn);
386
387 if (i !== -1) {
388 this.preProcessors.splice(i, 1);
389 }
390 };
391
392 _proto.addPostProcessor = function addPostProcessor(fn) {
393 this.postProcessors.push(fn);
394 };
395
396 _proto.removePostProcessor = function removePostProcessor(fn) {
397 var i = this.postProcessors.indexOf(fn);
398
399 if (i !== -1) {
400 this.postProcessors.splice(i, 1);
401 }
402 };
403
404 _proto.addUploader = function addUploader(fn) {
405 this.uploaders.push(fn);
406 };
407
408 _proto.removeUploader = function removeUploader(fn) {
409 var i = this.uploaders.indexOf(fn);
410
411 if (i !== -1) {
412 this.uploaders.splice(i, 1);
413 }
414 };
415
416 _proto.setMeta = function setMeta(data) {
417 var updatedMeta = _extends({}, this.getState().meta, data);
418
419 var updatedFiles = _extends({}, this.getState().files);
420
421 Object.keys(updatedFiles).forEach(function (fileID) {
422 updatedFiles[fileID] = _extends({}, updatedFiles[fileID], {
423 meta: _extends({}, updatedFiles[fileID].meta, data)
424 });
425 });
426 this.log('Adding metadata:');
427 this.log(data);
428 this.setState({
429 meta: updatedMeta,
430 files: updatedFiles
431 });
432 };
433
434 _proto.setFileMeta = function setFileMeta(fileID, data) {
435 var updatedFiles = _extends({}, this.getState().files);
436
437 if (!updatedFiles[fileID]) {
438 this.log('Was trying to set metadata for a file that has been removed: ', fileID);
439 return;
440 }
441
442 var newMeta = _extends({}, updatedFiles[fileID].meta, data);
443
444 updatedFiles[fileID] = _extends({}, updatedFiles[fileID], {
445 meta: newMeta
446 });
447 this.setState({
448 files: updatedFiles
449 });
450 }
451 /**
452 * Get a file object.
453 *
454 * @param {string} fileID The ID of the file object to return.
455 */
456 ;
457
458 _proto.getFile = function getFile(fileID) {
459 return this.getState().files[fileID];
460 }
461 /**
462 * Get all files in an array.
463 */
464 ;
465
466 _proto.getFiles = function getFiles() {
467 var _this$getState = this.getState(),
468 files = _this$getState.files;
469
470 return Object.keys(files).map(function (fileID) {
471 return files[fileID];
472 });
473 }
474 /**
475 * A public wrapper for _checkRestrictions — checks if a file passes a set of restrictions.
476 * For use in UI pluigins (like Providers), to disallow selecting files that won’t pass restrictions.
477 *
478 * @param {object} file object to check
479 * @param {Array} [files] array to check maxNumberOfFiles and maxTotalFileSize
480 * @returns {object} { result: true/false, reason: why file didn’t pass restrictions }
481 */
482 ;
483
484 _proto.validateRestrictions = function validateRestrictions(file, files) {
485 try {
486 this._checkRestrictions(file, files);
487
488 return {
489 result: true
490 };
491 } catch (err) {
492 return {
493 result: false,
494 reason: err.message
495 };
496 }
497 }
498 /**
499 * Check if file passes a set of restrictions set in options: maxFileSize, minFileSize,
500 * maxNumberOfFiles and allowedFileTypes.
501 *
502 * @param {object} file object to check
503 * @param {Array} [files] array to check maxNumberOfFiles and maxTotalFileSize
504 * @private
505 */
506 ;
507
508 _proto._checkRestrictions = function _checkRestrictions(file, files) {
509 if (files === void 0) {
510 files = this.getFiles();
511 }
512
513 var _this$opts$restrictio = this.opts.restrictions,
514 maxFileSize = _this$opts$restrictio.maxFileSize,
515 minFileSize = _this$opts$restrictio.minFileSize,
516 maxTotalFileSize = _this$opts$restrictio.maxTotalFileSize,
517 maxNumberOfFiles = _this$opts$restrictio.maxNumberOfFiles,
518 allowedFileTypes = _this$opts$restrictio.allowedFileTypes;
519
520 if (maxNumberOfFiles) {
521 if (files.length + 1 > maxNumberOfFiles) {
522 throw new RestrictionError("" + this.i18n('youCanOnlyUploadX', {
523 smart_count: maxNumberOfFiles
524 }));
525 }
526 }
527
528 if (allowedFileTypes) {
529 var isCorrectFileType = allowedFileTypes.some(function (type) {
530 // check if this is a mime-type
531 if (type.indexOf('/') > -1) {
532 if (!file.type) return false;
533 return match(file.type.replace(/;.*?$/, ''), type);
534 } // otherwise this is likely an extension
535
536
537 if (type[0] === '.' && file.extension) {
538 return file.extension.toLowerCase() === type.substr(1).toLowerCase();
539 }
540
541 return false;
542 });
543
544 if (!isCorrectFileType) {
545 var allowedFileTypesString = allowedFileTypes.join(', ');
546 throw new RestrictionError(this.i18n('youCanOnlyUploadFileTypes', {
547 types: allowedFileTypesString
548 }));
549 }
550 } // We can't check maxTotalFileSize if the size is unknown.
551
552
553 if (maxTotalFileSize && file.size != null) {
554 var totalFilesSize = 0;
555 totalFilesSize += file.size;
556 files.forEach(function (file) {
557 totalFilesSize += file.size;
558 });
559
560 if (totalFilesSize > maxTotalFileSize) {
561 throw new RestrictionError(this.i18n('exceedsSize2', {
562 backwardsCompat: this.i18n('exceedsSize'),
563 size: prettierBytes(maxTotalFileSize)
564 }));
565 }
566 } // We can't check maxFileSize if the size is unknown.
567
568
569 if (maxFileSize && file.size != null) {
570 if (file.size > maxFileSize) {
571 throw new RestrictionError(this.i18n('exceedsSize2', {
572 backwardsCompat: this.i18n('exceedsSize'),
573 size: prettierBytes(maxFileSize)
574 }));
575 }
576 } // We can't check minFileSize if the size is unknown.
577
578
579 if (minFileSize && file.size != null) {
580 if (file.size < minFileSize) {
581 throw new RestrictionError(this.i18n('inferiorSize', {
582 size: prettierBytes(minFileSize)
583 }));
584 }
585 }
586 }
587 /**
588 * Check if minNumberOfFiles restriction is reached before uploading.
589 *
590 * @private
591 */
592 ;
593
594 _proto._checkMinNumberOfFiles = function _checkMinNumberOfFiles(files) {
595 var minNumberOfFiles = this.opts.restrictions.minNumberOfFiles;
596
597 if (Object.keys(files).length < minNumberOfFiles) {
598 throw new RestrictionError("" + this.i18n('youHaveToAtLeastSelectX', {
599 smart_count: minNumberOfFiles
600 }));
601 }
602 }
603 /**
604 * Logs an error, sets Informer message, then throws the error.
605 * Emits a 'restriction-failed' event if it’s a restriction error
606 *
607 * @param {object | string} err — Error object or plain string message
608 * @param {object} [options]
609 * @param {boolean} [options.showInformer=true] — Sometimes developer might want to show Informer manually
610 * @param {object} [options.file=null] — File object used to emit the restriction error
611 * @param {boolean} [options.throwErr=true] — Errors shouldn’t be thrown, for example, in `upload-error` event
612 * @private
613 */
614 ;
615
616 _proto._showOrLogErrorAndThrow = function _showOrLogErrorAndThrow(err, _temp) {
617 var _ref = _temp === void 0 ? {} : _temp,
618 _ref$showInformer = _ref.showInformer,
619 showInformer = _ref$showInformer === void 0 ? true : _ref$showInformer,
620 _ref$file = _ref.file,
621 file = _ref$file === void 0 ? null : _ref$file,
622 _ref$throwErr = _ref.throwErr,
623 throwErr = _ref$throwErr === void 0 ? true : _ref$throwErr;
624
625 var message = typeof err === 'object' ? err.message : err;
626 var details = typeof err === 'object' && err.details ? err.details : ''; // Restriction errors should be logged, but not as errors,
627 // as they are expected and shown in the UI.
628
629 var logMessageWithDetails = message;
630
631 if (details) {
632 logMessageWithDetails += ' ' + details;
633 }
634
635 if (err.isRestriction) {
636 this.log(logMessageWithDetails);
637 this.emit('restriction-failed', file, err);
638 } else {
639 this.log(logMessageWithDetails, 'error');
640 } // Sometimes informer has to be shown manually by the developer,
641 // for example, in `onBeforeFileAdded`.
642
643
644 if (showInformer) {
645 this.info({
646 message: message,
647 details: details
648 }, 'error', this.opts.infoTimeout);
649 }
650
651 if (throwErr) {
652 throw typeof err === 'object' ? err : new Error(err);
653 }
654 };
655
656 _proto._assertNewUploadAllowed = function _assertNewUploadAllowed(file) {
657 var _this$getState2 = this.getState(),
658 allowNewUpload = _this$getState2.allowNewUpload;
659
660 if (allowNewUpload === false) {
661 this._showOrLogErrorAndThrow(new RestrictionError(this.i18n('noNewAlreadyUploading')), {
662 file: file
663 });
664 }
665 }
666 /**
667 * Create a file state object based on user-provided `addFile()` options.
668 *
669 * Note this is extremely side-effectful and should only be done when a file state object will be added to state immediately afterward!
670 *
671 * The `files` value is passed in because it may be updated by the caller without updating the store.
672 */
673 ;
674
675 _proto._checkAndCreateFileStateObject = function _checkAndCreateFileStateObject(files, file) {
676 var fileType = getFileType(file);
677 file.type = fileType;
678 var onBeforeFileAddedResult = this.opts.onBeforeFileAdded(file, files);
679
680 if (onBeforeFileAddedResult === false) {
681 // Don’t show UI info for this error, as it should be done by the developer
682 this._showOrLogErrorAndThrow(new RestrictionError('Cannot add the file because onBeforeFileAdded returned false.'), {
683 showInformer: false,
684 file: file
685 });
686 }
687
688 if (typeof onBeforeFileAddedResult === 'object' && onBeforeFileAddedResult) {
689 file = onBeforeFileAddedResult;
690 }
691
692 var fileName;
693
694 if (file.name) {
695 fileName = file.name;
696 } else if (fileType.split('/')[0] === 'image') {
697 fileName = fileType.split('/')[0] + '.' + fileType.split('/')[1];
698 } else {
699 fileName = 'noname';
700 }
701
702 var fileExtension = getFileNameAndExtension(fileName).extension;
703 var isRemote = file.isRemote || false;
704 var fileID = generateFileID(file);
705
706 if (files[fileID]) {
707 this._showOrLogErrorAndThrow(new RestrictionError(this.i18n('noDuplicates', {
708 fileName: fileName
709 })), {
710 file: file
711 });
712 }
713
714 var meta = file.meta || {};
715 meta.name = fileName;
716 meta.type = fileType; // `null` means the size is unknown.
717
718 var size = isFinite(file.data.size) ? file.data.size : null;
719 var newFile = {
720 source: file.source || '',
721 id: fileID,
722 name: fileName,
723 extension: fileExtension || '',
724 meta: _extends({}, this.getState().meta, meta),
725 type: fileType,
726 data: file.data,
727 progress: {
728 percentage: 0,
729 bytesUploaded: 0,
730 bytesTotal: size,
731 uploadComplete: false,
732 uploadStarted: null
733 },
734 size: size,
735 isRemote: isRemote,
736 remote: file.remote || '',
737 preview: file.preview
738 };
739
740 try {
741 var filesArray = Object.keys(files).map(function (i) {
742 return files[i];
743 });
744
745 this._checkRestrictions(newFile, filesArray);
746 } catch (err) {
747 this._showOrLogErrorAndThrow(err, {
748 file: newFile
749 });
750 }
751
752 return newFile;
753 } // Schedule an upload if `autoProceed` is enabled.
754 ;
755
756 _proto._startIfAutoProceed = function _startIfAutoProceed() {
757 var _this3 = this;
758
759 if (this.opts.autoProceed && !this.scheduledAutoProceed) {
760 this.scheduledAutoProceed = setTimeout(function () {
761 _this3.scheduledAutoProceed = null;
762
763 _this3.upload().catch(function (err) {
764 if (!err.isRestriction) {
765 _this3.log(err.stack || err.message || err);
766 }
767 });
768 }, 4);
769 }
770 }
771 /**
772 * Add a new file to `state.files`. This will run `onBeforeFileAdded`,
773 * try to guess file type in a clever way, check file against restrictions,
774 * and start an upload if `autoProceed === true`.
775 *
776 * @param {object} file object to add
777 * @returns {string} id for the added file
778 */
779 ;
780
781 _proto.addFile = function addFile(file) {
782 var _extends3;
783
784 this._assertNewUploadAllowed(file);
785
786 var _this$getState3 = this.getState(),
787 files = _this$getState3.files;
788
789 var newFile = this._checkAndCreateFileStateObject(files, file);
790
791 this.setState({
792 files: _extends({}, files, (_extends3 = {}, _extends3[newFile.id] = newFile, _extends3))
793 });
794 this.emit('file-added', newFile);
795 this.emit('files-added', [newFile]);
796 this.log("Added file: " + newFile.name + ", " + newFile.id + ", mime type: " + newFile.type);
797
798 this._startIfAutoProceed();
799
800 return newFile.id;
801 }
802 /**
803 * Add multiple files to `state.files`. See the `addFile()` documentation.
804 *
805 * This cuts some corners for performance, so should typically only be used in cases where there may be a lot of files.
806 *
807 * If an error occurs while adding a file, it is logged and the user is notified. This is good for UI plugins, but not for programmatic use. Programmatic users should usually still use `addFile()` on individual files.
808 */
809 ;
810
811 _proto.addFiles = function addFiles(fileDescriptors) {
812 var _this4 = this;
813
814 this._assertNewUploadAllowed(); // create a copy of the files object only once
815
816
817 var files = _extends({}, this.getState().files);
818
819 var newFiles = [];
820 var errors = [];
821
822 for (var i = 0; i < fileDescriptors.length; i++) {
823 try {
824 var newFile = this._checkAndCreateFileStateObject(files, fileDescriptors[i]);
825
826 newFiles.push(newFile);
827 files[newFile.id] = newFile;
828 } catch (err) {
829 if (!err.isRestriction) {
830 errors.push(err);
831 }
832 }
833 }
834
835 this.setState({
836 files: files
837 });
838 newFiles.forEach(function (newFile) {
839 _this4.emit('file-added', newFile);
840 });
841 this.emit('files-added', newFiles);
842
843 if (newFiles.length > 5) {
844 this.log("Added batch of " + newFiles.length + " files");
845 } else {
846 Object.keys(newFiles).forEach(function (fileID) {
847 _this4.log("Added file: " + newFiles[fileID].name + "\n id: " + newFiles[fileID].id + "\n type: " + newFiles[fileID].type);
848 });
849 }
850
851 if (newFiles.length > 0) {
852 this._startIfAutoProceed();
853 }
854
855 if (errors.length > 0) {
856 var message = 'Multiple errors occurred while adding files:\n';
857 errors.forEach(function (subError) {
858 message += "\n * " + subError.message;
859 });
860 this.info({
861 message: this.i18n('addBulkFilesFailed', {
862 smart_count: errors.length
863 }),
864 details: message
865 }, 'error', this.opts.infoTimeout);
866 var err = new Error(message);
867 err.errors = errors;
868 throw err;
869 }
870 };
871
872 _proto.removeFiles = function removeFiles(fileIDs, reason) {
873 var _this5 = this;
874
875 var _this$getState4 = this.getState(),
876 files = _this$getState4.files,
877 currentUploads = _this$getState4.currentUploads;
878
879 var updatedFiles = _extends({}, files);
880
881 var updatedUploads = _extends({}, currentUploads);
882
883 var removedFiles = Object.create(null);
884 fileIDs.forEach(function (fileID) {
885 if (files[fileID]) {
886 removedFiles[fileID] = files[fileID];
887 delete updatedFiles[fileID];
888 }
889 }); // Remove files from the `fileIDs` list in each upload.
890
891 function fileIsNotRemoved(uploadFileID) {
892 return removedFiles[uploadFileID] === undefined;
893 }
894
895 var uploadsToRemove = [];
896 Object.keys(updatedUploads).forEach(function (uploadID) {
897 var newFileIDs = currentUploads[uploadID].fileIDs.filter(fileIsNotRemoved); // Remove the upload if no files are associated with it anymore.
898
899 if (newFileIDs.length === 0) {
900 uploadsToRemove.push(uploadID);
901 return;
902 }
903
904 updatedUploads[uploadID] = _extends({}, currentUploads[uploadID], {
905 fileIDs: newFileIDs
906 });
907 });
908 uploadsToRemove.forEach(function (uploadID) {
909 delete updatedUploads[uploadID];
910 });
911 var stateUpdate = {
912 currentUploads: updatedUploads,
913 files: updatedFiles
914 }; // If all files were removed - allow new uploads!
915
916 if (Object.keys(updatedFiles).length === 0) {
917 stateUpdate.allowNewUpload = true;
918 stateUpdate.error = null;
919 }
920
921 this.setState(stateUpdate);
922
923 this._calculateTotalProgress();
924
925 var removedFileIDs = Object.keys(removedFiles);
926 removedFileIDs.forEach(function (fileID) {
927 _this5.emit('file-removed', removedFiles[fileID], reason);
928 });
929
930 if (removedFileIDs.length > 5) {
931 this.log("Removed " + removedFileIDs.length + " files");
932 } else {
933 this.log("Removed files: " + removedFileIDs.join(', '));
934 }
935 };
936
937 _proto.removeFile = function removeFile(fileID, reason) {
938 if (reason === void 0) {
939 reason = null;
940 }
941
942 this.removeFiles([fileID], reason);
943 };
944
945 _proto.pauseResume = function pauseResume(fileID) {
946 if (!this.getState().capabilities.resumableUploads || this.getFile(fileID).uploadComplete) {
947 return;
948 }
949
950 var wasPaused = this.getFile(fileID).isPaused || false;
951 var isPaused = !wasPaused;
952 this.setFileState(fileID, {
953 isPaused: isPaused
954 });
955 this.emit('upload-pause', fileID, isPaused);
956 return isPaused;
957 };
958
959 _proto.pauseAll = function pauseAll() {
960 var updatedFiles = _extends({}, this.getState().files);
961
962 var inProgressUpdatedFiles = Object.keys(updatedFiles).filter(function (file) {
963 return !updatedFiles[file].progress.uploadComplete && updatedFiles[file].progress.uploadStarted;
964 });
965 inProgressUpdatedFiles.forEach(function (file) {
966 var updatedFile = _extends({}, updatedFiles[file], {
967 isPaused: true
968 });
969
970 updatedFiles[file] = updatedFile;
971 });
972 this.setState({
973 files: updatedFiles
974 });
975 this.emit('pause-all');
976 };
977
978 _proto.resumeAll = function resumeAll() {
979 var updatedFiles = _extends({}, this.getState().files);
980
981 var inProgressUpdatedFiles = Object.keys(updatedFiles).filter(function (file) {
982 return !updatedFiles[file].progress.uploadComplete && updatedFiles[file].progress.uploadStarted;
983 });
984 inProgressUpdatedFiles.forEach(function (file) {
985 var updatedFile = _extends({}, updatedFiles[file], {
986 isPaused: false,
987 error: null
988 });
989
990 updatedFiles[file] = updatedFile;
991 });
992 this.setState({
993 files: updatedFiles
994 });
995 this.emit('resume-all');
996 };
997
998 _proto.retryAll = function retryAll() {
999 var updatedFiles = _extends({}, this.getState().files);
1000
1001 var filesToRetry = Object.keys(updatedFiles).filter(function (file) {
1002 return updatedFiles[file].error;
1003 });
1004 filesToRetry.forEach(function (file) {
1005 var updatedFile = _extends({}, updatedFiles[file], {
1006 isPaused: false,
1007 error: null
1008 });
1009
1010 updatedFiles[file] = updatedFile;
1011 });
1012 this.setState({
1013 files: updatedFiles,
1014 error: null
1015 });
1016 this.emit('retry-all', filesToRetry);
1017
1018 if (filesToRetry.length === 0) {
1019 return Promise.resolve({
1020 successful: [],
1021 failed: []
1022 });
1023 }
1024
1025 var uploadID = this._createUpload(filesToRetry, {
1026 forceAllowNewUpload: true // create new upload even if allowNewUpload: false
1027
1028 });
1029
1030 return this._runUpload(uploadID);
1031 };
1032
1033 _proto.cancelAll = function cancelAll() {
1034 this.emit('cancel-all');
1035
1036 var _this$getState5 = this.getState(),
1037 files = _this$getState5.files;
1038
1039 var fileIDs = Object.keys(files);
1040
1041 if (fileIDs.length) {
1042 this.removeFiles(fileIDs, 'cancel-all');
1043 }
1044
1045 this.setState({
1046 totalProgress: 0,
1047 error: null
1048 });
1049 };
1050
1051 _proto.retryUpload = function retryUpload(fileID) {
1052 this.setFileState(fileID, {
1053 error: null,
1054 isPaused: false
1055 });
1056 this.emit('upload-retry', fileID);
1057
1058 var uploadID = this._createUpload([fileID], {
1059 forceAllowNewUpload: true // create new upload even if allowNewUpload: false
1060
1061 });
1062
1063 return this._runUpload(uploadID);
1064 };
1065
1066 _proto.reset = function reset() {
1067 this.cancelAll();
1068 };
1069
1070 _proto._calculateProgress = function _calculateProgress(file, data) {
1071 if (!this.getFile(file.id)) {
1072 this.log("Not setting progress for a file that has been removed: " + file.id);
1073 return;
1074 } // bytesTotal may be null or zero; in that case we can't divide by it
1075
1076
1077 var canHavePercentage = isFinite(data.bytesTotal) && data.bytesTotal > 0;
1078 this.setFileState(file.id, {
1079 progress: _extends({}, this.getFile(file.id).progress, {
1080 bytesUploaded: data.bytesUploaded,
1081 bytesTotal: data.bytesTotal,
1082 percentage: canHavePercentage // TODO(goto-bus-stop) flooring this should probably be the choice of the UI?
1083 // we get more accurate calculations if we don't round this at all.
1084 ? Math.round(data.bytesUploaded / data.bytesTotal * 100) : 0
1085 })
1086 });
1087
1088 this._calculateTotalProgress();
1089 };
1090
1091 _proto._calculateTotalProgress = function _calculateTotalProgress() {
1092 // calculate total progress, using the number of files currently uploading,
1093 // multiplied by 100 and the summ of individual progress of each file
1094 var files = this.getFiles();
1095 var inProgress = files.filter(function (file) {
1096 return file.progress.uploadStarted || file.progress.preprocess || file.progress.postprocess;
1097 });
1098
1099 if (inProgress.length === 0) {
1100 this.emit('progress', 0);
1101 this.setState({
1102 totalProgress: 0
1103 });
1104 return;
1105 }
1106
1107 var sizedFiles = inProgress.filter(function (file) {
1108 return file.progress.bytesTotal != null;
1109 });
1110 var unsizedFiles = inProgress.filter(function (file) {
1111 return file.progress.bytesTotal == null;
1112 });
1113
1114 if (sizedFiles.length === 0) {
1115 var progressMax = inProgress.length * 100;
1116 var currentProgress = unsizedFiles.reduce(function (acc, file) {
1117 return acc + file.progress.percentage;
1118 }, 0);
1119
1120 var _totalProgress = Math.round(currentProgress / progressMax * 100);
1121
1122 this.setState({
1123 totalProgress: _totalProgress
1124 });
1125 return;
1126 }
1127
1128 var totalSize = sizedFiles.reduce(function (acc, file) {
1129 return acc + file.progress.bytesTotal;
1130 }, 0);
1131 var averageSize = totalSize / sizedFiles.length;
1132 totalSize += averageSize * unsizedFiles.length;
1133 var uploadedSize = 0;
1134 sizedFiles.forEach(function (file) {
1135 uploadedSize += file.progress.bytesUploaded;
1136 });
1137 unsizedFiles.forEach(function (file) {
1138 uploadedSize += averageSize * (file.progress.percentage || 0) / 100;
1139 });
1140 var totalProgress = totalSize === 0 ? 0 : Math.round(uploadedSize / totalSize * 100); // hot fix, because:
1141 // uploadedSize ended up larger than totalSize, resulting in 1325% total
1142
1143 if (totalProgress > 100) {
1144 totalProgress = 100;
1145 }
1146
1147 this.setState({
1148 totalProgress: totalProgress
1149 });
1150 this.emit('progress', totalProgress);
1151 }
1152 /**
1153 * Registers listeners for all global actions, like:
1154 * `error`, `file-removed`, `upload-progress`
1155 */
1156 ;
1157
1158 _proto._addListeners = function _addListeners() {
1159 var _this6 = this;
1160
1161 this.on('error', function (error) {
1162 var errorMsg = 'Unknown error';
1163
1164 if (error.message) {
1165 errorMsg = error.message;
1166 }
1167
1168 if (error.details) {
1169 errorMsg += ' ' + error.details;
1170 }
1171
1172 _this6.setState({
1173 error: errorMsg
1174 });
1175 });
1176 this.on('upload-error', function (file, error, response) {
1177 var errorMsg = 'Unknown error';
1178
1179 if (error.message) {
1180 errorMsg = error.message;
1181 }
1182
1183 if (error.details) {
1184 errorMsg += ' ' + error.details;
1185 }
1186
1187 _this6.setFileState(file.id, {
1188 error: errorMsg,
1189 response: response
1190 });
1191
1192 _this6.setState({
1193 error: error.message
1194 });
1195
1196 if (typeof error === 'object' && error.message) {
1197 var newError = new Error(error.message);
1198 newError.details = error.message;
1199
1200 if (error.details) {
1201 newError.details += ' ' + error.details;
1202 }
1203
1204 newError.message = _this6.i18n('failedToUpload', {
1205 file: file.name
1206 });
1207
1208 _this6._showOrLogErrorAndThrow(newError, {
1209 throwErr: false
1210 });
1211 } else {
1212 _this6._showOrLogErrorAndThrow(error, {
1213 throwErr: false
1214 });
1215 }
1216 });
1217 this.on('upload', function () {
1218 _this6.setState({
1219 error: null
1220 });
1221 });
1222 this.on('upload-started', function (file, upload) {
1223 if (!_this6.getFile(file.id)) {
1224 _this6.log("Not setting progress for a file that has been removed: " + file.id);
1225
1226 return;
1227 }
1228
1229 _this6.setFileState(file.id, {
1230 progress: {
1231 uploadStarted: Date.now(),
1232 uploadComplete: false,
1233 percentage: 0,
1234 bytesUploaded: 0,
1235 bytesTotal: file.size
1236 }
1237 });
1238 });
1239 this.on('upload-progress', this._calculateProgress);
1240 this.on('upload-success', function (file, uploadResp) {
1241 if (!_this6.getFile(file.id)) {
1242 _this6.log("Not setting progress for a file that has been removed: " + file.id);
1243
1244 return;
1245 }
1246
1247 var currentProgress = _this6.getFile(file.id).progress;
1248
1249 _this6.setFileState(file.id, {
1250 progress: _extends({}, currentProgress, {
1251 postprocess: _this6.postProcessors.length > 0 ? {
1252 mode: 'indeterminate'
1253 } : null,
1254 uploadComplete: true,
1255 percentage: 100,
1256 bytesUploaded: currentProgress.bytesTotal
1257 }),
1258 response: uploadResp,
1259 uploadURL: uploadResp.uploadURL,
1260 isPaused: false
1261 });
1262
1263 _this6._calculateTotalProgress();
1264 });
1265 this.on('preprocess-progress', function (file, progress) {
1266 if (!_this6.getFile(file.id)) {
1267 _this6.log("Not setting progress for a file that has been removed: " + file.id);
1268
1269 return;
1270 }
1271
1272 _this6.setFileState(file.id, {
1273 progress: _extends({}, _this6.getFile(file.id).progress, {
1274 preprocess: progress
1275 })
1276 });
1277 });
1278 this.on('preprocess-complete', function (file) {
1279 if (!_this6.getFile(file.id)) {
1280 _this6.log("Not setting progress for a file that has been removed: " + file.id);
1281
1282 return;
1283 }
1284
1285 var files = _extends({}, _this6.getState().files);
1286
1287 files[file.id] = _extends({}, files[file.id], {
1288 progress: _extends({}, files[file.id].progress)
1289 });
1290 delete files[file.id].progress.preprocess;
1291
1292 _this6.setState({
1293 files: files
1294 });
1295 });
1296 this.on('postprocess-progress', function (file, progress) {
1297 if (!_this6.getFile(file.id)) {
1298 _this6.log("Not setting progress for a file that has been removed: " + file.id);
1299
1300 return;
1301 }
1302
1303 _this6.setFileState(file.id, {
1304 progress: _extends({}, _this6.getState().files[file.id].progress, {
1305 postprocess: progress
1306 })
1307 });
1308 });
1309 this.on('postprocess-complete', function (file) {
1310 if (!_this6.getFile(file.id)) {
1311 _this6.log("Not setting progress for a file that has been removed: " + file.id);
1312
1313 return;
1314 }
1315
1316 var files = _extends({}, _this6.getState().files);
1317
1318 files[file.id] = _extends({}, files[file.id], {
1319 progress: _extends({}, files[file.id].progress)
1320 });
1321 delete files[file.id].progress.postprocess; // TODO should we set some kind of `fullyComplete` property on the file object
1322 // so it's easier to see that the file is upload…fully complete…rather than
1323 // what we have to do now (`uploadComplete && !postprocess`)
1324
1325 _this6.setState({
1326 files: files
1327 });
1328 });
1329 this.on('restored', function () {
1330 // Files may have changed--ensure progress is still accurate.
1331 _this6._calculateTotalProgress();
1332 }); // show informer if offline
1333
1334 if (typeof window !== 'undefined' && window.addEventListener) {
1335 window.addEventListener('online', function () {
1336 return _this6.updateOnlineStatus();
1337 });
1338 window.addEventListener('offline', function () {
1339 return _this6.updateOnlineStatus();
1340 });
1341 setTimeout(function () {
1342 return _this6.updateOnlineStatus();
1343 }, 3000);
1344 }
1345 };
1346
1347 _proto.updateOnlineStatus = function updateOnlineStatus() {
1348 var online = typeof window.navigator.onLine !== 'undefined' ? window.navigator.onLine : true;
1349
1350 if (!online) {
1351 this.emit('is-offline');
1352 this.info(this.i18n('noInternetConnection'), 'error', 0);
1353 this.wasOffline = true;
1354 } else {
1355 this.emit('is-online');
1356
1357 if (this.wasOffline) {
1358 this.emit('back-online');
1359 this.info(this.i18n('connectedToInternet'), 'success', 3000);
1360 this.wasOffline = false;
1361 }
1362 }
1363 };
1364
1365 _proto.getID = function getID() {
1366 return this.opts.id;
1367 }
1368 /**
1369 * Registers a plugin with Core.
1370 *
1371 * @param {object} Plugin object
1372 * @param {object} [opts] object with options to be passed to Plugin
1373 * @returns {object} self for chaining
1374 */
1375 ;
1376
1377 _proto.use = function use(Plugin, opts) {
1378 if (typeof Plugin !== 'function') {
1379 var msg = "Expected a plugin class, but got " + (Plugin === null ? 'null' : typeof Plugin) + "." + ' Please verify that the plugin was imported and spelled correctly.';
1380 throw new TypeError(msg);
1381 } // Instantiate
1382
1383
1384 var plugin = new Plugin(this, opts);
1385 var pluginId = plugin.id;
1386 this.plugins[plugin.type] = this.plugins[plugin.type] || [];
1387
1388 if (!pluginId) {
1389 throw new Error('Your plugin must have an id');
1390 }
1391
1392 if (!plugin.type) {
1393 throw new Error('Your plugin must have a type');
1394 }
1395
1396 var existsPluginAlready = this.getPlugin(pluginId);
1397
1398 if (existsPluginAlready) {
1399 var _msg = "Already found a plugin named '" + existsPluginAlready.id + "'. " + ("Tried to use: '" + pluginId + "'.\n") + 'Uppy plugins must have unique `id` options. See https://uppy.io/docs/plugins/#id.';
1400
1401 throw new Error(_msg);
1402 }
1403
1404 if (Plugin.VERSION) {
1405 this.log("Using " + pluginId + " v" + Plugin.VERSION);
1406 }
1407
1408 this.plugins[plugin.type].push(plugin);
1409 plugin.install();
1410 return this;
1411 }
1412 /**
1413 * Find one Plugin by name.
1414 *
1415 * @param {string} id plugin id
1416 * @returns {object|boolean}
1417 */
1418 ;
1419
1420 _proto.getPlugin = function getPlugin(id) {
1421 var foundPlugin = null;
1422 this.iteratePlugins(function (plugin) {
1423 if (plugin.id === id) {
1424 foundPlugin = plugin;
1425 return false;
1426 }
1427 });
1428 return foundPlugin;
1429 }
1430 /**
1431 * Iterate through all `use`d plugins.
1432 *
1433 * @param {Function} method that will be run on each plugin
1434 */
1435 ;
1436
1437 _proto.iteratePlugins = function iteratePlugins(method) {
1438 var _this7 = this;
1439
1440 Object.keys(this.plugins).forEach(function (pluginType) {
1441 _this7.plugins[pluginType].forEach(method);
1442 });
1443 }
1444 /**
1445 * Uninstall and remove a plugin.
1446 *
1447 * @param {object} instance The plugin instance to remove.
1448 */
1449 ;
1450
1451 _proto.removePlugin = function removePlugin(instance) {
1452 this.log("Removing plugin " + instance.id);
1453 this.emit('plugin-remove', instance);
1454
1455 if (instance.uninstall) {
1456 instance.uninstall();
1457 }
1458
1459 var list = this.plugins[instance.type].slice();
1460 var index = list.indexOf(instance);
1461
1462 if (index !== -1) {
1463 list.splice(index, 1);
1464 this.plugins[instance.type] = list;
1465 }
1466
1467 var updatedState = this.getState();
1468 delete updatedState.plugins[instance.id];
1469 this.setState(updatedState);
1470 }
1471 /**
1472 * Uninstall all plugins and close down this Uppy instance.
1473 */
1474 ;
1475
1476 _proto.close = function close() {
1477 var _this8 = this;
1478
1479 this.log("Closing Uppy instance " + this.opts.id + ": removing all files and uninstalling plugins");
1480 this.reset();
1481
1482 this._storeUnsubscribe();
1483
1484 this.iteratePlugins(function (plugin) {
1485 _this8.removePlugin(plugin);
1486 });
1487 }
1488 /**
1489 * Set info message in `state.info`, so that UI plugins like `Informer`
1490 * can display the message.
1491 *
1492 * @param {string | object} message Message to be displayed by the informer
1493 * @param {string} [type]
1494 * @param {number} [duration]
1495 */
1496 ;
1497
1498 _proto.info = function info(message, type, duration) {
1499 if (type === void 0) {
1500 type = 'info';
1501 }
1502
1503 if (duration === void 0) {
1504 duration = 3000;
1505 }
1506
1507 var isComplexMessage = typeof message === 'object';
1508 this.setState({
1509 info: {
1510 isHidden: false,
1511 type: type,
1512 message: isComplexMessage ? message.message : message,
1513 details: isComplexMessage ? message.details : null
1514 }
1515 });
1516 this.emit('info-visible');
1517 clearTimeout(this.infoTimeoutID);
1518
1519 if (duration === 0) {
1520 this.infoTimeoutID = undefined;
1521 return;
1522 } // hide the informer after `duration` milliseconds
1523
1524
1525 this.infoTimeoutID = setTimeout(this.hideInfo, duration);
1526 };
1527
1528 _proto.hideInfo = function hideInfo() {
1529 var newInfo = _extends({}, this.getState().info, {
1530 isHidden: true
1531 });
1532
1533 this.setState({
1534 info: newInfo
1535 });
1536 this.emit('info-hidden');
1537 }
1538 /**
1539 * Passes messages to a function, provided in `opts.logger`.
1540 * If `opts.logger: Uppy.debugLogger` or `opts.debug: true`, logs to the browser console.
1541 *
1542 * @param {string|object} message to log
1543 * @param {string} [type] optional `error` or `warning`
1544 */
1545 ;
1546
1547 _proto.log = function log(message, type) {
1548 var logger = this.opts.logger;
1549
1550 switch (type) {
1551 case 'error':
1552 logger.error(message);
1553 break;
1554
1555 case 'warning':
1556 logger.warn(message);
1557 break;
1558
1559 default:
1560 logger.debug(message);
1561 break;
1562 }
1563 }
1564 /**
1565 * Obsolete, event listeners are now added in the constructor.
1566 */
1567 ;
1568
1569 _proto.run = function run() {
1570 this.log('Calling run() is no longer necessary.', 'warning');
1571 return this;
1572 }
1573 /**
1574 * Restore an upload by its ID.
1575 */
1576 ;
1577
1578 _proto.restore = function restore(uploadID) {
1579 this.log("Core: attempting to restore upload \"" + uploadID + "\"");
1580
1581 if (!this.getState().currentUploads[uploadID]) {
1582 this._removeUpload(uploadID);
1583
1584 return Promise.reject(new Error('Nonexistent upload'));
1585 }
1586
1587 return this._runUpload(uploadID);
1588 }
1589 /**
1590 * Create an upload for a bunch of files.
1591 *
1592 * @param {Array<string>} fileIDs File IDs to include in this upload.
1593 * @returns {string} ID of this upload.
1594 */
1595 ;
1596
1597 _proto._createUpload = function _createUpload(fileIDs, opts) {
1598 var _extends4;
1599
1600 if (opts === void 0) {
1601 opts = {};
1602 }
1603
1604 var _opts = opts,
1605 _opts$forceAllowNewUp = _opts.forceAllowNewUpload,
1606 forceAllowNewUpload = _opts$forceAllowNewUp === void 0 ? false : _opts$forceAllowNewUp;
1607
1608 var _this$getState6 = this.getState(),
1609 allowNewUpload = _this$getState6.allowNewUpload,
1610 currentUploads = _this$getState6.currentUploads;
1611
1612 if (!allowNewUpload && !forceAllowNewUpload) {
1613 throw new Error('Cannot create a new upload: already uploading.');
1614 }
1615
1616 var uploadID = cuid();
1617 this.emit('upload', {
1618 id: uploadID,
1619 fileIDs: fileIDs
1620 });
1621 this.setState({
1622 allowNewUpload: this.opts.allowMultipleUploads !== false,
1623 currentUploads: _extends({}, currentUploads, (_extends4 = {}, _extends4[uploadID] = {
1624 fileIDs: fileIDs,
1625 step: 0,
1626 result: {}
1627 }, _extends4))
1628 });
1629 return uploadID;
1630 };
1631
1632 _proto._getUpload = function _getUpload(uploadID) {
1633 var _this$getState7 = this.getState(),
1634 currentUploads = _this$getState7.currentUploads;
1635
1636 return currentUploads[uploadID];
1637 }
1638 /**
1639 * Add data to an upload's result object.
1640 *
1641 * @param {string} uploadID The ID of the upload.
1642 * @param {object} data Data properties to add to the result object.
1643 */
1644 ;
1645
1646 _proto.addResultData = function addResultData(uploadID, data) {
1647 var _extends5;
1648
1649 if (!this._getUpload(uploadID)) {
1650 this.log("Not setting result for an upload that has been removed: " + uploadID);
1651 return;
1652 }
1653
1654 var currentUploads = this.getState().currentUploads;
1655
1656 var currentUpload = _extends({}, currentUploads[uploadID], {
1657 result: _extends({}, currentUploads[uploadID].result, data)
1658 });
1659
1660 this.setState({
1661 currentUploads: _extends({}, currentUploads, (_extends5 = {}, _extends5[uploadID] = currentUpload, _extends5))
1662 });
1663 }
1664 /**
1665 * Remove an upload, eg. if it has been canceled or completed.
1666 *
1667 * @param {string} uploadID The ID of the upload.
1668 */
1669 ;
1670
1671 _proto._removeUpload = function _removeUpload(uploadID) {
1672 var currentUploads = _extends({}, this.getState().currentUploads);
1673
1674 delete currentUploads[uploadID];
1675 this.setState({
1676 currentUploads: currentUploads
1677 });
1678 }
1679 /**
1680 * Run an upload. This picks up where it left off in case the upload is being restored.
1681 *
1682 * @private
1683 */
1684 ;
1685
1686 _proto._runUpload = function _runUpload(uploadID) {
1687 var _this9 = this;
1688
1689 var uploadData = this.getState().currentUploads[uploadID];
1690 var restoreStep = uploadData.step;
1691 var steps = [].concat(this.preProcessors, this.uploaders, this.postProcessors);
1692 var lastStep = Promise.resolve();
1693 steps.forEach(function (fn, step) {
1694 // Skip this step if we are restoring and have already completed this step before.
1695 if (step < restoreStep) {
1696 return;
1697 }
1698
1699 lastStep = lastStep.then(function () {
1700 var _extends6;
1701
1702 var _this9$getState = _this9.getState(),
1703 currentUploads = _this9$getState.currentUploads;
1704
1705 var currentUpload = currentUploads[uploadID];
1706
1707 if (!currentUpload) {
1708 return;
1709 }
1710
1711 var updatedUpload = _extends({}, currentUpload, {
1712 step: step
1713 });
1714
1715 _this9.setState({
1716 currentUploads: _extends({}, currentUploads, (_extends6 = {}, _extends6[uploadID] = updatedUpload, _extends6))
1717 }); // TODO give this the `updatedUpload` object as its only parameter maybe?
1718 // Otherwise when more metadata may be added to the upload this would keep getting more parameters
1719
1720
1721 return fn(updatedUpload.fileIDs, uploadID);
1722 }).then(function (result) {
1723 return null;
1724 });
1725 }); // Not returning the `catch`ed promise, because we still want to return a rejected
1726 // promise from this method if the upload failed.
1727
1728 lastStep.catch(function (err) {
1729 _this9.emit('error', err, uploadID);
1730
1731 _this9._removeUpload(uploadID);
1732 });
1733 return lastStep.then(function () {
1734 // Set result data.
1735 var _this9$getState2 = _this9.getState(),
1736 currentUploads = _this9$getState2.currentUploads;
1737
1738 var currentUpload = currentUploads[uploadID];
1739
1740 if (!currentUpload) {
1741 return;
1742 } // Mark postprocessing step as complete if necessary; this addresses a case where we might get
1743 // stuck in the postprocessing UI while the upload is fully complete.
1744 // If the postprocessing steps do not do any work, they may not emit postprocessing events at
1745 // all, and never mark the postprocessing as complete. This is fine on its own but we
1746 // introduced code in the @uppy/core upload-success handler to prepare postprocessing progress
1747 // state if any postprocessors are registered. That is to avoid a "flash of completed state"
1748 // before the postprocessing plugins can emit events.
1749 //
1750 // So, just in case an upload with postprocessing plugins *has* completed *without* emitting
1751 // postprocessing completion, we do it instead.
1752
1753
1754 currentUpload.fileIDs.forEach(function (fileID) {
1755 var file = _this9.getFile(fileID);
1756
1757 if (file && file.progress.postprocess) {
1758 _this9.emit('postprocess-complete', file);
1759 }
1760 });
1761 var files = currentUpload.fileIDs.map(function (fileID) {
1762 return _this9.getFile(fileID);
1763 });
1764 var successful = files.filter(function (file) {
1765 return !file.error;
1766 });
1767 var failed = files.filter(function (file) {
1768 return file.error;
1769 });
1770
1771 _this9.addResultData(uploadID, {
1772 successful: successful,
1773 failed: failed,
1774 uploadID: uploadID
1775 });
1776 }).then(function () {
1777 // Emit completion events.
1778 // This is in a separate function so that the `currentUploads` variable
1779 // always refers to the latest state. In the handler right above it refers
1780 // to an outdated object without the `.result` property.
1781 var _this9$getState3 = _this9.getState(),
1782 currentUploads = _this9$getState3.currentUploads;
1783
1784 if (!currentUploads[uploadID]) {
1785 return;
1786 }
1787
1788 var currentUpload = currentUploads[uploadID];
1789 var result = currentUpload.result;
1790
1791 _this9.emit('complete', result);
1792
1793 _this9._removeUpload(uploadID);
1794
1795 return result;
1796 }).then(function (result) {
1797 if (result == null) {
1798 _this9.log("Not setting result for an upload that has been removed: " + uploadID);
1799 }
1800
1801 return result;
1802 });
1803 }
1804 /**
1805 * Start an upload for all the files that are not currently being uploaded.
1806 *
1807 * @returns {Promise}
1808 */
1809 ;
1810
1811 _proto.upload = function upload() {
1812 var _this10 = this;
1813
1814 if (!this.plugins.uploader) {
1815 this.log('No uploader type plugins are used', 'warning');
1816 }
1817
1818 var files = this.getState().files;
1819 var onBeforeUploadResult = this.opts.onBeforeUpload(files);
1820
1821 if (onBeforeUploadResult === false) {
1822 return Promise.reject(new Error('Not starting the upload because onBeforeUpload returned false'));
1823 }
1824
1825 if (onBeforeUploadResult && typeof onBeforeUploadResult === 'object') {
1826 files = onBeforeUploadResult; // Updating files in state, because uploader plugins receive file IDs,
1827 // and then fetch the actual file object from state
1828
1829 this.setState({
1830 files: files
1831 });
1832 }
1833
1834 return Promise.resolve().then(function () {
1835 return _this10._checkMinNumberOfFiles(files);
1836 }).catch(function (err) {
1837 _this10._showOrLogErrorAndThrow(err);
1838 }).then(function () {
1839 var _this10$getState = _this10.getState(),
1840 currentUploads = _this10$getState.currentUploads; // get a list of files that are currently assigned to uploads
1841
1842
1843 var currentlyUploadingFiles = Object.keys(currentUploads).reduce(function (prev, curr) {
1844 return prev.concat(currentUploads[curr].fileIDs);
1845 }, []);
1846 var waitingFileIDs = [];
1847 Object.keys(files).forEach(function (fileID) {
1848 var file = _this10.getFile(fileID); // if the file hasn't started uploading and hasn't already been assigned to an upload..
1849
1850
1851 if (!file.progress.uploadStarted && currentlyUploadingFiles.indexOf(fileID) === -1) {
1852 waitingFileIDs.push(file.id);
1853 }
1854 });
1855
1856 var uploadID = _this10._createUpload(waitingFileIDs);
1857
1858 return _this10._runUpload(uploadID);
1859 }).catch(function (err) {
1860 _this10._showOrLogErrorAndThrow(err, {
1861 showInformer: false
1862 });
1863 });
1864 };
1865
1866 _createClass(Uppy, [{
1867 key: "state",
1868 get: function get() {
1869 return this.getState();
1870 }
1871 }]);
1872
1873 return Uppy;
1874}();
1875
1876Uppy.VERSION = "1.16.1";
1877
1878module.exports = function (opts) {
1879 return new Uppy(opts);
1880}; // Expose class constructor.
1881
1882
1883module.exports.Uppy = Uppy;
1884module.exports.Plugin = Plugin;
1885module.exports.debugLogger = debugLogger;
\No newline at end of file