index.js 47 KB

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