Skip to content

Instantly share code, notes, and snippets.

@hedgerh
Last active June 3, 2017 05:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hedgerh/2297951deed8cee17e4566ce62783582 to your computer and use it in GitHub Desktop.
Save hedgerh/2297951deed8cee17e4566ce62783582 to your computer and use it in GitHub Desktop.
const Client = require('./Client')
const StatusSocket = require('./Socket')
/**
* Upload files to Transloadit using Tus.
*/
module.exports = class Transloadit extends EventEmitter {
constructor (core, opts) {
super(core, opts)
this.type = 'uploader'
this.id = 'Transloadit'
this.title = 'Transloadit'
const defaultOptions = {
waitForEncoding: false,
waitForMetadata: false,
signature: null,
params: null,
fields: {}
}
this.opts = Object.assign({}, defaultOptions, opts)
if (!this.opts.params) {
throw new Error('Transloadit: The `params` option is required.')
}
let params = this.opts.params
if (typeof params === 'string') {
try {
params = JSON.parse(params)
} catch (err) {
// Tell the user that this is not an Uppy bug!
err.message = 'Transloadit: The `params` option is a malformed JSON string: ' +
err.message
throw err
}
}
if (!params.auth || !params.auth.key) {
throw new Error('Transloadit: The `params.auth.key` option is required. ' +
'You can find your Transloadit API key at https://transloadit.com/accounts/credentials.')
}
this.client = new Client()
}
createAssembly (files) {
const expectedFiles = Object.keys(files).reduce((count, fileID) => {
if (!files[fileID].progress.uploadStarted || files[fileID].isRemote) {
return count + 1
}
return count
}, 0)
const filesWithAssemblyMetadata = {}
return this.client.createAssembly({
params: this.opts.params,
fields: this.opts.fields,
expectedFiles,
signature: this.opts.signature
}).then((assembly) => {
this.connectSocket(assembly).then(() => {
this.emit('createAssemblyComplete')
this.emit('start')
}).catch((err) => {
this.emit('createAssemblyFailed', err)
throw err
})
function attachAssemblyMetadata (file, assembly) {
// Attach meta parameters for the Tus plugin. See:
// https://github.com/tus/tusd/wiki/Uploading-to-Transloadit-using-tus#uploading-using-tus
// TODO Should this `meta` be moved to a `tus.meta` property instead?
// If the MetaData plugin can add eg. resize parameters, it doesn't
// make much sense to set those as upload-metadata for tus.
const meta = Object.assign({}, file.meta, {
assembly_url: assembly.assembly_url,
filename: file.name,
fieldname: 'file'
})
// Add assembly-specific Tus endpoint.
const tus = Object.assign({}, file.tus, {
endpoint: assembly.tus_url
})
return Object.assign(
{},
file,
{ meta, tus }
)
}
const filesWithAssemblyMetadata = Object.keys(files).reduce((prev, id) => {
return Object.assign({}, prev, {
[id]: attachAssemblyMetadata(files[id], assembly)
})
})
return filesWithAssemblyMetadata
})
}
shouldWait () {
return this.opts.waitForEncoding || this.opts.waitForMetadata
}
connectSocket (assembly) {
this.socket = new StatusSocket(
assembly.websocket_url,
assembly
)
this.socket.on('upload', (file) => this.emit('uploadComplete', file))
if (this.opts.waitForEncoding) {
this.socket.on('result', (stepName, result) =>
this.emit('result', stepName, result)
)
}
this.assemblyReady = new Promise((resolve, reject) => {
if (this.opts.waitForEncoding) {
this.socket.on('finished', () => resolve(assembly))
} else if (this.opts.waitForMetadata) {
this.socket.on('metadata', () => resolve(assembly))
}
this.socket.on('error', reject)
})
return new Promise((resolve, reject) => {
this.socket.on('connect', resolve)
this.socket.on('error', reject)
})
}
start (files) {
return this.createAssembly(files)
}
end () {
if (!this.shouldWait()) {
this.socket.close()
this.emit('complete')
return
}
return this.assemblyReady.then((assembly) => {
return this.client.getAssemblyStatus(assembly.assembly_ssl_url)
}).then((status) => {
// not 100% what to do here yet
this.emit('complete')
return status
})
}
}
'use strict'
import tus from 'tus-js-client'
import UppySocket from '../utils/UppySocket'
import EventEmitter from 'events'
/**
* Tus resumable file uploader
*/
export default class Tus10 extends EventEmitter {
constructor (opts) {
super()
// set default options
const defaultOptions = {
resume: true,
allowPause: true
}
// merge default options with the ones set by user
this.opts = Object.assign({}, defaultOptions, opts)
this.preProcess = opts.before || (files) => files
this.postProcess = opts.after || (result) => result
}
/**
* Start uploading for batch of files.
* @param {Array} files Files to upload
* @return {Promise} Resolves when all uploads succeed/fail
*/
start (files) {
const total = files.length
const uploaders = this.preProcess(files).map((file, index) => {
const current = parseInt(index, 10) + 1
if (file.isRemote) {
return this.uploadRemote(file, current, total)
}
return this.upload(file, current, total)
})
return Promise.all(uploaders).then(() => {
const result = {
uploadedCount: files.length
}
this.after()
this.emit('upload-complete', result)
return result
})
}
/**
* 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) {
// Create a new tus upload
return new Promise((resolve, reject) => {
const upload = new tus.Upload(file.data, {
// TODO merge this.opts or this.opts.tus here
metadata: file.meta,
resume: this.opts.resume,
endpoint: this.opts.endpoint,
onError: (err) => {
reject('Failed because: ' + err)
},
onProgress: (bytesUploaded, bytesTotal) => {
console.log('progress:')
console.log(bytesUploaded / bytesTotal)
// Dispatch progress event
this.emit('progress', {
uploader: this,
id: file.id,
bytesUploaded: bytesUploaded,
bytesTotal: bytesTotal
})
},
onSuccess: () => {
console.log('success!')
this.emit('success', file.id, upload.url)
resolve(upload)
}
})
this.on('abort', (fileID) => {
// If no fileID provided, abort all uploads
if (fileID === file.id || !fileID) {
console.log('aborting file upload: ', fileID)
upload.abort()
resolve(`upload ${fileID} was aborted`)
}
})
this.on('pause', (fileID) => {
// If no fileID provided, pause all uploads
if (fileID === file.id || !fileID) {
upload.abort()
}
})
this.on('resume', (fileID) => {
// If no fileID provided, resume all uploads
if (fileID === file.id || !fileID) {
upload.start()
}
})
upload.start()
this.emit('file-upload-started', file.id, upload)
})
}
uploadRemote (file, current, total) {
return new Promise((resolve, reject) => {
const remoteHost = this.opts.remoteHost ? this.opts.remoteHost : file.remote.host
fetch(`${remoteHost}/${file.remote.provider}/get`, {
method: 'post',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(Object.assign({}, file.remote.body, {
target: this.opts.endpoint,
protocol: 'tus'
}))
})
.then((res) => {
if (res.status < 200 && res.status > 300) {
return reject(res.statusText)
}
res.json()
.then((data) => {
// get the host domain
var regex = /^(?:https?:\/\/)?(?:[^@\/\n]+@)?(?:www\.)?([^:\/\n]+)/
var host = regex.exec(remoteHost)[1]
var token = data.token
var socket = new UppySocket({
target: `ws://${host}:3020/api/${token}`
})
socket.on('progress', (progressData) => {
const {progress, bytesUploaded, bytesTotal} = progressData
if (progress) {
console.log(progress)
// Dispatch progress event
this.emit('progress', {
uploader: this,
id: file.id,
bytesUploaded: bytesUploaded,
bytesTotal: bytesTotal
})
if (progress === '100.00') {
socket.close()
return resolve()
}
}
})
})
})
})
}
abort (fileID) {
this.emit('abort', fileID)
}
pause (fileID) {
this.emit('pause', fileID)
}
resume (fileID) {
this.emit('resume', fileID)
}
abortAll () {
this.abort()
}
pauseAll () {
this.pause()
}
resumeAll () {
this.resume()
}
}
import React from 'react';
import { Transloadit, Tus10, addMetadata } from 'uppy';
class Dashboard extends React.Component {
constructor () {
super()
this.addFile = this.addFile.bind(this)
this.startUpload = this.startUpload.bind(this)
this.onAssemblyComplete = this.onAssemblyComplete.bind(this)
this.onUploadProgress = this.onProgress.bind(this)
this.onUploadComplete = this.onUploadComplete.bind(this)
this.state = {
files: []
}
}
componentDidMount () {
this.transloadit = new Transloadit({
params: {
template_id: 'faceDetectTemplate',
auth: {
key: 'demoAccountKey',
},
},
waitForEncoding: true
})
this.transloadit.on('complete', this.onAssemblyComplete)
this.uploader = new Tus10({
before: this.transloadit.start,
after: this.transloadit.end,
resume: false
})
this.uploader.on('progress', this.onUploadProgress)
}
startUpload () {
this.uploader.start(this.state.files)
.then(this.onUploadComplete)
}
addFile (fileObj) {
const fileWithMetadata = addMetadata(file, { name: 'foo' })
this.setState({
files: this.state.files.concat(fileWithMetadata)
})
}
onAssemblyComplete () {
this.setState({
assemblyComplete: true
})
}
onUploadProgress (progress) {
this.setState({ /* update progress */ })
}
onUploadComplete (err, res) {
if (err) {
this.setState({
error: true,
errorMessage: err
})
return err
}
this.setState({
success: res,
files: []
})
return res
}
render () {
return (
<div className='Dashboard'>
<FileUI
files={this.state.files}
addFile={this.addFile}/>
<button onClick={this.startUpload}>Start Upload</button>
</div>
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment