UNPKG

27.6 kBJavaScriptView Raw
1const Translator = require('@uppy/utils/lib/Translator')
2const hasProperty = require('@uppy/utils/lib/hasProperty')
3const { Plugin } = require('@uppy/core')
4const Tus = require('@uppy/tus')
5const Assembly = require('./Assembly')
6const Client = require('./Client')
7const AssemblyOptions = require('./AssemblyOptions')
8const AssemblyWatcher = require('./AssemblyWatcher')
9
10function defaultGetAssemblyOptions (file, options) {
11 return {
12 params: options.params,
13 signature: options.signature,
14 fields: options.fields
15 }
16}
17
18const COMPANION = 'https://api2.transloadit.com/companion'
19// Regex matching acceptable postMessage() origins for authentication feedback from companion.
20const ALLOWED_COMPANION_PATTERN = /\.transloadit\.com$/
21// Regex used to check if a Companion address is run by Transloadit.
22const TL_COMPANION = /https?:\/\/api2(?:-\w+)?\.transloadit\.com\/companion/
23const TL_UPPY_SERVER = /https?:\/\/api2(?:-\w+)?\.transloadit\.com\/uppy-server/
24
25/**
26 * Upload files to Transloadit using Tus.
27 */
28module.exports = class Transloadit extends Plugin {
29 static VERSION = require('../package.json').version
30
31 constructor (uppy, opts) {
32 super(uppy, opts)
33 this.type = 'uploader'
34 this.id = this.opts.id || 'Transloadit'
35 this.title = 'Transloadit'
36
37 this.defaultLocale = {
38 strings: {
39 creatingAssembly: 'Preparing upload...',
40 creatingAssemblyFailed: 'Transloadit: Could not create Assembly',
41 encoding: 'Encoding...'
42 }
43 }
44
45 const defaultOptions = {
46 service: 'https://api2.transloadit.com',
47 errorReporting: true,
48 waitForEncoding: false,
49 waitForMetadata: false,
50 alwaysRunAssembly: false,
51 importFromUploadURLs: false,
52 signature: null,
53 params: null,
54 fields: {},
55 getAssemblyOptions: defaultGetAssemblyOptions,
56 limit: 0
57 }
58
59 this.opts = { ...defaultOptions, ...opts }
60
61 this.i18nInit()
62
63 this._prepareUpload = this._prepareUpload.bind(this)
64 this._afterUpload = this._afterUpload.bind(this)
65 this._onError = this._onError.bind(this)
66 this._onTusError = this._onTusError.bind(this)
67 this._onCancelAll = this._onCancelAll.bind(this)
68 this._onFileUploadURLAvailable = this._onFileUploadURLAvailable.bind(this)
69 this._onRestored = this._onRestored.bind(this)
70 this._getPersistentData = this._getPersistentData.bind(this)
71
72 const hasCustomAssemblyOptions = this.opts.getAssemblyOptions !== defaultOptions.getAssemblyOptions
73 if (this.opts.params) {
74 AssemblyOptions.validateParams(this.opts.params)
75 } else if (!hasCustomAssemblyOptions) {
76 // Throw the same error that we'd throw if the `params` returned from a
77 // `getAssemblyOptions()` function is null.
78 AssemblyOptions.validateParams(null)
79 }
80
81 this.client = new Client({
82 service: this.opts.service,
83 client: this._getClientVersion(),
84 errorReporting: this.opts.errorReporting
85 })
86 // Contains Assembly instances for in-progress Assemblies.
87 this.activeAssemblies = {}
88 // Contains a mapping of uploadID to AssemblyWatcher
89 this.assemblyWatchers = {}
90 // Contains a file IDs that have completed postprocessing before the upload they belong to has entered the postprocess stage.
91 this.completedFiles = Object.create(null)
92 }
93
94 setOptions (newOpts) {
95 super.setOptions(newOpts)
96 this.i18nInit()
97 }
98
99 i18nInit () {
100 this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
101 this.i18n = this.translator.translate.bind(this.translator)
102 this.i18nArray = this.translator.translateArray.bind(this.translator)
103 this.setPluginState() // so that UI re-renders and we see the updated locale
104 }
105
106 _getClientVersion () {
107 const list = [
108 `uppy-core:${this.uppy.constructor.VERSION}`,
109 `uppy-transloadit:${this.constructor.VERSION}`,
110 `uppy-tus:${Tus.VERSION}`
111 ]
112
113 const addPluginVersion = (pluginName, versionName) => {
114 const plugin = this.uppy.getPlugin(pluginName)
115 if (plugin) {
116 list.push(`${versionName}:${plugin.constructor.VERSION}`)
117 }
118 }
119
120 if (this.opts.importFromUploadURLs) {
121 addPluginVersion('XHRUpload', 'uppy-xhr-upload')
122 addPluginVersion('AwsS3', 'uppy-aws-s3')
123 addPluginVersion('AwsS3Multipart', 'uppy-aws-s3-multipart')
124 }
125
126 addPluginVersion('Dropbox', 'uppy-dropbox')
127 addPluginVersion('Facebook', 'uppy-facebook')
128 addPluginVersion('GoogleDrive', 'uppy-google-drive')
129 addPluginVersion('Instagram', 'uppy-instagram')
130 addPluginVersion('OneDrive', 'uppy-onedrive')
131 addPluginVersion('Zoom', 'uppy-zoom')
132 addPluginVersion('Url', 'uppy-url')
133
134 return list.join(',')
135 }
136
137 /**
138 * Attach metadata to files to configure the Tus plugin to upload to Transloadit.
139 * Also use Transloadit's Companion
140 *
141 * See: https://github.com/tus/tusd/wiki/Uploading-to-Transloadit-using-tus#uploading-using-tus
142 *
143 * @param {object} file
144 * @param {object} status
145 */
146 _attachAssemblyMetadata (file, status) {
147 // Add the metadata parameters Transloadit needs.
148 const meta = {
149 ...file.meta,
150 assembly_url: status.assembly_url,
151 filename: file.name,
152 fieldname: 'file'
153 }
154 // Add Assembly-specific Tus endpoint.
155 const tus = {
156 ...file.tus,
157 endpoint: status.tus_url,
158 // Include X-Request-ID headers for better debugging.
159 addRequestId: true
160 }
161
162 // Set Companion location. We only add this, if 'file' has the attribute
163 // remote, because this is the criteria to identify remote files.
164 // We only replace the hostname for Transloadit's companions, so that
165 // people can also self-host them while still using Transloadit for encoding.
166 let remote = file.remote
167 if (file.remote && TL_UPPY_SERVER.test(file.remote.companionUrl)) {
168 const err = new Error(
169 'The https://api2.transloadit.com/uppy-server endpoint was renamed to ' +
170 'https://api2.transloadit.com/companion, please update your `companionUrl` ' +
171 'options accordingly.')
172 // Explicitly log this error here because it is caught by the `createAssembly`
173 // Promise further along.
174 // That's fine, but createAssembly only shows the informer, we need something a
175 // little more noisy.
176 this.uppy.log(err)
177 throw err
178 }
179
180 if (file.remote && TL_COMPANION.test(file.remote.companionUrl)) {
181 const newHost = status.companion_url
182 .replace(/\/$/, '')
183 const path = file.remote.url
184 .replace(file.remote.companionUrl, '')
185 .replace(/^\//, '')
186
187 remote = {
188 ...file.remote,
189 companionUrl: newHost,
190 url: `${newHost}/${path}`
191 }
192 }
193
194 // Store the Assembly ID this file is in on the file under the `transloadit` key.
195 const newFile = {
196 ...file,
197 transloadit: {
198 assembly: status.assembly_id
199 }
200 }
201 // Only configure the Tus plugin if we are uploading straight to Transloadit (the default).
202 if (!this.opts.importFromUploadURLs) {
203 Object.assign(newFile, { meta, tus, remote })
204 }
205 return newFile
206 }
207
208 _createAssembly (fileIDs, uploadID, options) {
209 this.uppy.log('[Transloadit] Create Assembly')
210
211 return this.client.createAssembly({
212 params: options.params,
213 fields: options.fields,
214 expectedFiles: fileIDs.length,
215 signature: options.signature
216 }).then((newAssembly) => {
217 const assembly = new Assembly(newAssembly)
218 const status = assembly.status
219 const assemblyID = status.assembly_id
220
221 const { assemblies, uploadsAssemblies } = this.getPluginState()
222 this.setPluginState({
223 // Store the Assembly status.
224 assemblies: {
225 ...assemblies,
226 [assemblyID]: status
227 },
228 // Store the list of Assemblies related to this upload.
229 uploadsAssemblies: {
230 ...uploadsAssemblies,
231 [uploadID]: [
232 ...uploadsAssemblies[uploadID],
233 assemblyID
234 ]
235 }
236 })
237
238 const { files } = this.uppy.getState()
239 const updatedFiles = {}
240 fileIDs.forEach((id) => {
241 updatedFiles[id] = this._attachAssemblyMetadata(this.uppy.getFile(id), status)
242 })
243 this.uppy.setState({
244 files: {
245 ...files,
246 ...updatedFiles
247 }
248 })
249
250 this.uppy.emit('transloadit:assembly-created', status, fileIDs)
251
252 this.uppy.log(`[Transloadit] Created Assembly ${assemblyID}`)
253 return assembly
254 }).catch((err) => {
255 err.message = `${this.i18n('creatingAssemblyFailed')}: ${err.message}`
256 // Reject the promise.
257 throw err
258 })
259 }
260
261 _createAssemblyWatcher (assemblyID, fileIDs, uploadID) {
262 // AssemblyWatcher tracks completion states of all Assemblies in this upload.
263 const watcher = new AssemblyWatcher(this.uppy, assemblyID)
264
265 watcher.on('assembly-complete', (id) => {
266 const files = this.getAssemblyFiles(id)
267 files.forEach((file) => {
268 this.completedFiles[file.id] = true
269 this.uppy.emit('postprocess-complete', file)
270 })
271 })
272
273 watcher.on('assembly-error', (id, error) => {
274 // Clear postprocessing state for all our files.
275 const files = this.getAssemblyFiles(id)
276 files.forEach((file) => {
277 // TODO Maybe make a postprocess-error event here?
278 this.uppy.emit('upload-error', file, error)
279
280 this.uppy.emit('postprocess-complete', file)
281 })
282 })
283
284 this.assemblyWatchers[uploadID] = watcher
285 }
286
287 _shouldWaitAfterUpload () {
288 return this.opts.waitForEncoding || this.opts.waitForMetadata
289 }
290
291 /**
292 * Used when `importFromUploadURLs` is enabled: reserves all files in
293 * the Assembly.
294 */
295 _reserveFiles (assembly, fileIDs) {
296 return Promise.all(fileIDs.map((fileID) => {
297 const file = this.uppy.getFile(fileID)
298 return this.client.reserveFile(assembly, file)
299 }))
300 }
301
302 /**
303 * Used when `importFromUploadURLs` is enabled: adds files to the Assembly
304 * once they have been fully uploaded.
305 */
306 _onFileUploadURLAvailable (file) {
307 if (!file || !file.transloadit || !file.transloadit.assembly) {
308 return
309 }
310
311 const { assemblies } = this.getPluginState()
312 const assembly = assemblies[file.transloadit.assembly]
313
314 this.client.addFile(assembly, file).catch((err) => {
315 this.uppy.log(err)
316 this.uppy.emit('transloadit:import-error', assembly, file.id, err)
317 })
318 }
319
320 _findFile (uploadedFile) {
321 const files = this.uppy.getFiles()
322 for (let i = 0; i < files.length; i++) {
323 const file = files[i]
324 // Completed file upload.
325 if (file.uploadURL === uploadedFile.tus_upload_url) {
326 return file
327 }
328 // In-progress file upload.
329 if (file.tus && file.tus.uploadUrl === uploadedFile.tus_upload_url) {
330 return file
331 }
332 if (!uploadedFile.is_tus_file) {
333 // Fingers-crossed check for non-tus uploads, eg imported from S3.
334 if (file.name === uploadedFile.name && file.size === uploadedFile.size) {
335 return file
336 }
337 }
338 }
339 }
340
341 _onFileUploadComplete (assemblyId, uploadedFile) {
342 const state = this.getPluginState()
343 const file = this._findFile(uploadedFile)
344 if (!file) {
345 this.uppy.log('[Transloadit] Couldn’t file the file, it was likely removed in the process')
346 return
347 }
348 this.setPluginState({
349 files: {
350 ...state.files,
351 [uploadedFile.id]: {
352 assembly: assemblyId,
353 id: file.id,
354 uploadedFile
355 }
356 }
357 })
358 this.uppy.emit('transloadit:upload', uploadedFile, this.getAssembly(assemblyId))
359 }
360
361 /**
362 * Callback when a new Assembly result comes in.
363 *
364 * @param {string} assemblyId
365 * @param {string} stepName
366 * @param {object} result
367 */
368 _onResult (assemblyId, stepName, result) {
369 const state = this.getPluginState()
370 const file = state.files[result.original_id]
371 // The `file` may not exist if an import robot was used instead of a file upload.
372 result.localId = file ? file.id : null
373
374 const entry = {
375 result,
376 stepName,
377 id: result.id,
378 assembly: assemblyId
379 }
380
381 this.setPluginState({
382 results: [...state.results, entry]
383 })
384 this.uppy.emit('transloadit:result', stepName, result, this.getAssembly(assemblyId))
385 }
386
387 /**
388 * When an Assembly has finished processing, get the final state
389 * and emit it.
390 *
391 * @param {object} status
392 */
393 _onAssemblyFinished (status) {
394 const url = status.assembly_ssl_url
395 this.client.getAssemblyStatus(url).then((finalStatus) => {
396 const assemblyId = finalStatus.assembly_id
397 const state = this.getPluginState()
398 this.setPluginState({
399 assemblies: {
400 ...state.assemblies,
401 [assemblyId]: finalStatus
402 }
403 })
404 this.uppy.emit('transloadit:complete', finalStatus)
405 })
406 }
407
408 _cancelAssembly (assembly) {
409 return this.client.cancelAssembly(assembly).then(() => {
410 // TODO bubble this through AssemblyWatcher so its event handlers can clean up correctly
411 this.uppy.emit('transloadit:assembly-cancelled', assembly)
412 })
413 }
414
415 /**
416 * When all files are removed, cancel in-progress Assemblies.
417 */
418 _onCancelAll () {
419 const { uploadsAssemblies } = this.getPluginState()
420
421 const assemblyIDs = Object.keys(uploadsAssemblies).reduce((acc, uploadID) => {
422 acc.push(...uploadsAssemblies[uploadID])
423 return acc
424 }, [])
425
426 const cancelPromises = assemblyIDs.map((assemblyID) => {
427 const assembly = this.getAssembly(assemblyID)
428 return this._cancelAssembly(assembly)
429 })
430
431 Promise.all(cancelPromises).catch((err) => {
432 this.uppy.log(err)
433 })
434 }
435
436 /**
437 * Custom state serialization for the Golden Retriever plugin.
438 * It will pass this back to the `_onRestored` function.
439 *
440 * @param {Function} setData
441 */
442 _getPersistentData (setData) {
443 const state = this.getPluginState()
444 const assemblies = state.assemblies
445 const uploadsAssemblies = state.uploadsAssemblies
446
447 setData({
448 [this.id]: {
449 assemblies,
450 uploadsAssemblies
451 }
452 })
453 }
454
455 _onRestored (pluginData) {
456 const savedState = pluginData && pluginData[this.id] ? pluginData[this.id] : {}
457 const previousAssemblies = savedState.assemblies || {}
458 const uploadsAssemblies = savedState.uploadsAssemblies || {}
459
460 if (Object.keys(uploadsAssemblies).length === 0) {
461 // Nothing to restore.
462 return
463 }
464
465 // Convert loaded Assembly statuses to a Transloadit plugin state object.
466 const restoreState = (assemblies) => {
467 const files = {}
468 const results = []
469 Object.keys(assemblies).forEach((id) => {
470 const status = assemblies[id]
471
472 status.uploads.forEach((uploadedFile) => {
473 const file = this._findFile(uploadedFile)
474 files[uploadedFile.id] = {
475 id: file.id,
476 assembly: id,
477 uploadedFile
478 }
479 })
480
481 const state = this.getPluginState()
482 Object.keys(status.results).forEach((stepName) => {
483 status.results[stepName].forEach((result) => {
484 const file = state.files[result.original_id]
485 result.localId = file ? file.id : null
486 results.push({
487 id: result.id,
488 result,
489 stepName,
490 assembly: id
491 })
492 })
493 })
494 })
495
496 this.setPluginState({
497 assemblies,
498 files,
499 results,
500 uploadsAssemblies
501 })
502 }
503
504 // Set up the Assembly instances and AssemblyWatchers for existing Assemblies.
505 const restoreAssemblies = () => {
506 const { assemblies, uploadsAssemblies } = this.getPluginState()
507
508 // Set up the assembly watchers again for all the ongoing uploads.
509 Object.keys(uploadsAssemblies).forEach((uploadID) => {
510 const assemblyIDs = uploadsAssemblies[uploadID]
511 const fileIDsInUpload = assemblyIDs.reduce((acc, assemblyID) => {
512 const fileIDsInAssembly = this.getAssemblyFiles(assemblyID).map((file) => file.id)
513 acc.push(...fileIDsInAssembly)
514 return acc
515 }, [])
516 this._createAssemblyWatcher(assemblyIDs, fileIDsInUpload, uploadID)
517 })
518
519 const allAssemblyIDs = Object.keys(assemblies)
520 allAssemblyIDs.forEach((id) => {
521 const assembly = new Assembly(assemblies[id])
522 this._connectAssembly(assembly)
523 })
524 }
525
526 // Force-update all Assemblies to check for missed events.
527 const updateAssemblies = () => {
528 const { assemblies } = this.getPluginState()
529 return Promise.all(
530 Object.keys(assemblies).map((id) => {
531 return this.activeAssemblies[id].update()
532 })
533 )
534 }
535
536 // Restore all Assembly state.
537 this.restored = Promise.resolve().then(() => {
538 restoreState(previousAssemblies)
539 restoreAssemblies()
540 return updateAssemblies()
541 })
542
543 this.restored.then(() => {
544 this.restored = null
545 })
546 }
547
548 _connectAssembly (assembly) {
549 const { status } = assembly
550 const id = status.assembly_id
551 this.activeAssemblies[id] = assembly
552
553 // Sync local `assemblies` state
554 assembly.on('status', (newStatus) => {
555 const { assemblies } = this.getPluginState()
556 this.setPluginState({
557 assemblies: {
558 ...assemblies,
559 [id]: newStatus
560 }
561 })
562 })
563
564 assembly.on('upload', (file) => {
565 this._onFileUploadComplete(id, file)
566 })
567 assembly.on('error', (error) => {
568 error.assembly = assembly.status
569 this.uppy.emit('transloadit:assembly-error', assembly.status, error)
570 })
571
572 assembly.on('executing', () => {
573 this.uppy.emit('transloadit:assembly-executing', assembly.status)
574 })
575
576 if (this.opts.waitForEncoding) {
577 assembly.on('result', (stepName, result) => {
578 this._onResult(id, stepName, result)
579 })
580 }
581
582 if (this.opts.waitForEncoding) {
583 assembly.on('finished', () => {
584 this._onAssemblyFinished(assembly.status)
585 })
586 } else if (this.opts.waitForMetadata) {
587 assembly.on('metadata', () => {
588 this._onAssemblyFinished(assembly.status)
589 })
590 }
591
592 // No need to connect to the socket if the Assembly has completed by now.
593 if (assembly.ok === 'ASSEMBLY_COMPLETE') {
594 return assembly
595 }
596
597 // TODO Do we still need this for anything…?
598 // eslint-disable-next-line no-unused-vars
599 const connected = new Promise((resolve, reject) => {
600 assembly.once('connect', resolve)
601 assembly.once('status', resolve)
602 assembly.once('error', reject)
603 }).then(() => {
604 this.uppy.log('[Transloadit] Socket is ready')
605 })
606
607 assembly.connect()
608 return assembly
609 }
610
611 _prepareUpload (fileIDs, uploadID) {
612 // Only use files without errors
613 fileIDs = fileIDs.filter((file) => !file.error)
614
615 fileIDs.forEach((fileID) => {
616 const file = this.uppy.getFile(fileID)
617 this.uppy.emit('preprocess-progress', file, {
618 mode: 'indeterminate',
619 message: this.i18n('creatingAssembly')
620 })
621 })
622
623 const createAssembly = ({ fileIDs, options }) => {
624 let createdAssembly
625 return this._createAssembly(fileIDs, uploadID, options).then((assembly) => {
626 createdAssembly = assembly
627 if (this.opts.importFromUploadURLs) {
628 return this._reserveFiles(assembly, fileIDs)
629 }
630 }).then(() => {
631 fileIDs.forEach((fileID) => {
632 const file = this.uppy.getFile(fileID)
633 this.uppy.emit('preprocess-complete', file)
634 })
635 return createdAssembly
636 }).catch((err) => {
637 fileIDs.forEach((fileID) => {
638 const file = this.uppy.getFile(fileID)
639 // Clear preprocessing state when the Assembly could not be created,
640 // otherwise the UI gets confused about the lingering progress keys
641 this.uppy.emit('preprocess-complete', file)
642 this.uppy.emit('upload-error', file, err)
643 })
644 throw err
645 })
646 }
647
648 const { uploadsAssemblies } = this.getPluginState()
649 this.setPluginState({
650 uploadsAssemblies: {
651 ...uploadsAssemblies,
652 [uploadID]: []
653 }
654 })
655
656 const files = fileIDs.map((id) => this.uppy.getFile(id))
657 const assemblyOptions = new AssemblyOptions(files, this.opts)
658
659 return assemblyOptions.build().then(
660 (assemblies) => Promise.all(
661 assemblies.map(createAssembly)
662 ).then((createdAssemblies) => {
663 const assemblyIDs = createdAssemblies.map(assembly => assembly.status.assembly_id)
664 this._createAssemblyWatcher(assemblyIDs, fileIDs, uploadID)
665 createdAssemblies.map(assembly => this._connectAssembly(assembly))
666 }),
667 // If something went wrong before any Assemblies could be created,
668 // clear all processing state.
669 (err) => {
670 fileIDs.forEach((fileID) => {
671 const file = this.uppy.getFile(fileID)
672 this.uppy.emit('preprocess-complete', file)
673 this.uppy.emit('upload-error', file, err)
674 })
675 throw err
676 }
677 )
678 }
679
680 _afterUpload (fileIDs, uploadID) {
681 const files = fileIDs.map(fileID => this.uppy.getFile(fileID))
682 // Only use files without errors
683 fileIDs = files.filter((file) => !file.error).map(file => file.id)
684
685 const state = this.getPluginState()
686
687 // If we're still restoring state, wait for that to be done.
688 if (this.restored) {
689 return this.restored.then(() => {
690 return this._afterUpload(fileIDs, uploadID)
691 })
692 }
693
694 const assemblyIDs = state.uploadsAssemblies[uploadID]
695
696 // If we don't have to wait for encoding metadata or results, we can close
697 // the socket immediately and finish the upload.
698 if (!this._shouldWaitAfterUpload()) {
699 assemblyIDs.forEach((assemblyID) => {
700 const assembly = this.activeAssemblies[assemblyID]
701 assembly.close()
702 delete this.activeAssemblies[assemblyID]
703 })
704 const assemblies = assemblyIDs.map((id) => this.getAssembly(id))
705 this.uppy.addResultData(uploadID, { transloadit: assemblies })
706 return Promise.resolve()
707 }
708
709 // If no Assemblies were created for this upload, we also do not have to wait.
710 // There's also no sockets or anything to close, so just return immediately.
711 if (assemblyIDs.length === 0) {
712 this.uppy.addResultData(uploadID, { transloadit: [] })
713 return Promise.resolve()
714 }
715
716 const incompleteFiles = files.filter(file => !hasProperty(this.completedFiles, file.id))
717 incompleteFiles.forEach((file) => {
718 this.uppy.emit('postprocess-progress', file, {
719 mode: 'indeterminate',
720 message: this.i18n('encoding')
721 })
722 })
723
724 const watcher = this.assemblyWatchers[uploadID]
725 return watcher.promise.then(() => {
726 const assemblies = assemblyIDs.map((id) => this.getAssembly(id))
727
728 // Remove the Assembly ID list for this upload,
729 // it's no longer going to be used anywhere.
730 const state = this.getPluginState()
731 const uploadsAssemblies = { ...state.uploadsAssemblies }
732 delete uploadsAssemblies[uploadID]
733 this.setPluginState({ uploadsAssemblies })
734
735 this.uppy.addResultData(uploadID, {
736 transloadit: assemblies
737 })
738 })
739 }
740
741 _onError (err = null, uploadID) {
742 const state = this.getPluginState()
743 const assemblyIDs = state.uploadsAssemblies[uploadID]
744
745 assemblyIDs.forEach((assemblyID) => {
746 if (this.activeAssemblies[assemblyID]) {
747 this.activeAssemblies[assemblyID].close()
748 }
749 })
750 }
751
752 _onTusError (err) {
753 if (err && /^tus: /.test(err.message)) {
754 const xhr = err.originalRequest ? err.originalRequest.getUnderlyingObject() : null
755 const url = xhr && xhr.responseURL ? xhr.responseURL : null
756 this.client.submitError(err, { url, type: 'TUS_ERROR' }).then((_) => {
757 // if we can't report the error that sucks
758 })
759 }
760 }
761
762 install () {
763 this.uppy.addPreProcessor(this._prepareUpload)
764 this.uppy.addPostProcessor(this._afterUpload)
765
766 // We may need to close socket.io connections on error.
767 this.uppy.on('error', this._onError)
768
769 // Handle cancellation.
770 this.uppy.on('cancel-all', this._onCancelAll)
771
772 // For error reporting.
773 this.uppy.on('upload-error', this._onTusError)
774
775 if (this.opts.importFromUploadURLs) {
776 // No uploader needed when importing; instead we take the upload URL from an existing uploader.
777 this.uppy.on('upload-success', this._onFileUploadURLAvailable)
778 } else {
779 this.uppy.use(Tus, {
780 // Disable tus-js-client fingerprinting, otherwise uploading the same file at different times
781 // will upload to an outdated Assembly, and we won't get socket events for it.
782 //
783 // To resume a Transloadit upload, we need to reconnect to the websocket, and the state that's
784 // required to do that is not saved by tus-js-client's fingerprinting. We need the tus URL,
785 // the Assembly URL, and the WebSocket URL, at least. We also need to know _all_ the files that
786 // were added to the Assembly, so we can properly complete it. All that state is handled by
787 // Golden Retriever. So, Golden Retriever is required to do resumability with the Transloadit plugin,
788 // and we disable Tus's default resume implementation to prevent bad behaviours.
789 storeFingerprintForResuming: false,
790 resume: false,
791 // Disable Companion's retry optimisation; we need to change the endpoint on retry
792 // so it can't just reuse the same tus.Upload instance server-side.
793 useFastRemoteRetry: false,
794 // Only send Assembly metadata to the tus endpoint.
795 metaFields: ['assembly_url', 'filename', 'fieldname'],
796 // Pass the limit option to @uppy/tus
797 limit: this.opts.limit
798 })
799 }
800
801 this.uppy.on('restore:get-data', this._getPersistentData)
802 this.uppy.on('restored', this._onRestored)
803
804 this.setPluginState({
805 // Contains Assembly status objects, indexed by their ID.
806 assemblies: {},
807 // Contains arrays of Assembly IDs, indexed by the upload ID that they belong to.
808 uploadsAssemblies: {},
809 // Contains file data from Transloadit, indexed by their Transloadit-assigned ID.
810 files: {},
811 // Contains result data from Transloadit.
812 results: []
813 })
814
815 // We cannot cancel individual files because Assemblies tend to contain many files.
816 const { capabilities } = this.uppy.getState()
817 this.uppy.setState({
818 capabilities: {
819 ...capabilities,
820 individualCancellation: false
821 }
822 })
823 }
824
825 uninstall () {
826 this.uppy.removePreProcessor(this._prepareUpload)
827 this.uppy.removePostProcessor(this._afterUpload)
828 this.uppy.off('error', this._onError)
829
830 if (this.opts.importFromUploadURLs) {
831 this.uppy.off('upload-success', this._onFileUploadURLAvailable)
832 }
833
834 const { capabilities } = this.uppy.getState()
835 this.uppy.setState({
836 capabilities: {
837 ...capabilities,
838 individualCancellation: true
839 }
840 })
841 }
842
843 getAssembly (id) {
844 const { assemblies } = this.getPluginState()
845 return assemblies[id]
846 }
847
848 getAssemblyFiles (assemblyID) {
849 return this.uppy.getFiles().filter((file) => {
850 return file && file.transloadit && file.transloadit.assembly === assemblyID
851 })
852 }
853}
854
855module.exports.COMPANION = COMPANION
856module.exports.UPPY_SERVER = COMPANION
857module.exports.COMPANION_PATTERN = ALLOWED_COMPANION_PATTERN