index.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  1. const Translator = require('../../core/Translator')
  2. const Plugin = require('../../core/Plugin')
  3. const Client = require('./Client')
  4. const StatusSocket = require('./Socket')
  5. function defaultGetAssemblyOptions (file, options) {
  6. return {
  7. params: options.params,
  8. signature: options.signature,
  9. fields: options.fields
  10. }
  11. }
  12. /**
  13. * Upload files to Transloadit using Tus.
  14. */
  15. module.exports = class Transloadit extends Plugin {
  16. constructor (uppy, opts) {
  17. super(uppy, opts)
  18. this.type = 'uploader'
  19. this.id = 'Transloadit'
  20. this.title = 'Transloadit'
  21. const defaultLocale = {
  22. strings: {
  23. creatingAssembly: 'Preparing upload...',
  24. creatingAssemblyFailed: 'Transloadit: Could not create assembly',
  25. encoding: 'Encoding...'
  26. }
  27. }
  28. const defaultOptions = {
  29. waitForEncoding: false,
  30. waitForMetadata: false,
  31. alwaysRunAssembly: false,
  32. importFromUploadURLs: false,
  33. signature: null,
  34. params: null,
  35. fields: {},
  36. getAssemblyOptions: defaultGetAssemblyOptions,
  37. locale: defaultLocale
  38. }
  39. this.opts = Object.assign({}, defaultOptions, opts)
  40. this.locale = Object.assign({}, defaultLocale, this.opts.locale)
  41. this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)
  42. this.translator = new Translator({ locale: this.locale })
  43. this.i18n = this.translator.translate.bind(this.translator)
  44. this.prepareUpload = this.prepareUpload.bind(this)
  45. this.afterUpload = this.afterUpload.bind(this)
  46. this.onFileUploadURLAvailable = this.onFileUploadURLAvailable.bind(this)
  47. this.onRestored = this.onRestored.bind(this)
  48. this.getPersistentData = this.getPersistentData.bind(this)
  49. if (this.opts.params) {
  50. this.validateParams(this.opts.params)
  51. }
  52. this.client = new Client()
  53. this.sockets = {}
  54. }
  55. validateParams (params) {
  56. if (!params) {
  57. throw new Error('Transloadit: The `params` option is required.')
  58. }
  59. if (typeof params === 'string') {
  60. try {
  61. params = JSON.parse(params)
  62. } catch (err) {
  63. // Tell the user that this is not an Uppy bug!
  64. err.message = 'Transloadit: The `params` option is a malformed JSON string: ' +
  65. err.message
  66. throw err
  67. }
  68. }
  69. if (!params.auth || !params.auth.key) {
  70. throw new Error('Transloadit: The `params.auth.key` option is required. ' +
  71. 'You can find your Transloadit API key at https://transloadit.com/accounts/credentials.')
  72. }
  73. }
  74. getAssemblyOptions (fileIDs) {
  75. const options = this.opts
  76. return Promise.all(
  77. fileIDs.map((fileID) => {
  78. const file = this.uppy.getFile(fileID)
  79. const promise = Promise.resolve()
  80. .then(() => options.getAssemblyOptions(file, options))
  81. return promise.then((assemblyOptions) => {
  82. this.validateParams(assemblyOptions.params)
  83. return {
  84. fileIDs: [fileID],
  85. options: assemblyOptions
  86. }
  87. })
  88. })
  89. )
  90. }
  91. dedupeAssemblyOptions (list) {
  92. const dedupeMap = Object.create(null)
  93. list.forEach(({ fileIDs, options }) => {
  94. const id = JSON.stringify(options)
  95. if (dedupeMap[id]) {
  96. dedupeMap[id].fileIDs.push(...fileIDs)
  97. } else {
  98. dedupeMap[id] = {
  99. options,
  100. fileIDs: [...fileIDs]
  101. }
  102. }
  103. })
  104. return Object.keys(dedupeMap).map((id) => dedupeMap[id])
  105. }
  106. createAssembly (fileIDs, uploadID, options) {
  107. const pluginOptions = this.opts
  108. this.uppy.log('[Transloadit] create assembly')
  109. return this.client.createAssembly({
  110. params: options.params,
  111. fields: options.fields,
  112. expectedFiles: fileIDs.length,
  113. signature: options.signature
  114. }).then((assembly) => {
  115. // Store the list of assemblies related to this upload.
  116. const state = this.getPluginState()
  117. const assemblyList = state.uploadsAssemblies[uploadID]
  118. const uploadsAssemblies = Object.assign({}, state.uploadsAssemblies, {
  119. [uploadID]: assemblyList.concat([ assembly.assembly_id ])
  120. })
  121. this.setPluginState({
  122. assemblies: Object.assign(state.assemblies, {
  123. [assembly.assembly_id]: assembly
  124. }),
  125. uploadsAssemblies
  126. })
  127. function attachAssemblyMetadata (file, assembly) {
  128. // Attach meta parameters for the Tus plugin. See:
  129. // https://github.com/tus/tusd/wiki/Uploading-to-Transloadit-using-tus#uploading-using-tus
  130. // TODO Should this `meta` be moved to a `tus.meta` property instead?
  131. const tlMeta = {
  132. assembly_url: assembly.assembly_url,
  133. filename: file.name,
  134. fieldname: 'file'
  135. }
  136. const meta = Object.assign({}, file.meta, tlMeta)
  137. // Add assembly-specific Tus endpoint.
  138. const tus = Object.assign({}, file.tus, {
  139. endpoint: assembly.tus_url,
  140. // Only send assembly metadata to the tus endpoint.
  141. metaFields: Object.keys(tlMeta),
  142. // Make sure tus doesn't resume a previous upload.
  143. uploadUrl: null,
  144. // Disable tus-js-client fingerprinting, otherwise uploading the same file at different times
  145. // will upload to the same assembly.
  146. resume: false
  147. })
  148. const transloadit = {
  149. assembly: assembly.assembly_id
  150. }
  151. const newFile = Object.assign({}, file, { transloadit })
  152. // Only configure the Tus plugin if we are uploading straight to Transloadit (the default).
  153. if (!pluginOptions.importFromUploadURLs) {
  154. Object.assign(newFile, { meta, tus })
  155. }
  156. return newFile
  157. }
  158. const files = Object.assign({}, this.uppy.state.files)
  159. fileIDs.forEach((id) => {
  160. files[id] = attachAssemblyMetadata(files[id], assembly)
  161. })
  162. this.uppy.setState({ files })
  163. this.uppy.emit('transloadit:assembly-created', assembly, fileIDs)
  164. return this.connectSocket(assembly)
  165. .then(() => assembly)
  166. }).then((assembly) => {
  167. this.uppy.log('[Transloadit] Created assembly')
  168. return assembly
  169. }).catch((err) => {
  170. this.uppy.info(this.i18n('creatingAssemblyFailed'), 'error', 0)
  171. // Reject the promise.
  172. throw err
  173. })
  174. }
  175. shouldWait () {
  176. return this.opts.waitForEncoding || this.opts.waitForMetadata
  177. }
  178. /**
  179. * Used when `importFromUploadURLs` is enabled: reserves all files in
  180. * the assembly.
  181. */
  182. reserveFiles (assembly, fileIDs) {
  183. return Promise.all(fileIDs.map((fileID) => {
  184. const file = this.uppy.getFile(fileID)
  185. return this.client.reserveFile(assembly, file)
  186. }))
  187. }
  188. /**
  189. * Used when `importFromUploadURLs` is enabled: adds files to the assembly
  190. * once they have been fully uploaded.
  191. */
  192. onFileUploadURLAvailable (fileID) {
  193. const file = this.uppy.getFile(fileID)
  194. if (!file || !file.transloadit || !file.transloadit.assembly) {
  195. return
  196. }
  197. const state = this.getPluginState()
  198. const assembly = state.assemblies[file.transloadit.assembly]
  199. this.client.addFile(assembly, file).catch((err) => {
  200. this.uppy.log(err)
  201. this.uppy.emit('transloadit:import-error', assembly, file.id, err)
  202. })
  203. }
  204. findFile (uploadedFile) {
  205. const files = this.uppy.state.files
  206. for (const id in files) {
  207. if (!files.hasOwnProperty(id)) {
  208. continue
  209. }
  210. // Completed file upload.
  211. if (files[id].uploadURL === uploadedFile.tus_upload_url) {
  212. return files[id]
  213. }
  214. // In-progress file upload.
  215. if (files[id].tus && files[id].tus.uploadUrl === uploadedFile.tus_upload_url) {
  216. return files[id]
  217. }
  218. if (!uploadedFile.is_tus_file) {
  219. // Fingers-crossed check for non-tus uploads, eg imported from S3.
  220. if (files[id].name === uploadedFile.name && files[id].size === uploadedFile.size) {
  221. return files[id]
  222. }
  223. }
  224. }
  225. }
  226. onFileUploadComplete (assemblyId, uploadedFile) {
  227. const state = this.getPluginState()
  228. const file = this.findFile(uploadedFile)
  229. this.setPluginState({
  230. files: Object.assign({}, state.files, {
  231. [uploadedFile.id]: {
  232. assembly: assemblyId,
  233. id: file.id,
  234. uploadedFile
  235. }
  236. })
  237. })
  238. this.uppy.emit('transloadit:upload', uploadedFile, this.getAssembly(assemblyId))
  239. }
  240. onResult (assemblyId, stepName, result) {
  241. const state = this.getPluginState()
  242. const file = state.files[result.original_id]
  243. // The `file` may not exist if an import robot was used instead of a file upload.
  244. result.localId = file ? file.id : null
  245. const entry = {
  246. result,
  247. stepName,
  248. id: result.id,
  249. assembly: assemblyId
  250. }
  251. this.setPluginState({
  252. results: [...state.results, entry]
  253. })
  254. this.uppy.emit('transloadit:result', stepName, result, this.getAssembly(assemblyId))
  255. }
  256. onAssemblyFinished (url) {
  257. this.client.getAssemblyStatus(url).then((assembly) => {
  258. const state = this.getPluginState()
  259. this.setPluginState({
  260. assemblies: Object.assign({}, state.assemblies, {
  261. [assembly.assembly_id]: assembly
  262. })
  263. })
  264. this.uppy.emit('transloadit:complete', assembly)
  265. })
  266. }
  267. getPersistentData (setData) {
  268. const state = this.getPluginState()
  269. const assemblies = state.assemblies
  270. const uploadsAssemblies = state.uploadsAssemblies
  271. const uploads = Object.keys(state.files)
  272. const results = state.results.map((result) => result.id)
  273. setData({
  274. [this.id]: {
  275. assemblies,
  276. uploadsAssemblies,
  277. uploads,
  278. results
  279. }
  280. })
  281. }
  282. /**
  283. * Emit the necessary events that must have occured to get from the `prevState`,
  284. * to the current state.
  285. * For completed uploads, `transloadit:upload` is emitted.
  286. * For new results, `transloadit:result` is emitted.
  287. * For completed or errored assemblies, `transloadit:complete` or `transloadit:assembly-error` is emitted.
  288. */
  289. emitEventsDiff (prevState) {
  290. const opts = this.opts
  291. const state = this.getPluginState()
  292. const emitMissedEvents = () => {
  293. // Emit events for completed uploads and completed results
  294. // that we've missed while we were away.
  295. const newUploads = Object.keys(state.files).filter((fileID) => {
  296. return !prevState.files.hasOwnProperty(fileID)
  297. }).map((fileID) => state.files[fileID])
  298. const newResults = state.results.filter((result) => {
  299. return !prevState.results.some((prev) => prev.id === result.id)
  300. })
  301. this.uppy.log('[Transloadit] New fully uploaded files since restore:')
  302. this.uppy.log(newUploads)
  303. newUploads.forEach(({ assembly, uploadedFile }) => {
  304. this.uppy.log(`[Transloadit] emitting transloadit:upload ${uploadedFile.id}`)
  305. this.uppy.emit('transloadit:upload', uploadedFile, this.getAssembly(assembly))
  306. })
  307. this.uppy.log('[Transloadit] New results since restore:')
  308. this.uppy.log(newResults)
  309. newResults.forEach(({ assembly, stepName, result, id }) => {
  310. this.uppy.log(`[Transloadit] emitting transloadit:result ${stepName}, ${id}`)
  311. this.uppy.emit('transloadit:result', stepName, result, this.getAssembly(assembly))
  312. })
  313. const newAssemblies = state.assemblies
  314. const previousAssemblies = prevState.assemblies
  315. this.uppy.log('[Transloadit] Current assembly status after restore')
  316. this.uppy.log(newAssemblies)
  317. this.uppy.log('[Transloadit] Assembly status before restore')
  318. this.uppy.log(previousAssemblies)
  319. Object.keys(newAssemblies).forEach((assemblyId) => {
  320. const oldAssembly = previousAssemblies[assemblyId]
  321. diffAssemblyStatus(oldAssembly, newAssemblies[assemblyId])
  322. })
  323. }
  324. // Emit events for assemblies that have completed or errored while we were away.
  325. const diffAssemblyStatus = (prev, next) => {
  326. this.uppy.log('[Transloadit] Diff assemblies')
  327. this.uppy.log(prev)
  328. this.uppy.log(next)
  329. if (opts.waitForEncoding && next.ok === 'ASSEMBLY_COMPLETED' && prev.ok !== 'ASSEMBLY_COMPLETED') {
  330. this.uppy.log(`[Transloadit] Emitting transloadit:complete for ${next.assembly_id}`)
  331. this.uppy.log(next)
  332. this.uppy.emit('transloadit:complete', next)
  333. } else if (opts.waitForMetadata && next.upload_meta_data_extracted && !prev.upload_meta_data_extracted) {
  334. this.uppy.log(`[Transloadit] Emitting transloadit:complete after metadata extraction for ${next.assembly_id}`)
  335. this.uppy.log(next)
  336. this.uppy.emit('transloadit:complete', next)
  337. }
  338. if (next.error && !prev.error) {
  339. this.uppy.log(`[Transloadit] !!! Emitting transloadit:assembly-error for ${next.assembly_id}`)
  340. this.uppy.log(next)
  341. this.uppy.emit('transloadit:assembly-error', next, new Error(next.message))
  342. }
  343. }
  344. emitMissedEvents()
  345. }
  346. onRestored (pluginData) {
  347. const savedState = pluginData && pluginData[this.id] ? pluginData[this.id] : {}
  348. const knownUploads = savedState.files || []
  349. const knownResults = savedState.results || []
  350. const previousAssemblies = savedState.assemblies || {}
  351. const uploadsAssemblies = savedState.uploadsAssemblies || {}
  352. if (Object.keys(uploadsAssemblies).length === 0) {
  353. // Nothing to restore.
  354. return
  355. }
  356. // Fetch up-to-date assembly statuses.
  357. const loadAssemblies = () => {
  358. const assemblyIDs = []
  359. Object.keys(uploadsAssemblies).forEach((uploadID) => {
  360. assemblyIDs.push(...uploadsAssemblies[uploadID])
  361. })
  362. return Promise.all(
  363. assemblyIDs.map((assemblyID) => {
  364. const url = `https://api2.transloadit.com/assemblies/${assemblyID}`
  365. return this.client.getAssemblyStatus(url)
  366. })
  367. )
  368. }
  369. const reconnectSockets = (assemblies) => {
  370. return Promise.all(assemblies.map((assembly) => {
  371. // No need to connect to the socket if the assembly has completed by now.
  372. if (assembly.ok === 'ASSEMBLY_COMPLETE') {
  373. return null
  374. }
  375. return this.connectSocket(assembly)
  376. }))
  377. }
  378. // Convert loaded assembly statuses to a Transloadit plugin state object.
  379. const restoreState = (assemblies) => {
  380. const assembliesById = {}
  381. const files = {}
  382. const results = []
  383. assemblies.forEach((assembly) => {
  384. assembliesById[assembly.assembly_id] = assembly
  385. assembly.uploads.forEach((uploadedFile) => {
  386. const file = this.findFile(uploadedFile)
  387. files[uploadedFile.id] = {
  388. id: file.id,
  389. assembly: assembly.assembly_id,
  390. uploadedFile
  391. }
  392. })
  393. const state = this.getPluginState()
  394. Object.keys(assembly.results).forEach((stepName) => {
  395. assembly.results[stepName].forEach((result) => {
  396. const file = state.files[result.original_id]
  397. result.localId = file ? file.id : null
  398. results.push({
  399. id: result.id,
  400. result,
  401. stepName,
  402. assembly: assembly.assembly_id
  403. })
  404. })
  405. })
  406. })
  407. this.setPluginState({
  408. assemblies: assembliesById,
  409. files: files,
  410. results: results,
  411. uploadsAssemblies: uploadsAssemblies
  412. })
  413. }
  414. // Restore all assembly state.
  415. this.restored = Promise.resolve()
  416. .then(loadAssemblies)
  417. .then((assemblies) => {
  418. restoreState(assemblies)
  419. return reconnectSockets(assemblies)
  420. })
  421. .then(() => {
  422. // Return a callback that will be called by `afterUpload`
  423. // once it has attached event listeners etc.
  424. const newState = this.getPluginState()
  425. const previousFiles = {}
  426. knownUploads.forEach((id) => {
  427. previousFiles[id] = newState.files[id]
  428. })
  429. return () => this.emitEventsDiff({
  430. assemblies: previousAssemblies,
  431. files: previousFiles,
  432. results: newState.results.filter(({ id }) => knownResults.indexOf(id) !== -1),
  433. uploadsAssemblies
  434. })
  435. })
  436. this.restored.then(() => {
  437. this.restored = null
  438. })
  439. }
  440. connectSocket (assembly) {
  441. const socket = new StatusSocket(
  442. assembly.websocket_url,
  443. assembly
  444. )
  445. this.sockets[assembly.assembly_id] = socket
  446. socket.on('upload', this.onFileUploadComplete.bind(this, assembly.assembly_id))
  447. socket.on('error', (error) => {
  448. this.uppy.emit('transloadit:assembly-error', assembly, error)
  449. })
  450. socket.on('executing', () => {
  451. this.uppy.emit('transloadit:assembly-executing', assembly)
  452. })
  453. if (this.opts.waitForEncoding) {
  454. socket.on('result', this.onResult.bind(this, assembly.assembly_id))
  455. }
  456. if (this.opts.waitForEncoding) {
  457. socket.on('finished', () => {
  458. this.onAssemblyFinished(assembly.assembly_ssl_url)
  459. })
  460. } else if (this.opts.waitForMetadata) {
  461. socket.on('metadata', () => {
  462. this.onAssemblyFinished(assembly.assembly_ssl_url)
  463. })
  464. }
  465. return new Promise((resolve, reject) => {
  466. socket.on('connect', resolve)
  467. socket.on('error', reject)
  468. }).then(() => {
  469. this.uppy.log('[Transloadit] Socket is ready')
  470. })
  471. }
  472. prepareUpload (fileIDs, uploadID) {
  473. // Only use files without errors
  474. fileIDs = fileIDs.filter((file) => !file.error)
  475. fileIDs.forEach((fileID) => {
  476. this.uppy.emit('preprocess-progress', fileID, {
  477. mode: 'indeterminate',
  478. message: this.i18n('creatingAssembly')
  479. })
  480. })
  481. const createAssembly = ({ fileIDs, options }) => {
  482. return this.createAssembly(fileIDs, uploadID, options).then((assembly) => {
  483. if (this.opts.importFromUploadURLs) {
  484. return this.reserveFiles(assembly, fileIDs)
  485. }
  486. }).then(() => {
  487. fileIDs.forEach((fileID) => {
  488. this.uppy.emit('preprocess-complete', fileID)
  489. })
  490. })
  491. }
  492. const state = this.getPluginState()
  493. const uploadsAssemblies = Object.assign({},
  494. state.uploadsAssemblies,
  495. { [uploadID]: [] })
  496. this.setPluginState({ uploadsAssemblies })
  497. let optionsPromise
  498. if (fileIDs.length > 0) {
  499. optionsPromise = this.getAssemblyOptions(fileIDs)
  500. .then((allOptions) => this.dedupeAssemblyOptions(allOptions))
  501. } else if (this.opts.alwaysRunAssembly) {
  502. optionsPromise = Promise.resolve(
  503. this.opts.getAssemblyOptions(null, this.opts)
  504. ).then((options) => {
  505. this.validateParams(options.params)
  506. return [
  507. { fileIDs, options }
  508. ]
  509. })
  510. } else {
  511. // If there are no files and we do not `alwaysRunAssembly`,
  512. // don't do anything.
  513. return Promise.resolve()
  514. }
  515. return optionsPromise.then((assemblies) => Promise.all(
  516. assemblies.map(createAssembly)
  517. ))
  518. }
  519. afterUpload (fileIDs, uploadID) {
  520. // Only use files without errors
  521. fileIDs = fileIDs.filter((file) => !file.error)
  522. const state = this.getPluginState()
  523. // If we're still restoring state, wait for that to be done.
  524. if (this.restored) {
  525. return this.restored.then((emitMissedEvents) => {
  526. const promise = this.afterUpload(fileIDs, uploadID)
  527. emitMissedEvents()
  528. return promise
  529. })
  530. }
  531. const assemblyIDs = state.uploadsAssemblies[uploadID]
  532. // If we don't have to wait for encoding metadata or results, we can close
  533. // the socket immediately and finish the upload.
  534. if (!this.shouldWait()) {
  535. assemblyIDs.forEach((assemblyID) => {
  536. const socket = this.sockets[assemblyID]
  537. socket.close()
  538. })
  539. const assemblies = assemblyIDs.map((id) => this.getAssembly(id))
  540. this.uppy.addResultData(uploadID, { transloadit: assemblies })
  541. return Promise.resolve()
  542. }
  543. // If no assemblies were created for this upload, we also do not have to wait.
  544. // There's also no sockets or anything to close, so just return immediately.
  545. if (assemblyIDs.length === 0) {
  546. this.uppy.addResultData(uploadID, { transloadit: [] })
  547. return Promise.resolve()
  548. }
  549. let finishedAssemblies = 0
  550. return new Promise((resolve, reject) => {
  551. fileIDs.forEach((fileID) => {
  552. this.uppy.emit('postprocess-progress', fileID, {
  553. mode: 'indeterminate',
  554. message: this.i18n('encoding')
  555. })
  556. })
  557. const onAssemblyFinished = (assembly) => {
  558. // An assembly for a different upload just finished. We can ignore it.
  559. if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
  560. this.uppy.log(`[Transloadit] afterUpload(): Ignoring finished assembly ${assembly.assembly_id}`)
  561. return
  562. }
  563. this.uppy.log(`[Transloadit] afterUpload(): Got assembly finish ${assembly.assembly_id}`)
  564. // TODO set the `file.uploadURL` to a result?
  565. // We will probably need an option here so the plugin user can tell us
  566. // which result to pick…?
  567. const files = this.getAssemblyFiles(assembly.assembly_id)
  568. files.forEach((file) => {
  569. this.uppy.emit('postprocess-complete', file.id)
  570. })
  571. checkAllComplete()
  572. }
  573. const onAssemblyError = (assembly, error) => {
  574. // An assembly for a different upload just errored. We can ignore it.
  575. if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
  576. this.uppy.log(`[Transloadit] afterUpload(): Ignoring errored assembly ${assembly.assembly_id}`)
  577. return
  578. }
  579. this.uppy.log(`[Transloadit] afterUpload(): Got assembly error ${assembly.assembly_id}`)
  580. this.uppy.log(error)
  581. // Clear postprocessing state for all our files.
  582. const files = this.getAssemblyFiles(assembly.assembly_id)
  583. files.forEach((file) => {
  584. // TODO Maybe make a postprocess-error event here?
  585. this.uppy.emit('upload-error', file.id, error)
  586. this.uppy.emit('postprocess-complete', file.id)
  587. })
  588. checkAllComplete()
  589. }
  590. const onImportError = (assembly, fileID, error) => {
  591. if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
  592. return
  593. }
  594. // Not sure if we should be doing something when it's just one file failing.
  595. // ATM, the only options are 1) ignoring or 2) failing the entire upload.
  596. // I think failing the upload is better than silently ignoring.
  597. // In the future we should maybe have a way to resolve uploads with some failures,
  598. // like returning an object with `{ successful, failed }` uploads.
  599. onAssemblyError(assembly, error)
  600. }
  601. const checkAllComplete = () => {
  602. finishedAssemblies += 1
  603. if (finishedAssemblies === assemblyIDs.length) {
  604. // We're done, these listeners can be removed
  605. removeListeners()
  606. const assemblies = assemblyIDs.map((id) => this.getAssembly(id))
  607. this.uppy.addResultData(uploadID, { transloadit: assemblies })
  608. resolve()
  609. }
  610. }
  611. const removeListeners = () => {
  612. this.uppy.off('transloadit:complete', onAssemblyFinished)
  613. this.uppy.off('transloadit:assembly-error', onAssemblyError)
  614. this.uppy.off('transloadit:import-error', onImportError)
  615. }
  616. this.uppy.on('transloadit:complete', onAssemblyFinished)
  617. this.uppy.on('transloadit:assembly-error', onAssemblyError)
  618. this.uppy.on('transloadit:import-error', onImportError)
  619. }).then((result) => {
  620. // Clean up uploadID → assemblyIDs, they're no longer going to be used anywhere.
  621. const state = this.getPluginState()
  622. const uploadsAssemblies = Object.assign({}, state.uploadsAssemblies)
  623. delete uploadsAssemblies[uploadID]
  624. this.setPluginState({ uploadsAssemblies })
  625. return result
  626. })
  627. }
  628. install () {
  629. this.uppy.addPreProcessor(this.prepareUpload)
  630. this.uppy.addPostProcessor(this.afterUpload)
  631. if (this.opts.importFromUploadURLs) {
  632. this.uppy.on('upload-success', this.onFileUploadURLAvailable)
  633. }
  634. this.uppy.on('restore:get-data', this.getPersistentData)
  635. this.uppy.on('restored', this.onRestored)
  636. this.setPluginState({
  637. // Contains assembly status objects, indexed by their ID.
  638. assemblies: {},
  639. // Contains arrays of assembly IDs, indexed by the upload ID that they belong to.
  640. uploadsAssemblies: {},
  641. // Contains file data from Transloadit, indexed by their Transloadit-assigned ID.
  642. files: {},
  643. // Contains result data from Transloadit.
  644. results: []
  645. })
  646. }
  647. uninstall () {
  648. this.uppy.removePreProcessor(this.prepareUpload)
  649. this.uppy.removePostProcessor(this.afterUpload)
  650. if (this.opts.importFromUploadURLs) {
  651. this.uppy.off('upload-success', this.onFileUploadURLAvailable)
  652. }
  653. }
  654. getAssembly (id) {
  655. const state = this.getPluginState()
  656. return state.assemblies[id]
  657. }
  658. getAssemblyFiles (assemblyID) {
  659. const fileIDs = Object.keys(this.uppy.state.files)
  660. return fileIDs.map((fileID) => {
  661. return this.uppy.getFile(fileID)
  662. }).filter((file) => {
  663. return file && file.transloadit && file.transloadit.assembly === assemblyID
  664. })
  665. }
  666. }