123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- const Plugin = require('./Plugin')
- const tus = require('tus-js-client')
- const UppySocket = require('../core/UppySocket')
- const {
- emitSocketProgress,
- getSocketHost,
- settle
- } = require('../core/Utils')
- require('whatwg-fetch')
- // Extracted from https://github.com/tus/tus-js-client/blob/master/lib/upload.js#L13
- // excepted we removed 'fingerprint' key to avoid adding more dependencies
- const tusDefaultOptions = {
- endpoint: '',
- resume: true,
- onProgress: null,
- onChunkComplete: null,
- onSuccess: null,
- onError: null,
- headers: {},
- chunkSize: Infinity,
- withCredentials: false,
- uploadUrl: null,
- uploadSize: null,
- overridePatchMethod: false,
- retryDelays: null
- }
- /**
- * Create a wrapper around an event emitter with a `remove` method to remove
- * all events that were added using the wrapped emitter.
- */
- function createEventTracker (emitter) {
- const events = []
- return {
- on (event, fn) {
- events.push([ event, fn ])
- return emitter.on(event, fn)
- },
- remove () {
- events.forEach(([ event, fn ]) => {
- emitter.off(event, fn)
- })
- }
- }
- }
- /**
- * Tus resumable file uploader
- *
- */
- module.exports = class Tus10 extends Plugin {
- constructor (core, opts) {
- super(core, opts)
- this.type = 'uploader'
- this.id = 'Tus'
- this.title = 'Tus'
- // set default options
- const defaultOptions = {
- resume: true,
- autoRetry: true,
- retryDelays: [0, 1000, 3000, 5000]
- }
- // merge default options with the ones set by user
- this.opts = Object.assign({}, defaultOptions, opts)
- this.uploaders = Object.create(null)
- this.uploaderEvents = Object.create(null)
- this.uploaderSockets = Object.create(null)
- this.handleResetProgress = this.handleResetProgress.bind(this)
- this.handleUpload = this.handleUpload.bind(this)
- }
- handleResetProgress () {
- const files = Object.assign({}, this.core.state.files)
- Object.keys(files).forEach((fileID) => {
- // Only clone the file object if it has a Tus `uploadUrl` attached.
- if (files[fileID].tus && files[fileID].tus.uploadUrl) {
- const tusState = Object.assign({}, files[fileID].tus)
- delete tusState.uploadUrl
- files[fileID] = Object.assign({}, files[fileID], { tus: tusState })
- }
- })
- this.core.setState({ files })
- }
- /**
- * Clean up all references for a file's upload: the tus.Upload instance,
- * any events related to the file, and the uppy-server WebSocket connection.
- */
- resetUploaderReferences (fileID) {
- if (this.uploaders[fileID]) {
- this.uploaders[fileID].abort()
- this.uploaders[fileID] = null
- }
- if (this.uploaderEvents[fileID]) {
- this.uploaderEvents[fileID].remove()
- this.uploaderEvents[fileID] = null
- }
- if (this.uploaderSockets[fileID]) {
- this.uploaderSockets[fileID].close()
- this.uploaderSockets[fileID] = null
- }
- }
- /**
- * Create a new Tus upload
- *
- * @param {object} file for use with upload
- * @param {integer} current file in a queue
- * @param {integer} total number of files in a queue
- * @returns {Promise}
- */
- upload (file, current, total) {
- this.core.log(`uploading ${current} of ${total}`)
- this.resetUploaderReferences(file.id)
- // Create a new tus upload
- return new Promise((resolve, reject) => {
- const optsTus = Object.assign(
- {},
- tusDefaultOptions,
- this.opts,
- // Install file-specific upload overrides.
- file.tus || {}
- )
- optsTus.onError = (err) => {
- this.core.log(err)
- this.core.emit('core:upload-error', file.id, err)
- err.message = `Failed because: ${err.message}`
- this.resetUploaderReferences(file.id)
- reject(err)
- }
- optsTus.onProgress = (bytesUploaded, bytesTotal) => {
- this.onReceiveUploadUrl(file, upload.url)
- this.core.emit('core:upload-progress', {
- uploader: this,
- id: file.id,
- bytesUploaded: bytesUploaded,
- bytesTotal: bytesTotal
- })
- }
- optsTus.onSuccess = () => {
- this.core.emit('core:upload-success', file.id, upload, upload.url)
- if (upload.url) {
- this.core.log('Download ' + upload.file.name + ' from ' + upload.url)
- }
- this.resetUploaderReferences(file.id)
- resolve(upload)
- }
- optsTus.metadata = file.meta
- const upload = new tus.Upload(file.data, optsTus)
- this.uploaders[file.id] = upload
- this.uploaderEvents[file.id] = createEventTracker(this.core)
- this.onFileRemove(file.id, (targetFileID) => {
- this.resetUploaderReferences(file.id)
- resolve(`upload ${targetFileID} was removed`)
- })
- this.onPause(file.id, (isPaused) => {
- isPaused ? upload.abort() : upload.start()
- })
- this.onPauseAll(file.id, () => {
- upload.abort()
- })
- this.onCancelAll(file.id, () => {
- this.resetUploaderReferences(file.id)
- })
- this.onResumeAll(file.id, () => {
- if (file.error) {
- upload.abort()
- }
- upload.start()
- })
- upload.start()
- this.core.emit('core:upload-started', file.id, upload)
- })
- }
- uploadRemote (file, current, total) {
- this.resetUploaderReferences(file.id)
- return new Promise((resolve, reject) => {
- this.core.log(file.remote.url)
- if (file.serverToken) {
- this.connectToServerSocket(file)
- } else {
- let endpoint = this.opts.endpoint
- if (file.tus && file.tus.endpoint) {
- endpoint = file.tus.endpoint
- }
- this.core.emitter.emit('core:upload-started', file.id)
- fetch(file.remote.url, {
- method: 'post',
- credentials: 'include',
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(Object.assign({}, file.remote.body, {
- endpoint,
- protocol: 'tus',
- size: file.data.size,
- metadata: file.meta
- }))
- })
- .then((res) => {
- if (res.status < 200 && res.status > 300) {
- return reject(res.statusText)
- }
- res.json().then((data) => {
- const token = data.token
- file = this.getFile(file.id)
- file.serverToken = token
- this.updateFile(file)
- this.connectToServerSocket(file)
- resolve()
- })
- })
- }
- })
- }
- connectToServerSocket (file) {
- const token = file.serverToken
- const host = getSocketHost(file.remote.host)
- const socket = new UppySocket({ target: `${host}/api/${token}` })
- this.uploaderSockets[file.id] = socket
- this.uploaderEvents[file.id] = createEventTracker(this.core)
- this.onFileRemove(file.id, () => socket.send('pause', {}))
- this.onPause(file.id, (isPaused) => {
- isPaused ? socket.send('pause', {}) : socket.send('resume', {})
- })
- this.onPauseAll(file.id, () => socket.send('pause', {}))
- this.onCancelAll(file.id, () => socket.send('pause', {}))
- this.onResumeAll(file.id, () => {
- if (file.error) {
- socket.send('pause', {})
- }
- socket.send('resume', {})
- })
- this.onRetry(file.id, () => {
- socket.send('pause', {})
- socket.send('resume', {})
- })
- this.onRetryAll(file.id, () => {
- socket.send('pause', {})
- socket.send('resume', {})
- })
- socket.on('progress', (progressData) => emitSocketProgress(this, progressData, file))
- socket.on('success', (data) => {
- this.core.emitter.emit('core:upload-success', file.id, data, data.url)
- this.resetUploaderReferences(file.id)
- })
- }
- getFile (fileID) {
- return this.core.state.files[fileID]
- }
- updateFile (file) {
- const files = Object.assign({}, this.core.state.files, {
- [file.id]: file
- })
- this.core.setState({ files })
- }
- onReceiveUploadUrl (file, uploadURL) {
- const currentFile = this.getFile(file.id)
- if (!currentFile) return
- // Only do the update if we didn't have an upload URL yet.
- if (!currentFile.tus || currentFile.tus.uploadUrl !== uploadURL) {
- const newFile = Object.assign({}, currentFile, {
- tus: Object.assign({}, currentFile.tus, {
- uploadUrl: uploadURL
- })
- })
- this.updateFile(newFile)
- }
- }
- onFileRemove (fileID, cb) {
- this.uploaderEvents[fileID].on('core:file-removed', (targetFileID) => {
- if (fileID === targetFileID) cb(targetFileID)
- })
- }
- onPause (fileID, cb) {
- this.uploaderEvents[fileID].on('core:upload-pause', (targetFileID, isPaused) => {
- if (fileID === targetFileID) {
- // const isPaused = this.core.pauseResume(fileID)
- cb(isPaused)
- }
- })
- }
- onRetry (fileID, cb) {
- this.uploaderEvents[fileID].on('core:upload-retry', (targetFileID) => {
- if (fileID === targetFileID) {
- cb()
- }
- })
- }
- onRetryAll (fileID, cb) {
- this.uploaderEvents[fileID].on('core:retry-all', (filesToRetry) => {
- if (!this.core.getFile(fileID)) return
- cb()
- })
- }
- onPauseAll (fileID, cb) {
- this.uploaderEvents[fileID].on('core:pause-all', () => {
- if (!this.core.getFile(fileID)) return
- cb()
- })
- }
- onCancelAll (fileID, cb) {
- this.uploaderEvents[fileID].on('core:cancel-all', () => {
- if (!this.core.getFile(fileID)) return
- cb()
- })
- }
- onResumeAll (fileID, cb) {
- this.uploaderEvents[fileID].on('core:resume-all', () => {
- if (!this.core.getFile(fileID)) return
- cb()
- })
- }
- uploadFiles (files) {
- const promises = files.map((file, index) => {
- const current = parseInt(index, 10) + 1
- const total = files.length
- if (!file.isRemote) {
- return this.upload(file, current, total)
- } else {
- return this.uploadRemote(file, current, total)
- }
- })
- return settle(promises)
- }
- handleUpload (fileIDs) {
- if (fileIDs.length === 0) {
- this.core.log('Tus: no files to upload!')
- return Promise.resolve()
- }
- this.core.log('Tus is uploading...')
- const filesToUpload = fileIDs.map((fileID) => this.core.getFile(fileID))
- return this.uploadFiles(filesToUpload)
- }
- addResumableUploadsCapabilityFlag () {
- const newCapabilities = Object.assign({}, this.core.getState().capabilities)
- newCapabilities.resumableUploads = true
- this.core.setState({
- capabilities: newCapabilities
- })
- }
- install () {
- this.addResumableUploadsCapabilityFlag()
- this.core.addUploader(this.handleUpload)
- this.core.on('core:reset-progress', this.handleResetProgress)
- if (this.opts.autoRetry) {
- this.core.on('back-online', this.core.retryAll)
- }
- }
- uninstall () {
- this.core.removeUploader(this.handleUpload)
- if (this.opts.autoRetry) {
- this.core.off('back-online', this.core.retryAll)
- }
- }
- }
|