123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945 |
- const Utils = require('../core/Utils')
- const Translator = require('../core/Translator')
- const UppySocket = require('./UppySocket')
- const ee = require('namespace-emitter')
- const cuid = require('cuid')
- const throttle = require('lodash.throttle')
- const prettyBytes = require('prettier-bytes')
- const match = require('mime-match')
- // const en_US = require('../locales/en_US')
- // const deepFreeze = require('deep-freeze-strict')
- /**
- * Main Uppy core
- *
- * @param {object} opts general options, like locales, to show modal or not to show
- */
- class Uppy {
- constructor (opts) {
- const defaultLocale = {
- strings: {
- youCanOnlyUploadX: {
- 0: 'You can only upload %{smart_count} file',
- 1: 'You can only upload %{smart_count} files'
- },
- youHaveToAtLeastSelectX: {
- 0: 'You have to select at least %{smart_count} file',
- 1: 'You have to select at least %{smart_count} files'
- },
- exceedsSize: 'This file exceeds maximum allowed size of',
- youCanOnlyUploadFileTypes: 'You can only upload:',
- uppyServerError: 'Connection with Uppy Server failed'
- }
- }
- // set default options
- const defaultOptions = {
- autoProceed: true,
- debug: false,
- restrictions: {
- maxFileSize: false,
- maxNumberOfFiles: false,
- minNumberOfFiles: false,
- allowedFileTypes: false
- },
- onBeforeFileAdded: (currentFile, files) => Promise.resolve(),
- onBeforeUpload: (files, done) => Promise.resolve(),
- locale: defaultLocale
- }
- // Merge default options with the ones set by user
- this.opts = Object.assign({}, defaultOptions, opts)
- // // Dictates in what order different plugin types are ran:
- // this.types = [ 'presetter', 'orchestrator', 'progressindicator',
- // 'acquirer', 'modifier', 'uploader', 'presenter', 'debugger']
- this.locale = Object.assign({}, defaultLocale, this.opts.locale)
- this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)
- // i18n
- this.translator = new Translator({locale: this.locale})
- this.i18n = this.translator.translate.bind(this.translator)
- // Container for different types of plugins
- this.plugins = {}
- // @TODO maybe bindall
- this.translator = new Translator({locale: this.opts.locale})
- this.i18n = this.translator.translate.bind(this.translator)
- this.getState = this.getState.bind(this)
- this.updateMeta = this.updateMeta.bind(this)
- this.initSocket = this.initSocket.bind(this)
- this.log = this.log.bind(this)
- this.addFile = this.addFile.bind(this)
- this.calculateProgress = this.calculateProgress.bind(this)
- this.resetProgress = this.resetProgress.bind(this)
- // this.bus = this.emitter = ee()
- this.emitter = ee()
- this.on = this.emitter.on.bind(this.emitter)
- this.off = this.emitter.off.bind(this.emitter)
- this.once = this.emitter.once.bind(this.emitter)
- this.emit = this.emitter.emit.bind(this.emitter)
- this.preProcessors = []
- this.uploaders = []
- this.postProcessors = []
- this.state = {
- files: {},
- capabilities: {
- resumableUploads: false
- },
- totalProgress: 0,
- meta: Object.assign({}, this.opts.meta),
- info: {
- isHidden: true,
- type: 'info',
- message: ''
- }
- }
- // for debugging and testing
- this.updateNum = 0
- if (this.opts.debug) {
- global.UppyState = this.state
- global.uppyLog = ''
- // global.UppyAddFile = this.addFile.bind(this)
- global._uppy = this
- }
- }
- /**
- * Iterate on all plugins and run `update` on them. Called each time state changes
- *
- */
- updateAll (state) {
- Object.keys(this.plugins).forEach((pluginType) => {
- this.plugins[pluginType].forEach((plugin) => {
- plugin.update(state)
- })
- })
- }
- /**
- * Updates state
- *
- * @param {newState} object
- */
- setState (stateUpdate) {
- const newState = Object.assign({}, this.state, stateUpdate)
- this.emit('core:state-update', this.state, newState, stateUpdate)
- this.state = newState
- this.updateAll(this.state)
- }
- /**
- * Returns current state
- *
- */
- getState () {
- // use deepFreeze for debugging
- // return deepFreeze(this.state)
- return this.state
- }
- reset () {
- this.emit('core:pause-all')
- this.emit('core:cancel-all')
- this.setState({
- totalProgress: 0
- })
- }
- resetProgress () {
- const defaultProgress = {
- percentage: 0,
- bytesUploaded: 0,
- uploadComplete: false,
- uploadStarted: false
- }
- const files = Object.assign({}, this.state.files)
- const updatedFiles = {}
- Object.keys(files).forEach(fileID => {
- const updatedFile = Object.assign({}, files[fileID])
- updatedFile.progress = Object.assign({}, updatedFile.progress, defaultProgress)
- updatedFiles[fileID] = updatedFile
- })
- console.log(updatedFiles)
- this.setState({
- files: updatedFiles,
- totalProgress: 0
- })
- }
- addPreProcessor (fn) {
- this.preProcessors.push(fn)
- }
- removePreProcessor (fn) {
- const i = this.preProcessors.indexOf(fn)
- if (i !== -1) {
- this.preProcessors.splice(i, 1)
- }
- }
- addPostProcessor (fn) {
- this.postProcessors.push(fn)
- }
- removePostProcessor (fn) {
- const i = this.postProcessors.indexOf(fn)
- if (i !== -1) {
- this.postProcessors.splice(i, 1)
- }
- }
- addUploader (fn) {
- this.uploaders.push(fn)
- }
- removeUploader (fn) {
- const i = this.uploaders.indexOf(fn)
- if (i !== -1) {
- this.uploaders.splice(i, 1)
- }
- }
- setMeta (data) {
- const newMeta = Object.assign({}, this.getState().meta, data)
- this.log('Adding metadata:')
- this.log(data)
- this.setState({meta: newMeta})
- }
- updateMeta (data, fileID) {
- const updatedFiles = Object.assign({}, this.getState().files)
- const newMeta = Object.assign({}, updatedFiles[fileID].meta, data)
- updatedFiles[fileID] = Object.assign({}, updatedFiles[fileID], {
- meta: newMeta
- })
- this.setState({files: updatedFiles})
- }
- checkRestrictions (checkMinNumberOfFiles, file, fileType) {
- const {maxFileSize, maxNumberOfFiles, minNumberOfFiles, allowedFileTypes} = this.opts.restrictions
- if (checkMinNumberOfFiles && minNumberOfFiles) {
- if (Object.keys(this.state.files).length < minNumberOfFiles) {
- this.info(`${this.i18n('youHaveToAtLeastSelectX', {smart_count: minNumberOfFiles})}`, 'error', 5000)
- return false
- }
- return true
- }
- if (maxNumberOfFiles) {
- if (Object.keys(this.state.files).length + 1 > maxNumberOfFiles) {
- this.info(`${this.i18n('youCanOnlyUploadX', {smart_count: maxNumberOfFiles})}`, 'error', 5000)
- return false
- }
- }
- if (allowedFileTypes) {
- const isCorrectFileType = allowedFileTypes.filter(match(fileType.join('/'))).length > 0
- if (!isCorrectFileType) {
- const allowedFileTypesString = allowedFileTypes.join(', ')
- this.info(`${this.i18n('youCanOnlyUploadFileTypes')} ${allowedFileTypesString}`, 'error', 5000)
- return false
- }
- }
- if (maxFileSize) {
- if (file.data.size > maxFileSize) {
- this.info(`${this.i18n('exceedsSize')} ${prettyBytes(maxFileSize)}`, 'error', 5000)
- return false
- }
- }
- return true
- }
- addFile (file) {
- return this.opts.onBeforeFileAdded(file, this.getState().files).catch((err) => {
- this.info(err, 'error', 5000)
- return Promise.reject(`onBeforeFileAdded: ${err}`)
- }).then(() => {
- return Utils.getFileType(file).then((fileType) => {
- const updatedFiles = Object.assign({}, this.state.files)
- const fileName = file.name || 'noname'
- const fileExtension = Utils.getFileNameAndExtension(fileName)[1]
- const isRemote = file.isRemote || false
- const fileID = Utils.generateFileID(file)
- const fileTypeGeneral = fileType[0]
- const fileTypeSpecific = fileType[1]
- const newFile = {
- source: file.source || '',
- id: fileID,
- name: fileName,
- extension: fileExtension || '',
- meta: Object.assign({}, { name: fileName }, this.getState().meta),
- type: {
- general: fileTypeGeneral,
- specific: fileTypeSpecific
- },
- data: file.data,
- progress: {
- percentage: 0,
- bytesUploaded: 0,
- bytesTotal: file.data.size || 0,
- uploadComplete: false,
- uploadStarted: false
- },
- size: file.data.size || 'N/A',
- isRemote: isRemote,
- remote: file.remote || '',
- preview: file.preview
- }
- const isFileAllowed = this.checkRestrictions(false, newFile, fileType)
- if (!isFileAllowed) return Promise.reject('File not allowed')
- updatedFiles[fileID] = newFile
- this.setState({files: updatedFiles})
- this.emit('core:file-added', newFile)
- this.log(`Added file: ${fileName}, ${fileID}, mime type: ${fileType}`)
- if (this.opts.autoProceed && !this.scheduledAutoProceed) {
- this.scheduledAutoProceed = setTimeout(() => {
- this.scheduledAutoProceed = null
- this.upload().catch((err) => {
- console.error(err.stack || err.message || err)
- })
- }, 4)
- }
- })
- })
- }
- /**
- * Get a file object.
- *
- * @param {string} fileID The ID of the file object to return.
- */
- getFile (fileID) {
- return this.getState().files[fileID]
- }
- /**
- * Generate a preview image for the given file, if possible.
- */
- generatePreview (file) {
- if (Utils.isPreviewSupported(file.type.specific) && !file.isRemote) {
- Utils.createThumbnail(file, 200).then((thumbnail) => {
- this.setPreviewURL(file.id, thumbnail)
- }).catch((err) => {
- console.warn(err.stack || err.message)
- })
- }
- }
- /**
- * Set the preview URL for a file.
- */
- setPreviewURL (fileID, preview) {
- const { files } = this.state
- this.setState({
- files: Object.assign({}, files, {
- [fileID]: Object.assign({}, files[fileID], {
- preview: preview
- })
- })
- })
- }
- removeFile (fileID) {
- const updatedFiles = Object.assign({}, this.getState().files)
- const removedFile = updatedFiles[fileID]
- delete updatedFiles[fileID]
- this.setState({files: updatedFiles})
- this.calculateTotalProgress()
- this.emit('core:file-removed', fileID)
- // Clean up object URLs.
- if (removedFile.preview && Utils.isObjectURL(removedFile.preview)) {
- URL.revokeObjectURL(removedFile.preview)
- }
- this.log(`Removed file: ${fileID}`)
- }
- calculateProgress (data) {
- const fileID = data.id
- const updatedFiles = Object.assign({}, this.getState().files)
- // skip progress event for a file that’s been removed
- if (!updatedFiles[fileID]) {
- this.log('Trying to set progress for a file that’s not with us anymore: ', fileID)
- return
- }
- const updatedFile = Object.assign({}, updatedFiles[fileID],
- Object.assign({}, {
- progress: Object.assign({}, updatedFiles[fileID].progress, {
- bytesUploaded: data.bytesUploaded,
- bytesTotal: data.bytesTotal,
- percentage: Math.floor((data.bytesUploaded / data.bytesTotal * 100).toFixed(2))
- })
- }
- ))
- updatedFiles[data.id] = updatedFile
- this.setState({
- files: updatedFiles
- })
- this.calculateTotalProgress()
- }
- calculateTotalProgress () {
- // calculate total progress, using the number of files currently uploading,
- // multiplied by 100 and the summ of individual progress of each file
- const files = Object.assign({}, this.getState().files)
- const inProgress = Object.keys(files).filter((file) => {
- return files[file].progress.uploadStarted
- })
- const progressMax = inProgress.length * 100
- let progressAll = 0
- inProgress.forEach((file) => {
- progressAll = progressAll + files[file].progress.percentage
- })
- const totalProgress = Math.floor((progressAll * 100 / progressMax).toFixed(2))
- this.setState({
- totalProgress: totalProgress
- })
- }
- /**
- * Registers listeners for all global actions, like:
- * `file-add`, `file-remove`, `upload-progress`, `reset`
- *
- */
- actions () {
- // this.bus.on('*', (payload) => {
- // console.log('emitted: ', this.event)
- // console.log('with payload: ', payload)
- // })
- // stress-test re-rendering
- // setInterval(() => {
- // this.setState({bla: 'bla'})
- // }, 20)
- this.on('core:error', (error) => {
- this.setState({ error })
- })
- this.on('core:upload-error', (fileID, error) => {
- const fileName = this.state.files[fileID].name
- this.info(`Failed to upload: ${fileName}`, 'error', 5000)
- })
- this.on('core:upload', () => {
- this.setState({ error: null })
- })
- this.on('core:file-add', (data) => {
- this.addFile(data)
- })
- this.on('core:file-added', (file) => {
- this.generatePreview(file)
- })
- // `remove-file` removes a file from `state.files`, for example when
- // a user decides not to upload particular file and clicks a button to remove it
- this.on('core:file-remove', (fileID) => {
- this.removeFile(fileID)
- })
- this.on('core:cancel-all', () => {
- // let updatedFiles = this.getState().files
- // updatedFiles = {}
- this.setState({files: {}})
- })
- this.on('core:upload-started', (fileID, upload) => {
- const updatedFiles = Object.assign({}, this.getState().files)
- const updatedFile = Object.assign({}, updatedFiles[fileID],
- Object.assign({}, {
- progress: Object.assign({}, updatedFiles[fileID].progress, {
- uploadStarted: Date.now()
- })
- }
- ))
- updatedFiles[fileID] = updatedFile
- this.setState({files: updatedFiles})
- })
- // upload progress events can occur frequently, especially when you have a good
- // connection to the remote server. Therefore, we are throtteling them to
- // prevent accessive function calls.
- // see also: https://github.com/tus/tus-js-client/commit/9940f27b2361fd7e10ba58b09b60d82422183bbb
- const throttledCalculateProgress = throttle(this.calculateProgress, 100, {leading: true, trailing: false})
- this.on('core:upload-progress', (data) => {
- // this.calculateProgress(data)
- throttledCalculateProgress(data)
- })
- this.on('core:upload-success', (fileID, uploadResp, uploadURL) => {
- const updatedFiles = Object.assign({}, this.getState().files)
- const updatedFile = Object.assign({}, updatedFiles[fileID], {
- progress: Object.assign({}, updatedFiles[fileID].progress, {
- uploadComplete: true,
- // good or bad idea? setting the percentage to 100 if upload is successful,
- // so that if we lost some progress events on the way, its still marked “compete”?
- percentage: 100
- }),
- uploadURL: uploadURL
- })
- updatedFiles[fileID] = updatedFile
- this.setState({
- files: updatedFiles
- })
- this.calculateTotalProgress()
- if (this.getState().totalProgress === 100) {
- const completeFiles = Object.keys(updatedFiles).filter((file) => {
- return updatedFiles[file].progress.uploadComplete
- })
- this.emit('core:upload-complete', completeFiles.length)
- }
- })
- this.on('core:update-meta', (data, fileID) => {
- this.updateMeta(data, fileID)
- })
- this.on('core:preprocess-progress', (fileID, progress) => {
- const files = Object.assign({}, this.getState().files)
- files[fileID] = Object.assign({}, files[fileID], {
- progress: Object.assign({}, files[fileID].progress, {
- preprocess: progress
- })
- })
- this.setState({ files: files })
- })
- this.on('core:preprocess-complete', (fileID) => {
- const files = Object.assign({}, this.getState().files)
- files[fileID] = Object.assign({}, files[fileID], {
- progress: Object.assign({}, files[fileID].progress)
- })
- delete files[fileID].progress.preprocess
- this.setState({ files: files })
- })
- this.on('core:postprocess-progress', (fileID, progress) => {
- const files = Object.assign({}, this.getState().files)
- files[fileID] = Object.assign({}, files[fileID], {
- progress: Object.assign({}, files[fileID].progress, {
- postprocess: progress
- })
- })
- this.setState({ files: files })
- })
- this.on('core:postprocess-complete', (fileID) => {
- const files = Object.assign({}, this.getState().files)
- files[fileID] = Object.assign({}, files[fileID], {
- progress: Object.assign({}, files[fileID].progress)
- })
- delete files[fileID].progress.postprocess
- // TODO should we set some kind of `fullyComplete` property on the file object
- // so it's easier to see that the file is upload…fully complete…rather than
- // what we have to do now (`uploadComplete && !postprocess`)
- this.setState({ files: files })
- })
- // show informer if offline
- if (typeof window !== 'undefined') {
- window.addEventListener('online', () => this.isOnline(true))
- window.addEventListener('offline', () => this.isOnline(false))
- setTimeout(() => this.isOnline(), 3000)
- }
- }
- isOnline (status) {
- const online = status || window.navigator.onLine
- if (!online) {
- this.emit('is-offline')
- this.info('No internet connection', 'error', 0)
- this.wasOffline = true
- } else {
- this.emit('is-online')
- if (this.wasOffline) {
- this.emit('back-online')
- this.info('Connected!', 'success', 3000)
- this.wasOffline = false
- }
- }
- }
- /**
- * Registers a plugin with Core
- *
- * @param {Class} Plugin object
- * @param {Object} options object that will be passed to Plugin later
- * @return {Object} self for chaining
- */
- use (Plugin, opts) {
- // Instantiate
- const plugin = new Plugin(this, opts)
- const pluginName = plugin.id
- this.plugins[plugin.type] = this.plugins[plugin.type] || []
- if (!pluginName) {
- throw new Error('Your plugin must have a name')
- }
- if (!plugin.type) {
- throw new Error('Your plugin must have a type')
- }
- let existsPluginAlready = this.getPlugin(pluginName)
- if (existsPluginAlready) {
- let msg = `Already found a plugin named '${existsPluginAlready.name}'.
- Tried to use: '${pluginName}'.
- Uppy is currently limited to running one of every plugin.
- Share your use case with us over at
- https://github.com/transloadit/uppy/issues/
- if you want us to reconsider.`
- throw new Error(msg)
- }
- this.plugins[plugin.type].push(plugin)
- plugin.install()
- return this
- }
- /**
- * Find one Plugin by name
- *
- * @param string name description
- */
- getPlugin (name) {
- let foundPlugin = false
- this.iteratePlugins((plugin) => {
- const pluginName = plugin.id
- if (pluginName === name) {
- foundPlugin = plugin
- return false
- }
- })
- return foundPlugin
- }
- /**
- * Iterate through all `use`d plugins
- *
- * @param function method description
- */
- iteratePlugins (method) {
- Object.keys(this.plugins).forEach((pluginType) => {
- this.plugins[pluginType].forEach(method)
- })
- }
- /**
- * Uninstall and remove a plugin.
- *
- * @param {Plugin} instance The plugin instance to remove.
- */
- removePlugin (instance) {
- const list = this.plugins[instance.type]
- if (instance.uninstall) {
- instance.uninstall()
- }
- const index = list.indexOf(instance)
- if (index !== -1) {
- list.splice(index, 1)
- }
- }
- /**
- * Uninstall all plugins and close down this Uppy instance.
- */
- close () {
- this.reset()
- this.iteratePlugins((plugin) => {
- plugin.uninstall()
- })
- if (this.socket) {
- this.socket.close()
- }
- }
- /**
- * Set info message in `state.info`, so that UI plugins like `Informer`
- * can display the message
- *
- * @param {string} msg Message to be displayed by the informer
- */
- info (message, type, duration) {
- const isComplexMessage = typeof message === 'object'
- this.setState({
- info: {
- isHidden: false,
- type: type || 'info',
- message: isComplexMessage ? message.message : message,
- details: isComplexMessage ? message.details : null
- }
- })
- this.emit('core:info-visible')
- window.clearTimeout(this.infoTimeoutID)
- if (duration === 0) {
- this.infoTimeoutID = undefined
- return
- }
- // hide the informer after `duration` milliseconds
- this.infoTimeoutID = setTimeout(() => {
- const newInformer = Object.assign({}, this.state.info, {
- isHidden: true
- })
- this.setState({
- info: newInformer
- })
- this.emit('core:info-hidden')
- }, duration)
- }
- hideInfo () {
- const newInfo = Object.assign({}, this.core.state.info, {
- isHidden: true
- })
- this.setState({
- info: newInfo
- })
- this.emit('core:info-hidden')
- }
- /**
- * Logs stuff to console, only if `debug` is set to true. Silent in production.
- *
- * @return {String|Object} to log
- */
- log (msg, type) {
- if (!this.opts.debug) {
- return
- }
- if (type === 'error') {
- console.error(`LOG: ${msg}`)
- return
- }
- if (msg === `${msg}`) {
- console.log(`LOG: ${msg}`)
- } else {
- console.dir(msg)
- }
- global.uppyLog = global.uppyLog + '\n' + 'DEBUG LOG: ' + msg
- }
- initSocket (opts) {
- if (!this.socket) {
- this.socket = new UppySocket(opts)
- }
- return this.socket
- }
- /**
- * Initializes actions, installs all plugins (by iterating on them and calling `install`), sets options
- *
- */
- run () {
- this.log('Core is run, initializing actions...')
- this.actions()
- // Forse set `autoProceed` option to false if there are multiple selector Plugins active
- // if (this.plugins.acquirer && this.plugins.acquirer.length > 1) {
- // this.opts.autoProceed = false
- // }
- // Install all plugins
- // this.installAll()
- return this
- }
- /**
- * Restore an upload by its ID.
- */
- restore (uploadID) {
- this.log(`Core: attempting to restore upload "${uploadID}"`)
- if (!this.state.currentUploads[uploadID]) {
- this.removeUpload(uploadID)
- return Promise.reject(new Error('Nonexistent upload'))
- }
- return this.runUpload(uploadID)
- }
- /**
- * Create an upload for a bunch of files.
- *
- * @param {Array<string>} fileIDs File IDs to include in this upload.
- * @return {string} ID of this upload.
- */
- createUpload (fileIDs) {
- const uploadID = cuid()
- this.emit('core:upload', {
- id: uploadID,
- fileIDs: fileIDs
- })
- this.setState({
- currentUploads: Object.assign({}, this.state.currentUploads, {
- [uploadID]: {
- fileIDs: fileIDs,
- step: 0
- }
- })
- })
- return uploadID
- }
- /**
- * Remove an upload, eg. if it has been canceled or completed.
- *
- * @param {string} uploadID The ID of the upload.
- */
- removeUpload (uploadID) {
- const currentUploads = Object.assign({}, this.state.currentUploads)
- delete currentUploads[uploadID]
- this.setState({
- currentUploads: currentUploads
- })
- }
- /**
- * Run an upload. This picks up where it left off in case the upload is being restored.
- *
- * @private
- */
- runUpload (uploadID) {
- const uploadData = this.state.currentUploads[uploadID]
- const fileIDs = uploadData.fileIDs
- const restoreStep = uploadData.step
- const steps = [
- ...this.preProcessors,
- ...this.uploaders,
- ...this.postProcessors
- ]
- let lastStep = Promise.resolve()
- steps.forEach((fn, step) => {
- // Skip this step if we are restoring and have already completed this step before.
- if (step < restoreStep) {
- return
- }
- lastStep = lastStep.then(() => {
- const currentUpload = Object.assign({}, this.state.currentUploads[uploadID], {
- step: step
- })
- this.setState({
- currentUploads: Object.assign({}, this.state.currentUploads, {
- [uploadID]: currentUpload
- })
- })
- // TODO give this the `currentUpload` object as its only parameter maybe?
- // Otherwise when more metadata may be added to the upload this would keep getting more parameters
- return fn(fileIDs, uploadID)
- })
- })
- // Not returning the `catch`ed promise, because we still want to return a rejected
- // promise from this method if the upload failed.
- lastStep.catch((err) => {
- this.emit('core:error', err)
- this.removeUpload(uploadID)
- })
- return lastStep.then(() => {
- // return number of uploaded files
- this.emit('core:success', fileIDs)
- this.removeUpload(uploadID)
- })
- }
- /**
- * Start an upload for all the files that are not currently being uploaded.
- *
- * @return {Promise}
- */
- upload (forceUpload) {
- const isMinNumberOfFilesReached = this.checkRestrictions(true)
- if (!isMinNumberOfFilesReached) {
- return Promise.reject('Minimum number of files has not been reached')
- }
- return this.opts.onBeforeUpload(this.state.files).catch((err) => {
- this.info(err, 'error', 5000)
- return Promise.reject(`onBeforeUpload: ${err}`)
- }).then(() => {
- const waitingFileIDs = []
- Object.keys(this.state.files).forEach((fileID) => {
- const file = this.getFile(fileID)
- // TODO: replace files[file].isRemote with some logic
- //
- // filter files that are now yet being uploaded / haven’t been uploaded
- // and remote too
- if (forceUpload) {
- this.resetProgress()
- waitingFileIDs.push(file.id)
- } else if (!file.progress.uploadStarted || file.isRemote) {
- waitingFileIDs.push(file.id)
- }
- })
- const uploadID = this.createUpload(waitingFileIDs)
- return this.runUpload(uploadID)
- })
- }
- }
- module.exports = function (opts) {
- if (!(this instanceof Uppy)) {
- return new Uppy(opts)
- }
- }
|