index.js 27 KB

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