Uppy.js 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569
  1. /* eslint-disable max-classes-per-file */
  2. /* global AggregateError */
  3. import Translator from '@uppy/utils/lib/Translator'
  4. import ee from 'namespace-emitter'
  5. import { nanoid } from 'nanoid/non-secure'
  6. import throttle from 'lodash.throttle'
  7. import DefaultStore from '@uppy/store-default'
  8. import getFileType from '@uppy/utils/lib/getFileType'
  9. import getFileNameAndExtension from '@uppy/utils/lib/getFileNameAndExtension'
  10. import generateFileID from '@uppy/utils/lib/generateFileID'
  11. import supportsUploadProgress from './supportsUploadProgress.js'
  12. import getFileName from './getFileName.js'
  13. import { justErrorsLogger, debugLogger } from './loggers.js'
  14. import {
  15. Restricter,
  16. defaultOptions as defaultRestrictionOptions,
  17. RestrictionError,
  18. } from './Restricter.js'
  19. import packageJson from '../package.json'
  20. import locale from './locale.js'
  21. /**
  22. * Uppy Core module.
  23. * Manages plugins, state updates, acts as an event bus,
  24. * adds/removes files and metadata.
  25. */
  26. class Uppy {
  27. static VERSION = packageJson.version
  28. /** @type {Record<string, BasePlugin[]>} */
  29. #plugins = Object.create(null)
  30. #restricter
  31. #storeUnsubscribe
  32. #emitter = ee()
  33. #preProcessors = new Set()
  34. #uploaders = new Set()
  35. #postProcessors = new Set()
  36. /**
  37. * Instantiate Uppy
  38. *
  39. * @param {object} opts — Uppy options
  40. */
  41. constructor (opts) {
  42. this.defaultLocale = locale
  43. const defaultOptions = {
  44. id: 'uppy',
  45. autoProceed: false,
  46. allowMultipleUploadBatches: true,
  47. debug: false,
  48. restrictions: defaultRestrictionOptions,
  49. meta: {},
  50. onBeforeFileAdded: (currentFile) => currentFile,
  51. onBeforeUpload: (files) => files,
  52. store: new DefaultStore(),
  53. logger: justErrorsLogger,
  54. infoTimeout: 5000,
  55. }
  56. // Merge default options with the ones set by user,
  57. // making sure to merge restrictions too
  58. this.opts = {
  59. ...defaultOptions,
  60. ...opts,
  61. restrictions: {
  62. ...defaultOptions.restrictions,
  63. ...(opts && opts.restrictions),
  64. },
  65. }
  66. // Support debug: true for backwards-compatability, unless logger is set in opts
  67. // opts instead of this.opts to avoid comparing objects — we set logger: justErrorsLogger in defaultOptions
  68. if (opts && opts.logger && opts.debug) {
  69. 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')
  70. } else if (opts && opts.debug) {
  71. this.opts.logger = debugLogger
  72. }
  73. this.log(`Using Core v${this.constructor.VERSION}`)
  74. this.i18nInit()
  75. // ___Why throttle at 500ms?
  76. // - We must throttle at >250ms for superfocus in Dashboard to work well
  77. // (because animation takes 0.25s, and we want to wait for all animations to be over before refocusing).
  78. // [Practical Check]: if thottle is at 100ms, then if you are uploading a file,
  79. // and click 'ADD MORE FILES', - focus won't activate in Firefox.
  80. // - We must throttle at around >500ms to avoid performance lags.
  81. // [Practical Check] Firefox, try to upload a big file for a prolonged period of time. Laptop will start to heat up.
  82. this.calculateProgress = throttle(this.calculateProgress.bind(this), 500, { leading: true, trailing: true })
  83. this.store = this.opts.store
  84. this.setState({
  85. plugins: {},
  86. files: {},
  87. currentUploads: {},
  88. allowNewUpload: true,
  89. capabilities: {
  90. uploadProgress: supportsUploadProgress(),
  91. individualCancellation: true,
  92. resumableUploads: false,
  93. },
  94. totalProgress: 0,
  95. meta: { ...this.opts.meta },
  96. info: [],
  97. recoveredState: null,
  98. })
  99. this.#restricter = new Restricter(() => this.opts, this.i18n)
  100. this.#storeUnsubscribe = this.store.subscribe((prevState, nextState, patch) => {
  101. this.emit('state-update', prevState, nextState, patch)
  102. this.updateAll(nextState)
  103. })
  104. // Exposing uppy object on window for debugging and testing
  105. if (this.opts.debug && typeof window !== 'undefined') {
  106. window[this.opts.id] = this
  107. }
  108. this.#addListeners()
  109. }
  110. emit (event, ...args) {
  111. this.#emitter.emit(event, ...args)
  112. }
  113. on (event, callback) {
  114. this.#emitter.on(event, callback)
  115. return this
  116. }
  117. once (event, callback) {
  118. this.#emitter.once(event, callback)
  119. return this
  120. }
  121. off (event, callback) {
  122. this.#emitter.off(event, callback)
  123. return this
  124. }
  125. /**
  126. * Iterate on all plugins and run `update` on them.
  127. * Called each time state changes.
  128. *
  129. */
  130. updateAll (state) {
  131. this.iteratePlugins(plugin => {
  132. plugin.update(state)
  133. })
  134. }
  135. /**
  136. * Updates state with a patch
  137. *
  138. * @param {object} patch {foo: 'bar'}
  139. */
  140. setState (patch) {
  141. this.store.setState(patch)
  142. }
  143. /**
  144. * Returns current state.
  145. *
  146. * @returns {object}
  147. */
  148. getState () {
  149. return this.store.getState()
  150. }
  151. /**
  152. * Shorthand to set state for a specific file.
  153. */
  154. setFileState (fileID, state) {
  155. if (!this.getState().files[fileID]) {
  156. throw new Error(`Can’t set state for ${fileID} (the file could have been removed)`)
  157. }
  158. this.setState({
  159. files: { ...this.getState().files, [fileID]: { ...this.getState().files[fileID], ...state } },
  160. })
  161. }
  162. i18nInit () {
  163. const translator = new Translator([this.defaultLocale, this.opts.locale])
  164. this.i18n = translator.translate.bind(translator)
  165. this.i18nArray = translator.translateArray.bind(translator)
  166. this.locale = translator.locale
  167. }
  168. setOptions (newOpts) {
  169. this.opts = {
  170. ...this.opts,
  171. ...newOpts,
  172. restrictions: {
  173. ...this.opts.restrictions,
  174. ...(newOpts && newOpts.restrictions),
  175. },
  176. }
  177. if (newOpts.meta) {
  178. this.setMeta(newOpts.meta)
  179. }
  180. this.i18nInit()
  181. if (newOpts.locale) {
  182. this.iteratePlugins((plugin) => {
  183. plugin.setOptions()
  184. })
  185. }
  186. // Note: this is not the preact `setState`, it's an internal function that has the same name.
  187. this.setState() // so that UI re-renders with new options
  188. }
  189. resetProgress () {
  190. const defaultProgress = {
  191. percentage: 0,
  192. bytesUploaded: 0,
  193. uploadComplete: false,
  194. uploadStarted: null,
  195. }
  196. const files = { ...this.getState().files }
  197. const updatedFiles = {}
  198. Object.keys(files).forEach(fileID => {
  199. const updatedFile = { ...files[fileID] }
  200. updatedFile.progress = { ...updatedFile.progress, ...defaultProgress }
  201. updatedFiles[fileID] = updatedFile
  202. })
  203. this.setState({
  204. files: updatedFiles,
  205. totalProgress: 0,
  206. })
  207. this.emit('reset-progress')
  208. }
  209. addPreProcessor (fn) {
  210. this.#preProcessors.add(fn)
  211. }
  212. removePreProcessor (fn) {
  213. return this.#preProcessors.delete(fn)
  214. }
  215. addPostProcessor (fn) {
  216. this.#postProcessors.add(fn)
  217. }
  218. removePostProcessor (fn) {
  219. return this.#postProcessors.delete(fn)
  220. }
  221. addUploader (fn) {
  222. this.#uploaders.add(fn)
  223. }
  224. removeUploader (fn) {
  225. return this.#uploaders.delete(fn)
  226. }
  227. setMeta (data) {
  228. const updatedMeta = { ...this.getState().meta, ...data }
  229. const updatedFiles = { ...this.getState().files }
  230. Object.keys(updatedFiles).forEach((fileID) => {
  231. updatedFiles[fileID] = { ...updatedFiles[fileID], meta: { ...updatedFiles[fileID].meta, ...data } }
  232. })
  233. this.log('Adding metadata:')
  234. this.log(data)
  235. this.setState({
  236. meta: updatedMeta,
  237. files: updatedFiles,
  238. })
  239. }
  240. setFileMeta (fileID, data) {
  241. const updatedFiles = { ...this.getState().files }
  242. if (!updatedFiles[fileID]) {
  243. this.log('Was trying to set metadata for a file that has been removed: ', fileID)
  244. return
  245. }
  246. const newMeta = { ...updatedFiles[fileID].meta, ...data }
  247. updatedFiles[fileID] = { ...updatedFiles[fileID], meta: newMeta }
  248. this.setState({ files: updatedFiles })
  249. }
  250. /**
  251. * Get a file object.
  252. *
  253. * @param {string} fileID The ID of the file object to return.
  254. */
  255. getFile (fileID) {
  256. return this.getState().files[fileID]
  257. }
  258. /**
  259. * Get all files in an array.
  260. */
  261. getFiles () {
  262. const { files } = this.getState()
  263. return Object.values(files)
  264. }
  265. getObjectOfFilesPerState () {
  266. const { files: filesObject, totalProgress, error } = this.getState()
  267. const files = Object.values(filesObject)
  268. const inProgressFiles = files.filter(({ progress }) => !progress.uploadComplete && progress.uploadStarted)
  269. const newFiles = files.filter((file) => !file.progress.uploadStarted)
  270. const startedFiles = files.filter(
  271. file => file.progress.uploadStarted || file.progress.preprocess || file.progress.postprocess,
  272. )
  273. const uploadStartedFiles = files.filter((file) => file.progress.uploadStarted)
  274. const pausedFiles = files.filter((file) => file.isPaused)
  275. const completeFiles = files.filter((file) => file.progress.uploadComplete)
  276. const erroredFiles = files.filter((file) => file.error)
  277. const inProgressNotPausedFiles = inProgressFiles.filter((file) => !file.isPaused)
  278. const processingFiles = files.filter((file) => file.progress.preprocess || file.progress.postprocess)
  279. return {
  280. newFiles,
  281. startedFiles,
  282. uploadStartedFiles,
  283. pausedFiles,
  284. completeFiles,
  285. erroredFiles,
  286. inProgressFiles,
  287. inProgressNotPausedFiles,
  288. processingFiles,
  289. isUploadStarted: uploadStartedFiles.length > 0,
  290. isAllComplete: totalProgress === 100
  291. && completeFiles.length === files.length
  292. && processingFiles.length === 0,
  293. isAllErrored: !!error && erroredFiles.length === files.length,
  294. isAllPaused: inProgressFiles.length !== 0 && pausedFiles.length === inProgressFiles.length,
  295. isUploadInProgress: inProgressFiles.length > 0,
  296. isSomeGhost: files.some(file => file.isGhost),
  297. }
  298. }
  299. /*
  300. * @constructs
  301. * @param { Error } error
  302. * @param { undefined } file
  303. */
  304. /*
  305. * @constructs
  306. * @param { RestrictionError } error
  307. * @param { UppyFile | undefined } file
  308. */
  309. #informAndEmit (error, file) {
  310. const { message, details = '' } = error
  311. if (error.isRestriction) {
  312. this.emit('restriction-failed', file, error)
  313. } else {
  314. this.emit('error', error)
  315. }
  316. this.info({ message, details }, 'error', this.opts.infoTimeout)
  317. this.log(error, 'warning')
  318. }
  319. validateRestrictions (file, files = this.getFiles()) {
  320. try {
  321. this.#restricter.validate(file, files)
  322. } catch (err) {
  323. return err
  324. }
  325. return null
  326. }
  327. #checkRequiredMetaFieldsOnFile (file) {
  328. const { missingFields, error } = this.#restricter.getMissingRequiredMetaFields(file)
  329. if (missingFields.length > 0) {
  330. this.setFileState(file.id, { missingRequiredMetaFields: missingFields })
  331. this.log(error.message)
  332. this.emit('restriction-failed', file, error)
  333. return false
  334. }
  335. return true
  336. }
  337. #checkRequiredMetaFields (files) {
  338. let success = true
  339. for (const file of Object.values(files)) {
  340. if (!this.#checkRequiredMetaFieldsOnFile(file)) {
  341. success = false
  342. }
  343. }
  344. return success
  345. }
  346. #assertNewUploadAllowed (file) {
  347. const { allowNewUpload } = this.getState()
  348. if (allowNewUpload === false) {
  349. const error = new RestrictionError(this.i18n('noMoreFilesAllowed'))
  350. this.#informAndEmit(error, file)
  351. throw error
  352. }
  353. }
  354. checkIfFileAlreadyExists (fileID) {
  355. const { files } = this.getState()
  356. if (files[fileID] && !files[fileID].isGhost) {
  357. return true
  358. }
  359. return false
  360. }
  361. /**
  362. * Create a file state object based on user-provided `addFile()` options.
  363. *
  364. * Note this is extremely side-effectful and should only be done when a file state object
  365. * will be added to state immediately afterward!
  366. *
  367. * The `files` value is passed in because it may be updated by the caller without updating the store.
  368. */
  369. #checkAndCreateFileStateObject (files, fileDescriptor) {
  370. // Uppy expects files in { name, type, size, data } format.
  371. // If the actual File object is passed from input[type=file] or drag-drop,
  372. // we normalize it to match Uppy file object
  373. if (fileDescriptor instanceof File) {
  374. // eslint-disable-next-line no-param-reassign
  375. fileDescriptor = {
  376. name: fileDescriptor.name,
  377. type: fileDescriptor.type,
  378. size: fileDescriptor.size,
  379. data: fileDescriptor,
  380. }
  381. }
  382. const fileType = getFileType(fileDescriptor)
  383. const fileName = getFileName(fileType, fileDescriptor)
  384. const fileExtension = getFileNameAndExtension(fileName).extension
  385. const isRemote = Boolean(fileDescriptor.isRemote)
  386. const fileID = generateFileID({
  387. ...fileDescriptor,
  388. type: fileType,
  389. })
  390. if (this.checkIfFileAlreadyExists(fileID)) {
  391. const error = new RestrictionError(this.i18n('noDuplicates', { fileName }))
  392. this.#informAndEmit(error, fileDescriptor)
  393. throw error
  394. }
  395. const meta = fileDescriptor.meta || {}
  396. meta.name = fileName
  397. meta.type = fileType
  398. // `null` means the size is unknown.
  399. const size = Number.isFinite(fileDescriptor.data.size) ? fileDescriptor.data.size : null
  400. let newFile = {
  401. source: fileDescriptor.source || '',
  402. id: fileID,
  403. name: fileName,
  404. extension: fileExtension || '',
  405. meta: {
  406. ...this.getState().meta,
  407. ...meta,
  408. },
  409. type: fileType,
  410. data: fileDescriptor.data,
  411. progress: {
  412. percentage: 0,
  413. bytesUploaded: 0,
  414. bytesTotal: size,
  415. uploadComplete: false,
  416. uploadStarted: null,
  417. },
  418. size,
  419. isRemote,
  420. remote: fileDescriptor.remote || '',
  421. preview: fileDescriptor.preview,
  422. }
  423. const onBeforeFileAddedResult = this.opts.onBeforeFileAdded(newFile, files)
  424. if (onBeforeFileAddedResult === false) {
  425. // Don’t show UI info for this error, as it should be done by the developer
  426. const error = new RestrictionError('Cannot add the file because onBeforeFileAdded returned false.')
  427. this.emit('restriction-failed', fileDescriptor, error)
  428. throw error
  429. } else if (typeof onBeforeFileAddedResult === 'object' && onBeforeFileAddedResult !== null) {
  430. newFile = onBeforeFileAddedResult
  431. }
  432. try {
  433. const filesArray = Object.keys(files).map(i => files[i])
  434. this.#restricter.validate(newFile, filesArray)
  435. } catch (err) {
  436. this.#informAndEmit(err, newFile)
  437. throw err
  438. }
  439. return newFile
  440. }
  441. // Schedule an upload if `autoProceed` is enabled.
  442. #startIfAutoProceed () {
  443. if (this.opts.autoProceed && !this.scheduledAutoProceed) {
  444. this.scheduledAutoProceed = setTimeout(() => {
  445. this.scheduledAutoProceed = null
  446. this.upload().catch((err) => {
  447. if (!err.isRestriction) {
  448. this.log(err.stack || err.message || err)
  449. }
  450. })
  451. }, 4)
  452. }
  453. }
  454. /**
  455. * Add a new file to `state.files`. This will run `onBeforeFileAdded`,
  456. * try to guess file type in a clever way, check file against restrictions,
  457. * and start an upload if `autoProceed === true`.
  458. *
  459. * @param {object} file object to add
  460. * @returns {string} id for the added file
  461. */
  462. addFile (file) {
  463. this.#assertNewUploadAllowed(file)
  464. const { files } = this.getState()
  465. let newFile = this.#checkAndCreateFileStateObject(files, file)
  466. // Users are asked to re-select recovered files without data,
  467. // and to keep the progress, meta and everthing else, we only replace said data
  468. if (files[newFile.id] && files[newFile.id].isGhost) {
  469. newFile = {
  470. ...files[newFile.id],
  471. data: file.data,
  472. isGhost: false,
  473. }
  474. this.log(`Replaced the blob in the restored ghost file: ${newFile.name}, ${newFile.id}`)
  475. }
  476. this.setState({
  477. files: {
  478. ...files,
  479. [newFile.id]: newFile,
  480. },
  481. })
  482. this.emit('file-added', newFile)
  483. this.emit('files-added', [newFile])
  484. this.log(`Added file: ${newFile.name}, ${newFile.id}, mime type: ${newFile.type}`)
  485. this.#startIfAutoProceed()
  486. return newFile.id
  487. }
  488. /**
  489. * Add multiple files to `state.files`. See the `addFile()` documentation.
  490. *
  491. * If an error occurs while adding a file, it is logged and the user is notified.
  492. * This is good for UI plugins, but not for programmatic use.
  493. * Programmatic users should usually still use `addFile()` on individual files.
  494. */
  495. addFiles (fileDescriptors) {
  496. this.#assertNewUploadAllowed()
  497. // create a copy of the files object only once
  498. const files = { ...this.getState().files }
  499. const newFiles = []
  500. const errors = []
  501. for (let i = 0; i < fileDescriptors.length; i++) {
  502. try {
  503. let newFile = this.#checkAndCreateFileStateObject(files, fileDescriptors[i])
  504. // Users are asked to re-select recovered files without data,
  505. // and to keep the progress, meta and everthing else, we only replace said data
  506. if (files[newFile.id] && files[newFile.id].isGhost) {
  507. newFile = {
  508. ...files[newFile.id],
  509. data: fileDescriptors[i].data,
  510. isGhost: false,
  511. }
  512. this.log(`Replaced blob in a ghost file: ${newFile.name}, ${newFile.id}`)
  513. }
  514. files[newFile.id] = newFile
  515. newFiles.push(newFile)
  516. } catch (err) {
  517. if (!err.isRestriction) {
  518. errors.push(err)
  519. }
  520. }
  521. }
  522. this.setState({ files })
  523. newFiles.forEach((newFile) => {
  524. this.emit('file-added', newFile)
  525. })
  526. this.emit('files-added', newFiles)
  527. if (newFiles.length > 5) {
  528. this.log(`Added batch of ${newFiles.length} files`)
  529. } else {
  530. Object.keys(newFiles).forEach(fileID => {
  531. this.log(`Added file: ${newFiles[fileID].name}\n id: ${newFiles[fileID].id}\n type: ${newFiles[fileID].type}`)
  532. })
  533. }
  534. if (newFiles.length > 0) {
  535. this.#startIfAutoProceed()
  536. }
  537. if (errors.length > 0) {
  538. let message = 'Multiple errors occurred while adding files:\n'
  539. errors.forEach((subError) => {
  540. message += `\n * ${subError.message}`
  541. })
  542. this.info({
  543. message: this.i18n('addBulkFilesFailed', { smart_count: errors.length }),
  544. details: message,
  545. }, 'error', this.opts.infoTimeout)
  546. if (typeof AggregateError === 'function') {
  547. throw new AggregateError(errors, message)
  548. } else {
  549. const err = new Error(message)
  550. err.errors = errors
  551. throw err
  552. }
  553. }
  554. }
  555. removeFiles (fileIDs, reason) {
  556. const { files, currentUploads } = this.getState()
  557. const updatedFiles = { ...files }
  558. const updatedUploads = { ...currentUploads }
  559. const removedFiles = Object.create(null)
  560. fileIDs.forEach((fileID) => {
  561. if (files[fileID]) {
  562. removedFiles[fileID] = files[fileID]
  563. delete updatedFiles[fileID]
  564. }
  565. })
  566. // Remove files from the `fileIDs` list in each upload.
  567. function fileIsNotRemoved (uploadFileID) {
  568. return removedFiles[uploadFileID] === undefined
  569. }
  570. Object.keys(updatedUploads).forEach((uploadID) => {
  571. const newFileIDs = currentUploads[uploadID].fileIDs.filter(fileIsNotRemoved)
  572. // Remove the upload if no files are associated with it anymore.
  573. if (newFileIDs.length === 0) {
  574. delete updatedUploads[uploadID]
  575. return
  576. }
  577. const { capabilities } = this.getState()
  578. if (newFileIDs.length !== currentUploads[uploadID].fileIDs.length
  579. && !capabilities.individualCancellation) {
  580. throw new Error('individualCancellation is disabled')
  581. }
  582. updatedUploads[uploadID] = {
  583. ...currentUploads[uploadID],
  584. fileIDs: newFileIDs,
  585. }
  586. })
  587. const stateUpdate = {
  588. currentUploads: updatedUploads,
  589. files: updatedFiles,
  590. }
  591. // If all files were removed - allow new uploads,
  592. // and clear recoveredState
  593. if (Object.keys(updatedFiles).length === 0) {
  594. stateUpdate.allowNewUpload = true
  595. stateUpdate.error = null
  596. stateUpdate.recoveredState = null
  597. }
  598. this.setState(stateUpdate)
  599. this.calculateTotalProgress()
  600. const removedFileIDs = Object.keys(removedFiles)
  601. removedFileIDs.forEach((fileID) => {
  602. this.emit('file-removed', removedFiles[fileID], reason)
  603. })
  604. if (removedFileIDs.length > 5) {
  605. this.log(`Removed ${removedFileIDs.length} files`)
  606. } else {
  607. this.log(`Removed files: ${removedFileIDs.join(', ')}`)
  608. }
  609. }
  610. removeFile (fileID, reason = null) {
  611. this.removeFiles([fileID], reason)
  612. }
  613. pauseResume (fileID) {
  614. if (!this.getState().capabilities.resumableUploads
  615. || this.getFile(fileID).uploadComplete) {
  616. return undefined
  617. }
  618. const wasPaused = this.getFile(fileID).isPaused || false
  619. const isPaused = !wasPaused
  620. this.setFileState(fileID, {
  621. isPaused,
  622. })
  623. this.emit('upload-pause', fileID, isPaused)
  624. return isPaused
  625. }
  626. pauseAll () {
  627. const updatedFiles = { ...this.getState().files }
  628. const inProgressUpdatedFiles = Object.keys(updatedFiles).filter((file) => {
  629. return !updatedFiles[file].progress.uploadComplete
  630. && updatedFiles[file].progress.uploadStarted
  631. })
  632. inProgressUpdatedFiles.forEach((file) => {
  633. const updatedFile = { ...updatedFiles[file], isPaused: true }
  634. updatedFiles[file] = updatedFile
  635. })
  636. this.setState({ files: updatedFiles })
  637. this.emit('pause-all')
  638. }
  639. resumeAll () {
  640. const updatedFiles = { ...this.getState().files }
  641. const inProgressUpdatedFiles = Object.keys(updatedFiles).filter((file) => {
  642. return !updatedFiles[file].progress.uploadComplete
  643. && updatedFiles[file].progress.uploadStarted
  644. })
  645. inProgressUpdatedFiles.forEach((file) => {
  646. const updatedFile = {
  647. ...updatedFiles[file],
  648. isPaused: false,
  649. error: null,
  650. }
  651. updatedFiles[file] = updatedFile
  652. })
  653. this.setState({ files: updatedFiles })
  654. this.emit('resume-all')
  655. }
  656. retryAll () {
  657. const updatedFiles = { ...this.getState().files }
  658. const filesToRetry = Object.keys(updatedFiles).filter(file => {
  659. return updatedFiles[file].error
  660. })
  661. filesToRetry.forEach((file) => {
  662. const updatedFile = {
  663. ...updatedFiles[file],
  664. isPaused: false,
  665. error: null,
  666. }
  667. updatedFiles[file] = updatedFile
  668. })
  669. this.setState({
  670. files: updatedFiles,
  671. error: null,
  672. })
  673. this.emit('retry-all', filesToRetry)
  674. if (filesToRetry.length === 0) {
  675. return Promise.resolve({
  676. successful: [],
  677. failed: [],
  678. })
  679. }
  680. const uploadID = this.#createUpload(filesToRetry, {
  681. forceAllowNewUpload: true, // create new upload even if allowNewUpload: false
  682. })
  683. return this.#runUpload(uploadID)
  684. }
  685. cancelAll ({ reason = 'user' } = {}) {
  686. this.emit('cancel-all', { reason })
  687. // Only remove existing uploads if user is canceling
  688. if (reason === 'user') {
  689. const { files } = this.getState()
  690. const fileIDs = Object.keys(files)
  691. if (fileIDs.length) {
  692. this.removeFiles(fileIDs, 'cancel-all')
  693. }
  694. this.setState({
  695. totalProgress: 0,
  696. error: null,
  697. recoveredState: null,
  698. })
  699. }
  700. }
  701. retryUpload (fileID) {
  702. this.setFileState(fileID, {
  703. error: null,
  704. isPaused: false,
  705. })
  706. this.emit('upload-retry', fileID)
  707. const uploadID = this.#createUpload([fileID], {
  708. forceAllowNewUpload: true, // create new upload even if allowNewUpload: false
  709. })
  710. return this.#runUpload(uploadID)
  711. }
  712. logout () {
  713. this.iteratePlugins(plugin => {
  714. if (plugin.provider && plugin.provider.logout) {
  715. plugin.provider.logout()
  716. }
  717. })
  718. }
  719. calculateProgress (file, data) {
  720. if (file == null || !this.getFile(file.id)) {
  721. this.log(`Not setting progress for a file that has been removed: ${file?.id}`)
  722. return
  723. }
  724. // bytesTotal may be null or zero; in that case we can't divide by it
  725. const canHavePercentage = Number.isFinite(data.bytesTotal) && data.bytesTotal > 0
  726. this.setFileState(file.id, {
  727. progress: {
  728. ...this.getFile(file.id).progress,
  729. bytesUploaded: data.bytesUploaded,
  730. bytesTotal: data.bytesTotal,
  731. percentage: canHavePercentage
  732. ? Math.round((data.bytesUploaded / data.bytesTotal) * 100)
  733. : 0,
  734. },
  735. })
  736. this.calculateTotalProgress()
  737. }
  738. calculateTotalProgress () {
  739. // calculate total progress, using the number of files currently uploading,
  740. // multiplied by 100 and the summ of individual progress of each file
  741. const files = this.getFiles()
  742. const inProgress = files.filter((file) => {
  743. return file.progress.uploadStarted
  744. || file.progress.preprocess
  745. || file.progress.postprocess
  746. })
  747. if (inProgress.length === 0) {
  748. this.emit('progress', 0)
  749. this.setState({ totalProgress: 0 })
  750. return
  751. }
  752. const sizedFiles = inProgress.filter((file) => file.progress.bytesTotal != null)
  753. const unsizedFiles = inProgress.filter((file) => file.progress.bytesTotal == null)
  754. if (sizedFiles.length === 0) {
  755. const progressMax = inProgress.length * 100
  756. const currentProgress = unsizedFiles.reduce((acc, file) => {
  757. return acc + file.progress.percentage
  758. }, 0)
  759. const totalProgress = Math.round((currentProgress / progressMax) * 100)
  760. this.setState({ totalProgress })
  761. return
  762. }
  763. let totalSize = sizedFiles.reduce((acc, file) => {
  764. return acc + file.progress.bytesTotal
  765. }, 0)
  766. const averageSize = totalSize / sizedFiles.length
  767. totalSize += averageSize * unsizedFiles.length
  768. let uploadedSize = 0
  769. sizedFiles.forEach((file) => {
  770. uploadedSize += file.progress.bytesUploaded
  771. })
  772. unsizedFiles.forEach((file) => {
  773. uploadedSize += (averageSize * (file.progress.percentage || 0)) / 100
  774. })
  775. let totalProgress = totalSize === 0
  776. ? 0
  777. : Math.round((uploadedSize / totalSize) * 100)
  778. // hot fix, because:
  779. // uploadedSize ended up larger than totalSize, resulting in 1325% total
  780. if (totalProgress > 100) {
  781. totalProgress = 100
  782. }
  783. this.setState({ totalProgress })
  784. this.emit('progress', totalProgress)
  785. }
  786. /**
  787. * Registers listeners for all global actions, like:
  788. * `error`, `file-removed`, `upload-progress`
  789. */
  790. #addListeners () {
  791. /**
  792. * @param {Error} error
  793. * @param {object} [file]
  794. * @param {object} [response]
  795. */
  796. const errorHandler = (error, file, response) => {
  797. let errorMsg = error.message || 'Unknown error'
  798. if (error.details) {
  799. errorMsg += ` ${error.details}`
  800. }
  801. this.setState({ error: errorMsg })
  802. if (file != null && file.id in this.getState().files) {
  803. this.setFileState(file.id, {
  804. error: errorMsg,
  805. response,
  806. })
  807. }
  808. }
  809. this.on('error', errorHandler)
  810. this.on('upload-error', (file, error, response) => {
  811. errorHandler(error, file, response)
  812. if (typeof error === 'object' && error.message) {
  813. const newError = new Error(error.message)
  814. newError.details = error.message
  815. if (error.details) {
  816. newError.details += ` ${error.details}`
  817. }
  818. newError.message = this.i18n('failedToUpload', { file: file?.name })
  819. this.#informAndEmit(newError)
  820. } else {
  821. this.#informAndEmit(error)
  822. }
  823. })
  824. let uploadStalledWarningRecentlyEmitted
  825. this.on('upload-stalled', (error, files) => {
  826. const { message } = error
  827. const details = files.map(file => file.meta.name).join(', ')
  828. if (!uploadStalledWarningRecentlyEmitted) {
  829. this.info({ message, details }, 'warning', this.opts.infoTimeout)
  830. uploadStalledWarningRecentlyEmitted = setTimeout(() => {
  831. uploadStalledWarningRecentlyEmitted = null
  832. }, this.opts.infoTimeout)
  833. }
  834. this.log(`${message} ${details}`.trim(), 'warning')
  835. })
  836. this.on('upload', () => {
  837. this.setState({ error: null })
  838. })
  839. this.on('upload-started', (file) => {
  840. if (file == null || !this.getFile(file.id)) {
  841. this.log(`Not setting progress for a file that has been removed: ${file?.id}`)
  842. return
  843. }
  844. this.setFileState(file.id, {
  845. progress: {
  846. uploadStarted: Date.now(),
  847. uploadComplete: false,
  848. percentage: 0,
  849. bytesUploaded: 0,
  850. bytesTotal: file.size,
  851. },
  852. })
  853. })
  854. this.on('upload-progress', this.calculateProgress)
  855. this.on('upload-success', (file, uploadResp) => {
  856. if (file == null || !this.getFile(file.id)) {
  857. this.log(`Not setting progress for a file that has been removed: ${file?.id}`)
  858. return
  859. }
  860. const currentProgress = this.getFile(file.id).progress
  861. this.setFileState(file.id, {
  862. progress: {
  863. ...currentProgress,
  864. postprocess: this.#postProcessors.size > 0 ? {
  865. mode: 'indeterminate',
  866. } : null,
  867. uploadComplete: true,
  868. percentage: 100,
  869. bytesUploaded: currentProgress.bytesTotal,
  870. },
  871. response: uploadResp,
  872. uploadURL: uploadResp.uploadURL,
  873. isPaused: false,
  874. })
  875. // Remote providers sometimes don't tell us the file size,
  876. // but we can know how many bytes we uploaded once the upload is complete.
  877. if (file.size == null) {
  878. this.setFileState(file.id, {
  879. size: uploadResp.bytesUploaded || currentProgress.bytesTotal,
  880. })
  881. }
  882. this.calculateTotalProgress()
  883. })
  884. this.on('preprocess-progress', (file, progress) => {
  885. if (file == null || !this.getFile(file.id)) {
  886. this.log(`Not setting progress for a file that has been removed: ${file?.id}`)
  887. return
  888. }
  889. this.setFileState(file.id, {
  890. progress: { ...this.getFile(file.id).progress, preprocess: progress },
  891. })
  892. })
  893. this.on('preprocess-complete', (file) => {
  894. if (file == null || !this.getFile(file.id)) {
  895. this.log(`Not setting progress for a file that has been removed: ${file?.id}`)
  896. return
  897. }
  898. const files = { ...this.getState().files }
  899. files[file.id] = { ...files[file.id], progress: { ...files[file.id].progress } }
  900. delete files[file.id].progress.preprocess
  901. this.setState({ files })
  902. })
  903. this.on('postprocess-progress', (file, progress) => {
  904. if (file == null || !this.getFile(file.id)) {
  905. this.log(`Not setting progress for a file that has been removed: ${file?.id}`)
  906. return
  907. }
  908. this.setFileState(file.id, {
  909. progress: { ...this.getState().files[file.id].progress, postprocess: progress },
  910. })
  911. })
  912. this.on('postprocess-complete', (file) => {
  913. if (file == null || !this.getFile(file.id)) {
  914. this.log(`Not setting progress for a file that has been removed: ${file?.id}`)
  915. return
  916. }
  917. const files = {
  918. ...this.getState().files,
  919. }
  920. files[file.id] = {
  921. ...files[file.id],
  922. progress: {
  923. ...files[file.id].progress,
  924. },
  925. }
  926. delete files[file.id].progress.postprocess
  927. this.setState({ files })
  928. })
  929. this.on('restored', () => {
  930. // Files may have changed--ensure progress is still accurate.
  931. this.calculateTotalProgress()
  932. })
  933. this.on('dashboard:file-edit-complete', (file) => {
  934. if (file) {
  935. this.#checkRequiredMetaFieldsOnFile(file)
  936. }
  937. })
  938. // show informer if offline
  939. if (typeof window !== 'undefined' && window.addEventListener) {
  940. window.addEventListener('online', this.#updateOnlineStatus)
  941. window.addEventListener('offline', this.#updateOnlineStatus)
  942. setTimeout(this.#updateOnlineStatus, 3000)
  943. }
  944. }
  945. updateOnlineStatus () {
  946. const online = typeof window.navigator.onLine !== 'undefined'
  947. ? window.navigator.onLine
  948. : true
  949. if (!online) {
  950. this.emit('is-offline')
  951. this.info(this.i18n('noInternetConnection'), 'error', 0)
  952. this.wasOffline = true
  953. } else {
  954. this.emit('is-online')
  955. if (this.wasOffline) {
  956. this.emit('back-online')
  957. this.info(this.i18n('connectedToInternet'), 'success', 3000)
  958. this.wasOffline = false
  959. }
  960. }
  961. }
  962. #updateOnlineStatus = this.updateOnlineStatus.bind(this)
  963. getID () {
  964. return this.opts.id
  965. }
  966. /**
  967. * Registers a plugin with Core.
  968. *
  969. * @param {object} Plugin object
  970. * @param {object} [opts] object with options to be passed to Plugin
  971. * @returns {object} self for chaining
  972. */
  973. // eslint-disable-next-line no-shadow
  974. use (Plugin, opts) {
  975. if (typeof Plugin !== 'function') {
  976. const msg = `Expected a plugin class, but got ${Plugin === null ? 'null' : typeof Plugin}.`
  977. + ' Please verify that the plugin was imported and spelled correctly.'
  978. throw new TypeError(msg)
  979. }
  980. // Instantiate
  981. const plugin = new Plugin(this, opts)
  982. const pluginId = plugin.id
  983. if (!pluginId) {
  984. throw new Error('Your plugin must have an id')
  985. }
  986. if (!plugin.type) {
  987. throw new Error('Your plugin must have a type')
  988. }
  989. const existsPluginAlready = this.getPlugin(pluginId)
  990. if (existsPluginAlready) {
  991. const msg = `Already found a plugin named '${existsPluginAlready.id}'. `
  992. + `Tried to use: '${pluginId}'.\n`
  993. + 'Uppy plugins must have unique `id` options. See https://uppy.io/docs/plugins/#id.'
  994. throw new Error(msg)
  995. }
  996. if (Plugin.VERSION) {
  997. this.log(`Using ${pluginId} v${Plugin.VERSION}`)
  998. }
  999. if (plugin.type in this.#plugins) {
  1000. this.#plugins[plugin.type].push(plugin)
  1001. } else {
  1002. this.#plugins[plugin.type] = [plugin]
  1003. }
  1004. plugin.install()
  1005. return this
  1006. }
  1007. /**
  1008. * Find one Plugin by name.
  1009. *
  1010. * @param {string} id plugin id
  1011. * @returns {BasePlugin|undefined}
  1012. */
  1013. getPlugin (id) {
  1014. for (const plugins of Object.values(this.#plugins)) {
  1015. const foundPlugin = plugins.find(plugin => plugin.id === id)
  1016. if (foundPlugin != null) return foundPlugin
  1017. }
  1018. return undefined
  1019. }
  1020. [Symbol.for('uppy test: getPlugins')] (type) {
  1021. return this.#plugins[type]
  1022. }
  1023. /**
  1024. * Iterate through all `use`d plugins.
  1025. *
  1026. * @param {Function} method that will be run on each plugin
  1027. */
  1028. iteratePlugins (method) {
  1029. Object.values(this.#plugins).flat(1).forEach(method)
  1030. }
  1031. /**
  1032. * Uninstall and remove a plugin.
  1033. *
  1034. * @param {object} instance The plugin instance to remove.
  1035. */
  1036. removePlugin (instance) {
  1037. this.log(`Removing plugin ${instance.id}`)
  1038. this.emit('plugin-remove', instance)
  1039. if (instance.uninstall) {
  1040. instance.uninstall()
  1041. }
  1042. const list = this.#plugins[instance.type]
  1043. // list.indexOf failed here, because Vue3 converted the plugin instance
  1044. // to a Proxy object, which failed the strict comparison test:
  1045. // obj !== objProxy
  1046. const index = list.findIndex(item => item.id === instance.id)
  1047. if (index !== -1) {
  1048. list.splice(index, 1)
  1049. }
  1050. const state = this.getState()
  1051. const updatedState = {
  1052. plugins: {
  1053. ...state.plugins,
  1054. [instance.id]: undefined,
  1055. },
  1056. }
  1057. this.setState(updatedState)
  1058. }
  1059. /**
  1060. * Uninstall all plugins and close down this Uppy instance.
  1061. */
  1062. close ({ reason } = {}) {
  1063. this.log(`Closing Uppy instance ${this.opts.id}: removing all files and uninstalling plugins`)
  1064. this.cancelAll({ reason })
  1065. this.#storeUnsubscribe()
  1066. this.iteratePlugins((plugin) => {
  1067. this.removePlugin(plugin)
  1068. })
  1069. if (typeof window !== 'undefined' && window.removeEventListener) {
  1070. window.removeEventListener('online', this.#updateOnlineStatus)
  1071. window.removeEventListener('offline', this.#updateOnlineStatus)
  1072. }
  1073. }
  1074. hideInfo () {
  1075. const { info } = this.getState()
  1076. this.setState({ info: info.slice(1) })
  1077. this.emit('info-hidden')
  1078. }
  1079. /**
  1080. * Set info message in `state.info`, so that UI plugins like `Informer`
  1081. * can display the message.
  1082. *
  1083. * @param {string | object} message Message to be displayed by the informer
  1084. * @param {string} [type]
  1085. * @param {number} [duration]
  1086. */
  1087. info (message, type = 'info', duration = 3000) {
  1088. const isComplexMessage = typeof message === 'object'
  1089. this.setState({
  1090. info: [
  1091. ...this.getState().info,
  1092. {
  1093. type,
  1094. message: isComplexMessage ? message.message : message,
  1095. details: isComplexMessage ? message.details : null,
  1096. },
  1097. ],
  1098. })
  1099. setTimeout(() => this.hideInfo(), duration)
  1100. this.emit('info-visible')
  1101. }
  1102. /**
  1103. * Passes messages to a function, provided in `opts.logger`.
  1104. * If `opts.logger: Uppy.debugLogger` or `opts.debug: true`, logs to the browser console.
  1105. *
  1106. * @param {string|object} message to log
  1107. * @param {string} [type] optional `error` or `warning`
  1108. */
  1109. log (message, type) {
  1110. const { logger } = this.opts
  1111. switch (type) {
  1112. case 'error': logger.error(message); break
  1113. case 'warning': logger.warn(message); break
  1114. default: logger.debug(message); break
  1115. }
  1116. }
  1117. /**
  1118. * Restore an upload by its ID.
  1119. */
  1120. restore (uploadID) {
  1121. this.log(`Core: attempting to restore upload "${uploadID}"`)
  1122. if (!this.getState().currentUploads[uploadID]) {
  1123. this.#removeUpload(uploadID)
  1124. return Promise.reject(new Error('Nonexistent upload'))
  1125. }
  1126. return this.#runUpload(uploadID)
  1127. }
  1128. /**
  1129. * Create an upload for a bunch of files.
  1130. *
  1131. * @param {Array<string>} fileIDs File IDs to include in this upload.
  1132. * @returns {string} ID of this upload.
  1133. */
  1134. #createUpload (fileIDs, opts = {}) {
  1135. // uppy.retryAll sets this to true — when retrying we want to ignore `allowNewUpload: false`
  1136. const { forceAllowNewUpload = false } = opts
  1137. const { allowNewUpload, currentUploads } = this.getState()
  1138. if (!allowNewUpload && !forceAllowNewUpload) {
  1139. throw new Error('Cannot create a new upload: already uploading.')
  1140. }
  1141. const uploadID = nanoid()
  1142. this.emit('upload', {
  1143. id: uploadID,
  1144. fileIDs,
  1145. })
  1146. this.setState({
  1147. allowNewUpload: this.opts.allowMultipleUploadBatches !== false && this.opts.allowMultipleUploads !== false,
  1148. currentUploads: {
  1149. ...currentUploads,
  1150. [uploadID]: {
  1151. fileIDs,
  1152. step: 0,
  1153. result: {},
  1154. },
  1155. },
  1156. })
  1157. return uploadID
  1158. }
  1159. [Symbol.for('uppy test: createUpload')] (...args) { return this.#createUpload(...args) }
  1160. #getUpload (uploadID) {
  1161. const { currentUploads } = this.getState()
  1162. return currentUploads[uploadID]
  1163. }
  1164. /**
  1165. * Add data to an upload's result object.
  1166. *
  1167. * @param {string} uploadID The ID of the upload.
  1168. * @param {object} data Data properties to add to the result object.
  1169. */
  1170. addResultData (uploadID, data) {
  1171. if (!this.#getUpload(uploadID)) {
  1172. this.log(`Not setting result for an upload that has been removed: ${uploadID}`)
  1173. return
  1174. }
  1175. const { currentUploads } = this.getState()
  1176. const currentUpload = { ...currentUploads[uploadID], result: { ...currentUploads[uploadID].result, ...data } }
  1177. this.setState({
  1178. currentUploads: { ...currentUploads, [uploadID]: currentUpload },
  1179. })
  1180. }
  1181. /**
  1182. * Remove an upload, eg. if it has been canceled or completed.
  1183. *
  1184. * @param {string} uploadID The ID of the upload.
  1185. */
  1186. #removeUpload (uploadID) {
  1187. const currentUploads = { ...this.getState().currentUploads }
  1188. delete currentUploads[uploadID]
  1189. this.setState({
  1190. currentUploads,
  1191. })
  1192. }
  1193. /**
  1194. * Run an upload. This picks up where it left off in case the upload is being restored.
  1195. *
  1196. * @private
  1197. */
  1198. async #runUpload (uploadID) {
  1199. let { currentUploads } = this.getState()
  1200. let currentUpload = currentUploads[uploadID]
  1201. const restoreStep = currentUpload.step || 0
  1202. const steps = [
  1203. ...this.#preProcessors,
  1204. ...this.#uploaders,
  1205. ...this.#postProcessors,
  1206. ]
  1207. try {
  1208. for (let step = restoreStep; step < steps.length; step++) {
  1209. if (!currentUpload) {
  1210. break
  1211. }
  1212. const fn = steps[step]
  1213. const updatedUpload = {
  1214. ...currentUpload,
  1215. step,
  1216. }
  1217. this.setState({
  1218. currentUploads: {
  1219. ...currentUploads,
  1220. [uploadID]: updatedUpload,
  1221. },
  1222. })
  1223. // TODO give this the `updatedUpload` object as its only parameter maybe?
  1224. // Otherwise when more metadata may be added to the upload this would keep getting more parameters
  1225. await fn(updatedUpload.fileIDs, uploadID)
  1226. // Update currentUpload value in case it was modified asynchronously.
  1227. currentUploads = this.getState().currentUploads
  1228. currentUpload = currentUploads[uploadID]
  1229. }
  1230. } catch (err) {
  1231. this.#removeUpload(uploadID)
  1232. throw err
  1233. }
  1234. // Set result data.
  1235. if (currentUpload) {
  1236. // Mark postprocessing step as complete if necessary; this addresses a case where we might get
  1237. // stuck in the postprocessing UI while the upload is fully complete.
  1238. // If the postprocessing steps do not do any work, they may not emit postprocessing events at
  1239. // all, and never mark the postprocessing as complete. This is fine on its own but we
  1240. // introduced code in the @uppy/core upload-success handler to prepare postprocessing progress
  1241. // state if any postprocessors are registered. That is to avoid a "flash of completed state"
  1242. // before the postprocessing plugins can emit events.
  1243. //
  1244. // So, just in case an upload with postprocessing plugins *has* completed *without* emitting
  1245. // postprocessing completion, we do it instead.
  1246. currentUpload.fileIDs.forEach((fileID) => {
  1247. const file = this.getFile(fileID)
  1248. if (file && file.progress.postprocess) {
  1249. this.emit('postprocess-complete', file)
  1250. }
  1251. })
  1252. const files = currentUpload.fileIDs.map((fileID) => this.getFile(fileID))
  1253. const successful = files.filter((file) => !file.error)
  1254. const failed = files.filter((file) => file.error)
  1255. await this.addResultData(uploadID, { successful, failed, uploadID })
  1256. // Update currentUpload value in case it was modified asynchronously.
  1257. currentUploads = this.getState().currentUploads
  1258. currentUpload = currentUploads[uploadID]
  1259. }
  1260. // Emit completion events.
  1261. // This is in a separate function so that the `currentUploads` variable
  1262. // always refers to the latest state. In the handler right above it refers
  1263. // to an outdated object without the `.result` property.
  1264. let result
  1265. if (currentUpload) {
  1266. result = currentUpload.result
  1267. this.emit('complete', result)
  1268. this.#removeUpload(uploadID)
  1269. }
  1270. if (result == null) {
  1271. this.log(`Not setting result for an upload that has been removed: ${uploadID}`)
  1272. }
  1273. return result
  1274. }
  1275. /**
  1276. * Start an upload for all the files that are not currently being uploaded.
  1277. *
  1278. * @returns {Promise}
  1279. */
  1280. upload () {
  1281. if (!this.#plugins.uploader?.length) {
  1282. this.log('No uploader type plugins are used', 'warning')
  1283. }
  1284. let { files } = this.getState()
  1285. const onBeforeUploadResult = this.opts.onBeforeUpload(files)
  1286. if (onBeforeUploadResult === false) {
  1287. return Promise.reject(new Error('Not starting the upload because onBeforeUpload returned false'))
  1288. }
  1289. if (onBeforeUploadResult && typeof onBeforeUploadResult === 'object') {
  1290. files = onBeforeUploadResult
  1291. // Updating files in state, because uploader plugins receive file IDs,
  1292. // and then fetch the actual file object from state
  1293. this.setState({
  1294. files,
  1295. })
  1296. }
  1297. return Promise.resolve()
  1298. .then(() => this.#restricter.validateMinNumberOfFiles(files))
  1299. .catch((err) => {
  1300. this.#informAndEmit(err)
  1301. throw err
  1302. })
  1303. .then(() => {
  1304. if (!this.#checkRequiredMetaFields(files)) {
  1305. throw new RestrictionError(this.i18n('missingRequiredMetaField'))
  1306. }
  1307. })
  1308. .catch((err) => {
  1309. // Doing this in a separate catch because we already emited and logged
  1310. // all the errors in `checkRequiredMetaFields` so we only throw a generic
  1311. // missing fields error here.
  1312. throw err
  1313. })
  1314. .then(() => {
  1315. const { currentUploads } = this.getState()
  1316. // get a list of files that are currently assigned to uploads
  1317. const currentlyUploadingFiles = Object.values(currentUploads).flatMap(curr => curr.fileIDs)
  1318. const waitingFileIDs = []
  1319. Object.keys(files).forEach((fileID) => {
  1320. const file = this.getFile(fileID)
  1321. // if the file hasn't started uploading and hasn't already been assigned to an upload..
  1322. if ((!file.progress.uploadStarted) && (currentlyUploadingFiles.indexOf(fileID) === -1)) {
  1323. waitingFileIDs.push(file.id)
  1324. }
  1325. })
  1326. const uploadID = this.#createUpload(waitingFileIDs)
  1327. return this.#runUpload(uploadID)
  1328. })
  1329. .catch((err) => {
  1330. this.emit('error', err)
  1331. this.log(err, 'error')
  1332. throw err
  1333. })
  1334. }
  1335. }
  1336. export default Uppy