1 |
|
2 | const Translator = require('@uppy/utils/lib/Translator')
|
3 | const ee = require('namespace-emitter')
|
4 | const cuid = require('cuid')
|
5 | const throttle = require('lodash.throttle')
|
6 | const prettierBytes = require('@transloadit/prettier-bytes')
|
7 | const match = require('mime-match')
|
8 | const DefaultStore = require('@uppy/store-default')
|
9 | const getFileType = require('@uppy/utils/lib/getFileType')
|
10 | const getFileNameAndExtension = require('@uppy/utils/lib/getFileNameAndExtension')
|
11 | const generateFileID = require('@uppy/utils/lib/generateFileID')
|
12 | const findIndex = require('@uppy/utils/lib/findIndex')
|
13 | const supportsUploadProgress = require('./supportsUploadProgress')
|
14 | const { justErrorsLogger, debugLogger } = require('./loggers')
|
15 | const Plugin = require('./Plugin')
|
16 |
|
17 | class RestrictionError extends Error {
|
18 | constructor (...args) {
|
19 | super(...args)
|
20 | this.isRestriction = true
|
21 | }
|
22 | }
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 | class Uppy {
|
30 | static VERSION = require('../package.json').version
|
31 |
|
32 | |
33 |
|
34 |
|
35 |
|
36 |
|
37 | constructor (opts) {
|
38 | this.defaultLocale = {
|
39 | strings: {
|
40 | addBulkFilesFailed: {
|
41 | 0: 'Failed to add %{smart_count} file due to an internal error',
|
42 | 1: 'Failed to add %{smart_count} files due to internal errors',
|
43 | },
|
44 | youCanOnlyUploadX: {
|
45 | 0: 'You can only upload %{smart_count} file',
|
46 | 1: 'You can only upload %{smart_count} files',
|
47 | },
|
48 | youHaveToAtLeastSelectX: {
|
49 | 0: 'You have to select at least %{smart_count} file',
|
50 | 1: 'You have to select at least %{smart_count} files',
|
51 | },
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 | exceedsSize2: '%{backwardsCompat} %{size}',
|
58 | exceedsSize: 'This file exceeds maximum allowed size of',
|
59 | inferiorSize: 'This file is smaller than the allowed size of %{size}',
|
60 | youCanOnlyUploadFileTypes: 'You can only upload: %{types}',
|
61 | noNewAlreadyUploading: 'Cannot add new files: already uploading',
|
62 | noDuplicates: 'Cannot add the duplicate file \'%{fileName}\', it already exists',
|
63 | companionError: 'Connection with Companion failed',
|
64 | companionUnauthorizeHint: 'To unauthorize to your %{provider} account, please go to %{url}',
|
65 | failedToUpload: 'Failed to upload %{file}',
|
66 | noInternetConnection: 'No Internet connection',
|
67 | connectedToInternet: 'Connected to the Internet',
|
68 |
|
69 | noFilesFound: 'You have no files or folders here',
|
70 | selectX: {
|
71 | 0: 'Select %{smart_count}',
|
72 | 1: 'Select %{smart_count}',
|
73 | },
|
74 | selectAllFilesFromFolderNamed: 'Select all files from folder %{name}',
|
75 | unselectAllFilesFromFolderNamed: 'Unselect all files from folder %{name}',
|
76 | selectFileNamed: 'Select file %{name}',
|
77 | unselectFileNamed: 'Unselect file %{name}',
|
78 | openFolderNamed: 'Open folder %{name}',
|
79 | cancel: 'Cancel',
|
80 | logOut: 'Log out',
|
81 | filter: 'Filter',
|
82 | resetFilter: 'Reset filter',
|
83 | loading: 'Loading...',
|
84 | authenticateWithTitle: 'Please authenticate with %{pluginName} to select files',
|
85 | authenticateWith: 'Connect to %{pluginName}',
|
86 | searchImages: 'Search for images',
|
87 | enterTextToSearch: 'Enter text to search for images',
|
88 | backToSearch: 'Back to Search',
|
89 | emptyFolderAdded: 'No files were added from empty folder',
|
90 | folderAdded: {
|
91 | 0: 'Added %{smart_count} file from %{folder}',
|
92 | 1: 'Added %{smart_count} files from %{folder}',
|
93 | },
|
94 | },
|
95 | }
|
96 |
|
97 | const defaultOptions = {
|
98 | id: 'uppy',
|
99 | autoProceed: false,
|
100 | allowMultipleUploads: true,
|
101 | debug: false,
|
102 | restrictions: {
|
103 | maxFileSize: null,
|
104 | minFileSize: null,
|
105 | maxTotalFileSize: null,
|
106 | maxNumberOfFiles: null,
|
107 | minNumberOfFiles: null,
|
108 | allowedFileTypes: null,
|
109 | },
|
110 | meta: {},
|
111 | onBeforeFileAdded: (currentFile, files) => currentFile,
|
112 | onBeforeUpload: (files) => files,
|
113 | store: DefaultStore(),
|
114 | logger: justErrorsLogger,
|
115 | infoTimeout: 5000,
|
116 | }
|
117 |
|
118 |
|
119 |
|
120 | this.opts = {
|
121 | ...defaultOptions,
|
122 | ...opts,
|
123 | restrictions: {
|
124 | ...defaultOptions.restrictions,
|
125 | ...(opts && opts.restrictions),
|
126 | },
|
127 | }
|
128 |
|
129 |
|
130 |
|
131 | if (opts && opts.logger && opts.debug) {
|
132 | 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')
|
133 | } else if (opts && opts.debug) {
|
134 | this.opts.logger = debugLogger
|
135 | }
|
136 |
|
137 | this.log(`Using Core v${this.constructor.VERSION}`)
|
138 |
|
139 | if (this.opts.restrictions.allowedFileTypes
|
140 | && this.opts.restrictions.allowedFileTypes !== null
|
141 | && !Array.isArray(this.opts.restrictions.allowedFileTypes)) {
|
142 | throw new TypeError('`restrictions.allowedFileTypes` must be an array')
|
143 | }
|
144 |
|
145 | this.i18nInit()
|
146 |
|
147 |
|
148 | this.plugins = {}
|
149 |
|
150 | this.getState = this.getState.bind(this)
|
151 | this.getPlugin = this.getPlugin.bind(this)
|
152 | this.setFileMeta = this.setFileMeta.bind(this)
|
153 | this.setFileState = this.setFileState.bind(this)
|
154 | this.log = this.log.bind(this)
|
155 | this.info = this.info.bind(this)
|
156 | this.hideInfo = this.hideInfo.bind(this)
|
157 | this.addFile = this.addFile.bind(this)
|
158 | this.removeFile = this.removeFile.bind(this)
|
159 | this.pauseResume = this.pauseResume.bind(this)
|
160 | this.validateRestrictions = this.validateRestrictions.bind(this)
|
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 | this._calculateProgress = throttle(this._calculateProgress.bind(this), 500, { leading: true, trailing: true })
|
168 |
|
169 | this.updateOnlineStatus = this.updateOnlineStatus.bind(this)
|
170 | this.resetProgress = this.resetProgress.bind(this)
|
171 |
|
172 | this.pauseAll = this.pauseAll.bind(this)
|
173 | this.resumeAll = this.resumeAll.bind(this)
|
174 | this.retryAll = this.retryAll.bind(this)
|
175 | this.cancelAll = this.cancelAll.bind(this)
|
176 | this.retryUpload = this.retryUpload.bind(this)
|
177 | this.upload = this.upload.bind(this)
|
178 |
|
179 | this.emitter = ee()
|
180 | this.on = this.on.bind(this)
|
181 | this.off = this.off.bind(this)
|
182 | this.once = this.emitter.once.bind(this.emitter)
|
183 | this.emit = this.emitter.emit.bind(this.emitter)
|
184 |
|
185 | this.preProcessors = []
|
186 | this.uploaders = []
|
187 | this.postProcessors = []
|
188 |
|
189 | this.store = this.opts.store
|
190 | this.setState({
|
191 | plugins: {},
|
192 | files: {},
|
193 | currentUploads: {},
|
194 | allowNewUpload: true,
|
195 | capabilities: {
|
196 | uploadProgress: supportsUploadProgress(),
|
197 | individualCancellation: true,
|
198 | resumableUploads: false,
|
199 | },
|
200 | totalProgress: 0,
|
201 | meta: { ...this.opts.meta },
|
202 | info: {
|
203 | isHidden: true,
|
204 | type: 'info',
|
205 | message: '',
|
206 | },
|
207 | })
|
208 |
|
209 | this._storeUnsubscribe = this.store.subscribe((prevState, nextState, patch) => {
|
210 | this.emit('state-update', prevState, nextState, patch)
|
211 | this.updateAll(nextState)
|
212 | })
|
213 |
|
214 |
|
215 | if (this.opts.debug && typeof window !== 'undefined') {
|
216 | window[this.opts.id] = this
|
217 | }
|
218 |
|
219 | this._addListeners()
|
220 |
|
221 |
|
222 |
|
223 | }
|
224 |
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
236 | on (event, callback) {
|
237 | this.emitter.on(event, callback)
|
238 | return this
|
239 | }
|
240 |
|
241 | off (event, callback) {
|
242 | this.emitter.off(event, callback)
|
243 | return this
|
244 | }
|
245 |
|
246 | |
247 |
|
248 |
|
249 |
|
250 |
|
251 | updateAll (state) {
|
252 | this.iteratePlugins(plugin => {
|
253 | plugin.update(state)
|
254 | })
|
255 | }
|
256 |
|
257 | |
258 |
|
259 |
|
260 |
|
261 |
|
262 | setState (patch) {
|
263 | this.store.setState(patch)
|
264 | }
|
265 |
|
266 | |
267 |
|
268 |
|
269 |
|
270 |
|
271 | getState () {
|
272 | return this.store.getState()
|
273 | }
|
274 |
|
275 | |
276 |
|
277 |
|
278 | get state () {
|
279 | return this.getState()
|
280 | }
|
281 |
|
282 | |
283 |
|
284 |
|
285 | setFileState (fileID, state) {
|
286 | if (!this.getState().files[fileID]) {
|
287 | throw new Error(`Can’t set state for ${fileID} (the file could have been removed)`)
|
288 | }
|
289 |
|
290 | this.setState({
|
291 | files: { ...this.getState().files, [fileID]: { ...this.getState().files[fileID], ...state } },
|
292 | })
|
293 | }
|
294 |
|
295 | i18nInit () {
|
296 | this.translator = new Translator([this.defaultLocale, this.opts.locale])
|
297 | this.locale = this.translator.locale
|
298 | this.i18n = this.translator.translate.bind(this.translator)
|
299 | this.i18nArray = this.translator.translateArray.bind(this.translator)
|
300 | }
|
301 |
|
302 | setOptions (newOpts) {
|
303 | this.opts = {
|
304 | ...this.opts,
|
305 | ...newOpts,
|
306 | restrictions: {
|
307 | ...this.opts.restrictions,
|
308 | ...(newOpts && newOpts.restrictions),
|
309 | },
|
310 | }
|
311 |
|
312 | if (newOpts.meta) {
|
313 | this.setMeta(newOpts.meta)
|
314 | }
|
315 |
|
316 | this.i18nInit()
|
317 |
|
318 | if (newOpts.locale) {
|
319 | this.iteratePlugins((plugin) => {
|
320 | plugin.setOptions()
|
321 | })
|
322 | }
|
323 |
|
324 | this.setState()
|
325 | }
|
326 |
|
327 | resetProgress () {
|
328 | const defaultProgress = {
|
329 | percentage: 0,
|
330 | bytesUploaded: 0,
|
331 | uploadComplete: false,
|
332 | uploadStarted: null,
|
333 | }
|
334 | const files = { ...this.getState().files }
|
335 | const updatedFiles = {}
|
336 | Object.keys(files).forEach(fileID => {
|
337 | const updatedFile = { ...files[fileID] }
|
338 | updatedFile.progress = { ...updatedFile.progress, ...defaultProgress }
|
339 | updatedFiles[fileID] = updatedFile
|
340 | })
|
341 |
|
342 | this.setState({
|
343 | files: updatedFiles,
|
344 | totalProgress: 0,
|
345 | })
|
346 |
|
347 | this.emit('reset-progress')
|
348 | }
|
349 |
|
350 | addPreProcessor (fn) {
|
351 | this.preProcessors.push(fn)
|
352 | }
|
353 |
|
354 | removePreProcessor (fn) {
|
355 | const i = this.preProcessors.indexOf(fn)
|
356 | if (i !== -1) {
|
357 | this.preProcessors.splice(i, 1)
|
358 | }
|
359 | }
|
360 |
|
361 | addPostProcessor (fn) {
|
362 | this.postProcessors.push(fn)
|
363 | }
|
364 |
|
365 | removePostProcessor (fn) {
|
366 | const i = this.postProcessors.indexOf(fn)
|
367 | if (i !== -1) {
|
368 | this.postProcessors.splice(i, 1)
|
369 | }
|
370 | }
|
371 |
|
372 | addUploader (fn) {
|
373 | this.uploaders.push(fn)
|
374 | }
|
375 |
|
376 | removeUploader (fn) {
|
377 | const i = this.uploaders.indexOf(fn)
|
378 | if (i !== -1) {
|
379 | this.uploaders.splice(i, 1)
|
380 | }
|
381 | }
|
382 |
|
383 | setMeta (data) {
|
384 | const updatedMeta = { ...this.getState().meta, ...data }
|
385 | const updatedFiles = { ...this.getState().files }
|
386 |
|
387 | Object.keys(updatedFiles).forEach((fileID) => {
|
388 | updatedFiles[fileID] = { ...updatedFiles[fileID], meta: { ...updatedFiles[fileID].meta, ...data } }
|
389 | })
|
390 |
|
391 | this.log('Adding metadata:')
|
392 | this.log(data)
|
393 |
|
394 | this.setState({
|
395 | meta: updatedMeta,
|
396 | files: updatedFiles,
|
397 | })
|
398 | }
|
399 |
|
400 | setFileMeta (fileID, data) {
|
401 | const updatedFiles = { ...this.getState().files }
|
402 | if (!updatedFiles[fileID]) {
|
403 | this.log('Was trying to set metadata for a file that has been removed: ', fileID)
|
404 | return
|
405 | }
|
406 | const newMeta = { ...updatedFiles[fileID].meta, ...data }
|
407 | updatedFiles[fileID] = { ...updatedFiles[fileID], meta: newMeta }
|
408 | this.setState({ files: updatedFiles })
|
409 | }
|
410 |
|
411 | |
412 |
|
413 |
|
414 |
|
415 |
|
416 | getFile (fileID) {
|
417 | return this.getState().files[fileID]
|
418 | }
|
419 |
|
420 | |
421 |
|
422 |
|
423 | getFiles () {
|
424 | const { files } = this.getState()
|
425 | return Object.keys(files).map((fileID) => files[fileID])
|
426 | }
|
427 |
|
428 | |
429 |
|
430 |
|
431 |
|
432 |
|
433 |
|
434 |
|
435 |
|
436 | validateRestrictions (file, files) {
|
437 | try {
|
438 | this._checkRestrictions(file, files)
|
439 | return {
|
440 | result: true,
|
441 | }
|
442 | } catch (err) {
|
443 | return {
|
444 | result: false,
|
445 | reason: err.message,
|
446 | }
|
447 | }
|
448 | }
|
449 |
|
450 | |
451 |
|
452 |
|
453 |
|
454 |
|
455 |
|
456 |
|
457 |
|
458 | _checkRestrictions (file, files = this.getFiles()) {
|
459 | const { maxFileSize, minFileSize, maxTotalFileSize, maxNumberOfFiles, allowedFileTypes } = this.opts.restrictions
|
460 |
|
461 | if (maxNumberOfFiles) {
|
462 | if (files.length + 1 > maxNumberOfFiles) {
|
463 | throw new RestrictionError(`${this.i18n('youCanOnlyUploadX', { smart_count: maxNumberOfFiles })}`)
|
464 | }
|
465 | }
|
466 |
|
467 | if (allowedFileTypes) {
|
468 | const isCorrectFileType = allowedFileTypes.some((type) => {
|
469 |
|
470 | if (type.indexOf('/') > -1) {
|
471 | if (!file.type) return false
|
472 | return match(file.type.replace(/;.*?$/, ''), type)
|
473 | }
|
474 |
|
475 |
|
476 | if (type[0] === '.' && file.extension) {
|
477 | return file.extension.toLowerCase() === type.substr(1).toLowerCase()
|
478 | }
|
479 | return false
|
480 | })
|
481 |
|
482 | if (!isCorrectFileType) {
|
483 | const allowedFileTypesString = allowedFileTypes.join(', ')
|
484 | throw new RestrictionError(this.i18n('youCanOnlyUploadFileTypes', { types: allowedFileTypesString }))
|
485 | }
|
486 | }
|
487 |
|
488 |
|
489 | if (maxTotalFileSize && file.size != null) {
|
490 | let totalFilesSize = 0
|
491 | totalFilesSize += file.size
|
492 | files.forEach((file) => {
|
493 | totalFilesSize += file.size
|
494 | })
|
495 | if (totalFilesSize > maxTotalFileSize) {
|
496 | throw new RestrictionError(this.i18n('exceedsSize2', {
|
497 | backwardsCompat: this.i18n('exceedsSize'),
|
498 | size: prettierBytes(maxTotalFileSize),
|
499 | }))
|
500 | }
|
501 | }
|
502 |
|
503 |
|
504 | if (maxFileSize && file.size != null) {
|
505 | if (file.size > maxFileSize) {
|
506 | throw new RestrictionError(this.i18n('exceedsSize2', {
|
507 | backwardsCompat: this.i18n('exceedsSize'),
|
508 | size: prettierBytes(maxFileSize),
|
509 | }))
|
510 | }
|
511 | }
|
512 |
|
513 |
|
514 | if (minFileSize && file.size != null) {
|
515 | if (file.size < minFileSize) {
|
516 | throw new RestrictionError(this.i18n('inferiorSize', {
|
517 | size: prettierBytes(minFileSize),
|
518 | }))
|
519 | }
|
520 | }
|
521 | }
|
522 |
|
523 | |
524 |
|
525 |
|
526 |
|
527 |
|
528 | _checkMinNumberOfFiles (files) {
|
529 | const { minNumberOfFiles } = this.opts.restrictions
|
530 | if (Object.keys(files).length < minNumberOfFiles) {
|
531 | throw new RestrictionError(`${this.i18n('youHaveToAtLeastSelectX', { smart_count: minNumberOfFiles })}`)
|
532 | }
|
533 | }
|
534 |
|
535 | |
536 |
|
537 |
|
538 |
|
539 |
|
540 |
|
541 |
|
542 |
|
543 |
|
544 |
|
545 |
|
546 | _showOrLogErrorAndThrow (err, { showInformer = true, file = null, throwErr = true } = {}) {
|
547 | const message = typeof err === 'object' ? err.message : err
|
548 | const details = (typeof err === 'object' && err.details) ? err.details : ''
|
549 |
|
550 |
|
551 |
|
552 | let logMessageWithDetails = message
|
553 | if (details) {
|
554 | logMessageWithDetails += ` ${details}`
|
555 | }
|
556 | if (err.isRestriction) {
|
557 | this.log(logMessageWithDetails)
|
558 | this.emit('restriction-failed', file, err)
|
559 | } else {
|
560 | this.log(logMessageWithDetails, 'error')
|
561 | }
|
562 |
|
563 |
|
564 |
|
565 | if (showInformer) {
|
566 | this.info({ message, details }, 'error', this.opts.infoTimeout)
|
567 | }
|
568 |
|
569 | if (throwErr) {
|
570 | throw (typeof err === 'object' ? err : new Error(err))
|
571 | }
|
572 | }
|
573 |
|
574 | _assertNewUploadAllowed (file) {
|
575 | const { allowNewUpload } = this.getState()
|
576 |
|
577 | if (allowNewUpload === false) {
|
578 | this._showOrLogErrorAndThrow(new RestrictionError(this.i18n('noNewAlreadyUploading')), { file })
|
579 | }
|
580 | }
|
581 |
|
582 | |
583 |
|
584 |
|
585 |
|
586 |
|
587 |
|
588 |
|
589 | _checkAndCreateFileStateObject (files, file) {
|
590 | const fileType = getFileType(file)
|
591 | file.type = fileType
|
592 |
|
593 | const onBeforeFileAddedResult = this.opts.onBeforeFileAdded(file, files)
|
594 |
|
595 | if (onBeforeFileAddedResult === false) {
|
596 |
|
597 | this._showOrLogErrorAndThrow(new RestrictionError('Cannot add the file because onBeforeFileAdded returned false.'), { showInformer: false, file })
|
598 | }
|
599 |
|
600 | if (typeof onBeforeFileAddedResult === 'object' && onBeforeFileAddedResult) {
|
601 | file = onBeforeFileAddedResult
|
602 | }
|
603 |
|
604 | let fileName
|
605 | if (file.name) {
|
606 | fileName = file.name
|
607 | } else if (fileType.split('/')[0] === 'image') {
|
608 | fileName = `${fileType.split('/')[0]}.${fileType.split('/')[1]}`
|
609 | } else {
|
610 | fileName = 'noname'
|
611 | }
|
612 | const fileExtension = getFileNameAndExtension(fileName).extension
|
613 | const isRemote = file.isRemote || false
|
614 |
|
615 | const fileID = generateFileID(file)
|
616 |
|
617 | if (files[fileID]) {
|
618 | this._showOrLogErrorAndThrow(new RestrictionError(this.i18n('noDuplicates', { fileName })), { file })
|
619 | }
|
620 |
|
621 | const meta = file.meta || {}
|
622 | meta.name = fileName
|
623 | meta.type = fileType
|
624 |
|
625 |
|
626 | const size = isFinite(file.data.size) ? file.data.size : null
|
627 | const newFile = {
|
628 | source: file.source || '',
|
629 | id: fileID,
|
630 | name: fileName,
|
631 | extension: fileExtension || '',
|
632 | meta: {
|
633 | ...this.getState().meta,
|
634 | ...meta,
|
635 | },
|
636 | type: fileType,
|
637 | data: file.data,
|
638 | progress: {
|
639 | percentage: 0,
|
640 | bytesUploaded: 0,
|
641 | bytesTotal: size,
|
642 | uploadComplete: false,
|
643 | uploadStarted: null,
|
644 | },
|
645 | size,
|
646 | isRemote,
|
647 | remote: file.remote || '',
|
648 | preview: file.preview,
|
649 | }
|
650 |
|
651 | try {
|
652 | const filesArray = Object.keys(files).map(i => files[i])
|
653 | this._checkRestrictions(newFile, filesArray)
|
654 | } catch (err) {
|
655 | this._showOrLogErrorAndThrow(err, { file: newFile })
|
656 | }
|
657 |
|
658 | return newFile
|
659 | }
|
660 |
|
661 |
|
662 | _startIfAutoProceed () {
|
663 | if (this.opts.autoProceed && !this.scheduledAutoProceed) {
|
664 | this.scheduledAutoProceed = setTimeout(() => {
|
665 | this.scheduledAutoProceed = null
|
666 | this.upload().catch((err) => {
|
667 | if (!err.isRestriction) {
|
668 | this.log(err.stack || err.message || err)
|
669 | }
|
670 | })
|
671 | }, 4)
|
672 | }
|
673 | }
|
674 |
|
675 | |
676 |
|
677 |
|
678 |
|
679 |
|
680 |
|
681 |
|
682 |
|
683 | addFile (file) {
|
684 | this._assertNewUploadAllowed(file)
|
685 |
|
686 | const { files } = this.getState()
|
687 | const newFile = this._checkAndCreateFileStateObject(files, file)
|
688 |
|
689 | this.setState({
|
690 | files: {
|
691 | ...files,
|
692 | [newFile.id]: newFile,
|
693 | },
|
694 | })
|
695 |
|
696 | this.emit('file-added', newFile)
|
697 | this.emit('files-added', [newFile])
|
698 | this.log(`Added file: ${newFile.name}, ${newFile.id}, mime type: ${newFile.type}`)
|
699 |
|
700 | this._startIfAutoProceed()
|
701 |
|
702 | return newFile.id
|
703 | }
|
704 |
|
705 | |
706 |
|
707 |
|
708 |
|
709 |
|
710 | addFiles (fileDescriptors) {
|
711 | this._assertNewUploadAllowed()
|
712 |
|
713 |
|
714 | const files = { ...this.getState().files }
|
715 | const newFiles = []
|
716 | const errors = []
|
717 | for (let i = 0; i < fileDescriptors.length; i++) {
|
718 | try {
|
719 | const newFile = this._checkAndCreateFileStateObject(files, fileDescriptors[i])
|
720 | newFiles.push(newFile)
|
721 | files[newFile.id] = newFile
|
722 | } catch (err) {
|
723 | if (!err.isRestriction) {
|
724 | errors.push(err)
|
725 | }
|
726 | }
|
727 | }
|
728 |
|
729 | this.setState({ files })
|
730 |
|
731 | newFiles.forEach((newFile) => {
|
732 | this.emit('file-added', newFile)
|
733 | })
|
734 |
|
735 | this.emit('files-added', newFiles)
|
736 |
|
737 | if (newFiles.length > 5) {
|
738 | this.log(`Added batch of ${newFiles.length} files`)
|
739 | } else {
|
740 | Object.keys(newFiles).forEach(fileID => {
|
741 | this.log(`Added file: ${newFiles[fileID].name}\n id: ${newFiles[fileID].id}\n type: ${newFiles[fileID].type}`)
|
742 | })
|
743 | }
|
744 |
|
745 | if (newFiles.length > 0) {
|
746 | this._startIfAutoProceed()
|
747 | }
|
748 |
|
749 | if (errors.length > 0) {
|
750 | let message = 'Multiple errors occurred while adding files:\n'
|
751 | errors.forEach((subError) => {
|
752 | message += `\n * ${subError.message}`
|
753 | })
|
754 |
|
755 | this.info({
|
756 | message: this.i18n('addBulkFilesFailed', { smart_count: errors.length }),
|
757 | details: message,
|
758 | }, 'error', this.opts.infoTimeout)
|
759 |
|
760 | if (typeof AggregateError === 'function') {
|
761 | throw new AggregateError(errors, message)
|
762 | } else {
|
763 | const err = new Error(message)
|
764 | err.errors = errors
|
765 | throw err
|
766 | }
|
767 | }
|
768 | }
|
769 |
|
770 | removeFiles (fileIDs, reason) {
|
771 | const { files, currentUploads } = this.getState()
|
772 | const updatedFiles = { ...files }
|
773 | const updatedUploads = { ...currentUploads }
|
774 |
|
775 | const removedFiles = Object.create(null)
|
776 | fileIDs.forEach((fileID) => {
|
777 | if (files[fileID]) {
|
778 | removedFiles[fileID] = files[fileID]
|
779 | delete updatedFiles[fileID]
|
780 | }
|
781 | })
|
782 |
|
783 |
|
784 | function fileIsNotRemoved (uploadFileID) {
|
785 | return removedFiles[uploadFileID] === undefined
|
786 | }
|
787 | const uploadsToRemove = []
|
788 | Object.keys(updatedUploads).forEach((uploadID) => {
|
789 | const newFileIDs = currentUploads[uploadID].fileIDs.filter(fileIsNotRemoved)
|
790 |
|
791 |
|
792 | if (newFileIDs.length === 0) {
|
793 | uploadsToRemove.push(uploadID)
|
794 | return
|
795 | }
|
796 |
|
797 | updatedUploads[uploadID] = {
|
798 | ...currentUploads[uploadID],
|
799 | fileIDs: newFileIDs,
|
800 | }
|
801 | })
|
802 |
|
803 | uploadsToRemove.forEach((uploadID) => {
|
804 | delete updatedUploads[uploadID]
|
805 | })
|
806 |
|
807 | const stateUpdate = {
|
808 | currentUploads: updatedUploads,
|
809 | files: updatedFiles,
|
810 | }
|
811 |
|
812 |
|
813 | if (Object.keys(updatedFiles).length === 0) {
|
814 | stateUpdate.allowNewUpload = true
|
815 | stateUpdate.error = null
|
816 | }
|
817 |
|
818 | this.setState(stateUpdate)
|
819 | this._calculateTotalProgress()
|
820 |
|
821 | const removedFileIDs = Object.keys(removedFiles)
|
822 | removedFileIDs.forEach((fileID) => {
|
823 | this.emit('file-removed', removedFiles[fileID], reason)
|
824 | })
|
825 |
|
826 | if (removedFileIDs.length > 5) {
|
827 | this.log(`Removed ${removedFileIDs.length} files`)
|
828 | } else {
|
829 | this.log(`Removed files: ${removedFileIDs.join(', ')}`)
|
830 | }
|
831 | }
|
832 |
|
833 | removeFile (fileID, reason = null) {
|
834 | this.removeFiles([fileID], reason)
|
835 | }
|
836 |
|
837 | pauseResume (fileID) {
|
838 | if (!this.getState().capabilities.resumableUploads
|
839 | || this.getFile(fileID).uploadComplete) {
|
840 | return
|
841 | }
|
842 |
|
843 | const wasPaused = this.getFile(fileID).isPaused || false
|
844 | const isPaused = !wasPaused
|
845 |
|
846 | this.setFileState(fileID, {
|
847 | isPaused,
|
848 | })
|
849 |
|
850 | this.emit('upload-pause', fileID, isPaused)
|
851 |
|
852 | return isPaused
|
853 | }
|
854 |
|
855 | pauseAll () {
|
856 | const updatedFiles = { ...this.getState().files }
|
857 | const inProgressUpdatedFiles = Object.keys(updatedFiles).filter((file) => {
|
858 | return !updatedFiles[file].progress.uploadComplete
|
859 | && updatedFiles[file].progress.uploadStarted
|
860 | })
|
861 |
|
862 | inProgressUpdatedFiles.forEach((file) => {
|
863 | const updatedFile = { ...updatedFiles[file], isPaused: true }
|
864 | updatedFiles[file] = updatedFile
|
865 | })
|
866 |
|
867 | this.setState({ files: updatedFiles })
|
868 | this.emit('pause-all')
|
869 | }
|
870 |
|
871 | resumeAll () {
|
872 | const updatedFiles = { ...this.getState().files }
|
873 | const inProgressUpdatedFiles = Object.keys(updatedFiles).filter((file) => {
|
874 | return !updatedFiles[file].progress.uploadComplete
|
875 | && updatedFiles[file].progress.uploadStarted
|
876 | })
|
877 |
|
878 | inProgressUpdatedFiles.forEach((file) => {
|
879 | const updatedFile = {
|
880 | ...updatedFiles[file],
|
881 | isPaused: false,
|
882 | error: null,
|
883 | }
|
884 | updatedFiles[file] = updatedFile
|
885 | })
|
886 | this.setState({ files: updatedFiles })
|
887 |
|
888 | this.emit('resume-all')
|
889 | }
|
890 |
|
891 | retryAll () {
|
892 | const updatedFiles = { ...this.getState().files }
|
893 | const filesToRetry = Object.keys(updatedFiles).filter(file => {
|
894 | return updatedFiles[file].error
|
895 | })
|
896 |
|
897 | filesToRetry.forEach((file) => {
|
898 | const updatedFile = {
|
899 | ...updatedFiles[file],
|
900 | isPaused: false,
|
901 | error: null,
|
902 | }
|
903 | updatedFiles[file] = updatedFile
|
904 | })
|
905 | this.setState({
|
906 | files: updatedFiles,
|
907 | error: null,
|
908 | })
|
909 |
|
910 | this.emit('retry-all', filesToRetry)
|
911 |
|
912 | if (filesToRetry.length === 0) {
|
913 | return Promise.resolve({
|
914 | successful: [],
|
915 | failed: [],
|
916 | })
|
917 | }
|
918 |
|
919 | const uploadID = this._createUpload(filesToRetry, {
|
920 | forceAllowNewUpload: true,
|
921 | })
|
922 | return this._runUpload(uploadID)
|
923 | }
|
924 |
|
925 | cancelAll () {
|
926 | this.emit('cancel-all')
|
927 |
|
928 | const { files } = this.getState()
|
929 |
|
930 | const fileIDs = Object.keys(files)
|
931 | if (fileIDs.length) {
|
932 | this.removeFiles(fileIDs, 'cancel-all')
|
933 | }
|
934 |
|
935 | this.setState({
|
936 | totalProgress: 0,
|
937 | error: null,
|
938 | })
|
939 | }
|
940 |
|
941 | retryUpload (fileID) {
|
942 | this.setFileState(fileID, {
|
943 | error: null,
|
944 | isPaused: false,
|
945 | })
|
946 |
|
947 | this.emit('upload-retry', fileID)
|
948 |
|
949 | const uploadID = this._createUpload([fileID], {
|
950 | forceAllowNewUpload: true,
|
951 | })
|
952 | return this._runUpload(uploadID)
|
953 | }
|
954 |
|
955 | reset () {
|
956 | this.cancelAll()
|
957 | }
|
958 |
|
959 | logout () {
|
960 | this.iteratePlugins(plugin => {
|
961 | if (plugin.provider && plugin.provider.logout) {
|
962 | plugin.provider.logout()
|
963 | }
|
964 | })
|
965 | }
|
966 |
|
967 | _calculateProgress (file, data) {
|
968 | if (!this.getFile(file.id)) {
|
969 | this.log(`Not setting progress for a file that has been removed: ${file.id}`)
|
970 | return
|
971 | }
|
972 |
|
973 |
|
974 | const canHavePercentage = isFinite(data.bytesTotal) && data.bytesTotal > 0
|
975 | this.setFileState(file.id, {
|
976 | progress: {
|
977 | ...this.getFile(file.id).progress,
|
978 | bytesUploaded: data.bytesUploaded,
|
979 | bytesTotal: data.bytesTotal,
|
980 | percentage: canHavePercentage
|
981 |
|
982 |
|
983 | ? Math.round(data.bytesUploaded / data.bytesTotal * 100)
|
984 | : 0,
|
985 | },
|
986 | })
|
987 |
|
988 | this._calculateTotalProgress()
|
989 | }
|
990 |
|
991 | _calculateTotalProgress () {
|
992 |
|
993 |
|
994 | const files = this.getFiles()
|
995 |
|
996 | const inProgress = files.filter((file) => {
|
997 | return file.progress.uploadStarted
|
998 | || file.progress.preprocess
|
999 | || file.progress.postprocess
|
1000 | })
|
1001 |
|
1002 | if (inProgress.length === 0) {
|
1003 | this.emit('progress', 0)
|
1004 | this.setState({ totalProgress: 0 })
|
1005 | return
|
1006 | }
|
1007 |
|
1008 | const sizedFiles = inProgress.filter((file) => file.progress.bytesTotal != null)
|
1009 | const unsizedFiles = inProgress.filter((file) => file.progress.bytesTotal == null)
|
1010 |
|
1011 | if (sizedFiles.length === 0) {
|
1012 | const progressMax = inProgress.length * 100
|
1013 | const currentProgress = unsizedFiles.reduce((acc, file) => {
|
1014 | return acc + file.progress.percentage
|
1015 | }, 0)
|
1016 | const totalProgress = Math.round(currentProgress / progressMax * 100)
|
1017 | this.setState({ totalProgress })
|
1018 | return
|
1019 | }
|
1020 |
|
1021 | let totalSize = sizedFiles.reduce((acc, file) => {
|
1022 | return acc + file.progress.bytesTotal
|
1023 | }, 0)
|
1024 | const averageSize = totalSize / sizedFiles.length
|
1025 | totalSize += averageSize * unsizedFiles.length
|
1026 |
|
1027 | let uploadedSize = 0
|
1028 | sizedFiles.forEach((file) => {
|
1029 | uploadedSize += file.progress.bytesUploaded
|
1030 | })
|
1031 | unsizedFiles.forEach((file) => {
|
1032 | uploadedSize += averageSize * (file.progress.percentage || 0) / 100
|
1033 | })
|
1034 |
|
1035 | let totalProgress = totalSize === 0
|
1036 | ? 0
|
1037 | : Math.round(uploadedSize / totalSize * 100)
|
1038 |
|
1039 |
|
1040 |
|
1041 | if (totalProgress > 100) {
|
1042 | totalProgress = 100
|
1043 | }
|
1044 |
|
1045 | this.setState({ totalProgress })
|
1046 | this.emit('progress', totalProgress)
|
1047 | }
|
1048 |
|
1049 | |
1050 |
|
1051 |
|
1052 |
|
1053 | _addListeners () {
|
1054 | this.on('error', (error) => {
|
1055 | let errorMsg = 'Unknown error'
|
1056 | if (error.message) {
|
1057 | errorMsg = error.message
|
1058 | }
|
1059 |
|
1060 | if (error.details) {
|
1061 | errorMsg += ` ${error.details}`
|
1062 | }
|
1063 |
|
1064 | this.setState({ error: errorMsg })
|
1065 | })
|
1066 |
|
1067 | this.on('upload-error', (file, error, response) => {
|
1068 | let errorMsg = 'Unknown error'
|
1069 | if (error.message) {
|
1070 | errorMsg = error.message
|
1071 | }
|
1072 |
|
1073 | if (error.details) {
|
1074 | errorMsg += ` ${error.details}`
|
1075 | }
|
1076 |
|
1077 | this.setFileState(file.id, {
|
1078 | error: errorMsg,
|
1079 | response,
|
1080 | })
|
1081 |
|
1082 | this.setState({ error: error.message })
|
1083 |
|
1084 | if (typeof error === 'object' && error.message) {
|
1085 | const newError = new Error(error.message)
|
1086 | newError.details = error.message
|
1087 | if (error.details) {
|
1088 | newError.details += ` ${error.details}`
|
1089 | }
|
1090 | newError.message = this.i18n('failedToUpload', { file: file.name })
|
1091 | this._showOrLogErrorAndThrow(newError, {
|
1092 | throwErr: false,
|
1093 | })
|
1094 | } else {
|
1095 | this._showOrLogErrorAndThrow(error, {
|
1096 | throwErr: false,
|
1097 | })
|
1098 | }
|
1099 | })
|
1100 |
|
1101 | this.on('upload', () => {
|
1102 | this.setState({ error: null })
|
1103 | })
|
1104 |
|
1105 | this.on('upload-started', (file, upload) => {
|
1106 | if (!this.getFile(file.id)) {
|
1107 | this.log(`Not setting progress for a file that has been removed: ${file.id}`)
|
1108 | return
|
1109 | }
|
1110 | this.setFileState(file.id, {
|
1111 | progress: {
|
1112 | uploadStarted: Date.now(),
|
1113 | uploadComplete: false,
|
1114 | percentage: 0,
|
1115 | bytesUploaded: 0,
|
1116 | bytesTotal: file.size,
|
1117 | },
|
1118 | })
|
1119 | })
|
1120 |
|
1121 | this.on('upload-progress', this._calculateProgress)
|
1122 |
|
1123 | this.on('upload-success', (file, uploadResp) => {
|
1124 | if (!this.getFile(file.id)) {
|
1125 | this.log(`Not setting progress for a file that has been removed: ${file.id}`)
|
1126 | return
|
1127 | }
|
1128 |
|
1129 | const currentProgress = this.getFile(file.id).progress
|
1130 | this.setFileState(file.id, {
|
1131 | progress: {
|
1132 | ...currentProgress,
|
1133 | postprocess: this.postProcessors.length > 0 ? {
|
1134 | mode: 'indeterminate',
|
1135 | } : null,
|
1136 | uploadComplete: true,
|
1137 | percentage: 100,
|
1138 | bytesUploaded: currentProgress.bytesTotal,
|
1139 | },
|
1140 | response: uploadResp,
|
1141 | uploadURL: uploadResp.uploadURL,
|
1142 | isPaused: false,
|
1143 | })
|
1144 |
|
1145 | this._calculateTotalProgress()
|
1146 | })
|
1147 |
|
1148 | this.on('preprocess-progress', (file, progress) => {
|
1149 | if (!this.getFile(file.id)) {
|
1150 | this.log(`Not setting progress for a file that has been removed: ${file.id}`)
|
1151 | return
|
1152 | }
|
1153 | this.setFileState(file.id, {
|
1154 | progress: { ...this.getFile(file.id).progress, preprocess: progress },
|
1155 | })
|
1156 | })
|
1157 |
|
1158 | this.on('preprocess-complete', (file) => {
|
1159 | if (!this.getFile(file.id)) {
|
1160 | this.log(`Not setting progress for a file that has been removed: ${file.id}`)
|
1161 | return
|
1162 | }
|
1163 | const files = { ...this.getState().files }
|
1164 | files[file.id] = { ...files[file.id], progress: { ...files[file.id].progress } }
|
1165 | delete files[file.id].progress.preprocess
|
1166 |
|
1167 | this.setState({ files })
|
1168 | })
|
1169 |
|
1170 | this.on('postprocess-progress', (file, progress) => {
|
1171 | if (!this.getFile(file.id)) {
|
1172 | this.log(`Not setting progress for a file that has been removed: ${file.id}`)
|
1173 | return
|
1174 | }
|
1175 | this.setFileState(file.id, {
|
1176 | progress: { ...this.getState().files[file.id].progress, postprocess: progress },
|
1177 | })
|
1178 | })
|
1179 |
|
1180 | this.on('postprocess-complete', (file) => {
|
1181 | if (!this.getFile(file.id)) {
|
1182 | this.log(`Not setting progress for a file that has been removed: ${file.id}`)
|
1183 | return
|
1184 | }
|
1185 | const files = {
|
1186 | ...this.getState().files,
|
1187 | }
|
1188 | files[file.id] = {
|
1189 | ...files[file.id],
|
1190 | progress: {
|
1191 | ...files[file.id].progress,
|
1192 | },
|
1193 | }
|
1194 | delete files[file.id].progress.postprocess
|
1195 |
|
1196 |
|
1197 |
|
1198 |
|
1199 | this.setState({ files })
|
1200 | })
|
1201 |
|
1202 | this.on('restored', () => {
|
1203 |
|
1204 | this._calculateTotalProgress()
|
1205 | })
|
1206 |
|
1207 |
|
1208 | if (typeof window !== 'undefined' && window.addEventListener) {
|
1209 | window.addEventListener('online', () => this.updateOnlineStatus())
|
1210 | window.addEventListener('offline', () => this.updateOnlineStatus())
|
1211 | setTimeout(() => this.updateOnlineStatus(), 3000)
|
1212 | }
|
1213 | }
|
1214 |
|
1215 | updateOnlineStatus () {
|
1216 | const online
|
1217 | = typeof window.navigator.onLine !== 'undefined'
|
1218 | ? window.navigator.onLine
|
1219 | : true
|
1220 | if (!online) {
|
1221 | this.emit('is-offline')
|
1222 | this.info(this.i18n('noInternetConnection'), 'error', 0)
|
1223 | this.wasOffline = true
|
1224 | } else {
|
1225 | this.emit('is-online')
|
1226 | if (this.wasOffline) {
|
1227 | this.emit('back-online')
|
1228 | this.info(this.i18n('connectedToInternet'), 'success', 3000)
|
1229 | this.wasOffline = false
|
1230 | }
|
1231 | }
|
1232 | }
|
1233 |
|
1234 | getID () {
|
1235 | return this.opts.id
|
1236 | }
|
1237 |
|
1238 | |
1239 |
|
1240 |
|
1241 |
|
1242 |
|
1243 |
|
1244 |
|
1245 | use (Plugin, opts) {
|
1246 | if (typeof Plugin !== 'function') {
|
1247 | const msg = `Expected a plugin class, but got ${Plugin === null ? 'null' : typeof Plugin}.`
|
1248 | + ' Please verify that the plugin was imported and spelled correctly.'
|
1249 | throw new TypeError(msg)
|
1250 | }
|
1251 |
|
1252 |
|
1253 | const plugin = new Plugin(this, opts)
|
1254 | const pluginId = plugin.id
|
1255 | this.plugins[plugin.type] = this.plugins[plugin.type] || []
|
1256 |
|
1257 | if (!pluginId) {
|
1258 | throw new Error('Your plugin must have an id')
|
1259 | }
|
1260 |
|
1261 | if (!plugin.type) {
|
1262 | throw new Error('Your plugin must have a type')
|
1263 | }
|
1264 |
|
1265 | const existsPluginAlready = this.getPlugin(pluginId)
|
1266 | if (existsPluginAlready) {
|
1267 | const msg = `Already found a plugin named '${existsPluginAlready.id}'. `
|
1268 | + `Tried to use: '${pluginId}'.\n`
|
1269 | + 'Uppy plugins must have unique `id` options. See https://uppy.io/docs/plugins/#id.'
|
1270 | throw new Error(msg)
|
1271 | }
|
1272 |
|
1273 | if (Plugin.VERSION) {
|
1274 | this.log(`Using ${pluginId} v${Plugin.VERSION}`)
|
1275 | }
|
1276 |
|
1277 | this.plugins[plugin.type].push(plugin)
|
1278 | plugin.install()
|
1279 |
|
1280 | return this
|
1281 | }
|
1282 |
|
1283 | |
1284 |
|
1285 |
|
1286 |
|
1287 |
|
1288 |
|
1289 | getPlugin (id) {
|
1290 | let foundPlugin = null
|
1291 | this.iteratePlugins((plugin) => {
|
1292 | if (plugin.id === id) {
|
1293 | foundPlugin = plugin
|
1294 | return false
|
1295 | }
|
1296 | })
|
1297 | return foundPlugin
|
1298 | }
|
1299 |
|
1300 | |
1301 |
|
1302 |
|
1303 |
|
1304 |
|
1305 | iteratePlugins (method) {
|
1306 | Object.keys(this.plugins).forEach(pluginType => {
|
1307 | this.plugins[pluginType].forEach(method)
|
1308 | })
|
1309 | }
|
1310 |
|
1311 | |
1312 |
|
1313 |
|
1314 |
|
1315 |
|
1316 | removePlugin (instance) {
|
1317 | this.log(`Removing plugin ${instance.id}`)
|
1318 | this.emit('plugin-remove', instance)
|
1319 |
|
1320 | if (instance.uninstall) {
|
1321 | instance.uninstall()
|
1322 | }
|
1323 |
|
1324 | const list = this.plugins[instance.type].slice()
|
1325 |
|
1326 |
|
1327 |
|
1328 | const index = findIndex(list, item => item.id === instance.id)
|
1329 | if (index !== -1) {
|
1330 | list.splice(index, 1)
|
1331 | this.plugins[instance.type] = list
|
1332 | }
|
1333 |
|
1334 | const state = this.getState()
|
1335 | const updatedState = {
|
1336 | plugins: {
|
1337 | ...state.plugins,
|
1338 | [instance.id]: undefined,
|
1339 | },
|
1340 | }
|
1341 | this.setState(updatedState)
|
1342 | }
|
1343 |
|
1344 | |
1345 |
|
1346 |
|
1347 | close () {
|
1348 | this.log(`Closing Uppy instance ${this.opts.id}: removing all files and uninstalling plugins`)
|
1349 |
|
1350 | this.reset()
|
1351 |
|
1352 | this._storeUnsubscribe()
|
1353 |
|
1354 | this.iteratePlugins((plugin) => {
|
1355 | this.removePlugin(plugin)
|
1356 | })
|
1357 | }
|
1358 |
|
1359 | |
1360 |
|
1361 |
|
1362 |
|
1363 |
|
1364 |
|
1365 |
|
1366 |
|
1367 |
|
1368 | info (message, type = 'info', duration = 3000) {
|
1369 | const isComplexMessage = typeof message === 'object'
|
1370 |
|
1371 | this.setState({
|
1372 | info: {
|
1373 | isHidden: false,
|
1374 | type,
|
1375 | message: isComplexMessage ? message.message : message,
|
1376 | details: isComplexMessage ? message.details : null,
|
1377 | },
|
1378 | })
|
1379 |
|
1380 | this.emit('info-visible')
|
1381 |
|
1382 | clearTimeout(this.infoTimeoutID)
|
1383 | if (duration === 0) {
|
1384 | this.infoTimeoutID = undefined
|
1385 | return
|
1386 | }
|
1387 |
|
1388 |
|
1389 | this.infoTimeoutID = setTimeout(this.hideInfo, duration)
|
1390 | }
|
1391 |
|
1392 | hideInfo () {
|
1393 | const newInfo = { ...this.getState().info, isHidden: true }
|
1394 | this.setState({
|
1395 | info: newInfo,
|
1396 | })
|
1397 | this.emit('info-hidden')
|
1398 | }
|
1399 |
|
1400 | |
1401 |
|
1402 |
|
1403 |
|
1404 |
|
1405 |
|
1406 |
|
1407 | log (message, type) {
|
1408 | const { logger } = this.opts
|
1409 | switch (type) {
|
1410 | case 'error': logger.error(message); break
|
1411 | case 'warning': logger.warn(message); break
|
1412 | default: logger.debug(message); break
|
1413 | }
|
1414 | }
|
1415 |
|
1416 | |
1417 |
|
1418 |
|
1419 | run () {
|
1420 | this.log('Calling run() is no longer necessary.', 'warning')
|
1421 | return this
|
1422 | }
|
1423 |
|
1424 | |
1425 |
|
1426 |
|
1427 | restore (uploadID) {
|
1428 | this.log(`Core: attempting to restore upload "${uploadID}"`)
|
1429 |
|
1430 | if (!this.getState().currentUploads[uploadID]) {
|
1431 | this._removeUpload(uploadID)
|
1432 | return Promise.reject(new Error('Nonexistent upload'))
|
1433 | }
|
1434 |
|
1435 | return this._runUpload(uploadID)
|
1436 | }
|
1437 |
|
1438 | |
1439 |
|
1440 |
|
1441 |
|
1442 |
|
1443 |
|
1444 | _createUpload (fileIDs, opts = {}) {
|
1445 | const {
|
1446 | forceAllowNewUpload = false,
|
1447 | } = opts
|
1448 |
|
1449 | const { allowNewUpload, currentUploads } = this.getState()
|
1450 | if (!allowNewUpload && !forceAllowNewUpload) {
|
1451 | throw new Error('Cannot create a new upload: already uploading.')
|
1452 | }
|
1453 |
|
1454 | const uploadID = cuid()
|
1455 |
|
1456 | this.emit('upload', {
|
1457 | id: uploadID,
|
1458 | fileIDs,
|
1459 | })
|
1460 |
|
1461 | this.setState({
|
1462 | allowNewUpload: this.opts.allowMultipleUploads !== false,
|
1463 |
|
1464 | currentUploads: {
|
1465 | ...currentUploads,
|
1466 | [uploadID]: {
|
1467 | fileIDs,
|
1468 | step: 0,
|
1469 | result: {},
|
1470 | },
|
1471 | },
|
1472 | })
|
1473 |
|
1474 | return uploadID
|
1475 | }
|
1476 |
|
1477 | _getUpload (uploadID) {
|
1478 | const { currentUploads } = this.getState()
|
1479 |
|
1480 | return currentUploads[uploadID]
|
1481 | }
|
1482 |
|
1483 | |
1484 |
|
1485 |
|
1486 |
|
1487 |
|
1488 |
|
1489 | addResultData (uploadID, data) {
|
1490 | if (!this._getUpload(uploadID)) {
|
1491 | this.log(`Not setting result for an upload that has been removed: ${uploadID}`)
|
1492 | return
|
1493 | }
|
1494 | const currentUploads = this.getState().currentUploads
|
1495 | const currentUpload = { ...currentUploads[uploadID], result: { ...currentUploads[uploadID].result, ...data } }
|
1496 | this.setState({
|
1497 | currentUploads: { ...currentUploads, [uploadID]: currentUpload },
|
1498 | })
|
1499 | }
|
1500 |
|
1501 | |
1502 |
|
1503 |
|
1504 |
|
1505 |
|
1506 | _removeUpload (uploadID) {
|
1507 | const currentUploads = { ...this.getState().currentUploads }
|
1508 | delete currentUploads[uploadID]
|
1509 |
|
1510 | this.setState({
|
1511 | currentUploads,
|
1512 | })
|
1513 | }
|
1514 |
|
1515 | |
1516 |
|
1517 |
|
1518 |
|
1519 |
|
1520 | _runUpload (uploadID) {
|
1521 | const uploadData = this.getState().currentUploads[uploadID]
|
1522 | const restoreStep = uploadData.step
|
1523 |
|
1524 | const steps = [
|
1525 | ...this.preProcessors,
|
1526 | ...this.uploaders,
|
1527 | ...this.postProcessors,
|
1528 | ]
|
1529 | let lastStep = Promise.resolve()
|
1530 | steps.forEach((fn, step) => {
|
1531 |
|
1532 | if (step < restoreStep) {
|
1533 | return
|
1534 | }
|
1535 |
|
1536 | lastStep = lastStep.then(() => {
|
1537 | const { currentUploads } = this.getState()
|
1538 | const currentUpload = currentUploads[uploadID]
|
1539 | if (!currentUpload) {
|
1540 | return
|
1541 | }
|
1542 |
|
1543 | const updatedUpload = {
|
1544 | ...currentUpload,
|
1545 | step,
|
1546 | }
|
1547 |
|
1548 | this.setState({
|
1549 | currentUploads: {
|
1550 | ...currentUploads,
|
1551 | [uploadID]: updatedUpload,
|
1552 | },
|
1553 | })
|
1554 |
|
1555 |
|
1556 |
|
1557 | return fn(updatedUpload.fileIDs, uploadID)
|
1558 | }).then((result) => {
|
1559 | return null
|
1560 | })
|
1561 | })
|
1562 |
|
1563 |
|
1564 |
|
1565 | lastStep.catch((err) => {
|
1566 | this.emit('error', err, uploadID)
|
1567 | this._removeUpload(uploadID)
|
1568 | })
|
1569 |
|
1570 | return lastStep.then(() => {
|
1571 |
|
1572 | const { currentUploads } = this.getState()
|
1573 | const currentUpload = currentUploads[uploadID]
|
1574 | if (!currentUpload) {
|
1575 | return
|
1576 | }
|
1577 |
|
1578 |
|
1579 |
|
1580 |
|
1581 |
|
1582 |
|
1583 |
|
1584 |
|
1585 |
|
1586 |
|
1587 |
|
1588 | currentUpload.fileIDs.forEach((fileID) => {
|
1589 | const file = this.getFile(fileID)
|
1590 | if (file && file.progress.postprocess) {
|
1591 | this.emit('postprocess-complete', file)
|
1592 | }
|
1593 | })
|
1594 |
|
1595 | const files = currentUpload.fileIDs.map((fileID) => this.getFile(fileID))
|
1596 | const successful = files.filter((file) => !file.error)
|
1597 | const failed = files.filter((file) => file.error)
|
1598 | this.addResultData(uploadID, { successful, failed, uploadID })
|
1599 | }).then(() => {
|
1600 |
|
1601 |
|
1602 |
|
1603 |
|
1604 | const { currentUploads } = this.getState()
|
1605 | if (!currentUploads[uploadID]) {
|
1606 | return
|
1607 | }
|
1608 | const currentUpload = currentUploads[uploadID]
|
1609 | const result = currentUpload.result
|
1610 | this.emit('complete', result)
|
1611 |
|
1612 | this._removeUpload(uploadID)
|
1613 |
|
1614 | return result
|
1615 | }).then((result) => {
|
1616 | if (result == null) {
|
1617 | this.log(`Not setting result for an upload that has been removed: ${uploadID}`)
|
1618 | }
|
1619 | return result
|
1620 | })
|
1621 | }
|
1622 |
|
1623 | |
1624 |
|
1625 |
|
1626 |
|
1627 |
|
1628 | upload () {
|
1629 | if (!this.plugins.uploader) {
|
1630 | this.log('No uploader type plugins are used', 'warning')
|
1631 | }
|
1632 |
|
1633 | let files = this.getState().files
|
1634 |
|
1635 | const onBeforeUploadResult = this.opts.onBeforeUpload(files)
|
1636 |
|
1637 | if (onBeforeUploadResult === false) {
|
1638 | return Promise.reject(new Error('Not starting the upload because onBeforeUpload returned false'))
|
1639 | }
|
1640 |
|
1641 | if (onBeforeUploadResult && typeof onBeforeUploadResult === 'object') {
|
1642 | files = onBeforeUploadResult
|
1643 |
|
1644 |
|
1645 | this.setState({
|
1646 | files,
|
1647 | })
|
1648 | }
|
1649 |
|
1650 | return Promise.resolve()
|
1651 | .then(() => this._checkMinNumberOfFiles(files))
|
1652 | .catch((err) => {
|
1653 | this._showOrLogErrorAndThrow(err)
|
1654 | })
|
1655 | .then(() => {
|
1656 | const { currentUploads } = this.getState()
|
1657 |
|
1658 | const currentlyUploadingFiles = Object.keys(currentUploads).reduce((prev, curr) => prev.concat(currentUploads[curr].fileIDs), [])
|
1659 |
|
1660 | const waitingFileIDs = []
|
1661 | Object.keys(files).forEach((fileID) => {
|
1662 | const file = this.getFile(fileID)
|
1663 |
|
1664 | if ((!file.progress.uploadStarted) && (currentlyUploadingFiles.indexOf(fileID) === -1)) {
|
1665 | waitingFileIDs.push(file.id)
|
1666 | }
|
1667 | })
|
1668 |
|
1669 | const uploadID = this._createUpload(waitingFileIDs)
|
1670 | return this._runUpload(uploadID)
|
1671 | })
|
1672 | .catch((err) => {
|
1673 | this._showOrLogErrorAndThrow(err, {
|
1674 | showInformer: false,
|
1675 | })
|
1676 | })
|
1677 | }
|
1678 | }
|
1679 |
|
1680 | module.exports = function (opts) {
|
1681 | return new Uppy(opts)
|
1682 | }
|
1683 |
|
1684 |
|
1685 | module.exports.Uppy = Uppy
|
1686 | module.exports.Plugin = Plugin
|
1687 | module.exports.debugLogger = debugLogger
|