index.js 43 KB

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