index.js 46 KB

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