uppy
Version:
Almost as cute as a Puppy :dog:
384 lines (330 loc) • 10.6 kB
JavaScript
const Plugin = require('./Plugin')
const tus = require('tus-js-client')
const settle = require('promise-settle')
const UppySocket = require('../core/UppySocket')
const Utils = 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
}
/**
* 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.handlePauseAll = this.handlePauseAll.bind(this)
this.handleResumeAll = this.handleResumeAll.bind(this)
this.handleResetProgress = this.handleResetProgress.bind(this)
this.handleUpload = this.handleUpload.bind(this)
}
pauseResume (action, fileID) {
const updatedFiles = Object.assign({}, this.core.getState().files)
const inProgressUpdatedFiles = Object.keys(updatedFiles).filter((file) => {
return !updatedFiles[file].progress.uploadComplete &&
updatedFiles[file].progress.uploadStarted
})
switch (action) {
case 'toggle':
if (updatedFiles[fileID].uploadComplete) return
const wasPaused = updatedFiles[fileID].isPaused || false
const isPaused = !wasPaused
let updatedFile
if (wasPaused) {
updatedFile = Object.assign({}, updatedFiles[fileID], {
isPaused: false
})
} else {
updatedFile = Object.assign({}, updatedFiles[fileID], {
isPaused: true
})
}
updatedFiles[fileID] = updatedFile
this.core.setState({files: updatedFiles})
return isPaused
case 'pauseAll':
inProgressUpdatedFiles.forEach((file) => {
const updatedFile = Object.assign({}, updatedFiles[file], {
isPaused: true
})
updatedFiles[file] = updatedFile
})
this.core.setState({files: updatedFiles})
return
case 'resumeAll':
inProgressUpdatedFiles.forEach((file) => {
const updatedFile = Object.assign({}, updatedFiles[file], {
isPaused: false
})
updatedFiles[file] = updatedFile
})
this.core.setState({files: updatedFiles})
return
}
}
handlePauseAll () {
this.pauseResume('pauseAll')
}
handleResumeAll () {
this.pauseResume('resumeAll')
}
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 })
}
/**
* 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}`)
// 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)
reject('Failed because: ' + 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)
}
resolve(upload)
}
optsTus.metadata = file.meta
const upload = new tus.Upload(file.data, optsTus)
this.onFileRemove(file.id, (targetFileID) => {
// this.core.log(`removing file: ${targetFileID}`)
upload.abort()
resolve(`upload ${targetFileID} was removed`)
})
this.onPause(file.id, (isPaused) => {
isPaused ? upload.abort() : upload.start()
})
this.onPauseAll(file.id, () => {
upload.abort()
})
this.onResumeAll(file.id, () => {
upload.start()
})
this.core.on('core:retry-started', () => {
const files = this.core.getState().files
if (files[file.id].progress.uploadComplete ||
!files[file.id].progress.uploadStarted ||
files[file.id].isPaused
) {
return
}
upload.start()
})
upload.start()
this.core.emit('core:upload-started', file.id, upload)
})
}
uploadRemote (file, current, total) {
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 = Utils.getSocketHost(file.remote.host)
const socket = new UppySocket({ target: `${host}/api/${token}` })
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.onResumeAll(file.id, () => socket.send('resume', {}))
socket.on('progress', (progressData) => Utils.emitSocketProgress(this, progressData, file))
socket.on('success', (data) => {
this.core.emitter.emit('core:upload-success', file.id, data, data.url)
socket.close()
})
}
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.core.on('core:file-removed', (targetFileID) => {
if (fileID === targetFileID) cb(targetFileID)
})
}
onPause (fileID, cb) {
this.core.on('core:upload-pause', (targetFileID) => {
if (fileID === targetFileID) {
const isPaused = this.pauseResume('toggle', fileID)
cb(isPaused)
}
})
}
onPauseAll (fileID, cb) {
this.core.on('core:pause-all', () => {
if (!this.core.getFile(fileID)) return
cb()
})
}
onResumeAll (fileID, cb) {
this.core.on('core:resume-all', () => {
if (!this.core.getFile(fileID)) return
cb()
})
}
uploadFiles (files) {
return settle(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)
}
}))
}
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)
}
actions () {
this.core.on('core:pause-all', this.handlePauseAll)
this.core.on('core:resume-all', this.handleResumeAll)
this.core.on('core:reset-progress', this.handleResetProgress)
if (this.opts.autoRetry) {
this.core.on('back-online', () => {
this.core.emit('core:retry-started')
})
}
}
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.actions()
}
uninstall () {
this.core.removeUploader(this.handleUpload)
this.core.off('core:pause-all', this.handlePauseAll)
this.core.off('core:resume-all', this.handleResumeAll)
}
}