Core.js 26 KB

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