Core.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  1. const Utils = require('../core/Utils')
  2. const Translator = require('../core/Translator')
  3. const UppySocket = require('./UppySocket')
  4. const ee = require('namespace-emitter')
  5. const cuid = require('cuid')
  6. const throttle = require('lodash.throttle')
  7. const prettyBytes = require('prettier-bytes')
  8. const match = require('mime-match')
  9. // const en_US = require('../locales/en_US')
  10. // const deepFreeze = require('deep-freeze-strict')
  11. /**
  12. * Main Uppy core
  13. *
  14. * @param {object} opts general options, like locales, to show modal or not to show
  15. */
  16. class Uppy {
  17. constructor (opts) {
  18. const defaultLocale = {
  19. strings: {
  20. youCanOnlyUploadX: {
  21. 0: 'You can only upload %{smart_count} file',
  22. 1: 'You can only upload %{smart_count} files'
  23. },
  24. youHaveToAtLeastSelectX: {
  25. 0: 'You have to select at least %{smart_count} file',
  26. 1: 'You have to select at least %{smart_count} files'
  27. },
  28. exceedsSize: 'This file exceeds maximum allowed size of',
  29. youCanOnlyUploadFileTypes: 'You can only upload:',
  30. uppyServerError: 'Connection with Uppy Server failed'
  31. }
  32. }
  33. // set default options
  34. const defaultOptions = {
  35. autoProceed: true,
  36. debug: false,
  37. restrictions: {
  38. maxFileSize: false,
  39. maxNumberOfFiles: false,
  40. minNumberOfFiles: false,
  41. allowedFileTypes: false
  42. },
  43. onBeforeFileAdded: (currentFile, files) => Promise.resolve(),
  44. onBeforeUpload: (files, done) => Promise.resolve(),
  45. locale: defaultLocale
  46. }
  47. // Merge default options with the ones set by user
  48. this.opts = Object.assign({}, defaultOptions, opts)
  49. // // Dictates in what order different plugin types are ran:
  50. // this.types = [ 'presetter', 'orchestrator', 'progressindicator',
  51. // 'acquirer', 'modifier', 'uploader', 'presenter', 'debugger']
  52. this.locale = Object.assign({}, defaultLocale, this.opts.locale)
  53. this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)
  54. // i18n
  55. this.translator = new Translator({locale: this.locale})
  56. this.i18n = this.translator.translate.bind(this.translator)
  57. // Container for different types of plugins
  58. this.plugins = {}
  59. // @TODO maybe bindall
  60. this.translator = new Translator({locale: this.opts.locale})
  61. this.i18n = this.translator.translate.bind(this.translator)
  62. this.getState = this.getState.bind(this)
  63. this.updateMeta = this.updateMeta.bind(this)
  64. this.initSocket = this.initSocket.bind(this)
  65. this.log = this.log.bind(this)
  66. this.addFile = this.addFile.bind(this)
  67. this.calculateProgress = this.calculateProgress.bind(this)
  68. this.resetProgress = this.resetProgress.bind(this)
  69. // this.bus = this.emitter = ee()
  70. this.emitter = ee()
  71. this.on = this.emitter.on.bind(this.emitter)
  72. this.off = this.emitter.off.bind(this.emitter)
  73. this.once = this.emitter.once.bind(this.emitter)
  74. this.emit = this.emitter.emit.bind(this.emitter)
  75. this.preProcessors = []
  76. this.uploaders = []
  77. this.postProcessors = []
  78. this.state = {
  79. files: {},
  80. capabilities: {
  81. resumableUploads: false
  82. },
  83. totalProgress: 0,
  84. meta: Object.assign({}, this.opts.meta),
  85. info: {
  86. isHidden: true,
  87. type: 'info',
  88. message: ''
  89. }
  90. }
  91. // for debugging and testing
  92. this.updateNum = 0
  93. if (this.opts.debug) {
  94. global.UppyState = this.state
  95. global.uppyLog = ''
  96. // global.UppyAddFile = this.addFile.bind(this)
  97. global._uppy = this
  98. }
  99. }
  100. /**
  101. * Iterate on all plugins and run `update` on them. Called each time state changes
  102. *
  103. */
  104. updateAll (state) {
  105. Object.keys(this.plugins).forEach((pluginType) => {
  106. this.plugins[pluginType].forEach((plugin) => {
  107. plugin.update(state)
  108. })
  109. })
  110. }
  111. /**
  112. * Updates state
  113. *
  114. * @param {newState} object
  115. */
  116. setState (stateUpdate) {
  117. const newState = Object.assign({}, this.state, stateUpdate)
  118. this.emit('core:state-update', this.state, newState, stateUpdate)
  119. this.state = newState
  120. this.updateAll(this.state)
  121. }
  122. /**
  123. * Returns current state
  124. *
  125. */
  126. getState () {
  127. // use deepFreeze for debugging
  128. // return deepFreeze(this.state)
  129. return this.state
  130. }
  131. reset () {
  132. this.emit('core:pause-all')
  133. this.emit('core:cancel-all')
  134. this.setState({
  135. totalProgress: 0
  136. })
  137. }
  138. resetProgress () {
  139. const defaultProgress = {
  140. percentage: 0,
  141. bytesUploaded: 0,
  142. uploadComplete: false,
  143. uploadStarted: false
  144. }
  145. const files = Object.assign({}, this.state.files)
  146. const updatedFiles = {}
  147. Object.keys(files).forEach(fileID => {
  148. const updatedFile = Object.assign({}, files[fileID])
  149. updatedFile.progress = Object.assign({}, updatedFile.progress, defaultProgress)
  150. updatedFiles[fileID] = updatedFile
  151. })
  152. console.log(updatedFiles)
  153. this.setState({
  154. files: updatedFiles,
  155. totalProgress: 0
  156. })
  157. }
  158. addPreProcessor (fn) {
  159. this.preProcessors.push(fn)
  160. }
  161. removePreProcessor (fn) {
  162. const i = this.preProcessors.indexOf(fn)
  163. if (i !== -1) {
  164. this.preProcessors.splice(i, 1)
  165. }
  166. }
  167. addPostProcessor (fn) {
  168. this.postProcessors.push(fn)
  169. }
  170. removePostProcessor (fn) {
  171. const i = this.postProcessors.indexOf(fn)
  172. if (i !== -1) {
  173. this.postProcessors.splice(i, 1)
  174. }
  175. }
  176. addUploader (fn) {
  177. this.uploaders.push(fn)
  178. }
  179. removeUploader (fn) {
  180. const i = this.uploaders.indexOf(fn)
  181. if (i !== -1) {
  182. this.uploaders.splice(i, 1)
  183. }
  184. }
  185. setMeta (data) {
  186. const newMeta = Object.assign({}, this.getState().meta, data)
  187. this.log('Adding metadata:')
  188. this.log(data)
  189. this.setState({meta: newMeta})
  190. }
  191. updateMeta (data, fileID) {
  192. const updatedFiles = Object.assign({}, this.getState().files)
  193. const newMeta = Object.assign({}, updatedFiles[fileID].meta, data)
  194. updatedFiles[fileID] = Object.assign({}, updatedFiles[fileID], {
  195. meta: newMeta
  196. })
  197. this.setState({files: updatedFiles})
  198. }
  199. checkRestrictions (checkMinNumberOfFiles, file, fileType) {
  200. const {maxFileSize, maxNumberOfFiles, minNumberOfFiles, allowedFileTypes} = this.opts.restrictions
  201. if (checkMinNumberOfFiles && minNumberOfFiles) {
  202. if (Object.keys(this.state.files).length < minNumberOfFiles) {
  203. this.info(`${this.i18n('youHaveToAtLeastSelectX', {smart_count: minNumberOfFiles})}`, 'error', 5000)
  204. return false
  205. }
  206. return true
  207. }
  208. if (maxNumberOfFiles) {
  209. if (Object.keys(this.state.files).length + 1 > maxNumberOfFiles) {
  210. this.info(`${this.i18n('youCanOnlyUploadX', {smart_count: maxNumberOfFiles})}`, 'error', 5000)
  211. return false
  212. }
  213. }
  214. if (allowedFileTypes) {
  215. const isCorrectFileType = allowedFileTypes.filter(match(fileType.join('/'))).length > 0
  216. if (!isCorrectFileType) {
  217. const allowedFileTypesString = allowedFileTypes.join(', ')
  218. this.info(`${this.i18n('youCanOnlyUploadFileTypes')} ${allowedFileTypesString}`, 'error', 5000)
  219. return false
  220. }
  221. }
  222. if (maxFileSize) {
  223. if (file.data.size > maxFileSize) {
  224. this.info(`${this.i18n('exceedsSize')} ${prettyBytes(maxFileSize)}`, 'error', 5000)
  225. return false
  226. }
  227. }
  228. return true
  229. }
  230. addFile (file) {
  231. return this.opts.onBeforeFileAdded(file, this.getState().files).catch((err) => {
  232. this.info(err, 'error', 5000)
  233. return Promise.reject(`onBeforeFileAdded: ${err}`)
  234. }).then(() => {
  235. return Utils.getFileType(file).then((fileType) => {
  236. const updatedFiles = Object.assign({}, this.state.files)
  237. const fileName = file.name || 'noname'
  238. const fileExtension = Utils.getFileNameAndExtension(fileName)[1]
  239. const isRemote = file.isRemote || false
  240. const fileID = Utils.generateFileID(file)
  241. const fileTypeGeneral = fileType[0]
  242. const fileTypeSpecific = fileType[1]
  243. const newFile = {
  244. source: file.source || '',
  245. id: fileID,
  246. name: fileName,
  247. extension: fileExtension || '',
  248. meta: Object.assign({}, { name: fileName }, this.getState().meta),
  249. type: {
  250. general: fileTypeGeneral,
  251. specific: fileTypeSpecific
  252. },
  253. data: file.data,
  254. progress: {
  255. percentage: 0,
  256. bytesUploaded: 0,
  257. bytesTotal: file.data.size || 0,
  258. uploadComplete: false,
  259. uploadStarted: false
  260. },
  261. size: file.data.size || 'N/A',
  262. isRemote: isRemote,
  263. remote: file.remote || '',
  264. preview: file.preview
  265. }
  266. const isFileAllowed = this.checkRestrictions(false, newFile, fileType)
  267. if (!isFileAllowed) return Promise.reject('File not allowed')
  268. updatedFiles[fileID] = newFile
  269. this.setState({files: updatedFiles})
  270. this.emit('core:file-added', newFile)
  271. this.log(`Added file: ${fileName}, ${fileID}, mime type: ${fileType}`)
  272. if (this.opts.autoProceed && !this.scheduledAutoProceed) {
  273. this.scheduledAutoProceed = setTimeout(() => {
  274. this.scheduledAutoProceed = null
  275. this.upload().catch((err) => {
  276. console.error(err.stack || err.message || err)
  277. })
  278. }, 4)
  279. }
  280. })
  281. })
  282. }
  283. /**
  284. * Get a file object.
  285. *
  286. * @param {string} fileID The ID of the file object to return.
  287. */
  288. getFile (fileID) {
  289. return this.getState().files[fileID]
  290. }
  291. /**
  292. * Generate a preview image for the given file, if possible.
  293. */
  294. generatePreview (file) {
  295. if (Utils.isPreviewSupported(file.type.specific) && !file.isRemote) {
  296. Utils.createThumbnail(file, 200).then((thumbnail) => {
  297. this.setPreviewURL(file.id, thumbnail)
  298. }).catch((err) => {
  299. console.warn(err.stack || err.message)
  300. })
  301. }
  302. }
  303. /**
  304. * Set the preview URL for a file.
  305. */
  306. setPreviewURL (fileID, preview) {
  307. const { files } = this.state
  308. this.setState({
  309. files: Object.assign({}, files, {
  310. [fileID]: Object.assign({}, files[fileID], {
  311. preview: preview
  312. })
  313. })
  314. })
  315. }
  316. removeFile (fileID) {
  317. const updatedFiles = Object.assign({}, this.getState().files)
  318. const removedFile = updatedFiles[fileID]
  319. delete updatedFiles[fileID]
  320. this.setState({files: updatedFiles})
  321. this.calculateTotalProgress()
  322. this.emit('core:file-removed', fileID)
  323. // Clean up object URLs.
  324. if (removedFile.preview && Utils.isObjectURL(removedFile.preview)) {
  325. URL.revokeObjectURL(removedFile.preview)
  326. }
  327. this.log(`Removed file: ${fileID}`)
  328. }
  329. calculateProgress (data) {
  330. const fileID = data.id
  331. const updatedFiles = Object.assign({}, this.getState().files)
  332. // skip progress event for a file that’s been removed
  333. if (!updatedFiles[fileID]) {
  334. this.log('Trying to set progress for a file that’s not with us anymore: ', fileID)
  335. return
  336. }
  337. const updatedFile = Object.assign({}, updatedFiles[fileID],
  338. Object.assign({}, {
  339. progress: Object.assign({}, updatedFiles[fileID].progress, {
  340. bytesUploaded: data.bytesUploaded,
  341. bytesTotal: data.bytesTotal,
  342. percentage: Math.floor((data.bytesUploaded / data.bytesTotal * 100).toFixed(2))
  343. })
  344. }
  345. ))
  346. updatedFiles[data.id] = updatedFile
  347. this.setState({
  348. files: updatedFiles
  349. })
  350. this.calculateTotalProgress()
  351. }
  352. calculateTotalProgress () {
  353. // calculate total progress, using the number of files currently uploading,
  354. // multiplied by 100 and the summ of individual progress of each file
  355. const files = Object.assign({}, this.getState().files)
  356. const inProgress = Object.keys(files).filter((file) => {
  357. return files[file].progress.uploadStarted
  358. })
  359. const progressMax = inProgress.length * 100
  360. let progressAll = 0
  361. inProgress.forEach((file) => {
  362. progressAll = progressAll + files[file].progress.percentage
  363. })
  364. const totalProgress = Math.floor((progressAll * 100 / progressMax).toFixed(2))
  365. this.setState({
  366. totalProgress: totalProgress
  367. })
  368. }
  369. /**
  370. * Registers listeners for all global actions, like:
  371. * `file-add`, `file-remove`, `upload-progress`, `reset`
  372. *
  373. */
  374. actions () {
  375. // this.bus.on('*', (payload) => {
  376. // console.log('emitted: ', this.event)
  377. // console.log('with payload: ', payload)
  378. // })
  379. // stress-test re-rendering
  380. // setInterval(() => {
  381. // this.setState({bla: 'bla'})
  382. // }, 20)
  383. this.on('core:error', (error) => {
  384. this.setState({ error })
  385. })
  386. this.on('core:upload-error', (fileID, error) => {
  387. const fileName = this.state.files[fileID].name
  388. this.info(`Failed to upload: ${fileName}`, 'error', 5000)
  389. })
  390. this.on('core:upload', () => {
  391. this.setState({ error: null })
  392. })
  393. this.on('core:file-add', (data) => {
  394. this.addFile(data)
  395. })
  396. this.on('core:file-added', (file) => {
  397. this.generatePreview(file)
  398. })
  399. // `remove-file` removes a file from `state.files`, for example when
  400. // a user decides not to upload particular file and clicks a button to remove it
  401. this.on('core:file-remove', (fileID) => {
  402. this.removeFile(fileID)
  403. })
  404. this.on('core:cancel-all', () => {
  405. // let updatedFiles = this.getState().files
  406. // updatedFiles = {}
  407. this.setState({files: {}})
  408. })
  409. this.on('core:upload-started', (fileID, upload) => {
  410. const updatedFiles = Object.assign({}, this.getState().files)
  411. const updatedFile = Object.assign({}, updatedFiles[fileID],
  412. Object.assign({}, {
  413. progress: Object.assign({}, updatedFiles[fileID].progress, {
  414. uploadStarted: Date.now()
  415. })
  416. }
  417. ))
  418. updatedFiles[fileID] = updatedFile
  419. this.setState({files: updatedFiles})
  420. })
  421. // upload progress events can occur frequently, especially when you have a good
  422. // connection to the remote server. Therefore, we are throtteling them to
  423. // prevent accessive function calls.
  424. // see also: https://github.com/tus/tus-js-client/commit/9940f27b2361fd7e10ba58b09b60d82422183bbb
  425. const throttledCalculateProgress = throttle(this.calculateProgress, 100, {leading: true, trailing: false})
  426. this.on('core:upload-progress', (data) => {
  427. // this.calculateProgress(data)
  428. throttledCalculateProgress(data)
  429. })
  430. this.on('core:upload-success', (fileID, uploadResp, uploadURL) => {
  431. const updatedFiles = Object.assign({}, this.getState().files)
  432. const updatedFile = Object.assign({}, updatedFiles[fileID], {
  433. progress: Object.assign({}, updatedFiles[fileID].progress, {
  434. uploadComplete: true,
  435. // good or bad idea? setting the percentage to 100 if upload is successful,
  436. // so that if we lost some progress events on the way, its still marked “compete”?
  437. percentage: 100
  438. }),
  439. uploadURL: uploadURL
  440. })
  441. updatedFiles[fileID] = updatedFile
  442. this.setState({
  443. files: updatedFiles
  444. })
  445. this.calculateTotalProgress()
  446. if (this.getState().totalProgress === 100) {
  447. const completeFiles = Object.keys(updatedFiles).filter((file) => {
  448. return updatedFiles[file].progress.uploadComplete
  449. })
  450. this.emit('core:upload-complete', completeFiles.length)
  451. }
  452. })
  453. this.on('core:update-meta', (data, fileID) => {
  454. this.updateMeta(data, fileID)
  455. })
  456. this.on('core:preprocess-progress', (fileID, progress) => {
  457. const files = Object.assign({}, this.getState().files)
  458. files[fileID] = Object.assign({}, files[fileID], {
  459. progress: Object.assign({}, files[fileID].progress, {
  460. preprocess: progress
  461. })
  462. })
  463. this.setState({ files: files })
  464. })
  465. this.on('core:preprocess-complete', (fileID) => {
  466. const files = Object.assign({}, this.getState().files)
  467. files[fileID] = Object.assign({}, files[fileID], {
  468. progress: Object.assign({}, files[fileID].progress)
  469. })
  470. delete files[fileID].progress.preprocess
  471. this.setState({ files: files })
  472. })
  473. this.on('core:postprocess-progress', (fileID, progress) => {
  474. const files = Object.assign({}, this.getState().files)
  475. files[fileID] = Object.assign({}, files[fileID], {
  476. progress: Object.assign({}, files[fileID].progress, {
  477. postprocess: progress
  478. })
  479. })
  480. this.setState({ files: files })
  481. })
  482. this.on('core:postprocess-complete', (fileID) => {
  483. const files = Object.assign({}, this.getState().files)
  484. files[fileID] = Object.assign({}, files[fileID], {
  485. progress: Object.assign({}, files[fileID].progress)
  486. })
  487. delete files[fileID].progress.postprocess
  488. // TODO should we set some kind of `fullyComplete` property on the file object
  489. // so it's easier to see that the file is upload…fully complete…rather than
  490. // what we have to do now (`uploadComplete && !postprocess`)
  491. this.setState({ files: files })
  492. })
  493. // show informer if offline
  494. if (typeof window !== 'undefined') {
  495. window.addEventListener('online', () => this.isOnline(true))
  496. window.addEventListener('offline', () => this.isOnline(false))
  497. setTimeout(() => this.isOnline(), 3000)
  498. }
  499. }
  500. isOnline (status) {
  501. const online = status || window.navigator.onLine
  502. if (!online) {
  503. this.emit('is-offline')
  504. this.info('No internet connection', 'error', 0)
  505. this.wasOffline = true
  506. } else {
  507. this.emit('is-online')
  508. if (this.wasOffline) {
  509. this.emit('back-online')
  510. this.info('Connected!', 'success', 3000)
  511. this.wasOffline = false
  512. }
  513. }
  514. }
  515. /**
  516. * Registers a plugin with Core
  517. *
  518. * @param {Class} Plugin object
  519. * @param {Object} options object that will be passed to Plugin later
  520. * @return {Object} self for chaining
  521. */
  522. use (Plugin, opts) {
  523. // Instantiate
  524. const plugin = new Plugin(this, opts)
  525. const pluginName = plugin.id
  526. this.plugins[plugin.type] = this.plugins[plugin.type] || []
  527. if (!pluginName) {
  528. throw new Error('Your plugin must have a name')
  529. }
  530. if (!plugin.type) {
  531. throw new Error('Your plugin must have a type')
  532. }
  533. let existsPluginAlready = this.getPlugin(pluginName)
  534. if (existsPluginAlready) {
  535. let msg = `Already found a plugin named '${existsPluginAlready.name}'.
  536. Tried to use: '${pluginName}'.
  537. Uppy is currently limited to running one of every plugin.
  538. Share your use case with us over at
  539. https://github.com/transloadit/uppy/issues/
  540. if you want us to reconsider.`
  541. throw new Error(msg)
  542. }
  543. this.plugins[plugin.type].push(plugin)
  544. plugin.install()
  545. return this
  546. }
  547. /**
  548. * Find one Plugin by name
  549. *
  550. * @param string name description
  551. */
  552. getPlugin (name) {
  553. let foundPlugin = false
  554. this.iteratePlugins((plugin) => {
  555. const pluginName = plugin.id
  556. if (pluginName === name) {
  557. foundPlugin = plugin
  558. return false
  559. }
  560. })
  561. return foundPlugin
  562. }
  563. /**
  564. * Iterate through all `use`d plugins
  565. *
  566. * @param function method description
  567. */
  568. iteratePlugins (method) {
  569. Object.keys(this.plugins).forEach((pluginType) => {
  570. this.plugins[pluginType].forEach(method)
  571. })
  572. }
  573. /**
  574. * Uninstall and remove a plugin.
  575. *
  576. * @param {Plugin} instance The plugin instance to remove.
  577. */
  578. removePlugin (instance) {
  579. const list = this.plugins[instance.type]
  580. if (instance.uninstall) {
  581. instance.uninstall()
  582. }
  583. const index = list.indexOf(instance)
  584. if (index !== -1) {
  585. list.splice(index, 1)
  586. }
  587. }
  588. /**
  589. * Uninstall all plugins and close down this Uppy instance.
  590. */
  591. close () {
  592. this.reset()
  593. this.iteratePlugins((plugin) => {
  594. plugin.uninstall()
  595. })
  596. if (this.socket) {
  597. this.socket.close()
  598. }
  599. }
  600. /**
  601. * Set info message in `state.info`, so that UI plugins like `Informer`
  602. * can display the message
  603. *
  604. * @param {string} msg Message to be displayed by the informer
  605. */
  606. info (message, type, duration) {
  607. const isComplexMessage = typeof message === 'object'
  608. this.setState({
  609. info: {
  610. isHidden: false,
  611. type: type || 'info',
  612. message: isComplexMessage ? message.message : message,
  613. details: isComplexMessage ? message.details : null
  614. }
  615. })
  616. this.emit('core:info-visible')
  617. window.clearTimeout(this.infoTimeoutID)
  618. if (duration === 0) {
  619. this.infoTimeoutID = undefined
  620. return
  621. }
  622. // hide the informer after `duration` milliseconds
  623. this.infoTimeoutID = setTimeout(() => {
  624. const newInformer = Object.assign({}, this.state.info, {
  625. isHidden: true
  626. })
  627. this.setState({
  628. info: newInformer
  629. })
  630. this.emit('core:info-hidden')
  631. }, duration)
  632. }
  633. hideInfo () {
  634. const newInfo = Object.assign({}, this.core.state.info, {
  635. isHidden: true
  636. })
  637. this.setState({
  638. info: newInfo
  639. })
  640. this.emit('core:info-hidden')
  641. }
  642. /**
  643. * Logs stuff to console, only if `debug` is set to true. Silent in production.
  644. *
  645. * @return {String|Object} to log
  646. */
  647. log (msg, type) {
  648. if (!this.opts.debug) {
  649. return
  650. }
  651. if (type === 'error') {
  652. console.error(`LOG: ${msg}`)
  653. return
  654. }
  655. if (msg === `${msg}`) {
  656. console.log(`LOG: ${msg}`)
  657. } else {
  658. console.dir(msg)
  659. }
  660. global.uppyLog = global.uppyLog + '\n' + 'DEBUG LOG: ' + msg
  661. }
  662. initSocket (opts) {
  663. if (!this.socket) {
  664. this.socket = new UppySocket(opts)
  665. }
  666. return this.socket
  667. }
  668. /**
  669. * Initializes actions, installs all plugins (by iterating on them and calling `install`), sets options
  670. *
  671. */
  672. run () {
  673. this.log('Core is run, initializing actions...')
  674. this.actions()
  675. // Forse set `autoProceed` option to false if there are multiple selector Plugins active
  676. // if (this.plugins.acquirer && this.plugins.acquirer.length > 1) {
  677. // this.opts.autoProceed = false
  678. // }
  679. // Install all plugins
  680. // this.installAll()
  681. return this
  682. }
  683. /**
  684. * Restore an upload by its ID.
  685. */
  686. restore (uploadID) {
  687. this.log(`Core: attempting to restore upload "${uploadID}"`)
  688. if (!this.state.currentUploads[uploadID]) {
  689. this.removeUpload(uploadID)
  690. return Promise.reject(new Error('Nonexistent upload'))
  691. }
  692. return this.runUpload(uploadID)
  693. }
  694. /**
  695. * Create an upload for a bunch of files.
  696. *
  697. * @param {Array<string>} fileIDs File IDs to include in this upload.
  698. * @return {string} ID of this upload.
  699. */
  700. createUpload (fileIDs) {
  701. const uploadID = cuid()
  702. this.emit('core:upload', {
  703. id: uploadID,
  704. fileIDs: fileIDs
  705. })
  706. this.setState({
  707. currentUploads: Object.assign({}, this.state.currentUploads, {
  708. [uploadID]: {
  709. fileIDs: fileIDs,
  710. step: 0
  711. }
  712. })
  713. })
  714. return uploadID
  715. }
  716. /**
  717. * Remove an upload, eg. if it has been canceled or completed.
  718. *
  719. * @param {string} uploadID The ID of the upload.
  720. */
  721. removeUpload (uploadID) {
  722. const currentUploads = Object.assign({}, this.state.currentUploads)
  723. delete currentUploads[uploadID]
  724. this.setState({
  725. currentUploads: currentUploads
  726. })
  727. }
  728. /**
  729. * Run an upload. This picks up where it left off in case the upload is being restored.
  730. *
  731. * @private
  732. */
  733. runUpload (uploadID) {
  734. const uploadData = this.state.currentUploads[uploadID]
  735. const fileIDs = uploadData.fileIDs
  736. const restoreStep = uploadData.step
  737. const steps = [
  738. ...this.preProcessors,
  739. ...this.uploaders,
  740. ...this.postProcessors
  741. ]
  742. let lastStep = Promise.resolve()
  743. steps.forEach((fn, step) => {
  744. // Skip this step if we are restoring and have already completed this step before.
  745. if (step < restoreStep) {
  746. return
  747. }
  748. lastStep = lastStep.then(() => {
  749. const currentUpload = Object.assign({}, this.state.currentUploads[uploadID], {
  750. step: step
  751. })
  752. this.setState({
  753. currentUploads: Object.assign({}, this.state.currentUploads, {
  754. [uploadID]: currentUpload
  755. })
  756. })
  757. // TODO give this the `currentUpload` object as its only parameter maybe?
  758. // Otherwise when more metadata may be added to the upload this would keep getting more parameters
  759. return fn(fileIDs, uploadID)
  760. })
  761. })
  762. // Not returning the `catch`ed promise, because we still want to return a rejected
  763. // promise from this method if the upload failed.
  764. lastStep.catch((err) => {
  765. this.emit('core:error', err)
  766. this.removeUpload(uploadID)
  767. })
  768. return lastStep.then(() => {
  769. // return number of uploaded files
  770. this.emit('core:success', fileIDs)
  771. this.removeUpload(uploadID)
  772. })
  773. }
  774. /**
  775. * Start an upload for all the files that are not currently being uploaded.
  776. *
  777. * @return {Promise}
  778. */
  779. upload (forceUpload) {
  780. const isMinNumberOfFilesReached = this.checkRestrictions(true)
  781. if (!isMinNumberOfFilesReached) {
  782. return Promise.reject('Minimum number of files has not been reached')
  783. }
  784. return this.opts.onBeforeUpload(this.state.files).catch((err) => {
  785. this.info(err, 'error', 5000)
  786. return Promise.reject(`onBeforeUpload: ${err}`)
  787. }).then(() => {
  788. const waitingFileIDs = []
  789. Object.keys(this.state.files).forEach((fileID) => {
  790. const file = this.getFile(fileID)
  791. // TODO: replace files[file].isRemote with some logic
  792. //
  793. // filter files that are now yet being uploaded / haven’t been uploaded
  794. // and remote too
  795. if (forceUpload) {
  796. this.resetProgress()
  797. waitingFileIDs.push(file.id)
  798. } else if (!file.progress.uploadStarted || file.isRemote) {
  799. waitingFileIDs.push(file.id)
  800. }
  801. })
  802. const uploadID = this.createUpload(waitingFileIDs)
  803. return this.runUpload(uploadID)
  804. })
  805. }
  806. }
  807. module.exports = function (opts) {
  808. if (!(this instanceof Uppy)) {
  809. return new Uppy(opts)
  810. }
  811. }